@tinypoly/gpu-renderer 0.2.1 → 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 (63) 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.map +1 -1
  21. package/dist/renderer.js +14 -3
  22. package/dist/renderer.js.map +1 -1
  23. package/dist/shaderProgram.d.ts.map +1 -1
  24. package/dist/shaderProgram.js +10 -1
  25. package/dist/shaderProgram.js.map +1 -1
  26. package/dist/subsurface.d.ts +2 -0
  27. package/dist/subsurface.d.ts.map +1 -1
  28. package/dist/subsurface.js +3 -2
  29. package/dist/subsurface.js.map +1 -1
  30. package/dist/wgsl/bsdf.wgsl.d.ts +1 -1
  31. package/dist/wgsl/bsdf.wgsl.d.ts.map +1 -1
  32. package/dist/wgsl/bsdf.wgsl.js +39 -3
  33. package/dist/wgsl/bsdf.wgsl.js.map +1 -1
  34. package/dist/wgsl/grass.wgsl.d.ts +14 -0
  35. package/dist/wgsl/grass.wgsl.d.ts.map +1 -0
  36. package/dist/wgsl/grass.wgsl.js +456 -0
  37. package/dist/wgsl/grass.wgsl.js.map +1 -0
  38. package/dist/wgsl/lights.wgsl.d.ts +1 -1
  39. package/dist/wgsl/lights.wgsl.d.ts.map +1 -1
  40. package/dist/wgsl/lights.wgsl.js +5 -0
  41. package/dist/wgsl/lights.wgsl.js.map +1 -1
  42. package/dist/wgsl/pathTrace.wgsl.d.ts +1 -1
  43. package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -1
  44. package/dist/wgsl/pathTrace.wgsl.js +34 -5
  45. package/dist/wgsl/pathTrace.wgsl.js.map +1 -1
  46. package/dist/wgsl/surface.wgsl.d.ts +1 -1
  47. package/dist/wgsl/surface.wgsl.d.ts.map +1 -1
  48. package/dist/wgsl/surface.wgsl.js +2 -0
  49. package/dist/wgsl/surface.wgsl.js.map +1 -1
  50. package/package.json +2 -2
  51. package/src/frameLayout.ts +2 -0
  52. package/src/grass.ts +523 -0
  53. package/src/index.ts +2 -0
  54. package/src/materialLayout.ts +7 -0
  55. package/src/pathTracePass.ts +11 -9
  56. package/src/renderer.ts +21 -6
  57. package/src/shaderProgram.ts +11 -1
  58. package/src/subsurface.ts +3 -2
  59. package/src/wgsl/bsdf.wgsl.ts +39 -3
  60. package/src/wgsl/grass.wgsl.ts +456 -0
  61. package/src/wgsl/lights.wgsl.ts +5 -0
  62. package/src/wgsl/pathTrace.wgsl.ts +34 -5
  63. package/src/wgsl/surface.wgsl.ts +2 -0
package/src/renderer.ts CHANGED
@@ -31,6 +31,7 @@ import {
31
31
  type SubsurfaceMaterial,
32
32
  type VolumeMaterial,
33
33
  } from "./subsurface.js";
34
+ import { GRASS_NONE, grassField, grassOf, packGrass, type GrassMaterial } from "./grass.js";
34
35
  import { containingVolume, NO_MATERIAL, volumeTriangles, type VolumeTriangles } from "./volumeContainment.js";
35
36
  import {
36
37
  CONVERGED_ERROR,
@@ -252,6 +253,9 @@ interface PackedTarget {
252
253
  volumetricCount: number;
253
254
  /** The boundaries of the scene's volumes, which say whether the camera stands inside one. */
254
255
  volumes: VolumeTriangles | null;
256
+ /** Where the lawns' hierarchy and entries start in the texture buffer: see grass.ts. */
257
+ grassShells: number;
258
+ grassEntries: number;
255
259
  }
256
260
 
257
261
  /**
@@ -887,7 +891,8 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
887
891
  // Portals need windows: thin glass, which is what a physical material with transmission and no thickness is.
888
892
  this.current.globalIllumination.portals !== false && materials.some(material =>
889
893
  material.type === "MeshPhysicalMaterial" && Number(material.values.transmission) > 0
890
- && !(Number(material.values.thickness) > 0)));
894
+ && !(Number(material.values.thickness) > 0)),
895
+ materials.some(material => grassOf(material) !== null));
891
896
 
892
897
  // A procedural sky stands in for the renderer's own environment, as it does in the CPU renderer.
893
898
  const environment = assets.sky?.environment ?? image;
@@ -998,6 +1003,10 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
998
1003
 
999
1004
  const volumetric = packVolumetricLights(scene.lights);
1000
1005
  const volumetricData = append(new Uint32Array(volumetric.buffer));
1006
+ const grass = packGrass(geometry, plan.extraOffset + extra.length);
1007
+
1008
+ if (grass) append(grass.words);
1009
+
1001
1010
  const textures = writeTextures(plan, new Uint32Array(extra));
1002
1011
  const bvh = await this.bvh.build(geometry.positions, geometry.triangleCount);
1003
1012
 
@@ -1023,6 +1032,8 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
1023
1032
  volumetricData,
1024
1033
  volumetricCount: Math.min(volumetric.length / VOLUMETRIC_WORDS, 8),
1025
1034
  volumes: volumeTriangles(geometry),
1035
+ grassShells: grass?.shells ?? GRASS_NONE,
1036
+ grassEntries: grass?.entries ?? 0,
1026
1037
  };
1027
1038
  }
1028
1039
 
@@ -1342,6 +1353,8 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
1342
1353
  pixelCount: width * height,
1343
1354
  volumetricCount: target?.volumetricCount ?? 0,
1344
1355
  volumetricData: target?.volumetricData ?? 0,
1356
+ grassShells: target?.grassShells ?? GRASS_NONE,
1357
+ grassEntries: target?.grassEntries ?? 0,
1345
1358
  };
1346
1359
  }
1347
1360
 
@@ -1388,7 +1401,7 @@ async function serializeLive(scene: Scene): Promise<SerializedScene> {
1388
1401
  const renamed = new Map<Material, string>();
1389
1402
  // The snapshot keeps a material's name but not its media, so the name carries a mark to them.
1390
1403
  const named = new Map<Material, string>();
1391
- const media: { subsurface?: unknown; volume?: unknown; bevel?: unknown }[] = [];
1404
+ const media: { subsurface?: unknown; volume?: unknown; bevel?: unknown; grass?: unknown }[] = [];
1392
1405
 
1393
1406
  scene.traverse(object => {
1394
1407
  const value = (object as Object3D & { material?: Material | Material[] }).material;
@@ -1402,13 +1415,14 @@ async function serializeLive(scene: Scene): Promise<SerializedScene> {
1402
1415
  }
1403
1416
 
1404
1417
  const data = material.userData?.cpuRenderer as
1405
- { subsurface?: unknown; volume?: unknown; bevel?: unknown } | undefined;
1418
+ { subsurface?: unknown; volume?: unknown; bevel?: unknown; grass?: unknown } | undefined;
1406
1419
 
1407
1420
  if (!named.has(material)
1408
- && (subsurfaceMedium(data?.subsurface) || volumeMedium(data?.volume) || bevelRadius(data?.bevel) > 0)) {
1421
+ && (subsurfaceMedium(data?.subsurface) || volumeMedium(data?.volume) || bevelRadius(data?.bevel) > 0
1422
+ || grassField(data?.grass))) {
1409
1423
  named.set(material, material.name);
1410
1424
  material.name = `${material.name}${MEDIA_MARK}${media.length}`;
1411
- media.push({ subsurface: data?.subsurface, volume: data?.volume, bevel: data?.bevel });
1425
+ media.push({ subsurface: data?.subsurface, volume: data?.volume, bevel: data?.bevel, grass: data?.grass });
1412
1426
  }
1413
1427
  }
1414
1428
  });
@@ -1425,7 +1439,7 @@ async function serializeLive(scene: Scene): Promise<SerializedScene> {
1425
1439
 
1426
1440
  // Materials are shared between the meshes that use them, so each one is unmarked once and seen marked no more.
1427
1441
  for (const mesh of snapshot.meshes) {
1428
- for (const material of mesh.materials as (SubsurfaceMaterial & VolumeMaterial & BevelMaterial)[]) {
1442
+ for (const material of mesh.materials as (SubsurfaceMaterial & VolumeMaterial & BevelMaterial & GrassMaterial)[]) {
1429
1443
  const at = material.name.lastIndexOf(MEDIA_MARK);
1430
1444
 
1431
1445
  if (at < 0) continue;
@@ -1436,6 +1450,7 @@ async function serializeLive(scene: Scene): Promise<SerializedScene> {
1436
1450
  if (medium.subsurface !== undefined) material.subsurface = medium.subsurface as SubsurfaceMaterial["subsurface"];
1437
1451
  if (medium.volume !== undefined) material.volume = medium.volume as VolumeMaterial["volume"];
1438
1452
  if (medium.bevel !== undefined) material.bevel = medium.bevel as BevelMaterial["bevel"];
1453
+ if (medium.grass !== undefined) material.grass = medium.grass as GrassMaterial["grass"];
1439
1454
  }
1440
1455
  }
1441
1456
 
@@ -178,6 +178,16 @@ const PATCHED_CONTEXT: [string, string][] = [
178
178
  ["vec3", "vNormal"],
179
179
  ];
180
180
 
181
+ /**
182
+ * Three's physical light function, which a hook may wrap to change its direct light (a relief shadowing its own
183
+ * joints). The integrator lights the surface, so the function is only there for the hook to compile and does nothing.
184
+ */
185
+ const PHYSICAL_LIGHT_STUB = `
186
+ struct PhysicalMaterial { vec3 diffuseColor; float roughness; vec3 specularColor; float specularF90; float dispersion; };
187
+ void RE_Direct_Physical(IncidentLight directLight, vec3 geometryPosition, vec3 geometryNormal, vec3 geometryViewDir,
188
+ vec3 geometryClearcoatNormal, PhysicalMaterial material, inout ReflectedLight reflectedLight) {}
189
+ `;
190
+
181
191
  /** Light helpers Three's hooks expect from its lighting chunks, which a patched shader no longer includes. */
182
192
  const PATCHED_HELPERS = `
183
193
  float getDistanceAttenuation(float lightDistance, float cutoffDistance, float decayExponent) {
@@ -374,7 +384,7 @@ export function prepareFragmentShader(
374
384
 
375
385
  let body = shader.fragment;
376
386
 
377
- if (kind === "patched" || kind === "after") body = `${SHADER_CHUNKS.common}\n${body}`;
387
+ if (kind === "patched" || kind === "after") body = `${SHADER_CHUNKS.common}\n${PHYSICAL_LIGHT_STUB}\n${body}`;
378
388
 
379
389
  body = unrollLoops(expandIncludes(body));
380
390
  body = stripComments(body)
package/src/subsurface.ts CHANGED
@@ -96,7 +96,8 @@ export type VolumeMaterial = SerializedMaterial & { volume?: VolumeInput };
96
96
 
97
97
  const srgbToLinear = (c: number) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
98
98
 
99
- function albedoOf(color: unknown): [number, number, number] {
99
+ /** A colour as `#rrggbb` or linear rgb, in linear rgb clamped to one; white when it reads as neither. */
100
+ export function linearAlbedo(color: unknown): [number, number, number] {
100
101
  if (Array.isArray(color) && color.length >= 3) {
101
102
  return [0, 1, 2].map(k => Math.min(1, Math.max(0, finite(color[k], 1)))) as [number, number, number];
102
103
  }
@@ -123,7 +124,7 @@ export function volumeMedium(input: unknown): VolumeMedium | null {
123
124
 
124
125
  return {
125
126
  density,
126
- albedo: albedoOf(value.color),
127
+ albedo: linearAlbedo(value.color),
127
128
  anisotropy: Math.min(0.95, Math.max(-0.95, finite(value.anisotropy, 0))),
128
129
  };
129
130
  }
@@ -181,6 +181,26 @@ fn coatTransmittance(surface: Surface, view: vec3<f32>) -> f32 {
181
181
  */
182
182
  const KIND_VOLUME: u32 = 100u;
183
183
 
184
+ /**
185
+ * Share of the diffuse lobe's directions drawn on the far side of a surface that lets light through it diffusely, as a
186
+ * grass blade does: by how much of the diffuse light each side accounts for.
187
+ */
188
+ fn translucentShare(surface: Surface) -> f32 {
189
+ if (!GRASS) {
190
+ return 0.0;
191
+ }
192
+
193
+ let through = luminanceOf(surface.translucent);
194
+
195
+ if (through <= 0.0) {
196
+ return 0.0;
197
+ }
198
+
199
+ let off = luminanceOf(surface.albedo * ((1.0 - surface.metalness) * (1.0 - surface.subsurface)));
200
+
201
+ return through / max(through + off, 1e-6);
202
+ }
203
+
184
204
  /** How often each lobe is chosen, as (diffuse, specular, clearcoat): by how much of the reflection each accounts for. */
185
205
  fn lobeWeights(surface: Surface, view: vec3<f32>) -> vec3<f32> {
186
206
  let nv = max(dot(surface.normal, view), 1e-4);
@@ -199,7 +219,7 @@ fn lobeWeights(surface: Surface, view: vec3<f32>) -> vec3<f32> {
199
219
  let base = coatTransmittance(surface, view);
200
220
  // The floors keep every lobe reachable: the diffuse one also carries the sheen and the multiple scattering term.
201
221
  let diffuse = (luminanceOf(surface.albedo * ((1.0 - surface.metalness) * (1.0 - surface.subsurface)))
202
- + max3(surface.sheenColor)) * base + 0.02;
222
+ + max3(surface.sheenColor) + select(0.0, luminanceOf(surface.translucent), GRASS)) * base + 0.02;
203
223
  let specular = luminanceOf(schlick3(surface.specularColor, surface.specularF90, nv)
204
224
  * (1.0 - surface.metalness) + surface.albedo * surface.metalness) * base + 0.05;
205
225
  let coat = select(0.0, (1.0 - base) + 0.02, surface.clearcoat > 0.0);
@@ -277,6 +297,11 @@ fn physicalValue(surface: Surface, view: vec3<f32>, light: vec3<f32>) -> vec3<f3
277
297
  }
278
298
  }
279
299
 
300
+ // Light through a translucent surface, from its far side.
301
+ if (GRASS && nl < 0.0) {
302
+ value = value + surface.translucent / PI * (-nl);
303
+ }
304
+
280
305
  return value;
281
306
  }
282
307
 
@@ -308,6 +333,10 @@ fn bsdfPdf(surface: Surface, view: vec3<f32>, light: vec3<f32>) -> f32 {
308
333
  let weights = lobeWeights(surface, view);
309
334
  var pdf = weights.x * max(nl, 0.0) / PI;
310
335
 
336
+ if (GRASS) {
337
+ pdf = weights.x * mix(max(nl, 0.0), max(-nl, 0.0), translucentShare(surface)) / PI;
338
+ }
339
+
311
340
  if (surface.kind == KIND_PHONG) {
312
341
  let half = normalize(light + view);
313
342
 
@@ -375,7 +404,7 @@ fn evaluateBsdf(surface: Surface, view: vec3<f32>, light: vec3<f32>) -> BsdfEval
375
404
  }
376
405
 
377
406
  // A clearcoat may face a light its base turns away from, so only a surface without one stops here.
378
- if (nl <= 0.0 && surface.clearcoat <= 0.0) {
407
+ if (nl <= 0.0 && surface.clearcoat <= 0.0 && !(GRASS && any(surface.translucent > vec3<f32>(0.0)))) {
379
408
  return result;
380
409
  }
381
410
 
@@ -554,7 +583,10 @@ fn sampleBsdf(surface: Surface, view: vec3<f32>, lobe: f32, u: vec2<f32>) -> Bsd
554
583
  result.diffuse = lobe < weights.x;
555
584
 
556
585
  if (lobe < weights.x) {
557
- result.direction = cosineDirection(normal, u);
586
+ // A translucent surface sends that share of its diffuse light through, to the far side.
587
+ let through = GRASS && lobe < weights.x * translucentShare(surface);
588
+
589
+ result.direction = cosineDirection(select(normal, -normal, through), u);
558
590
  } else if (surface.kind == KIND_PHONG) {
559
591
  result.direction = reflect(-view, sampleBlinnPhongHalf(normal, surface.shininess, u));
560
592
  } else if (lobe < weights.x + weights.y) {
@@ -606,6 +638,10 @@ fn diffuseValue(surface: Surface, view: vec3<f32>, light: vec3<f32>) -> vec3<f32
606
638
  return surface.albedo * henyeyGreenstein(dot(-view, light), surface.anisotropy);
607
639
  }
608
640
 
641
+ if (GRASS && nl < 0.0) {
642
+ return surface.translucent / PI * (-nl);
643
+ }
644
+
609
645
  if (nl <= 0.0) {
610
646
  return vec3<f32>(0.0);
611
647
  }
@@ -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
+ `;