@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.
Files changed (194) hide show
  1. package/ARCHITECTURE.md +334 -0
  2. package/LICENSE +21 -0
  3. package/README.md +258 -2
  4. package/dist/accumulator.d.ts +27 -0
  5. package/dist/accumulator.d.ts.map +1 -0
  6. package/dist/accumulator.js +57 -0
  7. package/dist/accumulator.js.map +1 -0
  8. package/dist/bvh.d.ts +26 -0
  9. package/dist/bvh.d.ts.map +1 -0
  10. package/dist/bvh.js +207 -0
  11. package/dist/bvh.js.map +1 -0
  12. package/dist/camera.d.ts +29 -0
  13. package/dist/camera.d.ts.map +1 -0
  14. package/dist/camera.js +48 -0
  15. package/dist/camera.js.map +1 -0
  16. package/dist/canvas.d.ts +9 -0
  17. package/dist/canvas.d.ts.map +1 -0
  18. package/dist/canvas.js +11 -0
  19. package/dist/canvas.js.map +1 -0
  20. package/dist/denoise.d.ts +69 -0
  21. package/dist/denoise.d.ts.map +1 -0
  22. package/dist/denoise.js +455 -0
  23. package/dist/denoise.js.map +1 -0
  24. package/dist/device.d.ts +37 -0
  25. package/dist/device.d.ts.map +1 -0
  26. package/dist/device.js +52 -0
  27. package/dist/device.js.map +1 -0
  28. package/dist/dispatchScheduler.d.ts +43 -0
  29. package/dist/dispatchScheduler.d.ts.map +1 -0
  30. package/dist/dispatchScheduler.js +49 -0
  31. package/dist/dispatchScheduler.js.map +1 -0
  32. package/dist/environment.d.ts +39 -0
  33. package/dist/environment.d.ts.map +1 -0
  34. package/dist/environment.js +168 -0
  35. package/dist/environment.js.map +1 -0
  36. package/dist/frameLayout.d.ts +4 -0
  37. package/dist/frameLayout.d.ts.map +1 -0
  38. package/dist/frameLayout.js +58 -0
  39. package/dist/frameLayout.js.map +1 -0
  40. package/dist/geometry.d.ts +75 -0
  41. package/dist/geometry.d.ts.map +1 -0
  42. package/dist/geometry.js +671 -0
  43. package/dist/geometry.js.map +1 -0
  44. package/dist/index.d.ts +25 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +17 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/lights.d.ts +64 -0
  49. package/dist/lights.d.ts.map +1 -0
  50. package/dist/lights.js +264 -0
  51. package/dist/lights.js.map +1 -0
  52. package/dist/materialLayout.d.ts +143 -0
  53. package/dist/materialLayout.d.ts.map +1 -0
  54. package/dist/materialLayout.js +297 -0
  55. package/dist/materialLayout.js.map +1 -0
  56. package/dist/math.d.ts +10 -0
  57. package/dist/math.d.ts.map +1 -0
  58. package/dist/math.js +40 -0
  59. package/dist/math.js.map +1 -0
  60. package/dist/pathTracePass.d.ts +27 -0
  61. package/dist/pathTracePass.d.ts.map +1 -0
  62. package/dist/pathTracePass.js +83 -0
  63. package/dist/pathTracePass.js.map +1 -0
  64. package/dist/planarCapture.d.ts +38 -0
  65. package/dist/planarCapture.d.ts.map +1 -0
  66. package/dist/planarCapture.js +163 -0
  67. package/dist/planarCapture.js.map +1 -0
  68. package/dist/presentPass.d.ts +31 -0
  69. package/dist/presentPass.d.ts.map +1 -0
  70. package/dist/presentPass.js +78 -0
  71. package/dist/presentPass.js.map +1 -0
  72. package/dist/renderer.d.ts +196 -0
  73. package/dist/renderer.d.ts.map +1 -0
  74. package/dist/renderer.js +839 -0
  75. package/dist/renderer.js.map +1 -0
  76. package/dist/sceneAssets.d.ts +24 -0
  77. package/dist/sceneAssets.d.ts.map +1 -0
  78. package/dist/sceneAssets.js +77 -0
  79. package/dist/sceneAssets.js.map +1 -0
  80. package/dist/sceneUpload.d.ts +61 -0
  81. package/dist/sceneUpload.d.ts.map +1 -0
  82. package/dist/sceneUpload.js +98 -0
  83. package/dist/sceneUpload.js.map +1 -0
  84. package/dist/shaderModule.d.ts +7 -0
  85. package/dist/shaderModule.d.ts.map +1 -0
  86. package/dist/shaderModule.js +19 -0
  87. package/dist/shaderModule.js.map +1 -0
  88. package/dist/shaderProgram.d.ts +124 -0
  89. package/dist/shaderProgram.d.ts.map +1 -0
  90. package/dist/shaderProgram.js +1030 -0
  91. package/dist/shaderProgram.js.map +1 -0
  92. package/dist/structLayout.d.ts +28 -0
  93. package/dist/structLayout.d.ts.map +1 -0
  94. package/dist/structLayout.js +58 -0
  95. package/dist/structLayout.js.map +1 -0
  96. package/dist/texturePack.d.ts +54 -0
  97. package/dist/texturePack.d.ts.map +1 -0
  98. package/dist/texturePack.js +86 -0
  99. package/dist/texturePack.js.map +1 -0
  100. package/dist/upstream.d.ts +7 -0
  101. package/dist/upstream.d.ts.map +1 -0
  102. package/dist/upstream.js +11 -0
  103. package/dist/upstream.js.map +1 -0
  104. package/dist/volumetric.d.ts +10 -0
  105. package/dist/volumetric.d.ts.map +1 -0
  106. package/dist/volumetric.js +34 -0
  107. package/dist/volumetric.js.map +1 -0
  108. package/dist/wgsl/bsdf.wgsl.d.ts +13 -0
  109. package/dist/wgsl/bsdf.wgsl.d.ts.map +1 -0
  110. package/dist/wgsl/bsdf.wgsl.js +619 -0
  111. package/dist/wgsl/bsdf.wgsl.js.map +1 -0
  112. package/dist/wgsl/bvh.wgsl.d.ts +3 -0
  113. package/dist/wgsl/bvh.wgsl.d.ts.map +1 -0
  114. package/dist/wgsl/bvh.wgsl.js +218 -0
  115. package/dist/wgsl/bvh.wgsl.js.map +1 -0
  116. package/dist/wgsl/common.wgsl.d.ts +3 -0
  117. package/dist/wgsl/common.wgsl.d.ts.map +1 -0
  118. package/dist/wgsl/common.wgsl.js +193 -0
  119. package/dist/wgsl/common.wgsl.js.map +1 -0
  120. package/dist/wgsl/environment.wgsl.d.ts +10 -0
  121. package/dist/wgsl/environment.wgsl.d.ts.map +1 -0
  122. package/dist/wgsl/environment.wgsl.js +145 -0
  123. package/dist/wgsl/environment.wgsl.js.map +1 -0
  124. package/dist/wgsl/lights.wgsl.d.ts +10 -0
  125. package/dist/wgsl/lights.wgsl.d.ts.map +1 -0
  126. package/dist/wgsl/lights.wgsl.js +434 -0
  127. package/dist/wgsl/lights.wgsl.js.map +1 -0
  128. package/dist/wgsl/pathTrace.wgsl.d.ts +14 -0
  129. package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -0
  130. package/dist/wgsl/pathTrace.wgsl.js +1020 -0
  131. package/dist/wgsl/pathTrace.wgsl.js.map +1 -0
  132. package/dist/wgsl/present.wgsl.d.ts +7 -0
  133. package/dist/wgsl/present.wgsl.d.ts.map +1 -0
  134. package/dist/wgsl/present.wgsl.js +154 -0
  135. package/dist/wgsl/present.wgsl.js.map +1 -0
  136. package/dist/wgsl/random.wgsl.d.ts +9 -0
  137. package/dist/wgsl/random.wgsl.d.ts.map +1 -0
  138. package/dist/wgsl/random.wgsl.js +91 -0
  139. package/dist/wgsl/random.wgsl.js.map +1 -0
  140. package/dist/wgsl/sampling.wgsl.d.ts +3 -0
  141. package/dist/wgsl/sampling.wgsl.d.ts.map +1 -0
  142. package/dist/wgsl/sampling.wgsl.js +31 -0
  143. package/dist/wgsl/sampling.wgsl.js.map +1 -0
  144. package/dist/wgsl/shaderSupport.wgsl.d.ts +6 -0
  145. package/dist/wgsl/shaderSupport.wgsl.d.ts.map +1 -0
  146. package/dist/wgsl/shaderSupport.wgsl.js +120 -0
  147. package/dist/wgsl/shaderSupport.wgsl.js.map +1 -0
  148. package/dist/wgsl/surface.wgsl.d.ts +10 -0
  149. package/dist/wgsl/surface.wgsl.d.ts.map +1 -0
  150. package/dist/wgsl/surface.wgsl.js +794 -0
  151. package/dist/wgsl/surface.wgsl.js.map +1 -0
  152. package/dist/wgsl/texture.wgsl.d.ts +11 -0
  153. package/dist/wgsl/texture.wgsl.d.ts.map +1 -0
  154. package/dist/wgsl/texture.wgsl.js +132 -0
  155. package/dist/wgsl/texture.wgsl.js.map +1 -0
  156. package/package.json +76 -3
  157. package/src/accumulator.ts +64 -0
  158. package/src/bvh.ts +272 -0
  159. package/src/camera.ts +77 -0
  160. package/src/canvas.ts +13 -0
  161. package/src/denoise.ts +514 -0
  162. package/src/device.ts +81 -0
  163. package/src/dispatchScheduler.ts +71 -0
  164. package/src/environment.ts +233 -0
  165. package/src/frameLayout.ts +59 -0
  166. package/src/geometry.ts +899 -0
  167. package/src/index.ts +61 -0
  168. package/src/lights.ts +365 -0
  169. package/src/materialLayout.ts +408 -0
  170. package/src/math.ts +48 -0
  171. package/src/pathTracePass.ts +97 -0
  172. package/src/planarCapture.ts +225 -0
  173. package/src/presentPass.ts +99 -0
  174. package/src/renderer.ts +1117 -0
  175. package/src/sceneAssets.ts +118 -0
  176. package/src/sceneUpload.ts +185 -0
  177. package/src/shaderModule.ts +19 -0
  178. package/src/shaderProgram.ts +1370 -0
  179. package/src/structLayout.ts +93 -0
  180. package/src/texturePack.ts +152 -0
  181. package/src/upstream.ts +56 -0
  182. package/src/volumetric.ts +42 -0
  183. package/src/wgsl/bsdf.wgsl.ts +618 -0
  184. package/src/wgsl/bvh.wgsl.ts +217 -0
  185. package/src/wgsl/common.wgsl.ts +193 -0
  186. package/src/wgsl/environment.wgsl.ts +144 -0
  187. package/src/wgsl/lights.wgsl.ts +433 -0
  188. package/src/wgsl/pathTrace.wgsl.ts +1019 -0
  189. package/src/wgsl/present.wgsl.ts +153 -0
  190. package/src/wgsl/random.wgsl.ts +90 -0
  191. package/src/wgsl/sampling.wgsl.ts +30 -0
  192. package/src/wgsl/shaderSupport.wgsl.ts +119 -0
  193. package/src/wgsl/surface.wgsl.ts +793 -0
  194. package/src/wgsl/texture.wgsl.ts +131 -0
@@ -0,0 +1,408 @@
1
+ import { layoutStruct, writeStruct, type StructField } from "./structLayout.js";
2
+ import { NO_TEXTURE } from "./texturePack.js";
3
+ import type { SerializedMaterial, UniformValue } from "./upstream.js";
4
+
5
+ /**
6
+ * What a surface is, reduced to what the integrator reads. Three's own defaults and its shading models are resolved
7
+ * here, following `material.js` in the CPU renderer and Three's own shader chunks, so the two renderers start from the
8
+ * same numbers even though they use them differently.
9
+ */
10
+ export interface MaterialProperties {
11
+ kind: number;
12
+ /** Linear rgb. */
13
+ color: number[];
14
+ opacity: number;
15
+ /** Linear rgb. */
16
+ emissive: number[];
17
+ emissiveIntensity: number;
18
+ /**
19
+ * The specular colour: for a physical material Three's `specularColor`, before the index of refraction turns it into
20
+ * a reflectance; for Phong its `specular`. A standard material reflects 0.04 regardless.
21
+ */
22
+ specular: number[];
23
+ specularIntensity: number;
24
+ /** Reflectance of the dielectric at normal incidence from the index of refraction, as Three derives it. */
25
+ dielectric: number;
26
+ roughness: number;
27
+ metalness: number;
28
+ alphaTest: number;
29
+ flags: number;
30
+ /** How far the normal map tilts the surface, along the two texture axes. */
31
+ normalScale: number[];
32
+ /** Share of the diffuse layer that lets light through instead, as Three's `transmission`. Physical materials only. */
33
+ transmission: number;
34
+ ior: number;
35
+ /** Zero is a thin wall, which light crosses without bending; above it the surface bounds a volume that refracts. */
36
+ thickness: number;
37
+ /** Linear rgb that light inside the volume fades to over `attenuationDistance`. */
38
+ attenuationColor: number[];
39
+ /** Zero when the volume does not absorb at all, which is Three's infinite default. */
40
+ attenuationDistance: number;
41
+ clearcoat: number;
42
+ clearcoatRoughness: number;
43
+ clearcoatNormalScale: number[];
44
+ /** Three's `sheenColor` already multiplied by `sheen`, as its own uniform is. */
45
+ sheenColor: number[];
46
+ sheenRoughness: number;
47
+ iridescence: number;
48
+ iridescenceIor: number;
49
+ iridescenceThickness: number[];
50
+ anisotropy: number;
51
+ anisotropyRotation: number;
52
+ dispersion: number;
53
+ shininess: number;
54
+ bumpScale: number;
55
+ lightMapIntensity: number;
56
+ envMapIntensity: number;
57
+ reflectivity: number;
58
+ refractionRatio: number;
59
+ combine: number;
60
+ side: number;
61
+ blending: number;
62
+ /** An index into the texture table per slot, or `NO_TEXTURE`. */
63
+ maps: Record<MaterialMap, number>;
64
+ }
65
+
66
+ /** The parts of a material entry that depend on the mesh and the packing, not on the material alone. */
67
+ export interface MaterialPlacement {
68
+ /** Where the material's clipping planes start in the texture buffer, and how many there are. */
69
+ clipStart: number;
70
+ clipCount: number;
71
+ /** The translated fragment shader, or `NO_PROGRAM`. */
72
+ program: number;
73
+ /** Where its uniform values start in the texture buffer. */
74
+ shaderData: number;
75
+ /** Where the shader's per-corner varyings start in the attribute buffer, how many floats a corner has, and which
76
+ * triangle they begin at. */
77
+ varyingBase: number;
78
+ varyingStride: number;
79
+ firstTriangle: number;
80
+ /** Rows of the mesh's world normal matrix, which an object space normal map is turned by. */
81
+ normalMatrix: number[];
82
+ /** The program a hook after `<opaque_fragment>` became, and where its uniforms start. */
83
+ afterProgram: number;
84
+ afterData: number;
85
+ }
86
+
87
+ /**
88
+ * The map slots the integrator reads, by Three's own property names, so a material's `maps` record is looked up directly.
89
+ * A baked ambient occlusion map is deliberately absent: this renderer traces the occlusion it stands for, and applying
90
+ * both would darken the same shadow twice. `displacementMap` moves vertices, so it is applied while packing instead.
91
+ */
92
+ export const MATERIAL_MAPS = [
93
+ "map", "emissiveMap", "roughnessMap", "metalnessMap", "normalMap", "alphaMap", "transmissionMap", "thicknessMap",
94
+ "clearcoatMap", "clearcoatRoughnessMap", "clearcoatNormalMap", "sheenColorMap", "sheenRoughnessMap",
95
+ "iridescenceMap", "iridescenceThicknessMap", "anisotropyMap", "specularMap", "specularIntensityMap",
96
+ "specularColorMap", "bumpMap", "lightMap", "gradientMap", "matcap", "envMap"] as const;
97
+
98
+ export type MaterialMap = typeof MATERIAL_MAPS[number];
99
+
100
+ /** How the integrator shades a surface. Each kind is one of Three's shading models. */
101
+ export const MATERIAL_KIND = {
102
+ standard: 0,
103
+ physical: 1,
104
+ lambert: 2,
105
+ phong: 3,
106
+ toon: 4,
107
+ /** Unlit: basic, points, lines and sprites. */
108
+ basic: 5,
109
+ normal: 6,
110
+ /** Depth and distance materials. */
111
+ depth: 7,
112
+ matcap: 8,
113
+ shadow: 9,
114
+ /** A `ShaderMaterial` or `RawShaderMaterial`: unlit, its colour from the translated fragment shader. */
115
+ shader: 10,
116
+ } as const;
117
+
118
+ /** A surface Three shades with light at all. The rest return their own colour and take no part in it. */
119
+ export const MATERIAL_LIT = 1;
120
+ /** Three's metalness workflow, which is the only one with a GGX specular lobe. */
121
+ export const MATERIAL_PBR = 2;
122
+ /** Three's `transparent`: only then does the opacity let anything through. */
123
+ export const MATERIAL_TRANSPARENT = 4;
124
+ export const MATERIAL_VERTEX_COLORS = 8;
125
+ export const MATERIAL_FLAT = 16;
126
+ export const MATERIAL_WIREFRAME = 32;
127
+ /** The material accepts `scene.fog`. */
128
+ export const MATERIAL_FOG = 64;
129
+ export const MATERIAL_CLIP_INTERSECTION = 128;
130
+ export const MATERIAL_OBJECT_NORMAL_MAP = 256;
131
+ /** A built-in material whose `onBeforeCompile` hooks run before lighting, as a program. */
132
+ export const MATERIAL_PATCHED = 512;
133
+ /** The material's own environment map bends the view ray instead of mirroring it. */
134
+ export const MATERIAL_ENV_REFRACTION = 1024;
135
+ /** Three's `alphaHash`, which keeps the coverage stochastic without `transparent`. */
136
+ export const MATERIAL_ALPHA_HASH = 2048;
137
+ /** A physical material, whose specular colour and intensity reach the lobe and whose grazing reflectance follows them. */
138
+ export const MATERIAL_SPECULAR = 4096;
139
+
140
+ /** What a material's `program` reads as when it has no shader. */
141
+ export const NO_PROGRAM = 0xffffffff;
142
+
143
+ const FIELDS: StructField[] = [
144
+ { name: "color", type: "vec4f", doc: "rgb is the base colour in linear light, w the opacity." },
145
+ { name: "emissive", type: "vec4f", doc: "rgb is the emissive colour, w its intensity." },
146
+ { name: "specular", type: "vec4f", doc: "rgb is the specular colour, w the roughness." },
147
+ { name: "attenuation", type: "vec4f", doc: "rgb is the colour a volume fades to over w; a w of zero never absorbs." },
148
+ { name: "sheen", type: "vec4f", doc: "rgb is the sheen colour times its strength, w its roughness." },
149
+ { name: "metalness", type: "f32" },
150
+ { name: "flags", type: "u32" },
151
+ { name: "alphaTest", type: "f32" },
152
+ { name: "transmission", type: "f32" },
153
+ { name: "normalScaleX", type: "f32" },
154
+ { name: "normalScaleY", type: "f32" },
155
+ { name: "ior", type: "f32" },
156
+ { name: "thickness", type: "f32", doc: "Zero is a thin wall; above it the surface bounds a volume that refracts." },
157
+ { name: "clearcoat", type: "f32" },
158
+ { name: "clearcoatRoughness", type: "f32" },
159
+ { name: "clearcoatNormalScaleX", type: "f32" },
160
+ { name: "clearcoatNormalScaleY", type: "f32" },
161
+ { name: "iridescence", type: "f32" },
162
+ { name: "iridescenceIor", type: "f32" },
163
+ { name: "iridescenceThicknessMin", type: "f32" },
164
+ { name: "iridescenceThicknessMax", type: "f32" },
165
+ { name: "anisotropy", type: "f32" },
166
+ { name: "anisotropyRotation", type: "f32" },
167
+ { name: "dispersion", type: "f32" },
168
+ { name: "shininess", type: "f32" },
169
+ { name: "bumpScale", type: "f32" },
170
+ { name: "lightMapIntensity", type: "f32" },
171
+ { name: "envMapIntensity", type: "f32" },
172
+ { name: "reflectivity", type: "f32" },
173
+ { name: "refractionRatio", type: "f32" },
174
+ { name: "combine", type: "u32" },
175
+ { name: "side", type: "u32" },
176
+ { name: "blending", type: "u32" },
177
+ { name: "clipStart", type: "u32" },
178
+ { name: "clipCount", type: "u32" },
179
+ { name: "program", type: "u32" },
180
+ { name: "shaderData", type: "u32" },
181
+ { name: "varyingBase", type: "u32" },
182
+ { name: "varyingStride", type: "u32" },
183
+ { name: "firstTriangle", type: "u32" },
184
+ { name: "kind", type: "u32" },
185
+ { name: "specularIntensity", type: "f32" },
186
+ { name: "dielectric", type: "f32" },
187
+ { name: "afterProgram", type: "u32", doc: "What a hook does after <opaque_fragment>, or NO_PROGRAM." },
188
+ { name: "afterData", type: "u32" },
189
+ { name: "normalRow0", type: "vec4f", doc: "The mesh's world normal matrix, for an object space normal map." },
190
+ { name: "normalRow1", type: "vec4f" },
191
+ { name: "normalRow2", type: "vec4f" },
192
+ ...MATERIAL_MAPS.map(slot => ({ name: slot, type: "u32" as const })),
193
+ ];
194
+
195
+ /** The `Material` struct, as both sides of the device see it. */
196
+ export const MATERIAL_LAYOUT = layoutStruct("Material", FIELDS);
197
+
198
+ export const MATERIAL_WORDS = MATERIAL_LAYOUT.words;
199
+
200
+ const KIND_OF: Record<string, number> = {
201
+ MeshStandardMaterial: MATERIAL_KIND.standard,
202
+ MeshPhysicalMaterial: MATERIAL_KIND.physical,
203
+ MeshLambertMaterial: MATERIAL_KIND.lambert,
204
+ MeshPhongMaterial: MATERIAL_KIND.phong,
205
+ MeshToonMaterial: MATERIAL_KIND.toon,
206
+ MeshNormalMaterial: MATERIAL_KIND.normal,
207
+ MeshDepthMaterial: MATERIAL_KIND.depth,
208
+ MeshDistanceMaterial: MATERIAL_KIND.depth,
209
+ MeshMatcapMaterial: MATERIAL_KIND.matcap,
210
+ ShadowMaterial: MATERIAL_KIND.shadow,
211
+ ShaderMaterial: MATERIAL_KIND.shader,
212
+ RawShaderMaterial: MATERIAL_KIND.shader,
213
+ };
214
+
215
+ /** The material types Three lights, exactly as `LIT_MATERIALS` in the CPU renderer. */
216
+ const LIT = new Set([
217
+ "MeshLambertMaterial", "MeshPhongMaterial", "MeshToonMaterial", "MeshStandardMaterial", "MeshPhysicalMaterial"]);
218
+
219
+ const PBR = new Set(["MeshStandardMaterial", "MeshPhysicalMaterial"]);
220
+
221
+ /** Three's `ShaderMaterial` keeps `fog` off by default; every built-in material has it on. */
222
+ const FOG_OFF_BY_DEFAULT = new Set(["ShaderMaterial", "RawShaderMaterial"]);
223
+
224
+ /** Three's `0x111111`, the Phong default specular colour, in linear light. */
225
+ const PHONG_SPECULAR = 0.005605391906346412;
226
+
227
+ /** Reads a serialized Three colour or vector, falling back when the material does not carry it. */
228
+ function vector(value: UniformValue | undefined, fallback: readonly number[], size = 3): number[] {
229
+ return Array.isArray(value) && value.length >= size && value.slice(0, size).every(entry => typeof entry === "number")
230
+ ? (value as number[]).slice(0, size)
231
+ : [...fallback];
232
+ }
233
+
234
+ function number(value: UniformValue | undefined, fallback: number): number {
235
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
236
+ }
237
+
238
+ function flag(value: UniformValue | undefined, fallback: boolean): boolean {
239
+ return typeof value === "boolean" ? value : fallback;
240
+ }
241
+
242
+ const clamp01 = (value: number) => Math.min(1, Math.max(0, value));
243
+
244
+ /** Whether a material is drawn at all: Three skips one whose `visible` is off. */
245
+ export function isMaterialVisible(material: SerializedMaterial | undefined): boolean {
246
+ return flag(material?.values?.visible, true) && flag(material?.values?.colorWrite, true);
247
+ }
248
+
249
+ /**
250
+ * Three's resolution of a material, defaults and all. A material outside the metalness workflow keeps its own specular
251
+ * colour; one Three does not light reads as its own colour.
252
+ */
253
+ export function materialProperties(
254
+ material: SerializedMaterial | undefined,
255
+ textures: ReadonlyMap<string, number> = new Map()): MaterialProperties {
256
+ const values = material?.values ?? {};
257
+ const type = material?.type ?? "MeshStandardMaterial";
258
+ const pbr = PBR.has(type);
259
+ const physical = type === "MeshPhysicalMaterial";
260
+ const ior = number(values.ior, 1.5);
261
+ const attenuationDistance = number(values.attenuationDistance, 0);
262
+ const transparent = flag(values.transparent, false);
263
+ const sheen = physical ? Math.max(0, number(values.sheen, 0)) : 0;
264
+ const anisotropy = physical ? number(values.anisotropy, 0) : 0;
265
+ const thickness = vector(values.iridescenceThicknessRange, [100, 400], 2);
266
+
267
+ const maps = Object.fromEntries(MATERIAL_MAPS.map(slot => {
268
+ const id = material?.maps?.[slot];
269
+
270
+ return [slot, id !== undefined ? textures.get(id) ?? NO_TEXTURE : NO_TEXTURE];
271
+ })) as Record<MaterialMap, number>;
272
+
273
+ // A shader material's own colour comes from its program; a patched built-in keeps its type's shading.
274
+ const shader = material?.shader;
275
+ const kind = KIND_OF[type] ?? MATERIAL_KIND.basic;
276
+
277
+ const flags = (LIT.has(type) ? MATERIAL_LIT : 0)
278
+ | (pbr ? MATERIAL_PBR : 0)
279
+ | (transparent ? MATERIAL_TRANSPARENT : 0)
280
+ | (flag(values.vertexColors, false) ? MATERIAL_VERTEX_COLORS : 0)
281
+ | (flag(values.flatShading, false) ? MATERIAL_FLAT : 0)
282
+ | (flag(values.wireframe, false) ? MATERIAL_WIREFRAME : 0)
283
+ | (flag(values.fog, !FOG_OFF_BY_DEFAULT.has(type)) ? MATERIAL_FOG : 0)
284
+ | (flag(values.clipIntersection, false) ? MATERIAL_CLIP_INTERSECTION : 0)
285
+ | (number(values.normalMapType, 0) === 1 ? MATERIAL_OBJECT_NORMAL_MAP : 0)
286
+ | (shader?.patched ? MATERIAL_PATCHED : 0)
287
+ | (flag(values.alphaHash, false) ? MATERIAL_ALPHA_HASH : 0)
288
+ | (physical ? MATERIAL_SPECULAR : 0);
289
+
290
+ return {
291
+ kind,
292
+ color: vector(values.color, [1, 1, 1]),
293
+ opacity: number(values.opacity, 1),
294
+ emissive: vector(values.emissive, [0, 0, 0]),
295
+ emissiveIntensity: number(values.emissiveIntensity, 1),
296
+ specular: physical
297
+ ? vector(values.specularColor, [1, 1, 1])
298
+ : vector(values.specular, [PHONG_SPECULAR, PHONG_SPECULAR, PHONG_SPECULAR]),
299
+ specularIntensity: physical ? number(values.specularIntensity, 1) : 1,
300
+ // A standard material has no index of refraction in Three's shader and reflects 0.04, which is what 1.5 gives.
301
+ dielectric: ((ior - 1) / (ior + 1)) ** 2,
302
+ roughness: number(values.roughness, 1),
303
+ metalness: number(values.metalness, 0),
304
+ alphaTest: number(values.alphaTest, 0),
305
+ flags,
306
+ normalScale: vector(values.normalScale, [1, 1], 2),
307
+ // Three only lets light through a physical material. Like its own shader, metal takes the share back.
308
+ transmission: physical ? clamp01(number(values.transmission, 0)) : 0,
309
+ ior,
310
+ thickness: Math.max(0, number(values.thickness, 0)),
311
+ attenuationColor: vector(values.attenuationColor, [1, 1, 1]),
312
+ attenuationDistance: attenuationDistance > 0 ? attenuationDistance : 0,
313
+ clearcoat: physical ? clamp01(number(values.clearcoat, 0)) : 0,
314
+ clearcoatRoughness: number(values.clearcoatRoughness, 0),
315
+ clearcoatNormalScale: vector(values.clearcoatNormalScale, [1, 1], 2),
316
+ sheenColor: vector(values.sheenColor, [0, 0, 0]).map(channel => channel * sheen),
317
+ sheenRoughness: Math.min(1, Math.max(1e-4, number(values.sheenRoughness, 1))),
318
+ iridescence: physical ? Math.max(0, number(values.iridescence, 0)) : 0,
319
+ iridescenceIor: number(values.iridescenceIOR, 1.3),
320
+ iridescenceThickness: thickness,
321
+ anisotropy,
322
+ anisotropyRotation: number(values.anisotropyRotation, 0),
323
+ dispersion: physical ? Math.max(0, number(values.dispersion, 0)) : 0,
324
+ shininess: Math.max(0, number(values.shininess, 30)),
325
+ bumpScale: number(values.bumpScale, 1),
326
+ lightMapIntensity: number(values.lightMapIntensity, 1),
327
+ envMapIntensity: number(values.envMapIntensity, 1),
328
+ reflectivity: number(values.reflectivity, 1),
329
+ refractionRatio: number(values.refractionRatio, 0.98),
330
+ combine: number(values.combine, 0),
331
+ side: number(values.side, 0),
332
+ blending: number(values.blending, 1),
333
+ maps,
334
+ };
335
+ }
336
+
337
+ /** A material entry with no mesh specific parts: no clipping, no program, no varyings, an identity normal matrix. */
338
+ export const NO_PLACEMENT: MaterialPlacement = {
339
+ clipStart: 0,
340
+ clipCount: 0,
341
+ program: NO_PROGRAM,
342
+ shaderData: 0,
343
+ varyingBase: 0,
344
+ varyingStride: 0,
345
+ firstTriangle: 0,
346
+ normalMatrix: [1, 0, 0, 0, 1, 0, 0, 0, 1],
347
+ afterProgram: NO_PROGRAM,
348
+ afterData: 0,
349
+ };
350
+
351
+ /** The bytes of one material, as the shader reads them. */
352
+ export function writeMaterial(
353
+ target: ArrayBuffer, index: number, properties: MaterialProperties, placement: MaterialPlacement = NO_PLACEMENT) {
354
+ const p = properties;
355
+ const n = placement.normalMatrix;
356
+
357
+ writeStruct(MATERIAL_LAYOUT, target, index * MATERIAL_WORDS, {
358
+ color: [...p.color, p.opacity],
359
+ emissive: [...p.emissive, p.emissiveIntensity],
360
+ specular: [...p.specular, p.roughness],
361
+ attenuation: [...p.attenuationColor, p.attenuationDistance],
362
+ sheen: [...p.sheenColor, p.sheenRoughness],
363
+ metalness: p.metalness,
364
+ flags: p.flags,
365
+ alphaTest: p.alphaTest,
366
+ transmission: p.transmission,
367
+ normalScaleX: p.normalScale[0],
368
+ normalScaleY: p.normalScale[1],
369
+ ior: p.ior,
370
+ thickness: p.thickness,
371
+ clearcoat: p.clearcoat,
372
+ clearcoatRoughness: p.clearcoatRoughness,
373
+ clearcoatNormalScaleX: p.clearcoatNormalScale[0],
374
+ clearcoatNormalScaleY: p.clearcoatNormalScale[1],
375
+ iridescence: p.iridescence,
376
+ iridescenceIor: p.iridescenceIor,
377
+ iridescenceThicknessMin: p.iridescenceThickness[0],
378
+ iridescenceThicknessMax: p.iridescenceThickness[1],
379
+ anisotropy: p.anisotropy,
380
+ anisotropyRotation: p.anisotropyRotation,
381
+ dispersion: p.dispersion,
382
+ shininess: p.shininess,
383
+ bumpScale: p.bumpScale,
384
+ lightMapIntensity: p.lightMapIntensity,
385
+ envMapIntensity: p.envMapIntensity,
386
+ reflectivity: p.reflectivity,
387
+ refractionRatio: p.refractionRatio,
388
+ combine: p.combine,
389
+ side: p.side,
390
+ blending: p.blending,
391
+ clipStart: placement.clipStart,
392
+ clipCount: placement.clipCount,
393
+ program: placement.program,
394
+ shaderData: placement.shaderData,
395
+ varyingBase: placement.varyingBase,
396
+ varyingStride: placement.varyingStride,
397
+ firstTriangle: placement.firstTriangle,
398
+ kind: p.kind,
399
+ specularIntensity: p.specularIntensity,
400
+ dielectric: p.dielectric,
401
+ afterProgram: placement.afterProgram,
402
+ afterData: placement.afterData,
403
+ normalRow0: [n[0], n[3], n[6], 0],
404
+ normalRow1: [n[1], n[4], n[7], 0],
405
+ normalRow2: [n[2], n[5], n[8], 0],
406
+ ...Object.fromEntries(MATERIAL_MAPS.map(slot => [slot, p.maps[slot]])),
407
+ });
408
+ }
package/src/math.ts ADDED
@@ -0,0 +1,48 @@
1
+ // Small matrix helpers. Three is a peer dependency, but the packing path stays free of it so it can be tested in Node
2
+ // without a scene. Matrices arrive as Three's column-major sixteen number arrays.
3
+
4
+ /** Applies a column-major 4x4 to a point, writing three floats at `offset`. */
5
+ export function transformPoint(m: readonly number[], x: number, y: number, z: number,
6
+ out: Float32Array, offset: number) {
7
+ const w = m[3] * x + m[7] * y + m[11] * z + m[15] || 1;
8
+
9
+ out[offset] = (m[0] * x + m[4] * y + m[8] * z + m[12]) / w;
10
+ out[offset + 1] = (m[1] * x + m[5] * y + m[9] * z + m[13]) / w;
11
+ out[offset + 2] = (m[2] * x + m[6] * y + m[10] * z + m[14]) / w;
12
+ }
13
+
14
+ /**
15
+ * The inverse transpose of the upper-left 3x3, as a column-major 3x3. Non-uniform scale makes the world matrix itself
16
+ * wrong for normals, which is what this corrects; a singular matrix falls back to the identity.
17
+ */
18
+ export function normalMatrix(m: readonly number[]): Float64Array {
19
+ const a = m[0], b = m[1], c = m[2];
20
+ const d = m[4], e = m[5], f = m[6];
21
+ const g = m[8], h = m[9], i = m[10];
22
+ const cofactor0 = e * i - f * h, cofactor1 = f * g - d * i, cofactor2 = d * h - e * g;
23
+ const determinant = a * cofactor0 + b * cofactor1 + c * cofactor2;
24
+
25
+ if (determinant === 0) return new Float64Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
26
+
27
+ const s = 1 / determinant;
28
+
29
+ // The inverse is the adjugate over the determinant; transposing it is the same as reading the adjugate row-wise.
30
+ return new Float64Array([
31
+ cofactor0 * s, cofactor1 * s, cofactor2 * s,
32
+ (c * h - b * i) * s, (a * i - c * g) * s, (b * g - a * h) * s,
33
+ (b * f - c * e) * s, (c * d - a * f) * s, (a * e - b * d) * s,
34
+ ]);
35
+ }
36
+
37
+ /** Applies a column-major 3x3 to a direction and normalizes it, writing three floats at `offset`. */
38
+ export function transformNormal(n: Float64Array, x: number, y: number, z: number,
39
+ out: Float32Array, offset: number) {
40
+ const nx = n[0] * x + n[3] * y + n[6] * z;
41
+ const ny = n[1] * x + n[4] * y + n[7] * z;
42
+ const nz = n[2] * x + n[5] * y + n[8] * z;
43
+ const length = Math.hypot(nx, ny, nz) || 1;
44
+
45
+ out[offset] = nx / length;
46
+ out[offset + 1] = ny / length;
47
+ out[offset + 2] = nz / length;
48
+ }
@@ -0,0 +1,97 @@
1
+ import { FRAME_LAYOUT, FRAME_WORDS } from "./frameLayout.js";
2
+ import { createShaderModule } from "./shaderModule.js";
3
+ import { programsWgsl, type ShaderPrograms } from "./shaderProgram.js";
4
+ import type { SceneBuffers } from "./sceneUpload.js";
5
+ import { writeStruct } from "./structLayout.js";
6
+ import { BSDF_WGSL } from "./wgsl/bsdf.wgsl.js";
7
+ import { BVH_WGSL } from "./wgsl/bvh.wgsl.js";
8
+ import { COMMON_WGSL } from "./wgsl/common.wgsl.js";
9
+ import { ENVIRONMENT_WGSL } from "./wgsl/environment.wgsl.js";
10
+ import { LIGHTS_WGSL } from "./wgsl/lights.wgsl.js";
11
+ import { PATH_TRACE_WGSL } from "./wgsl/pathTrace.wgsl.js";
12
+ import { RANDOM_WGSL } from "./wgsl/random.wgsl.js";
13
+ import { SAMPLING_WGSL } from "./wgsl/sampling.wgsl.js";
14
+ import { SHADER_SUPPORT_WGSL } from "./wgsl/shaderSupport.wgsl.js";
15
+ import { SURFACE_WGSL } from "./wgsl/surface.wgsl.js";
16
+ import { TEXTURE_WGSL } from "./wgsl/texture.wgsl.js";
17
+
18
+ /** One dispatch worth of state: which band, which samples, and everything the kernel reads about the frame. */
19
+ export type FrameUniforms = Record<keyof typeof FRAME_LAYOUT.offsets & string, number | boolean | ArrayLike<number>>;
20
+
21
+ /**
22
+ * The whole kernel, in the order WGSL needs: every stage may only call what comes before it, as there are no forward
23
+ * declarations. The scene's translated shaders sit between the texture code they call and the surface code that calls
24
+ * them.
25
+ */
26
+ export function pathTraceSource(programs: ShaderPrograms | null = null): string {
27
+ return COMMON_WGSL + RANDOM_WGSL + BVH_WGSL + SAMPLING_WGSL + TEXTURE_WGSL + SHADER_SUPPORT_WGSL
28
+ + programsWgsl(programs) + ENVIRONMENT_WGSL + SURFACE_WGSL + BSDF_WGSL + LIGHTS_WGSL + PATH_TRACE_WGSL;
29
+ }
30
+
31
+ const FRAME_BYTES = FRAME_WORDS * 4;
32
+
33
+ const WORKGROUP = 8;
34
+
35
+ export class PathTracePass {
36
+ private pipeline: GPUComputePipeline;
37
+ private readonly uniforms: GPUBuffer;
38
+ private readonly values = new ArrayBuffer(FRAME_BYTES);
39
+ private bindGroup: GPUBindGroup | null = null;
40
+ private programKey = "";
41
+
42
+ constructor(private readonly device: GPUDevice) {
43
+ this.pipeline = this.build(null);
44
+ this.uniforms = device.createBuffer({ size: FRAME_BYTES, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
45
+ }
46
+
47
+ /** Recompiles the kernel when the scene's shaders changed; the same set of programs keeps the pipeline it has. */
48
+ setPrograms(programs: ShaderPrograms | null) {
49
+ const key = programs?.key ?? "";
50
+
51
+ if (key === this.programKey) return;
52
+
53
+ this.programKey = key;
54
+ this.pipeline = this.build(programs);
55
+ this.bindGroup = null;
56
+ }
57
+
58
+ private build(programs: ShaderPrograms | null): GPUComputePipeline {
59
+ const module = createShaderModule(this.device, "path trace", pathTraceSource(programs));
60
+
61
+ return this.device.createComputePipeline({ layout: "auto", compute: { module, entryPoint: "main" } });
62
+ }
63
+
64
+ /** Rebinds after the scene or the accumulation buffer was replaced. */
65
+ setBindings(scene: SceneBuffers, accumulation: GPUBuffer) {
66
+ this.bindGroup = this.device.createBindGroup({
67
+ layout: this.pipeline.getBindGroupLayout(0),
68
+ entries: [
69
+ { binding: 0, resource: { buffer: this.uniforms } },
70
+ { binding: 1, resource: { buffer: scene.positions } },
71
+ { binding: 2, resource: { buffer: scene.attributes } },
72
+ { binding: 3, resource: { buffer: scene.indices } },
73
+ { binding: 4, resource: { buffer: scene.materials } },
74
+ { binding: 5, resource: { buffer: scene.nodes } },
75
+ { binding: 6, resource: { buffer: scene.lights } },
76
+ { binding: 7, resource: { buffer: accumulation } },
77
+ { binding: 8, resource: { buffer: scene.textures } },
78
+ ],
79
+ });
80
+ }
81
+
82
+ encode(encoder: GPUCommandEncoder, frame: FrameUniforms) {
83
+ if (!this.bindGroup) throw new Error("GPU renderer: the path tracing pass has no scene bound.");
84
+
85
+ new Uint32Array(this.values).fill(0);
86
+ writeStruct(FRAME_LAYOUT, this.values, 0, frame);
87
+ this.device.queue.writeBuffer(this.uniforms, 0, this.values);
88
+
89
+ const pass = encoder.beginComputePass({ label: "path trace" });
90
+ const size = frame.bandSize as ArrayLike<number>;
91
+
92
+ pass.setPipeline(this.pipeline);
93
+ pass.setBindGroup(0, this.bindGroup);
94
+ pass.dispatchWorkgroups(Math.ceil(size[0] / WORKGROUP), Math.ceil(size[1] / WORKGROUP));
95
+ pass.end();
96
+ }
97
+ }