Skip to the content
Your own Metal pipeline

Your own Metal pipeline

KausticsRenderer computes caustics into your own Metal frame. Set its light, casters and receivers, call encode(into:) once a frame with your command buffer, and read each receiver’s light with irradiance(for:) in any pass encoded after it.

Swift
let kaustics = try KausticsRenderer(device: device)
kaustics.light = KausticsLight(position: [1, 2, 1], intensity: 6, radius: 0.03)
kaustics.casters = [
    KausticsCaster(mesh: mesh, material: .glass(ior: 1.5), transform: model)
]
kaustics.receivers = [
    .floor(size: [2, 2]),
    .wall(center: [0, 1, -1], facing: [0, 0, 1], size: [2, 2])
]

// Every frame, before the passes that use it:
kaustics.encode(into: commandBuffer)

// rgba16Float, covering the floor's rectangle
let floorLight = kaustics.irradiance(for: 0)

#Using the texture

In your receiver’s fragment shader, sample the texture at the point’s UV on the receiver (KausticsReceiver.uv(of:) does the same on the CPU) and add it to the direct light:

Metal
float3 caustic = floorLight.sample(linearSampler, uv).rgb;
float3 color = albedo * (ambient + direct * shadow + caustic);

The texture is irradiance in the light’s own units: a surface facing a light of intensity I from d away receives I / d², so caustics and direct light add up correctly. Its alpha is 0. You can also put it on a plane in SceneKit or RealityKit.

#Shadows from glass and water

Glass and water casters should still block direct light in your shadows. The light that gets through them arrives in the caustic texture instead, so treat them as opaque in your shadow map.

If you want to reuse the casters’ shadow map, it is there once encode(into:) has run:

Property What it is
shadowMap The casters’ shadow map from the light: depth32Float, perspective.
lightViewProjection World to the shadow map’s clip space.

#Settings

Property Default Meaning
resolution 1024 Texels along each receiver’s longer side, unless the receiver sets its own. Rounded up to a multiple of 32.
gain 1 Multiplies all caustic light. 1 is physically consistent with the light.
stretchLimit 48 The most a triangle may be stretched when it lands, as a multiple of its own longest side, before it is dropped as too thin to matter.

The initializer takes a Metal device, or uses the system’s default, and a shadowResolution, 1024 by default, which is the shadow map’s width and height.

#When it throws

Setting up the renderer, or a mesh, throws a KausticsError when it cannot be done:

Case Meaning
noMetalDevice There is no Metal device to draw with.
libraryUnavailable The package’s compiled shaders could not be loaded.
pipeline A pipeline could not be made.
allocation A buffer or texture could not be allocated.
invalidMesh A mesh’s arrays do not describe a triangle mesh.

#Threads and frames in flight

The renderer is not thread safe: use it from one thread. If a caster’s mesh is dynamic, keep no more than two frames in flight.