Search code examples
opengljoglopengl-4

Is there a simple way to get the depth of an object in OpenGL (JOGL)


how can I get the z-Coordinate of an Object in 3D-space when I click on it. (Its not really an Object more an graph, I need to know what an user selected) I use JOGL.


Solution

  • I just finished to port a picking sample from g-truck ogl-samples.

    I will try to give you a quick explanation about the code.

    We start by enabling the depth test

    private boolean initTest(GL4 gl4) {
    
        gl4.glEnable(GL_DEPTH_TEST);
    
        return true;
    }
    

    In the initBuffer we:

    • generate all the buffer we need with glGenBuffers
    • bind the element buffer and we transfer the content of our indices. Each index refers to the vertex to use. We need to bind it first because glBufferData will be using whatever is bounded at the target specify by the first argument, GL_ELEMENT_ARRAY_BUFFER in this case
    • do the same for the vertices themselves.
    • get the GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT(it's a global parameter) to determine the minimum uniform block size to store our transform variable. This is necessary if we want to bind it via glBindBufferRange, function that we will not use, instead, for binding our picking buffer, this is why we pass just the size of a float, Float.BYTES
    • the last argument of glBufferData is just an hint (it's up to OpenGL and the driver do what they want), as you see is static for the indices and vertices, because we are not gonna change them anymore, but is dynamic for the uniform buffers, since we will update them every frame.

    Code:

    private boolean initBuffer(GL4 gl4) {
    
        gl4.glGenBuffers(Buffer.MAX.ordinal(), bufferName, 0);
    
        gl4.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bufferName[Buffer.ELEMENT.ordinal()]);
        ShortBuffer elementBuffer = GLBuffers.newDirectShortBuffer(elementData);
        gl4.glBufferData(GL_ELEMENT_ARRAY_BUFFER, elementSize, elementBuffer, GL_STATIC_DRAW);
        gl4.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    
        gl4.glBindBuffer(GL_ARRAY_BUFFER, bufferName[Buffer.VERTEX.ordinal()]);
        FloatBuffer vertexBuffer = GLBuffers.newDirectFloatBuffer(vertexData);
        gl4.glBufferData(GL_ARRAY_BUFFER, vertexSize, vertexBuffer, GL_STATIC_DRAW);
        gl4.glBindBuffer(GL_ARRAY_BUFFER, 0);
    
        int[] uniformBufferOffset = {0};
        gl4.glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, uniformBufferOffset, 0);
        int uniformBlockSize = Math.max(projection.length * Float.BYTES, uniformBufferOffset[0]);
    
        gl4.glBindBuffer(GL_UNIFORM_BUFFER, bufferName[Buffer.TRANSFORM.ordinal()]);
        gl4.glBufferData(GL_UNIFORM_BUFFER, uniformBlockSize, null, GL_DYNAMIC_DRAW);
        gl4.glBindBuffer(GL_UNIFORM_BUFFER, 0);
    
        gl4.glBindBuffer(GL_TEXTURE_BUFFER, bufferName[Buffer.PICKING.ordinal()]);
        gl4.glBufferData(GL_TEXTURE_BUFFER, Float.BYTES, null, GL_DYNAMIC_READ);
        gl4.glBindBuffer(GL_TEXTURE_BUFFER, 0);
    
        return true;
    }
    

    In the initTexture we initialize our textures, we:

    • generate both the textures with glGenTextures
    • set the GL_UNPACK_ALIGNMENT to 1 (default is usually 4 bytes), in order to avoid any problem at all, (because your horizontal texture size must match the alignment).
    • set the activeTexture to GL_TEXTURE0, there is a specific number of texture slots and you need to specify it before working on any texture.
    • bind the diffuse texture
    • set the swizzle, that is what each channel will receive
    • set the levels (mipmap), where 0 is the base (original/biggest)
    • set the filters
    • allocate the space, levels included with glTexStorage2D
    • transfer for each level the corresponding data
    • reset back the GL_UNPACK_ALIGNMENT
    • bind to GL_TEXTURE0 our other texture PICKING
    • allocate a single 32b float storage and associate the PICKING texture to the PICKING buffer with glTexBuffer

    Code:

    private boolean initTexture(GL4 gl4) {
    
        try {
            jgli.Texture2D texture = new Texture2D(jgli.Load.load(TEXTURE_ROOT + "/" + TEXTURE_DIFFUSE));
            jgli.Gl.Format format = jgli.Gl.instance.translate(texture.format());
    
            gl4.glGenTextures(Texture.MAX.ordinal(), textureName, 0);
    
            // Diffuse
            {
                gl4.glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    
                gl4.glActiveTexture(GL_TEXTURE0);
                gl4.glBindTexture(GL_TEXTURE_2D, textureName[Texture.DIFFUSE.ordinal()]);
                gl4.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_RED);
                gl4.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G, GL_GREEN);
                gl4.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_BLUE);
                gl4.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_ALPHA);
                gl4.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
                gl4.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, texture.levels() - 1);
                gl4.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
                gl4.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    
                gl4.glTexStorage2D(GL_TEXTURE_2D, texture.levels(), format.internal.value,
                        texture.dimensions(0)[0], texture.dimensions(0)[1]);
    
                for (int level = 0; level < texture.levels(); ++level) {
                    gl4.glTexSubImage2D(GL_TEXTURE_2D, level,
                            0, 0,
                            texture.dimensions(level)[0], texture.dimensions(level)[1],
                            format.external.value, format.type.value,
                            texture.data(0, 0, level));
                }
    
                gl4.glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
            }
    
            // Piking
            {
                gl4.glBindTexture(GL_TEXTURE_BUFFER, textureName[Texture.PICKING.ordinal()]);
                gl4.glTexBuffer(GL_TEXTURE_BUFFER, GL_R32F, bufferName[Buffer.PICKING.ordinal()]);
                gl4.glBindTexture(GL_TEXTURE_BUFFER, 0);
            }
    
        } catch (IOException ex) {
            Logger.getLogger(Gl_420_picking.class.getName()).log(Level.SEVERE, null, ex);
        }
        return true;
    }
    

    In the initProgram we initialize our program, by:

    • generating a pipeline (composition of different shaders), glGenProgramPipelines
    • creating a vertex shader code vertShaderCode, where GL_VERTEX_SHADER is the shader type, SHADERS_ROOT is the place where the shader source is located, SHADERS_SOURCE_UPDATE is the name and "vert" is the extension.
    • initializing it, similarly for the fragment shader
    • grabbing the generated index and saving in programName
    • setting the program separable, nothing useful here, just pure sport, glProgramParameteri
    • adding both shader to our shaderProgram and linking and compiling it, link
    • specifing which program stage our pipelineName has, glUseProgramStages

    Code:

    private boolean initProgram(GL4 gl4) {
    
        boolean validated = true;
    
        gl4.glGenProgramPipelines(1, pipelineName, 0);
    
        // Create program
        if (validated) {
    
            ShaderProgram shaderProgram = new ShaderProgram();
    
            ShaderCode vertShaderCode = ShaderCode.create(gl4, GL_VERTEX_SHADER,
                    this.getClass(), SHADERS_ROOT, null, SHADERS_SOURCE_UPDATE, "vert", null, true);
            ShaderCode fragShaderCode = ShaderCode.create(gl4, GL_FRAGMENT_SHADER,
                    this.getClass(), SHADERS_ROOT, null, SHADERS_SOURCE_UPDATE, "frag", null, true);
    
            shaderProgram.init(gl4);
            programName = shaderProgram.program();
    
            gl4.glProgramParameteri(programName, GL_PROGRAM_SEPARABLE, GL_TRUE);
    
            shaderProgram.add(vertShaderCode);
            shaderProgram.add(fragShaderCode);
            shaderProgram.link(gl4, System.out);
        }
    
        if (validated) {
    
            gl4.glUseProgramStages(pipelineName[0], GL_VERTEX_SHADER_BIT | GL_FRAGMENT_SHADER_BIT, programName);
        }
    
        return validated & checkError(gl4, "initProgram");
    }
    

    In the initVertexArray we:

    • generate a single vertex array, glGenVertexArrays, and bind it, glBindVertexArray
    • bind the vertices buffer and set the attribute for the position and the color, here interleaved. The position is identified by the attribute index Semantic.Attr.POSITION (this will match the one in the vertex shader), component size 2, type GL_FLOAT, normalized false, stride or the total size of each vertex attribute 2 * 2 * Float.BYTES and the offset in this attribute 0. Similarly for the color.
    • unbind the vertices buffer since it is not part of the vertex array state. It must be bound only for the glVertexAttribPointer so that OpenGL can know which buffer those parameters refers to.
    • enable the corresponding vertex attribute array, glEnableVertexAttribArray
    • bind the element (indices) array, part of the vertex array

    Code:

    private boolean initVertexArray(GL4 gl4) {
    
        gl4.glGenVertexArrays(1, vertexArrayName, 0);
        gl4.glBindVertexArray(vertexArrayName[0]);
        {
            gl4.glBindBuffer(GL_ARRAY_BUFFER, bufferName[Buffer.VERTEX.ordinal()]);
            gl4.glVertexAttribPointer(Semantic.Attr.POSITION, 2, GL_FLOAT, false, 2 * 2 * Float.BYTES, 0);
            gl4.glVertexAttribPointer(Semantic.Attr.TEXCOORD, 2, GL_FLOAT, false, 2 * 2 * Float.BYTES, 2 * Float.BYTES);
            gl4.glBindBuffer(GL_ARRAY_BUFFER, 0);
    
            gl4.glEnableVertexAttribArray(Semantic.Attr.POSITION);
            gl4.glEnableVertexAttribArray(Semantic.Attr.TEXCOORD);
    
            gl4.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bufferName[Buffer.ELEMENT.ordinal()]);
        }
        gl4.glBindVertexArray(0);
    
        return true;
    }
    

    In the render we:

    • bind the TRANSFORM buffer that will contain our transformation matrix.
    • get a byteBuffer pointer out of that.
    • calculate the projection, view and model matrices and multiplying them in the same order p * v * m, called also mvp matrix.
    • save our mvp matrix in our pointer and rewind the buffer (position set to 0 again).
    • unmap it to make sure it gets uploaded to the gpu
    • set the viewport to match our window size
    • set the clear depthValue to 1 (superflous, since it is the default value), clear depth, with the depthValue, and color buffer, with the color {1.0f, 0.5f, 0.0f, 1.0f}
    • bind the pipeline
    • set active texture 0
    • bind the diffuse texture and the picking image texture
    • bind the vertex array
    • bind the transform uniform buffer
    • render, glDrawElementsInstancedBaseVertexBaseInstance is overused it, but what is important is the primitive type GL_TRIANGLES, the number of indices elementCount and their type GL_UNSIGNED_SHORT
    • bind the picking texture buffer and retrieve its value

    Code:

    @Override
    protected boolean render(GL gl) {
    
        GL4 gl4 = (GL4) gl;
    
        {
            gl4.glBindBuffer(GL_UNIFORM_BUFFER, bufferName[Buffer.TRANSFORM.ordinal()]);
            ByteBuffer pointer = gl4.glMapBufferRange(
                    GL_UNIFORM_BUFFER, 0, projection.length * Float.BYTES,
                    GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT);
    
            FloatUtil.makePerspective(projection, 0, true, (float) Math.PI * 0.25f,
                    (float) windowSize.x / windowSize.y, 0.1f, 100.0f);
            FloatUtil.makeIdentity(model);
    
            FloatUtil.multMatrix(projection, view());
            FloatUtil.multMatrix(projection, model);
    
            for (float f : projection) {
                pointer.putFloat(f);
            }
            pointer.rewind();
    
            // Make sure the uniform buffer is uploaded
            gl4.glUnmapBuffer(GL_UNIFORM_BUFFER);
        }
    
        gl4.glViewportIndexedf(0, 0, 0, windowSize.x, windowSize.y);
    
        float[] depthValue = {1.0f};
        gl4.glClearBufferfv(GL_DEPTH, 0, depthValue, 0);
        gl4.glClearBufferfv(GL_COLOR, 0, new float[]{1.0f, 0.5f, 0.0f, 1.0f}, 0);
    
        gl4.glBindProgramPipeline(pipelineName[0]);
        gl4.glActiveTexture(GL_TEXTURE0);
        gl4.glBindTexture(GL_TEXTURE_2D, textureName[Texture.DIFFUSE.ordinal()]);
        gl4.glBindImageTexture(Semantic.Image.PICKING, textureName[Texture.PICKING.ordinal()],
                0, false, 0, GL_WRITE_ONLY, GL_R32F);
        gl4.glBindVertexArray(vertexArrayName[0]);
        gl4.glBindBufferBase(GL_UNIFORM_BUFFER, Semantic.Uniform.TRANSFORM0, bufferName[Buffer.TRANSFORM.ordinal()]);
    
        gl4.glDrawElementsInstancedBaseVertexBaseInstance(GL_TRIANGLES, elementCount, GL_UNSIGNED_SHORT, 0, 5, 0, 0);
    
        gl4.glBindBuffer(GL_ARRAY_BUFFER, bufferName[Buffer.PICKING.ordinal()]);
        ByteBuffer pointer = gl4.glMapBufferRange(GL_ARRAY_BUFFER, 0, Float.BYTES, GL_MAP_READ_BIT);
        float depth = pointer.getFloat();
        gl4.glUnmapBuffer(GL_ARRAY_BUFFER);
    
        System.out.printf("Depth: %2.3f\n", depth);
    
        return true;
    }
    

    In our vertex shader, executed for each vertex, we:

    • define the glsl version and profile
    • define all the attribute indices, that must coincide with our coming from the Semantic we used previously
    • set some memory layout parameters, such as std140 and column_mayor (useless, default value for matrices)
    • declare the Transform uniform buffer
    • declare a vec3 position and vec2 texCoord inputs
    • declare a (built in, incomplete and useless) gl_PerVertex output
    • declare a Block block output
    • save inside our block the incoming texCoord and inside gl_Position our vertex in clip space position. The incoming position vertex is in Model space -> * model matrix = vertex in World space, * view/camera matrix = vertex in Camera/View space, * projection matrix = vertex in Clip space.

    Code:

    #version 420 core
    
    #define POSITION    0
    #define COLOR       3
    #define TEXCOORD    4
    
    #define TRANSFORM0  1
    
    precision highp float;
    precision highp int;
    layout(std140, column_major) uniform;
    
    layout(binding = TRANSFORM0) uniform Transform
    {
        mat4 mvp;
    } transform;
    
    layout(location = POSITION) in vec3 position;
    layout(location = TEXCOORD) in vec2 texCoord;
    
    out gl_PerVertex
    {
        vec4 gl_Position;
    };
    
    out Block
    {
        vec2 texCoord;
    } outBlock;
    
    void main()
    {   
        outBlock.texCoord = texCoord;
        gl_Position = transform.mvp * vec4(position, 1.0);
    }
    

    There may be are other stages after the vertex shader, such as tessellation control/evaluation and geometry, but they are not mandatory. The last stage is the fragment shader, executed once per fragment/pixel, that starts similarly, then we:

    • declare the texture diffuse on binding 0, that matches with our glActiveTexture(GL_TEXTURE0) inside the render and the imageBuffer picking where we will save our depth identified by binding 1, that matches our Semantic.Image.PICKING inside our render.glBindImageTexture
    • declare the picking coordinates, here hardcoded, but nothing stops you from turning them out as uniform variable and set it on runtime
    • declare the incoming Block block holding the texture coordinates
    • declare the default output color
    • if the current fragment coordinates gl_FragCoord (built in function) corresponds to the picking coordinates pickingCoord, save the current z value gl_FragCoord.z inside the imageBuffer depth and set the output color to vec4(1, 0, 1, 1), otherwise we set it equal to the diffuse texture by texture(diffuse, inBlock.texCoord.st). st is part of the stqp selection, synonymous of xywz or rgba.

    Code:

    #version 420 core
    
    #define FRAG_COLOR  0
    
    precision highp float;
    precision highp int;
    layout(std140, column_major) uniform;
    
    in vec4 gl_FragCoord;
    
    layout(binding = 0) uniform sampler2D diffuse;
    layout(binding = 1, r32f) writeonly uniform imageBuffer depth;
    
    uvec2 pickingCoord = uvec2(320, 240);
    
    in Block
    {
        vec2 texCoord;
    } inBlock;
    
    layout(location = FRAG_COLOR, index = 0) out vec4 color;
    
    void main()
    {
        if(all(equal(pickingCoord, uvec2(gl_FragCoord.xy))))
        {
            imageStore(depth, 0, vec4(gl_FragCoord.z, 0, 0, 0));
            color = vec4(1, 0, 1, 1);
        }
        else
            color = texture(diffuse, inBlock.texCoord.st);
    }
    

    Finally in the end we clean up all our OpenGL resources:

    @Override
    protected boolean end(GL gl) {
    
        GL4 gl4 = (GL4) gl;
    
        gl4.glDeleteProgramPipelines(1, pipelineName, 0);
        gl4.glDeleteProgram(programName);
        gl4.glDeleteBuffers(Buffer.MAX.ordinal(), bufferName, 0);
        gl4.glDeleteTextures(Texture.MAX.ordinal(), textureName, 0);
        gl4.glDeleteVertexArrays(1, vertexArrayName, 0);
    
        return true;
    }