Meshes
A KausticsMesh is a triangle mesh on the GPU: positions, normals and triangle indices. Caustics come from how the normals turn across a surface, so a mesh needs smooth normals and enough triangles where it curves. Every triangle lands as one patch of light, so a few thousand to a few tens of thousands of triangles is typical.
#Shapes made in code
Three shapes that throw good caustics.
| Shape | Made with | Notes |
|---|---|---|
| Sphere | .sphere(radius:subdivisions:) |
Centred on the origin and made by subdividing an icosahedron, so its triangles are all about the same size. Each subdivision makes four times as many: 5, the default, gives 20,480. The default radius is 0.5. |
| Ring | .ring(radius:thickness:height:segments:rows:) |
An upright ring standing on y = 0, like a napkin ring or a wedding band: a short tube with a flat top and bottom. Lit from low down, the inside of its wall gathers light into a cardioid on the surface it stands on. |
| Grid | .grid(size:columns:rows:dynamic:) |
A flat grid in the xz plane, centred on the origin and facing up: a water surface to move. It is dynamic by default. |
let ball = try KausticsMesh.sphere(radius: 0.26, subdivisions: 6)
let ring = try KausticsMesh.ring(radius: 0.3, thickness: 0.014, height: 0.11,
segments: 720, rows: 8)#Your own meshes
From arrays:
let mesh = try KausticsMesh(
positions: positions, // [SIMD3<Float>]
normals: normals, // [SIMD3<Float>], one for each position
indices: indices) // [UInt32], three for each triangleOr from Model I/O, which uses its position and normal attributes (normals are generated if it has none) and every triangle submesh:
let mesh = try KausticsMesh(mdlMesh: mdlMesh)The initializers throw a KausticsError.invalidMesh when the arrays do not describe a triangle mesh: the positions and normals differ in number, the indices are not a whole number of triangles, or an index points past the last vertex.
#Meshes that move
A mesh made with dynamic: true can change every frame with update(positions:normals:), as a water surface does. The number of vertices must stay the same.
let size: SIMD2<Float> = [1.6, 2.6]
let mesh = try KausticsMesh.grid(size: size, columns: 112, rows: 182)
let flat = KausticsMesh.gridLayout(size: size, columns: 112, rows: 182).positions
scene.onFrame = { _, time in
var positions = flat
var normals = [SIMD3<Float>](repeating: [0, 1, 0], count: flat.count)
for i in positions.indices {
let angle = 9 * flat[i].x - 2 * Float(time)
positions[i].y = 0.01 * sin(angle)
normals[i] = simd_normalize([-0.09 * cos(angle), 1, 0])
}
mesh.update(positions: positions, normals: normals)
}The normals matter as much as the positions: they are what bends the light. For a height h(x, z) the normal is normalize(-dh/dx, 1, -dh/dz).
A dynamic mesh keeps three copies of its vertices and writes the next one each time, so keep no more than two frames in flight.
The demo’s pool sums six travelling waves in this way. See The demo app.