@tinypoly/gpu-renderer 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/frameLayout.d.ts.map +1 -1
  2. package/dist/frameLayout.js +2 -0
  3. package/dist/frameLayout.js.map +1 -1
  4. package/dist/grass.d.ts +106 -0
  5. package/dist/grass.d.ts.map +1 -0
  6. package/dist/grass.js +350 -0
  7. package/dist/grass.js.map +1 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/materialLayout.d.ts +6 -0
  13. package/dist/materialLayout.d.ts.map +1 -1
  14. package/dist/materialLayout.js +2 -0
  15. package/dist/materialLayout.js.map +1 -1
  16. package/dist/pathTracePass.d.ts +2 -2
  17. package/dist/pathTracePass.d.ts.map +1 -1
  18. package/dist/pathTracePass.js +11 -9
  19. package/dist/pathTracePass.js.map +1 -1
  20. package/dist/renderer.d.ts +4 -1
  21. package/dist/renderer.d.ts.map +1 -1
  22. package/dist/renderer.js +25 -4
  23. package/dist/renderer.js.map +1 -1
  24. package/dist/shaderProgram.d.ts.map +1 -1
  25. package/dist/shaderProgram.js +10 -1
  26. package/dist/shaderProgram.js.map +1 -1
  27. package/dist/subsurface.d.ts +2 -0
  28. package/dist/subsurface.d.ts.map +1 -1
  29. package/dist/subsurface.js +3 -2
  30. package/dist/subsurface.js.map +1 -1
  31. package/dist/wgsl/bsdf.wgsl.d.ts +1 -1
  32. package/dist/wgsl/bsdf.wgsl.d.ts.map +1 -1
  33. package/dist/wgsl/bsdf.wgsl.js +39 -3
  34. package/dist/wgsl/bsdf.wgsl.js.map +1 -1
  35. package/dist/wgsl/grass.wgsl.d.ts +14 -0
  36. package/dist/wgsl/grass.wgsl.d.ts.map +1 -0
  37. package/dist/wgsl/grass.wgsl.js +456 -0
  38. package/dist/wgsl/grass.wgsl.js.map +1 -0
  39. package/dist/wgsl/lights.wgsl.d.ts +1 -1
  40. package/dist/wgsl/lights.wgsl.d.ts.map +1 -1
  41. package/dist/wgsl/lights.wgsl.js +5 -0
  42. package/dist/wgsl/lights.wgsl.js.map +1 -1
  43. package/dist/wgsl/pathTrace.wgsl.d.ts +1 -1
  44. package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -1
  45. package/dist/wgsl/pathTrace.wgsl.js +34 -5
  46. package/dist/wgsl/pathTrace.wgsl.js.map +1 -1
  47. package/dist/wgsl/surface.wgsl.d.ts +1 -1
  48. package/dist/wgsl/surface.wgsl.d.ts.map +1 -1
  49. package/dist/wgsl/surface.wgsl.js +2 -0
  50. package/dist/wgsl/surface.wgsl.js.map +1 -1
  51. package/package.json +2 -2
  52. package/src/frameLayout.ts +2 -0
  53. package/src/grass.ts +523 -0
  54. package/src/index.ts +2 -0
  55. package/src/materialLayout.ts +7 -0
  56. package/src/pathTracePass.ts +11 -9
  57. package/src/renderer.ts +36 -7
  58. package/src/shaderProgram.ts +11 -1
  59. package/src/subsurface.ts +3 -2
  60. package/src/wgsl/bsdf.wgsl.ts +39 -3
  61. package/src/wgsl/grass.wgsl.ts +456 -0
  62. package/src/wgsl/lights.wgsl.ts +5 -0
  63. package/src/wgsl/pathTrace.wgsl.ts +34 -5
  64. package/src/wgsl/surface.wgsl.ts +2 -0
@@ -0,0 +1,456 @@
1
+ /**
2
+ * Grass grown on a material's surface: see grass.ts for the layout of what the texture buffer holds.
3
+ *
4
+ * A triangle that grows grass is a shell, its footprint on the ground times the height of the lawn above its plane, and
5
+ * the shells have a hierarchy of their own. Inside a shell the ray walks the world's tiles front to back, and in each
6
+ * one it is taken into the space of that tile's patch, where the plane is the ground and blades stand on it: a shear
7
+ * that keeps the ray's distances as they are. The search in a tile is held to the stretch of the ray over that tile and
8
+ * that triangle, so a blade that leans over a border is seen once, from the patch it is drawn in, and the first tile
9
+ * with a hit holds the closest one in the shell.
10
+ */
11
+ export const GRASS_WGSL = /* wgsl */ `
12
+ const GRASS_NONE: u32 = 0xffffffffu;
13
+ const GRASS_MAX_TILES: u32 = 48u;
14
+ // What a hit on a blade carries as its triangle, which no scene triangle can be.
15
+ const GRASS_TRIANGLE: u32 = 0xfffffffeu;
16
+
17
+ struct GrassHit {
18
+ found: bool,
19
+ distance: f32,
20
+ // The lawn's record and the patch triangle hit, both as offsets into the texture buffer.
21
+ record: u32,
22
+ triangle: u32,
23
+ u: f32,
24
+ v: f32,
25
+ // The ground plane's slope and where the tile starts, which take the patch back to the world.
26
+ slope: vec2<f32>,
27
+ tile: vec2<f32>,
28
+ };
29
+
30
+ fn noGrass(maxDistance: f32) -> GrassHit {
31
+ var hit: GrassHit;
32
+ hit.found = false;
33
+ hit.distance = maxDistance;
34
+
35
+ return hit;
36
+ }
37
+
38
+ fn grassNode(at: u32) -> BvhNode {
39
+ var node: BvhNode;
40
+ node.leftLow = vec3<f32>(extraFloat(at), extraFloat(at + 1u), extraFloat(at + 2u));
41
+ node.leftLink = bitcast<i32>(textures[at + 3u]);
42
+ node.leftHigh = vec3<f32>(extraFloat(at + 4u), extraFloat(at + 5u), extraFloat(at + 6u));
43
+ node.leftCount = bitcast<i32>(textures[at + 7u]);
44
+ node.rightLow = vec3<f32>(extraFloat(at + 8u), extraFloat(at + 9u), extraFloat(at + 10u));
45
+ node.rightLink = bitcast<i32>(textures[at + 11u]);
46
+ node.rightHigh = vec3<f32>(extraFloat(at + 12u), extraFloat(at + 13u), extraFloat(at + 14u));
47
+ node.rightCount = bitcast<i32>(textures[at + 15u]);
48
+
49
+ return node;
50
+ }
51
+
52
+ fn grassVec3(at: u32) -> vec3<f32> {
53
+ return vec3<f32>(extraFloat(at), extraFloat(at + 1u), extraFloat(at + 2u));
54
+ }
55
+
56
+ fn grassHash(cell: vec2<i32>) -> u32 {
57
+ return pcgHash((bitcast<u32>(cell.x) * 1597334677u) ^ pcgHash(bitcast<u32>(cell.y) * 3812015801u));
58
+ }
59
+
60
+ /** Smooth value noise in zero to one, for what changes across the lawn rather than from blade to blade. */
61
+ fn grassNoise(p: vec2<f32>) -> f32 {
62
+ let cell = floor(p);
63
+ let f = p - cell;
64
+ let s = f * f * (3.0 - 2.0 * f);
65
+ let c = vec2<i32>(cell);
66
+ let corner = vec4<f32>(
67
+ f32(grassHash(c) >> 8u), f32(grassHash(c + vec2<i32>(1, 0)) >> 8u),
68
+ f32(grassHash(c + vec2<i32>(0, 1)) >> 8u), f32(grassHash(c + vec2<i32>(1, 1)) >> 8u)) / 16777216.0;
69
+
70
+ return mix(mix(corner.x, corner.y, s.x), mix(corner.z, corner.w, s.x), s.y);
71
+ }
72
+
73
+ /** Whether a blade grows where its root stands: the lawn thins out in patches, and each blade has its own number. */
74
+ fn grassKeeps(record: u32, root: vec2<f32>, rand: f32) -> bool {
75
+ let patchiness = extraFloat(record + 22u);
76
+
77
+ if (patchiness <= 0.0) {
78
+ return true;
79
+ }
80
+
81
+ let thin = smoothstep(0.3, 0.75, grassNoise(root * extraFloat(record + 23u) + vec2<f32>(31.7, 11.3)));
82
+
83
+ return rand >= patchiness * thin;
84
+ }
85
+
86
+ /**
87
+ * The closest blade of one patch along the ray, between near and the hit so far: origin and direction already in the
88
+ * patch's space. Records the triangle in best and returns whether it found one.
89
+ */
90
+ fn tracePatch(record: u32, root: u32, tile: vec2<f32>, origin: vec3<f32>, direction: vec3<f32>, near: f32,
91
+ far: f32, best: ptr<function, GrassHit>) -> bool {
92
+ let triangles = textures[record + 3u];
93
+ let blades = textures[record + 4u];
94
+ let signs = select(vec3<f32>(-1.0), vec3<f32>(1.0), direction >= vec3<f32>(0.0));
95
+ let inverse = signs / max(abs(direction), vec3<f32>(1e-20));
96
+ var closest = far;
97
+ var found = false;
98
+ var stack: array<u32, 32>;
99
+ var depth = 0u;
100
+ var current = 0u;
101
+
102
+ loop {
103
+ let node = grassNode(root + current * 16u);
104
+ let leftEnter = enterBox(node.leftLow, node.leftHigh, origin, inverse, closest);
105
+ let rightEnter = enterBox(node.rightLow, node.rightHigh, origin, inverse, closest);
106
+ var next = NO_NODE;
107
+
108
+ for (var side = 0u; side < 2u; side = side + 1u) {
109
+ let enter = select(leftEnter, rightEnter, side == 1u);
110
+ let link = select(node.leftLink, node.rightLink, side == 1u);
111
+ let count = select(node.leftCount, node.rightCount, side == 1u);
112
+
113
+ if (enter >= 1e30) {
114
+ continue;
115
+ }
116
+
117
+ if (link >= 0) {
118
+ if (next == NO_NODE) {
119
+ next = u32(link);
120
+ } else if (depth < 32u) {
121
+ stack[depth] = u32(link);
122
+ depth = depth + 1u;
123
+ }
124
+
125
+ continue;
126
+ }
127
+
128
+ let start = u32(-1 - link);
129
+
130
+ for (var i = 0u; i < u32(count); i = i + 1u) {
131
+ let at = triangles + (start + i) * 16u;
132
+ let a = grassVec3(at);
133
+ let edge1 = grassVec3(at + 3u) - a;
134
+ let edge2 = grassVec3(at + 6u) - a;
135
+ let p = cross(direction, edge2);
136
+ let determinant = dot(edge1, p);
137
+
138
+ if (abs(determinant) < 1e-14) {
139
+ continue;
140
+ }
141
+
142
+ let invDet = 1.0 / determinant;
143
+ let offset = origin - a;
144
+ let u = dot(offset, p) * invDet;
145
+ let q = cross(offset, edge1);
146
+ let v = dot(direction, q) * invDet;
147
+ let t = dot(edge2, q) * invDet;
148
+
149
+ if (u < 0.0 || v < 0.0 || u + v > 1.0 || t <= near || t >= closest) {
150
+ continue;
151
+ }
152
+
153
+ let blade = blades + textures[at + 15u] * 4u;
154
+ let rootAt = tile + vec2<f32>(extraFloat(blade), extraFloat(blade + 1u));
155
+
156
+ if (!grassKeeps(record, rootAt, extraFloat(blade + 2u))) {
157
+ continue;
158
+ }
159
+
160
+ closest = t;
161
+ found = true;
162
+ (*best).triangle = at;
163
+ (*best).u = u;
164
+ (*best).v = v;
165
+ }
166
+ }
167
+
168
+ if (next == NO_NODE) {
169
+ if (depth == 0u) {
170
+ break;
171
+ }
172
+
173
+ depth = depth - 1u;
174
+ next = stack[depth];
175
+ }
176
+
177
+ current = next;
178
+ }
179
+
180
+ if (found) {
181
+ (*best).distance = closest;
182
+ }
183
+
184
+ return found;
185
+ }
186
+
187
+ /** Keeps the part of [range.x, range.y] where offset + rate t stays at or above zero. */
188
+ fn grassClip(range: vec2<f32>, offset: f32, rate: f32) -> vec2<f32> {
189
+ if (abs(rate) < 1e-20) {
190
+ return select(range, vec2<f32>(1.0, 0.0), offset < 0.0);
191
+ }
192
+
193
+ let at = -offset / rate;
194
+
195
+ return select(vec2<f32>(range.x, min(range.y, at)), vec2<f32>(max(range.x, at), range.y), rate > 0.0);
196
+ }
197
+
198
+ /** The edge from p to q of an anticlockwise footprint, as the ray crosses it on the ground: inside is on the left. */
199
+ fn grassEdge(range: vec2<f32>, p: vec2<f32>, q: vec2<f32>, origin: vec2<f32>, direction: vec2<f32>) -> vec2<f32> {
200
+ let e = q - p;
201
+
202
+ return grassClip(range, e.x * (origin.y - p.y) - e.y * (origin.x - p.x), e.x * direction.y - e.y * direction.x);
203
+ }
204
+
205
+ /** The grass over one triangle, against the closest hit so far. */
206
+ fn grassInShell(entry: u32, origin: vec3<f32>, direction: vec3<f32>, best: ptr<function, GrassHit>) {
207
+ let record = textures[entry];
208
+ let a = vec2<f32>(extraFloat(entry + 1u), extraFloat(entry + 2u));
209
+ let b = vec2<f32>(extraFloat(entry + 3u), extraFloat(entry + 4u));
210
+ let c = vec2<f32>(extraFloat(entry + 5u), extraFloat(entry + 6u));
211
+ let p0 = extraFloat(entry + 7u);
212
+ let slope = vec2<f32>(extraFloat(entry + 8u), extraFloat(entry + 9u));
213
+ let tile = extraFloat(record);
214
+ let top = extraFloat(record + 1u);
215
+ // The ray's height over the plane, at its start and per unit of distance.
216
+ let h0 = origin.y - p0 - slope.x * origin.x - slope.y * origin.z;
217
+ let dh = direction.y - slope.x * direction.x - slope.y * direction.z;
218
+ let ground = origin.xz;
219
+ let along = direction.xz;
220
+ var range = vec2<f32>(RAY_EPSILON, (*best).distance);
221
+
222
+ range = grassClip(range, h0, dh);
223
+ range = grassClip(range, top - h0, -dh);
224
+ range = grassEdge(range, a, b, ground, along);
225
+ range = grassEdge(range, b, c, ground, along);
226
+ range = grassEdge(range, c, a, ground, along);
227
+
228
+ if (range.x >= range.y) {
229
+ return;
230
+ }
231
+
232
+ // Amanatides and Woo over the tiles of the ground.
233
+ var cell = floor((ground + along * range.x) / tile);
234
+ let stepping = select(vec2<f32>(-1.0), vec2<f32>(1.0), along >= vec2<f32>(0.0));
235
+ let span = abs(vec2<f32>(tile) / select(along, vec2<f32>(1e-20), abs(along) < vec2<f32>(1e-20)));
236
+ var crossing = vec2<f32>(1e30);
237
+
238
+ if (abs(along.x) >= 1e-20) {
239
+ crossing.x = ((cell.x + select(0.0, 1.0, along.x > 0.0)) * tile - ground.x) / along.x;
240
+ }
241
+
242
+ if (abs(along.y) >= 1e-20) {
243
+ crossing.y = ((cell.y + select(0.0, 1.0, along.y > 0.0)) * tile - ground.y) / along.y;
244
+ }
245
+
246
+ var start = range.x;
247
+ let variants = textures[record + 2u];
248
+
249
+ for (var i = 0u; i < GRASS_MAX_TILES; i = i + 1u) {
250
+ let end = min(min(crossing.x, crossing.y), range.y);
251
+ let corner = cell * tile;
252
+ let variant = grassHash(vec2<i32>(cell)) % max(variants, 1u);
253
+ let patchOrigin = vec3<f32>(origin.x - corner.x, h0, origin.z - corner.y);
254
+ let patchDirection = vec3<f32>(direction.x, dh, direction.z);
255
+
256
+ if (end > start
257
+ && tracePatch(record, textures[record + 5u + variant], corner, patchOrigin, patchDirection, start, end, best)) {
258
+ (*best).found = true;
259
+ (*best).record = record;
260
+ (*best).slope = slope;
261
+ (*best).tile = corner;
262
+
263
+ return;
264
+ }
265
+
266
+ if (end >= range.y) {
267
+ return;
268
+ }
269
+
270
+ if (crossing.x < crossing.y) {
271
+ cell.x = cell.x + stepping.x;
272
+ start = crossing.x;
273
+ crossing.x = crossing.x + span.x;
274
+ } else {
275
+ cell.y = cell.y + stepping.y;
276
+ start = crossing.y;
277
+ crossing.y = crossing.y + span.y;
278
+ }
279
+ }
280
+ }
281
+
282
+ /** The closest blade along a ray before maxDistance, in any lawn of the scene. */
283
+ fn traceGrass(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> GrassHit {
284
+ var best = noGrass(maxDistance);
285
+
286
+ if (frame.grassShells == GRASS_NONE) {
287
+ return best;
288
+ }
289
+
290
+ let signs = select(vec3<f32>(-1.0), vec3<f32>(1.0), direction >= vec3<f32>(0.0));
291
+ let inverse = signs / max(abs(direction), vec3<f32>(1e-20));
292
+ var stack: array<u32, 32>;
293
+ var enters: array<f32, 32>;
294
+ var depth = 0u;
295
+ var current = 0u;
296
+
297
+ loop {
298
+ if (current == NO_NODE) {
299
+ if (depth == 0u) {
300
+ break;
301
+ }
302
+
303
+ depth = depth - 1u;
304
+
305
+ if (enters[depth] > best.distance) {
306
+ continue;
307
+ }
308
+
309
+ current = stack[depth];
310
+ }
311
+
312
+ let node = grassNode(frame.grassShells + current * 16u);
313
+ let leftEnter = enterBox(node.leftLow, node.leftHigh, origin, inverse, best.distance);
314
+ let rightEnter = enterBox(node.rightLow, node.rightHigh, origin, inverse, best.distance);
315
+ let leftFirst = leftEnter <= rightEnter;
316
+
317
+ current = NO_NODE;
318
+
319
+ for (var k = 0u; k < 2u; k = k + 1u) {
320
+ // The nearer child first; a leaf is searched at once, a node is visited next or waits.
321
+ let left = (k == 0u) == leftFirst;
322
+ let enter = select(rightEnter, leftEnter, left);
323
+ let link = select(node.rightLink, node.leftLink, left);
324
+ let count = select(node.rightCount, node.leftCount, left);
325
+
326
+ if (enter >= best.distance) {
327
+ continue;
328
+ }
329
+
330
+ if (link < 0) {
331
+ let first = u32(-1 - link);
332
+
333
+ for (var i = 0u; i < u32(count); i = i + 1u) {
334
+ grassInShell(frame.grassEntries + (first + i) * 12u, origin, direction, &best);
335
+ }
336
+ } else if (current == NO_NODE) {
337
+ current = u32(link);
338
+ } else if (depth < 32u) {
339
+ stack[depth] = u32(link);
340
+ enters[depth] = enter;
341
+ depth = depth + 1u;
342
+ }
343
+ }
344
+ }
345
+
346
+ return best;
347
+ }
348
+
349
+ /**
350
+ * A blade as a surface: thin, waxy and translucent, a quarter of its light let through to the far side. The colour runs
351
+ * from the root to the tip, changes from blade to blade and across the lawn, and some blades are dry.
352
+ */
353
+ fn grassSurface(hit: GrassHit, direction: vec3<f32>) -> Surface {
354
+ let at = hit.triangle;
355
+ let record = hit.record;
356
+ let w = 1.0 - hit.u - hit.v;
357
+ let height = w * extraFloat(at + 9u) + hit.u * extraFloat(at + 10u) + hit.v * extraFloat(at + 11u);
358
+ let side = w * extraFloat(at + 12u) + hit.u * extraFloat(at + 13u) + hit.v * extraFloat(at + 14u);
359
+ let blade = textures[record + 4u] + textures[at + 15u] * 4u;
360
+ let root = hit.tile + vec2<f32>(extraFloat(blade), extraFloat(blade + 1u));
361
+ let rand = extraFloat(blade + 2u);
362
+ let yaw = extraFloat(blade + 3u);
363
+ let a = grassVec3(at);
364
+ let local = cross(grassVec3(at + 3u) - a, grassVec3(at + 6u) - a);
365
+ // Normals go back through the inverse transpose of the shear.
366
+ var normal = normalize(vec3<f32>(local.x - hit.slope.x * local.y, local.y, local.z - hit.slope.y * local.y));
367
+
368
+ if (dot(normal, direction) > 0.0) {
369
+ normal = -normal;
370
+ }
371
+
372
+ let across = normalize(vec3<f32>(cos(yaw), hit.slope.x * cos(yaw) + hit.slope.y * sin(yaw), sin(yaw)));
373
+ // A blade is rounded across: its normal turns towards each edge.
374
+ var shading = normalize(normal + across * (side * 0.55));
375
+
376
+ if (dot(shading, direction) >= 0.0) {
377
+ shading = normal;
378
+ }
379
+
380
+ let variation = extraFloat(record + 21u);
381
+ let scale = extraFloat(record + 23u);
382
+ let wide = grassNoise(root * scale);
383
+ let fine = grassNoise(root * scale * 4.3 + vec2<f32>(5.2, 1.7));
384
+ var albedo = mix(grassVec3(record + 9u), grassVec3(record + 12u), smoothstep(0.0, 1.0, pow(height, 0.75)));
385
+
386
+ // Across the lawn, greener and yellower stretches; from blade to blade, lighter and darker.
387
+ albedo = albedo * mix(vec3<f32>(1.0), vec3<f32>(1.12, 1.0, 0.7), variation * (wide - 0.4));
388
+ albedo = albedo * (1.0 + variation * 0.6 * (fract(rand * 13.37) - 0.5));
389
+
390
+ let dryness = extraFloat(record + 18u);
391
+ let dry = grassVec3(record + 15u) * (0.85 + 0.3 * fract(rand * 7.77));
392
+
393
+ if (fract(rand * 31.1) < dryness * (0.4 + 1.2 * fine)) {
394
+ albedo = mix(dry * 0.8, dry, height);
395
+ } else {
396
+ // Tips dry first.
397
+ albedo = mix(albedo, dry, smoothstep(0.82, 1.0, height) * dryness * 1.5);
398
+ }
399
+
400
+ // The sheath at the root is paler and duller.
401
+ albedo = mix(albedo * 0.7 + vec3<f32>(0.03, 0.03, 0.01), albedo, smoothstep(0.0, 0.2, height));
402
+ albedo = clamp(albedo, vec3<f32>(0.0), vec3<f32>(0.9));
403
+
404
+ let translucency = extraFloat(record + 19u);
405
+ var surface: Surface;
406
+
407
+ surface.material = 0u;
408
+ surface.flags = MATERIAL_LIT | MATERIAL_PBR;
409
+ surface.kind = KIND_STANDARD;
410
+ surface.blending = BLEND_NORMAL;
411
+ surface.facing = normal;
412
+ surface.front = true;
413
+ surface.normal = shading;
414
+ surface.albedo = albedo * (1.0 - 0.5 * translucency);
415
+ // Light through a leaf is more saturated and yellower than off it.
416
+ surface.translucent = min(albedo * translucency * vec3<f32>(1.05, 1.2, 0.6), vec3<f32>(0.9));
417
+ surface.opacity = 1.0;
418
+ surface.alpha = 1.0;
419
+ surface.roughness = clamp(extraFloat(record + 20u), 0.0525, 1.0);
420
+ surface.metalness = 0.0;
421
+ surface.ior = 1.5;
422
+ surface.specularColor = vec3<f32>(0.04);
423
+ surface.specularF90 = 1.0;
424
+ surface.specularStrength = 1.0;
425
+ surface.envMap = NO_TEXTURE;
426
+ surface.envMapIntensity = 1.0;
427
+ surface.gradientMap = NO_TEXTURE;
428
+
429
+ return surface;
430
+ }
431
+ `;
432
+
433
+ /** What the kernel calls when no material grows grass: the same names, doing nothing. */
434
+ export const GRASS_STUB_WGSL = /* wgsl */ `
435
+ const GRASS_NONE: u32 = 0xffffffffu;
436
+ const GRASS_TRIANGLE: u32 = 0xfffffffeu;
437
+
438
+ struct GrassHit {
439
+ found: bool,
440
+ distance: f32,
441
+ };
442
+
443
+ fn traceGrass(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> GrassHit {
444
+ var hit: GrassHit;
445
+ hit.found = false;
446
+ hit.distance = maxDistance;
447
+
448
+ return hit;
449
+ }
450
+
451
+ fn grassSurface(hit: GrassHit, direction: vec3<f32>) -> Surface {
452
+ var surface: Surface;
453
+
454
+ return surface;
455
+ }
456
+ `;
@@ -660,6 +660,11 @@ fn lightTransmittance(position: vec3<f32>, chosen: LightSample, rng: ptr<functio
660
660
  return vec3<f32>(1.0);
661
661
  }
662
662
 
663
+ // A blade of grass on the way blocks the light.
664
+ if (GRASS && chosen.occlude && traceGrass(position, chosen.direction, chosen.distance).found) {
665
+ return vec3<f32>(0.0);
666
+ }
667
+
663
668
  if (chosen.occlude) {
664
669
  // Most blockers are plainly opaque, and for those any triangle on the way settles it: the search stops at the
665
670
  // first one found instead of looking for the closest. Anything that lets light through goes the long way below.
@@ -132,7 +132,8 @@ fn emissionWeight(hit: Hit, direction: vec3<f32>, bsdfPdf: f32, lightsOwnDirect:
132
132
  * volume, a toon ramp read all round, a clearcoat that may face what its base turns from, and transmission.
133
133
  */
134
134
  fn treeNormal(surface: Surface) -> vec3<f32> {
135
- if (surface.kind == KIND_VOLUME || surface.kind == KIND_TOON || surface.clearcoat > 0.0 || surface.transmission > 0.0) {
135
+ if (surface.kind == KIND_VOLUME || surface.kind == KIND_TOON || surface.clearcoat > 0.0 || surface.transmission > 0.0
136
+ || (GRASS && any(surface.translucent > vec3<f32>(0.0)))) {
136
137
  return vec3<f32>(0.0);
137
138
  }
138
139
 
@@ -171,7 +172,14 @@ fn directLight(
171
172
  let toon = surface.kind == KIND_TOON;
172
173
  // Three's shadow map leaves a toon mesh's own far side unshadowed; tracing through the mesh would not.
173
174
  let skip = select(NO_MATERIAL, surface.material, toon && dot(surface.normal, chosen.direction) < 0.1);
174
- let through = lightTransmittance(position, chosen, rng, skip, medium);
175
+ var shadowFrom = position;
176
+
177
+ // Light from behind a translucent surface is looked for from its far side, past the surface itself.
178
+ if (GRASS && any(surface.translucent > vec3<f32>(0.0)) && dot(chosen.direction, surface.facing) < 0.0) {
179
+ shadowFrom = position - (surface.normal + surface.facing) * RAY_EPSILON;
180
+ }
181
+
182
+ let through = lightTransmittance(shadowFrom, chosen, rng, skip, medium);
175
183
 
176
184
  if (all(through <= vec3<f32>(0.0))) {
177
185
  return vec3<f32>(0.0);
@@ -457,7 +465,20 @@ fn traceRadiance(
457
465
  }
458
466
 
459
467
  for (var step = 0u; step < MAX_STEPS + MAX_WALK; step = step + 1u) {
460
- let hit = traceScene(origin, direction, limit);
468
+ var hit = traceScene(origin, direction, limit);
469
+ // A blade of grass in front of whatever the ray hit. A walk inside a material never meets one.
470
+ var grass: GrassHit;
471
+
472
+ if (GRASS && !(SUBSURFACE_WALK && inMedium)) {
473
+ grass = traceGrass(origin, direction, hit.distance);
474
+
475
+ if (grass.found) {
476
+ hit.distance = grass.distance;
477
+ hit.triangle = GRASS_TRIANGLE;
478
+ hit.u = 0.0;
479
+ hit.v = 0.0;
480
+ }
481
+ }
461
482
 
462
483
  // A step of the walk inside a material that ends in the medium: the light scatters there, keeping each colour's
463
484
  // share of it. It needs three numbers, which a plain hash gives for a fraction of what the sequence costs.
@@ -570,7 +591,7 @@ fn traceRadiance(
570
591
  let point = origin + direction * travel;
571
592
  var m = 0u;
572
593
 
573
- if (!scattering) {
594
+ if (!scattering && !(GRASS && grass.found)) {
574
595
  m = materialIndexOf(hit.triangle);
575
596
  }
576
597
 
@@ -582,6 +603,8 @@ fn traceRadiance(
582
603
  direction = -surface.normal;
583
604
  } else if (VOLUMES && scattering) {
584
605
  surface = volumePoint(direction, volumeAlbedo, volumeG);
606
+ } else if (GRASS && grass.found) {
607
+ surface = grassSurface(grass, direction);
585
608
  } else {
586
609
 
587
610
  // A volume's boundary is not a surface: the ray goes in or out, and the medium is what it meets.
@@ -730,7 +753,8 @@ fn traceRadiance(
730
753
  if (primary) {
731
754
  alpha = 1.0;
732
755
 
733
- if (!scattering && !fogged && afterMaterial == NO_MATERIAL && materials[m].afterProgram != NO_PROGRAM) {
756
+ if (!scattering && !(GRASS && grass.found) && !fogged && afterMaterial == NO_MATERIAL
757
+ && materials[m].afterProgram != NO_PROGRAM) {
734
758
  afterMaterial = m;
735
759
  afterTriangle = hit.triangle;
736
760
  afterBary = bary;
@@ -893,6 +917,11 @@ fn traceRadiance(
893
917
  escapeIntensity = surface.envMapIntensity;
894
918
  next = scattered.direction;
895
919
  nextOrigin = position;
920
+
921
+ // Through a translucent surface, the path carries on from its far side.
922
+ if (GRASS && dot(next, surface.facing) < 0.0 && any(surface.translucent > vec3<f32>(0.0))) {
923
+ nextOrigin = point - surface.facing * RAY_EPSILON;
924
+ }
896
925
  primary = false;
897
926
  straight = false;
898
927
  bounce = bounce + 1u;
@@ -64,6 +64,8 @@ struct Surface {
64
64
  material: u32,
65
65
  // Share of the diffuse layer that scatters inside the material instead of at its surface.
66
66
  subsurface: f32,
67
+ // What a translucent surface lets through to its far side, diffusely: grass blades. Zero for everything else.
68
+ translucent: vec3<f32>,
67
69
  };
68
70
 
69
71
  /** Barycentric weights of the two trailing vertices, for a point drawn uniformly over a triangle's area. */