Mouse picking 3D objects

Inspired by this post, find here a program to detect which 3D object is under the mouse cursor.

The idea is to do two render passes. One of them is what the user sees. The other render pass is used internally. It contains the same geometry, but the shapes have been rendered with flat and known colors. Later, we sample the pixel color under the mouse position and use that color to identify the shape under the mouse.

In this simple example I’m using the red component of the pixel. Since we use the default UINT8 pixel colors, we can only identify 256 shapes. We could switch to a different color model, or use the red, green and blue to encode up to 16 million shapes.

These might not be production quality, but they are shared here to serve as a starting point.

Using a perspective camera

import org.openrndr.WindowMultisample
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.color.rgb
import org.openrndr.draw.*
import org.openrndr.extra.camera.Orbital
import org.openrndr.extra.meshgenerators.boxMesh
import org.openrndr.extra.noise.Random
import org.openrndr.extra.noise.uniform
import org.openrndr.math.Vector3
import org.openrndr.math.transforms.transform

fun main() = application {
    configure {
        multisample = WindowMultisample.SampleCount(4)
    }
    program {
        // Add a render target to draw and detect shapes under the mouse
        val rt = renderTarget(width, height) {
            colorBuffer()
            depthBuffer()
        }
        // This CPU container of pixels
        val shadow = rt.colorBuffer(0).shadow

        val cube = boxMesh()
        val cam = Orbital()
        cam.eye = -Vector3.UNIT_Z * 150.0

        val simpleLightShader = shadeStyle {
            fragmentTransform = """
                vec3 lightDir = normalize(vec3(0.3, 1.0, 0.5));
                float l = dot(va_normal, lightDir) * 0.4 + 0.5;
                x_fill.rgb *= l; 
            """.trimIndent()
        }

        val moireShader = shadeStyle {
            fragmentTransform = """
                x_fill.rgb = vec3(fract(va_position.y * 100.0));
            """.trimIndent()
        }

        // A container of 3D transformations and a color for each
        val data = List(100) {
            transform {
                translate(Random.double0(100.0), Random.double0(100.0), Random.double0(100.0))
                rotate(Vector3.uniform(-1.0, 1.0), Random.double0(360.0))
                scale(Random.double0(100.0), Random.double0(100.0), Random.double0(100.0))
            } to ColorRGBa.fromVector(Random.vector3(0.0, 1.0))
        }

        extend(cam)
        extend {
            // Clear rt
            drawer.isolatedWithTarget(rt) {
                clear(ColorRGBa.TRANSPARENT)
            }
            // Draw all the shapes into the rt.
            // Use the index value as a color (max 256 shapes)
            data.forEachIndexed { i, (tr, _) ->
                drawer.isolatedWithTarget(rt) {
                    fill = rgb(i / 255.0)
                    model *= tr
                    vertexBuffer(cube, DrawPrimitive.TRIANGLES)
                }
            }

            // Download the rt pixels into shadow
            shadow.download()
            val mousePos = mouse.position.toInt()
            // Convert the red value of the pixel under the mouse to a number between 0 and 255
            val redUnderMouse = (shadow[mousePos.x, mousePos.y].r * 255).toInt()

            // Make the 3D shapes visible
            drawer.clear(ColorRGBa.WHITE)
            data.forEachIndexed { i, (tr, color) ->
                drawer.isolated {
                    model *= tr
                    fill = color
                    // If the mouse is over a shape, use the moireShader
                    shadeStyle = if(i == redUnderMouse) moireShader else simpleLightShader
                    vertexBuffer(cube, DrawPrimitive.TRIANGLES)
                }
            }
        }
    }
}

With an ortho camera

import org.openrndr.WindowMultisample
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.color.rgb
import org.openrndr.draw.*
import org.openrndr.extra.meshgenerators.boxMesh
import org.openrndr.extra.noise.Random
import org.openrndr.math.Vector3
import org.openrndr.math.transforms.transform

fun main() = application {
    configure {
        multisample = WindowMultisample.SampleCount(4)
    }
    program {
        // Add a render target to draw and detect shapes under the mouse
        val rt = renderTarget(width, height) {
            colorBuffer()
            depthBuffer()
        }
        // This CPU container of pixels
        val shadow = rt.colorBuffer(0).shadow

        val cube = boxMesh()

        val simpleLightShader = shadeStyle {
            fragmentTransform = """
                vec3 lightDir = normalize(vec3(0.3, 1.0, 0.5));
                float l = dot(va_normal, lightDir) * 0.4 + 0.5;
                x_fill.rgb *= l; 
            """.trimIndent()
        }

        val moireShader = shadeStyle {
            fragmentTransform = """
                x_fill.rgb = vec3(fract(10.0 * dot(va_position, vec3(1.0))));
            """.trimIndent()
        }

        // A container of 3D transformations and a color for each
        val data = List(100) {
            transform {
                translate(Random.double0(width * 1.0), Random.double0(height * 1.0), 10.0)
                rotate(Vector3(1.0, 1.0, 0.0), 45.0)
                scale(Random.int(1, 10) * 10.0, Random.int(1, 10) * 10.0, Random.int(1, 10) * 10.0)
            } to ColorRGBa.fromVector(Random.vector3(0.0, 1.0))
        }

        extend {
            drawer.ortho(0.0, width.toDouble(), height.toDouble(), 0.0, -100.0, 100.0)
            drawer.depthWrite = true
            drawer.depthTestPass = DepthTestPass.LESS_OR_EQUAL

            // Clear rt
            drawer.isolatedWithTarget(rt) {
                clear(ColorRGBa.TRANSPARENT)
            }
            // Draw all the shapes into the rt.
            // Use the index value as a color (max 256 shapes)
            data.forEachIndexed { i, (tr, _) ->
                drawer.isolatedWithTarget(rt) {
                    fill = rgb(i / 255.0)
                    model *= tr
                    vertexBuffer(cube, DrawPrimitive.TRIANGLES)
                }
            }

            // Download the rt pixels into shadow
            shadow.download()
            val mousePos = mouse.position.toInt()
            // Convert the red value of the pixel under the mouse to a number between 0 and 255
            val redUnderMouse = (shadow[mousePos.x, mousePos.y].r * 255).toInt()

            // Make the 3D shapes visible
            drawer.clear(ColorRGBa.WHITE)
            data.forEachIndexed { i, (tr, color) ->
                drawer.isolated {
                    model *= tr
                    fill = color
                    // If the mouse is over a shape, use the moireShader
                    shadeStyle = if(i == redUnderMouse) moireShader else simpleLightShader
                    vertexBuffer(cube, DrawPrimitive.TRIANGLES)
                }
            }
        }
    }
}

It might be nice to create an extension to encapsulate the required code and allow for simpler user code.