codecraft 0.2.0

A minimalist 3D game engine built on parts of Bevy (ECS, color) with wgpu and winit: OpenPBR materials, clustered lighting, a yakui-drawn UI, audio and gamepad haptics; its binary maps any folder, and the symbols of its Rust files, as a 3D wall of boxes
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
struct Camera {
    view_proj: mat4x4<f32>,
    light_view_proj: mat4x4<f32>,
    // Straight down over the scene, for the height field the occlusion is
    // read from.
    ao_view_proj: mat4x4<f32>,
    // xyz towards the key light, w its strength.
    light_dir: vec4<f32>,
    key_color: vec4<f32>,
    // xyz towards the rim light, w its strength.
    rim_dir: vec4<f32>,
    rim_color: vec4<f32>,
    ambient: vec4<f32>,
    // Where the camera is, for the half-angle a highlight needs.
    eye: vec4<f32>,
    // x: penumbra per unit of map depth, y: a shadow texel in world units,
    // z: a shadow texel in uv.
    shadow: vec4<f32>,
    // x: how far the occlusion hunt reaches in uv, y: world height per unit
    // of the map's depth, z: how much it darkens, w: that reach in world
    // units.
    ao: vec4<f32>,
    // x: how many local lights there are this frame.
    counts: vec4<f32>,
    // x, y: scale and bias taking log(view depth) to a cluster slice.
    // z, w: the frame in pixels.
    cluster: vec4<f32>,
};

@group(0) @binding(0)
var<uniform> camera: Camera;
@group(0) @binding(1)
var shadow_map: texture_depth_2d;
@group(0) @binding(2)
var shadow_sampler: sampler_comparison;
@group(0) @binding(3)
var ao_map: texture_depth_2d;

struct VertexInput {
    @location(0) position: vec3<f32>,
    @location(1) normal: vec3<f32>,
};

// One per drawn model: its transform, and the tint to draw it in.
struct InstanceInput {
    @location(2) model_0: vec4<f32>,
    @location(3) model_1: vec4<f32>,
    @location(4) model_2: vec4<f32>,
    @location(5) model_3: vec4<f32>,
    @location(6) color: vec4<f32>,
    // x: which material in the table.
    @location(7) material: vec4<u32>,
};

struct VertexOutput {
    @builtin(position) clip_position: vec4<f32>,
    @location(0) normal: vec3<f32>,
    @location(1) color: vec4<f32>,
    @location(2) world: vec3<f32>,
    // Flat: an index is the same for every fragment of the triangle, and
    // interpolating one would give a material that does not exist.
    @location(3) @interpolate(flat) material: u32,
};

@vertex
fn vs_main(vertex: VertexInput, instance: InstanceInput) -> VertexOutput {
    let model = mat4x4<f32>(
        instance.model_0,
        instance.model_1,
        instance.model_2,
        instance.model_3,
    );

    var out: VertexOutput;
    let world = model * vec4<f32>(vertex.position, 1.0);
    out.world = world.xyz;
    out.clip_position = camera.view_proj * world;
    // Placement is a rotation and a uniform scale, both of which keep angles,
    // so the model matrix carries normals without an inverse transpose.
    out.normal = normalize((model * vec4<f32>(vertex.normal, 0.0)).xyz);
    out.color = instance.color;
    out.material = instance.material.x;
    return out;
}

/// Taps in the blocker hunt, and in the filter over the penumbra it works out.
///
/// The filter gets more because it is what is actually seen: its answer is
/// quantised to roughly one part in this many, and over a wide penumbra that
/// quantisation is the grain. The hunt only has to land on a mean depth.
const BLOCKER_TAPS: u32 = 16u;
const SHADOW_TAPS: u32 = 32u;

/// Widest any shadow filter is allowed to open, as a fraction of the map.
///
/// A cap rather than a physical limit: taps spread over more than this stop
/// reading as a soft edge and start reading as taps.
const PENUMBRA_CEILING: f32 = 0.02;

/// One number per pixel, from an integer hash of where it is. A hash rather
/// than a gradient noise, which lays a visible weave over a still image.
fn dither(at: vec2<f32>) -> f32 {
    var h = u32(at.x) * 73856093u ^ u32(at.y) * 19349663u;
    h = h ^ (h >> 13u);
    h = h * 1274126177u;
    h = h ^ (h >> 16u);
    return f32(h) * (1.0 / 4294967296.0);
}

/// A point on a spiral: the golden angle between one tap and the next, and a
/// radius growing as the square root of the index, which keeps them evenly
/// dense rather than crowded in the middle.
///
/// `turn` rotates the whole set and `slip` slides it along its own radius,
/// both per pixel — turning alone leaves neighbouring pixels sampling the same
/// radii, and pixels that agree about where to look agree about what they find.
fn spiral(i: u32, count: u32, turn: f32, slip: f32) -> vec2<f32> {
    let index = f32(i) + slip;
    let radius = sqrt(index / f32(count));
    let angle = index * 2.3999632 + turn;
    return vec2<f32>(cos(angle), sin(angle)) * radius;
}

/// How far away, on average, whatever stands between this point and the light
/// is — over a disc of `reach` in the map's uv. Negative when nothing does.
///
/// The map is orthographic, so its depth is already proportional to distance
/// and the mean of the depths found is the depth of the mean.
fn blockers_within(
    uv: vec2<f32>,
    depth: f32,
    reach: f32,
    size: vec2<f32>,
    turn: f32,
    slip: f32,
) -> f32 {
    var total = 0.0;
    var found = 0.0;
    for (var i = 0u; i < BLOCKER_TAPS; i = i + 1u) {
        let step = spiral(i, BLOCKER_TAPS, turn, slip);
        let at = clamp(uv + step * reach, vec2<f32>(0.0), vec2<f32>(1.0));
        let there = textureLoad(shadow_map, vec2<i32>(at * size), 0);
        if (there < depth) {
            total = total + there;
            found = found + 1.0;
        }
    }
    if (found == 0.0) {
        return -1.0;
    }
    return total / found;
}

/// How much of the light reaches a point, softened by how big the light is.
///
/// Percentage-closer soft shadows. The map holds a depth, and a depth is all
/// that is needed to know how much to blur: how far a blocker stands in front
/// of the receiver, times how wide the light is. That is the penumbra, by
/// similar triangles.
///
/// The catch is that the depth has to be looked up over the right area. Hunting
/// over a fixed wide disc averages in occluders that have nothing to do with
/// the point being shaded — at the foot of a piece it finds the top of the
/// piece along with the base, splits the difference, and blurs as though the
/// whole thing were floating. So the hunt is sized from the depth as well, and
/// then run again over what the first one worked out, which is what makes a
/// shadow tighten as it approaches whatever cast it.
fn soft_shadow(uv: vec2<f32>, depth: f32, pixel: vec2<f32>) -> f32 {
    let size = vec2<f32>(textureDimensions(shadow_map));
    let turn = dither(pixel) * 6.2831853;
    // A second, unrelated number for the radial slide, so it is not the same
    // hash of the same thing.
    let slip = dither(pixel + vec2<f32>(37.0, 17.0));
    // Never below a texel: a filter narrower than the map's resolution is only
    // a slower way of drawing a hard edge.
    let texel = camera.shadow.z;
    let spread = camera.shadow.x;

    // The widest a penumbra could be here: a blocker as close to the light as
    // the map can record. The hunt starts there, because a hunt narrower than
    // the answer cannot find it.
    let widest = clamp(spread * depth, texel, PENUMBRA_CEILING);

    let blocker = blockers_within(uv, depth, widest, size, turn, slip);
    // Nothing in the way at all — most of a frame, and why this is affordable.
    if (blocker < 0.0) {
        return 1.0;
    }
    var reach = clamp(spread * max(depth - blocker, 0.0), texel, widest);

    // Again, over what that came to. Where the first hunt was already about
    // right this changes nothing; where it was too wide — which is everywhere
    // near a contact — this is what narrows it.
    let closer = blockers_within(uv, depth, reach, size, turn + 2.0, slip);
    if (closer >= 0.0) {
        reach = clamp(spread * max(depth - closer, 0.0), texel, reach);
    }

    // Filter over it, on the same spiral turned to a different phase so the
    // passes do not share tap positions. Each tap is a comparison sample, so
    // the hardware has tested and blended four texels by the time it comes
    // back, and what arrives is a fraction rather than a yes or no.
    //
    // Tapered rather than flat: a tap at the rim is the one most likely to
    // disagree with its neighbours, and weighting it down stops the estimate
    // jumping as the rim crosses an edge.
    var lit = 0.0;
    var weight = 0.0;
    for (var i = 0u; i < SHADOW_TAPS; i = i + 1u) {
        let step = spiral(i, SHADOW_TAPS, turn + 1.0, slip);
        let taper = 1.0 - f32(i) / f32(SHADOW_TAPS);
        let at = clamp(uv + step * reach, vec2<f32>(0.0), vec2<f32>(1.0));
        lit = lit + textureSampleCompare(shadow_map, shadow_sampler, at, depth) * taper;
        weight = weight + taper;
    }
    return lit / weight;
}

/// How much of the light reaches a point: 1 in the open, 0 in shadow, and a
/// penumbra between that widens with distance from whatever is casting it.
///
/// Anything outside the map is treated as lit, so the world beyond the fitted
/// volume does not get a hard black edge around it.
fn sun_visible(world: vec3<f32>, normal: vec3<f32>, pixel: vec2<f32>) -> f32 {
    let light = normalize(camera.light_dir.xyz);
    // Lifted along the normal by a texel of the map before testing, which is
    // what keeps a surface from shadowing itself — and further the more the
    // surface leans away from the light, since a texel covers a fixed width
    // but an unbounded depth across a face seen edge on. Held to a few texels
    // however grazing it is: lifting further silences the last of the acne,
    // but walks the sample out of the umbra, and a shadow that has come away
    // from what casts it will not tighten into a contact.
    let leaning = clamp(1.0 / max(dot(normal, light), 0.05), 1.0, 3.0);
    let lifted = world + normal * camera.shadow.y * 1.5 * leaning;

    let clip = camera.light_view_proj * vec4<f32>(lifted, 1.0);
    let ndc = clip.xyz / clip.w;
    if (any(abs(ndc.xy) > vec2<f32>(1.0)) || ndc.z < 0.0 || ndc.z > 1.0) {
        return 1.0;
    }
    let uv = vec2<f32>(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
    return soft_shadow(uv, ndc.z, pixel);
}

/// Taps in the occlusion hunt. Fewer than the shadow filter gets: this is a
/// slow, wide term, and its grain is hidden by everything else going on.
const AO_TAPS: u32 = 12u;

/// How much of the sky reaches a point, from the height field looked down at
/// the scene.
///
/// Everything standing higher nearby is something the sky has to get past. A
/// neighbour level with this point shuts out none of it and one standing a
/// reach above shuts out all of it, weighted by how close it is — which is
/// what darkens the board around the foot of a piece, and the inside of a
/// crease before the outside of it.
fn ambient_occlusion(world: vec3<f32>, pixel: vec2<f32>) -> f32 {
    let strength = camera.ao.z;
    if (strength <= 0.0) {
        return 1.0;
    }

    let clip = camera.ao_view_proj * vec4<f32>(world, 1.0);
    let ndc = clip.xyz / clip.w;
    if (any(abs(ndc.xy) > vec2<f32>(1.0)) || ndc.z < 0.0 || ndc.z > 1.0) {
        return 1.0;
    }
    let uv = vec2<f32>(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
    let size = vec2<f32>(textureDimensions(ao_map));

    // The same spiral for every pixel, unlike the shadow filter. A shadow
    // has a hard edge to hide and can afford the grain that dithering trades
    // for it; occlusion is a slow, wide term, so neighbouring pixels
    // disagreeing about where to look shows up as speckle across a flat face
    // and nothing is gained.
    let turn = 0.0;
    let slip = 0.5;
    let span = camera.ao.y;
    let world_reach = max(camera.ao.w, 1e-5);

    var shut = 0.0;
    var weight = 0.0;
    for (var i = 0u; i < AO_TAPS; i = i + 1u) {
        let step = spiral(i, AO_TAPS, turn, slip);
        let at = clamp(uv + step * camera.ao.x, vec2<f32>(0.0), vec2<f32>(1.0));
        let there = textureLoad(ao_map, vec2<i32>(at * size), 0);
        // A smaller depth is nearer the overhead camera, which is higher up.
        let above = (ndc.z - there) * span;
        // Square-rooted: a neighbour only slightly higher than this point
        // still takes a good deal of the sky, and a linear ramp leaves the
        // whole term looking like nothing until the geometry is extreme.
        let shading = sqrt(clamp(above / world_reach, 0.0, 1.0));
        let nearness = 1.0 - length(step);
        shut = shut + shading * nearness;
        weight = weight + nearness;
    }
    if (weight <= 0.0) {
        return 1.0;
    }
    return clamp(1.0 - strength * shut / weight, 0.0, 1.0);
}

// The radiance a light of `radiance` arriving from `towards_light` puts back
// towards the eye: the BSDF, the light, and the cosine that says a surface
// edge-on to a light catches less of it.
fn direct(basis: Basis, wiL: vec3<f32>, towards_light: vec3<f32>, radiance: vec3<f32>) -> vec3<f32> {
    if (all(radiance <= vec3<f32>(0.0))) {
        return vec3<f32>(0.0);
    }
    let woL = worldToLocal(towards_light, basis);
    // Below the horizon of the shading frame: the light is behind the
    // surface, and an opaque one does not pass it through.
    if (woL.z <= 0.0) {
        return vec3<f32>(0.0);
    }
    let eval = openpbr_bsdf_evaluate(wiL, woL);
    return eval.f * radiance * woL.z;
}

// What a local light delivers here, after its distance and its cone.
fn local_light_radiance(light: LocalLight, world: vec3<f32>) -> vec3<f32> {
    let to_light = light.position_range.xyz - world;
    let distance = length(to_light);
    let range = light.position_range.w;
    if (distance >= range || distance <= 0.0) {
        return vec3<f32>(0.0);
    }

    // Inverse square, windowed so the light actually reaches zero at its
    // range instead of merely getting small -- a light that never quite ends
    // is a light every cluster has to carry.
    let falloff = 1.0 / max(distance * distance, 1e-4);
    let window = saturate(1.0 - pow(distance / range, 4.0));
    var radiance = light.color.rgb * falloff * window * window;

    // A spot narrows it further, smoothly between the two cone angles.
    let outer = light.direction_outer.w;
    if (outer > NOT_A_CONE) {
        let towards_surface = -to_light / distance;
        let alignment = dot(towards_surface, light.direction_outer.xyz);
        let inner = light.cone.x;
        radiance = radiance * smoothstep(outer, max(inner, outer + 1e-4), alignment);
    }
    return radiance;
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    // The material, tinted by the instance: one polished dielectric serves
    // both sides of the board, and the tint is what tells them apart.
    openpbr_begin(in.material, u32(in.clip_position.x) * 1973u + u32(in.clip_position.y) * 9277u);
    // The instance's tint, after the unpack so it lands on the global the
    // lobes actually read.
    base_color = base_color * in.color.rgb;

    let towards_eye = normalize(camera.eye.xyz - in.world);
    // Two-sided: a piece's own back faces are wound away from the camera, and
    // shading them by their winding lights them from inside.
    var normal = normalize(in.normal);
    if (dot(normal, towards_eye) < 0.0) {
        normal = -normal;
    }
    let basis = makeBasis(normal);
    let wiL = worldToLocal(towards_eye, basis);

    // Lobe weights and probabilities, once for the whole shading point rather
    // than once per light.
    openpbr_prepare(wiL);

    let occlusion = ambient_occlusion(in.world, in.clip_position.xy);

    // The key light, and the only one that is shadowed.
    let key = normalize(camera.light_dir.xyz);
    let lit = sun_visible(in.world, normal, in.clip_position.xy);
    var radiance = direct(
        basis,
        wiL,
        key,
        camera.key_color.rgb * camera.light_dir.w * lit,
    );

    // The rim casts nothing. It is there to find the edge the key misses, and
    // a second shadow map to do that with would cost more than the edge.
    let rim = normalize(camera.rim_dir.xyz);
    radiance = radiance + direct(
        basis,
        wiL,
        rim,
        camera.rim_color.rgb * camera.rim_dir.w * occlusion,
    );

    // Only the lights whose reach covers the cell this fragment sits in.
    // `clip_position.w` is the view-space depth, which is what the slices are
    // cut along; the x and y cells come straight off the pixel.
    let cell_x = min(
        u32(in.clip_position.x / camera.cluster.z * f32(CLUSTER_X)),
        CLUSTER_X - 1u,
    );
    let cell_y = min(
        u32(in.clip_position.y / camera.cluster.w * f32(CLUSTER_Y)),
        CLUSTER_Y - 1u,
    );
    let slice = log(max(in.clip_position.w, 1.0e-4)) * camera.cluster.x + camera.cluster.y;
    let cell_z = min(u32(max(slice, 0.0)), CLUSTER_Z - 1u);
    let cell = cell_x + cell_y * CLUSTER_X + cell_z * CLUSTER_X * CLUSTER_Y;

    let count = cluster_counts[cell];
    let first = cell * MAX_LIGHTS_PER_CLUSTER;
    for (var i = 0u; i < count; i = i + 1u) {
        let light = lights[cluster_lights[first + i]];
        let radiance_here = local_light_radiance(light, in.world);
        if (all(radiance_here <= vec3<f32>(0.0))) {
            continue;
        }
        let towards_light = normalize(light.position_range.xyz - in.world);
        radiance = radiance + direct(basis, wiL, towards_light, radiance_here);
    }

    // Ambient, from a sky above and a ground below. A BSDF answers about one
    // direction at a time, so a whole hemisphere of light is not something it
    // can be asked; this is the same split the pass has always used, driven
    // now by the material rather than by two numbers beside it.
    let sky = 0.5 + 0.5 * normal.y;
    let ambient = camera.ambient.rgb * mix(0.5, 1.0, sky) * occlusion;
    let diffuse_albedo = base_color * base_weight * (1.0 - base_metalness);
    radiance = radiance + diffuse_albedo * ambient;

    // What the sky would look like in a mirror, weighted by how glancing the
    // view is -- the Fresnel effect, which is why a polished floor is a
    // mirror across the room and matte underfoot. A rough surface scatters
    // that reflection away, so roughness takes it back out.
    let bounced = reflect(-towards_eye, normal);
    let sky_color = camera.ambient.rgb * mix(0.3, 1.4, 0.5 + 0.5 * bounced.y);
    let fresnel = pow(1.0 - clamp(dot(normal, towards_eye), 0.0, 1.0), 5.0);
    let sharpness = 1.0 - specular_roughness;
    let tint = mix(specular_color, base_color, base_metalness);
    let reflection = sky_color
        * mix(0.04, 1.0, fresnel)
        * specular_weight
        * sharpness
        * sharpness
        * tint
        * occlusion;
    radiance = radiance + reflection;

    // A material that makes its own light.
    radiance = radiance + emission_color * emission_luminance;

    return vec4<f32>(radiance, in.color.a * geometry_opacity);
}