Skip to content

FabricateRenderPlan.kt

src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricateRenderPlan.kt · 130 lines

package afield.graphics.scene.fabricated

import afield.math.linalg.Matrix4f
import afield.math.linalg.Vector3f

Render queue data structures and the FabricateScenePlanner that builds them from a FabricateScene.

The planner walks the scene graph, assigns each visible renderable to one of three queues based on its material's alphaMode (Opaque, AlphaMask, or Transparent), and sorts each queue for correct draw ordering: opaque and masked items front-to-back by sort order, transparent items back-to-front by camera distance. Shadow casters are collected from the opaque and masked sets. FabricateScene.planScene calls the planner; StandardRenderer.Bundle.applyScenePlan consumes the resulting FabricateRenderPlan.

data class FabricateRenderItem

One visible renderable node, resolved with its material ID, world transforms, and camera distance.

data class FabricateRenderItem(
    val node: FabricateNode,
    val renderable: FabricateRenderable,
    val materialId: Int,
    val queue: FabricateRenderQueue,
    val world: Matrix4f,
    val previousWorld: Matrix4f,
    val cameraDistanceSquared: Float
)

data class FabricateRenderPlan

data class FabricateRenderPlan(
    val opaque: List<FabricateRenderItem>,
    val alphaMask: List<FabricateRenderItem>,
    val transparent: List<FabricateRenderItem>,
    val shadowCasters: List<FabricateRenderItem>,
    val materials: List<FabricateMaterialRecord>
) {

val allDrawItems: List<FabricateRenderItem>

    val allDrawItems: List<FabricateRenderItem>
        get() = opaque + alphaMask + transparent
}

enum class FabricateRenderQueue

enum class FabricateRenderQueue {
    Opaque,
    AlphaMask,
    Transparent
}

val FabricateMaterial.renderQueue: FabricateRenderQueue

val FabricateMaterial.renderQueue: FabricateRenderQueue
    get() = when (alphaMode) {
        FabricateAlphaMode.OPAQUE -> FabricateRenderQueue.Opaque
        FabricateAlphaMode.MASK -> FabricateRenderQueue.AlphaMask
        FabricateAlphaMode.BLEND -> FabricateRenderQueue.Transparent
    }

class FabricateScenePlanner

Builds deterministic opaque/masked/transparent queues from a FabricateScene.

class FabricateScenePlanner(
    private val materialLibrary: FabricateMaterialLibrary = FabricateMaterialLibrary()
) {

fun plan(scene: FabricateScene, cameraPosition: Vector3f? = null): FabricateRenderPlan

    fun plan(scene: FabricateScene, cameraPosition: Vector3f? = null): FabricateRenderPlan {
        scene.updateWorldTransforms()
        return buildPlan(scene.root, cameraPosition)
    }

fun plan(root: FabricateNode, cameraPosition: Vector3f? = null): FabricateRenderPlan

body — 60 lines, from line 61
    fun plan(root: FabricateNode, cameraPosition: Vector3f? = null): FabricateRenderPlan {
        root.updateFromRoot()
        return buildPlan(root, cameraPosition)
    }

    private fun buildPlan(root: FabricateNode, cameraPosition: Vector3f?): FabricateRenderPlan {
        materialLibrary.clear()
        val opaque = mutableListOf<FabricateRenderItem>()
        val alphaMask = mutableListOf<FabricateRenderItem>()
        val transparent = mutableListOf<FabricateRenderItem>()
        val shadowCasters = mutableListOf<FabricateRenderItem>()

        root.walk().forEach { node ->
            val renderable = node.renderable
            if (renderable == null || !node.isEffectivelyVisible()) return@forEach
            val item = itemFor(node, renderable, cameraPosition)
            when (item.queue) {
                FabricateRenderQueue.Opaque -> opaque += item
                FabricateRenderQueue.AlphaMask -> alphaMask += item
                FabricateRenderQueue.Transparent -> transparent += item
            }
            if (renderable.castShadow && item.queue != FabricateRenderQueue.Transparent) {
                shadowCasters += item
            }
        }

        val stableFrontToBack = compareBy<FabricateRenderItem> { it.renderable.material.sortOrder }.thenBy { it.node.name }
        opaque.sortWith(stableFrontToBack)
        alphaMask.sortWith(stableFrontToBack)
        shadowCasters.sortWith(stableFrontToBack)
        transparent.sortWith(
            compareBy<FabricateRenderItem> { it.renderable.material.sortOrder }
                .thenByDescending { it.cameraDistanceSquared }
                .thenBy { it.node.name }
        )

        return FabricateRenderPlan(
            opaque = opaque,
            alphaMask = alphaMask,
            transparent = transparent,
            shadowCasters = shadowCasters,
            materials = materialLibrary.records()
        )
    }

    private fun itemFor(node: FabricateNode, renderable: FabricateRenderable, cameraPosition: Vector3f?): FabricateRenderItem {
        val materialId = materialLibrary.refresh(renderable.material)
        val position = node.worldPoint(renderable.boundsCenter)
        val distanceSquared = cameraPosition?.distanceSquared(position) ?: 0f
        return FabricateRenderItem(
            node = node,
            renderable = renderable,
            materialId = materialId,
            queue = renderable.material.renderQueue,
            world = Matrix4f(node.worldMatrix),
            previousWorld = Matrix4f(node.previousWorldMatrix),
            cameraDistanceSquared = distanceSquared
        )
    }
}

fun FabricateNode.isEffectivelyVisible(): Boolean

fun FabricateNode.isEffectivelyVisible(): Boolean {
    var n: FabricateNode? = this
    while (n != null) {
        if (!n.visible) return false
        n = n.parent
    }
    return true
}