@needle-tools/gltf-build-pipeline 2.13.0 → 2.13.1
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/CHANGELOG.md +520 -517
- package/README.md +38 -38
- package/dist/cache/cache.d.ts +55 -55
- package/dist/cache/cache.js +326 -326
- package/dist/cache/index.d.ts +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.js +198 -198
- package/dist/config/index.d.ts +53 -53
- package/dist/config/index.js +152 -152
- package/dist/constants.d.ts +7 -7
- package/dist/constants.js +7 -7
- package/dist/extensions/NEEDLE_compression_texture/NEEDLE_compression_texture.d.ts +24 -24
- package/dist/extensions/NEEDLE_compression_texture/NEEDLE_compression_texture.js +54 -54
- package/dist/extensions/NEEDLE_compression_texture/index.d.ts +1 -1
- package/dist/extensions/NEEDLE_compression_texture/index.js +1 -1
- package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.d.ts +27 -27
- package/dist/extensions/NEEDLE_mesh_compression/NEEDLE_mesh_compression.js +88 -88
- package/dist/extensions/NEEDLE_mesh_compression/index.d.ts +1 -1
- package/dist/extensions/NEEDLE_mesh_compression/index.js +1 -1
- package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.d.ts +27 -27
- package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.js +980 -980
- package/dist/extensions/NEEDLE_opaque/index.d.ts +3 -3
- package/dist/extensions/NEEDLE_opaque/index.js +30 -30
- package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.d.ts +48 -48
- package/dist/extensions/NEEDLE_progressive/NEEDLE_progressive.js +69 -69
- package/dist/extensions/NEEDLE_progressive/index.d.ts +1 -1
- package/dist/extensions/NEEDLE_progressive/index.js +1 -1
- package/dist/extensions/NEEDLE_progressive_mesh_settings/index.d.ts +23 -23
- package/dist/extensions/NEEDLE_progressive_mesh_settings/index.js +41 -41
- package/dist/extensions/NEEDLE_progressive_texture_settings/index.d.ts +25 -25
- package/dist/extensions/NEEDLE_progressive_texture_settings/index.js +38 -38
- package/dist/extensions/index.d.ts +7 -7
- package/dist/extensions/index.js +17 -17
- package/dist/extensions/utils.d.ts +6 -6
- package/dist/extensions/utils.js +72 -72
- package/dist/functions/weld.3.d.ts +76 -76
- package/dist/functions/weld.3.js +414 -414
- package/dist/index.d.ts +8 -8
- package/dist/index.js +8 -8
- package/dist/scripts/index.d.ts +2 -2
- package/dist/scripts/index.js +2 -2
- package/dist/scripts/pack-gltf.d.ts +15 -15
- package/dist/scripts/pack-gltf.js +135 -133
- package/dist/scripts/stats.d.ts +2 -2
- package/dist/scripts/stats.js +19 -19
- package/dist/transforms/index.d.ts +9 -9
- package/dist/transforms/index.js +9 -9
- package/dist/transforms/needle_animation_transform.d.ts +2 -2
- package/dist/transforms/needle_animation_transform.js +5 -5
- package/dist/transforms/needle_asset.d.ts +3 -3
- package/dist/transforms/needle_asset.js +50 -50
- package/dist/transforms/needle_common.d.ts +17 -17
- package/dist/transforms/needle_common.js +1 -1
- package/dist/transforms/needle_mesh_transform.d.ts +15 -15
- package/dist/transforms/needle_mesh_transform.js +223 -223
- package/dist/transforms/needle_progressive.d.ts +45 -45
- package/dist/transforms/needle_progressive.js +1043 -1043
- package/dist/transforms/needle_texture_transform.d.ts +13 -13
- package/dist/transforms/needle_texture_transform.js +257 -257
- package/dist/transforms/needle_toktx.d.ts +26 -26
- package/dist/transforms/needle_toktx.js +243 -243
- package/dist/transforms/needle_webp.d.ts +15 -15
- package/dist/transforms/needle_webp.js +53 -53
- package/dist/transforms/toktx.d.ts +52 -52
- package/dist/transforms/toktx.js +221 -221
- package/dist/transforms/toktx_types.d.ts +78 -78
- package/dist/transforms/toktx_types.js +62 -62
- package/dist/transforms/util.d.ts +21 -21
- package/dist/transforms/util.js +90 -90
- package/dist/utils/compression_utils.d.ts +2 -2
- package/dist/utils/compression_utils.js +6 -6
- package/dist/utils/fileutils.d.ts +19 -19
- package/dist/utils/fileutils.js +177 -177
- package/dist/utils/guid.d.ts +1 -1
- package/dist/utils/guid.js +5 -5
- package/dist/utils/index.d.ts +10 -10
- package/dist/utils/index.js +10 -10
- package/dist/utils/merge.d.ts +12 -12
- package/dist/utils/merge.js +31 -31
- package/dist/utils/mesh.d.ts +13 -13
- package/dist/utils/mesh.js +124 -124
- package/dist/utils/nodeio.d.ts +6 -6
- package/dist/utils/nodeio.js +86 -86
- package/dist/utils/stats.d.ts +70 -70
- package/dist/utils/stats.js +258 -258
- package/dist/utils/texture.d.ts +5 -5
- package/dist/utils/texture.js +14 -14
- package/dist/utils/validate.d.ts +5 -5
- package/dist/utils/validate.js +17 -17
- package/dist/utils/version.d.ts +1 -1
- package/dist/utils/version.gen.d.ts +1 -1
- package/dist/utils/version.gen.js +1 -1
- package/dist/utils/version.js +4 -4
- package/package.json +79 -79
- package/scripts/clear-caches.mjs +3 -3
- package/scripts/defaults.mjs +11 -11
- package/scripts/fileutils.mjs +5 -5
- package/scripts/index.mjs +41 -41
- package/scripts/limit-caches.mjs +3 -3
- package/scripts/make-progressive.mjs +16 -16
- package/scripts/pack-gltf.mjs +106 -106
- package/tsconfig.json +19 -19
|
@@ -1,980 +1,980 @@
|
|
|
1
|
-
import { Extension, ExtensionProperty, PropertyType, Node, TextureChannel } from '@gltf-transform/core';
|
|
2
|
-
import { getTexture, tryGetInArray, writeExtension } from '../utils.js';
|
|
3
|
-
import { isLightmap } from '../../utils/texture.js';
|
|
4
|
-
const ALL_PROPERTY_TYPES = [];
|
|
5
|
-
for (const key in PropertyType) {
|
|
6
|
-
ALL_PROPERTY_TYPES.push(PropertyType[key]);
|
|
7
|
-
}
|
|
8
|
-
export let currentGenerator = "";
|
|
9
|
-
// an opaque extension can be used to pass data through the gltf transform process
|
|
10
|
-
// it should also be able to re-write/fix json pointers after the transformation which might have shuffled them around
|
|
11
|
-
export function createOpaqueExtension(name, types, opts) {
|
|
12
|
-
if (!Array.isArray(types))
|
|
13
|
-
types = [types];
|
|
14
|
-
const EXTENSION_NAME = name;
|
|
15
|
-
const ExtensionPropertyTypes = ALL_PROPERTY_TYPES;
|
|
16
|
-
const debugLog = opts?.debug ?? false;
|
|
17
|
-
class NEEDLE_opaque extends Extension {
|
|
18
|
-
extensionName = EXTENSION_NAME;
|
|
19
|
-
static EXTENSION_NAME = EXTENSION_NAME;
|
|
20
|
-
prewriteTypes = [PropertyType.MATERIAL];
|
|
21
|
-
// private roots: Array<OpaqueProperty> = [];
|
|
22
|
-
jsonPointerHandler;
|
|
23
|
-
read(context) {
|
|
24
|
-
if (opts?.breakpoint === true)
|
|
25
|
-
debugger;
|
|
26
|
-
currentGenerator = context.jsonDoc.json.asset?.generator;
|
|
27
|
-
if (debugLog)
|
|
28
|
-
console.log("READ " + EXTENSION_NAME);
|
|
29
|
-
this.jsonPointerHandler = new JsonPointerHandler(this.document, EXTENSION_NAME);
|
|
30
|
-
this.jsonPointerHandler.debug = debugLog;
|
|
31
|
-
this.jsonPointerHandler.breakPoint = opts?.breakpoint;
|
|
32
|
-
for (const type of types) {
|
|
33
|
-
switch (type) {
|
|
34
|
-
case PropertyType.ACCESSOR:
|
|
35
|
-
this.onRead(context, "accessors", "accessorIndexMap");
|
|
36
|
-
break;
|
|
37
|
-
case PropertyType.ANIMATION:
|
|
38
|
-
this.onRead(context, "animations", "animationIndexMap");
|
|
39
|
-
break;
|
|
40
|
-
case PropertyType.ANIMATION_CHANNEL:
|
|
41
|
-
if (EXTENSION_NAME === "KHR_animation_pointer") {
|
|
42
|
-
context.jsonDoc.json.animations?.forEach((animDef, animIndex) => {
|
|
43
|
-
const anim = context.animations[animIndex];
|
|
44
|
-
const channels = anim.listChannels();
|
|
45
|
-
animDef.channels?.forEach((channel, channelIndex) => {
|
|
46
|
-
const jsonTarget = channel.target;
|
|
47
|
-
const khr_animation = jsonTarget.extensions && jsonTarget.extensions[EXTENSION_NAME];
|
|
48
|
-
if (!khr_animation)
|
|
49
|
-
return;
|
|
50
|
-
const pointer = khr_animation.pointer;
|
|
51
|
-
if (pointer) {
|
|
52
|
-
const sections = pointer.split("/");
|
|
53
|
-
const nodeIndex = parseInt(sections[2]);
|
|
54
|
-
const targetName = sections[3];
|
|
55
|
-
if (targetName === "scale" || targetName === "translation" || targetName === "rotation") {
|
|
56
|
-
channels[channelIndex].setTargetNode(context.nodes[nodeIndex]);
|
|
57
|
-
channels[channelIndex].setTargetPath(targetName);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
const existing = anim.getExtension(EXTENSION_NAME);
|
|
61
|
-
if (debugLog)
|
|
62
|
-
console.log(">>>", EXTENSION_NAME, "animation", animIndex, "channel", channelIndex, khr_animation);
|
|
63
|
-
if (existing) {
|
|
64
|
-
assignChannels(existing);
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
const prop = this.createExtension();
|
|
68
|
-
// there seems no way (yet?) to add extensions to channel.targets (?)
|
|
69
|
-
// https://github.com/donmccurdy/glTF-Transform/discussions/613
|
|
70
|
-
// so we add it to an object by keys to the animation itself and read it back in WRITE
|
|
71
|
-
const khr_animation_data = {};
|
|
72
|
-
khr_animation_data[channelIndex] = khr_animation;
|
|
73
|
-
anim.setExtension(EXTENSION_NAME, prop);
|
|
74
|
-
assignChannels(prop);
|
|
75
|
-
function assignChannels(prop) {
|
|
76
|
-
prop.add(ctx => {
|
|
77
|
-
const animations = ctx.jsonDoc.json.animations;
|
|
78
|
-
if (!animations)
|
|
79
|
-
return;
|
|
80
|
-
const animDef = animations[animIndex];
|
|
81
|
-
const channel = animDef?.channels[channelIndex];
|
|
82
|
-
if (channel) {
|
|
83
|
-
if (debugLog)
|
|
84
|
-
console.log("<<<", EXTENSION_NAME, "animation", animIndex, "channel", channelIndex, khr_animation);
|
|
85
|
-
channel.target = jsonTarget;
|
|
86
|
-
}
|
|
87
|
-
else if (debugLog)
|
|
88
|
-
console.log("WARN: ", EXTENSION_NAME, "animation", animIndex, "channel", channelIndex, "not found");
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
;
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
break;
|
|
96
|
-
case PropertyType.ANIMATION_SAMPLER:
|
|
97
|
-
break;
|
|
98
|
-
case PropertyType.BUFFER:
|
|
99
|
-
this.onRead(context, "buffers", "bufferIndexMap");
|
|
100
|
-
break;
|
|
101
|
-
case PropertyType.CAMERA:
|
|
102
|
-
this.onRead(context, "cameras", "cameraIndexMap");
|
|
103
|
-
break;
|
|
104
|
-
case PropertyType.MATERIAL:
|
|
105
|
-
this.onRead(context, "materials", "materialIndexMap");
|
|
106
|
-
break;
|
|
107
|
-
case PropertyType.TEXTURE:
|
|
108
|
-
context.jsonDoc.json.textures?.forEach((textureDefinition, index) => {
|
|
109
|
-
if (!textureDefinition.extensions)
|
|
110
|
-
return;
|
|
111
|
-
const ext = textureDefinition.extensions[EXTENSION_NAME];
|
|
112
|
-
if (!ext)
|
|
113
|
-
return;
|
|
114
|
-
// TODO: textures and textureDefinition are not in the same order?
|
|
115
|
-
// see https://github.com/donmccurdy/glTF-Transform/blob/0e99c426b8ef41536228fe860ab8329be5d1d50f/packages/core/src/io/reader.ts#L132
|
|
116
|
-
// textures are in image order
|
|
117
|
-
// TODO: we need to use the image index to find the texture
|
|
118
|
-
let texture = getTexture(context, textureDefinition);
|
|
119
|
-
if (!texture) {
|
|
120
|
-
console.log("WARN: could potentially not find gltf transform texture for " + index);
|
|
121
|
-
texture = context.textures[index];
|
|
122
|
-
}
|
|
123
|
-
const prop = this.createExtension();
|
|
124
|
-
this.jsonPointerHandler.read(context, texture, ext, prop);
|
|
125
|
-
texture.setExtension(EXTENSION_NAME, prop);
|
|
126
|
-
const handler = new TextureHandler(this.document, textureDefinition, EXTENSION_NAME, ext, debugLog);
|
|
127
|
-
prop.add(handler);
|
|
128
|
-
});
|
|
129
|
-
break;
|
|
130
|
-
case PropertyType.NODE:
|
|
131
|
-
this.onRead(context, "nodes", "nodeIndexMap");
|
|
132
|
-
break;
|
|
133
|
-
case PropertyType.MESH:
|
|
134
|
-
this.onRead(context, "meshes", "meshIndexMap");
|
|
135
|
-
break;
|
|
136
|
-
case PropertyType.PRIMITIVE:
|
|
137
|
-
context.jsonDoc.json.meshes?.forEach((meshDefinition, meshIndex) => {
|
|
138
|
-
const mesh = context.meshes[meshIndex];
|
|
139
|
-
const primitives = mesh.listPrimitives();
|
|
140
|
-
meshDefinition.primitives?.forEach((primitiveDefinition, primitiveIndex) => {
|
|
141
|
-
if (!primitiveDefinition.extensions)
|
|
142
|
-
return;
|
|
143
|
-
const ext = primitiveDefinition.extensions[EXTENSION_NAME];
|
|
144
|
-
if (!ext)
|
|
145
|
-
return;
|
|
146
|
-
const primitive = primitives[primitiveIndex];
|
|
147
|
-
const prop = this.createExtension();
|
|
148
|
-
this.jsonPointerHandler.read(context, primitive, ext, prop);
|
|
149
|
-
primitive.setExtension(EXTENSION_NAME, prop);
|
|
150
|
-
// this is invoked on write
|
|
151
|
-
prop.add(ctx => {
|
|
152
|
-
const newMeshIndex = ctx.meshIndexMap.get(mesh);
|
|
153
|
-
const newMesh = ctx.jsonDoc.json.meshes[newMeshIndex];
|
|
154
|
-
const newPrimitive = newMesh.primitives[primitiveIndex];
|
|
155
|
-
newPrimitive.extensions = newPrimitive.extensions || {};
|
|
156
|
-
newPrimitive.extensions[EXTENSION_NAME] = ext;
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
break;
|
|
161
|
-
case PropertyType.ROOT:
|
|
162
|
-
const sourceArray = context.jsonDoc.json.extensions;
|
|
163
|
-
if (!sourceArray)
|
|
164
|
-
continue;
|
|
165
|
-
const ext = sourceArray[EXTENSION_NAME];
|
|
166
|
-
if (ext) {
|
|
167
|
-
const root = this.document.getRoot();
|
|
168
|
-
const prop = this.createExtension();
|
|
169
|
-
this.jsonPointerHandler.read(context, root, ext, prop);
|
|
170
|
-
root.setExtension(EXTENSION_NAME, prop);
|
|
171
|
-
this.lightmapWorkaround(context, prop, this.document.getLogger());
|
|
172
|
-
prop.add(ctx => {
|
|
173
|
-
ctx.jsonDoc.json.extensions = ctx.jsonDoc.json.extensions || {};
|
|
174
|
-
ctx.jsonDoc.json.extensions[EXTENSION_NAME] = ext;
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
break;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return this;
|
|
181
|
-
}
|
|
182
|
-
// this is temporarely here to prevent compression on textures named Lightmap
|
|
183
|
-
// until we have better control over the compression process
|
|
184
|
-
lightmapWorkaround(context, prop, logger) {
|
|
185
|
-
const textureDefinitions = context.jsonDoc.json.textures;
|
|
186
|
-
if (!textureDefinitions)
|
|
187
|
-
return;
|
|
188
|
-
const textures = this.document.getRoot().listTextures();
|
|
189
|
-
for (let i = 0; i < textures.length; i++) {
|
|
190
|
-
const tex = textures[i];
|
|
191
|
-
const texDef = textureDefinitions[i];
|
|
192
|
-
const name = texDef?.name;
|
|
193
|
-
if (isLightmap(tex) && tex.getMimeType() !== "image/exr") {
|
|
194
|
-
prop.setReference("lightmapTexture", tex, { channels: TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A });
|
|
195
|
-
logger.info(`Found lightmap texture ${name}: ${tex.getMimeType()} in ${EXTENSION_NAME}`);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
createExtension(name = '') {
|
|
200
|
-
return new OpaqueProperty(this.document.getGraph(), name);
|
|
201
|
-
;
|
|
202
|
-
}
|
|
203
|
-
onRead(context, gltfField, map) {
|
|
204
|
-
if (gltfField === undefined)
|
|
205
|
-
return;
|
|
206
|
-
try {
|
|
207
|
-
const sourceArray = context.jsonDoc.json[gltfField];
|
|
208
|
-
const target = context[gltfField];
|
|
209
|
-
sourceArray?.forEach((def, index) => {
|
|
210
|
-
if (!def.extensions || !def.extensions[EXTENSION_NAME])
|
|
211
|
-
return;
|
|
212
|
-
const ext = def.extensions[EXTENSION_NAME];
|
|
213
|
-
if (debugLog)
|
|
214
|
-
console.log("READ", index, ext);
|
|
215
|
-
const prop = this.createExtension();
|
|
216
|
-
// attach the property
|
|
217
|
-
target[index].setExtension(EXTENSION_NAME, prop);
|
|
218
|
-
// capture all the json pointers in the extension
|
|
219
|
-
this.jsonPointerHandler.read(context, target[index], ext, prop);
|
|
220
|
-
// make sure we write our extension back to the gltf
|
|
221
|
-
prop.add(new OpaqueWriter(map, gltfField, ext));
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
catch (error) {
|
|
225
|
-
console.error("ERR: " + error);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
prewrite(context, _propertyType) {
|
|
229
|
-
// Attempt resolve textures here so that the json.textures is already populated for cases where a texture isn't used by any material
|
|
230
|
-
// this is e.g. the case for textures that are just referenced by a component but not used by any texture
|
|
231
|
-
// https://linear.app/needle/issue/NE-4928
|
|
232
|
-
this.jsonPointerHandler.resolveAndWrite(context, "prewrite");
|
|
233
|
-
return this;
|
|
234
|
-
}
|
|
235
|
-
write(context) {
|
|
236
|
-
if (opts?.breakpoint === true)
|
|
237
|
-
debugger;
|
|
238
|
-
if (opts?.write === false) {
|
|
239
|
-
return this;
|
|
240
|
-
}
|
|
241
|
-
if (debugLog)
|
|
242
|
-
console.log("WRITE " + EXTENSION_NAME);
|
|
243
|
-
this.jsonPointerHandler.resolveAndWrite(context, "write");
|
|
244
|
-
const root = this.document.getRoot();
|
|
245
|
-
// for (const rootExt of this.roots) {
|
|
246
|
-
// rootExt.onWrite(context, root);
|
|
247
|
-
// }
|
|
248
|
-
for (const type of types) {
|
|
249
|
-
switch (type) {
|
|
250
|
-
case PropertyType.ACCESSOR:
|
|
251
|
-
this.onWrite(context, () => root.listAccessors());
|
|
252
|
-
break;
|
|
253
|
-
case PropertyType.ANIMATION:
|
|
254
|
-
this.onWrite(context, () => root.listAnimations());
|
|
255
|
-
break;
|
|
256
|
-
case PropertyType.ANIMATION_CHANNEL:
|
|
257
|
-
break;
|
|
258
|
-
case PropertyType.ANIMATION_SAMPLER:
|
|
259
|
-
break;
|
|
260
|
-
case PropertyType.BUFFER:
|
|
261
|
-
this.onWrite(context, () => root.listBuffers());
|
|
262
|
-
break;
|
|
263
|
-
case PropertyType.CAMERA:
|
|
264
|
-
this.onWrite(context, () => root.listCameras());
|
|
265
|
-
break;
|
|
266
|
-
case PropertyType.MATERIAL:
|
|
267
|
-
this.onWrite(context, () => root.listMaterials());
|
|
268
|
-
break;
|
|
269
|
-
case PropertyType.TEXTURE:
|
|
270
|
-
this.onWrite(context, () => root.listTextures());
|
|
271
|
-
break;
|
|
272
|
-
case PropertyType.NODE:
|
|
273
|
-
this.onWrite(context, () => root.listNodes());
|
|
274
|
-
break;
|
|
275
|
-
case PropertyType.MESH:
|
|
276
|
-
this.onWrite(context, () => root.listMeshes());
|
|
277
|
-
break;
|
|
278
|
-
case PropertyType.PRIMITIVE:
|
|
279
|
-
const allPrimitives = root.listMeshes().flatMap(mesh => mesh.listPrimitives());
|
|
280
|
-
for (const primitive of allPrimitives) {
|
|
281
|
-
const ext = primitive.getExtension(EXTENSION_NAME);
|
|
282
|
-
if (ext instanceof OpaqueProperty) {
|
|
283
|
-
ext.onWrite(context, primitive);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
break;
|
|
287
|
-
case PropertyType.ROOT:
|
|
288
|
-
const prop = root.getExtension(EXTENSION_NAME);
|
|
289
|
-
if (prop instanceof OpaqueProperty) {
|
|
290
|
-
prop.onWrite(context, root);
|
|
291
|
-
}
|
|
292
|
-
else if (debugLog)
|
|
293
|
-
console.warn("WARN: no extension found on root", EXTENSION_NAME);
|
|
294
|
-
break;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
return this;
|
|
298
|
-
}
|
|
299
|
-
onWrite(context, get) {
|
|
300
|
-
const arr = get();
|
|
301
|
-
if (!arr)
|
|
302
|
-
return;
|
|
303
|
-
for (const entry of arr) {
|
|
304
|
-
const ext = entry.getExtension(EXTENSION_NAME);
|
|
305
|
-
if (ext instanceof OpaqueProperty) {
|
|
306
|
-
ext.onWrite(context, entry);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
// function setExtension(ext: ExtensibleProperty, name, extensionProperty) {
|
|
313
|
-
// // for opaque extension we need to bypass the type system
|
|
314
|
-
// // because we generate the OpaqueProperty class at runtime and thus the method name for _validateParent is different when compiled
|
|
315
|
-
// // there's likely a better way to do this... as usual
|
|
316
|
-
// const pext = ext as any as { setRefMap: (mapName: string, key: string, value: any) => any }
|
|
317
|
-
// return pext.setRefMap('extensions', name, extensionProperty);
|
|
318
|
-
// }
|
|
319
|
-
class OpaqueProperty extends ExtensionProperty {
|
|
320
|
-
static EXTENSION_NAME = EXTENSION_NAME;
|
|
321
|
-
extensionName = typeof EXTENSION_NAME;
|
|
322
|
-
parentTypes = ExtensionPropertyTypes;
|
|
323
|
-
propertyType = "NEEDLE_opaque_property";
|
|
324
|
-
init() {
|
|
325
|
-
}
|
|
326
|
-
setReference(name, value, attributes = { modifyChild: true }) {
|
|
327
|
-
this.setRef(name, value, attributes);
|
|
328
|
-
}
|
|
329
|
-
writers = [];
|
|
330
|
-
add(writer) {
|
|
331
|
-
if (typeof writer === "function") {
|
|
332
|
-
writer = new GenericWriter(writer);
|
|
333
|
-
}
|
|
334
|
-
this.writers.push(writer);
|
|
335
|
-
}
|
|
336
|
-
onWrite(context, prop) {
|
|
337
|
-
for (const writer of this.writers) {
|
|
338
|
-
writer.write?.call(writer, context, prop);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
getExtensionDefinition() {
|
|
342
|
-
const ext = {};
|
|
343
|
-
for (const writer of this.writers) {
|
|
344
|
-
if (writer.assignTo)
|
|
345
|
-
writer.assignTo(ext);
|
|
346
|
-
}
|
|
347
|
-
return ext;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
// called after the transformation
|
|
351
|
-
// writes the extension json back to the correct object
|
|
352
|
-
class OpaqueWriter {
|
|
353
|
-
map;
|
|
354
|
-
gltfField;
|
|
355
|
-
ext;
|
|
356
|
-
assignTo(obj) {
|
|
357
|
-
Object.assign(obj, this.ext);
|
|
358
|
-
}
|
|
359
|
-
constructor(map, gltfField, ext) {
|
|
360
|
-
this.map = map;
|
|
361
|
-
this.gltfField = gltfField;
|
|
362
|
-
this.ext = ext;
|
|
363
|
-
}
|
|
364
|
-
write(context, prop) {
|
|
365
|
-
if (prop && this.gltfField !== undefined) {
|
|
366
|
-
const map = context[this.map];
|
|
367
|
-
const index = map.get(prop);
|
|
368
|
-
const result = tryGetInArray(context.jsonDoc.json[this.gltfField], index);
|
|
369
|
-
writeExtension(result, EXTENSION_NAME, this.ext);
|
|
370
|
-
if (debugLog) {
|
|
371
|
-
// console.log("WRITE", EXTENSION_NAME, "to", this.gltfField, "at index", index);
|
|
372
|
-
// console.log("WRITE RESULT", result);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
return NEEDLE_opaque;
|
|
378
|
-
}
|
|
379
|
-
/** use to register a generic callback */
|
|
380
|
-
class GenericWriter {
|
|
381
|
-
fn;
|
|
382
|
-
constructor(fn) {
|
|
383
|
-
this.fn = fn;
|
|
384
|
-
}
|
|
385
|
-
write(ctx) {
|
|
386
|
-
this.fn(ctx);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
class TextureHandler {
|
|
390
|
-
document;
|
|
391
|
-
textureDefinition;
|
|
392
|
-
index;
|
|
393
|
-
key;
|
|
394
|
-
ext;
|
|
395
|
-
debug = false;
|
|
396
|
-
assignTo(obj) {
|
|
397
|
-
Object.assign(obj, this.ext);
|
|
398
|
-
}
|
|
399
|
-
constructor(doc, def, key, ext, debug) {
|
|
400
|
-
this.document = doc;
|
|
401
|
-
this.textureDefinition = def;
|
|
402
|
-
this.key = key;
|
|
403
|
-
this.ext = ext;
|
|
404
|
-
this.debug = debug;
|
|
405
|
-
this.index = this.tryGetSourceIndex(this.textureDefinition);
|
|
406
|
-
if (debug)
|
|
407
|
-
console.log(def);
|
|
408
|
-
}
|
|
409
|
-
write(ctx) {
|
|
410
|
-
let foundTexture = false;
|
|
411
|
-
const textureDefs = ctx.jsonDoc.json.textures || [];
|
|
412
|
-
const imageIndex = this.index !== undefined ? this.index : this.tryGetSourceIndex(this.textureDefinition);
|
|
413
|
-
if (imageIndex === undefined) {
|
|
414
|
-
console.warn("WARN: no source index found for texture", this.key);
|
|
415
|
-
}
|
|
416
|
-
textureDefs?.forEach((textureDef, i) => {
|
|
417
|
-
if (foundTexture)
|
|
418
|
-
return;
|
|
419
|
-
const textureImageIndex = this.tryGetSourceIndex(textureDef);
|
|
420
|
-
if (imageIndex === textureImageIndex) {
|
|
421
|
-
if (!textureDef.extensions || !textureDef.extensions[this.key]) {
|
|
422
|
-
// assign texture extension again
|
|
423
|
-
textureDef.extensions = textureDef.extensions || {};
|
|
424
|
-
foundTexture = true;
|
|
425
|
-
if (this.debug)
|
|
426
|
-
console.log("Re-Assign extension:", this.key, "at", i);
|
|
427
|
-
const newExt = { ...this.ext };
|
|
428
|
-
if (newExt.source !== undefined)
|
|
429
|
-
newExt.source = imageIndex;
|
|
430
|
-
textureDef.extensions[this.key] = newExt;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
});
|
|
434
|
-
if (foundTexture === false) {
|
|
435
|
-
if (this.key !== "EXT_texture_exr" && this.debug)
|
|
436
|
-
console.warn("WARN: failed to re-assign extension:", this.key, this.index, this.tryGetSourceIndex(this.textureDefinition), textureDefs.length);
|
|
437
|
-
}
|
|
438
|
-
// this.document
|
|
439
|
-
// .getRoot()
|
|
440
|
-
// .listTextures()
|
|
441
|
-
// .forEach((texture, index) => {
|
|
442
|
-
// if (foundTexture) return;
|
|
443
|
-
// if (!texture.getExtension(this.key)) return;
|
|
444
|
-
// const imageIndex = ctx.imageIndexMap.get(texture);
|
|
445
|
-
// });
|
|
446
|
-
}
|
|
447
|
-
tryGetSourceIndex(def) {
|
|
448
|
-
let index = def.source;
|
|
449
|
-
if (index !== undefined)
|
|
450
|
-
return index;
|
|
451
|
-
// if a glb was already compressed the source index might be missing so we also need to search in extensions
|
|
452
|
-
if (def.extensions) {
|
|
453
|
-
for (const ext of Object.keys(def.extensions)) {
|
|
454
|
-
const extDef = def.extensions[ext];
|
|
455
|
-
if (extDef.source !== undefined) {
|
|
456
|
-
index = extDef.source;
|
|
457
|
-
break;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
return index;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
// declare type MaterialTextureCallback = (ctx: WriterContext, map: TemporaryMaterial, index: number) => void;
|
|
465
|
-
// class TemporaryMaterial implements IExtensionWriter {
|
|
466
|
-
// private readonly document: Document;
|
|
467
|
-
// private readonly material: Material;
|
|
468
|
-
// private readonly tex: Texture;
|
|
469
|
-
// private readonly callbacks: MaterialTextureCallback[] = [];
|
|
470
|
-
// constructor(doc: Document, tex: Texture, name: string) {
|
|
471
|
-
// console.log("Create material", name);
|
|
472
|
-
// this.document = doc;
|
|
473
|
-
// this.tex = tex;
|
|
474
|
-
// this.material = this.document
|
|
475
|
-
// .createMaterial(name)
|
|
476
|
-
// .setBaseColorTexture(tex);
|
|
477
|
-
// }
|
|
478
|
-
// add(cb: MaterialTextureCallback): TemporaryMaterial {
|
|
479
|
-
// this.callbacks.push(cb);
|
|
480
|
-
// return this;
|
|
481
|
-
// }
|
|
482
|
-
// write(context: WriterContext): void {
|
|
483
|
-
// const matIndex = context.materialIndexMap.get(this.material);
|
|
484
|
-
// if (matIndex !== undefined && matIndex !== -1) {
|
|
485
|
-
// console.log("Remove material", this.material.getName(), matIndex);
|
|
486
|
-
// context.jsonDoc.json.materials?.splice(matIndex, 1);
|
|
487
|
-
// if (this.callbacks.length > 0) {
|
|
488
|
-
// const texInfo = this.material.getBaseColorTextureInfo();
|
|
489
|
-
// if (texInfo) {
|
|
490
|
-
// const def = context.textureInfoDefMap.get(texInfo);
|
|
491
|
-
// if (def) {
|
|
492
|
-
// const index = def.index;
|
|
493
|
-
// for (const cb of this.callbacks) {
|
|
494
|
-
// cb(context, this, index);
|
|
495
|
-
// }
|
|
496
|
-
// }
|
|
497
|
-
// }
|
|
498
|
-
// }
|
|
499
|
-
// }
|
|
500
|
-
// }
|
|
501
|
-
// }
|
|
502
|
-
// json pointer -------------------------------------------------------------
|
|
503
|
-
// abstract class PointerProperty extends ExtensionProperty {
|
|
504
|
-
// abstract setPointer(texture: Texture, name: string);
|
|
505
|
-
// }
|
|
506
|
-
// const { R, G, B, A } = TextureChannel;
|
|
507
|
-
// function createPointerProperty(EXTENSION_NAME: string, doc: Document): PointerProperty {
|
|
508
|
-
// class TexturePointerPointerProperty extends ExtensionProperty<any> {
|
|
509
|
-
// static EXTENSION_NAME = EXTENSION_NAME;
|
|
510
|
-
// readonly extensionName: string = EXTENSION_NAME;
|
|
511
|
-
// parentTypes: PropertyType[] = ALL_PROPERTY_TYPES;
|
|
512
|
-
// propertyType = "NEEDLE_pointer_property";
|
|
513
|
-
// init() { }
|
|
514
|
-
// public setPointer(texture: Texture | null, name: string): this {
|
|
515
|
-
// console.log("SET REF", name);
|
|
516
|
-
// return this.setRef(name, texture, { channels: R | G | B | A });
|
|
517
|
-
// }
|
|
518
|
-
// }
|
|
519
|
-
// return new TexturePointerPointerProperty(doc.getGraph());
|
|
520
|
-
// }
|
|
521
|
-
class JsonPointerHandler {
|
|
522
|
-
extension_name;
|
|
523
|
-
document;
|
|
524
|
-
map = new Map();
|
|
525
|
-
debug = false;
|
|
526
|
-
breakPoint = false;
|
|
527
|
-
VRM_0_MaterialIndexRegex = new RegExp(/materialProperties\/(?<material_index>\d+)\//g);
|
|
528
|
-
constructor(document, extension_name) {
|
|
529
|
-
this.document = document;
|
|
530
|
-
this.extension_name = extension_name;
|
|
531
|
-
}
|
|
532
|
-
read(context, extProp, obj, prop, key, fullPath) {
|
|
533
|
-
const value = key !== undefined ? obj[key] : obj;
|
|
534
|
-
// the pointer path contains the full path from the root object to the json pointer
|
|
535
|
-
if (key !== undefined) {
|
|
536
|
-
fullPath ??= "";
|
|
537
|
-
fullPath += "/" + key;
|
|
538
|
-
if (this.breakPoint === "pointer:read") {
|
|
539
|
-
debugger;
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
// if(this.debug && key === "colliders") debugger;
|
|
543
|
-
switch (typeof value) {
|
|
544
|
-
case 'object':
|
|
545
|
-
if (value !== null && value !== undefined) {
|
|
546
|
-
if (Array.isArray(value)) {
|
|
547
|
-
for (let i = 0; i < value.length; i++) {
|
|
548
|
-
this.read(context, extProp, value, prop, i.toString(), fullPath);
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
else {
|
|
552
|
-
for (const key of Object.keys(value)) {
|
|
553
|
-
this.read(context, extProp, value, prop, key, fullPath);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
break;
|
|
558
|
-
case "string":
|
|
559
|
-
if (value && key) {
|
|
560
|
-
let prefix = "/textures/";
|
|
561
|
-
let isTexturePointer = value.startsWith(prefix);
|
|
562
|
-
if (!isTexturePointer) {
|
|
563
|
-
// backwards compatibility for old format where it didnt start with /textures/ but with textures/
|
|
564
|
-
prefix = "textures/";
|
|
565
|
-
isTexturePointer = value.startsWith(prefix);
|
|
566
|
-
}
|
|
567
|
-
if (isTexturePointer) {
|
|
568
|
-
if (this.debug)
|
|
569
|
-
console.log("Read pointer", value);
|
|
570
|
-
let pointer = this.map.get(value);
|
|
571
|
-
if (!pointer) {
|
|
572
|
-
const length = prefix.length;
|
|
573
|
-
const index = parseInt(value.substring(length));
|
|
574
|
-
const path = value.substring(0, length);
|
|
575
|
-
const definition = context.jsonDoc.json.textures[index];
|
|
576
|
-
const imageIndex = findSource(definition);
|
|
577
|
-
if (this.debug)
|
|
578
|
-
console.log("Texture index", index, imageIndex, context.jsonDoc.json.textures.length, "Textures:" + context.textures.length);
|
|
579
|
-
const object = tryGetInArray(context.textures, imageIndex);
|
|
580
|
-
if (object) {
|
|
581
|
-
const res = new TextureIndexResolver(this.document, object, definition);
|
|
582
|
-
res.debug = this.debug;
|
|
583
|
-
res.init(context); // < init here otherwise the fields defined in the TexturePointer will be undefined (this caused e.g. finding the previous texture filter to fail)
|
|
584
|
-
pointer = new JsonPointer(path, res, value);
|
|
585
|
-
pointer.debug = this.debug;
|
|
586
|
-
this.map.set(value, pointer);
|
|
587
|
-
// Assume all texture slots are being used. The referenceName is the json pointer right now - usually this is e.g. `baseColorTexture` (the name ends up in the slots array)
|
|
588
|
-
const referenceName = value;
|
|
589
|
-
// const slots = listTextureSlots(object);
|
|
590
|
-
prop.setReference(referenceName, object, { channels: TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A });
|
|
591
|
-
// make sure there's a reference to the buffer to avoid dedup or the buffer being removed because it MIGHT not be used by any node
|
|
592
|
-
// TODO: how do we get the buffer that the texture/image is currently using - here we just take the first buffer but this might not be correct
|
|
593
|
-
const buffer = this.document.getRoot().listBuffers()[0];
|
|
594
|
-
if (buffer)
|
|
595
|
-
prop.setReference("buffer", buffer);
|
|
596
|
-
}
|
|
597
|
-
else {
|
|
598
|
-
console.warn("Texture not found", fullPath, value);
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
if (pointer) {
|
|
602
|
-
if (this.debug)
|
|
603
|
-
console.log("> Register pointer", this.extension_name + "/" + fullPath + " : " + value);
|
|
604
|
-
pointer?.register(obj, key, fullPath);
|
|
605
|
-
}
|
|
606
|
-
else
|
|
607
|
-
console.warn("WARN: failed registering pointer", fullPath, value);
|
|
608
|
-
}
|
|
609
|
-
if (value.startsWith("/materials/")) {
|
|
610
|
-
if (this.debug)
|
|
611
|
-
console.log("Read pointer", value);
|
|
612
|
-
const index = parseInt(value.substring("/materials/".length));
|
|
613
|
-
const object = context.materials[index];
|
|
614
|
-
if (object) {
|
|
615
|
-
// set a reference to the material to avoid dedup or the material being removed because it MIGHT not be used by any node
|
|
616
|
-
// BUT a component is referencing the material so we don't want it ever to be removed
|
|
617
|
-
// see https://github.com/donmccurdy/glTF-Transform/issues/980#issuecomment-1585748202
|
|
618
|
-
if (prop) {
|
|
619
|
-
prop.setReference(value, object);
|
|
620
|
-
}
|
|
621
|
-
let pointer = this.map.get(value);
|
|
622
|
-
if (!pointer) {
|
|
623
|
-
const definition = context.jsonDoc.json.materials[index];
|
|
624
|
-
const res = new MaterialIndexResolver(this.document, object, definition);
|
|
625
|
-
res.debug = this.debug;
|
|
626
|
-
res.init(context); // < init here otherwise the fields defined in the TexturePointer will be undefined (this caused e.g. finding the previous texture filter to fail)
|
|
627
|
-
pointer = new JsonPointer("/materials/", res, value);
|
|
628
|
-
pointer.debug = this.debug;
|
|
629
|
-
this.map.set(value, pointer);
|
|
630
|
-
prop.setReference(value, object);
|
|
631
|
-
}
|
|
632
|
-
pointer?.register(obj, key, fullPath);
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
if (value.startsWith("/animations/")) {
|
|
636
|
-
if (this.debug)
|
|
637
|
-
console.log("Read pointer", value);
|
|
638
|
-
const index = parseInt(value.substring("/animations/".length));
|
|
639
|
-
const object = context.animations[index];
|
|
640
|
-
if (object) {
|
|
641
|
-
let pointer = this.map.get(value);
|
|
642
|
-
if (!pointer) {
|
|
643
|
-
// TODO: resolve animations + we need to set the reference properly so we can use prune. See https://linear.app/needle/issue/NE-4982
|
|
644
|
-
// const definition = context.jsonDoc.json.animations![index];
|
|
645
|
-
// const res = new AnimationIndexResolver(this.document, object, definition);
|
|
646
|
-
// res.debug = this.debug;
|
|
647
|
-
// res.init(context);
|
|
648
|
-
// pointer = new JsonPointer("/animations/", res, value);
|
|
649
|
-
// pointer.debug = this.debug;
|
|
650
|
-
// this.map.set(value, pointer);
|
|
651
|
-
prop.setReference(value, object);
|
|
652
|
-
}
|
|
653
|
-
pointer?.register(obj, key, fullPath);
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
if (value.startsWith("/meshes/")) {
|
|
657
|
-
if (this.debug)
|
|
658
|
-
console.log("Read pointer", value);
|
|
659
|
-
const index = parseInt(value.substring("/meshes/".length));
|
|
660
|
-
const object = context.meshes[index];
|
|
661
|
-
if (object) {
|
|
662
|
-
let pointer = this.map.get(value);
|
|
663
|
-
if (!pointer) {
|
|
664
|
-
const definition = context.jsonDoc.json.meshes[index];
|
|
665
|
-
const res = new MeshIndexResolver(this.document, object, definition, prop);
|
|
666
|
-
res.debug = this.debug;
|
|
667
|
-
res.init(context);
|
|
668
|
-
pointer = new JsonPointer("/meshes/", res, value);
|
|
669
|
-
pointer.debug = this.debug;
|
|
670
|
-
this.map.set(value, pointer);
|
|
671
|
-
prop.setReference(value, object);
|
|
672
|
-
}
|
|
673
|
-
pointer?.register(obj, key, fullPath);
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
break;
|
|
678
|
-
case 'number':
|
|
679
|
-
if (key === "node" || key === "mesh"
|
|
680
|
-
// VRM 1.0 spring bone center https://github.com/vrm-c/vrm-specification/blob/master/specification/VRMC_springBone-1.0/schema/VRMC_springBone.spring.schema.json
|
|
681
|
-
|| fullPath.match(/springs\/\d+\/center/g)) {
|
|
682
|
-
const object = context.nodes[value];
|
|
683
|
-
if (object) {
|
|
684
|
-
const res = new NodeIndexResolver(this.document, object, context.jsonDoc.json.nodes[value]);
|
|
685
|
-
res.debug = this.debug;
|
|
686
|
-
res.init(context);
|
|
687
|
-
const dictKey = `/${key}/${value}`;
|
|
688
|
-
let pointer = this.map.get(dictKey);
|
|
689
|
-
if (!pointer) {
|
|
690
|
-
pointer = new JsonPointer(null, res, value);
|
|
691
|
-
}
|
|
692
|
-
pointer.debug = this.debug;
|
|
693
|
-
pointer.register(obj, key, fullPath);
|
|
694
|
-
this.map.set(dictKey, pointer);
|
|
695
|
-
prop.setReference(dictKey, object);
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
else if (
|
|
699
|
-
// key texture is matching for example for VRM meta/texture (AvatarSample_A)
|
|
700
|
-
key === "texture"
|
|
701
|
-
// VRM 0.0 material properties
|
|
702
|
-
|| fullPath.match(/materialProperties\/(?<material_index>\d+)\/textureProperties/g)
|
|
703
|
-
// VRM 1.0 meta
|
|
704
|
-
|| fullPath.match(/meta\/thumbnailImage/g)
|
|
705
|
-
// NEEDLE_materials_mtlx where the root extension has { textures: [0, 1, 2, ...] }
|
|
706
|
-
|| fullPath.match(/^\/textures\/\d{1,}$/)) {
|
|
707
|
-
const index = value;
|
|
708
|
-
// e.g. in meta.texture for VRM avatars it seems like we can have negative values
|
|
709
|
-
if (index < 0) {
|
|
710
|
-
console.warn(`WARN: texture index is negative ${index} at ${fullPath}`);
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
const definition = context.jsonDoc.json.textures[index];
|
|
714
|
-
if (!definition) {
|
|
715
|
-
console.warn(`WARN: texture definition with index ${index} not found or does not exist. Set in \"${fullPath}\"`);
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
const imageIndex = findSource(definition);
|
|
719
|
-
if (this.debug)
|
|
720
|
-
console.log("Texture index", index, imageIndex, context.jsonDoc.json.textures.length, "Textures:" + context.textures.length);
|
|
721
|
-
const object = tryGetInArray(context.textures, imageIndex);
|
|
722
|
-
if (object) {
|
|
723
|
-
if (this.debug)
|
|
724
|
-
console.log("Found texture index at " + fullPath + ": " + value);
|
|
725
|
-
const res = new TextureIndexResolver(this.document, object, definition);
|
|
726
|
-
res.debug = this.debug;
|
|
727
|
-
res.init(context);
|
|
728
|
-
const dictKey = `/textures/${value}`;
|
|
729
|
-
let pointer = this.map.get(dictKey);
|
|
730
|
-
if (!pointer) {
|
|
731
|
-
pointer = new JsonPointer(null, res, value);
|
|
732
|
-
}
|
|
733
|
-
pointer.debug = this.debug;
|
|
734
|
-
pointer.register(obj, key, fullPath);
|
|
735
|
-
this.map.set(dictKey, pointer);
|
|
736
|
-
prop.setReference(dictKey, object, { channels: TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A });
|
|
737
|
-
}
|
|
738
|
-
const materialMatch = this.VRM_0_MaterialIndexRegex.exec(fullPath);
|
|
739
|
-
if (materialMatch) {
|
|
740
|
-
const materialIndex = parseInt(materialMatch.groups.material_index);
|
|
741
|
-
const material = context.materials[materialIndex];
|
|
742
|
-
prop.setReference(`/materials/${materialIndex}`, material);
|
|
743
|
-
// const res = new MaterialIndexResolver(this.document, material, context.jsonDoc.json.materials![materialIndex]);
|
|
744
|
-
// res.debug = this.debug;
|
|
745
|
-
// res.init(context);
|
|
746
|
-
// const dictKey = `/materials/${materialIndex}`
|
|
747
|
-
// let pointer = this.map.get(dictKey);
|
|
748
|
-
// if (!pointer) {
|
|
749
|
-
// pointer = new JsonPointer(null, res, value);
|
|
750
|
-
// }
|
|
751
|
-
// pointer.debug = this.debug;
|
|
752
|
-
// pointer.register(obj, key, fullPath);
|
|
753
|
-
// this.map.set(dictKey, pointer);
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
break;
|
|
757
|
-
default:
|
|
758
|
-
break;
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
resolveAndWrite(context, step) {
|
|
762
|
-
this.map?.forEach(ptr => {
|
|
763
|
-
if (ptr.resolve(context, step))
|
|
764
|
-
ptr.write();
|
|
765
|
-
});
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
class JsonPointer {
|
|
769
|
-
debug = false;
|
|
770
|
-
resolver;
|
|
771
|
-
pointerBase;
|
|
772
|
-
oldPointer;
|
|
773
|
-
constructor(pointerBase, resolver, oldPointer) {
|
|
774
|
-
this.pointerBase = pointerBase;
|
|
775
|
-
this.resolver = resolver;
|
|
776
|
-
this.oldPointer = oldPointer;
|
|
777
|
-
}
|
|
778
|
-
_objects = [];
|
|
779
|
-
_props = [];
|
|
780
|
-
_debugInfo = [];
|
|
781
|
-
register(obj, prop, fullpath) {
|
|
782
|
-
this._objects.push(obj);
|
|
783
|
-
this._props.push(prop);
|
|
784
|
-
this._debugInfo.push(fullpath);
|
|
785
|
-
}
|
|
786
|
-
newPointer = undefined;
|
|
787
|
-
resolve(context, step) {
|
|
788
|
-
// If the pointer was already resolved, we don't need to do it again
|
|
789
|
-
if (this.newPointer !== undefined) {
|
|
790
|
-
return true;
|
|
791
|
-
}
|
|
792
|
-
//@ts-ignore
|
|
793
|
-
this.resolver.debug = this.debug;
|
|
794
|
-
const index = this.resolver.resolve(context);
|
|
795
|
-
if (index != undefined) {
|
|
796
|
-
if (this.pointerBase != null) {
|
|
797
|
-
this.newPointer = this.pointerBase + index;
|
|
798
|
-
}
|
|
799
|
-
else {
|
|
800
|
-
this.newPointer = index;
|
|
801
|
-
}
|
|
802
|
-
return true;
|
|
803
|
-
}
|
|
804
|
-
else {
|
|
805
|
-
// If the pointer could not be resolved during write we need to log an error
|
|
806
|
-
if (step === "write") {
|
|
807
|
-
console.log(`ERR: 🧨 FAILED to resolve pointer \"${this.oldPointer}\" used by ${this._debugInfo.join(", ")}`);
|
|
808
|
-
if (this.debug)
|
|
809
|
-
debugger;
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
return false;
|
|
813
|
-
}
|
|
814
|
-
_didWrite = false;
|
|
815
|
-
write() {
|
|
816
|
-
if (this._didWrite)
|
|
817
|
-
return;
|
|
818
|
-
this._didWrite = false;
|
|
819
|
-
for (let i = 0; i < this._objects.length; i++) {
|
|
820
|
-
const obj = this._objects[i];
|
|
821
|
-
const prop = this._props[i];
|
|
822
|
-
if (this.debug) {
|
|
823
|
-
console.log(`< Resolved pointer ${this._debugInfo[i]}: ${this.oldPointer} → ${this.newPointer}`);
|
|
824
|
-
}
|
|
825
|
-
obj[prop] = this.newPointer;
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
class PointerResolver {
|
|
830
|
-
document;
|
|
831
|
-
/* the object originally being referenced by the pointer */
|
|
832
|
-
inputObject;
|
|
833
|
-
/* the original json definition */
|
|
834
|
-
inputDefinition;
|
|
835
|
-
/** The graph property this pointer belongs to */
|
|
836
|
-
property;
|
|
837
|
-
constructor(doc, obj, def, prop) {
|
|
838
|
-
this.document = doc;
|
|
839
|
-
this.inputObject = obj;
|
|
840
|
-
this.inputDefinition = def;
|
|
841
|
-
this.property = prop;
|
|
842
|
-
}
|
|
843
|
-
debug = false;
|
|
844
|
-
}
|
|
845
|
-
class TextureIndexResolver extends PointerResolver {
|
|
846
|
-
_debugName;
|
|
847
|
-
_samplerDef;
|
|
848
|
-
init(context) {
|
|
849
|
-
this._debugName = this.inputDefinition.name ?? this.inputDefinition.source?.toString() ?? "unknown";
|
|
850
|
-
const samplerIndex = this.inputDefinition.sampler;
|
|
851
|
-
if (samplerIndex !== undefined) {
|
|
852
|
-
this._samplerDef = context.jsonDoc.json.samplers[samplerIndex];
|
|
853
|
-
}
|
|
854
|
-
if (this.debug)
|
|
855
|
-
console.log(this._debugName, this._samplerDef);
|
|
856
|
-
}
|
|
857
|
-
resolve(context) {
|
|
858
|
-
const imgIndex = context.imageIndexMap.get(this.inputObject);
|
|
859
|
-
if (imgIndex === undefined) {
|
|
860
|
-
console.error("ERR: image is missing", imgIndex);
|
|
861
|
-
return -1;
|
|
862
|
-
}
|
|
863
|
-
// ensure we have a texture array
|
|
864
|
-
context.jsonDoc.json.textures = context.jsonDoc.json.textures || [];
|
|
865
|
-
// try find the texture that points to the same image source
|
|
866
|
-
for (let i = 0; i < context.jsonDoc.json.textures.length; i++) {
|
|
867
|
-
const textureDef = context.jsonDoc.json.textures[i];
|
|
868
|
-
if (findSource(textureDef) === imgIndex) {
|
|
869
|
-
if (textureDef.sampler === undefined) {
|
|
870
|
-
textureDef.sampler = this.findOrCreateSampler(context.jsonDoc.json);
|
|
871
|
-
}
|
|
872
|
-
if (this.debug)
|
|
873
|
-
console.log("Found texture def at " + i, "using image", imgIndex, textureDef);
|
|
874
|
-
return i;
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
// the texture was not found - add it
|
|
878
|
-
// create a new texture def entry
|
|
879
|
-
if (this.debug)
|
|
880
|
-
console.log("Could not find texture, creating a new one: " + this._debugName);
|
|
881
|
-
const hasExtensionWithSource = this.inputDefinition.extensions && Object.keys(this.inputDefinition.extensions).some(key => this.inputDefinition.extensions[key]["source"] !== undefined);
|
|
882
|
-
const textureDef = {
|
|
883
|
-
source: hasExtensionWithSource ? undefined : imgIndex,
|
|
884
|
-
sampler: this.findOrCreateSampler(context.jsonDoc.json),
|
|
885
|
-
extensions: this.inputDefinition.extensions,
|
|
886
|
-
extras: this.inputDefinition.extras,
|
|
887
|
-
};
|
|
888
|
-
context.jsonDoc.json.textures.push(textureDef);
|
|
889
|
-
return context.jsonDoc.json.textures.length - 1;
|
|
890
|
-
}
|
|
891
|
-
findOrCreateSampler(json) {
|
|
892
|
-
json.samplers = json.samplers || [];
|
|
893
|
-
if (this._samplerDef === undefined) {
|
|
894
|
-
console.log("WARN: Creating default sampler", this._debugName);
|
|
895
|
-
// create a default sampler
|
|
896
|
-
this._samplerDef = {
|
|
897
|
-
magFilter: 9729,
|
|
898
|
-
minFilter: 9729,
|
|
899
|
-
wrapS: 33648,
|
|
900
|
-
wrapT: 33648
|
|
901
|
-
};
|
|
902
|
-
}
|
|
903
|
-
else {
|
|
904
|
-
for (let i = 0; i < json.samplers.length; i++) {
|
|
905
|
-
const sampler = json.samplers[i];
|
|
906
|
-
if (sampler.minFilter === this._samplerDef.minFilter &&
|
|
907
|
-
sampler.magFilter === this._samplerDef.magFilter &&
|
|
908
|
-
sampler.wrapS === this._samplerDef.wrapS &&
|
|
909
|
-
sampler.wrapT === this._samplerDef.wrapT) {
|
|
910
|
-
return i;
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
// append previous sampler
|
|
915
|
-
json.samplers.push(this._samplerDef);
|
|
916
|
-
return json.samplers.length - 1;
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
function findSource(texDef) {
|
|
920
|
-
if (texDef.source !== undefined) {
|
|
921
|
-
return texDef.source;
|
|
922
|
-
}
|
|
923
|
-
if (texDef.extensions) {
|
|
924
|
-
const keys = Object.keys(texDef.extensions);
|
|
925
|
-
for (let i = 0; i < keys.length; i++) {
|
|
926
|
-
const key = keys[i];
|
|
927
|
-
const ext = texDef.extensions[key];
|
|
928
|
-
if (ext["source"] !== undefined) {
|
|
929
|
-
return ext["source"];
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
return undefined;
|
|
934
|
-
}
|
|
935
|
-
class MaterialIndexResolver extends PointerResolver {
|
|
936
|
-
init(context) {
|
|
937
|
-
}
|
|
938
|
-
resolve(context) {
|
|
939
|
-
return context.materialIndexMap.get(this.inputObject);
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
class NodeIndexResolver extends PointerResolver {
|
|
943
|
-
init(context) {
|
|
944
|
-
}
|
|
945
|
-
resolve(context) {
|
|
946
|
-
return context.nodeIndexMap.get(this.inputObject);
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
class MeshIndexResolver extends PointerResolver {
|
|
950
|
-
init(context) {
|
|
951
|
-
}
|
|
952
|
-
resolve(context) {
|
|
953
|
-
let index = context.meshIndexMap.get(this.inputObject);
|
|
954
|
-
if (index >= 0) {
|
|
955
|
-
return index;
|
|
956
|
-
}
|
|
957
|
-
let obj = null;
|
|
958
|
-
if (this.inputObject.isDisposed()) {
|
|
959
|
-
// If the mesh was deduplicated we try to get the mesh from a parent node
|
|
960
|
-
// This is a HACK, we need to get the reference to the mesh this one was deduplicated with since the parent node potentially has many children
|
|
961
|
-
const parents = this.property?.listParents();
|
|
962
|
-
for (const parent of parents) {
|
|
963
|
-
if (parent instanceof Node) {
|
|
964
|
-
obj = parent.getMesh();
|
|
965
|
-
if (obj) {
|
|
966
|
-
this.document.getLogger().info(`INFO: resolved mesh \"${this.inputObject.getName()}\" from parent node \"${parent.getName()}\"`);
|
|
967
|
-
break;
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
if (obj.isDisposed()) {
|
|
972
|
-
console.warn("WARN: mesh is disposed", this.inputObject.getName());
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
if (!obj) {
|
|
976
|
-
return undefined;
|
|
977
|
-
}
|
|
978
|
-
return context.meshIndexMap.get(obj);
|
|
979
|
-
}
|
|
980
|
-
}
|
|
1
|
+
import { Extension, ExtensionProperty, PropertyType, Node, TextureChannel } from '@gltf-transform/core';
|
|
2
|
+
import { getTexture, tryGetInArray, writeExtension } from '../utils.js';
|
|
3
|
+
import { isLightmap } from '../../utils/texture.js';
|
|
4
|
+
const ALL_PROPERTY_TYPES = [];
|
|
5
|
+
for (const key in PropertyType) {
|
|
6
|
+
ALL_PROPERTY_TYPES.push(PropertyType[key]);
|
|
7
|
+
}
|
|
8
|
+
export let currentGenerator = "";
|
|
9
|
+
// an opaque extension can be used to pass data through the gltf transform process
|
|
10
|
+
// it should also be able to re-write/fix json pointers after the transformation which might have shuffled them around
|
|
11
|
+
export function createOpaqueExtension(name, types, opts) {
|
|
12
|
+
if (!Array.isArray(types))
|
|
13
|
+
types = [types];
|
|
14
|
+
const EXTENSION_NAME = name;
|
|
15
|
+
const ExtensionPropertyTypes = ALL_PROPERTY_TYPES;
|
|
16
|
+
const debugLog = opts?.debug ?? false;
|
|
17
|
+
class NEEDLE_opaque extends Extension {
|
|
18
|
+
extensionName = EXTENSION_NAME;
|
|
19
|
+
static EXTENSION_NAME = EXTENSION_NAME;
|
|
20
|
+
prewriteTypes = [PropertyType.MATERIAL];
|
|
21
|
+
// private roots: Array<OpaqueProperty> = [];
|
|
22
|
+
jsonPointerHandler;
|
|
23
|
+
read(context) {
|
|
24
|
+
if (opts?.breakpoint === true)
|
|
25
|
+
debugger;
|
|
26
|
+
currentGenerator = context.jsonDoc.json.asset?.generator;
|
|
27
|
+
if (debugLog)
|
|
28
|
+
console.log("READ " + EXTENSION_NAME);
|
|
29
|
+
this.jsonPointerHandler = new JsonPointerHandler(this.document, EXTENSION_NAME);
|
|
30
|
+
this.jsonPointerHandler.debug = debugLog;
|
|
31
|
+
this.jsonPointerHandler.breakPoint = opts?.breakpoint;
|
|
32
|
+
for (const type of types) {
|
|
33
|
+
switch (type) {
|
|
34
|
+
case PropertyType.ACCESSOR:
|
|
35
|
+
this.onRead(context, "accessors", "accessorIndexMap");
|
|
36
|
+
break;
|
|
37
|
+
case PropertyType.ANIMATION:
|
|
38
|
+
this.onRead(context, "animations", "animationIndexMap");
|
|
39
|
+
break;
|
|
40
|
+
case PropertyType.ANIMATION_CHANNEL:
|
|
41
|
+
if (EXTENSION_NAME === "KHR_animation_pointer") {
|
|
42
|
+
context.jsonDoc.json.animations?.forEach((animDef, animIndex) => {
|
|
43
|
+
const anim = context.animations[animIndex];
|
|
44
|
+
const channels = anim.listChannels();
|
|
45
|
+
animDef.channels?.forEach((channel, channelIndex) => {
|
|
46
|
+
const jsonTarget = channel.target;
|
|
47
|
+
const khr_animation = jsonTarget.extensions && jsonTarget.extensions[EXTENSION_NAME];
|
|
48
|
+
if (!khr_animation)
|
|
49
|
+
return;
|
|
50
|
+
const pointer = khr_animation.pointer;
|
|
51
|
+
if (pointer) {
|
|
52
|
+
const sections = pointer.split("/");
|
|
53
|
+
const nodeIndex = parseInt(sections[2]);
|
|
54
|
+
const targetName = sections[3];
|
|
55
|
+
if (targetName === "scale" || targetName === "translation" || targetName === "rotation") {
|
|
56
|
+
channels[channelIndex].setTargetNode(context.nodes[nodeIndex]);
|
|
57
|
+
channels[channelIndex].setTargetPath(targetName);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const existing = anim.getExtension(EXTENSION_NAME);
|
|
61
|
+
if (debugLog)
|
|
62
|
+
console.log(">>>", EXTENSION_NAME, "animation", animIndex, "channel", channelIndex, khr_animation);
|
|
63
|
+
if (existing) {
|
|
64
|
+
assignChannels(existing);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const prop = this.createExtension();
|
|
68
|
+
// there seems no way (yet?) to add extensions to channel.targets (?)
|
|
69
|
+
// https://github.com/donmccurdy/glTF-Transform/discussions/613
|
|
70
|
+
// so we add it to an object by keys to the animation itself and read it back in WRITE
|
|
71
|
+
const khr_animation_data = {};
|
|
72
|
+
khr_animation_data[channelIndex] = khr_animation;
|
|
73
|
+
anim.setExtension(EXTENSION_NAME, prop);
|
|
74
|
+
assignChannels(prop);
|
|
75
|
+
function assignChannels(prop) {
|
|
76
|
+
prop.add(ctx => {
|
|
77
|
+
const animations = ctx.jsonDoc.json.animations;
|
|
78
|
+
if (!animations)
|
|
79
|
+
return;
|
|
80
|
+
const animDef = animations[animIndex];
|
|
81
|
+
const channel = animDef?.channels[channelIndex];
|
|
82
|
+
if (channel) {
|
|
83
|
+
if (debugLog)
|
|
84
|
+
console.log("<<<", EXTENSION_NAME, "animation", animIndex, "channel", channelIndex, khr_animation);
|
|
85
|
+
channel.target = jsonTarget;
|
|
86
|
+
}
|
|
87
|
+
else if (debugLog)
|
|
88
|
+
console.log("WARN: ", EXTENSION_NAME, "animation", animIndex, "channel", channelIndex, "not found");
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
;
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
case PropertyType.ANIMATION_SAMPLER:
|
|
97
|
+
break;
|
|
98
|
+
case PropertyType.BUFFER:
|
|
99
|
+
this.onRead(context, "buffers", "bufferIndexMap");
|
|
100
|
+
break;
|
|
101
|
+
case PropertyType.CAMERA:
|
|
102
|
+
this.onRead(context, "cameras", "cameraIndexMap");
|
|
103
|
+
break;
|
|
104
|
+
case PropertyType.MATERIAL:
|
|
105
|
+
this.onRead(context, "materials", "materialIndexMap");
|
|
106
|
+
break;
|
|
107
|
+
case PropertyType.TEXTURE:
|
|
108
|
+
context.jsonDoc.json.textures?.forEach((textureDefinition, index) => {
|
|
109
|
+
if (!textureDefinition.extensions)
|
|
110
|
+
return;
|
|
111
|
+
const ext = textureDefinition.extensions[EXTENSION_NAME];
|
|
112
|
+
if (!ext)
|
|
113
|
+
return;
|
|
114
|
+
// TODO: textures and textureDefinition are not in the same order?
|
|
115
|
+
// see https://github.com/donmccurdy/glTF-Transform/blob/0e99c426b8ef41536228fe860ab8329be5d1d50f/packages/core/src/io/reader.ts#L132
|
|
116
|
+
// textures are in image order
|
|
117
|
+
// TODO: we need to use the image index to find the texture
|
|
118
|
+
let texture = getTexture(context, textureDefinition);
|
|
119
|
+
if (!texture) {
|
|
120
|
+
console.log("WARN: could potentially not find gltf transform texture for " + index);
|
|
121
|
+
texture = context.textures[index];
|
|
122
|
+
}
|
|
123
|
+
const prop = this.createExtension();
|
|
124
|
+
this.jsonPointerHandler.read(context, texture, ext, prop);
|
|
125
|
+
texture.setExtension(EXTENSION_NAME, prop);
|
|
126
|
+
const handler = new TextureHandler(this.document, textureDefinition, EXTENSION_NAME, ext, debugLog);
|
|
127
|
+
prop.add(handler);
|
|
128
|
+
});
|
|
129
|
+
break;
|
|
130
|
+
case PropertyType.NODE:
|
|
131
|
+
this.onRead(context, "nodes", "nodeIndexMap");
|
|
132
|
+
break;
|
|
133
|
+
case PropertyType.MESH:
|
|
134
|
+
this.onRead(context, "meshes", "meshIndexMap");
|
|
135
|
+
break;
|
|
136
|
+
case PropertyType.PRIMITIVE:
|
|
137
|
+
context.jsonDoc.json.meshes?.forEach((meshDefinition, meshIndex) => {
|
|
138
|
+
const mesh = context.meshes[meshIndex];
|
|
139
|
+
const primitives = mesh.listPrimitives();
|
|
140
|
+
meshDefinition.primitives?.forEach((primitiveDefinition, primitiveIndex) => {
|
|
141
|
+
if (!primitiveDefinition.extensions)
|
|
142
|
+
return;
|
|
143
|
+
const ext = primitiveDefinition.extensions[EXTENSION_NAME];
|
|
144
|
+
if (!ext)
|
|
145
|
+
return;
|
|
146
|
+
const primitive = primitives[primitiveIndex];
|
|
147
|
+
const prop = this.createExtension();
|
|
148
|
+
this.jsonPointerHandler.read(context, primitive, ext, prop);
|
|
149
|
+
primitive.setExtension(EXTENSION_NAME, prop);
|
|
150
|
+
// this is invoked on write
|
|
151
|
+
prop.add(ctx => {
|
|
152
|
+
const newMeshIndex = ctx.meshIndexMap.get(mesh);
|
|
153
|
+
const newMesh = ctx.jsonDoc.json.meshes[newMeshIndex];
|
|
154
|
+
const newPrimitive = newMesh.primitives[primitiveIndex];
|
|
155
|
+
newPrimitive.extensions = newPrimitive.extensions || {};
|
|
156
|
+
newPrimitive.extensions[EXTENSION_NAME] = ext;
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
break;
|
|
161
|
+
case PropertyType.ROOT:
|
|
162
|
+
const sourceArray = context.jsonDoc.json.extensions;
|
|
163
|
+
if (!sourceArray)
|
|
164
|
+
continue;
|
|
165
|
+
const ext = sourceArray[EXTENSION_NAME];
|
|
166
|
+
if (ext) {
|
|
167
|
+
const root = this.document.getRoot();
|
|
168
|
+
const prop = this.createExtension();
|
|
169
|
+
this.jsonPointerHandler.read(context, root, ext, prop);
|
|
170
|
+
root.setExtension(EXTENSION_NAME, prop);
|
|
171
|
+
this.lightmapWorkaround(context, prop, this.document.getLogger());
|
|
172
|
+
prop.add(ctx => {
|
|
173
|
+
ctx.jsonDoc.json.extensions = ctx.jsonDoc.json.extensions || {};
|
|
174
|
+
ctx.jsonDoc.json.extensions[EXTENSION_NAME] = ext;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return this;
|
|
181
|
+
}
|
|
182
|
+
// this is temporarely here to prevent compression on textures named Lightmap
|
|
183
|
+
// until we have better control over the compression process
|
|
184
|
+
lightmapWorkaround(context, prop, logger) {
|
|
185
|
+
const textureDefinitions = context.jsonDoc.json.textures;
|
|
186
|
+
if (!textureDefinitions)
|
|
187
|
+
return;
|
|
188
|
+
const textures = this.document.getRoot().listTextures();
|
|
189
|
+
for (let i = 0; i < textures.length; i++) {
|
|
190
|
+
const tex = textures[i];
|
|
191
|
+
const texDef = textureDefinitions[i];
|
|
192
|
+
const name = texDef?.name;
|
|
193
|
+
if (isLightmap(tex) && tex.getMimeType() !== "image/exr") {
|
|
194
|
+
prop.setReference("lightmapTexture", tex, { channels: TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A });
|
|
195
|
+
logger.info(`Found lightmap texture ${name}: ${tex.getMimeType()} in ${EXTENSION_NAME}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
createExtension(name = '') {
|
|
200
|
+
return new OpaqueProperty(this.document.getGraph(), name);
|
|
201
|
+
;
|
|
202
|
+
}
|
|
203
|
+
onRead(context, gltfField, map) {
|
|
204
|
+
if (gltfField === undefined)
|
|
205
|
+
return;
|
|
206
|
+
try {
|
|
207
|
+
const sourceArray = context.jsonDoc.json[gltfField];
|
|
208
|
+
const target = context[gltfField];
|
|
209
|
+
sourceArray?.forEach((def, index) => {
|
|
210
|
+
if (!def.extensions || !def.extensions[EXTENSION_NAME])
|
|
211
|
+
return;
|
|
212
|
+
const ext = def.extensions[EXTENSION_NAME];
|
|
213
|
+
if (debugLog)
|
|
214
|
+
console.log("READ", index, ext);
|
|
215
|
+
const prop = this.createExtension();
|
|
216
|
+
// attach the property
|
|
217
|
+
target[index].setExtension(EXTENSION_NAME, prop);
|
|
218
|
+
// capture all the json pointers in the extension
|
|
219
|
+
this.jsonPointerHandler.read(context, target[index], ext, prop);
|
|
220
|
+
// make sure we write our extension back to the gltf
|
|
221
|
+
prop.add(new OpaqueWriter(map, gltfField, ext));
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
console.error("ERR: " + error);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
prewrite(context, _propertyType) {
|
|
229
|
+
// Attempt resolve textures here so that the json.textures is already populated for cases where a texture isn't used by any material
|
|
230
|
+
// this is e.g. the case for textures that are just referenced by a component but not used by any texture
|
|
231
|
+
// https://linear.app/needle/issue/NE-4928
|
|
232
|
+
this.jsonPointerHandler.resolveAndWrite(context, "prewrite");
|
|
233
|
+
return this;
|
|
234
|
+
}
|
|
235
|
+
write(context) {
|
|
236
|
+
if (opts?.breakpoint === true)
|
|
237
|
+
debugger;
|
|
238
|
+
if (opts?.write === false) {
|
|
239
|
+
return this;
|
|
240
|
+
}
|
|
241
|
+
if (debugLog)
|
|
242
|
+
console.log("WRITE " + EXTENSION_NAME);
|
|
243
|
+
this.jsonPointerHandler.resolveAndWrite(context, "write");
|
|
244
|
+
const root = this.document.getRoot();
|
|
245
|
+
// for (const rootExt of this.roots) {
|
|
246
|
+
// rootExt.onWrite(context, root);
|
|
247
|
+
// }
|
|
248
|
+
for (const type of types) {
|
|
249
|
+
switch (type) {
|
|
250
|
+
case PropertyType.ACCESSOR:
|
|
251
|
+
this.onWrite(context, () => root.listAccessors());
|
|
252
|
+
break;
|
|
253
|
+
case PropertyType.ANIMATION:
|
|
254
|
+
this.onWrite(context, () => root.listAnimations());
|
|
255
|
+
break;
|
|
256
|
+
case PropertyType.ANIMATION_CHANNEL:
|
|
257
|
+
break;
|
|
258
|
+
case PropertyType.ANIMATION_SAMPLER:
|
|
259
|
+
break;
|
|
260
|
+
case PropertyType.BUFFER:
|
|
261
|
+
this.onWrite(context, () => root.listBuffers());
|
|
262
|
+
break;
|
|
263
|
+
case PropertyType.CAMERA:
|
|
264
|
+
this.onWrite(context, () => root.listCameras());
|
|
265
|
+
break;
|
|
266
|
+
case PropertyType.MATERIAL:
|
|
267
|
+
this.onWrite(context, () => root.listMaterials());
|
|
268
|
+
break;
|
|
269
|
+
case PropertyType.TEXTURE:
|
|
270
|
+
this.onWrite(context, () => root.listTextures());
|
|
271
|
+
break;
|
|
272
|
+
case PropertyType.NODE:
|
|
273
|
+
this.onWrite(context, () => root.listNodes());
|
|
274
|
+
break;
|
|
275
|
+
case PropertyType.MESH:
|
|
276
|
+
this.onWrite(context, () => root.listMeshes());
|
|
277
|
+
break;
|
|
278
|
+
case PropertyType.PRIMITIVE:
|
|
279
|
+
const allPrimitives = root.listMeshes().flatMap(mesh => mesh.listPrimitives());
|
|
280
|
+
for (const primitive of allPrimitives) {
|
|
281
|
+
const ext = primitive.getExtension(EXTENSION_NAME);
|
|
282
|
+
if (ext instanceof OpaqueProperty) {
|
|
283
|
+
ext.onWrite(context, primitive);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
break;
|
|
287
|
+
case PropertyType.ROOT:
|
|
288
|
+
const prop = root.getExtension(EXTENSION_NAME);
|
|
289
|
+
if (prop instanceof OpaqueProperty) {
|
|
290
|
+
prop.onWrite(context, root);
|
|
291
|
+
}
|
|
292
|
+
else if (debugLog)
|
|
293
|
+
console.warn("WARN: no extension found on root", EXTENSION_NAME);
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return this;
|
|
298
|
+
}
|
|
299
|
+
onWrite(context, get) {
|
|
300
|
+
const arr = get();
|
|
301
|
+
if (!arr)
|
|
302
|
+
return;
|
|
303
|
+
for (const entry of arr) {
|
|
304
|
+
const ext = entry.getExtension(EXTENSION_NAME);
|
|
305
|
+
if (ext instanceof OpaqueProperty) {
|
|
306
|
+
ext.onWrite(context, entry);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// function setExtension(ext: ExtensibleProperty, name, extensionProperty) {
|
|
313
|
+
// // for opaque extension we need to bypass the type system
|
|
314
|
+
// // because we generate the OpaqueProperty class at runtime and thus the method name for _validateParent is different when compiled
|
|
315
|
+
// // there's likely a better way to do this... as usual
|
|
316
|
+
// const pext = ext as any as { setRefMap: (mapName: string, key: string, value: any) => any }
|
|
317
|
+
// return pext.setRefMap('extensions', name, extensionProperty);
|
|
318
|
+
// }
|
|
319
|
+
class OpaqueProperty extends ExtensionProperty {
|
|
320
|
+
static EXTENSION_NAME = EXTENSION_NAME;
|
|
321
|
+
extensionName = typeof EXTENSION_NAME;
|
|
322
|
+
parentTypes = ExtensionPropertyTypes;
|
|
323
|
+
propertyType = "NEEDLE_opaque_property";
|
|
324
|
+
init() {
|
|
325
|
+
}
|
|
326
|
+
setReference(name, value, attributes = { modifyChild: true }) {
|
|
327
|
+
this.setRef(name, value, attributes);
|
|
328
|
+
}
|
|
329
|
+
writers = [];
|
|
330
|
+
add(writer) {
|
|
331
|
+
if (typeof writer === "function") {
|
|
332
|
+
writer = new GenericWriter(writer);
|
|
333
|
+
}
|
|
334
|
+
this.writers.push(writer);
|
|
335
|
+
}
|
|
336
|
+
onWrite(context, prop) {
|
|
337
|
+
for (const writer of this.writers) {
|
|
338
|
+
writer.write?.call(writer, context, prop);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
getExtensionDefinition() {
|
|
342
|
+
const ext = {};
|
|
343
|
+
for (const writer of this.writers) {
|
|
344
|
+
if (writer.assignTo)
|
|
345
|
+
writer.assignTo(ext);
|
|
346
|
+
}
|
|
347
|
+
return ext;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// called after the transformation
|
|
351
|
+
// writes the extension json back to the correct object
|
|
352
|
+
class OpaqueWriter {
|
|
353
|
+
map;
|
|
354
|
+
gltfField;
|
|
355
|
+
ext;
|
|
356
|
+
assignTo(obj) {
|
|
357
|
+
Object.assign(obj, this.ext);
|
|
358
|
+
}
|
|
359
|
+
constructor(map, gltfField, ext) {
|
|
360
|
+
this.map = map;
|
|
361
|
+
this.gltfField = gltfField;
|
|
362
|
+
this.ext = ext;
|
|
363
|
+
}
|
|
364
|
+
write(context, prop) {
|
|
365
|
+
if (prop && this.gltfField !== undefined) {
|
|
366
|
+
const map = context[this.map];
|
|
367
|
+
const index = map.get(prop);
|
|
368
|
+
const result = tryGetInArray(context.jsonDoc.json[this.gltfField], index);
|
|
369
|
+
writeExtension(result, EXTENSION_NAME, this.ext);
|
|
370
|
+
if (debugLog) {
|
|
371
|
+
// console.log("WRITE", EXTENSION_NAME, "to", this.gltfField, "at index", index);
|
|
372
|
+
// console.log("WRITE RESULT", result);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return NEEDLE_opaque;
|
|
378
|
+
}
|
|
379
|
+
/** use to register a generic callback */
|
|
380
|
+
class GenericWriter {
|
|
381
|
+
fn;
|
|
382
|
+
constructor(fn) {
|
|
383
|
+
this.fn = fn;
|
|
384
|
+
}
|
|
385
|
+
write(ctx) {
|
|
386
|
+
this.fn(ctx);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
class TextureHandler {
|
|
390
|
+
document;
|
|
391
|
+
textureDefinition;
|
|
392
|
+
index;
|
|
393
|
+
key;
|
|
394
|
+
ext;
|
|
395
|
+
debug = false;
|
|
396
|
+
assignTo(obj) {
|
|
397
|
+
Object.assign(obj, this.ext);
|
|
398
|
+
}
|
|
399
|
+
constructor(doc, def, key, ext, debug) {
|
|
400
|
+
this.document = doc;
|
|
401
|
+
this.textureDefinition = def;
|
|
402
|
+
this.key = key;
|
|
403
|
+
this.ext = ext;
|
|
404
|
+
this.debug = debug;
|
|
405
|
+
this.index = this.tryGetSourceIndex(this.textureDefinition);
|
|
406
|
+
if (debug)
|
|
407
|
+
console.log(def);
|
|
408
|
+
}
|
|
409
|
+
write(ctx) {
|
|
410
|
+
let foundTexture = false;
|
|
411
|
+
const textureDefs = ctx.jsonDoc.json.textures || [];
|
|
412
|
+
const imageIndex = this.index !== undefined ? this.index : this.tryGetSourceIndex(this.textureDefinition);
|
|
413
|
+
if (imageIndex === undefined) {
|
|
414
|
+
console.warn("WARN: no source index found for texture", this.key);
|
|
415
|
+
}
|
|
416
|
+
textureDefs?.forEach((textureDef, i) => {
|
|
417
|
+
if (foundTexture)
|
|
418
|
+
return;
|
|
419
|
+
const textureImageIndex = this.tryGetSourceIndex(textureDef);
|
|
420
|
+
if (imageIndex === textureImageIndex) {
|
|
421
|
+
if (!textureDef.extensions || !textureDef.extensions[this.key]) {
|
|
422
|
+
// assign texture extension again
|
|
423
|
+
textureDef.extensions = textureDef.extensions || {};
|
|
424
|
+
foundTexture = true;
|
|
425
|
+
if (this.debug)
|
|
426
|
+
console.log("Re-Assign extension:", this.key, "at", i);
|
|
427
|
+
const newExt = { ...this.ext };
|
|
428
|
+
if (newExt.source !== undefined)
|
|
429
|
+
newExt.source = imageIndex;
|
|
430
|
+
textureDef.extensions[this.key] = newExt;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
if (foundTexture === false) {
|
|
435
|
+
if (this.key !== "EXT_texture_exr" && this.debug)
|
|
436
|
+
console.warn("WARN: failed to re-assign extension:", this.key, this.index, this.tryGetSourceIndex(this.textureDefinition), textureDefs.length);
|
|
437
|
+
}
|
|
438
|
+
// this.document
|
|
439
|
+
// .getRoot()
|
|
440
|
+
// .listTextures()
|
|
441
|
+
// .forEach((texture, index) => {
|
|
442
|
+
// if (foundTexture) return;
|
|
443
|
+
// if (!texture.getExtension(this.key)) return;
|
|
444
|
+
// const imageIndex = ctx.imageIndexMap.get(texture);
|
|
445
|
+
// });
|
|
446
|
+
}
|
|
447
|
+
tryGetSourceIndex(def) {
|
|
448
|
+
let index = def.source;
|
|
449
|
+
if (index !== undefined)
|
|
450
|
+
return index;
|
|
451
|
+
// if a glb was already compressed the source index might be missing so we also need to search in extensions
|
|
452
|
+
if (def.extensions) {
|
|
453
|
+
for (const ext of Object.keys(def.extensions)) {
|
|
454
|
+
const extDef = def.extensions[ext];
|
|
455
|
+
if (extDef.source !== undefined) {
|
|
456
|
+
index = extDef.source;
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return index;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
// declare type MaterialTextureCallback = (ctx: WriterContext, map: TemporaryMaterial, index: number) => void;
|
|
465
|
+
// class TemporaryMaterial implements IExtensionWriter {
|
|
466
|
+
// private readonly document: Document;
|
|
467
|
+
// private readonly material: Material;
|
|
468
|
+
// private readonly tex: Texture;
|
|
469
|
+
// private readonly callbacks: MaterialTextureCallback[] = [];
|
|
470
|
+
// constructor(doc: Document, tex: Texture, name: string) {
|
|
471
|
+
// console.log("Create material", name);
|
|
472
|
+
// this.document = doc;
|
|
473
|
+
// this.tex = tex;
|
|
474
|
+
// this.material = this.document
|
|
475
|
+
// .createMaterial(name)
|
|
476
|
+
// .setBaseColorTexture(tex);
|
|
477
|
+
// }
|
|
478
|
+
// add(cb: MaterialTextureCallback): TemporaryMaterial {
|
|
479
|
+
// this.callbacks.push(cb);
|
|
480
|
+
// return this;
|
|
481
|
+
// }
|
|
482
|
+
// write(context: WriterContext): void {
|
|
483
|
+
// const matIndex = context.materialIndexMap.get(this.material);
|
|
484
|
+
// if (matIndex !== undefined && matIndex !== -1) {
|
|
485
|
+
// console.log("Remove material", this.material.getName(), matIndex);
|
|
486
|
+
// context.jsonDoc.json.materials?.splice(matIndex, 1);
|
|
487
|
+
// if (this.callbacks.length > 0) {
|
|
488
|
+
// const texInfo = this.material.getBaseColorTextureInfo();
|
|
489
|
+
// if (texInfo) {
|
|
490
|
+
// const def = context.textureInfoDefMap.get(texInfo);
|
|
491
|
+
// if (def) {
|
|
492
|
+
// const index = def.index;
|
|
493
|
+
// for (const cb of this.callbacks) {
|
|
494
|
+
// cb(context, this, index);
|
|
495
|
+
// }
|
|
496
|
+
// }
|
|
497
|
+
// }
|
|
498
|
+
// }
|
|
499
|
+
// }
|
|
500
|
+
// }
|
|
501
|
+
// }
|
|
502
|
+
// json pointer -------------------------------------------------------------
|
|
503
|
+
// abstract class PointerProperty extends ExtensionProperty {
|
|
504
|
+
// abstract setPointer(texture: Texture, name: string);
|
|
505
|
+
// }
|
|
506
|
+
// const { R, G, B, A } = TextureChannel;
|
|
507
|
+
// function createPointerProperty(EXTENSION_NAME: string, doc: Document): PointerProperty {
|
|
508
|
+
// class TexturePointerPointerProperty extends ExtensionProperty<any> {
|
|
509
|
+
// static EXTENSION_NAME = EXTENSION_NAME;
|
|
510
|
+
// readonly extensionName: string = EXTENSION_NAME;
|
|
511
|
+
// parentTypes: PropertyType[] = ALL_PROPERTY_TYPES;
|
|
512
|
+
// propertyType = "NEEDLE_pointer_property";
|
|
513
|
+
// init() { }
|
|
514
|
+
// public setPointer(texture: Texture | null, name: string): this {
|
|
515
|
+
// console.log("SET REF", name);
|
|
516
|
+
// return this.setRef(name, texture, { channels: R | G | B | A });
|
|
517
|
+
// }
|
|
518
|
+
// }
|
|
519
|
+
// return new TexturePointerPointerProperty(doc.getGraph());
|
|
520
|
+
// }
|
|
521
|
+
class JsonPointerHandler {
|
|
522
|
+
extension_name;
|
|
523
|
+
document;
|
|
524
|
+
map = new Map();
|
|
525
|
+
debug = false;
|
|
526
|
+
breakPoint = false;
|
|
527
|
+
VRM_0_MaterialIndexRegex = new RegExp(/materialProperties\/(?<material_index>\d+)\//g);
|
|
528
|
+
constructor(document, extension_name) {
|
|
529
|
+
this.document = document;
|
|
530
|
+
this.extension_name = extension_name;
|
|
531
|
+
}
|
|
532
|
+
read(context, extProp, obj, prop, key, fullPath) {
|
|
533
|
+
const value = key !== undefined ? obj[key] : obj;
|
|
534
|
+
// the pointer path contains the full path from the root object to the json pointer
|
|
535
|
+
if (key !== undefined) {
|
|
536
|
+
fullPath ??= "";
|
|
537
|
+
fullPath += "/" + key;
|
|
538
|
+
if (this.breakPoint === "pointer:read") {
|
|
539
|
+
debugger;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// if(this.debug && key === "colliders") debugger;
|
|
543
|
+
switch (typeof value) {
|
|
544
|
+
case 'object':
|
|
545
|
+
if (value !== null && value !== undefined) {
|
|
546
|
+
if (Array.isArray(value)) {
|
|
547
|
+
for (let i = 0; i < value.length; i++) {
|
|
548
|
+
this.read(context, extProp, value, prop, i.toString(), fullPath);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
for (const key of Object.keys(value)) {
|
|
553
|
+
this.read(context, extProp, value, prop, key, fullPath);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
break;
|
|
558
|
+
case "string":
|
|
559
|
+
if (value && key) {
|
|
560
|
+
let prefix = "/textures/";
|
|
561
|
+
let isTexturePointer = value.startsWith(prefix);
|
|
562
|
+
if (!isTexturePointer) {
|
|
563
|
+
// backwards compatibility for old format where it didnt start with /textures/ but with textures/
|
|
564
|
+
prefix = "textures/";
|
|
565
|
+
isTexturePointer = value.startsWith(prefix);
|
|
566
|
+
}
|
|
567
|
+
if (isTexturePointer) {
|
|
568
|
+
if (this.debug)
|
|
569
|
+
console.log("Read pointer", value);
|
|
570
|
+
let pointer = this.map.get(value);
|
|
571
|
+
if (!pointer) {
|
|
572
|
+
const length = prefix.length;
|
|
573
|
+
const index = parseInt(value.substring(length));
|
|
574
|
+
const path = value.substring(0, length);
|
|
575
|
+
const definition = context.jsonDoc.json.textures[index];
|
|
576
|
+
const imageIndex = findSource(definition);
|
|
577
|
+
if (this.debug)
|
|
578
|
+
console.log("Texture index", index, imageIndex, context.jsonDoc.json.textures.length, "Textures:" + context.textures.length);
|
|
579
|
+
const object = tryGetInArray(context.textures, imageIndex);
|
|
580
|
+
if (object) {
|
|
581
|
+
const res = new TextureIndexResolver(this.document, object, definition);
|
|
582
|
+
res.debug = this.debug;
|
|
583
|
+
res.init(context); // < init here otherwise the fields defined in the TexturePointer will be undefined (this caused e.g. finding the previous texture filter to fail)
|
|
584
|
+
pointer = new JsonPointer(path, res, value);
|
|
585
|
+
pointer.debug = this.debug;
|
|
586
|
+
this.map.set(value, pointer);
|
|
587
|
+
// Assume all texture slots are being used. The referenceName is the json pointer right now - usually this is e.g. `baseColorTexture` (the name ends up in the slots array)
|
|
588
|
+
const referenceName = value;
|
|
589
|
+
// const slots = listTextureSlots(object);
|
|
590
|
+
prop.setReference(referenceName, object, { channels: TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A });
|
|
591
|
+
// make sure there's a reference to the buffer to avoid dedup or the buffer being removed because it MIGHT not be used by any node
|
|
592
|
+
// TODO: how do we get the buffer that the texture/image is currently using - here we just take the first buffer but this might not be correct
|
|
593
|
+
const buffer = this.document.getRoot().listBuffers()[0];
|
|
594
|
+
if (buffer)
|
|
595
|
+
prop.setReference("buffer", buffer);
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
console.warn("Texture not found", fullPath, value);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (pointer) {
|
|
602
|
+
if (this.debug)
|
|
603
|
+
console.log("> Register pointer", this.extension_name + "/" + fullPath + " : " + value);
|
|
604
|
+
pointer?.register(obj, key, fullPath);
|
|
605
|
+
}
|
|
606
|
+
else
|
|
607
|
+
console.warn("WARN: failed registering pointer", fullPath, value);
|
|
608
|
+
}
|
|
609
|
+
if (value.startsWith("/materials/")) {
|
|
610
|
+
if (this.debug)
|
|
611
|
+
console.log("Read pointer", value);
|
|
612
|
+
const index = parseInt(value.substring("/materials/".length));
|
|
613
|
+
const object = context.materials[index];
|
|
614
|
+
if (object) {
|
|
615
|
+
// set a reference to the material to avoid dedup or the material being removed because it MIGHT not be used by any node
|
|
616
|
+
// BUT a component is referencing the material so we don't want it ever to be removed
|
|
617
|
+
// see https://github.com/donmccurdy/glTF-Transform/issues/980#issuecomment-1585748202
|
|
618
|
+
if (prop) {
|
|
619
|
+
prop.setReference(value, object);
|
|
620
|
+
}
|
|
621
|
+
let pointer = this.map.get(value);
|
|
622
|
+
if (!pointer) {
|
|
623
|
+
const definition = context.jsonDoc.json.materials[index];
|
|
624
|
+
const res = new MaterialIndexResolver(this.document, object, definition);
|
|
625
|
+
res.debug = this.debug;
|
|
626
|
+
res.init(context); // < init here otherwise the fields defined in the TexturePointer will be undefined (this caused e.g. finding the previous texture filter to fail)
|
|
627
|
+
pointer = new JsonPointer("/materials/", res, value);
|
|
628
|
+
pointer.debug = this.debug;
|
|
629
|
+
this.map.set(value, pointer);
|
|
630
|
+
prop.setReference(value, object);
|
|
631
|
+
}
|
|
632
|
+
pointer?.register(obj, key, fullPath);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (value.startsWith("/animations/")) {
|
|
636
|
+
if (this.debug)
|
|
637
|
+
console.log("Read pointer", value);
|
|
638
|
+
const index = parseInt(value.substring("/animations/".length));
|
|
639
|
+
const object = context.animations[index];
|
|
640
|
+
if (object) {
|
|
641
|
+
let pointer = this.map.get(value);
|
|
642
|
+
if (!pointer) {
|
|
643
|
+
// TODO: resolve animations + we need to set the reference properly so we can use prune. See https://linear.app/needle/issue/NE-4982
|
|
644
|
+
// const definition = context.jsonDoc.json.animations![index];
|
|
645
|
+
// const res = new AnimationIndexResolver(this.document, object, definition);
|
|
646
|
+
// res.debug = this.debug;
|
|
647
|
+
// res.init(context);
|
|
648
|
+
// pointer = new JsonPointer("/animations/", res, value);
|
|
649
|
+
// pointer.debug = this.debug;
|
|
650
|
+
// this.map.set(value, pointer);
|
|
651
|
+
prop.setReference(value, object);
|
|
652
|
+
}
|
|
653
|
+
pointer?.register(obj, key, fullPath);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (value.startsWith("/meshes/")) {
|
|
657
|
+
if (this.debug)
|
|
658
|
+
console.log("Read pointer", value);
|
|
659
|
+
const index = parseInt(value.substring("/meshes/".length));
|
|
660
|
+
const object = context.meshes[index];
|
|
661
|
+
if (object) {
|
|
662
|
+
let pointer = this.map.get(value);
|
|
663
|
+
if (!pointer) {
|
|
664
|
+
const definition = context.jsonDoc.json.meshes[index];
|
|
665
|
+
const res = new MeshIndexResolver(this.document, object, definition, prop);
|
|
666
|
+
res.debug = this.debug;
|
|
667
|
+
res.init(context);
|
|
668
|
+
pointer = new JsonPointer("/meshes/", res, value);
|
|
669
|
+
pointer.debug = this.debug;
|
|
670
|
+
this.map.set(value, pointer);
|
|
671
|
+
prop.setReference(value, object);
|
|
672
|
+
}
|
|
673
|
+
pointer?.register(obj, key, fullPath);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
break;
|
|
678
|
+
case 'number':
|
|
679
|
+
if (key === "node" || key === "mesh"
|
|
680
|
+
// VRM 1.0 spring bone center https://github.com/vrm-c/vrm-specification/blob/master/specification/VRMC_springBone-1.0/schema/VRMC_springBone.spring.schema.json
|
|
681
|
+
|| fullPath.match(/springs\/\d+\/center/g)) {
|
|
682
|
+
const object = context.nodes[value];
|
|
683
|
+
if (object) {
|
|
684
|
+
const res = new NodeIndexResolver(this.document, object, context.jsonDoc.json.nodes[value]);
|
|
685
|
+
res.debug = this.debug;
|
|
686
|
+
res.init(context);
|
|
687
|
+
const dictKey = `/${key}/${value}`;
|
|
688
|
+
let pointer = this.map.get(dictKey);
|
|
689
|
+
if (!pointer) {
|
|
690
|
+
pointer = new JsonPointer(null, res, value);
|
|
691
|
+
}
|
|
692
|
+
pointer.debug = this.debug;
|
|
693
|
+
pointer.register(obj, key, fullPath);
|
|
694
|
+
this.map.set(dictKey, pointer);
|
|
695
|
+
prop.setReference(dictKey, object);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
else if (
|
|
699
|
+
// key texture is matching for example for VRM meta/texture (AvatarSample_A)
|
|
700
|
+
key === "texture"
|
|
701
|
+
// VRM 0.0 material properties
|
|
702
|
+
|| fullPath.match(/materialProperties\/(?<material_index>\d+)\/textureProperties/g)
|
|
703
|
+
// VRM 1.0 meta
|
|
704
|
+
|| fullPath.match(/meta\/thumbnailImage/g)
|
|
705
|
+
// NEEDLE_materials_mtlx where the root extension has { textures: [0, 1, 2, ...] }
|
|
706
|
+
|| fullPath.match(/^\/textures\/\d{1,}$/)) {
|
|
707
|
+
const index = value;
|
|
708
|
+
// e.g. in meta.texture for VRM avatars it seems like we can have negative values
|
|
709
|
+
if (index < 0) {
|
|
710
|
+
console.warn(`WARN: texture index is negative ${index} at ${fullPath}`);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const definition = context.jsonDoc.json.textures[index];
|
|
714
|
+
if (!definition) {
|
|
715
|
+
console.warn(`WARN: texture definition with index ${index} not found or does not exist. Set in \"${fullPath}\"`);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
const imageIndex = findSource(definition);
|
|
719
|
+
if (this.debug)
|
|
720
|
+
console.log("Texture index", index, imageIndex, context.jsonDoc.json.textures.length, "Textures:" + context.textures.length);
|
|
721
|
+
const object = tryGetInArray(context.textures, imageIndex);
|
|
722
|
+
if (object) {
|
|
723
|
+
if (this.debug)
|
|
724
|
+
console.log("Found texture index at " + fullPath + ": " + value);
|
|
725
|
+
const res = new TextureIndexResolver(this.document, object, definition);
|
|
726
|
+
res.debug = this.debug;
|
|
727
|
+
res.init(context);
|
|
728
|
+
const dictKey = `/textures/${value}`;
|
|
729
|
+
let pointer = this.map.get(dictKey);
|
|
730
|
+
if (!pointer) {
|
|
731
|
+
pointer = new JsonPointer(null, res, value);
|
|
732
|
+
}
|
|
733
|
+
pointer.debug = this.debug;
|
|
734
|
+
pointer.register(obj, key, fullPath);
|
|
735
|
+
this.map.set(dictKey, pointer);
|
|
736
|
+
prop.setReference(dictKey, object, { channels: TextureChannel.R | TextureChannel.G | TextureChannel.B | TextureChannel.A });
|
|
737
|
+
}
|
|
738
|
+
const materialMatch = this.VRM_0_MaterialIndexRegex.exec(fullPath);
|
|
739
|
+
if (materialMatch) {
|
|
740
|
+
const materialIndex = parseInt(materialMatch.groups.material_index);
|
|
741
|
+
const material = context.materials[materialIndex];
|
|
742
|
+
prop.setReference(`/materials/${materialIndex}`, material);
|
|
743
|
+
// const res = new MaterialIndexResolver(this.document, material, context.jsonDoc.json.materials![materialIndex]);
|
|
744
|
+
// res.debug = this.debug;
|
|
745
|
+
// res.init(context);
|
|
746
|
+
// const dictKey = `/materials/${materialIndex}`
|
|
747
|
+
// let pointer = this.map.get(dictKey);
|
|
748
|
+
// if (!pointer) {
|
|
749
|
+
// pointer = new JsonPointer(null, res, value);
|
|
750
|
+
// }
|
|
751
|
+
// pointer.debug = this.debug;
|
|
752
|
+
// pointer.register(obj, key, fullPath);
|
|
753
|
+
// this.map.set(dictKey, pointer);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
break;
|
|
757
|
+
default:
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
resolveAndWrite(context, step) {
|
|
762
|
+
this.map?.forEach(ptr => {
|
|
763
|
+
if (ptr.resolve(context, step))
|
|
764
|
+
ptr.write();
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
class JsonPointer {
|
|
769
|
+
debug = false;
|
|
770
|
+
resolver;
|
|
771
|
+
pointerBase;
|
|
772
|
+
oldPointer;
|
|
773
|
+
constructor(pointerBase, resolver, oldPointer) {
|
|
774
|
+
this.pointerBase = pointerBase;
|
|
775
|
+
this.resolver = resolver;
|
|
776
|
+
this.oldPointer = oldPointer;
|
|
777
|
+
}
|
|
778
|
+
_objects = [];
|
|
779
|
+
_props = [];
|
|
780
|
+
_debugInfo = [];
|
|
781
|
+
register(obj, prop, fullpath) {
|
|
782
|
+
this._objects.push(obj);
|
|
783
|
+
this._props.push(prop);
|
|
784
|
+
this._debugInfo.push(fullpath);
|
|
785
|
+
}
|
|
786
|
+
newPointer = undefined;
|
|
787
|
+
resolve(context, step) {
|
|
788
|
+
// If the pointer was already resolved, we don't need to do it again
|
|
789
|
+
if (this.newPointer !== undefined) {
|
|
790
|
+
return true;
|
|
791
|
+
}
|
|
792
|
+
//@ts-ignore
|
|
793
|
+
this.resolver.debug = this.debug;
|
|
794
|
+
const index = this.resolver.resolve(context);
|
|
795
|
+
if (index != undefined) {
|
|
796
|
+
if (this.pointerBase != null) {
|
|
797
|
+
this.newPointer = this.pointerBase + index;
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
this.newPointer = index;
|
|
801
|
+
}
|
|
802
|
+
return true;
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
// If the pointer could not be resolved during write we need to log an error
|
|
806
|
+
if (step === "write") {
|
|
807
|
+
console.log(`ERR: 🧨 FAILED to resolve pointer \"${this.oldPointer}\" used by ${this._debugInfo.join(", ")}`);
|
|
808
|
+
if (this.debug)
|
|
809
|
+
debugger;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
_didWrite = false;
|
|
815
|
+
write() {
|
|
816
|
+
if (this._didWrite)
|
|
817
|
+
return;
|
|
818
|
+
this._didWrite = false;
|
|
819
|
+
for (let i = 0; i < this._objects.length; i++) {
|
|
820
|
+
const obj = this._objects[i];
|
|
821
|
+
const prop = this._props[i];
|
|
822
|
+
if (this.debug) {
|
|
823
|
+
console.log(`< Resolved pointer ${this._debugInfo[i]}: ${this.oldPointer} → ${this.newPointer}`);
|
|
824
|
+
}
|
|
825
|
+
obj[prop] = this.newPointer;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
class PointerResolver {
|
|
830
|
+
document;
|
|
831
|
+
/* the object originally being referenced by the pointer */
|
|
832
|
+
inputObject;
|
|
833
|
+
/* the original json definition */
|
|
834
|
+
inputDefinition;
|
|
835
|
+
/** The graph property this pointer belongs to */
|
|
836
|
+
property;
|
|
837
|
+
constructor(doc, obj, def, prop) {
|
|
838
|
+
this.document = doc;
|
|
839
|
+
this.inputObject = obj;
|
|
840
|
+
this.inputDefinition = def;
|
|
841
|
+
this.property = prop;
|
|
842
|
+
}
|
|
843
|
+
debug = false;
|
|
844
|
+
}
|
|
845
|
+
class TextureIndexResolver extends PointerResolver {
|
|
846
|
+
_debugName;
|
|
847
|
+
_samplerDef;
|
|
848
|
+
init(context) {
|
|
849
|
+
this._debugName = this.inputDefinition.name ?? this.inputDefinition.source?.toString() ?? "unknown";
|
|
850
|
+
const samplerIndex = this.inputDefinition.sampler;
|
|
851
|
+
if (samplerIndex !== undefined) {
|
|
852
|
+
this._samplerDef = context.jsonDoc.json.samplers[samplerIndex];
|
|
853
|
+
}
|
|
854
|
+
if (this.debug)
|
|
855
|
+
console.log(this._debugName, this._samplerDef);
|
|
856
|
+
}
|
|
857
|
+
resolve(context) {
|
|
858
|
+
const imgIndex = context.imageIndexMap.get(this.inputObject);
|
|
859
|
+
if (imgIndex === undefined) {
|
|
860
|
+
console.error("ERR: image is missing", imgIndex);
|
|
861
|
+
return -1;
|
|
862
|
+
}
|
|
863
|
+
// ensure we have a texture array
|
|
864
|
+
context.jsonDoc.json.textures = context.jsonDoc.json.textures || [];
|
|
865
|
+
// try find the texture that points to the same image source
|
|
866
|
+
for (let i = 0; i < context.jsonDoc.json.textures.length; i++) {
|
|
867
|
+
const textureDef = context.jsonDoc.json.textures[i];
|
|
868
|
+
if (findSource(textureDef) === imgIndex) {
|
|
869
|
+
if (textureDef.sampler === undefined) {
|
|
870
|
+
textureDef.sampler = this.findOrCreateSampler(context.jsonDoc.json);
|
|
871
|
+
}
|
|
872
|
+
if (this.debug)
|
|
873
|
+
console.log("Found texture def at " + i, "using image", imgIndex, textureDef);
|
|
874
|
+
return i;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
// the texture was not found - add it
|
|
878
|
+
// create a new texture def entry
|
|
879
|
+
if (this.debug)
|
|
880
|
+
console.log("Could not find texture, creating a new one: " + this._debugName);
|
|
881
|
+
const hasExtensionWithSource = this.inputDefinition.extensions && Object.keys(this.inputDefinition.extensions).some(key => this.inputDefinition.extensions[key]["source"] !== undefined);
|
|
882
|
+
const textureDef = {
|
|
883
|
+
source: hasExtensionWithSource ? undefined : imgIndex,
|
|
884
|
+
sampler: this.findOrCreateSampler(context.jsonDoc.json),
|
|
885
|
+
extensions: this.inputDefinition.extensions,
|
|
886
|
+
extras: this.inputDefinition.extras,
|
|
887
|
+
};
|
|
888
|
+
context.jsonDoc.json.textures.push(textureDef);
|
|
889
|
+
return context.jsonDoc.json.textures.length - 1;
|
|
890
|
+
}
|
|
891
|
+
findOrCreateSampler(json) {
|
|
892
|
+
json.samplers = json.samplers || [];
|
|
893
|
+
if (this._samplerDef === undefined) {
|
|
894
|
+
console.log("WARN: Creating default sampler", this._debugName);
|
|
895
|
+
// create a default sampler
|
|
896
|
+
this._samplerDef = {
|
|
897
|
+
magFilter: 9729,
|
|
898
|
+
minFilter: 9729,
|
|
899
|
+
wrapS: 33648,
|
|
900
|
+
wrapT: 33648
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
for (let i = 0; i < json.samplers.length; i++) {
|
|
905
|
+
const sampler = json.samplers[i];
|
|
906
|
+
if (sampler.minFilter === this._samplerDef.minFilter &&
|
|
907
|
+
sampler.magFilter === this._samplerDef.magFilter &&
|
|
908
|
+
sampler.wrapS === this._samplerDef.wrapS &&
|
|
909
|
+
sampler.wrapT === this._samplerDef.wrapT) {
|
|
910
|
+
return i;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
// append previous sampler
|
|
915
|
+
json.samplers.push(this._samplerDef);
|
|
916
|
+
return json.samplers.length - 1;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
function findSource(texDef) {
|
|
920
|
+
if (texDef.source !== undefined) {
|
|
921
|
+
return texDef.source;
|
|
922
|
+
}
|
|
923
|
+
if (texDef.extensions) {
|
|
924
|
+
const keys = Object.keys(texDef.extensions);
|
|
925
|
+
for (let i = 0; i < keys.length; i++) {
|
|
926
|
+
const key = keys[i];
|
|
927
|
+
const ext = texDef.extensions[key];
|
|
928
|
+
if (ext["source"] !== undefined) {
|
|
929
|
+
return ext["source"];
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
return undefined;
|
|
934
|
+
}
|
|
935
|
+
class MaterialIndexResolver extends PointerResolver {
|
|
936
|
+
init(context) {
|
|
937
|
+
}
|
|
938
|
+
resolve(context) {
|
|
939
|
+
return context.materialIndexMap.get(this.inputObject);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
class NodeIndexResolver extends PointerResolver {
|
|
943
|
+
init(context) {
|
|
944
|
+
}
|
|
945
|
+
resolve(context) {
|
|
946
|
+
return context.nodeIndexMap.get(this.inputObject);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
class MeshIndexResolver extends PointerResolver {
|
|
950
|
+
init(context) {
|
|
951
|
+
}
|
|
952
|
+
resolve(context) {
|
|
953
|
+
let index = context.meshIndexMap.get(this.inputObject);
|
|
954
|
+
if (index >= 0) {
|
|
955
|
+
return index;
|
|
956
|
+
}
|
|
957
|
+
let obj = null;
|
|
958
|
+
if (this.inputObject.isDisposed()) {
|
|
959
|
+
// If the mesh was deduplicated we try to get the mesh from a parent node
|
|
960
|
+
// This is a HACK, we need to get the reference to the mesh this one was deduplicated with since the parent node potentially has many children
|
|
961
|
+
const parents = this.property?.listParents();
|
|
962
|
+
for (const parent of parents) {
|
|
963
|
+
if (parent instanceof Node) {
|
|
964
|
+
obj = parent.getMesh();
|
|
965
|
+
if (obj) {
|
|
966
|
+
this.document.getLogger().info(`INFO: resolved mesh \"${this.inputObject.getName()}\" from parent node \"${parent.getName()}\"`);
|
|
967
|
+
break;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
if (obj.isDisposed()) {
|
|
972
|
+
console.warn("WARN: mesh is disposed", this.inputObject.getName());
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
if (!obj) {
|
|
976
|
+
return undefined;
|
|
977
|
+
}
|
|
978
|
+
return context.meshIndexMap.get(obj);
|
|
979
|
+
}
|
|
980
|
+
}
|