Planar reflections and stencil buffers

I have been recently interested in stencil buffers (here and here), which are quite useful when creating reflections, shadows, portals, etc.
Here’s a little implementation of reflections from a planar mirror of any shape. The main idea is to render the object(s) as reflected by the mirror, and through enabling and disabling the stencil buffer we can make so that the only fragments of the reflected object(s) which don’t get discarded are those that coincide with the reflective surface.

import org.openrndr.WindowMultisample
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.*
import org.openrndr.extensions.Screenshots
import org.openrndr.extra.meshgenerators.boxMesh
import org.openrndr.extra.noise.Random
import org.openrndr.extra.shapes.hobbyCurve
import org.openrndr.math.Polar
import org.openrndr.math.Vector3
import org.openrndr.math.transforms.transform
import org.openrndr.shape.ShapeContour


fun main() = application {
    configure {
        width = 1000
        height = 1000
        multisample = WindowMultisample.SampleCount(8)
    }
    program {
        val n = 30
        val points = List(n) {
            Polar(it * 360 / n.toDouble(), Random.double(1.0, 20.0) + 40.0).cartesian
        }
        val sh = ShapeContour.fromPoints(points, closed = true).hobbyCurve()
        val numVerts = 200
        val vertices = sh.equidistantPositions(numVerts)
        val shapeBuffer = vertexBuffer(vertexFormat {
            position(3)
            normal(3)
        }, numVerts * 3)

        shapeBuffer.put {
            repeat(numVerts) {
                write(Vector3(0.0, 0.0, 0.0))
                write(Vector3(0.0, 0.0, 1.0))
                write(Vector3(vertices[it % numVerts].x, vertices[it % numVerts].y, 0.0))
                write(Vector3(0.0, 0.0, 1.0))
                write(Vector3(vertices[(it + 1) % numVerts].x, vertices[(it + 1) % numVerts].y, 0.0))
                write(Vector3(0.0, 0.0, 1.0))
            }
        }

        val cube = boxMesh(25.0, 25.0, 25.0)

        extend {
            val lightPos = Vector3(0.0, 30.0, -80.0)
            val objectColor = Vector3(0.5, 0.1, 0.6)

            val tr_mirror = transform {
                translate(0.0, 0.0, -150.0)
                rotate(Vector3.UNIT_X, -70.0)
            }

            val tr_cube = transform {
                translate(0.0, 25.0, -150.0)
                rotate(Vector3.UNIT_XYZ, 70.0 + seconds * 40.0)
            }

            val tr_reflected = transform {
                scale(1.0, -1.0, 1.0)
                translate(0.0, 25.0, -150.0)
                rotate(Vector3.UNIT_XYZ, 70.0 + seconds * 40.0)
            }

            drawer.perspective(60.0, width * 1.0 / height, 0.01, 1000.0)

            //Draw object
            drawer.depthWrite = true
            drawer.depthTestPass = DepthTestPass.LESS_OR_EQUAL

            drawer.stroke = null
            drawer.fill = ColorRGBa.WHITE
            drawer.shadeStyle = shadeStyle {
                vertexTransform = """
                    x_modelMatrix = x_modelMatrix * p_tr;
                    x_modelNormalMatrix = x_modelNormalMatrix * p_tr;
                """.trimIndent()
                parameter("tr", tr_cube)
                fragmentTransform = """
                    vec3 lightDir = normalize(p_lightPos  - v_worldPosition);
                    float d = max(dot(v_worldNormal, lightDir), 0.0);
                    x_fill.rgb = d * vec3(0.9, 0.2, 0.9);
                """.trimIndent()
                parameter("lightPos", lightPos)
                parameter("objectColor", objectColor)
            }
            drawer.vertexBuffer(cube, DrawPrimitive.TRIANGLES)

            //Draw mirror
            drawer.drawStyle.depthWrite = false
            drawer.drawStyle.stencil.stencilFunc(StencilTest.ALWAYS, 1, 0xFF)
            drawer.drawStyle.stencil.stencilWriteMask = 0xFF
            drawer.drawStyle.stencil.stencilOp(
                StencilOperation.KEEP,
                StencilOperation.KEEP,
                StencilOperation.REPLACE
            )

            drawer.shadeStyle = shadeStyle {
                vertexTransform = """
                    x_modelMatrix = x_modelMatrix * p_tr;
                    x_modelNormalMatrix = x_modelNormalMatrix * p_tr;
                """.trimIndent()
                parameter("tr", tr_mirror)
                fragmentTransform = """     
                    vec3 lightDir = normalize(p_lightPos - v_worldPosition);
                    float d = max(dot(v_worldNormal, lightDir), 0.0);
                    x_fill.rgb = d * vec3(0.0, 0.8, 0.9);
                """.trimIndent()
                parameter("lightPos", lightPos)
            }
            drawer.vertexBuffer(shapeBuffer, DrawPrimitive.TRIANGLES)

            //Draw reflected object
            drawer.depthWrite = true
            drawer.drawStyle.stencil.stencilFunc(StencilTest.EQUAL, 1, 0xFF)
            drawer.drawStyle.stencil.stencilWriteMask = 0x00

            drawer.shadeStyle = shadeStyle {
                vertexTransform = """
                    x_modelMatrix = x_modelMatrix * p_tr;
                    x_modelNormalMatrix = x_modelNormalMatrix * p_tr;
                """.trimIndent()
                parameter("tr", tr_reflected)
                fragmentTransform = """
                    vec3 lightDir = normalize(p_lightPos - v_worldPosition);
                    float d = max(dot(v_worldNormal, lightDir), 0.0);
                    x_fill.rgb = d * p_objectColor * 0.5;
                """.trimIndent()
                parameter("lightPos", lightPos)
                parameter("objectColor", objectColor)
            }
            drawer.vertexBuffer(cube, DrawPrimitive.TRIANGLES)
        }
    }
}

It looks like this :slight_smile:

1 Like