@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
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
  /**
@@ -491,10 +495,24 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
491
495
  this.sceneDirty = true;
492
496
  }
493
497
 
494
- /** Used by `attachCanvas`; pass `null` to detach. */
498
+ /**
499
+ * Used by `attachCanvas`; pass `null` to detach. A canvas attached after a frame started, or after it finished,
500
+ * shows what the accumulation already holds at once, instead of staying empty until the next frame.
501
+ */
495
502
  setOutputCanvas(canvas: HTMLCanvasElement | null) {
496
503
  this.canvas = canvas;
497
504
  this.context = null;
505
+
506
+ const gpu = this.gpu, accumulator = this.accumulator;
507
+
508
+ if (!canvas || !gpu || !accumulator || this.disposed) return;
509
+
510
+ this.configureCanvas(gpu, accumulator.width, accumulator.height);
511
+
512
+ const encoder = gpu.device.createCommandEncoder();
513
+
514
+ this.encodePresent(encoder, accumulator.width, accumulator.height);
515
+ gpu.device.queue.submit([encoder.finish()]);
498
516
  }
499
517
 
500
518
  /**
@@ -873,7 +891,8 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
873
891
  // Portals need windows: thin glass, which is what a physical material with transmission and no thickness is.
874
892
  this.current.globalIllumination.portals !== false && materials.some(material =>
875
893
  material.type === "MeshPhysicalMaterial" && Number(material.values.transmission) > 0
876
- && !(Number(material.values.thickness) > 0)));
894
+ && !(Number(material.values.thickness) > 0)),
895
+ materials.some(material => grassOf(material) !== null));
877
896
 
878
897
  // A procedural sky stands in for the renderer's own environment, as it does in the CPU renderer.
879
898
  const environment = assets.sky?.environment ?? image;
@@ -984,6 +1003,10 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
984
1003
 
985
1004
  const volumetric = packVolumetricLights(scene.lights);
986
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
+
987
1010
  const textures = writeTextures(plan, new Uint32Array(extra));
988
1011
  const bvh = await this.bvh.build(geometry.positions, geometry.triangleCount);
989
1012
 
@@ -1009,6 +1032,8 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
1009
1032
  volumetricData,
1010
1033
  volumetricCount: Math.min(volumetric.length / VOLUMETRIC_WORDS, 8),
1011
1034
  volumes: volumeTriangles(geometry),
1035
+ grassShells: grass?.shells ?? GRASS_NONE,
1036
+ grassEntries: grass?.entries ?? 0,
1012
1037
  };
1013
1038
  }
1014
1039
 
@@ -1328,6 +1353,8 @@ export class GpuRenderer extends EventEmitter<GpuRendererEvents> {
1328
1353
  pixelCount: width * height,
1329
1354
  volumetricCount: target?.volumetricCount ?? 0,
1330
1355
  volumetricData: target?.volumetricData ?? 0,
1356
+ grassShells: target?.grassShells ?? GRASS_NONE,
1357
+ grassEntries: target?.grassEntries ?? 0,
1331
1358
  };
1332
1359
  }
1333
1360
 
@@ -1374,7 +1401,7 @@ async function serializeLive(scene: Scene): Promise<SerializedScene> {
1374
1401
  const renamed = new Map<Material, string>();
1375
1402
  // The snapshot keeps a material's name but not its media, so the name carries a mark to them.
1376
1403
  const named = new Map<Material, string>();
1377
- const media: { subsurface?: unknown; volume?: unknown; bevel?: unknown }[] = [];
1404
+ const media: { subsurface?: unknown; volume?: unknown; bevel?: unknown; grass?: unknown }[] = [];
1378
1405
 
1379
1406
  scene.traverse(object => {
1380
1407
  const value = (object as Object3D & { material?: Material | Material[] }).material;
@@ -1388,13 +1415,14 @@ async function serializeLive(scene: Scene): Promise<SerializedScene> {
1388
1415
  }
1389
1416
 
1390
1417
  const data = material.userData?.cpuRenderer as
1391
- { subsurface?: unknown; volume?: unknown; bevel?: unknown } | undefined;
1418
+ { subsurface?: unknown; volume?: unknown; bevel?: unknown; grass?: unknown } | undefined;
1392
1419
 
1393
1420
  if (!named.has(material)
1394
- && (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))) {
1395
1423
  named.set(material, material.name);
1396
1424
  material.name = `${material.name}${MEDIA_MARK}${media.length}`;
1397
- 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 });
1398
1426
  }
1399
1427
  }
1400
1428
  });
@@ -1411,7 +1439,7 @@ async function serializeLive(scene: Scene): Promise<SerializedScene> {
1411
1439
 
1412
1440
  // Materials are shared between the meshes that use them, so each one is unmarked once and seen marked no more.
1413
1441
  for (const mesh of snapshot.meshes) {
1414
- for (const material of mesh.materials as (SubsurfaceMaterial & VolumeMaterial & BevelMaterial)[]) {
1442
+ for (const material of mesh.materials as (SubsurfaceMaterial & VolumeMaterial & BevelMaterial & GrassMaterial)[]) {
1415
1443
  const at = material.name.lastIndexOf(MEDIA_MARK);
1416
1444
 
1417
1445
  if (at < 0) continue;
@@ -1422,6 +1450,7 @@ async function serializeLive(scene: Scene): Promise<SerializedScene> {
1422
1450
  if (medium.subsurface !== undefined) material.subsurface = medium.subsurface as SubsurfaceMaterial["subsurface"];
1423
1451
  if (medium.volume !== undefined) material.volume = medium.volume as VolumeMaterial["volume"];
1424
1452
  if (medium.bevel !== undefined) material.bevel = medium.bevel as BevelMaterial["bevel"];
1453
+ if (medium.grass !== undefined) material.grass = medium.grass as GrassMaterial["grass"];
1425
1454
  }
1426
1455
  }
1427
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
  }