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.
- package/LICENSE.txt +0 -0
- package/README.md +0 -0
- package/dist/Renderer.cjs +20844 -0
- package/dist/Renderer.cjs.map +1 -0
- package/dist/Renderer.js +20827 -0
- package/dist/Renderer.js.map +1 -0
- package/dist/client.cjs +91 -260
- package/dist/client.cjs.map +1 -1
- package/dist/client.js +68 -215
- package/dist/client.js.map +1 -1
- package/dist/server.cjs +165 -432
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +117 -370
- package/dist/server.js.map +1 -1
- package/dist/terminal.cjs +113 -200
- package/dist/terminal.cjs.map +1 -1
- package/dist/terminal.js +50 -51
- package/dist/terminal.js.map +1 -1
- package/dist/utils-CRhi1BDa.cjs +259 -0
- package/dist/utils-CRhi1BDa.cjs.map +1 -0
- package/dist/utils-D7tXt6-2.js +260 -0
- package/dist/utils-D7tXt6-2.js.map +1 -0
- package/package.json +19 -15
- package/src/{client.ts → network/client.js} +170 -403
- package/src/{compress-browser.ts → network/compress-browser.js} +2 -4
- package/src/{compress-node.ts → network/compress-node.js} +8 -14
- package/src/{server.ts → network/server.js} +229 -317
- package/src/{terminal.js → network/terminal.js} +0 -0
- package/src/{topazcube.ts → network/topazcube.js} +2 -2
- package/src/network/utils.js +375 -0
- package/src/renderer/Camera.js +191 -0
- package/src/renderer/DebugUI.js +703 -0
- package/src/renderer/Geometry.js +1049 -0
- package/src/renderer/Material.js +64 -0
- package/src/renderer/Mesh.js +211 -0
- package/src/renderer/Node.js +112 -0
- package/src/renderer/Pipeline.js +645 -0
- package/src/renderer/Renderer.js +1496 -0
- package/src/renderer/Skin.js +792 -0
- package/src/renderer/Texture.js +584 -0
- package/src/renderer/core/AssetManager.js +394 -0
- package/src/renderer/core/CullingSystem.js +308 -0
- package/src/renderer/core/EntityManager.js +541 -0
- package/src/renderer/core/InstanceManager.js +343 -0
- package/src/renderer/core/ParticleEmitter.js +358 -0
- package/src/renderer/core/ParticleSystem.js +564 -0
- package/src/renderer/core/SpriteSystem.js +349 -0
- package/src/renderer/gltf.js +563 -0
- package/src/renderer/math.js +161 -0
- package/src/renderer/rendering/HistoryBufferManager.js +333 -0
- package/src/renderer/rendering/ProbeCapture.js +1495 -0
- package/src/renderer/rendering/ReflectionProbeManager.js +352 -0
- package/src/renderer/rendering/RenderGraph.js +2258 -0
- package/src/renderer/rendering/passes/AOPass.js +308 -0
- package/src/renderer/rendering/passes/AmbientCapturePass.js +593 -0
- package/src/renderer/rendering/passes/BasePass.js +101 -0
- package/src/renderer/rendering/passes/BloomPass.js +420 -0
- package/src/renderer/rendering/passes/CRTPass.js +724 -0
- package/src/renderer/rendering/passes/FogPass.js +445 -0
- package/src/renderer/rendering/passes/GBufferPass.js +730 -0
- package/src/renderer/rendering/passes/HiZPass.js +744 -0
- package/src/renderer/rendering/passes/LightingPass.js +753 -0
- package/src/renderer/rendering/passes/ParticlePass.js +841 -0
- package/src/renderer/rendering/passes/PlanarReflectionPass.js +456 -0
- package/src/renderer/rendering/passes/PostProcessPass.js +405 -0
- package/src/renderer/rendering/passes/ReflectionPass.js +157 -0
- package/src/renderer/rendering/passes/RenderPostPass.js +364 -0
- package/src/renderer/rendering/passes/SSGIPass.js +266 -0
- package/src/renderer/rendering/passes/SSGITilePass.js +305 -0
- package/src/renderer/rendering/passes/ShadowPass.js +2072 -0
- package/src/renderer/rendering/passes/TransparentPass.js +831 -0
- package/src/renderer/rendering/passes/VolumetricFogPass.js +715 -0
- package/src/renderer/rendering/shaders/ao.wgsl +182 -0
- package/src/renderer/rendering/shaders/bloom.wgsl +97 -0
- package/src/renderer/rendering/shaders/bloom_blur.wgsl +80 -0
- package/src/renderer/rendering/shaders/crt.wgsl +455 -0
- package/src/renderer/rendering/shaders/depth_copy.wgsl +17 -0
- package/src/renderer/rendering/shaders/geometry.wgsl +580 -0
- package/src/renderer/rendering/shaders/hiz_reduce.wgsl +114 -0
- package/src/renderer/rendering/shaders/light_culling.wgsl +204 -0
- package/src/renderer/rendering/shaders/lighting.wgsl +932 -0
- package/src/renderer/rendering/shaders/lighting_common.wgsl +143 -0
- package/src/renderer/rendering/shaders/particle_render.wgsl +672 -0
- package/src/renderer/rendering/shaders/particle_simulate.wgsl +440 -0
- package/src/renderer/rendering/shaders/postproc.wgsl +293 -0
- package/src/renderer/rendering/shaders/render_post.wgsl +289 -0
- package/src/renderer/rendering/shaders/shadow.wgsl +117 -0
- package/src/renderer/rendering/shaders/ssgi.wgsl +266 -0
- package/src/renderer/rendering/shaders/ssgi_accumulate.wgsl +114 -0
- package/src/renderer/rendering/shaders/ssgi_propagate.wgsl +132 -0
- package/src/renderer/rendering/shaders/volumetric_blur.wgsl +80 -0
- package/src/renderer/rendering/shaders/volumetric_composite.wgsl +80 -0
- package/src/renderer/rendering/shaders/volumetric_raymarch.wgsl +634 -0
- package/src/renderer/utils/BoundingSphere.js +439 -0
- package/src/renderer/utils/Frustum.js +281 -0
- package/src/renderer/utils/Raycaster.js +761 -0
- package/dist/client.d.cts +0 -211
- package/dist/client.d.ts +0 -211
- package/dist/server.d.cts +0 -120
- package/dist/server.d.ts +0 -120
- package/dist/terminal.d.cts +0 -64
- package/dist/terminal.d.ts +0 -64
- 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 }
|