Java tutorial
/* * Copyright LWJGL. All rights reserved. * License terms: http://lwjgl.org/license.php */ package org.lwjgl.demo.opengl.raytracing; import org.lwjgl.BufferUtils; import org.lwjgl.demo.opengl.util.DynamicByteBuffer; import org.lwjgl.demo.opengl.util.KDTree; import org.lwjgl.demo.opengl.util.WavefrontMeshLoader; import org.lwjgl.demo.opengl.util.KDTree.Box; import org.lwjgl.demo.opengl.util.KDTree.Node; import org.lwjgl.demo.opengl.util.KDTree.Triangle; import org.lwjgl.demo.opengl.util.Member; import org.lwjgl.demo.opengl.util.Std430Writer; import org.lwjgl.demo.opengl.util.WavefrontMeshLoader.Mesh; import org.lwjgl.glfw.*; import org.lwjgl.opengl.GL; import org.lwjgl.opengl.GLCapabilities; import org.lwjgl.opengl.GLUtil; import org.lwjgl.opengl.NVDrawTexture; import org.lwjgl.system.Callback; import org.joml.Matrix4f; import org.joml.Vector3f; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import static java.lang.Math.*; import static org.lwjgl.demo.opengl.util.DemoUtils.*; import static org.lwjgl.glfw.GLFW.*; import static org.lwjgl.opengl.GL11.*; import static org.lwjgl.opengl.GL15.*; import static org.lwjgl.opengl.GL20.*; import static org.lwjgl.opengl.GL30.*; import static org.lwjgl.opengl.GL33.*; import static org.lwjgl.opengl.GL42.*; import static org.lwjgl.opengl.GL43.*; import static org.lwjgl.system.MathUtil.*; import static org.lwjgl.system.MemoryUtil.*; /** * Like {@link HybridDemoSsboTriangles} but uses the <a href= * "https://graphics.cg.uni-saarland.de/fileadmin/cguds/papers/2007/popov_07_GPURT/Popov_et_al._-_Stackless_KD-Tree_Traversal_for_High_Performance_GPU_Ray_Tracing.pdf" * >Stackless KD-Tree Traversal</a> algorithm for binary space partitioning to avoid testing all triangles of the scene * for each ray. * <p> * Normally, traversing a kd-tree requires recursion (or a stack), but those are not available in GLSL. So * "Stackless kd-tree traversal" works by connecting adjacent nodes of the tree. The connections/links are called * "ropes" in that paper. * <p> * The kd-tree as well as the rope-building is implemented in {@link KDTree}. * * @author Kai Burjack */ public class DemoSsboTrianglesStacklessKdTree { long window; int width = 1024; int height = 768; boolean resetFramebuffer = true; int raytraceTexture; int vao; int computeProgram; int quadProgram; int nodesSsbo; int trianglesSsbo; int sampler; int eyeUniform; int ray00Uniform; int ray10Uniform; int ray01Uniform; int ray11Uniform; int debugUniform; int sceneMinUniform; int sceneMaxUniform; int nodesSsboBinding; int trianglesSsboBinding; int framebufferImageBinding; int workGroupSizeX; int workGroupSizeY; Mesh mesh; float mouseDownX; float mouseX; boolean mouseDown; boolean debug; float currRotationAboutY = 0.0f; float rotationAboutY = (float) Math.toRadians(-45); float cameraRadius = 4.0f; float cameraHeight = 2.0f; Matrix4f viewMatrix = new Matrix4f(); Matrix4f projMatrix = new Matrix4f(); Matrix4f invViewProjMatrix = new Matrix4f(); Vector3f tmpVector = new Vector3f(); Vector3f cameraPosition = new Vector3f(0.0f, 0.0f, 0.0f); Vector3f cameraLookAt = new Vector3f(-0.2f, 0.25f, -0.2f); Vector3f cameraUp = new Vector3f(0.0f, 1.0f, 0.0f); Box sceneBounds; GLFWErrorCallback errCallback; GLFWKeyCallback keyCallback; GLFWFramebufferSizeCallback fbCallback; GLFWCursorPosCallback cpCallback; GLFWMouseButtonCallback mbCallback; GLCapabilities caps; Callback debugProc; void init() throws IOException { glfwSetErrorCallback(errCallback = new GLFWErrorCallback() { GLFWErrorCallback delegate = GLFWErrorCallback.createPrint(System.err); @Override public void invoke(int error, long description) { if (error == GLFW_VERSION_UNAVAILABLE) System.err.println("This demo requires OpenGL 4.3 or higher."); delegate.invoke(error, description); } @Override public void free() { delegate.free(); } }); if (!glfwInit()) throw new IllegalStateException("Unable to initialize GLFW"); glfwDefaultWindowHints(); 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_TRUE); window = glfwCreateWindow(width, height, "Raytracing Demo (triangle mesh, stackless kd-tree)", NULL, NULL); if (window == NULL) { throw new AssertionError("Failed to create the GLFW window"); } System.out.println("Hold down any mouse button and drag to rotate."); System.out.println("Press 'D' to toggle debug view."); glfwSetKeyCallback(window, keyCallback = new GLFWKeyCallback() { @Override public void invoke(long window, int key, int scancode, int action, int mods) { if (action == GLFW_PRESS) return; if (key == GLFW_KEY_ESCAPE) { glfwSetWindowShouldClose(window, true); } else if (key == GLFW_KEY_D) { debug = !debug; } } }); glfwSetFramebufferSizeCallback(window, fbCallback = new GLFWFramebufferSizeCallback() { @Override public void invoke(long window, int width, int height) { if (width > 0 && height > 0 && (DemoSsboTrianglesStacklessKdTree.this.width != width || DemoSsboTrianglesStacklessKdTree.this.height != height)) { DemoSsboTrianglesStacklessKdTree.this.width = width; DemoSsboTrianglesStacklessKdTree.this.height = height; DemoSsboTrianglesStacklessKdTree.this.resetFramebuffer = true; } } }); glfwSetCursorPosCallback(window, cpCallback = new GLFWCursorPosCallback() { @Override public void invoke(long window, double x, double y) { DemoSsboTrianglesStacklessKdTree.this.mouseX = (float) x; } }); glfwSetMouseButtonCallback(window, mbCallback = new GLFWMouseButtonCallback() { @Override public void invoke(long window, int button, int action, int mods) { if (action == GLFW_PRESS) { DemoSsboTrianglesStacklessKdTree.this.mouseDownX = DemoSsboTrianglesStacklessKdTree.this.mouseX; DemoSsboTrianglesStacklessKdTree.this.mouseDown = true; } else if (action == GLFW_RELEASE) { DemoSsboTrianglesStacklessKdTree.this.mouseDown = false; DemoSsboTrianglesStacklessKdTree.this.rotationAboutY = DemoSsboTrianglesStacklessKdTree.this.currRotationAboutY; } } }); GLFWVidMode vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor()); glfwSetWindowPos(window, (vidmode.width() - width) / 2, (vidmode.height() - height) / 2); glfwMakeContextCurrent(window); glfwSwapInterval(0); glfwShowWindow(window); IntBuffer framebufferSize = BufferUtils.createIntBuffer(2); nglfwGetFramebufferSize(window, memAddress(framebufferSize), memAddress(framebufferSize) + 4); width = framebufferSize.get(0); height = framebufferSize.get(1); caps = GL.createCapabilities(); debugProc = GLUtil.setupDebugMessageCallback(); /* Load OBJ model */ WavefrontMeshLoader loader = new WavefrontMeshLoader(); mesh = loader.loadMesh("org/lwjgl/demo/opengl/models/lwjgl3.obj.zip"); /* Create all needed GL resources */ createRaytracingTexture(); createSampler(); createSceneSSBO(); createComputeProgram(); initComputeProgram(); if (!caps.GL_NV_draw_texture) { createFullScreenVao(); createQuadProgram(); } } /** * Java-pendant of the GLSL struct 'node' in the compute shader 'ssboTriangleStacklessKdTree.glsl'. */ public static class GPUNode { public Vector3f min; public Vector3f max; public int dim; public float plane; public @Member(length = 6) int[] ropes; public int left; public int right; public int firstTri; public int numTris; } static void kdTreeToBuffers(KDTree tree, DynamicByteBuffer nodesBuffer, DynamicByteBuffer trianglesBuffer) { Map<Node, Integer> indexes = new LinkedHashMap<Node, Integer>(); // Allocate indexes for each of the nodes allocate(tree.mRootNode, indexes); int triangleIndex = 0; List<GPUNode> gpuNodes = new ArrayList<GPUNode>(); // Iterate over each node in insertion order and write to the buffers for (Map.Entry<Node, Integer> e : indexes.entrySet()) { Node n = e.getKey(); GPUNode gn = new GPUNode(); gn.min = n.boundingBox.min; gn.max = n.boundingBox.max; gn.dim = n.splitAxis.dim; gn.plane = n.splitPlane; gn.ropes = new int[6]; /* Write ropes */ for (int i = 0; i < 6; i++) { Node r = n.ropes[i]; if (r != null) { gn.ropes[i] = indexes.get(r).intValue(); } else { gn.ropes[i] = -1; // no neighbor } } if (n.isLeafNode()) { gn.left = -1; // no left child gn.right = -1; // no right child gn.firstTri = triangleIndex; gn.numTris = n.triangles.size(); triangleIndex += n.triangles.size(); /* Write triangles to buffer */ for (int i = 0; i < n.triangles.size(); i++) { Triangle t = n.triangles.get(i); trianglesBuffer.putFloat(t.v0.x).putFloat(t.v0.y).putFloat(t.v0.z).putFloat(1.0f); trianglesBuffer.putFloat(t.v1.x).putFloat(t.v1.y).putFloat(t.v1.z).putFloat(1.0f); trianglesBuffer.putFloat(t.v2.x).putFloat(t.v2.y).putFloat(t.v2.z).putFloat(1.0f); } } else { gn.left = indexes.get(n.left).intValue(); gn.right = indexes.get(n.right).intValue(); gn.firstTri = 0; // no triangles gn.numTris = 0; // no triangles } gpuNodes.add(gn); } // Write GPUNode list to ByteBuffer in std430 layout Std430Writer.write(gpuNodes, GPUNode.class, nodesBuffer); } static void allocate(Node node, Map<Node, Integer> indexes) { Queue<Node> nodes = new LinkedList<Node>(); nodes.add(node); while (!nodes.isEmpty()) { Node n = nodes.poll(); if (n == null) continue; int index = indexes.size(); indexes.put(n, Integer.valueOf(index)); nodes.add(n.left); nodes.add(n.right); } } /** * Build the kd-tree of the scene and create two SSBOs: * <ul> * <li>one for the nodes of the kd-tree * <li>and another one to hold all the triangles stored in the leaf nodes of the kd-tree * </ul> */ void createSceneSSBO() { /* Build Kd-tree */ KDTree kdtree = new KDTree(); List<Triangle> triangles = new ArrayList<Triangle>(); int trianglesCount = mesh.positions.remaining() / 3 / 3; sceneBounds = new Box(); Vector3f min = new Vector3f(Float.MAX_VALUE, Float.MAX_VALUE, Float.MAX_VALUE); Vector3f max = new Vector3f(-Float.MAX_VALUE, -Float.MAX_VALUE, -Float.MAX_VALUE); sceneBounds.min = min; sceneBounds.max = max; for (int i = 0; i < trianglesCount; i++) { Triangle t = new Triangle(); t.v0 = new Vector3f(mesh.positions.get(i * 3 * 3 + 0), mesh.positions.get(i * 3 * 3 + 1), mesh.positions.get(i * 3 * 3 + 2)); t.v1 = new Vector3f(mesh.positions.get(i * 3 * 3 + 3), mesh.positions.get(i * 3 * 3 + 4), mesh.positions.get(i * 3 * 3 + 5)); t.v2 = new Vector3f(mesh.positions.get(i * 3 * 3 + 6), mesh.positions.get(i * 3 * 3 + 7), mesh.positions.get(i * 3 * 3 + 8)); triangles.add(t); min.min(t.v0).min(t.v1).min(t.v2); max.max(t.v0).max(t.v1).max(t.v2); } kdtree.buildTree(triangles, sceneBounds); DynamicByteBuffer nodesBuffer = new DynamicByteBuffer(); DynamicByteBuffer trianglesBuffer = new DynamicByteBuffer(); kdTreeToBuffers(kdtree, nodesBuffer, trianglesBuffer); nodesBuffer.flip(); trianglesBuffer.flip(); this.nodesSsbo = glGenBuffers(); glBindBuffer(GL_ARRAY_BUFFER, nodesSsbo); glBufferData(GL_ARRAY_BUFFER, nodesBuffer.bb, GL_STATIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0); this.trianglesSsbo = glGenBuffers(); glBindBuffer(GL_ARRAY_BUFFER, trianglesSsbo); glBufferData(GL_ARRAY_BUFFER, trianglesBuffer.bb, GL_STATIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0); } /** * Creates a VAO with a full-screen quad VBO. */ void createFullScreenVao() { this.vao = glGenVertexArrays(); int vbo = glGenBuffers(); glBindVertexArray(vao); glBindBuffer(GL_ARRAY_BUFFER, vbo); ByteBuffer bb = BufferUtils.createByteBuffer(4 * 2 * 6); FloatBuffer fv = bb.asFloatBuffer(); fv.put(-1.0f).put(-1.0f); fv.put(1.0f).put(-1.0f); fv.put(1.0f).put(1.0f); fv.put(1.0f).put(1.0f); fv.put(-1.0f).put(1.0f); fv.put(-1.0f).put(-1.0f); glBufferData(GL_ARRAY_BUFFER, bb, GL_STATIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT, false, 0, 0L); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); } /** * Create the full-scren quad shader. * * @throws IOException */ void createQuadProgram() throws IOException { int program = glCreateProgram(); int vshader = createShader("org/lwjgl/demo/opengl/raytracing/quad.vs", GL_VERTEX_SHADER, "330"); int fshader = createShader("org/lwjgl/demo/opengl/raytracing/quad.fs", GL_FRAGMENT_SHADER, "330"); glAttachShader(program, vshader); glAttachShader(program, fshader); glBindAttribLocation(program, 0, "vertex"); glBindFragDataLocation(program, 0, "color"); glLinkProgram(program); int linked = glGetProgrami(program, GL_LINK_STATUS); String programLog = glGetProgramInfoLog(program); if (programLog.trim().length() > 0) { System.err.println(programLog); } if (linked == 0) { throw new AssertionError("Could not link program"); } this.quadProgram = program; glUseProgram(quadProgram); int texUniform = glGetUniformLocation(quadProgram, "tex"); glUniform1i(texUniform, 0); glUseProgram(0); } /** * Create the tracing compute shader program. * * @throws IOException */ void createComputeProgram() throws IOException { int program = glCreateProgram(); int cshader = createShader("org/lwjgl/demo/opengl/raytracing/ssboTriangleStacklessKdTree.glsl", GL_COMPUTE_SHADER); int random = createShader("org/lwjgl/demo/opengl/raytracing/random.glsl", GL_COMPUTE_SHADER); int randomCommon = createShader("org/lwjgl/demo/opengl/raytracing/randomCommon.glsl", GL_COMPUTE_SHADER, "330"); glAttachShader(program, cshader); glAttachShader(program, random); glAttachShader(program, randomCommon); glLinkProgram(program); int linked = glGetProgrami(program, GL_LINK_STATUS); String programLog = glGetProgramInfoLog(program); if (programLog.trim().length() > 0) { System.err.println(programLog); } if (linked == 0) { throw new AssertionError("Could not link program"); } this.computeProgram = program; } /** * Initialize the compute shader. */ void initComputeProgram() { glUseProgram(computeProgram); IntBuffer workGroupSize = BufferUtils.createIntBuffer(3); glGetProgramiv(computeProgram, GL_COMPUTE_WORK_GROUP_SIZE, workGroupSize); workGroupSizeX = workGroupSize.get(0); workGroupSizeY = workGroupSize.get(1); eyeUniform = glGetUniformLocation(computeProgram, "eye"); ray00Uniform = glGetUniformLocation(computeProgram, "ray00"); ray10Uniform = glGetUniformLocation(computeProgram, "ray10"); ray01Uniform = glGetUniformLocation(computeProgram, "ray01"); ray11Uniform = glGetUniformLocation(computeProgram, "ray11"); debugUniform = glGetUniformLocation(computeProgram, "debug"); sceneMinUniform = glGetUniformLocation(computeProgram, "sceneMin"); sceneMaxUniform = glGetUniformLocation(computeProgram, "sceneMax"); IntBuffer props = BufferUtils.createIntBuffer(1); IntBuffer params = BufferUtils.createIntBuffer(1); props.put(0, GL_BUFFER_BINDING); int nodesResourceIndex = glGetProgramResourceIndex(computeProgram, GL_SHADER_STORAGE_BLOCK, "Nodes"); glGetProgramResourceiv(computeProgram, GL_SHADER_STORAGE_BLOCK, nodesResourceIndex, props, null, params); nodesSsboBinding = params.get(0); int trianglesResourceIndex = glGetProgramResourceIndex(computeProgram, GL_SHADER_STORAGE_BLOCK, "Triangles"); glGetProgramResourceiv(computeProgram, GL_SHADER_STORAGE_BLOCK, trianglesResourceIndex, props, null, params); trianglesSsboBinding = params.get(0); int loc = glGetUniformLocation(computeProgram, "framebufferImage"); glGetUniformiv(computeProgram, loc, params); framebufferImageBinding = params.get(0); glUseProgram(0); } /** * Create the texture that will serve as our framebuffer for the ray tracer. */ void createRaytracingTexture() { this.raytraceTexture = glGenTextures(); glBindTexture(GL_TEXTURE_2D, raytraceTexture); glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, width, height); glBindTexture(GL_TEXTURE_2D, 0); } /** * Create the sampler to sample the framebuffer texture within the shader. */ void createSampler() { this.sampler = glGenSamplers(); glSamplerParameteri(this.sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glSamplerParameteri(this.sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST); } /** * Resize the framebuffer textures for both rasterization and ray tracing. */ void resizeFramebufferTexture() { glDeleteTextures(raytraceTexture); createRaytracingTexture(); } void update() { if (mouseDown) { /* * If mouse is down, compute the camera rotation based on mouse cursor location. */ currRotationAboutY = rotationAboutY + (mouseX - mouseDownX) * 0.01f; } else { currRotationAboutY = rotationAboutY; } /* Rotate camera about Y axis. */ cameraPosition.set((float) sin(-currRotationAboutY) * cameraRadius, cameraHeight, (float) cos(-currRotationAboutY) * cameraRadius); projMatrix.setPerspective((float) Math.toRadians(30.0f), (float) width / height, 0.01f, 100.0f); viewMatrix.setLookAt(cameraPosition, cameraLookAt, cameraUp); projMatrix.invertPerspectiveView(viewMatrix, invViewProjMatrix); if (resetFramebuffer) { resizeFramebufferTexture(); resetFramebuffer = false; } } /** * Compute one frame by tracing the scene using our compute shader. */ void trace() { glUseProgram(computeProgram); /* Set viewing frustum corner rays in shader */ glUniform3f(eyeUniform, cameraPosition.x, cameraPosition.y, cameraPosition.z); invViewProjMatrix.transformProject(tmpVector.set(-1, -1, 0)).sub(cameraPosition).normalize(); glUniform3f(ray00Uniform, tmpVector.x, tmpVector.y, tmpVector.z); invViewProjMatrix.transformProject(tmpVector.set(-1, 1, 0)).sub(cameraPosition).normalize(); glUniform3f(ray01Uniform, tmpVector.x, tmpVector.y, tmpVector.z); invViewProjMatrix.transformProject(tmpVector.set(1, -1, 0)).sub(cameraPosition).normalize(); glUniform3f(ray10Uniform, tmpVector.x, tmpVector.y, tmpVector.z); invViewProjMatrix.transformProject(tmpVector.set(1, 1, 0)).sub(cameraPosition).normalize(); glUniform3f(ray11Uniform, tmpVector.x, tmpVector.y, tmpVector.z); glUniform3f(sceneMinUniform, sceneBounds.min.x, sceneBounds.min.y, sceneBounds.min.z); glUniform3f(sceneMaxUniform, sceneBounds.max.x, sceneBounds.max.y, sceneBounds.max.z); glUniform1i(debugUniform, debug ? 1 : 0); /* Bind level 0 of framebuffer texture as writable image in the shader. */ glBindImageTexture(framebufferImageBinding, raytraceTexture, 0, false, 0, GL_WRITE_ONLY, GL_RGBA8); /* Bind the SSBO containing our kd tree nodes */ glBindBufferBase(GL_SHADER_STORAGE_BUFFER, nodesSsboBinding, nodesSsbo); /* Bind the SSBO containing our triangles */ glBindBufferBase(GL_SHADER_STORAGE_BUFFER, trianglesSsboBinding, trianglesSsbo); /* Compute appropriate invocation dimension. */ int worksizeX = mathRoundPoT(width); int worksizeY = mathRoundPoT(height); /* Invoke the compute shader. */ glDispatchCompute(worksizeX / workGroupSizeX, worksizeY / workGroupSizeY, 1); /* * Synchronize all writes to the framebuffer image before we let OpenGL source texels from it afterwards when * rendering the final image with the full-screen quad. */ glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); /* Reset bindings. */ glBindBufferBase(GL_SHADER_STORAGE_BUFFER, nodesSsboBinding, 0); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, trianglesSsboBinding, 0); glBindImageTexture(framebufferImageBinding, 0, 0, false, 0, GL_WRITE_ONLY, GL_RGBA8); glUseProgram(0); } /** * Present the final image on the screen/viewport. */ void present() { if (caps.GL_NV_draw_texture) { /* * Use some fancy NV extension to draw a screen-aligned textured quad without needing a VAO/VBO or a shader. */ NVDrawTexture.glDrawTextureNV(raytraceTexture, sampler, 0.0f, 0.0f, width, height, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f); } else { /* * Draw a full-screen quad using the VAO and shader. */ glUseProgram(quadProgram); glBindVertexArray(vao); glBindTexture(GL_TEXTURE_2D, raytraceTexture); glBindSampler(0, this.sampler); glDrawArrays(GL_TRIANGLES, 0, 6); glBindSampler(0, 0); glBindTexture(GL_TEXTURE_2D, 0); glBindVertexArray(0); glUseProgram(0); } } void loop() { while (!glfwWindowShouldClose(window)) { glfwPollEvents(); glViewport(0, 0, width, height); update(); trace(); present(); glfwSwapBuffers(window); } } void run() { try { init(); loop(); if (debugProc != null) debugProc.free(); errCallback.free(); keyCallback.free(); fbCallback.free(); mbCallback.free(); glfwDestroyWindow(window); } catch (Throwable t) { t.printStackTrace(); } finally { glfwTerminate(); } } public static void main(String[] args) { new DemoSsboTrianglesStacklessKdTree().run(); } }