LWJGL 3: Simple OpenGL 4.3 Compute Shader Ray Tracing

I wanted to play around a little bit with Java 13 text blocks and thought it would be a good idea to finally have GLSL shader source code literals embedded into Java source code to get a really single-source-file demo.
So here is a simple ray tracing example (static scene, static camera) in under 180 lines of code (Java + GLSL code) (requires JDK13+ with --enable-preview):


import org.lwjgl.glfw.*;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL.createCapabilities;
import static org.lwjgl.opengl.GL43C.*;
import static org.lwjgl.opengl.GLUtil.setupDebugMessageCallback;
import static org.lwjgl.system.MemoryUtil.*;

public class Main {
  private static final int WINDOW_WIDTH = 800;
  private static final int WINDOW_HEIGHT = 800;
  private static final String QUAD_PROGRAM_VS_SOURCE = """
#version 430 core
out vec2 texcoord;
void main(void) {
  vec2 vertex = vec2((gl_VertexID & 1) << 2, (gl_VertexID & 2) << 1) - vec2(1.0);
  gl_Position = vec4(vertex, 0.0, 1.0);
  texcoord = vertex * 0.5 + vec2(0.5, 0.5);
}
""";
  private static final String QUAD_PROGRAM_FS_SOURCE = """
#version 430 core
uniform sampler2D tex;
in vec2 texcoord;
layout(location = 0) out vec4 color;
void main(void) {
  color = texture2D(tex, texcoord);
}
""";
  private static final String COMPUTE_SHADER_SOURCE = """
#version 430 core
layout(binding = 0, rgba8) uniform image2D framebufferImage;
layout(location = 0) uniform vec3 cam[5] = {
  vec3(0.0, 2.0, 5.0), // <- position
  vec3(-1.0, -1.0, -1.0), vec3(-1.0, 1.0, -1.0), // <- left corner directions
  vec3(1.0, -1.0, -1.0), vec3(1.0, 1.0, -1.0) // <- right corner directions
};
struct box {
  vec3 min, max;
};
#define NUM_BOXES 9
const box boxes[NUM_BOXES] = {
  {vec3(-5.0, -0.1, -5.0), vec3(5.0, 0.0, 5.0)},  // <- bottom
  {vec3(-5.1, 0.0, -5.0), vec3(-5.0, 5.0, 5.0)},  // <- left
  {vec3(5.0, 0.0, -5.0), vec3(5.1, 5.0, 5.0)},    // <- right
  {vec3(-5.0, 0.0, -5.1), vec3(5.0, 5.0, -5.0)},  // <- back
  {vec3(-1.0, 1.0, -1.0), vec3(1.0, 1.1, 1.0)},   // <- table top
  {vec3(-1.0, 0.0, -1.0), vec3(-0.8, 1.0, -0.8)}, // <- table foot
  {vec3(-1.0, 0.0,  0.8), vec3(-0.8, 1.0, 1.0)},  // <- table foot
  {vec3(0.8, 0.0, -1.0), vec3(1.0, 1.0, -0.8)},   // <- table foot
  {vec3(0.8, 0.0,  0.8), vec3(1.0, 1.0, 1.0)}     // <- table foot
};
struct hitinfo {
  float near;
  int i;
};
vec2 intersectBox(vec3 origin, vec3 invdir, box b) {
  vec3 tMin = (b.min - origin) * invdir;
  vec3 tMax = (b.max - origin) * invdir;
  vec3 t1 = min(tMin, tMax);
  vec3 t2 = max(tMin, tMax);
  float tNear = max(max(t1.x, t1.y), t1.z);
  float tFar = min(min(t2.x, t2.y), t2.z);
  return vec2(tNear, tFar);
}
bool intersectBoxes(vec3 origin, vec3 invdir, out hitinfo info) {
  float smallest = 1.0/0.0;
  bool found = false;
  for (int i = 0; i < NUM_BOXES; i++) {
    vec2 lambda = intersectBox(origin, invdir, boxes[i]);
    if (lambda.y >= 0.0 && lambda.x < lambda.y && lambda.x < smallest) {
      info.near = lambda.x;
      info.i = i;
      smallest = lambda.x;
      found = true;
    }
  }
  return found;
}
vec3 trace(vec3 origin, vec3 dir) {
  hitinfo hinfo;
  if (!intersectBoxes(origin, 1.0/dir, hinfo))
    return vec3(0.0); // <- nothing hit, return black
  box b = boxes[hinfo.i];
  return vec3(float(hinfo.i+1) / NUM_BOXES);
}
layout(local_size_x = 8, local_size_y = 8) in;
void main(void) {
  ivec2 px = ivec2(gl_GlobalInvocationID.xy);
  ivec2 size = imageSize(framebufferImage);
  if (any(greaterThanEqual(px, size)))
    return;
  vec2 p = (vec2(px) + vec2(0.5)) / vec2(size);
  vec3 dir = mix(mix(cam[1], cam[2], p.y), mix(cam[3], cam[4], p.y), p.x);
  imageStore(framebufferImage, px, vec4(trace(cam[0], normalize(dir)), 1.0));
}
""";

  public static void main(String[] args) {
    // Initialize GLFW and create window
    if (!glfwInit())
      throw new IllegalStateException("Unable to initialize GLFW");
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
    glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
    glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE);
    long window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Ray Tracing Tutorial 1", NULL, NULL);
    if (window == NULL)
      throw new AssertionError("Failed to create the GLFW window");
    glfwSetKeyCallback(window, new GLFWKeyCallback() {
      public void invoke(long window, int key, int scancode, int action, int mods) {
        if (action == GLFW_RELEASE && key == GLFW_KEY_ESCAPE)
          glfwSetWindowShouldClose(window, true);
      }
    });

    // Make context current and install debug message callback
    glfwMakeContextCurrent(window);
    createCapabilities();
    setupDebugMessageCallback();

    // Create VAO
    glBindVertexArray(glGenVertexArrays());

    // Create framebuffer texture to render into
    int framebuffer = glGenTextures();
    glBindTexture(GL_TEXTURE_2D, framebuffer);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, WINDOW_WIDTH, WINDOW_HEIGHT);
    glBindImageTexture(0, framebuffer, 0, false, 0, GL_WRITE_ONLY, GL_RGBA8);

    // Create program to render framebuffer texture as fullscreen quad
    int quadProgram = glCreateProgram();
    int quadProgramVs = glCreateShader(GL_VERTEX_SHADER);
    int quadProgramFs = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(quadProgramVs, QUAD_PROGRAM_VS_SOURCE);
    glShaderSource(quadProgramFs, QUAD_PROGRAM_FS_SOURCE);
    glCompileShader(quadProgramVs);
    glCompileShader(quadProgramFs);
    glAttachShader(quadProgram, quadProgramVs);
    glAttachShader(quadProgram, quadProgramFs);
    glLinkProgram(quadProgram);

    // Create ray tracing compute shader
    int computeProgram = glCreateProgram();
    int computeProgramShader = glCreateShader(GL_COMPUTE_SHADER);
    glShaderSource(computeProgramShader, COMPUTE_SHADER_SOURCE);
    glCompileShader(computeProgramShader);
    glAttachShader(computeProgram, computeProgramShader);
    glLinkProgram(computeProgram);
    // Determine number of work groups to dispatch
    int numGroupsX = (int) Math.ceil((double)WINDOW_WIDTH / 8);
    int numGroupsY = (int) Math.ceil((double)WINDOW_HEIGHT / 8);

    // Make window visible and loop until window should be closed
    glfwShowWindow(window);
    while (!glfwWindowShouldClose(window)) {
      glfwPollEvents();

      // Trace the scene
      glUseProgram(computeProgram);
      glDispatchCompute(numGroupsX, numGroupsY, 1);
      glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);

      // Display framebuffer texture
      glUseProgram(quadProgram);
      glDrawArrays(GL_TRIANGLES, 0, 3);
      glfwSwapBuffers(window);
    }
  }
}

Though I like the fenced strings, I still miss the stripWithIndent method. The indent strings are removed by default in Java 13 if text blocks are used.

Anyways, I think for stuff like shader source, it is better to keep them in separate files and read them, so I can edit the shaders with an editor that supports syntax highlighting.

Even better: use “language injection” in IntelliJ IDEA 8)

Nice but how does it know what language it is? Does it make a guess?

Cas :slight_smile:

I think it is a right click and select the language to be highlighted in thing. That is what it is in CLion so I assume it is the same. It is not persisted across editor restarts.

What the world needs is what Eclipse has for its autoformatter stuff…

//@formatter:on/off

We could have
//@language:sql
//@language:java11
//@language:glsl1.6
which would automatically highlight multiline strings following in the specified syntax. Wouldn’t that be nice.

Cas :slight_smile:

See here for options that persist. I’ve been using text blocks and the annotation on method parameters (e.g. @Language(“TSQL”) String sql) for a few months now, very happy with how seamless it is (code formatting, auto-complete, navigation, refactorings all work).

Cool on the persisting part, I had only played with it a bit and not used it a huge amount. The tooling they produce is really rather good.