@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,671 @@
1
+ import { clipToWorld, transform4, worldToClip } from "./camera.js";
2
+ import { isMaterialVisible, MATERIAL_KIND, materialProperties, NO_PLACEMENT, NO_PROGRAM, } from "./materialLayout.js";
3
+ import { normalMatrix, transformNormal, transformPoint } from "./math.js";
4
+ import { matrixValue, sampleTexture } from "./upstream.js";
5
+ /** Three's wireframe and line width: WebGL draws every line one pixel wide whatever the material asks. */
6
+ const LINE_WIDTH = 1;
7
+ /** A dashed line split finer than this is drawn solid rather than as millions of dashes. */
8
+ const MAX_DASHES = 4096;
9
+ /** Grows a float array as triangles are added, since how many a primitive makes is only known as it is built. */
10
+ class FloatStream {
11
+ data;
12
+ length = 0;
13
+ constructor(capacity) {
14
+ this.data = new Float32Array(Math.max(16, capacity));
15
+ }
16
+ reserve(count) {
17
+ if (this.length + count <= this.data.length)
18
+ return;
19
+ const next = new Float32Array(Math.max(this.data.length * 2, this.length + count));
20
+ next.set(this.data.subarray(0, this.length));
21
+ this.data = next;
22
+ }
23
+ push(values, count = values.length) {
24
+ this.reserve(count);
25
+ for (let i = 0; i < count; i++)
26
+ this.data[this.length + i] = values[i] ?? 0;
27
+ this.length += count;
28
+ }
29
+ pad(count) {
30
+ this.reserve(count);
31
+ this.data.fill(0, this.length, this.length + count);
32
+ this.length += count;
33
+ }
34
+ view() {
35
+ return this.data.subarray(0, this.length);
36
+ }
37
+ }
38
+ function triangleCountOf(mesh) {
39
+ const vertices = mesh.index ? mesh.index.length : mesh.attributes.position.data.length / 3;
40
+ const groups = mesh.groups.length
41
+ ? mesh.groups.reduce((total, group) => total + Math.min(group.count, Math.max(0, vertices - group.start)), 0)
42
+ : vertices;
43
+ return Math.floor(groups / 3);
44
+ }
45
+ function determinant3(m) {
46
+ 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]);
47
+ }
48
+ const normalize = (v) => {
49
+ const length = Math.hypot(v[0], v[1], v[2]) || 1;
50
+ return [v[0] / length, v[1] / length, v[2] / length];
51
+ };
52
+ function multiply4(a, b) {
53
+ const out = new Array(16).fill(0);
54
+ for (let column = 0; column < 4; column++)
55
+ for (let row = 0; row < 4; row++)
56
+ for (let k = 0; k < 4; k++)
57
+ out[column * 4 + row] += a[k * 4 + row] * b[column * 4 + k];
58
+ return out;
59
+ }
60
+ /** Three's normalMatrix for a model view matrix: the inverse transpose of its upper 3x3, column-major. */
61
+ function normalMatrix3(m) {
62
+ return Array.from(normalMatrix(m));
63
+ }
64
+ /** The uniforms Three sets on every shader for this camera and mesh. */
65
+ export function builtinUniforms(camera, matrixWorld, fog) {
66
+ const view = camera?.view ?? [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
67
+ const modelView = multiply4(view, matrixWorld);
68
+ return {
69
+ modelMatrix: [...matrixWorld],
70
+ modelViewMatrix: modelView,
71
+ normalMatrix: normalMatrix3(modelView),
72
+ viewMatrix: [...view],
73
+ projectionMatrix: [...camera?.projection ?? [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, -1, 0, 0, -0.2, 0]],
74
+ cameraPosition: [...camera?.position ?? [0, 0, 0]],
75
+ isOrthographic: camera?.orthographic ?? false,
76
+ cameraNear: camera?.near ?? 0.1,
77
+ cameraFar: camera?.far ?? 1000,
78
+ fogColor: fog?.color ?? [0, 0, 0],
79
+ fogNear: fog?.near ?? 0,
80
+ fogFar: fog?.far ?? 0,
81
+ fogDensity: fog?.density ?? 0,
82
+ };
83
+ }
84
+ /** The same uniforms in the form the CPU renderer's interpreter reads, where a matrix is a tagged value. */
85
+ function interpreterUniforms(uniforms) {
86
+ const out = { ...uniforms };
87
+ for (const name of ["modelMatrix", "modelViewMatrix", "normalMatrix", "viewMatrix", "projectionMatrix"])
88
+ out[name] = matrixValue(uniforms[name]);
89
+ return out;
90
+ }
91
+ /** A flat number array from whatever an interpreter returned, or null when it is not one. */
92
+ function flatten(value) {
93
+ if (typeof value === "number")
94
+ return [value];
95
+ if (typeof value === "boolean")
96
+ return [value ? 1 : 0];
97
+ if (Array.isArray(value))
98
+ return value.flatMap(entry => flatten(entry) ?? [0]);
99
+ if (value && typeof value === "object" && "matrix" in value)
100
+ return [...value.matrix];
101
+ return null;
102
+ }
103
+ /**
104
+ * Flattens the scene into triangles, materials resolved and transforms applied. Every triangle carries its own three
105
+ * vertices: the path tracer reads a hit through its triangle index, and sharing vertices would cost an indirection on
106
+ * the hottest path of the kernel for memory that low poly scenes do not miss.
107
+ *
108
+ * Points, lines and sprites become triangles that face the camera, sized the way Three draws them, so a path has a
109
+ * surface to find; vertex shaders run here, on the CPU renderer's interpreter, and their varyings ride along per corner.
110
+ */
111
+ export function packGeometry(scene, options = {}) {
112
+ const settings = options instanceof Map ? { textures: options } : options;
113
+ const textures = settings.textures ?? new Map();
114
+ const textureData = settings.textureData ?? new Map();
115
+ const camera = settings.camera;
116
+ const width = Math.max(1, settings.width ?? 1);
117
+ const height = Math.max(1, settings.height ?? 1);
118
+ const programs = settings.programs;
119
+ const extraBase = settings.extraBase ?? 0;
120
+ const meshes = scene.meshes.filter(mesh => mesh.attributes.position);
121
+ const estimate = meshes.reduce((count, mesh) => count + (mesh.primitive ? 0 : triangleCountOf(mesh)), 0);
122
+ const anyVertexColors = meshes.some(mesh => mesh.attributes.color && mesh.materials.some(m => m?.values?.vertexColors === true));
123
+ const positions = new FloatStream(estimate * 9);
124
+ const normals = new FloatStream(estimate * 9);
125
+ const uvs = new FloatStream(estimate * 12);
126
+ const colors = new FloatStream(anyVertexColors ? estimate * 12 : 0);
127
+ const varyings = new FloatStream(0);
128
+ const triangleMaterials = [];
129
+ const entries = [];
130
+ const shared = new Map();
131
+ const extra = [];
132
+ let cameraDependent = false;
133
+ /** A material's clipping planes, written once to the extra words. */
134
+ const clipPlanes = new Map();
135
+ const planesOf = (material) => {
136
+ const planes = material?.values?.clippingPlanes;
137
+ if (!material || !Array.isArray(planes) || !planes.length)
138
+ return { start: 0, count: 0 };
139
+ let known = clipPlanes.get(material);
140
+ if (!known) {
141
+ known = { start: extraBase + extra.length, count: 0 };
142
+ for (const plane of planes) {
143
+ if (!Array.isArray(plane) || plane.length < 4)
144
+ continue;
145
+ const floats = new Float32Array(plane.slice(0, 4));
146
+ extra.push(...new Uint32Array(floats.buffer));
147
+ known.count++;
148
+ }
149
+ clipPlanes.set(material, known);
150
+ }
151
+ return known;
152
+ };
153
+ /** One entry per material, or one per mesh and group when the entry carries something of the mesh. */
154
+ const entryFor = (material, mesh) => {
155
+ const program = material ? programs?.fragment(material) ?? null : null;
156
+ const perMesh = !!program || material?.values?.normalMapType === 1;
157
+ if (!perMesh) {
158
+ const known = shared.get(material);
159
+ if (known)
160
+ return known;
161
+ }
162
+ const properties = materialProperties(material, textures);
163
+ const planes = planesOf(material);
164
+ // Points, lines and sprites always face the camera, so Three never culls them.
165
+ if (mesh.primitive)
166
+ properties.side = 2;
167
+ const placement = {
168
+ ...NO_PLACEMENT,
169
+ clipStart: planes.start,
170
+ clipCount: planes.count,
171
+ normalMatrix: perMesh ? Array.from(normalMatrix(mesh.matrixWorld)) : NO_PLACEMENT.normalMatrix,
172
+ };
173
+ if (program && material) {
174
+ const uniforms = {
175
+ ...builtinUniforms(camera, mesh.matrixWorld, settings.fog),
176
+ ...material.shader?.uniforms,
177
+ ...material.time !== undefined ? { time: material.time } : {},
178
+ };
179
+ const words = program.packUniforms(uniforms, textures);
180
+ placement.program = program.id;
181
+ placement.shaderData = extraBase + extra.length;
182
+ placement.varyingStride = program.varyingStride;
183
+ placement.varyingBase = varyings.length;
184
+ placement.firstTriangle = triangleMaterials.length;
185
+ for (let i = 0; i < words.length; i++)
186
+ extra.push(words[i]);
187
+ const after = programs?.after(material) ?? null;
188
+ if (after) {
189
+ const afterWords = after.packUniforms(uniforms, textures);
190
+ placement.afterProgram = after.id;
191
+ placement.afterData = extraBase + extra.length;
192
+ for (let i = 0; i < afterWords.length; i++)
193
+ extra.push(afterWords[i]);
194
+ }
195
+ }
196
+ const entry = { index: entries.length, properties, placement };
197
+ entries.push(entry);
198
+ if (!perMesh)
199
+ shared.set(material, entry);
200
+ return entry;
201
+ };
202
+ const writeTriangle = (corners, entry) => {
203
+ // Geometry without normals is shaded flat: the three corners share the face normal.
204
+ const flat = corners.some(corner => corner.normal.length < 3)
205
+ ? faceNormal(corners[0].position, corners[1].position, corners[2].position)
206
+ : null;
207
+ for (const corner of corners) {
208
+ positions.push(corner.position, 3);
209
+ normals.push(flat ?? corner.normal, 3);
210
+ uvs.push([corner.uv0[0], corner.uv0[1], corner.uv1[0], corner.uv1[1]], 4);
211
+ if (anyVertexColors)
212
+ colors.push(corner.color, 4);
213
+ }
214
+ const stride = entry.placement.varyingStride;
215
+ if (stride > 0)
216
+ for (const corner of corners) {
217
+ if (corner.varyings)
218
+ varyings.push(corner.varyings, stride);
219
+ else
220
+ varyings.pad(stride);
221
+ }
222
+ triangleMaterials.push(entry.index);
223
+ };
224
+ for (const mesh of meshes) {
225
+ const groups = mesh.groups.length
226
+ ? mesh.groups
227
+ : [{
228
+ start: 0,
229
+ count: mesh.index ? mesh.index.length : mesh.attributes.position.data.length / 3,
230
+ materialIndex: 0,
231
+ }];
232
+ if (mesh.primitive)
233
+ cameraDependent = true;
234
+ for (const group of groups) {
235
+ const material = mesh.materials[group.materialIndex] ?? mesh.materials[0];
236
+ if (!isMaterialVisible(material))
237
+ continue;
238
+ const entry = entryFor(material, mesh);
239
+ const vertexShader = material ? programs?.vertex(material) ?? null : null;
240
+ if (vertexShader)
241
+ cameraDependent = true;
242
+ // Primitives that face the camera need one to face; without it there is nothing sensible to build.
243
+ if (mesh.primitive && !camera)
244
+ continue;
245
+ const context = { camera, width, height, textureData, vertexShader, programs, fog: settings.fog };
246
+ const vertices = new VertexReader(mesh, material, context);
247
+ const count = mesh.index ? mesh.index.length : mesh.attributes.position.data.length / 3;
248
+ const start = Math.max(0, group.start);
249
+ const end = Math.min(group.start + group.count, count);
250
+ const at = (offset) => (mesh.index ? mesh.index[offset] : offset);
251
+ if (!mesh.primitive) {
252
+ // A mirroring transform turns the winding around; Three flips its front face to match, and so does this.
253
+ const mirrored = determinant3(mesh.matrixWorld) < 0;
254
+ // Instanced geometry drawn by a shader (Line2, particles built in a vertex shader): the template runs once per
255
+ // instance, reading the per-instance attributes at that instance.
256
+ const instancing = vertexShader ? instancedAttributes(mesh) : null;
257
+ for (let instance = 0; instance < (instancing?.count ?? 1); instance++) {
258
+ const reader = instancing ? new VertexReader(mesh, material, context, instancing.names, instance) : vertices;
259
+ for (let v = start; v + 2 < end; v += 3) {
260
+ const a = reader.corner(at(v)), b = reader.corner(at(v + 1)), c = reader.corner(at(v + 2));
261
+ if (!a || !b || !c)
262
+ continue;
263
+ writeTriangle(mirrored ? [a, c, b] : [a, b, c], entry);
264
+ }
265
+ }
266
+ continue;
267
+ }
268
+ if (mesh.primitive === "points") {
269
+ for (let v = start; v < end; v++)
270
+ for (const triangle of vertices.point(at(v)))
271
+ writeTriangle(triangle, entry);
272
+ continue;
273
+ }
274
+ if (mesh.primitive === "sprite") {
275
+ for (let v = start; v + 2 < end; v += 3) {
276
+ const a = vertices.corner(at(v)), b = vertices.corner(at(v + 1)), c = vertices.corner(at(v + 2));
277
+ if (a && b && c)
278
+ writeTriangle([a, b, c], entry);
279
+ }
280
+ continue;
281
+ }
282
+ // Lines: segments, a strip, or a strip that closes on itself.
283
+ const segments = [];
284
+ if (mesh.primitive === "lineSegments") {
285
+ for (let v = start; v + 1 < end; v += 2)
286
+ segments.push([at(v), at(v + 1)]);
287
+ }
288
+ else {
289
+ for (let v = start; v + 1 < end; v++)
290
+ segments.push([at(v), at(v + 1)]);
291
+ if (mesh.primitive === "lineLoop" && end - start > 2)
292
+ segments.push([at(end - 1), at(start)]);
293
+ }
294
+ for (const [a, b] of segments)
295
+ for (const triangle of vertices.segment(a, b))
296
+ writeTriangle(triangle, entry);
297
+ }
298
+ }
299
+ const properties = entries.map(entry => entry.properties);
300
+ return {
301
+ positions: positions.view(),
302
+ normals: normals.view(),
303
+ uvs: uvs.view(),
304
+ colors: colors.view(),
305
+ varyings: varyings.view(),
306
+ materials: new Uint32Array(triangleMaterials),
307
+ triangleCount: triangleMaterials.length,
308
+ materialColors: new Float32Array(properties.flatMap(m => [...m.color, m.opacity])),
309
+ // Only a lit or plainly emissive surface is a light: a shader's colour is not known until it runs.
310
+ materialEmissive: new Float32Array(properties.flatMap(m => m.kind === MATERIAL_KIND.shader ? [0, 0, 0, 0] : [...m.emissive, m.emissiveIntensity])),
311
+ materialProperties: properties,
312
+ materialPlacements: entries.map(entry => entry.placement),
313
+ materialCount: properties.length,
314
+ extraWords: new Uint32Array(extra),
315
+ cameraDependent,
316
+ };
317
+ }
318
+ /** Reads, deforms and projects the vertices of one mesh group, caching each one. */
319
+ class VertexReader {
320
+ mesh;
321
+ material;
322
+ context;
323
+ perInstance;
324
+ instance;
325
+ cache = new Map();
326
+ rotation;
327
+ program;
328
+ shaderInputs;
329
+ constructor(mesh, material, context, perInstance = new Set(), instance = 0) {
330
+ this.mesh = mesh;
331
+ this.material = material;
332
+ this.context = context;
333
+ this.perInstance = perInstance;
334
+ this.instance = instance;
335
+ this.rotation = normalMatrix(mesh.matrixWorld);
336
+ this.program = material ? context.programs?.fragment(material) ?? null : null;
337
+ this.shaderInputs = context.vertexShader && material?.shader
338
+ ? {
339
+ ...interpreterUniforms(builtinUniforms(context.camera, mesh.matrixWorld, context.fog)),
340
+ ...material.shader.uniforms,
341
+ ...material.time !== undefined ? { time: material.time } : {},
342
+ }
343
+ : null;
344
+ }
345
+ value(name) {
346
+ const value = this.material?.values?.[name];
347
+ return typeof value === "number" && Number.isFinite(value) ? value : NaN;
348
+ }
349
+ sample(id, uv) {
350
+ const texture = id ? this.context.textureData.get(id) : undefined;
351
+ return texture ? sampleTexture(texture, [uv[0], uv[1]], true) : null;
352
+ }
353
+ vertex(index) {
354
+ if (this.cache.has(index))
355
+ return this.cache.get(index) ?? null;
356
+ const processed = this.process(index);
357
+ this.cache.set(index, processed);
358
+ return processed;
359
+ }
360
+ attribute(name, index) {
361
+ const attribute = this.mesh.attributes[name];
362
+ if (!attribute)
363
+ return undefined;
364
+ const at = (this.perInstance.has(name) ? this.instance : index) * attribute.itemSize;
365
+ return Array.from(attribute.data.subarray(at, at + attribute.itemSize));
366
+ }
367
+ process(index) {
368
+ const mesh = this.mesh;
369
+ const camera = this.context.camera;
370
+ let position = this.attribute("position", index);
371
+ if (!position || position.length < 3 || position.some(value => !Number.isFinite(value)))
372
+ return null;
373
+ let normal = this.attribute("normal", index) ?? null;
374
+ const uv = this.attribute("uv", index) ?? [0, 0];
375
+ const uv1 = this.attribute("uv1", index) ?? uv;
376
+ const color = this.attribute("color", index);
377
+ const displacement = this.material?.maps?.displacementMap;
378
+ // Three moves each vertex along its normal by the displacement map, in the vertex shader, before anything else.
379
+ if (displacement) {
380
+ const channel = this.context.textureData.get(displacement)?.channel ?? 0;
381
+ const texel = this.sample(displacement, channel ? uv1 : uv);
382
+ const n = normal ?? [0, 0, 1];
383
+ const scale = Number.isFinite(this.value("displacementScale")) ? this.value("displacementScale") : 1;
384
+ const bias = Number.isFinite(this.value("displacementBias")) ? this.value("displacementBias") : 0;
385
+ const offset = texel ? texel[0] * scale + bias : 0;
386
+ position = [position[0] + n[0] * offset, position[1] + n[1] * offset, position[2] + n[2] * offset];
387
+ }
388
+ let clip = null;
389
+ let pointSize = Number.isFinite(this.value("size")) ? this.value("size") : 1;
390
+ let varyings = null;
391
+ const shader = this.context.vertexShader;
392
+ if (shader && this.shaderInputs) {
393
+ const attributes = Object.fromEntries(Object.keys(mesh.attributes).map(name => {
394
+ const value = this.attribute(name, index) ?? [0];
395
+ return [name, value.length === 1 ? value[0] : value];
396
+ }));
397
+ const result = shader.run({
398
+ ...this.shaderInputs,
399
+ ...attributes,
400
+ uPointScale: this.context.height / (2 * Math.tan((camera?.fov ?? 50) * Math.PI / 360)),
401
+ gl_PointSize: 1,
402
+ position,
403
+ normal: normal ?? [0, 0, 1],
404
+ transformed: position,
405
+ objectNormal: normal ?? [0, 0, 1],
406
+ cpuPosition: position,
407
+ cpuNormal: normal ?? [0, 0, 1],
408
+ gl_Position: [NaN, NaN, NaN, NaN],
409
+ gl_VertexID: index,
410
+ }, {
411
+ texture: (sampler, coordinate) => {
412
+ const id = sampler && typeof sampler === "object" && !Array.isArray(sampler)
413
+ ? sampler.texture : undefined;
414
+ const texture = typeof id === "string" ? this.context.textureData.get(id) : undefined;
415
+ return texture ? sampleTexture(texture, coordinate, false) : [0, 0, 0, 1];
416
+ },
417
+ });
418
+ if (!result)
419
+ return null;
420
+ const projected = flatten(result.gl_Position);
421
+ if (this.material?.shader?.patched) {
422
+ position = flatten(result.cpuPosition)?.slice(0, 3) ?? position;
423
+ normal = flatten(result.cpuNormal)?.slice(0, 3) ?? normal;
424
+ if (projected?.length === 4 && projected.every(Number.isFinite))
425
+ clip = projected;
426
+ }
427
+ else if (projected?.length === 4 && projected.every(Number.isFinite)) {
428
+ clip = projected;
429
+ }
430
+ else {
431
+ return null;
432
+ }
433
+ const size = Number(result.gl_PointSize ?? 1);
434
+ if (Number.isFinite(size))
435
+ pointSize = size;
436
+ if (this.program) {
437
+ varyings = new Float32Array(this.program.varyingStride);
438
+ for (const varying of this.program.varyings) {
439
+ const values = flatten(result[varying.name]);
440
+ if (values)
441
+ for (let i = 0; i < varying.size; i++)
442
+ varyings[varying.offset + i] = values[i] ?? 0;
443
+ }
444
+ }
445
+ }
446
+ const world = new Float32Array(3);
447
+ transformPoint(mesh.matrixWorld, position[0], position[1], position[2], world, 0);
448
+ let worldPosition = Array.from(world);
449
+ // A vertex shader's clip position is the truth for where the vertex is: displacement and billboards both live there.
450
+ if (clip && camera && clip[3] !== 0)
451
+ worldPosition = clipToWorld(camera, clip);
452
+ const worldNormal = new Float32Array(3);
453
+ if (normal)
454
+ transformNormal(this.rotation, normal[0], normal[1], normal[2], worldNormal, 0);
455
+ const lineDistance = (this.attribute("lineDistance", index)?.[0] ?? 0)
456
+ * (Number.isFinite(this.value("scale")) ? this.value("scale") : 1);
457
+ return {
458
+ world: worldPosition,
459
+ normal: normal ? Array.from(worldNormal) : [],
460
+ uv0: uv.slice(0, 2),
461
+ uv1: uv1.slice(0, 2),
462
+ color: color ? [color[0] ?? 1, color[1] ?? 1, color[2] ?? 1, color.length >= 4 ? color[3] : 1] : [1, 1, 1, 1],
463
+ varyings,
464
+ clip,
465
+ pointSize,
466
+ lineDistance,
467
+ };
468
+ }
469
+ /** A mesh corner, or a sprite's, which faces the camera. */
470
+ corner(index) {
471
+ const vertex = this.vertex(index);
472
+ if (!vertex)
473
+ return null;
474
+ if (this.mesh.primitive === "sprite" && !this.context.vertexShader)
475
+ return this.spriteCorner(index, vertex);
476
+ return {
477
+ position: vertex.world,
478
+ normal: vertex.normal,
479
+ uv0: vertex.uv0,
480
+ uv1: vertex.uv1,
481
+ color: vertex.color,
482
+ varyings: vertex.varyings,
483
+ };
484
+ }
485
+ /** Three's sprite vertex shader: the quad is laid out in view space around the sprite's centre, then rotated. */
486
+ spriteCorner(index, vertex) {
487
+ const camera = this.context.camera;
488
+ const position = this.attribute("position", index);
489
+ if (!camera || !position)
490
+ return null;
491
+ const m = this.mesh.matrixWorld;
492
+ const centre = transform4(camera.view, m[12], m[13], m[14]);
493
+ const attenuation = !camera.orthographic && this.material?.values?.sizeAttenuation === false ? -centre[2] : 1;
494
+ const sx = Math.hypot(m[0], m[1], m[2]) * attenuation;
495
+ const sy = Math.hypot(m[4], m[5], m[6]) * attenuation;
496
+ const pivot = this.mesh.center ?? [0.5, 0.5];
497
+ const x = (position[0] - (pivot[0] - 0.5)) * sx;
498
+ const y = (position[1] - (pivot[1] - 0.5)) * sy;
499
+ const rotation = Number.isFinite(this.value("rotation")) ? this.value("rotation") : 0;
500
+ const c = Math.cos(rotation), s = Math.sin(rotation);
501
+ const view = [centre[0] + x * c - y * s, centre[1] + x * s + y * c, centre[2]];
502
+ const world = transform4(camera.cameraWorld, view[0], view[1], view[2]).slice(0, 3);
503
+ return {
504
+ position: world,
505
+ normal: this.towardsCamera(world),
506
+ uv0: vertex.uv0,
507
+ uv1: vertex.uv1,
508
+ color: vertex.color,
509
+ varyings: vertex.varyings,
510
+ };
511
+ }
512
+ towardsCamera(point) {
513
+ const camera = this.context.camera;
514
+ if (!camera)
515
+ return [0, 0, 1];
516
+ if (camera.orthographic)
517
+ return normalize([camera.cameraWorld[8], camera.cameraWorld[9], camera.cameraWorld[10]]);
518
+ return normalize([camera.position[0] - point[0], camera.position[1] - point[1], camera.position[2] - point[2]]);
519
+ }
520
+ clipOf(vertex) {
521
+ return vertex.clip ?? worldToClip(this.context.camera, vertex.world);
522
+ }
523
+ /** A clip space corner turned back into a world space one, carrying the vertex's attributes. */
524
+ cornerAt(vertex, clip, uv0) {
525
+ const world = clipToWorld(this.context.camera, clip);
526
+ return {
527
+ position: world,
528
+ normal: this.towardsCamera(world),
529
+ uv0: uv0 ?? vertex.uv0,
530
+ uv1: vertex.uv1,
531
+ color: vertex.color,
532
+ varyings: vertex.varyings,
533
+ };
534
+ }
535
+ /** A point: a square of `size` pixels facing the camera, its UV running over the square as gl_PointCoord does. */
536
+ point(index) {
537
+ const vertex = this.vertex(index);
538
+ const camera = this.context.camera;
539
+ if (!vertex || !camera)
540
+ return [];
541
+ const clip = this.clipOf(vertex);
542
+ let size = vertex.pointSize;
543
+ // Three's points vertex shader scales the size by the distance when the material asks for attenuation.
544
+ if (!this.context.vertexShader && !camera.orthographic && this.material?.values?.sizeAttenuation !== false) {
545
+ const depth = -transform4(camera.view, vertex.world[0], vertex.world[1], vertex.world[2])[2];
546
+ size *= this.context.height / (2 * depth);
547
+ }
548
+ if (!(size > 0) || !(clip[3] > 0) || !Number.isFinite(size))
549
+ return [];
550
+ const corners = [[0, 0], [1, 0], [1, 1], [0, 1]].map(([u, v]) => this.cornerAt(vertex, [
551
+ clip[0] + (u * 2 - 1) * size / this.context.width * clip[3],
552
+ clip[1] + (v * 2 - 1) * size / this.context.height * clip[3],
553
+ clip[2],
554
+ clip[3],
555
+ ], [u, v]));
556
+ return [[corners[0], corners[1], corners[2]], [corners[0], corners[2], corners[3]]];
557
+ }
558
+ /** A line segment: a quad one pixel wide in screen space, cut into dashes for a dashed material. */
559
+ segment(first, second) {
560
+ const a = this.vertex(first), b = this.vertex(second);
561
+ if (!a || !b || !this.context.camera)
562
+ return [];
563
+ const dash = this.material?.type === "LineDashedMaterial"
564
+ ? { size: Number.isFinite(this.value("dashSize")) ? this.value("dashSize") : 3,
565
+ gap: Number.isFinite(this.value("gapSize")) ? this.value("gapSize") : 1 }
566
+ : null;
567
+ const pieces = [];
568
+ if (dash && dash.size > 0 && dash.size + dash.gap > 0 && dash.gap > 0) {
569
+ const period = dash.size + dash.gap;
570
+ const from = a.lineDistance, to = b.lineDistance;
571
+ const low = Math.min(from, to), high = Math.max(from, to);
572
+ if ((high - low) / period > MAX_DASHES) {
573
+ pieces.push([0, 1]);
574
+ }
575
+ else if (high > low) {
576
+ for (let k = Math.floor(low / period); k * period < high; k++) {
577
+ const start = Math.max(low, k * period), end = Math.min(high, k * period + dash.size);
578
+ if (end > start) {
579
+ const t0 = (start - from) / (to - from), t1 = (end - from) / (to - from);
580
+ pieces.push([Math.min(t0, t1), Math.max(t0, t1)]);
581
+ }
582
+ }
583
+ }
584
+ else if (((low % period) + period) % period <= dash.size) {
585
+ pieces.push([0, 1]);
586
+ }
587
+ }
588
+ else {
589
+ pieces.push([0, 1]);
590
+ }
591
+ return pieces.flatMap(([t0, t1]) => this.quad(a, b, t0, t1));
592
+ }
593
+ quad(a, b, t0, t1) {
594
+ const lerp = (x, y, t) => x.map((value, i) => value + (y[i] - value) * t);
595
+ const clipA = this.clipOf(a), clipB = this.clipOf(b);
596
+ let start = lerp(clipA, clipB, t0), end = lerp(clipA, clipB, t1);
597
+ // The centre line is clipped against the near and far planes before the divide, as the rasterizer does.
598
+ for (const sign of [1, -1]) {
599
+ const da = start[3] + sign * start[2], db = end[3] + sign * end[2];
600
+ if (da < 0 && db < 0)
601
+ return [];
602
+ if (da < 0 || db < 0) {
603
+ const t = da / (da - db);
604
+ const cut = lerp(start, end, t);
605
+ if (da < 0)
606
+ start = cut;
607
+ else
608
+ end = cut;
609
+ }
610
+ }
611
+ if (start[3] <= 0 || end[3] <= 0)
612
+ return [];
613
+ const dx = (end[0] / end[3] - start[0] / start[3]) * this.context.width;
614
+ const dy = (end[1] / end[3] - start[1] / start[3]) * this.context.height;
615
+ const length = Math.hypot(dx, dy);
616
+ if (length === 0)
617
+ return [];
618
+ const offset = (clip, sign) => [
619
+ clip[0] - dy / length * LINE_WIDTH / this.context.width * clip[3] * sign,
620
+ clip[1] + dx / length * LINE_WIDTH / this.context.height * clip[3] * sign,
621
+ clip[2],
622
+ clip[3],
623
+ ];
624
+ const vertexAt = (t) => {
625
+ const w = t0 + (t1 - t0) * t;
626
+ return {
627
+ ...a,
628
+ world: lerp(a.world, b.world, w),
629
+ uv0: lerp(a.uv0, b.uv0, w),
630
+ uv1: lerp(a.uv1, b.uv1, w),
631
+ color: lerp(a.color, b.color, w),
632
+ varyings: a.varyings && b.varyings
633
+ ? Float32Array.from(a.varyings, (value, i) => value + (b.varyings[i] - value) * w)
634
+ : a.varyings,
635
+ };
636
+ };
637
+ const head = vertexAt(0), tail = vertexAt(1);
638
+ const corners = [
639
+ this.cornerAt(head, offset(start, -1)),
640
+ this.cornerAt(tail, offset(end, -1)),
641
+ this.cornerAt(tail, offset(end, 1)),
642
+ this.cornerAt(head, offset(start, 1)),
643
+ ];
644
+ return [[corners[0], corners[1], corners[2]], [corners[0], corners[2], corners[3]]];
645
+ }
646
+ }
647
+ /**
648
+ * The attributes of an instanced geometry, which the snapshot keeps as plain attributes with one element per instance
649
+ * rather than per vertex, and how many instances there are.
650
+ */
651
+ export function instancedAttributes(mesh) {
652
+ const vertices = mesh.attributes.position.data.length / 3;
653
+ const names = new Set();
654
+ let count = Infinity;
655
+ for (const [name, attribute] of Object.entries(mesh.attributes)) {
656
+ const elements = Math.floor(attribute.data.length / attribute.itemSize);
657
+ if (elements !== vertices && elements > 0) {
658
+ names.add(name);
659
+ count = Math.min(count, elements);
660
+ }
661
+ }
662
+ return names.size ? { names, count } : null;
663
+ }
664
+ /** Flat shading for geometry that arrives without normals: the three vertices share the face normal. */
665
+ export function faceNormal(a, b, c) {
666
+ const ax = b[0] - a[0], ay = b[1] - a[1], az = b[2] - a[2];
667
+ const bx = c[0] - a[0], by = c[1] - a[1], bz = c[2] - a[2];
668
+ return normalize([ay * bz - az * by, az * bx - ax * bz, ax * by - ay * bx]);
669
+ }
670
+ export { NO_PROGRAM };
671
+ //# sourceMappingURL=geometry.js.map