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.
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:
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.