Skip to content

standardShapes.kt

src/jvmMain/kotlin/afield/graphics/scene/fabricated/standardShapes.kt · 314 lines

package afield.graphics.scene.fabricated

import afield.graphics.vulkan.buffers.DynamicBuffer
import afield.graphics.vulkan.buffers.DynamicallyWriteable
import afield.math.linalg.Vec3
import org.lwjgl.util.par.ParShapes.*
import org.lwjgl.util.par.ParShapesMesh
import kotlin.math.sqrt

Ten par_shapes primitives, written straight into vertex-pulling buffers as triangle soup.

StandardShapes is the layer under StandardRenderer.Bundle.registerCube, registerSphere and the eight other register* shape helpers, one per function here. Each builds a par_shapes mesh, scales it, translates it if the call gave a centre, and writes it into two buffers the caller supplies, one for positions and one for normals. The write is non-indexed: the source mesh's triangle list is expanded, so three vertices go out per triangle and a vertex-pulling shader reads a position and its normal at the same index with no indirection. ShapeWriteResult comes back carrying the source mesh's vertex and triangle counts alongside what was actually written, which is how a caller sizes its mesh afterwards instead of predicting the triangle count up front.

WriteConfig covers the two decisions a caller usually has to make. padVec3To4 writes each vector as four floats for std430 alignment and is on by default; append chooses between continuing after whatever is already in the buffer and starting at its beginning, and all ten register* helpers pass append = false. Normals come from par_shapes, computed there if the mesh arrived without any, with an index that falls outside the normal buffer reading the triangle's face normal instead. Each call frees its par_shapes mesh before returning.

object StandardShapes

Thin Kotlin helpers around par_shapes that emit non-indexed positions + normals into caller-provided DynamicallyWriteable buffers (useful for vertex-pulling tests).

object StandardShapes {

data class WriteConfig

    data class WriteConfig(
        val append: Boolean = true,            // when false, starts writing at buffer start
        val padVec3To4: Boolean = true,        // write vec3 as (x,y,z,w) for std430 alignment
        val positionW: Float = 1f,
        val normalW: Float = 0f
    )

data class ShapeWriteResult

    data class ShapeWriteResult(
        val sourceVertices: Int,
        val sourceTriangles: Int,
        val writtenVertices: Int,
        val positionFloatsWritten: Int,
        val normalFloatsWritten: Int
    )

fun sphere

    fun sphere(
        positions: DynamicallyWriteable,
        normals: DynamicallyWriteable,
        radius: Float = 1f,
        slices: Int = 32,
        stacks: Int = 32,
        center: Vec3? = null,
        config: WriteConfig = WriteConfig()
    ): ShapeWriteResult = buildAndWrite(positions, normals, config, {
        par_shapes_create_parametric_sphere(slices, stacks)
    }) { mesh ->
        par_shapes_scale(mesh, radius, radius, radius)
        center?.let { par_shapes_translate(mesh, it.x, it.y, it.z) }
    }

fun hemisphere

    fun hemisphere(
        positions: DynamicallyWriteable,
        normals: DynamicallyWriteable,
        radius: Float = 1f,
        slices: Int = 32,
        stacks: Int = 32,
        config: WriteConfig = WriteConfig()
    ): ShapeWriteResult = buildAndWrite(positions, normals, config, {
        par_shapes_create_hemisphere(slices, stacks)
    }) { mesh ->
        par_shapes_scale(mesh, radius, radius, radius)
    }

fun cylinder

    fun cylinder(
        positions: DynamicallyWriteable,
        normals: DynamicallyWriteable,
        radius: Float = 1f,
        height: Float = 1f,
        slices: Int = 32,
        stacks: Int = 1,
        config: WriteConfig = WriteConfig()
    ): ShapeWriteResult = buildAndWrite(positions, normals, config, {
        par_shapes_create_cylinder(slices, stacks)
    }) { mesh ->
        par_shapes_scale(mesh, radius, height, radius)
    }

fun cone

    fun cone(
        positions: DynamicallyWriteable,
        normals: DynamicallyWriteable,
        radius: Float = 1f,
        height: Float = 1f,
        slices: Int = 32,
        stacks: Int = 1,
        config: WriteConfig = WriteConfig()
    ): ShapeWriteResult = buildAndWrite(positions, normals, config, {
        par_shapes_create_cone(slices, stacks)
    }) { mesh ->
        par_shapes_scale(mesh, radius, height, radius)
    }

fun disk

    fun disk(
        positions: DynamicallyWriteable,
        normals: DynamicallyWriteable,
        radius: Float = 1f,
        slices: Int = 32,
        stacks: Int = 1,
        config: WriteConfig = WriteConfig()
    ): ShapeWriteResult = buildAndWrite(positions, normals, config, {
        par_shapes_create_parametric_disk(slices, stacks)
    }) { mesh ->
        par_shapes_scale(mesh, radius, radius, radius)
    }

fun cube

    fun cube(
        positions: DynamicallyWriteable,
        normals: DynamicallyWriteable,
        size: Float = 1f,
        center: Vec3? = null,
        config: WriteConfig = WriteConfig()
    ): ShapeWriteResult = buildAndWrite(positions, normals, config, {
        par_shapes_create_cube()
    }) { mesh ->
        par_shapes_scale(mesh, size, size, size)
        center?.let { par_shapes_translate(mesh, it.x, it.y, it.z) }
    }

fun torus

    fun torus(
        positions: DynamicallyWriteable,
        normals: DynamicallyWriteable,
        slices: Int = 32,
        stacks: Int = 32,
        tubeRadius: Float = 0.25f,
        majorRadius: Float = 1f,
        config: WriteConfig = WriteConfig()
    ): ShapeWriteResult = buildAndWrite(positions, normals, config, {
        par_shapes_create_torus(slices, stacks, tubeRadius)
    }) { mesh ->
        par_shapes_scale(mesh, majorRadius, majorRadius, majorRadius)
    }

fun trefoilKnot

    fun trefoilKnot(
        positions: DynamicallyWriteable,
        normals: DynamicallyWriteable,
        slices: Int = 64,
        stacks: Int = 16,
        radius: Float = 1f,
        config: WriteConfig = WriteConfig()
    ): ShapeWriteResult = buildAndWrite(positions, normals, config, {
        par_shapes_create_trefoil_knot(slices, stacks, 1f)
    }) { mesh ->
        par_shapes_scale(mesh, radius, radius, radius)
    }

fun kleinBottle

    fun kleinBottle(
        positions: DynamicallyWriteable,
        normals: DynamicallyWriteable,
        slices: Int = 32,
        stacks: Int = 32,
        scale: Float = 0.1f,
        config: WriteConfig = WriteConfig()
    ): ShapeWriteResult = buildAndWrite(positions, normals, config, {
        par_shapes_create_klein_bottle(slices, stacks)
    }) { mesh ->
        par_shapes_scale(mesh, scale, scale, scale)
    }

fun rock

body — 140 lines, from line 174
    fun rock(
        positions: DynamicallyWriteable,
        normals: DynamicallyWriteable,
        seed: Int = 1,
        subdivisions: Int = 4,
        radius: Float = 1f,
        config: WriteConfig = WriteConfig()
    ): ShapeWriteResult = buildAndWrite(positions, normals, config, {
        par_shapes_create_rock(seed, subdivisions)
    }) { mesh ->
        par_shapes_scale(mesh, radius, radius, radius)
    }

    private inline fun buildAndWrite(
        positions: DynamicallyWriteable,
        normals: DynamicallyWriteable,
        config: WriteConfig,
        factory: () -> ParShapesMesh?,
        crossinline transform: (ParShapesMesh) -> Unit
    ): ShapeWriteResult {
        val mesh = requireNotNull(factory()) { "par_shapes factory returned null" }
        transform(mesh)
        if (mesh.normals(mesh.npoints() * 3) == null) {
            par_shapes_compute_normals(mesh)
        }
        val result = writeMesh(mesh, positions, normals, config)
        par_shapes_free_mesh(mesh)
        return result
    }

    private fun writeMesh(
        mesh: ParShapesMesh,
        positions: DynamicallyWriteable,
        normals: DynamicallyWriteable,
        config: WriteConfig
    ): ShapeWriteResult {
        val triCount = mesh.ntriangles()
        val srcVertices = mesh.npoints()
        val triangles = mesh.triangles(triCount * 3)
        val srcPositions = mesh.points(srcVertices * 3)
        val srcNormals = mesh.normals(srcVertices * 3)
            ?: error("par_shapes mesh has no normals (even after compute)")

        val outVertices = triCount * 3
        val posStride = if (config.padVec3To4) 4 else 3
        val normStride = if (config.padVec3To4) 4 else 3

        fun writeVec(target: DynamicBuffer, x: Float, y: Float, z: Float, w: Float) {
            if (config.padVec3To4) target.put(x, y, z, w) else {
                target.putScalar(x)
                target.putScalar(y)
                target.putScalar(z)
            }
        }

        fun writeAll(target: DynamicallyWriteable, block: DynamicBuffer.() -> Unit) {
            if (config.append) target.append(block) else target.write(block)
        }

        writeAll(positions) {
            for (t in 0 until triCount) {
                val i0 = triangles.get(t * 3 + 0)
                val i1 = triangles.get(t * 3 + 1)
                val i2 = triangles.get(t * 3 + 2)

                val p0 = i0 * 3
                val p1 = i1 * 3
                val p2 = i2 * 3

                writeVec(this, srcPositions.get(p0 + 0), srcPositions.get(p0 + 1), srcPositions.get(p0 + 2), config.positionW)
                writeVec(this, srcPositions.get(p1 + 0), srcPositions.get(p1 + 1), srcPositions.get(p1 + 2), config.positionW)
                writeVec(this, srcPositions.get(p2 + 0), srcPositions.get(p2 + 1), srcPositions.get(p2 + 2), config.positionW)
            }
        }

        writeAll(normals) {
            for (t in 0 until triCount) {
                val i0 = triangles.get(t * 3 + 0)
                val i1 = triangles.get(t * 3 + 1)
                val i2 = triangles.get(t * 3 + 2)

                val n0 = i0 * 3
                val n1 = i1 * 3
                val n2 = i2 * 3

                // If source normals are missing or degenerate, fall back to the face normal.
                val face = faceNormal(srcPositions, i0, i1, i2)

                writeVec(this, srcNormals.getOrNull(n0 + 0, face[0]), srcNormals.getOrNull(n0 + 1, face[1]), srcNormals.getOrNull(n0 + 2, face[2]), config.normalW)
                writeVec(this, srcNormals.getOrNull(n1 + 0, face[0]), srcNormals.getOrNull(n1 + 1, face[1]), srcNormals.getOrNull(n1 + 2, face[2]), config.normalW)
                writeVec(this, srcNormals.getOrNull(n2 + 0, face[0]), srcNormals.getOrNull(n2 + 1, face[1]), srcNormals.getOrNull(n2 + 2, face[2]), config.normalW)
            }
        }

        return ShapeWriteResult(
            sourceVertices = srcVertices,
            sourceTriangles = triCount,
            writtenVertices = outVertices,
            positionFloatsWritten = outVertices * posStride,
            normalFloatsWritten = outVertices * normStride
        )
    }

    private fun faceNormal(pos: java.nio.FloatBuffer, i0: Int, i1: Int, i2: Int): FloatArray {
        val a = i0 * 3
        val b = i1 * 3
        val c = i2 * 3

        val ax = pos.get(a + 0)
        val ay = pos.get(a + 1)
        val az = pos.get(a + 2)

        val bx = pos.get(b + 0)
        val by = pos.get(b + 1)
        val bz = pos.get(b + 2)

        val cx = pos.get(c + 0)
        val cy = pos.get(c + 1)
        val cz = pos.get(c + 2)

        val ux = bx - ax
        val uy = by - ay
        val uz = bz - az

        val vx = cx - ax
        val vy = cy - ay
        val vz = cz - az

        val nx = uy * vz - uz * vy
        val ny = uz * vx - ux * vz
        val nz = ux * vy - uy * vx

        val len = sqrt(nx * nx + ny * ny + nz * nz)
        return if (len > 1e-8f) floatArrayOf(nx / len, ny / len, nz / len) else floatArrayOf(0f, 1f, 0f)
    }

    private fun java.nio.FloatBuffer.getOrNull(index: Int, fallback: Float): Float {
        return if (index in 0 until this.limit()) this.get(index) else fallback
    }
}