@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,899 @@
1
+ import { clipToWorld, transform4, worldToClip, type CameraMatrices } from "./camera.js";
2
+ import {
3
+ isMaterialVisible,
4
+ MATERIAL_KIND,
5
+ materialProperties,
6
+ NO_PLACEMENT,
7
+ NO_PROGRAM,
8
+ type MaterialPlacement,
9
+ type MaterialProperties,
10
+ } from "./materialLayout.js";
11
+ import { normalMatrix, transformNormal, transformPoint } from "./math.js";
12
+ import type { ShaderPrograms } from "./shaderProgram.js";
13
+ import { type CpuShader, matrixValue, sampleTexture } from "./upstream.js";
14
+ import type { SerializedMaterial, SerializedMesh, SerializedScene, SerializedTexture, UniformValue } from "./upstream.js";
15
+
16
+ /** The flat, GPU ready form of a scene's triangles. Positions and normals are world space, three vertices per entry. */
17
+ export interface PackedGeometry {
18
+ /** Nine coordinates per triangle. */
19
+ positions: Float32Array;
20
+ /** Nine normal components per triangle, one per vertex. */
21
+ normals: Float32Array;
22
+ /** Twelve per triangle: two UV sets for each of the three vertices, which is what Three's map channels need. */
23
+ uvs: Float32Array;
24
+ /** Twelve per triangle, RGBA per vertex, when any material reads vertex colours; empty otherwise. */
25
+ colors: Float32Array;
26
+ /** Shader varyings, three corners per triangle of a shader material, laid out as each program asks. */
27
+ varyings: Float32Array;
28
+ /** One material index per triangle. */
29
+ materials: Uint32Array;
30
+ triangleCount: number;
31
+ /** Four floats per material: linear rgb and opacity. */
32
+ materialColors: Float32Array;
33
+ /** Four floats per material: linear emissive rgb and its intensity. */
34
+ materialEmissive: Float32Array;
35
+ /** Every material of the scene, resolved, in the order the triangle indices refer to them. */
36
+ materialProperties: MaterialProperties[];
37
+ /** The mesh specific half of each material entry. */
38
+ materialPlacements: MaterialPlacement[];
39
+ materialCount: number;
40
+ /** Words the materials keep in the texture buffer: clipping planes and shader uniforms. */
41
+ extraWords: Uint32Array;
42
+ /**
43
+ * Whether the triangles depend on the camera: points, lines and sprites face it, and a vertex shader projects through
44
+ * it. Such a scene is packed again whenever the camera or the image size changes.
45
+ */
46
+ cameraDependent: boolean;
47
+ }
48
+
49
+ export interface PackOptions {
50
+ /** The index in the texture table of each texture, by id. */
51
+ textures?: ReadonlyMap<string, number>;
52
+ /** The textures themselves, for the work done here on the CPU: displacement maps and vertex shaders. */
53
+ textureData?: ReadonlyMap<string, SerializedTexture>;
54
+ /** The camera and image size points, lines, sprites and vertex shaders are built for. */
55
+ camera?: CameraMatrices;
56
+ width?: number;
57
+ height?: number;
58
+ /** Translated fragment shaders, for the materials that have one. */
59
+ programs?: ShaderPrograms;
60
+ /** Where the extra words will start in the texture buffer, so the materials can point at them absolutely. */
61
+ extraBase?: number;
62
+ /** Scene fog, which a shader's fog uniforms read. */
63
+ fog?: SerializedScene["fog"];
64
+ }
65
+
66
+ /** Three's wireframe and line width: WebGL draws every line one pixel wide whatever the material asks. */
67
+ const LINE_WIDTH = 1;
68
+
69
+ /** A dashed line split finer than this is drawn solid rather than as millions of dashes. */
70
+ const MAX_DASHES = 4096;
71
+
72
+ /** One corner as it is about to be written: world space, with everything a triangle stores per vertex. */
73
+ interface Corner {
74
+ position: number[];
75
+ normal: number[];
76
+ uv0: number[];
77
+ uv1: number[];
78
+ color: number[];
79
+ varyings: Float32Array | null;
80
+ }
81
+
82
+ /** A material entry: what the triangles index, and what it was made from. */
83
+ interface Entry {
84
+ index: number;
85
+ properties: MaterialProperties;
86
+ placement: MaterialPlacement;
87
+ }
88
+
89
+ /** Grows a float array as triangles are added, since how many a primitive makes is only known as it is built. */
90
+ class FloatStream {
91
+ data: Float32Array;
92
+ length = 0;
93
+
94
+ constructor(capacity: number) {
95
+ this.data = new Float32Array(Math.max(16, capacity));
96
+ }
97
+
98
+ reserve(count: number) {
99
+ if (this.length + count <= this.data.length) return;
100
+
101
+ const next = new Float32Array(Math.max(this.data.length * 2, this.length + count));
102
+
103
+ next.set(this.data.subarray(0, this.length));
104
+ this.data = next;
105
+ }
106
+
107
+ push(values: ArrayLike<number>, count = values.length) {
108
+ this.reserve(count);
109
+
110
+ for (let i = 0; i < count; i++) this.data[this.length + i] = values[i] ?? 0;
111
+
112
+ this.length += count;
113
+ }
114
+
115
+ pad(count: number) {
116
+ this.reserve(count);
117
+ this.data.fill(0, this.length, this.length + count);
118
+ this.length += count;
119
+ }
120
+
121
+ view(): Float32Array {
122
+ return this.data.subarray(0, this.length);
123
+ }
124
+ }
125
+
126
+ function triangleCountOf(mesh: SerializedMesh): number {
127
+ const vertices = mesh.index ? mesh.index.length : mesh.attributes.position.data.length / 3;
128
+
129
+ const groups = mesh.groups.length
130
+ ? mesh.groups.reduce((total, group) => total + Math.min(group.count, Math.max(0, vertices - group.start)), 0)
131
+ : vertices;
132
+
133
+ return Math.floor(groups / 3);
134
+ }
135
+
136
+ function determinant3(m: readonly number[]): number {
137
+ return m[0] * (m[5] * m[10] - m[6] * m[9]) - m[4] * (m[1] * m[10] - m[2] * m[9]) + m[8] * (m[1] * m[6] - m[2] * m[5]);
138
+ }
139
+
140
+ const normalize = (v: readonly number[]) => {
141
+ const length = Math.hypot(v[0], v[1], v[2]) || 1;
142
+
143
+ return [v[0] / length, v[1] / length, v[2] / length];
144
+ };
145
+
146
+ function multiply4(a: readonly number[], b: readonly number[]): number[] {
147
+ const out = new Array<number>(16).fill(0);
148
+
149
+ for (let column = 0; column < 4; column++)
150
+ for (let row = 0; row < 4; row++)
151
+ for (let k = 0; k < 4; k++) out[column * 4 + row] += a[k * 4 + row] * b[column * 4 + k];
152
+
153
+ return out;
154
+ }
155
+
156
+ /** Three's normalMatrix for a model view matrix: the inverse transpose of its upper 3x3, column-major. */
157
+ function normalMatrix3(m: readonly number[]): number[] {
158
+ return Array.from(normalMatrix(m));
159
+ }
160
+
161
+ /** The uniforms Three sets on every shader for this camera and mesh. */
162
+ export function builtinUniforms(camera: CameraMatrices | undefined, matrixWorld: readonly number[],
163
+ fog?: SerializedScene["fog"]): Record<string, UniformValue> {
164
+ const view = camera?.view ?? [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
165
+ const modelView = multiply4(view, matrixWorld);
166
+
167
+ return {
168
+ modelMatrix: [...matrixWorld],
169
+ modelViewMatrix: modelView,
170
+ normalMatrix: normalMatrix3(modelView),
171
+ viewMatrix: [...view],
172
+ projectionMatrix: [...camera?.projection ?? [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, -1, 0, 0, -0.2, 0]],
173
+ cameraPosition: [...camera?.position ?? [0, 0, 0]],
174
+ isOrthographic: camera?.orthographic ?? false,
175
+ cameraNear: camera?.near ?? 0.1,
176
+ cameraFar: camera?.far ?? 1000,
177
+ fogColor: fog?.color ?? [0, 0, 0],
178
+ fogNear: fog?.near ?? 0,
179
+ fogFar: fog?.far ?? 0,
180
+ fogDensity: fog?.density ?? 0,
181
+ };
182
+ }
183
+
184
+ /** The same uniforms in the form the CPU renderer's interpreter reads, where a matrix is a tagged value. */
185
+ function interpreterUniforms(uniforms: Record<string, UniformValue>): Record<string, unknown> {
186
+ const out: Record<string, unknown> = { ...uniforms };
187
+
188
+ for (const name of ["modelMatrix", "modelViewMatrix", "normalMatrix", "viewMatrix", "projectionMatrix"])
189
+ out[name] = matrixValue(uniforms[name] as number[]);
190
+
191
+ return out;
192
+ }
193
+
194
+ /** A flat number array from whatever an interpreter returned, or null when it is not one. */
195
+ function flatten(value: unknown): number[] | null {
196
+ if (typeof value === "number") return [value];
197
+ if (typeof value === "boolean") return [value ? 1 : 0];
198
+ if (Array.isArray(value)) return value.flatMap(entry => flatten(entry) ?? [0]);
199
+ if (value && typeof value === "object" && "matrix" in value) return [...(value as { matrix: number[] }).matrix];
200
+
201
+ return null;
202
+ }
203
+
204
+ /**
205
+ * Flattens the scene into triangles, materials resolved and transforms applied. Every triangle carries its own three
206
+ * vertices: the path tracer reads a hit through its triangle index, and sharing vertices would cost an indirection on
207
+ * the hottest path of the kernel for memory that low poly scenes do not miss.
208
+ *
209
+ * Points, lines and sprites become triangles that face the camera, sized the way Three draws them, so a path has a
210
+ * surface to find; vertex shaders run here, on the CPU renderer's interpreter, and their varyings ride along per corner.
211
+ */
212
+ export function packGeometry(
213
+ scene: SerializedScene, options: PackOptions | ReadonlyMap<string, number> = {}): PackedGeometry {
214
+ const settings: PackOptions = options instanceof Map ? { textures: options } : options as PackOptions;
215
+ const textures = settings.textures ?? new Map<string, number>();
216
+ const textureData = settings.textureData ?? new Map<string, SerializedTexture>();
217
+ const camera = settings.camera;
218
+ const width = Math.max(1, settings.width ?? 1);
219
+ const height = Math.max(1, settings.height ?? 1);
220
+ const programs = settings.programs;
221
+ const extraBase = settings.extraBase ?? 0;
222
+ const meshes = scene.meshes.filter(mesh => mesh.attributes.position);
223
+ const estimate = meshes.reduce((count, mesh) => count + (mesh.primitive ? 0 : triangleCountOf(mesh)), 0);
224
+
225
+ const anyVertexColors = meshes.some(mesh =>
226
+ mesh.attributes.color && mesh.materials.some(m => m?.values?.vertexColors === true));
227
+
228
+ const positions = new FloatStream(estimate * 9);
229
+ const normals = new FloatStream(estimate * 9);
230
+ const uvs = new FloatStream(estimate * 12);
231
+ const colors = new FloatStream(anyVertexColors ? estimate * 12 : 0);
232
+ const varyings = new FloatStream(0);
233
+ const triangleMaterials: number[] = [];
234
+ const entries: Entry[] = [];
235
+ const shared = new Map<SerializedMaterial | undefined, Entry>();
236
+ const extra: number[] = [];
237
+ let cameraDependent = false;
238
+
239
+ /** A material's clipping planes, written once to the extra words. */
240
+ const clipPlanes = new Map<SerializedMaterial, { start: number; count: number }>();
241
+
242
+ const planesOf = (material: SerializedMaterial | undefined) => {
243
+ const planes = material?.values?.clippingPlanes;
244
+
245
+ if (!material || !Array.isArray(planes) || !planes.length) return { start: 0, count: 0 };
246
+
247
+ let known = clipPlanes.get(material);
248
+
249
+ if (!known) {
250
+ known = { start: extraBase + extra.length, count: 0 };
251
+
252
+ for (const plane of planes) {
253
+ if (!Array.isArray(plane) || plane.length < 4) continue;
254
+
255
+ const floats = new Float32Array((plane as number[]).slice(0, 4));
256
+
257
+ extra.push(...new Uint32Array(floats.buffer));
258
+ known.count++;
259
+ }
260
+
261
+ clipPlanes.set(material, known);
262
+ }
263
+
264
+ return known;
265
+ };
266
+
267
+ /** One entry per material, or one per mesh and group when the entry carries something of the mesh. */
268
+ const entryFor = (material: SerializedMaterial | undefined, mesh: SerializedMesh): Entry => {
269
+ const program = material ? programs?.fragment(material) ?? null : null;
270
+ const perMesh = !!program || material?.values?.normalMapType === 1;
271
+
272
+ if (!perMesh) {
273
+ const known = shared.get(material);
274
+
275
+ if (known) return known;
276
+ }
277
+
278
+ const properties = materialProperties(material, textures);
279
+ const planes = planesOf(material);
280
+
281
+ // Points, lines and sprites always face the camera, so Three never culls them.
282
+ if (mesh.primitive) properties.side = 2;
283
+
284
+ const placement: MaterialPlacement = {
285
+ ...NO_PLACEMENT,
286
+ clipStart: planes.start,
287
+ clipCount: planes.count,
288
+ normalMatrix: perMesh ? Array.from(normalMatrix(mesh.matrixWorld)) : NO_PLACEMENT.normalMatrix,
289
+ };
290
+
291
+ if (program && material) {
292
+ const uniforms: Record<string, UniformValue> = {
293
+ ...builtinUniforms(camera, mesh.matrixWorld, settings.fog),
294
+ ...material.shader?.uniforms,
295
+ ...material.time !== undefined ? { time: material.time } : {},
296
+ };
297
+
298
+ const words = program.packUniforms(uniforms, textures);
299
+
300
+ placement.program = program.id;
301
+ placement.shaderData = extraBase + extra.length;
302
+ placement.varyingStride = program.varyingStride;
303
+ placement.varyingBase = varyings.length;
304
+ placement.firstTriangle = triangleMaterials.length;
305
+
306
+ for (let i = 0; i < words.length; i++) extra.push(words[i]);
307
+
308
+ const after = programs?.after(material) ?? null;
309
+
310
+ if (after) {
311
+ const afterWords = after.packUniforms(uniforms, textures);
312
+
313
+ placement.afterProgram = after.id;
314
+ placement.afterData = extraBase + extra.length;
315
+
316
+ for (let i = 0; i < afterWords.length; i++) extra.push(afterWords[i]);
317
+ }
318
+ }
319
+
320
+ const entry = { index: entries.length, properties, placement };
321
+
322
+ entries.push(entry);
323
+
324
+ if (!perMesh) shared.set(material, entry);
325
+
326
+ return entry;
327
+ };
328
+
329
+ const writeTriangle = (corners: readonly Corner[], entry: Entry) => {
330
+ // Geometry without normals is shaded flat: the three corners share the face normal.
331
+ const flat = corners.some(corner => corner.normal.length < 3)
332
+ ? faceNormal(corners[0].position, corners[1].position, corners[2].position)
333
+ : null;
334
+
335
+ for (const corner of corners) {
336
+ positions.push(corner.position, 3);
337
+ normals.push(flat ?? corner.normal, 3);
338
+ uvs.push([corner.uv0[0], corner.uv0[1], corner.uv1[0], corner.uv1[1]], 4);
339
+
340
+ if (anyVertexColors) colors.push(corner.color, 4);
341
+ }
342
+
343
+ const stride = entry.placement.varyingStride;
344
+
345
+ if (stride > 0)
346
+ for (const corner of corners) {
347
+ if (corner.varyings) varyings.push(corner.varyings, stride);
348
+ else varyings.pad(stride);
349
+ }
350
+
351
+ triangleMaterials.push(entry.index);
352
+ };
353
+
354
+ for (const mesh of meshes) {
355
+ const groups = mesh.groups.length
356
+ ? mesh.groups
357
+ : [{
358
+ start: 0,
359
+ count: mesh.index ? mesh.index.length : mesh.attributes.position.data.length / 3,
360
+ materialIndex: 0,
361
+ }];
362
+
363
+ if (mesh.primitive) cameraDependent = true;
364
+
365
+ for (const group of groups) {
366
+ const material = mesh.materials[group.materialIndex] ?? mesh.materials[0];
367
+
368
+ if (!isMaterialVisible(material)) continue;
369
+
370
+ const entry = entryFor(material, mesh);
371
+ const vertexShader = material ? programs?.vertex(material) ?? null : null;
372
+
373
+ if (vertexShader) cameraDependent = true;
374
+
375
+ // Primitives that face the camera need one to face; without it there is nothing sensible to build.
376
+ if (mesh.primitive && !camera) continue;
377
+
378
+ const context = { camera, width, height, textureData, vertexShader, programs, fog: settings.fog };
379
+ const vertices = new VertexReader(mesh, material, context);
380
+ const count = mesh.index ? mesh.index.length : mesh.attributes.position.data.length / 3;
381
+ const start = Math.max(0, group.start);
382
+ const end = Math.min(group.start + group.count, count);
383
+ const at = (offset: number) => (mesh.index ? mesh.index[offset] : offset);
384
+
385
+ if (!mesh.primitive) {
386
+ // A mirroring transform turns the winding around; Three flips its front face to match, and so does this.
387
+ const mirrored = determinant3(mesh.matrixWorld) < 0;
388
+ // Instanced geometry drawn by a shader (Line2, particles built in a vertex shader): the template runs once per
389
+ // instance, reading the per-instance attributes at that instance.
390
+ const instancing = vertexShader ? instancedAttributes(mesh) : null;
391
+
392
+ for (let instance = 0; instance < (instancing?.count ?? 1); instance++) {
393
+ const reader = instancing ? new VertexReader(mesh, material, context, instancing.names, instance) : vertices;
394
+
395
+ for (let v = start; v + 2 < end; v += 3) {
396
+ const a = reader.corner(at(v)), b = reader.corner(at(v + 1)), c = reader.corner(at(v + 2));
397
+
398
+ if (!a || !b || !c) continue;
399
+
400
+ writeTriangle(mirrored ? [a, c, b] : [a, b, c], entry);
401
+ }
402
+ }
403
+
404
+ continue;
405
+ }
406
+
407
+ if (mesh.primitive === "points") {
408
+ for (let v = start; v < end; v++)
409
+ for (const triangle of vertices.point(at(v))) writeTriangle(triangle, entry);
410
+
411
+ continue;
412
+ }
413
+
414
+ if (mesh.primitive === "sprite") {
415
+ for (let v = start; v + 2 < end; v += 3) {
416
+ const a = vertices.corner(at(v)), b = vertices.corner(at(v + 1)), c = vertices.corner(at(v + 2));
417
+
418
+ if (a && b && c) writeTriangle([a, b, c], entry);
419
+ }
420
+
421
+ continue;
422
+ }
423
+
424
+ // Lines: segments, a strip, or a strip that closes on itself.
425
+ const segments: [number, number][] = [];
426
+
427
+ if (mesh.primitive === "lineSegments") {
428
+ for (let v = start; v + 1 < end; v += 2) segments.push([at(v), at(v + 1)]);
429
+ } else {
430
+ for (let v = start; v + 1 < end; v++) segments.push([at(v), at(v + 1)]);
431
+
432
+ if (mesh.primitive === "lineLoop" && end - start > 2) segments.push([at(end - 1), at(start)]);
433
+ }
434
+
435
+ for (const [a, b] of segments)
436
+ for (const triangle of vertices.segment(a, b)) writeTriangle(triangle, entry);
437
+ }
438
+ }
439
+
440
+ const properties = entries.map(entry => entry.properties);
441
+
442
+ return {
443
+ positions: positions.view(),
444
+ normals: normals.view(),
445
+ uvs: uvs.view(),
446
+ colors: colors.view(),
447
+ varyings: varyings.view(),
448
+ materials: new Uint32Array(triangleMaterials),
449
+ triangleCount: triangleMaterials.length,
450
+ materialColors: new Float32Array(properties.flatMap(m => [...m.color, m.opacity])),
451
+ // Only a lit or plainly emissive surface is a light: a shader's colour is not known until it runs.
452
+ materialEmissive: new Float32Array(properties.flatMap(m =>
453
+ m.kind === MATERIAL_KIND.shader ? [0, 0, 0, 0] : [...m.emissive, m.emissiveIntensity])),
454
+ materialProperties: properties,
455
+ materialPlacements: entries.map(entry => entry.placement),
456
+ materialCount: properties.length,
457
+ extraWords: new Uint32Array(extra),
458
+ cameraDependent,
459
+ };
460
+ }
461
+
462
+ interface VertexContext {
463
+ camera?: CameraMatrices;
464
+ width: number;
465
+ height: number;
466
+ textureData: ReadonlyMap<string, SerializedTexture>;
467
+ vertexShader: CpuShader | null;
468
+ programs?: ShaderPrograms;
469
+ fog?: SerializedScene["fog"];
470
+ }
471
+
472
+ /** What one vertex of a mesh becomes: computed once, whichever triangles share it. */
473
+ interface ProcessedVertex {
474
+ world: number[];
475
+ normal: number[];
476
+ uv0: number[];
477
+ uv1: number[];
478
+ color: number[];
479
+ varyings: Float32Array | null;
480
+ clip: number[] | null;
481
+ pointSize: number;
482
+ lineDistance: number;
483
+ }
484
+
485
+ /** Reads, deforms and projects the vertices of one mesh group, caching each one. */
486
+ class VertexReader {
487
+ private readonly cache = new Map<number, ProcessedVertex | null>();
488
+ private readonly rotation: Float64Array;
489
+ private readonly program;
490
+ private readonly shaderInputs: Record<string, unknown> | null;
491
+
492
+ constructor(
493
+ private readonly mesh: SerializedMesh,
494
+ private readonly material: SerializedMaterial | undefined,
495
+ private readonly context: VertexContext,
496
+ private readonly perInstance: ReadonlySet<string> = new Set(),
497
+ private readonly instance = 0) {
498
+ this.rotation = normalMatrix(mesh.matrixWorld);
499
+ this.program = material ? context.programs?.fragment(material) ?? null : null;
500
+
501
+ this.shaderInputs = context.vertexShader && material?.shader
502
+ ? {
503
+ ...interpreterUniforms(builtinUniforms(context.camera, mesh.matrixWorld, context.fog)),
504
+ ...material.shader.uniforms,
505
+ ...material.time !== undefined ? { time: material.time } : {},
506
+ }
507
+ : null;
508
+ }
509
+
510
+ private value(name: string): number {
511
+ const value = this.material?.values?.[name];
512
+
513
+ return typeof value === "number" && Number.isFinite(value) ? value : NaN;
514
+ }
515
+
516
+ private sample(id: string | undefined, uv: readonly number[]): number[] | null {
517
+ const texture = id ? this.context.textureData.get(id) : undefined;
518
+
519
+ return texture ? sampleTexture(texture, [uv[0], uv[1]], true) : null;
520
+ }
521
+
522
+ vertex(index: number): ProcessedVertex | null {
523
+ if (this.cache.has(index)) return this.cache.get(index) ?? null;
524
+
525
+ const processed = this.process(index);
526
+
527
+ this.cache.set(index, processed);
528
+
529
+ return processed;
530
+ }
531
+
532
+ private attribute(name: string, index: number): number[] | undefined {
533
+ const attribute = this.mesh.attributes[name];
534
+
535
+ if (!attribute) return undefined;
536
+
537
+ const at = (this.perInstance.has(name) ? this.instance : index) * attribute.itemSize;
538
+
539
+ return Array.from(attribute.data.subarray(at, at + attribute.itemSize));
540
+ }
541
+
542
+ private process(index: number): ProcessedVertex | null {
543
+ const mesh = this.mesh;
544
+ const camera = this.context.camera;
545
+ let position = this.attribute("position", index);
546
+
547
+ if (!position || position.length < 3 || position.some(value => !Number.isFinite(value))) return null;
548
+
549
+ let normal = this.attribute("normal", index) ?? null;
550
+ const uv = this.attribute("uv", index) ?? [0, 0];
551
+ const uv1 = this.attribute("uv1", index) ?? uv;
552
+ const color = this.attribute("color", index);
553
+ const displacement = this.material?.maps?.displacementMap;
554
+
555
+ // Three moves each vertex along its normal by the displacement map, in the vertex shader, before anything else.
556
+ if (displacement) {
557
+ const channel = this.context.textureData.get(displacement)?.channel ?? 0;
558
+ const texel = this.sample(displacement, channel ? uv1 : uv);
559
+ const n = normal ?? [0, 0, 1];
560
+ const scale = Number.isFinite(this.value("displacementScale")) ? this.value("displacementScale") : 1;
561
+ const bias = Number.isFinite(this.value("displacementBias")) ? this.value("displacementBias") : 0;
562
+ const offset = texel ? texel[0] * scale + bias : 0;
563
+
564
+ position = [position[0] + n[0] * offset, position[1] + n[1] * offset, position[2] + n[2] * offset];
565
+ }
566
+
567
+ let clip: number[] | null = null;
568
+ let pointSize = Number.isFinite(this.value("size")) ? this.value("size") : 1;
569
+ let varyings: Float32Array | null = null;
570
+ const shader = this.context.vertexShader;
571
+
572
+ if (shader && this.shaderInputs) {
573
+ const attributes = Object.fromEntries(Object.keys(mesh.attributes).map(name => {
574
+ const value = this.attribute(name, index) ?? [0];
575
+
576
+ return [name, value.length === 1 ? value[0] : value];
577
+ }));
578
+
579
+ const result = shader.run({
580
+ ...this.shaderInputs,
581
+ ...attributes,
582
+ uPointScale: this.context.height / (2 * Math.tan((camera?.fov ?? 50) * Math.PI / 360)),
583
+ gl_PointSize: 1,
584
+ position,
585
+ normal: normal ?? [0, 0, 1],
586
+ transformed: position,
587
+ objectNormal: normal ?? [0, 0, 1],
588
+ cpuPosition: position,
589
+ cpuNormal: normal ?? [0, 0, 1],
590
+ gl_Position: [NaN, NaN, NaN, NaN],
591
+ gl_VertexID: index,
592
+ } as never, {
593
+ texture: (sampler, coordinate) => {
594
+ const id = sampler && typeof sampler === "object" && !Array.isArray(sampler)
595
+ ? (sampler as { texture?: unknown }).texture : undefined;
596
+
597
+ const texture = typeof id === "string" ? this.context.textureData.get(id) : undefined;
598
+
599
+ return texture ? sampleTexture(texture, coordinate, false) : [0, 0, 0, 1];
600
+ },
601
+ });
602
+
603
+ if (!result) return null;
604
+
605
+ const projected = flatten(result.gl_Position);
606
+
607
+ if (this.material?.shader?.patched) {
608
+ position = flatten(result.cpuPosition)?.slice(0, 3) ?? position;
609
+ normal = flatten(result.cpuNormal)?.slice(0, 3) ?? normal;
610
+
611
+ if (projected?.length === 4 && projected.every(Number.isFinite)) clip = projected;
612
+ } else if (projected?.length === 4 && projected.every(Number.isFinite)) {
613
+ clip = projected;
614
+ } else {
615
+ return null;
616
+ }
617
+
618
+ const size = Number(result.gl_PointSize ?? 1);
619
+
620
+ if (Number.isFinite(size)) pointSize = size;
621
+
622
+ if (this.program) {
623
+ varyings = new Float32Array(this.program.varyingStride);
624
+
625
+ for (const varying of this.program.varyings) {
626
+ const values = flatten(result[varying.name]);
627
+
628
+ if (values) for (let i = 0; i < varying.size; i++) varyings[varying.offset + i] = values[i] ?? 0;
629
+ }
630
+ }
631
+ }
632
+
633
+ const world = new Float32Array(3);
634
+
635
+ transformPoint(mesh.matrixWorld as number[], position[0], position[1], position[2], world, 0);
636
+
637
+ let worldPosition = Array.from(world);
638
+
639
+ // A vertex shader's clip position is the truth for where the vertex is: displacement and billboards both live there.
640
+ if (clip && camera && clip[3] !== 0) worldPosition = clipToWorld(camera, clip);
641
+
642
+ const worldNormal = new Float32Array(3);
643
+
644
+ if (normal) transformNormal(this.rotation, normal[0], normal[1], normal[2], worldNormal, 0);
645
+
646
+ const lineDistance = (this.attribute("lineDistance", index)?.[0] ?? 0)
647
+ * (Number.isFinite(this.value("scale")) ? this.value("scale") : 1);
648
+
649
+ return {
650
+ world: worldPosition,
651
+ normal: normal ? Array.from(worldNormal) : [],
652
+ uv0: uv.slice(0, 2),
653
+ uv1: uv1.slice(0, 2),
654
+ color: color ? [color[0] ?? 1, color[1] ?? 1, color[2] ?? 1, color.length >= 4 ? color[3] : 1] : [1, 1, 1, 1],
655
+ varyings,
656
+ clip,
657
+ pointSize,
658
+ lineDistance,
659
+ };
660
+ }
661
+
662
+ /** A mesh corner, or a sprite's, which faces the camera. */
663
+ corner(index: number): Corner | null {
664
+ const vertex = this.vertex(index);
665
+
666
+ if (!vertex) return null;
667
+
668
+ if (this.mesh.primitive === "sprite" && !this.context.vertexShader) return this.spriteCorner(index, vertex);
669
+
670
+ return {
671
+ position: vertex.world,
672
+ normal: vertex.normal,
673
+ uv0: vertex.uv0,
674
+ uv1: vertex.uv1,
675
+ color: vertex.color,
676
+ varyings: vertex.varyings,
677
+ };
678
+ }
679
+
680
+ /** Three's sprite vertex shader: the quad is laid out in view space around the sprite's centre, then rotated. */
681
+ private spriteCorner(index: number, vertex: ProcessedVertex): Corner | null {
682
+ const camera = this.context.camera;
683
+ const position = this.attribute("position", index);
684
+
685
+ if (!camera || !position) return null;
686
+
687
+ const m = this.mesh.matrixWorld;
688
+ const centre = transform4(camera.view, m[12], m[13], m[14]);
689
+ const attenuation = !camera.orthographic && this.material?.values?.sizeAttenuation === false ? -centre[2] : 1;
690
+ const sx = Math.hypot(m[0], m[1], m[2]) * attenuation;
691
+ const sy = Math.hypot(m[4], m[5], m[6]) * attenuation;
692
+ const pivot = this.mesh.center ?? [0.5, 0.5];
693
+ const x = (position[0] - (pivot[0] - 0.5)) * sx;
694
+ const y = (position[1] - (pivot[1] - 0.5)) * sy;
695
+ const rotation = Number.isFinite(this.value("rotation")) ? this.value("rotation") : 0;
696
+ const c = Math.cos(rotation), s = Math.sin(rotation);
697
+ const view = [centre[0] + x * c - y * s, centre[1] + x * s + y * c, centre[2]];
698
+ const world = transform4(camera.cameraWorld, view[0], view[1], view[2]).slice(0, 3);
699
+
700
+ return {
701
+ position: world,
702
+ normal: this.towardsCamera(world),
703
+ uv0: vertex.uv0,
704
+ uv1: vertex.uv1,
705
+ color: vertex.color,
706
+ varyings: vertex.varyings,
707
+ };
708
+ }
709
+
710
+ private towardsCamera(point: readonly number[]): number[] {
711
+ const camera = this.context.camera;
712
+
713
+ if (!camera) return [0, 0, 1];
714
+
715
+ if (camera.orthographic) return normalize([camera.cameraWorld[8], camera.cameraWorld[9], camera.cameraWorld[10]]);
716
+
717
+ return normalize([camera.position[0] - point[0], camera.position[1] - point[1], camera.position[2] - point[2]]);
718
+ }
719
+
720
+ private clipOf(vertex: ProcessedVertex): number[] {
721
+ return vertex.clip ?? worldToClip(this.context.camera as CameraMatrices, vertex.world);
722
+ }
723
+
724
+ /** A clip space corner turned back into a world space one, carrying the vertex's attributes. */
725
+ private cornerAt(vertex: ProcessedVertex, clip: number[], uv0?: number[]): Corner {
726
+ const world = clipToWorld(this.context.camera as CameraMatrices, clip);
727
+
728
+ return {
729
+ position: world,
730
+ normal: this.towardsCamera(world),
731
+ uv0: uv0 ?? vertex.uv0,
732
+ uv1: vertex.uv1,
733
+ color: vertex.color,
734
+ varyings: vertex.varyings,
735
+ };
736
+ }
737
+
738
+ /** A point: a square of `size` pixels facing the camera, its UV running over the square as gl_PointCoord does. */
739
+ point(index: number): Corner[][] {
740
+ const vertex = this.vertex(index);
741
+ const camera = this.context.camera;
742
+
743
+ if (!vertex || !camera) return [];
744
+
745
+ const clip = this.clipOf(vertex);
746
+ let size = vertex.pointSize;
747
+
748
+ // Three's points vertex shader scales the size by the distance when the material asks for attenuation.
749
+ if (!this.context.vertexShader && !camera.orthographic && this.material?.values?.sizeAttenuation !== false) {
750
+ const depth = -transform4(camera.view, vertex.world[0], vertex.world[1], vertex.world[2])[2];
751
+
752
+ size *= this.context.height / (2 * depth);
753
+ }
754
+
755
+ if (!(size > 0) || !(clip[3] > 0) || !Number.isFinite(size)) return [];
756
+
757
+ const corners = [[0, 0], [1, 0], [1, 1], [0, 1]].map(([u, v]) => this.cornerAt(vertex, [
758
+ clip[0] + (u * 2 - 1) * size / this.context.width * clip[3],
759
+ clip[1] + (v * 2 - 1) * size / this.context.height * clip[3],
760
+ clip[2],
761
+ clip[3],
762
+ ], [u, v]));
763
+
764
+ return [[corners[0], corners[1], corners[2]], [corners[0], corners[2], corners[3]]];
765
+ }
766
+
767
+ /** A line segment: a quad one pixel wide in screen space, cut into dashes for a dashed material. */
768
+ segment(first: number, second: number): Corner[][] {
769
+ const a = this.vertex(first), b = this.vertex(second);
770
+
771
+ if (!a || !b || !this.context.camera) return [];
772
+
773
+ const dash = this.material?.type === "LineDashedMaterial"
774
+ ? { size: Number.isFinite(this.value("dashSize")) ? this.value("dashSize") : 3,
775
+ gap: Number.isFinite(this.value("gapSize")) ? this.value("gapSize") : 1 }
776
+ : null;
777
+
778
+ const pieces: [number, number][] = [];
779
+
780
+ if (dash && dash.size > 0 && dash.size + dash.gap > 0 && dash.gap > 0) {
781
+ const period = dash.size + dash.gap;
782
+ const from = a.lineDistance, to = b.lineDistance;
783
+ const low = Math.min(from, to), high = Math.max(from, to);
784
+
785
+ if ((high - low) / period > MAX_DASHES) {
786
+ pieces.push([0, 1]);
787
+ } else if (high > low) {
788
+ for (let k = Math.floor(low / period); k * period < high; k++) {
789
+ const start = Math.max(low, k * period), end = Math.min(high, k * period + dash.size);
790
+
791
+ if (end > start) {
792
+ const t0 = (start - from) / (to - from), t1 = (end - from) / (to - from);
793
+
794
+ pieces.push([Math.min(t0, t1), Math.max(t0, t1)]);
795
+ }
796
+ }
797
+ } else if (((low % period) + period) % period <= dash.size) {
798
+ pieces.push([0, 1]);
799
+ }
800
+ } else {
801
+ pieces.push([0, 1]);
802
+ }
803
+
804
+ return pieces.flatMap(([t0, t1]) => this.quad(a, b, t0, t1));
805
+ }
806
+
807
+ private quad(a: ProcessedVertex, b: ProcessedVertex, t0: number, t1: number): Corner[][] {
808
+ const lerp = (x: number[], y: number[], t: number) => x.map((value, i) => value + (y[i] - value) * t);
809
+ const clipA = this.clipOf(a), clipB = this.clipOf(b);
810
+ let start = lerp(clipA, clipB, t0), end = lerp(clipA, clipB, t1);
811
+
812
+ // The centre line is clipped against the near and far planes before the divide, as the rasterizer does.
813
+ for (const sign of [1, -1]) {
814
+ const da = start[3] + sign * start[2], db = end[3] + sign * end[2];
815
+
816
+ if (da < 0 && db < 0) return [];
817
+
818
+ if (da < 0 || db < 0) {
819
+ const t = da / (da - db);
820
+ const cut = lerp(start, end, t);
821
+
822
+ if (da < 0) start = cut;
823
+ else end = cut;
824
+ }
825
+ }
826
+
827
+ if (start[3] <= 0 || end[3] <= 0) return [];
828
+
829
+ const dx = (end[0] / end[3] - start[0] / start[3]) * this.context.width;
830
+ const dy = (end[1] / end[3] - start[1] / start[3]) * this.context.height;
831
+ const length = Math.hypot(dx, dy);
832
+
833
+ if (length === 0) return [];
834
+
835
+ const offset = (clip: number[], sign: number) => [
836
+ clip[0] - dy / length * LINE_WIDTH / this.context.width * clip[3] * sign,
837
+ clip[1] + dx / length * LINE_WIDTH / this.context.height * clip[3] * sign,
838
+ clip[2],
839
+ clip[3],
840
+ ];
841
+
842
+ const vertexAt = (t: number): ProcessedVertex => {
843
+ const w = t0 + (t1 - t0) * t;
844
+
845
+ return {
846
+ ...a,
847
+ world: lerp(a.world, b.world, w),
848
+ uv0: lerp(a.uv0, b.uv0, w),
849
+ uv1: lerp(a.uv1, b.uv1, w),
850
+ color: lerp(a.color, b.color, w),
851
+ varyings: a.varyings && b.varyings
852
+ ? Float32Array.from(a.varyings, (value, i) => value + ((b.varyings as Float32Array)[i] - value) * w)
853
+ : a.varyings,
854
+ };
855
+ };
856
+
857
+ const head = vertexAt(0), tail = vertexAt(1);
858
+
859
+ const corners = [
860
+ this.cornerAt(head, offset(start, -1)),
861
+ this.cornerAt(tail, offset(end, -1)),
862
+ this.cornerAt(tail, offset(end, 1)),
863
+ this.cornerAt(head, offset(start, 1)),
864
+ ];
865
+
866
+ return [[corners[0], corners[1], corners[2]], [corners[0], corners[2], corners[3]]];
867
+ }
868
+ }
869
+
870
+ /**
871
+ * The attributes of an instanced geometry, which the snapshot keeps as plain attributes with one element per instance
872
+ * rather than per vertex, and how many instances there are.
873
+ */
874
+ export function instancedAttributes(mesh: SerializedMesh): { names: Set<string>; count: number } | null {
875
+ const vertices = mesh.attributes.position.data.length / 3;
876
+ const names = new Set<string>();
877
+ let count = Infinity;
878
+
879
+ for (const [name, attribute] of Object.entries(mesh.attributes)) {
880
+ const elements = Math.floor(attribute.data.length / attribute.itemSize);
881
+
882
+ if (elements !== vertices && elements > 0) {
883
+ names.add(name);
884
+ count = Math.min(count, elements);
885
+ }
886
+ }
887
+
888
+ return names.size ? { names, count } : null;
889
+ }
890
+
891
+ /** Flat shading for geometry that arrives without normals: the three vertices share the face normal. */
892
+ export function faceNormal(a: readonly number[], b: readonly number[], c: readonly number[]): number[] {
893
+ const ax = b[0] - a[0], ay = b[1] - a[1], az = b[2] - a[2];
894
+ const bx = c[0] - a[0], by = c[1] - a[1], bz = c[2] - a[2];
895
+
896
+ return normalize([ay * bz - az * by, az * bx - ax * bz, ax * by - ay * bx]);
897
+ }
898
+
899
+ export { NO_PROGRAM };