Skip to content

OpaqueRayScene.kt

src/jvmMain/kotlin/afield/graphics/render/lighting/OpaqueRayScene.kt · 128 lines

Opaque triangle geometry for ray-query visibility, as used by DirectLighting for shadows.

OpaqueTriangles is an immutable indexed mesh (OpaqueTriangles.box makes a box), and an OpaqueInstance places one with a stable id and transform. OpaqueRayScene.commit takes the instance list and builds one bottom-level acceleration structure per geometry and a new top-level structure for each edit, waiting for the build, so commit between frames on the render thread. OpaqueRayScene.available reports whether the device supports ray queries.

package afield.graphics.render.lighting

import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.buffers.VulkanBuffer
import afield.document.context.*
import afield.graphics.render.*
import afield.graphics.scene.*
import afield.graphics.shader.*
import afield.math.linalg.Matrix4f
import org.lwjgl.vulkan.KHRAccelerationStructure.*
import org.lwjgl.vulkan.VK12.*

class OpaqueTriangles(positions: FloatArray, indices: IntArray)

Immutable indexed XYZ triangles. Opaque, double sided visibility only; no alpha or transmission.

internal val vertexData = positions.copyOf()

internal val indexData = indices.copyOf()

val positions get() = vertexData.copyOf()

val indices get() = indexData.copyOf()

init

    init {
        require(positions.isNotEmpty() && positions.size % 3 == 0 && positions.all { it.isFinite() })
        require(indices.isNotEmpty() && indices.size % 3 == 0 && indices.all { it in 0 until positions.size / 3 })
    }

companion object

fun box(x: Float, y: Float, z: Float): OpaqueTriangles

        fun box(x: Float, y: Float, z: Float): OpaqueTriangles {
            require(listOf(x,y,z).all { it.isFinite() && it > 0 })
            return OpaqueTriangles(floatArrayOf(-x,-y,-z, x,-y,-z, x,y,-z, -x,y,-z, -x,-y,z, x,-y,z, x,y,z, -x,y,z),
                intArrayOf(0,2,1,0,3,2, 4,5,6,4,6,7, 0,1,5,0,5,4, 3,7,6,3,6,2, 0,4,7,0,7,3, 1,2,6,1,6,5))
        }
    }
}

data class OpaqueInstance(val id: Int, val geometry: OpaqueTriangles, val transform: Matrix4f = Matrix4f())

Stable object id is independent of list position. Transform is copied at OpaqueRayScene.commit.

data class OpaqueInstance(val id: Int, val geometry: OpaqueTriangles, val transform: Matrix4f = Matrix4f())

class OpaqueRayScene(val renderer: Renderer) : HasAccelerationStructure, AutoCloseable

Static BLAS per geometry identity; a new TLAS for each explicit instance edit. No refit is claimed. Commit on the render thread between frames. Builds submit on Field's graphics queue and wait for their fence; removed resources retire via Field's frames-in-flight queue. Rebuilding rather than refitting makes edits safe, at the cost of a stall on instance or topology edits.

class OpaqueRayScene(val renderer: Renderer) : HasAccelerationStructure, AutoCloseable {
    private data class Geometry(val vertices: VulkanBuffer, val indices: VulkanBuffer, val blas: AccelerationStructure) {
        fun close() { blas.cleanup(); indices.cleanup(); vertices.cleanup() }
    }
    private var geometry = mapOf<OpaqueTriangles, Geometry>()
    private var top: AccelerationStructure? = null
    private var signature = emptyList<Pair<Int, List<Float>>>()
    private var order = emptyList<OpaqueTriangles>()

var blasBuilds = 0; private set

var tlasBuilds = 0; private set

var closed = false; private set

val available get() = renderer.device.supportsRayQuery

override val vkAccelerationStructure get() = top?.vkAccelerationStructure ?: 0L

override fun deviceAddress(): Long = checkNotNull(top).deviceAddress()

fun commit(instances: List<OpaqueInstance>): Boolean

Returns false when unchanged or unavailable. Empty scenes are represented by a masked instance.

body — 42 lines, from line 67
    fun commit(instances: List<OpaqueInstance>): Boolean {
        check(!closed)
        require(instances.map { it.id }.distinct().size == instances.size) { "Duplicate object identity" }
        require(instances.all { it.id in 0..0xffffff })
        val actual = if (instances.isEmpty()) listOf(OpaqueInstance(0, emptyGeometry)) else instances
        val transforms = actual.map { rowMajor3x4(it.transform) }
        val nextSignature = actual.mapIndexed { i, v -> v.id to transforms[i].toList() } + (-1 to listOf(instances.size.toFloat()))
        val nextOrder = actual.map { it.geometry }
        if (!available || (nextSignature == signature && nextOrder == order)) return false
        val created = mutableMapOf<OpaqueTriangles, Geometry>()
        var candidate: AccelerationStructure? = null
        try {
            nextOrder.distinct().filter { it !in geometry }.forEach { mesh ->
                val flags = VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR or VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT
                val vertices = VulkanBuffer(renderer, mesh.vertexData.size * 4L, flags,
                    VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT or VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, needsDeviceAddress = true)
                var indices: VulkanBuffer? = null
                var blas: AccelerationStructure? = null
                try {
                    vertices.mapAndWrite_bytes { asFloatBuffer().put(mesh.vertexData) }
                    indices = VulkanBuffer(renderer, mesh.indexData.size * 4L, flags,
                        VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT or VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, needsDeviceAddress = true)
                    indices.mapAndWrite_bytes { asIntBuffer().put(mesh.indexData) }
                    blas = AccelerationStructure(renderer, "direct-light-blas")
                    blas.buildBottomLevelNow(listOf(AccelerationStructureTriangles(vertices, maxVertex = mesh.vertexData.size / 3 - 1,
                        primitiveCount = mesh.indexData.size / 3, indexBuffer = indices)))
                    created[mesh] = Geometry(vertices, indices, blas)
                    blasBuilds++
                } catch (t: Throwable) { blas?.cleanup(); indices?.cleanup(); vertices.cleanup(); throw t }
            }
            val nextGeometry = nextOrder.distinct().associateWith { geometry[it] ?: created.getValue(it) }
            candidate = AccelerationStructure(renderer, "direct-light-tlas")
            candidate.buildTopLevelNow(actual.mapIndexed { i, instance -> RayTracingInstance(nextGeometry.getValue(instance.geometry).blas,
                transforms[i], customIndex = instance.id, mask = if (instances.isEmpty()) 0 else 255) })
            tlasBuilds++
            val oldTop = top
            val removed = geometry.filterKeys { it !in nextGeometry }.values.toList()
            top = candidate; geometry = nextGeometry; signature = nextSignature; order = nextOrder
            renderer.cleanup += Runnable { oldTop?.cleanup(); removed.forEach { it.close() } }
            return true
        } catch (t: Throwable) { candidate?.cleanup(); created.values.forEach { it.close() }; throw t }
    }

override fun close()

    override fun close() {
        if (closed) return
        closed = true
        val oldTop = top; val oldGeometry = geometry.values.toList()
        top = null; geometry = emptyMap()
        renderer.cleanup += Runnable { oldTop?.cleanup(); oldGeometry.forEach { it.close() } }
    }

companion object

    companion object {
        private val emptyGeometry = OpaqueTriangles.box(0.01f,0.01f,0.01f)

fun rowMajor3x4(m: Matrix4f): FloatArray

        fun rowMajor3x4(m: Matrix4f): FloatArray {
            require(m.m03() == 0f && m.m13() == 0f && m.m23() == 0f && m.m33() == 1f) { "Instance transform must be affine" }
            require(m.determinant().isFinite() && kotlin.math.abs(m.determinant()) > 1e-12f) { "Singular instance transform" }
            return floatArrayOf(m.m00(),m.m10(),m.m20(),m.m30(), m.m01(),m.m11(),m.m21(),m.m31(), m.m02(),m.m12(),m.m22(),m.m32()).also {
                require(it.all { v -> v.isFinite() })
            }
        }
    }
}