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.