topazcube 0.1.31 → 0.1.35

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 (103) hide show
  1. package/LICENSE.txt +0 -0
  2. package/README.md +0 -0
  3. package/dist/Renderer.cjs +20844 -0
  4. package/dist/Renderer.cjs.map +1 -0
  5. package/dist/Renderer.js +20827 -0
  6. package/dist/Renderer.js.map +1 -0
  7. package/dist/client.cjs +91 -260
  8. package/dist/client.cjs.map +1 -1
  9. package/dist/client.js +68 -215
  10. package/dist/client.js.map +1 -1
  11. package/dist/server.cjs +165 -432
  12. package/dist/server.cjs.map +1 -1
  13. package/dist/server.js +117 -370
  14. package/dist/server.js.map +1 -1
  15. package/dist/terminal.cjs +113 -200
  16. package/dist/terminal.cjs.map +1 -1
  17. package/dist/terminal.js +50 -51
  18. package/dist/terminal.js.map +1 -1
  19. package/dist/utils-CRhi1BDa.cjs +259 -0
  20. package/dist/utils-CRhi1BDa.cjs.map +1 -0
  21. package/dist/utils-D7tXt6-2.js +260 -0
  22. package/dist/utils-D7tXt6-2.js.map +1 -0
  23. package/package.json +19 -15
  24. package/src/{client.ts → network/client.js} +170 -403
  25. package/src/{compress-browser.ts → network/compress-browser.js} +2 -4
  26. package/src/{compress-node.ts → network/compress-node.js} +8 -14
  27. package/src/{server.ts → network/server.js} +229 -317
  28. package/src/{terminal.js → network/terminal.js} +0 -0
  29. package/src/{topazcube.ts → network/topazcube.js} +2 -2
  30. package/src/network/utils.js +375 -0
  31. package/src/renderer/Camera.js +191 -0
  32. package/src/renderer/DebugUI.js +703 -0
  33. package/src/renderer/Geometry.js +1049 -0
  34. package/src/renderer/Material.js +64 -0
  35. package/src/renderer/Mesh.js +211 -0
  36. package/src/renderer/Node.js +112 -0
  37. package/src/renderer/Pipeline.js +645 -0
  38. package/src/renderer/Renderer.js +1496 -0
  39. package/src/renderer/Skin.js +792 -0
  40. package/src/renderer/Texture.js +584 -0
  41. package/src/renderer/core/AssetManager.js +394 -0
  42. package/src/renderer/core/CullingSystem.js +308 -0
  43. package/src/renderer/core/EntityManager.js +541 -0
  44. package/src/renderer/core/InstanceManager.js +343 -0
  45. package/src/renderer/core/ParticleEmitter.js +358 -0
  46. package/src/renderer/core/ParticleSystem.js +564 -0
  47. package/src/renderer/core/SpriteSystem.js +349 -0
  48. package/src/renderer/gltf.js +563 -0
  49. package/src/renderer/math.js +161 -0
  50. package/src/renderer/rendering/HistoryBufferManager.js +333 -0
  51. package/src/renderer/rendering/ProbeCapture.js +1495 -0
  52. package/src/renderer/rendering/ReflectionProbeManager.js +352 -0
  53. package/src/renderer/rendering/RenderGraph.js +2258 -0
  54. package/src/renderer/rendering/passes/AOPass.js +308 -0
  55. package/src/renderer/rendering/passes/AmbientCapturePass.js +593 -0
  56. package/src/renderer/rendering/passes/BasePass.js +101 -0
  57. package/src/renderer/rendering/passes/BloomPass.js +420 -0
  58. package/src/renderer/rendering/passes/CRTPass.js +724 -0
  59. package/src/renderer/rendering/passes/FogPass.js +445 -0
  60. package/src/renderer/rendering/passes/GBufferPass.js +730 -0
  61. package/src/renderer/rendering/passes/HiZPass.js +744 -0
  62. package/src/renderer/rendering/passes/LightingPass.js +753 -0
  63. package/src/renderer/rendering/passes/ParticlePass.js +841 -0
  64. package/src/renderer/rendering/passes/PlanarReflectionPass.js +456 -0
  65. package/src/renderer/rendering/passes/PostProcessPass.js +405 -0
  66. package/src/renderer/rendering/passes/ReflectionPass.js +157 -0
  67. package/src/renderer/rendering/passes/RenderPostPass.js +364 -0
  68. package/src/renderer/rendering/passes/SSGIPass.js +266 -0
  69. package/src/renderer/rendering/passes/SSGITilePass.js +305 -0
  70. package/src/renderer/rendering/passes/ShadowPass.js +2072 -0
  71. package/src/renderer/rendering/passes/TransparentPass.js +831 -0
  72. package/src/renderer/rendering/passes/VolumetricFogPass.js +715 -0
  73. package/src/renderer/rendering/shaders/ao.wgsl +182 -0
  74. package/src/renderer/rendering/shaders/bloom.wgsl +97 -0
  75. package/src/renderer/rendering/shaders/bloom_blur.wgsl +80 -0
  76. package/src/renderer/rendering/shaders/crt.wgsl +455 -0
  77. package/src/renderer/rendering/shaders/depth_copy.wgsl +17 -0
  78. package/src/renderer/rendering/shaders/geometry.wgsl +580 -0
  79. package/src/renderer/rendering/shaders/hiz_reduce.wgsl +114 -0
  80. package/src/renderer/rendering/shaders/light_culling.wgsl +204 -0
  81. package/src/renderer/rendering/shaders/lighting.wgsl +932 -0
  82. package/src/renderer/rendering/shaders/lighting_common.wgsl +143 -0
  83. package/src/renderer/rendering/shaders/particle_render.wgsl +672 -0
  84. package/src/renderer/rendering/shaders/particle_simulate.wgsl +440 -0
  85. package/src/renderer/rendering/shaders/postproc.wgsl +293 -0
  86. package/src/renderer/rendering/shaders/render_post.wgsl +289 -0
  87. package/src/renderer/rendering/shaders/shadow.wgsl +117 -0
  88. package/src/renderer/rendering/shaders/ssgi.wgsl +266 -0
  89. package/src/renderer/rendering/shaders/ssgi_accumulate.wgsl +114 -0
  90. package/src/renderer/rendering/shaders/ssgi_propagate.wgsl +132 -0
  91. package/src/renderer/rendering/shaders/volumetric_blur.wgsl +80 -0
  92. package/src/renderer/rendering/shaders/volumetric_composite.wgsl +80 -0
  93. package/src/renderer/rendering/shaders/volumetric_raymarch.wgsl +634 -0
  94. package/src/renderer/utils/BoundingSphere.js +439 -0
  95. package/src/renderer/utils/Frustum.js +281 -0
  96. package/src/renderer/utils/Raycaster.js +761 -0
  97. package/dist/client.d.cts +0 -211
  98. package/dist/client.d.ts +0 -211
  99. package/dist/server.d.cts +0 -120
  100. package/dist/server.d.ts +0 -120
  101. package/dist/terminal.d.cts +0 -64
  102. package/dist/terminal.d.ts +0 -64
  103. package/src/utils.ts +0 -403
@@ -0,0 +1,563 @@
1
+ import { parse } from '@loaders.gl/core';
2
+ import { GLTFLoader } from '@loaders.gl/gltf';
3
+ import { Geometry } from "./Geometry.js"
4
+ import { Texture } from "./Texture.js"
5
+ import { Material } from "./Material.js"
6
+ import { Mesh } from "./Mesh.js"
7
+ import { Skin, Joint } from "./Skin.js"
8
+
9
+ /**
10
+ * Expand triangles by moving vertices away from triangle centroids
11
+ * This creates small overlaps to eliminate gaps between adjacent triangles
12
+ * @param {Object} attributes - Mesh attributes (position, normal, uv, indices, etc.)
13
+ * @param {number} expansion - Expansion distance in model units (e.g., 0.025 meters)
14
+ * @returns {Object} New attributes with expanded triangles (vertices duplicated per triangle)
15
+ */
16
+ function expandTriangles(attributes, expansion) {
17
+ const positions = attributes.position
18
+ const normals = attributes.normal
19
+ const tangents = attributes.tangent
20
+ const uvs = attributes.uv
21
+ const indices = attributes.indices
22
+ const weights = attributes.weights
23
+ const joints = attributes.joints
24
+
25
+ if (!positions || !indices) return attributes
26
+
27
+ const triCount = indices.length / 3
28
+
29
+ // Create new arrays - each triangle gets its own 3 vertices
30
+ const newPositions = new Float32Array(triCount * 3 * 3)
31
+ const newNormals = normals ? new Float32Array(triCount * 3 * 3) : null
32
+ const newTangents = tangents ? new Float32Array(triCount * 3 * 4) : null
33
+ const newUvs = uvs ? new Float32Array(triCount * 3 * 2) : null
34
+ const newWeights = weights ? new Float32Array(triCount * 3 * 4) : null
35
+ const newJoints = joints ? new Uint16Array(triCount * 3 * 4) : null
36
+ const newIndices = new Uint32Array(triCount * 3)
37
+
38
+ for (let t = 0; t < triCount; t++) {
39
+ const i0 = indices[t * 3 + 0]
40
+ const i1 = indices[t * 3 + 1]
41
+ const i2 = indices[t * 3 + 2]
42
+
43
+ // Get original positions
44
+ const p0 = [positions[i0 * 3], positions[i0 * 3 + 1], positions[i0 * 3 + 2]]
45
+ const p1 = [positions[i1 * 3], positions[i1 * 3 + 1], positions[i1 * 3 + 2]]
46
+ const p2 = [positions[i2 * 3], positions[i2 * 3 + 1], positions[i2 * 3 + 2]]
47
+
48
+ // Calculate centroid
49
+ const cx = (p0[0] + p1[0] + p2[0]) / 3
50
+ const cy = (p0[1] + p1[1] + p2[1]) / 3
51
+ const cz = (p0[2] + p1[2] + p2[2]) / 3
52
+
53
+ // Expand each vertex away from centroid
54
+ for (let v = 0; v < 3; v++) {
55
+ const origIdx = [i0, i1, i2][v]
56
+ const p = [p0, p1, p2][v]
57
+ const newIdx = t * 3 + v
58
+
59
+ // Direction from centroid to vertex
60
+ let dx = p[0] - cx
61
+ let dy = p[1] - cy
62
+ let dz = p[2] - cz
63
+ const len = Math.sqrt(dx * dx + dy * dy + dz * dz)
64
+
65
+ if (len > 0.0001) {
66
+ dx /= len
67
+ dy /= len
68
+ dz /= len
69
+ }
70
+
71
+ // Expanded position
72
+ newPositions[newIdx * 3 + 0] = p[0] + dx * expansion
73
+ newPositions[newIdx * 3 + 1] = p[1] + dy * expansion
74
+ newPositions[newIdx * 3 + 2] = p[2] + dz * expansion
75
+
76
+ // Copy normals
77
+ if (newNormals) {
78
+ newNormals[newIdx * 3 + 0] = normals[origIdx * 3 + 0]
79
+ newNormals[newIdx * 3 + 1] = normals[origIdx * 3 + 1]
80
+ newNormals[newIdx * 3 + 2] = normals[origIdx * 3 + 2]
81
+ }
82
+
83
+ // Copy tangents (vec4: xyz = tangent direction, w = handedness)
84
+ if (newTangents) {
85
+ newTangents[newIdx * 4 + 0] = tangents[origIdx * 4 + 0]
86
+ newTangents[newIdx * 4 + 1] = tangents[origIdx * 4 + 1]
87
+ newTangents[newIdx * 4 + 2] = tangents[origIdx * 4 + 2]
88
+ newTangents[newIdx * 4 + 3] = tangents[origIdx * 4 + 3]
89
+ }
90
+
91
+ // Copy UVs
92
+ if (newUvs) {
93
+ newUvs[newIdx * 2 + 0] = uvs[origIdx * 2 + 0]
94
+ newUvs[newIdx * 2 + 1] = uvs[origIdx * 2 + 1]
95
+ }
96
+
97
+ // Copy weights
98
+ if (newWeights) {
99
+ newWeights[newIdx * 4 + 0] = weights[origIdx * 4 + 0]
100
+ newWeights[newIdx * 4 + 1] = weights[origIdx * 4 + 1]
101
+ newWeights[newIdx * 4 + 2] = weights[origIdx * 4 + 2]
102
+ newWeights[newIdx * 4 + 3] = weights[origIdx * 4 + 3]
103
+ }
104
+
105
+ // Copy joints
106
+ if (newJoints) {
107
+ newJoints[newIdx * 4 + 0] = joints[origIdx * 4 + 0]
108
+ newJoints[newIdx * 4 + 1] = joints[origIdx * 4 + 1]
109
+ newJoints[newIdx * 4 + 2] = joints[origIdx * 4 + 2]
110
+ newJoints[newIdx * 4 + 3] = joints[origIdx * 4 + 3]
111
+ }
112
+
113
+ // New indices are sequential
114
+ newIndices[newIdx] = newIdx
115
+ }
116
+ }
117
+
118
+ return {
119
+ position: newPositions,
120
+ normal: newNormals,
121
+ tangent: newTangents,
122
+ uv: newUvs,
123
+ indices: newIndices,
124
+ weights: newWeights,
125
+ joints: newJoints
126
+ }
127
+ }
128
+
129
+ async function loadGltfData(engine, url, options = {}) {
130
+ options = {
131
+ scale: 1.0,
132
+ rotation: [0, 0, 0],
133
+ expandTriangles: 0, // Expansion distance in model units (0 = disabled)
134
+ ...options
135
+ }
136
+ const gltf = await parse(fetch(url), GLTFLoader)
137
+ //console.log('loaded gltf '+url, gltf)
138
+ // Transform GLTF into desired format
139
+ const meshes = {};
140
+ const nodes = [];
141
+ const skins = [];
142
+ const animations = [];
143
+
144
+ function getTexture(texture_id) {
145
+ let tex = gltf.json.textures[texture_id]
146
+ let filter = 'linear'
147
+ let wrapS = 'mirror-repeat' // Default to mirror-repeat to avoid black borders
148
+ let wrapT = 'mirror-repeat'
149
+ if (tex.sampler !== undefined) {
150
+ const samplerData = gltf.json.samplers[tex.sampler]
151
+ // Filter mode
152
+ if (samplerData.magFilter === 9728) filter = 'nearest'
153
+ else if (samplerData.magFilter === 9729) filter = 'linear'
154
+ // Wrap modes (GLTF: 33071=CLAMP, 33648=MIRROR, 10497=REPEAT)
155
+ // Default to mirror-repeat unless explicitly set to repeat
156
+ if (samplerData.wrapS === 10497) wrapS = 'repeat'
157
+ else if (samplerData.wrapS === 33648) wrapS = 'mirror-repeat'
158
+ else wrapS = 'mirror-repeat' // CLAMP or undefined -> mirror
159
+ if (samplerData.wrapT === 10497) wrapT = 'repeat'
160
+ else if (samplerData.wrapT === 33648) wrapT = 'mirror-repeat'
161
+ else wrapT = 'mirror-repeat' // CLAMP or undefined -> mirror
162
+ }
163
+ let image = gltf.images[tex.source]
164
+ // Get image URI from GLTF JSON (for sourceUrl tracking)
165
+ let imageUri = gltf.json.images?.[tex.source]?.uri || null
166
+ return {image: image, filter: filter, wrapS: wrapS, wrapT: wrapT, uri: imageUri}
167
+ }
168
+
169
+ function getTextures(property) {
170
+ let textures = {}
171
+ for (const [key, value] of Object.entries(property)) {
172
+ if (key.includes('Texture') && value.index !== undefined) {
173
+ property[key] = getTexture(value.index)
174
+ }
175
+ }
176
+ return property
177
+ }
178
+
179
+ function getMaterial(material_id) {
180
+ if (material_id === undefined) {
181
+ return {
182
+ pbrMetallicRoughness: {
183
+ baseColorFactor: [1, 1, 1, 1],
184
+ metallicFactor: 0,
185
+ roughnessFactor: 1
186
+ }
187
+ }
188
+ }
189
+ let mat = gltf.json.materials[material_id]
190
+ mat = getTextures(mat)
191
+ if (mat.pbrMetallicRoughness) {
192
+ mat.pbrMetallicRoughness = getTextures(mat.pbrMetallicRoughness)
193
+ }
194
+ return mat
195
+ }
196
+
197
+ function getAccessor(accessor_id, name) {
198
+ if (accessor_id == undefined) return false
199
+ let acc = gltf.json.accessors[accessor_id]
200
+ let bufferView = gltf.json.bufferViews[acc.bufferView]
201
+ let buffer = gltf.buffers[bufferView.buffer]
202
+
203
+ // Get number of components based on type
204
+ let numComponents = 0
205
+ switch(acc.type) {
206
+ case 'SCALAR': numComponents = 1; break;
207
+ case 'VEC2': numComponents = 2; break;
208
+ case 'VEC3': numComponents = 3; break;
209
+ case 'VEC4': numComponents = 4; break;
210
+ case 'MAT2': numComponents = 4; break;
211
+ case 'MAT3': numComponents = 9; break;
212
+ case 'MAT4': numComponents = 16; break;
213
+ }
214
+
215
+ // Create typed array based on componentType
216
+ let byteOffset = (bufferView.byteOffset || 0) + (acc.byteOffset || 0) + (buffer.byteOffset || 0)
217
+ let ab = buffer.arrayBuffer
218
+ switch(acc.componentType) {
219
+ case 5120: // BYTE
220
+ acc.data = new Int8Array(ab, byteOffset, acc.count * numComponents)
221
+ break
222
+ case 5121: // UNSIGNED_BYTE
223
+ acc.data = new Uint8Array(ab, byteOffset, acc.count * numComponents)
224
+ break
225
+ case 5122: // SHORT
226
+ acc.data = new Int16Array(ab, byteOffset, acc.count * numComponents)
227
+ break
228
+ case 5123: // UNSIGNED_SHORT
229
+ acc.data = new Uint16Array(ab, byteOffset, acc.count * numComponents)
230
+ break
231
+ case 5125: // UNSIGNED_INT
232
+ acc.data = new Uint32Array(ab, byteOffset, acc.count * numComponents)
233
+ break
234
+ case 5126: // FLOAT
235
+ acc.data = new Float32Array(ab, byteOffset, acc.count * numComponents)
236
+ break
237
+ }
238
+ return acc.data
239
+ }
240
+
241
+ // Parse all nodes first
242
+ if (gltf.json.nodes) {
243
+ for (let i = 0; i < gltf.json.nodes.length; i++) {
244
+ const nodeData = gltf.json.nodes[i]
245
+ const joint = new Joint(nodeData.name || `node_${i}`)
246
+
247
+ // Set transform
248
+ if (nodeData.matrix) {
249
+ joint.setMatrix(new Float32Array(nodeData.matrix))
250
+ } else {
251
+ if (nodeData.translation) {
252
+ vec3.set(joint.position, nodeData.translation[0], nodeData.translation[1], nodeData.translation[2])
253
+ }
254
+ if (nodeData.rotation) {
255
+ quat.set(joint.rotation, nodeData.rotation[0], nodeData.rotation[1], nodeData.rotation[2], nodeData.rotation[3])
256
+ }
257
+ if (nodeData.scale) {
258
+ vec3.set(joint.scale, nodeData.scale[0], nodeData.scale[1], nodeData.scale[2])
259
+ }
260
+ }
261
+
262
+ joint.saveBindPose()
263
+ joint.nodeIndex = i
264
+ nodes.push(joint)
265
+ }
266
+
267
+ // Build parent-child relationships
268
+ for (let i = 0; i < gltf.json.nodes.length; i++) {
269
+ const nodeData = gltf.json.nodes[i]
270
+ if (nodeData.children) {
271
+ for (const childIndex of nodeData.children) {
272
+ nodes[i].addChild(nodes[childIndex])
273
+ }
274
+ }
275
+ }
276
+ }
277
+
278
+ // Parse skins
279
+ if (gltf.json.skins) {
280
+ for (const skinData of gltf.json.skins) {
281
+ const skin = new Skin(engine)
282
+
283
+ // Get joint nodes
284
+ const jointNodes = skinData.joints.map(jointIndex => nodes[jointIndex])
285
+
286
+ // Get inverse bind matrices
287
+ const inverseBindMatrices = getAccessor(skinData.inverseBindMatrices, 'inverseBindMatrices')
288
+
289
+ // Find root node (skeleton)
290
+ let rootNode = skinData.skeleton !== undefined ? nodes[skinData.skeleton] : jointNodes[0]
291
+ // Find the topmost parent
292
+ while (rootNode.parent) {
293
+ rootNode = rootNode.parent
294
+ }
295
+
296
+ skin.init(jointNodes, inverseBindMatrices, rootNode)
297
+ skins.push(skin)
298
+ }
299
+ }
300
+
301
+ // Parse animations
302
+ if (gltf.json.animations) {
303
+ for (const animData of gltf.json.animations) {
304
+ const animation = {
305
+ name: animData.name || 'default',
306
+ duration: 0,
307
+ channels: []
308
+ }
309
+
310
+ // Parse samplers
311
+ const samplers = animData.samplers.map(samplerData => {
312
+ const input = getAccessor(samplerData.input, 'animation_input') // times
313
+ const output = getAccessor(samplerData.output, 'animation_output') // values
314
+
315
+ // Update animation duration
316
+ if (input && input.length > 0) {
317
+ animation.duration = Math.max(animation.duration, input[input.length - 1])
318
+ }
319
+
320
+ return {
321
+ input: input,
322
+ output: output,
323
+ interpolation: samplerData.interpolation || 'LINEAR'
324
+ }
325
+ })
326
+
327
+ // Parse channels
328
+ for (const channelData of animData.channels) {
329
+ const targetNode = nodes[channelData.target.node]
330
+ const sampler = samplers[channelData.sampler]
331
+
332
+ animation.channels.push({
333
+ target: targetNode,
334
+ path: channelData.target.path,
335
+ sampler: sampler
336
+ })
337
+ }
338
+
339
+ animations.push(animation)
340
+
341
+ // Add animation to relevant skins
342
+ for (const skin of skins) {
343
+ skin.addAnimation(animation.name, animation)
344
+ }
345
+ }
346
+ }
347
+
348
+ // Parse meshes
349
+ for (let mi = 0; mi < gltf.json.meshes.length; mi++) {
350
+ const mesh = gltf.json.meshes[mi]
351
+
352
+ // Find which node uses this mesh
353
+ let meshNodeIndex = null
354
+ let skinIndex = null
355
+ if (gltf.json.nodes) {
356
+ for (let ni = 0; ni < gltf.json.nodes.length; ni++) {
357
+ if (gltf.json.nodes[ni].mesh === mi) {
358
+ meshNodeIndex = ni
359
+ skinIndex = gltf.json.nodes[ni].skin
360
+ break
361
+ }
362
+ }
363
+ }
364
+
365
+ // Handle all primitives in the mesh (some GLTF files have multiple primitives per mesh)
366
+ for (let pi = 0; pi < mesh.primitives.length; pi++) {
367
+ const primitive = mesh.primitives[pi]
368
+ const attributes = primitive.attributes;
369
+
370
+ // Generate unique name: meshName_primitiveIndex or mesh_meshIndex_primitiveIndex
371
+ const baseName = mesh.name || `mesh_${mi}`
372
+ const meshName = mesh.primitives.length > 1 ? `${baseName}_${pi}` : baseName
373
+
374
+ // Collect mesh attributes
375
+ let attrs = {
376
+ position: getAccessor(attributes.POSITION, 'position'),
377
+ normal: getAccessor(attributes.NORMAL, 'normal'),
378
+ tangent: getAccessor(attributes.TANGENT, 'tangent'),
379
+ uv: getAccessor(attributes.TEXCOORD_0, 'uv'),
380
+ indices: getAccessor(primitive.indices, 'indices'),
381
+ weights: getAccessor(attributes.WEIGHTS_0, 'weights'),
382
+ joints: getAccessor(attributes.JOINTS_0, 'joints')
383
+ }
384
+
385
+ // Apply triangle expansion if requested (helps eliminate gaps between triangles)
386
+ if (options.expandTriangles > 0) {
387
+ attrs = expandTriangles(attrs, options.expandTriangles)
388
+ }
389
+
390
+ meshes[meshName] = {
391
+ attributes: attrs,
392
+ material: getMaterial(primitive.material),
393
+ scale: options.scale,
394
+ rotation: options.rotation,
395
+ skinIndex: skinIndex,
396
+ nodeIndex: meshNodeIndex
397
+ };
398
+ }
399
+ }
400
+
401
+ return { meshes, nodes, skins, animations }
402
+ }
403
+
404
+ async function loadGltf(engine, url, options = {}) {
405
+ options = {
406
+ flipY: false,
407
+ ...options
408
+ }
409
+ const data = await loadGltfData(engine, url, options)
410
+ const { meshes: mdata, skins, animations, nodes } = data
411
+ const meshes = {}
412
+
413
+ for (const name in mdata) {
414
+ const mesh = mdata[name]
415
+ const geometry = new Geometry(engine, mesh.attributes)
416
+ const pbr = mesh.material.pbrMetallicRoughness || {}
417
+
418
+ // Albedo/Base Color texture
419
+ let albedo
420
+ if (pbr.baseColorTexture) {
421
+ const tex = pbr.baseColorTexture
422
+ albedo = await Texture.fromImage(engine, tex.image, {
423
+ srgb: true,
424
+ flipY: options.flipY,
425
+ addressModeU: tex.wrapS,
426
+ addressModeV: tex.wrapT
427
+ })
428
+ if (tex.uri) albedo.sourceUrl = tex.uri
429
+ } else {
430
+ // Use baseColorFactor if available, otherwise white
431
+ const bcf = pbr.baseColorFactor || [1, 1, 1, 1]
432
+ albedo = await Texture.fromRGBA(engine, bcf[0], bcf[1], bcf[2], bcf[3])
433
+ }
434
+
435
+ // Normal map
436
+ let normal
437
+ if (mesh.material.normalTexture) {
438
+ const tex = mesh.material.normalTexture
439
+ normal = await Texture.fromImage(engine, tex.image, {
440
+ srgb: false,
441
+ flipY: options.flipY,
442
+ addressModeU: tex.wrapS,
443
+ addressModeV: tex.wrapT
444
+ })
445
+ if (tex.uri) normal.sourceUrl = tex.uri
446
+ } else {
447
+ normal = await Texture.fromColor(engine, "#8080FF")
448
+ }
449
+
450
+ // Metallic/Roughness texture
451
+ // Format: R=ambient occlusion (if packed), G=roughness, B=metallic
452
+ let rm
453
+ if (pbr.metallicRoughnessTexture) {
454
+ const tex = pbr.metallicRoughnessTexture
455
+ rm = await Texture.fromImage(engine, tex.image, {
456
+ srgb: false,
457
+ flipY: options.flipY,
458
+ addressModeU: tex.wrapS,
459
+ addressModeV: tex.wrapT
460
+ })
461
+ if (tex.uri) rm.sourceUrl = tex.uri
462
+ } else {
463
+ // Use material factors or defaults
464
+ // Default: roughness 0.9, metallic 0.0
465
+ const roughness = pbr.roughnessFactor !== undefined ? pbr.roughnessFactor : 0.9
466
+ const metallic = pbr.metallicFactor !== undefined ? pbr.metallicFactor : 0.0
467
+ // R=1 (no AO in this channel), G=roughness, B=metallic
468
+ rm = await Texture.fromRGBA(engine, 1.0, roughness, metallic, 1.0)
469
+ }
470
+
471
+ // Ambient Occlusion texture
472
+ let ambient
473
+ if (mesh.material.occlusionTexture) {
474
+ const tex = mesh.material.occlusionTexture
475
+ ambient = await Texture.fromImage(engine, tex.image, {
476
+ srgb: false,
477
+ flipY: options.flipY,
478
+ addressModeU: tex.wrapS,
479
+ addressModeV: tex.wrapT
480
+ })
481
+ if (tex.uri) ambient.sourceUrl = tex.uri
482
+ } else {
483
+ // Default: no occlusion (white = full light)
484
+ ambient = await Texture.fromColor(engine, "#FFFFFF")
485
+ }
486
+
487
+ // Emission texture
488
+ let emission
489
+ if (mesh.material.emissiveTexture) {
490
+ const tex = mesh.material.emissiveTexture
491
+ emission = await Texture.fromImage(engine, tex.image, {
492
+ srgb: true,
493
+ flipY: options.flipY,
494
+ addressModeU: tex.wrapS,
495
+ addressModeV: tex.wrapT
496
+ })
497
+ if (tex.uri) emission.sourceUrl = tex.uri
498
+ } else {
499
+ // Use emissiveFactor if available, otherwise black (no emission)
500
+ const ef = mesh.material.emissiveFactor || [0, 0, 0]
501
+ emission = await Texture.fromRGBA(engine, ef[0], ef[1], ef[2], 1.0)
502
+ }
503
+
504
+ // Get material name from GLTF or generate from index
505
+ const materialName = mesh.material.name || null
506
+ const material = new Material([albedo, normal, ambient, rm, emission], {}, materialName)
507
+
508
+ // Check GLTF alphaMode for transparency handling
509
+ // "MASK" = alpha cutout (use alpha hashing for deferred)
510
+ // "BLEND" = true alpha blending (requires forward transparent pass)
511
+ // "OPAQUE" = fully opaque (default)
512
+ const alphaMode = mesh.material.alphaMode || 'OPAQUE'
513
+ if (alphaMode === 'MASK') {
514
+ // Alpha cutout: use alpha hashing in deferred renderer
515
+ material.alphaHash = true
516
+ // GLTF alphaCutoff defaults to 0.5, use as scale factor
517
+ const alphaCutoff = mesh.material.alphaCutoff ?? 0.5
518
+ material.alphaHashScale = 1.0 / Math.max(alphaCutoff, 0.01)
519
+ } else if (alphaMode === 'BLEND') {
520
+ // True transparency: render in forward transparent pass
521
+ material.transparent = true
522
+ // Get base opacity from baseColorFactor alpha if available
523
+ const bcf = pbr.baseColorFactor || [1, 1, 1, 1]
524
+ material.opacity = bcf[3]
525
+ }
526
+
527
+ // Handle KHR_materials_transmission extension (for glass-like materials)
528
+ if (mesh.material.extensions?.KHR_materials_transmission) {
529
+ material.transparent = true
530
+ const transmission = mesh.material.extensions.KHR_materials_transmission
531
+ material.opacity = 1.0 - (transmission.transmissionFactor ?? 0)
532
+ }
533
+
534
+ // Handle double-sided materials
535
+ if (mesh.material.doubleSided) {
536
+ material.doubleSided = true
537
+ }
538
+
539
+ // Create mesh with name from GLTF (name is the key from mesh data)
540
+ const nmesh = new Mesh(geometry, material, name)
541
+
542
+ // Attach skin if this mesh has one
543
+ if (mesh.skinIndex !== undefined && mesh.skinIndex !== null && skins[mesh.skinIndex]) {
544
+ nmesh.skin = skins[mesh.skinIndex]
545
+ nmesh.hasSkin = true
546
+ }
547
+
548
+ // Preserve node index for scene placement
549
+ nmesh.nodeIndex = mesh.nodeIndex
550
+
551
+ meshes[name] = nmesh
552
+ }
553
+
554
+ return { meshes, skins, animations, nodes }
555
+ }
556
+
557
+ // Simpler version that returns just meshes for backward compatibility
558
+ async function loadGltfMeshes(engine, url, options = {}) {
559
+ const result = await loadGltf(engine, url, options)
560
+ return result.meshes
561
+ }
562
+
563
+ export { loadGltf, loadGltfMeshes, loadGltfData }