@tinypoly/gpu-renderer 0.0.0-stage → 0.0.0
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/ARCHITECTURE.md +334 -0
- package/LICENSE +21 -0
- package/README.md +258 -2
- package/dist/accumulator.d.ts +27 -0
- package/dist/accumulator.d.ts.map +1 -0
- package/dist/accumulator.js +57 -0
- package/dist/accumulator.js.map +1 -0
- package/dist/bvh.d.ts +26 -0
- package/dist/bvh.d.ts.map +1 -0
- package/dist/bvh.js +207 -0
- package/dist/bvh.js.map +1 -0
- package/dist/camera.d.ts +29 -0
- package/dist/camera.d.ts.map +1 -0
- package/dist/camera.js +48 -0
- package/dist/camera.js.map +1 -0
- package/dist/canvas.d.ts +9 -0
- package/dist/canvas.d.ts.map +1 -0
- package/dist/canvas.js +11 -0
- package/dist/canvas.js.map +1 -0
- package/dist/denoise.d.ts +69 -0
- package/dist/denoise.d.ts.map +1 -0
- package/dist/denoise.js +455 -0
- package/dist/denoise.js.map +1 -0
- package/dist/device.d.ts +37 -0
- package/dist/device.d.ts.map +1 -0
- package/dist/device.js +52 -0
- package/dist/device.js.map +1 -0
- package/dist/dispatchScheduler.d.ts +43 -0
- package/dist/dispatchScheduler.d.ts.map +1 -0
- package/dist/dispatchScheduler.js +49 -0
- package/dist/dispatchScheduler.js.map +1 -0
- package/dist/environment.d.ts +39 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +168 -0
- package/dist/environment.js.map +1 -0
- package/dist/frameLayout.d.ts +4 -0
- package/dist/frameLayout.d.ts.map +1 -0
- package/dist/frameLayout.js +58 -0
- package/dist/frameLayout.js.map +1 -0
- package/dist/geometry.d.ts +75 -0
- package/dist/geometry.d.ts.map +1 -0
- package/dist/geometry.js +671 -0
- package/dist/geometry.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/lights.d.ts +64 -0
- package/dist/lights.d.ts.map +1 -0
- package/dist/lights.js +264 -0
- package/dist/lights.js.map +1 -0
- package/dist/materialLayout.d.ts +143 -0
- package/dist/materialLayout.d.ts.map +1 -0
- package/dist/materialLayout.js +297 -0
- package/dist/materialLayout.js.map +1 -0
- package/dist/math.d.ts +10 -0
- package/dist/math.d.ts.map +1 -0
- package/dist/math.js +40 -0
- package/dist/math.js.map +1 -0
- package/dist/pathTracePass.d.ts +27 -0
- package/dist/pathTracePass.d.ts.map +1 -0
- package/dist/pathTracePass.js +83 -0
- package/dist/pathTracePass.js.map +1 -0
- package/dist/planarCapture.d.ts +38 -0
- package/dist/planarCapture.d.ts.map +1 -0
- package/dist/planarCapture.js +163 -0
- package/dist/planarCapture.js.map +1 -0
- package/dist/presentPass.d.ts +31 -0
- package/dist/presentPass.d.ts.map +1 -0
- package/dist/presentPass.js +78 -0
- package/dist/presentPass.js.map +1 -0
- package/dist/renderer.d.ts +196 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +839 -0
- package/dist/renderer.js.map +1 -0
- package/dist/sceneAssets.d.ts +24 -0
- package/dist/sceneAssets.d.ts.map +1 -0
- package/dist/sceneAssets.js +77 -0
- package/dist/sceneAssets.js.map +1 -0
- package/dist/sceneUpload.d.ts +61 -0
- package/dist/sceneUpload.d.ts.map +1 -0
- package/dist/sceneUpload.js +98 -0
- package/dist/sceneUpload.js.map +1 -0
- package/dist/shaderModule.d.ts +7 -0
- package/dist/shaderModule.d.ts.map +1 -0
- package/dist/shaderModule.js +19 -0
- package/dist/shaderModule.js.map +1 -0
- package/dist/shaderProgram.d.ts +124 -0
- package/dist/shaderProgram.d.ts.map +1 -0
- package/dist/shaderProgram.js +1030 -0
- package/dist/shaderProgram.js.map +1 -0
- package/dist/structLayout.d.ts +28 -0
- package/dist/structLayout.d.ts.map +1 -0
- package/dist/structLayout.js +58 -0
- package/dist/structLayout.js.map +1 -0
- package/dist/texturePack.d.ts +54 -0
- package/dist/texturePack.d.ts.map +1 -0
- package/dist/texturePack.js +86 -0
- package/dist/texturePack.js.map +1 -0
- package/dist/upstream.d.ts +7 -0
- package/dist/upstream.d.ts.map +1 -0
- package/dist/upstream.js +11 -0
- package/dist/upstream.js.map +1 -0
- package/dist/volumetric.d.ts +10 -0
- package/dist/volumetric.d.ts.map +1 -0
- package/dist/volumetric.js +34 -0
- package/dist/volumetric.js.map +1 -0
- package/dist/wgsl/bsdf.wgsl.d.ts +13 -0
- package/dist/wgsl/bsdf.wgsl.d.ts.map +1 -0
- package/dist/wgsl/bsdf.wgsl.js +619 -0
- package/dist/wgsl/bsdf.wgsl.js.map +1 -0
- package/dist/wgsl/bvh.wgsl.d.ts +3 -0
- package/dist/wgsl/bvh.wgsl.d.ts.map +1 -0
- package/dist/wgsl/bvh.wgsl.js +218 -0
- package/dist/wgsl/bvh.wgsl.js.map +1 -0
- package/dist/wgsl/common.wgsl.d.ts +3 -0
- package/dist/wgsl/common.wgsl.d.ts.map +1 -0
- package/dist/wgsl/common.wgsl.js +193 -0
- package/dist/wgsl/common.wgsl.js.map +1 -0
- package/dist/wgsl/environment.wgsl.d.ts +10 -0
- package/dist/wgsl/environment.wgsl.d.ts.map +1 -0
- package/dist/wgsl/environment.wgsl.js +145 -0
- package/dist/wgsl/environment.wgsl.js.map +1 -0
- package/dist/wgsl/lights.wgsl.d.ts +10 -0
- package/dist/wgsl/lights.wgsl.d.ts.map +1 -0
- package/dist/wgsl/lights.wgsl.js +434 -0
- package/dist/wgsl/lights.wgsl.js.map +1 -0
- package/dist/wgsl/pathTrace.wgsl.d.ts +14 -0
- package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -0
- package/dist/wgsl/pathTrace.wgsl.js +1020 -0
- package/dist/wgsl/pathTrace.wgsl.js.map +1 -0
- package/dist/wgsl/present.wgsl.d.ts +7 -0
- package/dist/wgsl/present.wgsl.d.ts.map +1 -0
- package/dist/wgsl/present.wgsl.js +154 -0
- package/dist/wgsl/present.wgsl.js.map +1 -0
- package/dist/wgsl/random.wgsl.d.ts +9 -0
- package/dist/wgsl/random.wgsl.d.ts.map +1 -0
- package/dist/wgsl/random.wgsl.js +91 -0
- package/dist/wgsl/random.wgsl.js.map +1 -0
- package/dist/wgsl/sampling.wgsl.d.ts +3 -0
- package/dist/wgsl/sampling.wgsl.d.ts.map +1 -0
- package/dist/wgsl/sampling.wgsl.js +31 -0
- package/dist/wgsl/sampling.wgsl.js.map +1 -0
- package/dist/wgsl/shaderSupport.wgsl.d.ts +6 -0
- package/dist/wgsl/shaderSupport.wgsl.d.ts.map +1 -0
- package/dist/wgsl/shaderSupport.wgsl.js +120 -0
- package/dist/wgsl/shaderSupport.wgsl.js.map +1 -0
- package/dist/wgsl/surface.wgsl.d.ts +10 -0
- package/dist/wgsl/surface.wgsl.d.ts.map +1 -0
- package/dist/wgsl/surface.wgsl.js +794 -0
- package/dist/wgsl/surface.wgsl.js.map +1 -0
- package/dist/wgsl/texture.wgsl.d.ts +11 -0
- package/dist/wgsl/texture.wgsl.d.ts.map +1 -0
- package/dist/wgsl/texture.wgsl.js +132 -0
- package/dist/wgsl/texture.wgsl.js.map +1 -0
- package/package.json +76 -3
- package/src/accumulator.ts +64 -0
- package/src/bvh.ts +272 -0
- package/src/camera.ts +77 -0
- package/src/canvas.ts +13 -0
- package/src/denoise.ts +514 -0
- package/src/device.ts +81 -0
- package/src/dispatchScheduler.ts +71 -0
- package/src/environment.ts +233 -0
- package/src/frameLayout.ts +59 -0
- package/src/geometry.ts +899 -0
- package/src/index.ts +61 -0
- package/src/lights.ts +365 -0
- package/src/materialLayout.ts +408 -0
- package/src/math.ts +48 -0
- package/src/pathTracePass.ts +97 -0
- package/src/planarCapture.ts +225 -0
- package/src/presentPass.ts +99 -0
- package/src/renderer.ts +1117 -0
- package/src/sceneAssets.ts +118 -0
- package/src/sceneUpload.ts +185 -0
- package/src/shaderModule.ts +19 -0
- package/src/shaderProgram.ts +1370 -0
- package/src/structLayout.ts +93 -0
- package/src/texturePack.ts +152 -0
- package/src/upstream.ts +56 -0
- package/src/volumetric.ts +42 -0
- package/src/wgsl/bsdf.wgsl.ts +618 -0
- package/src/wgsl/bvh.wgsl.ts +217 -0
- package/src/wgsl/common.wgsl.ts +193 -0
- package/src/wgsl/environment.wgsl.ts +144 -0
- package/src/wgsl/lights.wgsl.ts +433 -0
- package/src/wgsl/pathTrace.wgsl.ts +1019 -0
- package/src/wgsl/present.wgsl.ts +153 -0
- package/src/wgsl/random.wgsl.ts +90 -0
- package/src/wgsl/sampling.wgsl.ts +30 -0
- package/src/wgsl/shaderSupport.wgsl.ts +119 -0
- package/src/wgsl/surface.wgsl.ts +793 -0
- package/src/wgsl/texture.wgsl.ts +131 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// The public API: path trace a Three.js scene on the GPU, in WGSL compute shaders. `attachCanvas` puts the image on a
|
|
2
|
+
// canvas while it converges; `render()` resolves with the finished pixels.
|
|
3
|
+
|
|
4
|
+
export { isWebGpuAvailable, requestRenderDevice, requestWebGpuSupport } from "./device.js";
|
|
5
|
+
export type { RenderDevice, RenderDeviceOptions, WebGpuSupport } from "./device.js";
|
|
6
|
+
|
|
7
|
+
export { GpuRenderer, isAbortError } from "./renderer.js";
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
GpuRendererEvents,
|
|
11
|
+
GpuRendererOptions,
|
|
12
|
+
RenderCamera,
|
|
13
|
+
RenderImage,
|
|
14
|
+
RenderPhase,
|
|
15
|
+
RenderPixels,
|
|
16
|
+
RenderScene,
|
|
17
|
+
} from "./renderer.js";
|
|
18
|
+
|
|
19
|
+
export { attachCanvas } from "./canvas.js";
|
|
20
|
+
|
|
21
|
+
export { DEFAULT_DENOISER } from "./denoise.js";
|
|
22
|
+
export type { DenoiserSettings } from "./denoise.js";
|
|
23
|
+
|
|
24
|
+
export type { Bucket, Listener } from "./upstream.js";
|
|
25
|
+
|
|
26
|
+
// Scene packing, published because the sizes it produces are what a host checks against a device's limits.
|
|
27
|
+
export { packGeometry } from "./geometry.js";
|
|
28
|
+
export type { PackedGeometry } from "./geometry.js";
|
|
29
|
+
export { buildBvh, MAX_BVH_DEPTH } from "./bvh.js";
|
|
30
|
+
export type { PackedBvh } from "./bvh.js";
|
|
31
|
+
export { sceneByteLength } from "./sceneUpload.js";
|
|
32
|
+
export type { SceneByteLength } from "./sceneUpload.js";
|
|
33
|
+
export { LIGHT_KIND, packLights } from "./lights.js";
|
|
34
|
+
export type { PackedLights } from "./lights.js";
|
|
35
|
+
export { MATERIAL_LIT, MATERIAL_PBR, materialProperties } from "./materialLayout.js";
|
|
36
|
+
export type { MaterialMap, MaterialProperties } from "./materialLayout.js";
|
|
37
|
+
export { NO_TEXTURE, packTextures } from "./texturePack.js";
|
|
38
|
+
export type { PackedTextures, TextureExtras } from "./texturePack.js";
|
|
39
|
+
export { loadEnvironment } from "./environment.js";
|
|
40
|
+
export type { PackedEnvironment } from "./environment.js";
|
|
41
|
+
|
|
42
|
+
// Settings, the environment and the scene snapshot come from the CPU renderer, unchanged.
|
|
43
|
+
export {
|
|
44
|
+
DEFAULT_RENDER_ENVIRONMENT,
|
|
45
|
+
DEFAULT_RENDER_SETTINGS,
|
|
46
|
+
mergeSettings,
|
|
47
|
+
sceneSignature,
|
|
48
|
+
serializeCamera,
|
|
49
|
+
serializeScene,
|
|
50
|
+
} from "./upstream.js";
|
|
51
|
+
|
|
52
|
+
export type {
|
|
53
|
+
CameraState,
|
|
54
|
+
RenderBackgroundMode,
|
|
55
|
+
RenderEnvironment,
|
|
56
|
+
RenderSettings,
|
|
57
|
+
RenderSettingsInput,
|
|
58
|
+
RenderToneMapping,
|
|
59
|
+
SerializedScene,
|
|
60
|
+
SerializedSceneResult,
|
|
61
|
+
} from "./upstream.js";
|
package/src/lights.ts
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import type { PackedGeometry } from "./geometry.js";
|
|
2
|
+
import type { SerializedLight } from "./upstream.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What the integrator can aim a shadow ray at. Emissive triangles and Three's analytic lights share one buffer and one
|
|
6
|
+
* cumulative distribution, so picking a light is a single binary search however the scene is lit.
|
|
7
|
+
*
|
|
8
|
+
* The emissive triangles come first, in triangle order, which lets the same buffer answer the other question the
|
|
9
|
+
* integrator asks: given a triangle a bounce ray happened to hit, how likely was direct sampling to have found it. That
|
|
10
|
+
* is a second binary search over the same prefix.
|
|
11
|
+
*/
|
|
12
|
+
export const LIGHT_KIND = {
|
|
13
|
+
/** An emissive triangle of the scene's geometry. */
|
|
14
|
+
area: 0,
|
|
15
|
+
directional: 1,
|
|
16
|
+
point: 2,
|
|
17
|
+
spot: 3,
|
|
18
|
+
/** A `RectAreaLight`: a rectangle that is not part of the geometry, so no bounce ray can hit it. */
|
|
19
|
+
rect: 4,
|
|
20
|
+
/** The surrounding image, sampled where it is brightest. */
|
|
21
|
+
environment: 5,
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
/** Twenty four words per light, as `Light` in `wgsl/common.wgsl.ts`. */
|
|
25
|
+
export const LIGHT_WORDS = 24;
|
|
26
|
+
|
|
27
|
+
/** Set when the light's own `castShadow` is on, which is what gates the occlusion ray. */
|
|
28
|
+
export const LIGHT_CASTS_SHADOW = 1;
|
|
29
|
+
/** A spot light with a projected map; its triangle word says where the map's record is in the texture buffer. */
|
|
30
|
+
export const LIGHT_HAS_MAP = 2;
|
|
31
|
+
|
|
32
|
+
/** Where the lights' own records go, and how their textures are found. */
|
|
33
|
+
export interface LightPackOptions {
|
|
34
|
+
/** The index in the texture table of each texture, by id. */
|
|
35
|
+
textures?: ReadonlyMap<string, number>;
|
|
36
|
+
/** Where the words these lights append will start in the texture buffer. */
|
|
37
|
+
extraBase?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface PackedLights {
|
|
41
|
+
/** `LIGHT_WORDS` words per light: see `Light` in `wgsl/common.wgsl.ts`. */
|
|
42
|
+
entries: ArrayBuffer;
|
|
43
|
+
count: number;
|
|
44
|
+
/** How many leading entries are emissive triangles. The rest are analytic and cannot be hit by a bounce ray. */
|
|
45
|
+
areaCount: number;
|
|
46
|
+
/**
|
|
47
|
+
* Radiance an `AmbientLight` or a `HemisphereLight` adds from every direction, which a path tracer can only honestly
|
|
48
|
+
* express as light arriving from outside the scene. `sky` applies along `axis`, `ground` against it.
|
|
49
|
+
*/
|
|
50
|
+
ambient: { sky: number[]; ground: number[]; axis: number[] };
|
|
51
|
+
/** How often direct sampling picks the environment rather than a light. Zero when there is nothing to aim at. */
|
|
52
|
+
environmentPick: number;
|
|
53
|
+
/** Words to append to the texture buffer: a spot light's map index and the matrix that projects it. */
|
|
54
|
+
extraWords: Uint32Array;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const luminance = (color: ArrayLike<number>) => 0.2126 * color[0] + 0.7152 * color[1] + 0.0722 * color[2];
|
|
58
|
+
|
|
59
|
+
/** Twice the area of the triangle at `offset`. */
|
|
60
|
+
function doubleArea(positions: Float32Array, offset: number): number {
|
|
61
|
+
const ax = positions[offset + 3] - positions[offset], ay = positions[offset + 4] - positions[offset + 1];
|
|
62
|
+
const az = positions[offset + 5] - positions[offset + 2];
|
|
63
|
+
const bx = positions[offset + 6] - positions[offset], by = positions[offset + 7] - positions[offset + 1];
|
|
64
|
+
const bz = positions[offset + 8] - positions[offset + 2];
|
|
65
|
+
|
|
66
|
+
return Math.hypot(ay * bz - az * by, az * bx - ax * bz, ax * by - ay * bx);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalize(vector: readonly number[]): number[] {
|
|
70
|
+
const length = Math.hypot(vector[0], vector[1], vector[2]);
|
|
71
|
+
|
|
72
|
+
return length > 0 ? [vector[0] / length, vector[1] / length, vector[2] / length] : [0, 1, 0];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const scale = (color: readonly number[], factor: number) => [color[0] * factor, color[1] * factor, color[2] * factor];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Where the scene is and how big it is, which is what makes the lights comparable. Three states a directional light in
|
|
79
|
+
* lux and a point light in candela, so their numbers cannot be weighed against each other directly; what can is the
|
|
80
|
+
* irradiance each of them actually delivers to the scene.
|
|
81
|
+
*/
|
|
82
|
+
interface Bounds {
|
|
83
|
+
centre: number[];
|
|
84
|
+
radius: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function sceneBounds(positions: Float32Array): Bounds {
|
|
88
|
+
if (!positions.length) return { centre: [0, 0, 0], radius: 1 };
|
|
89
|
+
|
|
90
|
+
const low = [Infinity, Infinity, Infinity];
|
|
91
|
+
const high = [-Infinity, -Infinity, -Infinity];
|
|
92
|
+
|
|
93
|
+
for (let at = 0; at < positions.length; at += 3) {
|
|
94
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
95
|
+
low[axis] = Math.min(low[axis], positions[at + axis]);
|
|
96
|
+
high[axis] = Math.max(high[axis], positions[at + axis]);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const centre = [0, 1, 2].map(axis => (low[axis] + high[axis]) / 2);
|
|
101
|
+
const radius = Math.hypot(...[0, 1, 2].map(axis => (high[axis] - low[axis]) / 2));
|
|
102
|
+
|
|
103
|
+
return { centre, radius: radius > 0 ? radius : 1 };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* How far a light is from the scene, never closer than a quarter of its radius. A light sitting exactly at the centre
|
|
108
|
+
* would otherwise be credited with an unbounded irradiance and swallow every sample.
|
|
109
|
+
*/
|
|
110
|
+
function reach(position: readonly number[], bounds: Bounds): number {
|
|
111
|
+
const distance = Math.hypot(...[0, 1, 2].map(axis => position[axis] - bounds.centre[axis]));
|
|
112
|
+
|
|
113
|
+
return Math.max(distance, bounds.radius * 0.25);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A candidate before the cumulative distribution is laid over it: the words it fills, and the power it radiates. */
|
|
117
|
+
interface Candidate {
|
|
118
|
+
kind: number;
|
|
119
|
+
weight: number;
|
|
120
|
+
write(words: Uint32Array, floats: Float32Array, at: number): void;
|
|
121
|
+
/** Area measure density of a point on it, before the pick probability is folded in. */
|
|
122
|
+
density?: number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Emissive triangles weighted by the power they radiate (area times emitted luminance), so a large or bright panel is
|
|
127
|
+
* chosen more often than a dim sliver.
|
|
128
|
+
*/
|
|
129
|
+
function emissiveTriangles(geometry: PackedGeometry, bounds: Bounds): Candidate[] {
|
|
130
|
+
const candidates: Candidate[] = [];
|
|
131
|
+
|
|
132
|
+
for (let triangle = 0; triangle < geometry.triangleCount; triangle++) {
|
|
133
|
+
const material = geometry.materials[triangle] * 4;
|
|
134
|
+
const emissive = geometry.materialEmissive;
|
|
135
|
+
|
|
136
|
+
if (material + 3 >= emissive.length) continue;
|
|
137
|
+
|
|
138
|
+
const glow = luminance(emissive.subarray(material, material + 3)) * emissive[material + 3];
|
|
139
|
+
const area = doubleArea(geometry.positions, triangle * 9) / 2;
|
|
140
|
+
|
|
141
|
+
if (!(glow > 0) || !(area > 0)) continue;
|
|
142
|
+
|
|
143
|
+
const at = triangle * 9;
|
|
144
|
+
|
|
145
|
+
const centroid = [0, 1, 2].map(axis =>
|
|
146
|
+
(geometry.positions[at + axis] + geometry.positions[at + 3 + axis] + geometry.positions[at + 6 + axis]) / 3);
|
|
147
|
+
|
|
148
|
+
candidates.push({
|
|
149
|
+
kind: LIGHT_KIND.area,
|
|
150
|
+
// The irradiance it delivers, so a bright panel and a bright lamp are weighed in the same unit.
|
|
151
|
+
weight: glow * area / reach(centroid, bounds) ** 2,
|
|
152
|
+
density: 1 / area,
|
|
153
|
+
write(words, _floats, at) {
|
|
154
|
+
words[at] = LIGHT_KIND.area;
|
|
155
|
+
words[at + 3] = triangle;
|
|
156
|
+
// The emission itself is read from the material, so the colour words stay zero.
|
|
157
|
+
words[at + 23] = LIGHT_CASTS_SHADOW;
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return candidates;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Three's analytic lights. The falloff, the cone and the rectangle's projected area follow `material.js` in the CPU
|
|
167
|
+
* renderer, so direct light from these agrees with it.
|
|
168
|
+
*/
|
|
169
|
+
function analyticLight(
|
|
170
|
+
light: SerializedLight, bounds: Bounds, options: LightPackOptions, extra: number[]): Candidate | null {
|
|
171
|
+
const intensity = light.intensity;
|
|
172
|
+
|
|
173
|
+
if (!(intensity > 0)) return null;
|
|
174
|
+
|
|
175
|
+
const color = scale(light.color, intensity);
|
|
176
|
+
const emitted = luminance(color);
|
|
177
|
+
|
|
178
|
+
if (!(emitted > 0)) return null;
|
|
179
|
+
|
|
180
|
+
// A directional light's intensity already is an irradiance; the others have to be carried to the scene first.
|
|
181
|
+
const distance = reach(light.position, bounds);
|
|
182
|
+
const falloff = 1 / Math.max(distance ** (light.decay || 0), 0.01);
|
|
183
|
+
|
|
184
|
+
// Three points a directional or spot light from its target towards its position, and so does the CPU renderer.
|
|
185
|
+
const axis = normalize([
|
|
186
|
+
light.position[0] - light.target[0],
|
|
187
|
+
light.position[1] - light.target[1],
|
|
188
|
+
light.position[2] - light.target[2]]);
|
|
189
|
+
|
|
190
|
+
// Three ignores `castShadow` on a RectAreaLight, but an area light that casts none looks broken once it is traced.
|
|
191
|
+
const flags = light.castShadow || light.type === "RectAreaLight" ? LIGHT_CASTS_SHADOW : 0;
|
|
192
|
+
|
|
193
|
+
const common = (words: Uint32Array, floats: Float32Array, at: number, kind: number) => {
|
|
194
|
+
words[at] = kind;
|
|
195
|
+
floats.set(color, at + 4);
|
|
196
|
+
floats[at + 7] = light.distance;
|
|
197
|
+
floats.set(light.position, at + 8);
|
|
198
|
+
floats[at + 11] = light.decay;
|
|
199
|
+
floats.set(axis, at + 12);
|
|
200
|
+
words[at + 23] = flags;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
if (light.type === "DirectionalLight") {
|
|
204
|
+
return {
|
|
205
|
+
kind: LIGHT_KIND.directional,
|
|
206
|
+
weight: emitted,
|
|
207
|
+
write: (words, floats, at) => common(words, floats, at, LIGHT_KIND.directional),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (light.type === "PointLight") {
|
|
212
|
+
return {
|
|
213
|
+
kind: LIGHT_KIND.point,
|
|
214
|
+
weight: emitted * falloff,
|
|
215
|
+
write: (words, floats, at) => common(words, floats, at, LIGHT_KIND.point),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (light.type === "SpotLight") {
|
|
220
|
+
const outer = Math.cos(light.angle);
|
|
221
|
+
const map = light.map !== undefined ? options.textures?.get(light.map) : undefined;
|
|
222
|
+
let record = -1;
|
|
223
|
+
|
|
224
|
+
// Three's spotLightMap: the texture and the matrix that takes a world position to its UV.
|
|
225
|
+
if (map !== undefined && light.mapMatrix?.length === 16) {
|
|
226
|
+
record = (options.extraBase ?? 0) + extra.length;
|
|
227
|
+
extra.push(map, ...new Uint32Array(new Float32Array(light.mapMatrix).buffer));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
kind: LIGHT_KIND.spot,
|
|
232
|
+
// Only the cone is lit, so only that share of the scene can expect anything from it.
|
|
233
|
+
weight: emitted * falloff * (1 - Math.cos(Math.min(light.angle, Math.PI))) / 2,
|
|
234
|
+
write(words, floats, at) {
|
|
235
|
+
common(words, floats, at, LIGHT_KIND.spot);
|
|
236
|
+
floats[at + 15] = outer;
|
|
237
|
+
floats[at + 19] = Math.cos(light.angle * (1 - light.penumbra));
|
|
238
|
+
|
|
239
|
+
if (record >= 0) {
|
|
240
|
+
words[at + 3] = record;
|
|
241
|
+
words[at + 23] |= LIGHT_HAS_MAP;
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (light.type === "RectAreaLight") {
|
|
248
|
+
const matrix = light.matrixWorld;
|
|
249
|
+
// The light's own axes, from its world matrix: Three lights the rectangle along its local -z.
|
|
250
|
+
const right = normalize([matrix[0], matrix[1], matrix[2]]);
|
|
251
|
+
const up = normalize([matrix[4], matrix[5], matrix[6]]);
|
|
252
|
+
const area = Math.max(light.width * light.height, 1e-12);
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
kind: LIGHT_KIND.rect,
|
|
256
|
+
weight: emitted * area / distance ** 2,
|
|
257
|
+
density: 1 / area,
|
|
258
|
+
write(words, floats, at) {
|
|
259
|
+
common(words, floats, at, LIGHT_KIND.rect);
|
|
260
|
+
floats.set(scale(right, light.width / 2), at + 16);
|
|
261
|
+
floats.set(scale(up, light.height / 2), at + 20);
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The irradiance an `AmbientLight` or a `HemisphereLight` adds, turned into the radiance of an equally bright
|
|
271
|
+
* surrounding. Three adds these straight to a surface's diffuse term, which no path can carry; light that arrives from
|
|
272
|
+
* everywhere is light from outside the scene, so that is where this renderer puts it. In a closed room it therefore
|
|
273
|
+
* contributes nothing, which is the difference a host has to know about.
|
|
274
|
+
*/
|
|
275
|
+
function ambientLight(lights: readonly SerializedLight[]): PackedLights["ambient"] {
|
|
276
|
+
const sky = [0, 0, 0], ground = [0, 0, 0];
|
|
277
|
+
let axis: number[] | null = null;
|
|
278
|
+
|
|
279
|
+
for (const light of lights) {
|
|
280
|
+
if (!(light.intensity > 0)) continue;
|
|
281
|
+
|
|
282
|
+
// A uniform surrounding of radiance L delivers an irradiance of pi times L.
|
|
283
|
+
const strength = light.intensity / Math.PI;
|
|
284
|
+
|
|
285
|
+
if (light.type === "AmbientLight") {
|
|
286
|
+
for (let channel = 0; channel < 3; channel++) {
|
|
287
|
+
sky[channel] += light.color[channel] * strength;
|
|
288
|
+
ground[channel] += light.color[channel] * strength;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (light.type === "HemisphereLight") {
|
|
293
|
+
// Three takes the axis from the light's position; several of them cannot share one gradient, so the first wins.
|
|
294
|
+
axis ??= normalize(light.position);
|
|
295
|
+
|
|
296
|
+
for (let channel = 0; channel < 3; channel++) {
|
|
297
|
+
sky[channel] += light.color[channel] * strength;
|
|
298
|
+
ground[channel] += light.groundColor[channel] * strength;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return { sky, ground, axis: axis ?? [0, 1, 0] };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Everything the integrator may aim a shadow ray at, with the cumulative distribution that picks one. The weights mix
|
|
308
|
+
* two unit systems, the radiant power of a surface and the intensity of an analytic light, so the balance between the
|
|
309
|
+
* two kinds is a heuristic; it costs variance when a scene leans on both at once and never costs correctness, because
|
|
310
|
+
* every contribution is divided by the probability it was actually chosen with.
|
|
311
|
+
*/
|
|
312
|
+
export function packLights(
|
|
313
|
+
geometry: PackedGeometry,
|
|
314
|
+
lights: readonly SerializedLight[] = [],
|
|
315
|
+
environment?: { power: number } | null,
|
|
316
|
+
options: LightPackOptions = {}): PackedLights {
|
|
317
|
+
const bounds = sceneBounds(geometry.positions);
|
|
318
|
+
const areas = emissiveTriangles(geometry, bounds);
|
|
319
|
+
const extra: number[] = [];
|
|
320
|
+
|
|
321
|
+
const analytic = lights
|
|
322
|
+
.map(light => analyticLight(light, bounds, options, extra))
|
|
323
|
+
.filter((light): light is Candidate => light !== null);
|
|
324
|
+
|
|
325
|
+
const surrounding: Candidate[] = environment && environment.power > 0
|
|
326
|
+
? [{
|
|
327
|
+
kind: LIGHT_KIND.environment,
|
|
328
|
+
weight: environment.power,
|
|
329
|
+
write(words, _floats, at) {
|
|
330
|
+
words[at] = LIGHT_KIND.environment;
|
|
331
|
+
// Nothing occludes the sky except the scene itself, which is exactly what the shadow ray tests.
|
|
332
|
+
words[at + 23] = LIGHT_CASTS_SHADOW;
|
|
333
|
+
},
|
|
334
|
+
}]
|
|
335
|
+
: [];
|
|
336
|
+
|
|
337
|
+
const candidates = [...areas, ...analytic, ...surrounding];
|
|
338
|
+
const total = candidates.reduce((sum, candidate) => sum + candidate.weight, 0);
|
|
339
|
+
const entries = new ArrayBuffer(Math.max(1, candidates.length) * LIGHT_WORDS * 4);
|
|
340
|
+
const words = new Uint32Array(entries);
|
|
341
|
+
const floats = new Float32Array(entries);
|
|
342
|
+
let cumulative = 0;
|
|
343
|
+
|
|
344
|
+
candidates.forEach((candidate, index) => {
|
|
345
|
+
const at = index * LIGHT_WORDS;
|
|
346
|
+
const probability = candidate.weight / total;
|
|
347
|
+
|
|
348
|
+
cumulative += probability;
|
|
349
|
+
candidate.write(words, floats, at);
|
|
350
|
+
// The last entry closes the range exactly, so a unit sample never falls past it.
|
|
351
|
+
floats[at + 1] = index === candidates.length - 1 ? 1 : cumulative;
|
|
352
|
+
floats[at + 2] = probability * (candidate.density ?? 1);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const environmentPick = surrounding.length ? surrounding[0].weight / total : 0;
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
entries,
|
|
359
|
+
count: candidates.length,
|
|
360
|
+
areaCount: areas.length,
|
|
361
|
+
ambient: ambientLight(lights),
|
|
362
|
+
environmentPick,
|
|
363
|
+
extraWords: new Uint32Array(extra),
|
|
364
|
+
};
|
|
365
|
+
}
|