RayTracingDocsHarness.kt
src/jvmMain/kotlin/afield/graphics/scene/fabricated/RayTracingDocsHarness.kt · 1541 lines
package and imports — 65 lines, from line 1
package afield.graphics.scene.fabricated
import afield.document.graph.Box
import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.Target
import afield.graphics.render.TexturePreviewCache
import afield.graphics.vulkan.buffers.VulkanBuffer
import afield.graphics.vulkan.device.CommandBuffer
import afield.graphics.vulkan.device.CommandPool
import afield.graphics.vulkan.device.Fence
import afield.graphics.render.AccelerationStructure
import afield.graphics.render.AccelerationStructureAabbs
import afield.graphics.render.AccelerationStructureTriangles
import afield.graphics.render.HasAccelerationStructure
import afield.graphics.render.Mesh
import afield.graphics.render.Module
import afield.graphics.render.Parameters
import afield.graphics.render.RayTracingDispatch
import afield.graphics.render.RayTracingGroups
import afield.graphics.render.RayTracingHitGroup
import afield.graphics.render.RayTracingInstance
import afield.graphics.render.RayTracingShaderStage
import afield.graphics.shader.ShaderKind
import afield.document.context.fromContext
import afield.graphics.render.parameters
import afield.graphics.render.pointer
import afield.graphics.texture.HasImageView
import afield.graphics.texture.HasImageViewArray
import afield.graphics.texture.Image
import afield.graphics.texture.Image.ImageData
import afield.graphics.texture.ImageView
import afield.graphics.texture.ImageView.ImageViewData
import afield.graphics.texture.TextureSampler
import afield.graphics.texture.image_layout_transition
import afield.graphics.vulkan.util.Camera
import afield.graphics.vulkan.util.Options
import afield.app.diagnostics.logging.lprintln
import kotlinx.coroutines.delay
import kotlinx.coroutines.yield
import afield.math.linalg.Vector3f
import org.lwjgl.system.MemoryStack
import org.lwjgl.vulkan.KHRAccelerationStructure.VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR
import org.lwjgl.vulkan.VK10.VK_FILTER_NEAREST
import org.lwjgl.vulkan.VK10.VK_FORMAT_R8G8B8A8_UNORM
import org.lwjgl.vulkan.VK10.VK_IMAGE_ASPECT_COLOR_BIT
import org.lwjgl.vulkan.VK10.VK_IMAGE_LAYOUT_GENERAL
import org.lwjgl.vulkan.VK10.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
import org.lwjgl.vulkan.VK10.VK_IMAGE_LAYOUT_UNDEFINED
import org.lwjgl.vulkan.VK10.VK_IMAGE_USAGE_SAMPLED_BIT
import org.lwjgl.vulkan.VK10.VK_IMAGE_USAGE_STORAGE_BIT
import org.lwjgl.vulkan.VK10.VK_IMAGE_USAGE_TRANSFER_SRC_BIT
import org.lwjgl.vulkan.VK10.VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
import org.lwjgl.vulkan.VK10.VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT
import org.lwjgl.vulkan.VK10.VK_SAMPLER_MIPMAP_MODE_NEAREST
import org.lwjgl.vulkan.VK12.VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT
import org.lwjgl.vulkan.VK13.VK_ACCESS_2_SHADER_SAMPLED_READ_BIT
import org.lwjgl.vulkan.VK13.VK_ACCESS_2_SHADER_WRITE_BIT
import org.lwjgl.vulkan.VK13.VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT
import org.lwjgl.vulkan.VkImageSubresourceRange
import java.io.File
import java.util.ArrayDeque
import java.util.Base64
import java.util.concurrent.atomic.AtomicReference
import javax.imageio.ImageIO
import kotlin.math.tan
The figures on Field's ray tracing pages: the StandardRendererDocsHarness shape, over the ray tracing pipeline instead of the rasterizer.
Every public render method runs a deterministic ray tracing example and captures the output to a PNG. If ray tracing is unavailable, or the module fails to build, the method throws rather than writing a stand-in image, so a page's figure is either a render or nothing. A placeholder is a valid PNG, and a build that accepts one cannot tell a capture from a picture of its own failure.
tools/check_doc_assets.py guards the other side of that: it rejects a published figure whose
pixels match the "Ray Tracing / Not Available" card a harness could once write in place of a
render on a device without VK_KHR_ray_tracing_pipeline.
From a box connected beneath a renderer, a figure is three lines:
val image = RayTracingDocsHarness.renderUvGradient("/tmp/rt-uv.png")
display("render", RayTracingDocsHarness.htmlForImage(image, "UV gradient"))
println("ray tracing example ready")
object RayTracingDocsHarness
Renders the ray tracing documentation's examples offscreen, or throws if the device cannot.
object RayTracingDocsHarness {
private const val WAIT_FRAMES = 60
private const val OUTPUT_WIDTH = 1024
private const val OUTPUT_HEIGHT = 1024
private const val CAPTURE_MAX_DIM = 2048
private const val MIRROR_BALLS_REFLECTION_BOUNCES = 2
private val liveScenes = java.util.WeakHashMap<Renderer, MutableMap<String, LiveMirrorBallsScene>>()
class LiveMirrorBallsScene internal constructor
class LiveMirrorBallsScene internal constructor(
val name: String,
private val renderer: Renderer,
private val image: Image,
private val imageView: ImageView,
private val floorVertexBuffer: VulkanBuffer,
private val sphereAabbBuffer: VulkanBuffer,
private val floorBlas: AccelerationStructure,
private val sphereBlas: AccelerationStructure,
private val tlas: AtomicReference<AccelerationStructure>,
private val rayTracingPointer: afield.graphics.render.RayTracingModulePointer,
private val blitPointer: afield.graphics.render.ModulePointer,
private val sampler: TextureSampler,
var camera: Camera,
private val spheres: MutableList<MirrorSphere>
) {
data class MirrorSphere
data class MirrorSphere(
var x: Float,
var y: Float,
var z: Float,
var scale: Float
)
private var cleaned = false
private val sphereLock = Any()
private var revision = 0L
private var builtRevision = 0L
private val startedAtNanos = System.nanoTime()
@Volatile private var animator: ((Double, LiveMirrorBallsScene) -> Unit)? = null
val sphereCount: Int
val sphereCount: Int
get() = synchronized(sphereLock) { spheres.size }
fun sphere(index: Int): MirrorSphere
fun sphere(index: Int): MirrorSphere = synchronized(sphereLock) {
require(index in spheres.indices) { "sphere index $index is out of range 0..${spheres.lastIndex}" }
spheres[index].copy()
}
fun setSphere(index: Int, x: Float, y: Float, z: Float): LiveMirrorBallsScene
fun setSphere(index: Int, x: Float, y: Float, z: Float): LiveMirrorBallsScene {
val scale = synchronized(sphereLock) {
require(index in spheres.indices) { "sphere index $index is out of range 0..${spheres.lastIndex}" }
spheres[index].scale
}
return setSphere(index, x, y, z, scale)
}
fun setSphere(index: Int, x: Float, y: Float, z: Float, scale: Float): LiveMirrorBallsScene
fun setSphere(index: Int, x: Float, y: Float, z: Float, scale: Float): LiveMirrorBallsScene {
synchronized(sphereLock) {
require(index in spheres.indices) { "sphere index $index is out of range 0..${spheres.lastIndex}" }
val s = spheres[index]
if (s.x == x && s.y == y && s.z == z && s.scale == scale) return this
s.x = x
s.y = y
s.z = z
s.scale = scale
revision++
}
return this
}
fun animate(block: (Double, LiveMirrorBallsScene) -> Unit): LiveMirrorBallsScene {
fun animate(block: (Double, LiveMirrorBallsScene) -> Unit): LiveMirrorBallsScene {
animator = block
return this
}
fun clearAnimation(): LiveMirrorBallsScene
fun clearAnimation(): LiveMirrorBallsScene {
animator = null
return this
}
fun useCamera(camera: Camera): LiveMirrorBallsScene
fun useCamera(camera: Camera): LiveMirrorBallsScene {
this.camera = camera
return this
}
suspend fun capture(outputPath: String): File
Capture the actual storage image, independently of the window's size/exposure.
suspend fun capture(outputPath: String): File {
check(!cleaned) { "Mirror scene is closed" }
waitForRendererFrames(renderer)
return captureRtImage(renderer, image, outputPath, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
}
internal fun updateTlas(command: CommandBuffer)
internal fun updateTlas(command: CommandBuffer) {
animator?.invoke((System.nanoTime() - startedAtNanos) / 1_000_000_000.0, this)
val snapshot = synchronized(sphereLock) {
if (builtRevision == revision) return
builtRevision = revision
mirrorBallsInstances(floorBlas, sphereBlas, spheres)
}
// An older frame may still traverse the previous TLAS. Keep both its storage and
// host-written instance input immutable until Field retires that frame. The static
// triangle/AABB BLASes are shared; only a changed instance list gets a new TLAS.
val next = AccelerationStructure(renderer, "$name-tlas-$builtRevision")
try {
next.buildTopLevel(command, snapshot)
} catch (t: Throwable) {
next.cleanup()
throw t
}
val previous = tlas.getAndSet(next)
renderer.cleanup += Runnable { previous.cleanup() }
}
fun cleanup()
fun cleanup() {
if (cleaned) return
cleaned = true
rayTracingPointer.disable()
blitPointer.disable()
renderer.computeList.rayTracingModules.remove(name, rayTracingPointer)
renderer.renderList.modules.remove("$name-blit", blitPointer)
liveScenes[renderer]?.remove(name, this)
renderer.cleanup += Runnable {
rayTracingPointer.current.cleanup()
blitPointer.current.cleanup()
sampler.cleanup()
imageView.cleanup()
image.cleanup()
tlas.get().cleanup()
floorBlas.cleanup()
sphereBlas.cleanup()
floorVertexBuffer.cleanup()
sphereAabbBuffer.cleanup()
}
}
}
Public render methods
suspend fun renderUvGradient
Renders a UV-gradient image using a raygen-only ray tracing pipeline.
The raygen shader writes UV coordinates directly to a storage image without calling traceRayEXT, so no acceleration structure is needed. This is the simplest possible ray tracing example: the pipeline behaves like a compute shader that happens to use the VK_KHR_ray_tracing_pipeline entry-point type.
Throws when ray tracing is not supported, rather than writing a stand-in PNG.
body — 56 lines, from line 238
suspend fun renderUvGradient(
outputPath: String,
renderer: Renderer = rendererFromContextOrDocument()
): File {
if (!renderer.device.supportsRayTracing) {
rayTracingUnavailable(outputPath, "UV Gradient")
}
val (image, imageView) = createRtOutputImage(renderer)
transitionImageToGeneral(renderer, image)
val rtName = "rt-docs-uv-${System.nanoTime()}"
val errors = mutableListOf<String>()
val p = Parameters(
renderer = renderer,
raygen = UV_GRADIENT_RAYGEN,
name = rtName,
imageResolver = { imageView },
errorHandler = { msg, _ -> errors += msg }
)
val rtModule = try {
afield.graphics.render.RayTracingModule(p)
} catch (e: Exception) {
lprintln("error,doc", "ray tracing module build failed: ${e.message}")
image.cleanup()
imageView.cleanup()
captureFailed(outputPath, "UV Gradient", "ray tracing module build failed: ${e.message}", e)
}
if (!rtModule.valid || errors.isNotEmpty()) {
lprintln("error,doc", "ray tracing module invalid: $errors")
image.cleanup()
imageView.cleanup()
captureFailed(outputPath, "UV Gradient", "ray tracing module invalid: ${summarizeShaderErrors(errors)}")
}
val pointer = rtModule.pointer()
val dispatch = RayTracingDispatch(pointer, OUTPUT_WIDTH, OUTPUT_HEIGHT)
pointer += "dispatch" to dispatch
renderer += rtName to pointer
try {
waitForRendererFrames(renderer)
return captureRtImage(renderer, image, outputPath)
} finally {
pointer.disable()
renderer.computeList.rayTracingModules.remove(rtName)
renderer.cleanup += Runnable {
rtModule.cleanup()
imageView.cleanup()
image.cleanup()
}
}
}
suspend fun renderTriangleScene
Renders a triangle scene using a full ray tracing pipeline: raygen → miss → closesthit.
The geometry is a single triangle assembled into a BLAS and wrapped in a TLAS. The raygen shader traces rays from a fixed camera position; the closesthit shader writes barycentric coordinates as RGB; the miss shader writes a dark-blue background.
Throws when ray tracing is not supported, rather than writing a stand-in PNG.
body — 73 lines, from line 306
suspend fun renderTriangleScene(
outputPath: String,
renderer: Renderer = rendererFromContextOrDocument()
): File {
if (!renderer.device.supportsRayTracing) {
rayTracingUnavailable(outputPath, "Triangle Scene")
}
val (image, imageView) = createRtOutputImage(renderer)
transitionImageToGeneral(renderer, image)
val vertexBuffer = buildTriangleVertexBuffer(renderer)
val blas = AccelerationStructure(renderer, "rt-docs-triangle-blas")
.buildBottomLevelNow(
triangles = listOf(
AccelerationStructureTriangles(
vertexBuffer = vertexBuffer,
maxVertex = 2,
primitiveCount = 1
)
)
)
val tlas = AccelerationStructure(renderer, "rt-docs-triangle-tlas")
.buildTopLevelNow(instances = listOf(RayTracingInstance(blas)))
val rtName = "rt-docs-tri-${System.nanoTime()}"
val errors = mutableListOf<String>()
val p = Parameters(
renderer = renderer,
raygen = TRIANGLE_RAYGEN,
miss = TRIANGLE_MISS,
closesthit = TRIANGLE_CLOSESTHIT,
name = rtName,
imageResolver = { imageView },
accelerationStructureResolver = { tlas },
errorHandler = { msg, _ -> errors += msg }
)
val rtModule = try {
afield.graphics.render.RayTracingModule(p)
} catch (e: Exception) {
lprintln("error,doc", "ray tracing module build failed: ${e.message}")
image.cleanup(); imageView.cleanup()
vertexBuffer.cleanup(); blas.cleanup(); tlas.cleanup()
captureFailed(outputPath, "Triangle Scene", "ray tracing module build failed: ${e.message}", e)
}
if (!rtModule.valid || errors.isNotEmpty()) {
lprintln("error,doc", "ray tracing module invalid: $errors")
image.cleanup(); imageView.cleanup()
vertexBuffer.cleanup(); blas.cleanup(); tlas.cleanup()
captureFailed(outputPath, "Triangle Scene", "ray tracing module invalid: ${summarizeShaderErrors(errors)}")
}
val pointer = rtModule.pointer()
val dispatch = RayTracingDispatch(pointer, OUTPUT_WIDTH, OUTPUT_HEIGHT)
pointer += "dispatch" to dispatch
renderer += rtName to pointer
try {
waitForRendererFrames(renderer)
return captureRtImage(renderer, image, outputPath)
} finally {
pointer.disable()
renderer.computeList.rayTracingModules.remove(rtName)
renderer.cleanup += Runnable {
rtModule.cleanup()
imageView.cleanup(); image.cleanup()
tlas.cleanup(); blas.cleanup(); vertexBuffer.cleanup()
}
}
}
suspend fun renderShaderTileScene
Renders a simple plane where checker tiles are computed in shaders.
Geometry is just two triangles (one quad). The closest-hit shader computes tile color from world hit position, so no tile mesh splitting is required.
Throws when ray tracing is not supported, rather than writing a stand-in PNG.
body — 73 lines, from line 388
suspend fun renderShaderTileScene(
outputPath: String,
renderer: Renderer = rendererFromContextOrDocument()
): File {
if (!renderer.device.supportsRayTracing) {
rayTracingUnavailable(outputPath, "Shader Tiles")
}
val (image, imageView) = createRtOutputImage(renderer)
transitionImageToGeneral(renderer, image)
val vertexBuffer = buildQuadVertexBuffer(renderer)
val blas = AccelerationStructure(renderer, "rt-docs-shader-tiles-blas")
.buildBottomLevelNow(
triangles = listOf(
AccelerationStructureTriangles(
vertexBuffer = vertexBuffer,
maxVertex = 5,
primitiveCount = 2
)
)
)
val tlas = AccelerationStructure(renderer, "rt-docs-shader-tiles-tlas")
.buildTopLevelNow(instances = listOf(RayTracingInstance(blas)))
val rtName = "rt-docs-shader-tiles-${System.nanoTime()}"
val errors = mutableListOf<String>()
val p = Parameters(
renderer = renderer,
raygen = SHADER_TILES_RAYGEN,
miss = SHADER_TILES_MISS,
closesthit = SHADER_TILES_CLOSESTHIT,
name = rtName,
imageResolver = { imageView },
accelerationStructureResolver = { tlas },
errorHandler = { msg, _ -> errors += msg }
)
val rtModule = try {
afield.graphics.render.RayTracingModule(p)
} catch (e: Exception) {
lprintln("error,doc", "ray tracing module build failed: ${e.message}")
image.cleanup(); imageView.cleanup()
vertexBuffer.cleanup(); blas.cleanup(); tlas.cleanup()
captureFailed(outputPath, "Shader Tiles", "ray tracing module build failed: ${e.message}", e)
}
if (!rtModule.valid || errors.isNotEmpty()) {
lprintln("error,doc", "ray tracing module invalid: $errors")
image.cleanup(); imageView.cleanup()
vertexBuffer.cleanup(); blas.cleanup(); tlas.cleanup()
captureFailed(outputPath, "Shader Tiles", "ray tracing module invalid: ${summarizeShaderErrors(errors)}")
}
val pointer = rtModule.pointer()
val dispatch = RayTracingDispatch(pointer, OUTPUT_WIDTH, OUTPUT_HEIGHT)
pointer += "dispatch" to dispatch
renderer += rtName to pointer
try {
waitForRendererFrames(renderer)
return captureRtImage(renderer, image, outputPath)
} finally {
pointer.disable()
renderer.computeList.rayTracingModules.remove(rtName)
renderer.cleanup += Runnable {
rtModule.cleanup()
imageView.cleanup(); image.cleanup()
tlas.cleanup(); blas.cleanup(); vertexBuffer.cleanup()
}
}
}
suspend fun renderMirrorBallsScene
Renders an Amiga Juggler-inspired scene: three reflective spheres above a checkerboard floor.
The floor uses a triangle hit group while the spheres are a procedural AABB BLAS resolved by an intersection shader. Sphere instances select their mirror-material hit group through SBT record offsets.
Throws when ray tracing is not supported, rather than writing a stand-in PNG.
body — 148 lines, from line 472
suspend fun renderMirrorBallsScene(
outputPath: String,
renderer: Renderer = rendererFromContextOrDocument(),
camera: Camera = cameraFromContextOrDefault(renderer)
): File {
if (!renderer.device.supportsRayTracing) {
rayTracingUnavailable(outputPath, "Mirror Balls")
}
val (image, imageView) = createRtOutputImage(renderer)
transitionImageToGeneral(renderer, image)
val floorVertexBuffer = buildMirrorFloorVertexBuffer(renderer)
val sphereAabbBuffer = buildSphereAabbBuffer(renderer)
val floorBlas = AccelerationStructure(renderer, "rt-docs-mirror-floor-blas")
.buildBottomLevelNow(
triangles = listOf(
AccelerationStructureTriangles(
vertexBuffer = floorVertexBuffer,
maxVertex = 5,
primitiveCount = 2
)
)
)
val sphereBlas = AccelerationStructure(renderer, "rt-docs-mirror-sphere-blas")
.buildBottomLevelNow(
aabbs = listOf(
AccelerationStructureAabbs(
aabbBuffer = sphereAabbBuffer,
primitiveCount = 1
)
)
)
val floorY = -0.62f
val sphereScale = 0.34f
val sphereY = floorY + sphereScale
val instances = listOf(
RayTracingInstance(
accelerationStructure = floorBlas,
shaderBindingTableRecordOffset = 0
),
RayTracingInstance(
accelerationStructure = sphereBlas,
transform = scaleTranslateTransform(sphereScale, 0.0f, sphereY, 2.35f),
shaderBindingTableRecordOffset = 1
),
RayTracingInstance(
accelerationStructure = sphereBlas,
transform = scaleTranslateTransform(sphereScale, -0.78f, sphereY, 1.72f),
shaderBindingTableRecordOffset = 1
),
RayTracingInstance(
accelerationStructure = sphereBlas,
transform = scaleTranslateTransform(sphereScale, 0.78f, sphereY, 1.72f),
shaderBindingTableRecordOffset = 1
)
)
val tlas = AccelerationStructure(renderer, "rt-docs-mirror-balls-tlas")
.buildTopLevelNow(instances = instances)
val rtName = "rt-docs-mirror-balls-${System.nanoTime()}"
val errors = mutableListOf<String>()
val groups = RayTracingGroups(
raygen = RayTracingShaderStage("raygen", ShaderKind.RAYGEN_SHADER, MIRROR_BALLS_RAYGEN, sourceName = "mirror_balls.rgen"),
miss = mutableListOf(
RayTracingShaderStage("miss", ShaderKind.MISS_SHADER, MIRROR_BALLS_MISS, sourceName = "mirror_balls.rmiss")
),
hit = mutableListOf(
RayTracingHitGroup(
name = "checker-floor",
closestHit = RayTracingShaderStage(
"floorClosestHit",
ShaderKind.CLOSEST_HIT_SHADER,
MIRROR_BALLS_FLOOR_CLOSESTHIT,
sourceName = "mirror_balls_floor.rchit"
)
),
RayTracingHitGroup(
name = "mirror-spheres",
closestHit = RayTracingShaderStage(
"sphereClosestHit",
ShaderKind.CLOSEST_HIT_SHADER,
MIRROR_BALLS_SPHERE_CLOSESTHIT,
sourceName = "mirror_balls_sphere.rchit"
),
intersection = RayTracingShaderStage(
"sphereIntersection",
ShaderKind.INTERSECTION_SHADER,
MIRROR_BALLS_SPHERE_INTERSECTION,
sourceName = "mirror_balls_sphere.rint"
)
)
)
)
val p = Parameters(
renderer = renderer,
rayTracingGroups = groups,
maxRayRecursionDepth = MIRROR_BALLS_REFLECTION_BOUNCES + 1,
uboResolver = { it.apply { perFrame = true } },
name = rtName,
imageResolver = { imageView },
accelerationStructureResolver = { tlas },
errorHandler = { msg, _ -> errors += msg }
)
val rtModule = try {
afield.graphics.render.RayTracingModule(p)
} catch (e: Exception) {
lprintln("error,doc", "ray tracing module build failed: ${e.message}")
image.cleanup(); imageView.cleanup()
floorVertexBuffer.cleanup(); sphereAabbBuffer.cleanup()
floorBlas.cleanup(); sphereBlas.cleanup(); tlas.cleanup()
captureFailed(outputPath, "Mirror Balls", "ray tracing module build failed: ${e.message}", e)
}
if (!rtModule.valid || errors.isNotEmpty()) {
lprintln("error,doc", "ray tracing module invalid: $errors")
image.cleanup(); imageView.cleanup()
floorVertexBuffer.cleanup(); sphereAabbBuffer.cleanup()
floorBlas.cleanup(); sphereBlas.cleanup(); tlas.cleanup()
captureFailed(outputPath, "Mirror Balls", "ray tracing module invalid: ${summarizeShaderErrors(errors)}")
}
val pointer = rtModule.pointer()
pointer.seedRayCameraUniforms({ camera }) { OUTPUT_WIDTH.toFloat() / OUTPUT_HEIGHT.toFloat() }
val dispatch = RayTracingDispatch(pointer, OUTPUT_WIDTH, OUTPUT_HEIGHT)
pointer += "dispatch" to dispatch
renderer += rtName to pointer
try {
waitForRendererFrames(renderer)
return captureRtImage(renderer, image, outputPath)
} finally {
pointer.disable()
renderer.computeList.rayTracingModules.remove(rtName)
renderer.cleanup += Runnable {
rtModule.cleanup()
imageView.cleanup(); image.cleanup()
tlas.cleanup(); floorBlas.cleanup(); sphereBlas.cleanup()
floorVertexBuffer.cleanup(); sphereAabbBuffer.cleanup()
}
}
}
fun installMirrorBallsSceneInWindow
Installs the mirror-balls ray tracing scene into the renderer window.
Unlike renderMirrorBallsScene, this does not capture or save a PNG. It creates a persistent ray tracing storage image, dispatches the ray tracing pipeline every frame through Renderer.computeList, then blits the result into the renderer's normal window render target. Call LiveMirrorBallsScene.cleanup to remove it.
body — 128 lines, from line 630
fun installMirrorBallsSceneInWindow(
renderer: Renderer = rendererFromContextOrDocument(),
camera: Camera = cameraFromContextOrDefault(renderer),
name: String = "rt-live-mirror-balls",
width: Int = renderer.window.width.coerceAtLeast(1),
height: Int = renderer.window.height.coerceAtLeast(1)
): LiveMirrorBallsScene {
require(renderer.device.supportsRayTracing) { "Device does not support VK_KHR_ray_tracing_pipeline." }
liveScenes.getOrPut(renderer) { mutableMapOf() }.remove(name)?.cleanup()
val (image, imageView) = createRtOutputImage(renderer, width, height)
transitionImageToGeneral(renderer, image)
val floorVertexBuffer = buildMirrorFloorVertexBuffer(renderer)
val sphereAabbBuffer = buildSphereAabbBuffer(renderer)
val floorBlas = AccelerationStructure(renderer, "$name-floor-blas")
.buildBottomLevelNow(
triangles = listOf(
AccelerationStructureTriangles(
vertexBuffer = floorVertexBuffer,
maxVertex = 5,
primitiveCount = 2
)
)
)
val sphereBlas = AccelerationStructure(renderer, "$name-sphere-blas")
.buildBottomLevelNow(
aabbs = listOf(
AccelerationStructureAabbs(
aabbBuffer = sphereAabbBuffer,
primitiveCount = 1
)
)
)
val floorY = -0.62f
val sphereScale = 0.34f
val sphereY = floorY + sphereScale
val sphereStates = mutableListOf(
LiveMirrorBallsScene.MirrorSphere(0.0f, sphereY, 2.35f, sphereScale),
LiveMirrorBallsScene.MirrorSphere(-0.78f, sphereY, 1.72f, sphereScale),
LiveMirrorBallsScene.MirrorSphere(0.78f, sphereY, 1.72f, sphereScale)
)
val instances = mirrorBallsInstances(floorBlas, sphereBlas, sphereStates)
val tlas = AccelerationStructure(renderer, "$name-tlas")
.buildTopLevelNow(instances = instances)
val currentTlas = AtomicReference(tlas)
val tlasBinding = object : HasAccelerationStructure {
override val vkAccelerationStructure: Long get() = currentTlas.get().vkAccelerationStructure
override fun deviceAddress(): Long = currentTlas.get().deviceAddress()
}
val errors = mutableListOf<String>()
val p = Parameters(
renderer = renderer,
rayTracingGroups = mirrorBallsGroups(),
maxRayRecursionDepth = MIRROR_BALLS_REFLECTION_BOUNCES + 1,
uboResolver = { it.apply { perFrame = true } },
name = name,
imageResolver = { imageView },
accelerationStructureResolver = { tlasBinding },
errorHandler = { msg, throwable ->
errors += msg
throwable?.let { lprintln("error,graphics", it.stackTraceToString()) }
}
)
val rtModule = afield.graphics.render.RayTracingModule(p)
if (!rtModule.valid || errors.isNotEmpty()) {
image.cleanup(); imageView.cleanup()
floorVertexBuffer.cleanup(); sphereAabbBuffer.cleanup()
floorBlas.cleanup(); sphereBlas.cleanup(); tlas.cleanup()
error("mirror-balls live ray tracing module invalid: ${summarizeShaderErrors(errors)}")
}
val layout = RtOutputLayoutTracker(image)
val rtPointer = rtModule.pointer()
rtPointer.current.before["$name-image-to-general"] = layout.transitionTarget(
VK_IMAGE_LAYOUT_GENERAL,
VK_ACCESS_2_SHADER_SAMPLED_READ_BIT,
VK_ACCESS_2_SHADER_WRITE_BIT
)
rtPointer.current.after["$name-image-to-sampled"] = layout.transitionTarget(
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
VK_ACCESS_2_SHADER_WRITE_BIT,
VK_ACCESS_2_SHADER_SAMPLED_READ_BIT
)
rtPointer += "dispatch" to RayTracingDispatch(rtPointer, width, height)
val sampler = TextureSampler(
renderer,
imageView.mipLevels,
false,
VK_FILTER_NEAREST,
VK_SAMPLER_MIPMAP_MODE_NEAREST
)
val blitPointer = installImageBlitToWindow(renderer, imageView, sampler, "$name-blit")
val liveScene = LiveMirrorBallsScene(
name = name,
renderer = renderer,
image = image,
imageView = imageView,
floorVertexBuffer = floorVertexBuffer,
sphereAabbBuffer = sphereAabbBuffer,
floorBlas = floorBlas,
sphereBlas = sphereBlas,
tlas = currentTlas,
rayTracingPointer = rtPointer,
blitPointer = blitPointer,
sampler = sampler,
camera = camera,
spheres = sphereStates
)
rtPointer.seedRayCameraUniforms({ liveScene.camera }) { width.toFloat() / height.toFloat() }
rtPointer.current.before["$name-update-tlas"] = Target { command, _ ->
liveScene.updateTlas(command)
}
renderer += name to rtPointer
return liveScene.also {
liveScenes.getOrPut(renderer) { mutableMapOf() }[name] = it
}
}
fun installMirrorBallsSceneInWindow
fun installMirrorBallsSceneInWindow(
renderer: Renderer,
name: String,
width: Int = renderer.window.width.coerceAtLeast(1),
height: Int = renderer.window.height.coerceAtLeast(1)
): LiveMirrorBallsScene = installMirrorBallsSceneInWindow(
renderer = renderer,
camera = cameraFromContextOrDefault(renderer),
name = name,
width = width,
height = height
)
fun renderMirrorBallsSceneInWindow
fun renderMirrorBallsSceneInWindow(
renderer: Renderer = rendererFromContextOrDocument(),
camera: Camera = cameraFromContextOrDefault(renderer),
name: String = "rt-live-mirror-balls",
width: Int = renderer.window.width.coerceAtLeast(1),
height: Int = renderer.window.height.coerceAtLeast(1)
): LiveMirrorBallsScene = installMirrorBallsSceneInWindow(renderer, camera, name, width, height)
fun renderMirrorBallsSceneInWindow
fun renderMirrorBallsSceneInWindow(
renderer: Renderer,
name: String,
width: Int = renderer.window.width.coerceAtLeast(1),
height: Int = renderer.window.height.coerceAtLeast(1)
): LiveMirrorBallsScene = installMirrorBallsSceneInWindow(renderer, name, width, height)
fun htmlForImage(imageFile: File, caption: String? = null): String
Wraps a captured PNG as an HTML block suitable for display(...).
fun htmlForImage(imageFile: File, caption: String? = null): String {
val encoded = Base64.getEncoder().encodeToString(imageFile.readBytes())
val captionHtml = caption?.takeIf { it.isNotBlank() }?.let {
"""<div style="margin-top:10px;color:#b8b8b8;font-size:13px;">$it</div>"""
} ?: ""
return """
<div style="padding:16px;background:#111;border:1px solid #2f2f2f;">
<img
src="data:image/png;base64,$encoded"
alt="RayTracing capture"
style="display:block;width:100%;height:auto;background:#111;"
/>
$captionHtml
</div>
""".trimIndent()
}
Internal helpers
source — 341 lines, from line 807
private fun createRtOutputImage(
renderer: Renderer,
width: Int = OUTPUT_WIDTH,
height: Int = OUTPUT_HEIGHT
): Pair<Image, ImageView> {
val data = ImageData()
.width(width)
.height(height)
.format(VK_FORMAT_R8G8B8A8_UNORM)
.usage(VK_IMAGE_USAGE_STORAGE_BIT or VK_IMAGE_USAGE_TRANSFER_SRC_BIT or VK_IMAGE_USAGE_SAMPLED_BIT)
val image = Image(renderer.device, data)
val viewData = ImageViewData().format(image.format).aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
val imageView = ImageView(renderer.device, image, viewData)
return image to imageView
}
private class RtOutputLayoutTracker(
private val image: Image
) {
private var currentLayout = VK_IMAGE_LAYOUT_GENERAL
fun transitionTarget(
nextLayout: Int,
srcAccess: Long,
dstAccess: Long
): Target = object : Target {
override fun render(command: CommandBuffer, index: Int) {
if (currentLayout == nextLayout) return
MemoryStack.stackPush().use { stack ->
val range = VkImageSubresourceRange.calloc(stack)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.baseMipLevel(0).levelCount(1).baseArrayLayer(0).layerCount(1)
image_layout_transition(
command.vkCommandBuffer,
image.vkImage,
VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
srcAccess,
dstAccess,
currentLayout,
nextLayout,
range
)
}
currentLayout = nextLayout
}
}
}
private fun installImageBlitToWindow(
renderer: Renderer,
imageView: ImageView,
sampler: TextureSampler,
name: String
): afield.graphics.render.ModulePointer {
val sourceProvider = object : HasImageViewArray {
override val imageViewArray: List<HasImageView>
get() = listOf(imageView)
}
val module = Module(parameters(renderer) {
this.name = name
color = 1
depth = 1
depthFormat = Options.defaultDepthFormat
depthTest = false
blend = false
vertex = RT_IMAGE_BLIT_VERTEX
fragment = RT_IMAGE_BLIT_FRAGMENT
textureResolver = { tex ->
if (tex.name == "sourceTex") sourceProvider to sampler else null
}
}).pointer()
val mesh = Mesh(renderer, module)
mesh.resize(3, 0, module)
module.current.meshes[name] = mesh
renderer += name to module
return module
}
private fun mirrorBallsGroups(): RayTracingGroups = RayTracingGroups(
raygen = RayTracingShaderStage("raygen", ShaderKind.RAYGEN_SHADER, MIRROR_BALLS_RAYGEN, sourceName = "mirror_balls.rgen"),
miss = mutableListOf(
RayTracingShaderStage("miss", ShaderKind.MISS_SHADER, MIRROR_BALLS_MISS, sourceName = "mirror_balls.rmiss")
),
hit = mutableListOf(
RayTracingHitGroup(
name = "checker-floor",
closestHit = RayTracingShaderStage(
"floorClosestHit",
ShaderKind.CLOSEST_HIT_SHADER,
MIRROR_BALLS_FLOOR_CLOSESTHIT,
sourceName = "mirror_balls_floor.rchit"
)
),
RayTracingHitGroup(
name = "mirror-spheres",
closestHit = RayTracingShaderStage(
"sphereClosestHit",
ShaderKind.CLOSEST_HIT_SHADER,
MIRROR_BALLS_SPHERE_CLOSESTHIT,
sourceName = "mirror_balls_sphere.rchit"
),
intersection = RayTracingShaderStage(
"sphereIntersection",
ShaderKind.INTERSECTION_SHADER,
MIRROR_BALLS_SPHERE_INTERSECTION,
sourceName = "mirror_balls_sphere.rint"
)
)
)
)
private fun afield.graphics.render.RayTracingModulePointer.seedRayCameraUniforms(
cameraProvider: () -> Camera,
aspectProvider: () -> Float
) {
current.liveUniformStructures["RayCamera"]?.let { rayCamera ->
rayCamera.bind("origin") { Vector3f(cameraProvider().at) }
rayCamera.bind("right") { Vector3f(cameraProvider().right) }
rayCamera.bind("up") { rayCameraUp(cameraProvider()) }
rayCamera.bind("forward") { Vector3f(cameraProvider().forward) }
rayCamera.bind("tanHalfFovY") { tanHalfFovY(cameraProvider()) }
rayCamera.bind("aspect") { aspectProvider().coerceAtLeast(0.0001f) }
}
}
private fun rayCameraUp(camera: Camera): Vector3f {
val right = camera.right
val forward = camera.forward
val up = Vector3f(right).cross(forward)
if (up.lengthSquared() < 1e-12f) return Vector3f(0f, 1f, 0f)
return up.normalize()
}
private fun tanHalfFovY(camera: Camera): Float {
// Projection.fov is radians (see Options.fov). This used to guess the unit with
// `if (configured > PI) toRadians(configured)`, a heuristic that existed only because the
// default was a degrees literal in a radians slot.
return tan(0.5 * camera.p.fov).toFloat()
}
private fun transitionImageToGeneral(renderer: Renderer, image: Image) {
val pool = CommandPool(renderer.device, renderer.graphQueue.queueFamilyIndex)
val cmd = CommandBuffer(pool, primary = true, oneTimeSubmit = true)
val fence = Fence(renderer.device, signaled = false)
try {
cmd.beginRecording()
MemoryStack.stackPush().use { stack ->
val range = VkImageSubresourceRange.calloc(stack)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.baseMipLevel(0).levelCount(1).baseArrayLayer(0).layerCount(1)
image_layout_transition(
cmd.vkCommandBuffer,
image.vkImage,
VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
0L,
VK_ACCESS_2_SHADER_WRITE_BIT,
VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_GENERAL,
range
)
}
cmd.endRecording()
MemoryStack.stackPush().use { stack ->
renderer.graphQueue.submit(stack.pointers(cmd.vkCommandBuffer), null, null, null, fence)
}
fence.fenceWait()
} finally {
fence.cleanup()
cmd.cleanup()
pool.cleanup()
}
}
private fun buildTriangleVertexBuffer(renderer: Renderer): VulkanBuffer {
val floats = floatArrayOf(
-0.5f, -0.5f, 0.0f, // bottom-left
0.5f, -0.5f, 0.0f, // bottom-right
0.0f, 0.5f, 0.0f // top-center
)
val buffer = VulkanBuffer(
renderer = renderer,
requestedSize = floats.size * 4L,
usage = VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR or
VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
reqMask = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT or VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
needsDeviceAddress = true
)
buffer.mapAndWrite_bytes { asFloatBuffer().put(floats) }
return buffer
}
private fun buildQuadVertexBuffer(renderer: Renderer): VulkanBuffer {
val half = 1.25f
val y = -0.2f
val zNear = 0.0f
val zFar = 2.3f
val floats = floatArrayOf(
-half, y, zNear,
half, y, zNear,
half, y, zFar,
-half, y, zNear,
half, y, zFar,
-half, y, zFar
)
val buffer = VulkanBuffer(
renderer = renderer,
requestedSize = floats.size * 4L,
usage = VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR or
VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
reqMask = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT or VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
needsDeviceAddress = true
)
buffer.mapAndWrite_bytes { asFloatBuffer().put(floats) }
return buffer
}
private fun buildMirrorFloorVertexBuffer(renderer: Renderer): VulkanBuffer {
val half = 2.7f
val y = -0.62f
val zNear = 0.2f
val zFar = 4.8f
val floats = floatArrayOf(
-half, y, zNear,
half, y, zNear,
half, y, zFar,
-half, y, zNear,
half, y, zFar,
-half, y, zFar
)
val buffer = VulkanBuffer(
renderer = renderer,
requestedSize = floats.size * 4L,
usage = VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR or
VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
reqMask = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT or VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
needsDeviceAddress = true
)
buffer.mapAndWrite_bytes { asFloatBuffer().put(floats) }
return buffer
}
private fun buildSphereAabbBuffer(renderer: Renderer): VulkanBuffer {
val floats = floatArrayOf(
-1f, -1f, -1f,
1f, 1f, 1f
)
val buffer = VulkanBuffer(
renderer = renderer,
requestedSize = floats.size * 4L,
usage = VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR or
VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
reqMask = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT or VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
needsDeviceAddress = true
)
buffer.mapAndWrite_bytes { asFloatBuffer().put(floats) }
return buffer
}
private fun scaleTranslateTransform(scale: Float, tx: Float, ty: Float, tz: Float): FloatArray = floatArrayOf(
scale, 0f, 0f, tx,
0f, scale, 0f, ty,
0f, 0f, scale, tz
)
private fun mirrorBallsInstances(
floorBlas: AccelerationStructure,
sphereBlas: AccelerationStructure,
spheres: List<LiveMirrorBallsScene.MirrorSphere>
): List<RayTracingInstance> = buildList {
add(
RayTracingInstance(
accelerationStructure = floorBlas,
shaderBindingTableRecordOffset = 0
)
)
spheres.forEach { sphere ->
add(
RayTracingInstance(
accelerationStructure = sphereBlas,
transform = scaleTranslateTransform(sphere.scale, sphere.x, sphere.y, sphere.z),
shaderBindingTableRecordOffset = 1
)
)
}
}
private suspend fun captureRtImage(
renderer: Renderer, image: Image, outputPath: String, layout: Int = VK_IMAGE_LAYOUT_GENERAL
): File {
val targetFile = File(outputPath)
targetFile.parentFile?.mkdirs()
targetFile.delete()
val DATA_IMAGE_RE = Regex("base64,([^']+)")
val html = TexturePreviewCache.capture(
renderer = renderer,
vkImage = image.vkImage,
format = image.format,
srcWidth = image.imageData.width,
srcHeight = image.imageData.height,
currentLayout = layout,
maxDim = CAPTURE_MAX_DIM
) ?: error("could not read back ray tracing output image")
val encoded = DATA_IMAGE_RE.find(html)?.groupValues?.get(1)
?: error("could not parse ray tracing output image data")
targetFile.writeBytes(Base64.getDecoder().decode(encoded))
require(targetFile.length() > 0L) { "ray tracing image readback wrote an empty PNG" }
return targetFile
}
private suspend fun waitForRendererFrames(renderer: Renderer) {
val startFrame = renderer.frameCounter
val targetFrame = startFrame + WAIT_FRAMES
val deadline = System.nanoTime() + 10_000_000_000L
var lastLogged = Long.MIN_VALUE
while (renderer.frameCounter < targetFrame) {
if (renderer.frameCounter != lastLogged) {
lastLogged = renderer.frameCounter
lprintln("info,doc", " -- RT docs waiting frame ${renderer.frameCounter}/$targetFrame --")
}
if (System.nanoTime() > deadline) {
error("timed out waiting for ray tracing frames: start=$startFrame current=${renderer.frameCounter} target=$targetFrame")
}
delay(16)
yield()
}
}
private fun summarizeShaderErrors(errors: List<String>): String {
if (errors.isEmpty()) return "shader error"
return errors.joinToString(separator = " | ")
.replace('\n', ' ')
.take(120)
}
A docs figure has no honest fallback. When a capture cannot run, the build must
stop -- not hand back a picture that looks like a render and gets committed as
one. See this object's KDoc for the four figures that reached main that way.
private fun rayTracingUnavailable(outputPath: String, scene: String): Nothing = error(
"$scene: this device has no VK_KHR_ray_tracing_pipeline, so $outputPath was not " +
"rendered. Nothing is written in its place -- run this capture on a machine with " +
"ray tracing support (Linux CI does; MoltenVK on macOS does not)."
)
As rayTracingUnavailable, but for a device that has RT and still could not render.
source — 41 lines, from line 1161
private fun captureFailed(
outputPath: String,
scene: String,
detail: String,
cause: Throwable? = null
): Nothing = throw IllegalStateException("$scene: $outputPath was not rendered -- $detail", cause)
private fun rendererFromContextOrDocument(): Renderer {
try {
return fromContext()
} catch (_: IllegalArgumentException) {
}
val start = Box.context.get()
val root = start.root
val seen = mutableSetOf<Box>()
val queue = ArrayDeque<Box>()
queue.add(root)
while (queue.isNotEmpty()) {
val box = queue.removeFirst()
if (!seen.add(box)) continue
box.properties.values.firstOrNull { it is Renderer }?.let { return it as Renderer }
box.ki.context.vars.values.firstOrNull { it.value is Renderer }?.let { return it.value as Renderer }
box.children.forEach { queue.add(it) }
}
throw IllegalArgumentException("must pass in a value for context parameter ${Renderer::class.java.name}")
}
private fun cameraFromContextOrDefault(renderer: Renderer): Camera {
try {
return fromContext()
} catch (_: IllegalArgumentException) {
}
return Camera(renderer).also {
it.lookAt(
Vector3f(0.0f, 1.1f, -2.6f),
Vector3f(0.0f, -0.2f, 2.1f),
Vector3f(0.0f, 1.0f, 0.0f)
)
it.p.fov = 0.62
}
}
GLSL shaders
private val RT_IMAGE_BLIT_VERTEX = """
#version 460
layout(location = 0) out vec2 fragUV;
void main() {
vec2 pos = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
gl_Position = vec4(pos * 2.0 - 1.0, 0.0, 1.0);
// Texel-aligned. The source is written with imageStore at gl_LaunchIDEXT.xy, so its
// row 0 is the top of the picture, same as the Y-flipped viewport's clip y = +1.
fragUV = vec2(pos.x, 1.0 - pos.y);
}
""".trimIndent()
private val RT_IMAGE_BLIT_FRAGMENT = """
#version 460
layout(location = 0) in vec2 fragUV;
layout(location = 0) out vec4 outColor;
layout(set = 0, binding = 0) uniform sampler2D sourceTex;
void main() {
outColor = texture(sourceTex, fragUV);
}
""".trimIndent()
val UV_GRADIENT_RAYGEN
Raygen that writes UV colours to a storage image (no traceRayEXT call).
val UV_GRADIENT_RAYGEN = """
#version 460
#extension GL_EXT_ray_tracing : require
layout(set = 0, binding = 0, rgba8) uniform image2D outputImage;
void main() {
ivec2 px = ivec2(gl_LaunchIDEXT.xy);
vec2 uv = (vec2(px) + 0.5) / vec2(max(gl_LaunchSizeEXT.xy, uvec2(1)));
imageStore(outputImage, px, vec4(uv, 0.2, 1.0));
}
""".trimIndent()
val TRIANGLE_RAYGEN
Raygen for the triangle scene: traces one ray per pixel.
val TRIANGLE_RAYGEN = """
#version 460
#extension GL_EXT_ray_tracing : require
layout(location = 0) rayPayloadEXT vec3 payload;
layout(set = 0, binding = 0, rgba8) uniform image2D outputImage;
layout(set = 0, binding = 1) uniform accelerationStructureEXT topLevelAS;
void main() {
ivec2 px = ivec2(gl_LaunchIDEXT.xy);
vec2 uv = (vec2(px) + 0.5) / vec2(max(gl_LaunchSizeEXT.xy, uvec2(1)));
vec3 origin = vec3(0.0, 0.0, -1.25);
vec3 dir = normalize(vec3((uv * 2.0 - 1.0) * vec2(0.7, -0.7), 1.0));
payload = vec3(0.05, 0.05, 0.12);
traceRayEXT(topLevelAS,
gl_RayFlagsOpaqueEXT,
0xFF, // cull mask
0, 0, 0, // sbt offsets / stride / miss index
origin, 0.001, dir, 100.0,
0); // payload location
imageStore(outputImage, px, vec4(payload, 1.0));
}
""".trimIndent()
val TRIANGLE_MISS
Miss shader: returns a dark-blue background.
val TRIANGLE_MISS = """
#version 460
#extension GL_EXT_ray_tracing : require
layout(location = 0) rayPayloadInEXT vec3 payload;
void main() {
payload = vec3(0.05, 0.05, 0.12);
}
""".trimIndent()
val TRIANGLE_CLOSESTHIT
Closest-hit shader: visualises barycentric coordinates as RGB.
val TRIANGLE_CLOSESTHIT = """
#version 460
#extension GL_EXT_ray_tracing : require
layout(location = 0) rayPayloadInEXT vec3 payload;
hitAttributeEXT vec2 baryCoord;
void main() {
float w = 1.0 - baryCoord.x - baryCoord.y;
payload = vec3(w, baryCoord.x, baryCoord.y);
}
""".trimIndent()
val SHADER_TILES_RAYGEN
Raygen for shader-tile scene: traces one ray per pixel toward a floor quad.
val SHADER_TILES_RAYGEN = """
#version 460
#extension GL_EXT_ray_tracing : require
layout(location = 0) rayPayloadEXT vec3 payload;
layout(set = 0, binding = 0, rgba8) uniform image2D outputImage;
layout(set = 0, binding = 1) uniform accelerationStructureEXT topLevelAS;
void main() {
ivec2 px = ivec2(gl_LaunchIDEXT.xy);
vec2 uv = (vec2(px) + 0.5) / vec2(max(gl_LaunchSizeEXT.xy, uvec2(1)));
vec3 origin = vec3(0.0, 1.4, -1.6);
vec3 dir = normalize(vec3((uv * 2.0 - 1.0) * vec2(0.75, -0.55) + vec2(0.0, -0.65), 1.0));
payload = vec3(0.03, 0.04, 0.08);
traceRayEXT(topLevelAS,
gl_RayFlagsOpaqueEXT,
0xFF,
0, 0, 0,
origin, 0.001, dir, 100.0,
0);
imageStore(outputImage, px, vec4(payload, 1.0));
}
""".trimIndent()
val SHADER_TILES_MISS
Miss shader for shader-tile scene.
val SHADER_TILES_MISS = """
#version 460
#extension GL_EXT_ray_tracing : require
layout(location = 0) rayPayloadInEXT vec3 payload;
void main() {
payload = vec3(0.03, 0.04, 0.08);
}
""".trimIndent()
val SHADER_TILES_CLOSESTHIT
Closest-hit shader that computes checker tiles procedurally from world hit position.
val SHADER_TILES_CLOSESTHIT = """
#version 460
#extension GL_EXT_ray_tracing : require
layout(location = 0) rayPayloadInEXT vec3 payload;
hitAttributeEXT vec2 baryCoord;
void main() {
vec3 worldHit = gl_WorldRayOriginEXT + gl_HitTEXT * gl_WorldRayDirectionEXT;
float tileScale = 3.6;
float checker = mod(abs(floor(worldHit.x * tileScale)) + abs(floor(worldHit.z * tileScale)), 2.0);
vec3 lightTile = vec3(0.78, 0.78, 0.82);
vec3 darkTile = vec3(0.22, 0.22, 0.26);
vec3 tileColor = mix(lightTile, darkTile, checker);
float vignette = clamp(1.0 - 0.13 * length(worldHit.xz), 0.6, 1.0);
payload = tileColor * vignette;
}
""".trimIndent()
val MIRROR_BALLS_RAYGEN
Raygen for the mirror-balls scene: traces one primary ray per pixel.
body — 48 lines, from line 1361
val MIRROR_BALLS_RAYGEN = """
#version 460
#extension GL_EXT_ray_tracing : require
struct Payload {
vec3 color;
int bouncesLeft;
};
layout(location = 0) rayPayloadEXT Payload payload;
layout(set = 0, binding = 0, rgba8) uniform image2D outputImage;
layout(set = 0, binding = 1) uniform accelerationStructureEXT topLevelAS;
layout(set = 0, binding = 2) uniform RayCamera {
vec3 origin;
float tanHalfFovY;
vec3 right;
float aspect;
vec3 up;
float _pad0;
vec3 forward;
float _pad1;
} rayCamera;
void main() {
const float rayEpsilon = 0.001;
ivec2 px = ivec2(gl_LaunchIDEXT.xy);
vec2 uv = (vec2(px) + 0.5) / vec2(max(gl_LaunchSizeEXT.xy, uvec2(1)));
vec2 ndc = uv * 2.0 - 1.0;
vec3 origin = rayCamera.origin;
vec3 dir = normalize(rayCamera.forward
- ndc.x * rayCamera.aspect * rayCamera.tanHalfFovY * rayCamera.right
- ndc.y * rayCamera.tanHalfFovY * rayCamera.up);
payload.color = vec3(0.02, 0.03, 0.05);
payload.bouncesLeft = $MIRROR_BALLS_REFLECTION_BOUNCES;
traceRayEXT(topLevelAS,
gl_RayFlagsOpaqueEXT,
0xFF,
0, 0, 0,
origin, rayEpsilon, dir, 100.0,
0);
vec3 color = pow(clamp(payload.color, vec3(0.0), vec3(1.0)), vec3(0.95));
imageStore(outputImage, px, vec4(color, 1.0));
}
""".trimIndent()
val MIRROR_BALLS_MISS
Miss shader for the mirror-balls scene.
val MIRROR_BALLS_MISS = """
#version 460
#extension GL_EXT_ray_tracing : require
struct Payload {
vec3 color;
int bouncesLeft;
};
layout(location = 0) rayPayloadInEXT Payload payload;
void main() {
vec3 d = normalize(gl_WorldRayDirectionEXT);
float horizon = clamp(0.5 + 0.5 * d.y, 0.0, 1.0);
vec3 skyLow = vec3(0.06, 0.07, 0.10);
vec3 skyHigh = vec3(0.22, 0.24, 0.30);
payload.color = mix(skyLow, skyHigh, pow(horizon, 0.8));
}
""".trimIndent()
val MIRROR_BALLS_FLOOR_CLOSESTHIT
Closest-hit shader for the checkerboard floor.
val MIRROR_BALLS_FLOOR_CLOSESTHIT = """
#version 460
#extension GL_EXT_ray_tracing : require
struct Payload {
vec3 color;
int bouncesLeft;
};
layout(location = 0) rayPayloadInEXT Payload payload;
hitAttributeEXT vec2 baryCoord;
void main() {
vec3 worldHit = gl_WorldRayOriginEXT + gl_HitTEXT * gl_WorldRayDirectionEXT;
const float checkerFrequency = 2.3;
float checker = mod(floor(worldHit.x * checkerFrequency) + floor(worldHit.z * checkerFrequency), 2.0);
vec3 lightTile = vec3(0.80, 0.80, 0.84);
vec3 darkTile = vec3(0.11, 0.11, 0.13);
vec3 tileColor = mix(lightTile, darkTile, checker);
float distanceFade = clamp(1.15 - 0.12 * length(worldHit.xz - vec2(0.0, 2.0)), 0.48, 1.0);
float stripe = 0.85 + 0.15 * smoothstep(0.0, 0.04, abs(fract(worldHit.x * checkerFrequency) - 0.5));
payload.color = tileColor * distanceFade * stripe;
}
""".trimIndent()
val MIRROR_BALLS_SPHERE_INTERSECTION
Analytic intersection shader for a unit sphere inside the procedural AABB BLAS.
val MIRROR_BALLS_SPHERE_INTERSECTION = """
#version 460
#extension GL_EXT_ray_tracing : require
hitAttributeEXT vec3 sphereNormal;
void main() {
const float rayEpsilon = 0.001;
vec3 origin = gl_ObjectRayOriginEXT;
vec3 dir = gl_ObjectRayDirectionEXT;
// Solve a*t^2 + b*t + c = 0 for a ray against a unit sphere at the origin.
float a = dot(dir, dir);
float b = 2.0 * dot(origin, dir);
float c = dot(origin, origin) - 1.0;
float disc = b * b - 4.0 * a * c;
if (disc < 0.0) return;
float rootDisc = sqrt(disc);
float invDenom = 0.5 / a;
float t0 = (-b - rootDisc) * invDenom;
float t1 = (-b + rootDisc) * invDenom;
float t = t0;
if (t <= rayEpsilon) t = t1;
if (t <= rayEpsilon) return;
vec3 hit = origin + t * dir;
sphereNormal = normalize(hit);
reportIntersectionEXT(t, 0);
}
""".trimIndent()
val MIRROR_BALLS_SPHERE_CLOSESTHIT
Closest-hit shader for reflective spheres.
body — 48 lines, from line 1493
val MIRROR_BALLS_SPHERE_CLOSESTHIT = """
#version 460
#extension GL_EXT_ray_tracing : require
struct Payload {
vec3 color;
int bouncesLeft;
};
layout(location = 0) rayPayloadInEXT Payload payload;
layout(set = 0, binding = 1) uniform accelerationStructureEXT topLevelAS;
hitAttributeEXT vec3 sphereNormal;
void main() {
const float rayEpsilon = 0.001;
vec3 worldHit = gl_WorldRayOriginEXT + gl_HitTEXT * gl_WorldRayDirectionEXT;
vec3 normal = normalize(vec3(gl_ObjectToWorldEXT * vec4(sphereNormal, 0.0)));
vec3 viewDir = normalize(-gl_WorldRayDirectionEXT);
vec3 reflected = reflect(gl_WorldRayDirectionEXT, normal);
vec3 skyTint = mix(vec3(0.16, 0.18, 0.22), vec3(0.55, 0.57, 0.62), clamp(0.5 + 0.5 * reflected.y, 0.0, 1.0));
float fresnel = pow(1.0 - max(dot(normal, viewDir), 0.0), 5.0);
vec3 baseMetal = vec3(0.72, 0.74, 0.78);
vec3 fallback = mix(baseMetal, skyTint, 0.75);
if (payload.bouncesLeft <= 0) {
payload.color = fallback;
return;
}
payload.bouncesLeft -= 1;
payload.color = fallback;
traceRayEXT(topLevelAS,
gl_RayFlagsOpaqueEXT,
0xFF,
0, 0, 0,
worldHit + normal * 0.01,
rayEpsilon,
reflected,
100.0,
0);
vec3 reflectedColor = payload.color;
payload.color = mix(baseMetal * 0.55, reflectedColor, 0.88 + 0.12 * fresnel);
}
""".trimIndent()
}