@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.
- package/dist/frameLayout.d.ts.map +1 -1
- package/dist/frameLayout.js +2 -0
- package/dist/frameLayout.js.map +1 -1
- package/dist/grass.d.ts +106 -0
- package/dist/grass.d.ts.map +1 -0
- package/dist/grass.js +350 -0
- package/dist/grass.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/materialLayout.d.ts +6 -0
- package/dist/materialLayout.d.ts.map +1 -1
- package/dist/materialLayout.js +2 -0
- package/dist/materialLayout.js.map +1 -1
- package/dist/pathTracePass.d.ts +2 -2
- package/dist/pathTracePass.d.ts.map +1 -1
- package/dist/pathTracePass.js +11 -9
- package/dist/pathTracePass.js.map +1 -1
- package/dist/renderer.d.ts +4 -1
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +25 -4
- package/dist/renderer.js.map +1 -1
- package/dist/shaderProgram.d.ts.map +1 -1
- package/dist/shaderProgram.js +10 -1
- package/dist/shaderProgram.js.map +1 -1
- package/dist/subsurface.d.ts +2 -0
- package/dist/subsurface.d.ts.map +1 -1
- package/dist/subsurface.js +3 -2
- package/dist/subsurface.js.map +1 -1
- package/dist/wgsl/bsdf.wgsl.d.ts +1 -1
- package/dist/wgsl/bsdf.wgsl.d.ts.map +1 -1
- package/dist/wgsl/bsdf.wgsl.js +39 -3
- package/dist/wgsl/bsdf.wgsl.js.map +1 -1
- package/dist/wgsl/grass.wgsl.d.ts +14 -0
- package/dist/wgsl/grass.wgsl.d.ts.map +1 -0
- package/dist/wgsl/grass.wgsl.js +456 -0
- package/dist/wgsl/grass.wgsl.js.map +1 -0
- package/dist/wgsl/lights.wgsl.d.ts +1 -1
- package/dist/wgsl/lights.wgsl.d.ts.map +1 -1
- package/dist/wgsl/lights.wgsl.js +5 -0
- package/dist/wgsl/lights.wgsl.js.map +1 -1
- package/dist/wgsl/pathTrace.wgsl.d.ts +1 -1
- package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -1
- package/dist/wgsl/pathTrace.wgsl.js +34 -5
- package/dist/wgsl/pathTrace.wgsl.js.map +1 -1
- package/dist/wgsl/surface.wgsl.d.ts +1 -1
- package/dist/wgsl/surface.wgsl.d.ts.map +1 -1
- package/dist/wgsl/surface.wgsl.js +2 -0
- package/dist/wgsl/surface.wgsl.js.map +1 -1
- package/package.json +2 -2
- package/src/frameLayout.ts +2 -0
- package/src/grass.ts +523 -0
- package/src/index.ts +2 -0
- package/src/materialLayout.ts +7 -0
- package/src/pathTracePass.ts +11 -9
- package/src/renderer.ts +36 -7
- package/src/shaderProgram.ts +11 -1
- package/src/subsurface.ts +3 -2
- package/src/wgsl/bsdf.wgsl.ts +39 -3
- package/src/wgsl/grass.wgsl.ts +456 -0
- package/src/wgsl/lights.wgsl.ts +5 -0
- package/src/wgsl/pathTrace.wgsl.ts +34 -5
- 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
|
-
/**
|
|
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
|
|
package/src/shaderProgram.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
127
|
+
albedo: linearAlbedo(value.color),
|
|
127
128
|
anisotropy: Math.min(0.95, Math.max(-0.95, finite(value.anisotropy, 0))),
|
|
128
129
|
};
|
|
129
130
|
}
|
package/src/wgsl/bsdf.wgsl.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|