@galfus/gltf-loader 0.0.0-stage → 0.31.1-alpha
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/README.md +93 -2
- package/package.json +43 -4
- package/src/animation.ts +160 -0
- package/src/bounds.ts +160 -0
- package/src/decoders/resolver.ts +131 -0
- package/src/factory.ts +303 -0
- package/src/index.ts +185 -0
- package/src/instance.ts +126 -0
- package/src/parser.ts +334 -0
- package/src/resources.ts +391 -0
- package/src/scene.ts +354 -0
- package/src/types.ts +270 -0
- package/src/zip.ts +173 -0
package/src/resources.ts
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared GPU resource extraction and uploading for glTF geometries, PBR materials, and textures.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Realm3D } from '@galfus/engine';
|
|
6
|
+
import type { ResolvedDecoders } from './decoders/resolver';
|
|
7
|
+
import { decodeDataUri, readAccessor } from './parser';
|
|
8
|
+
import type { GlTF, GltfMaterialAlphaModeResolver, GlTFPrimitive } from './types';
|
|
9
|
+
|
|
10
|
+
export interface UploadedGltfResources {
|
|
11
|
+
geometryMap: Map<string, number>; // key: `${meshIndex}_${primitiveIndex}` -> geometryId
|
|
12
|
+
materialMap: Map<number, number>; // gltfMaterialIndex -> realmMaterialId
|
|
13
|
+
textureMap: Map<number, number>; // gltfTextureIndex -> realmTextureId
|
|
14
|
+
createdGeometryIds: number[];
|
|
15
|
+
createdMaterialIds: number[];
|
|
16
|
+
createdTextureIds: number[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extracts raw image bytes from a glTF image descriptor.
|
|
21
|
+
*/
|
|
22
|
+
async function getImageBytes(
|
|
23
|
+
gltf: GlTF,
|
|
24
|
+
buffers: Uint8Array[],
|
|
25
|
+
imageIndex: number,
|
|
26
|
+
externalResolver?: (uri: string) => Promise<Uint8Array>
|
|
27
|
+
): Promise<Uint8Array | undefined> {
|
|
28
|
+
const image = gltf.images?.[imageIndex];
|
|
29
|
+
if (!image) return undefined;
|
|
30
|
+
|
|
31
|
+
if (image.bufferView !== undefined) {
|
|
32
|
+
const bv = gltf.bufferViews?.[image.bufferView];
|
|
33
|
+
if (bv) {
|
|
34
|
+
const buffer = buffers[bv.buffer];
|
|
35
|
+
if (buffer) {
|
|
36
|
+
const start = bv.byteOffset ?? 0;
|
|
37
|
+
return buffer.subarray(start, start + bv.byteLength);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (image.uri && image.uri.startsWith('data:')) {
|
|
43
|
+
return decodeDataUri(image.uri);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (image.uri && externalResolver) {
|
|
47
|
+
return await externalResolver(image.uri);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (image.uri) throw new Error(`Cannot resolve external glTF image: ${image.uri}`);
|
|
51
|
+
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Maps glTF 2.0 primitive rendering mode to the Galfus Engine PrimitiveTopology3D enum.
|
|
57
|
+
*
|
|
58
|
+
* glTF specification:
|
|
59
|
+
* - 0: POINTS -> 0 (Points)
|
|
60
|
+
* - 1: LINES -> 1 (Lines)
|
|
61
|
+
* - 2: LINE_LOOP -> 2 (LineStrip)
|
|
62
|
+
* - 3: LINE_STRIP -> 2 (LineStrip)
|
|
63
|
+
* - 4: TRIANGLES -> 3 (Triangles / TriangleList)
|
|
64
|
+
* - 5: TRIANGLE_STRIP -> 4 (TriangleStrip)
|
|
65
|
+
*/
|
|
66
|
+
function mapGltfPrimitiveTopology(mode?: number): number {
|
|
67
|
+
switch (mode) {
|
|
68
|
+
case 0:
|
|
69
|
+
return 0; // Points
|
|
70
|
+
case 1:
|
|
71
|
+
return 1; // Lines
|
|
72
|
+
case 2:
|
|
73
|
+
case 3:
|
|
74
|
+
return 2; // LineStrip
|
|
75
|
+
case 5:
|
|
76
|
+
return 4; // TriangleStrip
|
|
77
|
+
case 4:
|
|
78
|
+
default:
|
|
79
|
+
return 3; // Triangles
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Uploads all shared geometries, textures, and PBR materials to the GPU for a glTF asset.
|
|
85
|
+
*
|
|
86
|
+
* @param realm The destination Realm3D.
|
|
87
|
+
* @param gltf The root glTF descriptor.
|
|
88
|
+
* @param buffers Array of resolved binary buffers.
|
|
89
|
+
* @param decoders Object holding optional active decoders (Draco, Meshopt).
|
|
90
|
+
* @param externalResolver Optional resolver callback for loading relative external assets.
|
|
91
|
+
* @returns Map of created GPU resource handles and ID registries.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```typescript
|
|
95
|
+
* const resources = await uploadGltfResources(realm, gltf, buffers, decoders);
|
|
96
|
+
* console.log(`Uploaded ${resources.createdGeometryIds.length} geometries to VRAM`);
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export async function uploadGltfResources(
|
|
100
|
+
realm: Realm3D,
|
|
101
|
+
gltf: GlTF,
|
|
102
|
+
buffers: Uint8Array[],
|
|
103
|
+
decoders: ResolvedDecoders,
|
|
104
|
+
externalResolver?: (uri: string) => Promise<Uint8Array>,
|
|
105
|
+
resolveMaterialAlphaMode?: GltfMaterialAlphaModeResolver
|
|
106
|
+
): Promise<UploadedGltfResources> {
|
|
107
|
+
const textureMap = new Map<number, number>();
|
|
108
|
+
const materialMap = new Map<number, number>();
|
|
109
|
+
const geometryMap = new Map<string, number>();
|
|
110
|
+
const textureBytes = new Map<number, Uint8Array>();
|
|
111
|
+
|
|
112
|
+
const createdTextureIds: number[] = [];
|
|
113
|
+
const createdMaterialIds: number[] = [];
|
|
114
|
+
const createdGeometryIds: number[] = [];
|
|
115
|
+
|
|
116
|
+
// Determine which textures are used as sRGB color maps (baseColor / emissive / diffuse)
|
|
117
|
+
const srgbTextureIndices = new Set<number>();
|
|
118
|
+
if (gltf.materials) {
|
|
119
|
+
for (const mat of gltf.materials) {
|
|
120
|
+
if (mat.pbrMetallicRoughness?.baseColorTexture) {
|
|
121
|
+
srgbTextureIndices.add(mat.pbrMetallicRoughness.baseColorTexture.index);
|
|
122
|
+
}
|
|
123
|
+
const specGloss = mat.extensions?.KHR_materials_pbrSpecularGlossiness;
|
|
124
|
+
if (specGloss?.diffuseTexture) {
|
|
125
|
+
srgbTextureIndices.add(specGloss.diffuseTexture.index);
|
|
126
|
+
}
|
|
127
|
+
if (mat.emissiveTexture) {
|
|
128
|
+
srgbTextureIndices.add(mat.emissiveTexture.index);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 1. Upload Textures
|
|
134
|
+
if (gltf.textures) {
|
|
135
|
+
for (let tIdx = 0; tIdx < gltf.textures.length; tIdx++) {
|
|
136
|
+
const tex = gltf.textures[tIdx]!;
|
|
137
|
+
const sourceIdx = tex.source ?? tex.extensions?.KHR_texture_basisu?.source;
|
|
138
|
+
if (sourceIdx !== undefined) {
|
|
139
|
+
const imageBytes = await getImageBytes(gltf, buffers, sourceIdx, externalResolver);
|
|
140
|
+
if (imageBytes && imageBytes.byteLength > 0) {
|
|
141
|
+
if (resolveMaterialAlphaMode) textureBytes.set(tIdx, imageBytes);
|
|
142
|
+
const isSrgb = srgbTextureIndices.has(tIdx);
|
|
143
|
+
const textureId = realm.resources.loadTexture(imageBytes, {
|
|
144
|
+
srgb: isSrgb,
|
|
145
|
+
label: tex.name ?? `glTF_Texture_${tIdx}`
|
|
146
|
+
});
|
|
147
|
+
textureMap.set(tIdx, textureId);
|
|
148
|
+
createdTextureIds.push(textureId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 2. Upload Materials
|
|
155
|
+
if (gltf.materials) {
|
|
156
|
+
for (let mIdx = 0; mIdx < gltf.materials.length; mIdx++) {
|
|
157
|
+
const mat = gltf.materials[mIdx]!;
|
|
158
|
+
const pbr = mat.pbrMetallicRoughness;
|
|
159
|
+
const specGloss = mat.extensions?.KHR_materials_pbrSpecularGlossiness;
|
|
160
|
+
|
|
161
|
+
const linearColor = pbr?.baseColorFactor ?? specGloss?.diffuseFactor ?? [1, 1, 1, 1];
|
|
162
|
+
const linearToSrgb = (value: number): number =>
|
|
163
|
+
value <= 0.0031308 ? value * 12.92 : 1.055 * value ** (1 / 2.4) - 0.055;
|
|
164
|
+
const baseColor: [number, number, number, number] = [
|
|
165
|
+
linearToSrgb(linearColor[0]),
|
|
166
|
+
linearToSrgb(linearColor[1]),
|
|
167
|
+
linearToSrgb(linearColor[2]),
|
|
168
|
+
linearColor[3]
|
|
169
|
+
];
|
|
170
|
+
const metallic = pbr?.metallicFactor ?? (specGloss ? 0 : 1);
|
|
171
|
+
const roughness =
|
|
172
|
+
pbr?.roughnessFactor ?? (specGloss ? 1.0 - (specGloss.glossinessFactor ?? 1.0) : 1.0);
|
|
173
|
+
const baseColorTextureId =
|
|
174
|
+
pbr?.baseColorTexture !== undefined
|
|
175
|
+
? textureMap.get(pbr.baseColorTexture.index)
|
|
176
|
+
: specGloss?.diffuseTexture !== undefined
|
|
177
|
+
? textureMap.get(specGloss.diffuseTexture.index)
|
|
178
|
+
: undefined;
|
|
179
|
+
const normalTextureId =
|
|
180
|
+
mat.normalTexture !== undefined ? textureMap.get(mat.normalTexture.index) : undefined;
|
|
181
|
+
const metallicRoughnessTextureId =
|
|
182
|
+
pbr?.metallicRoughnessTexture !== undefined
|
|
183
|
+
? textureMap.get(pbr.metallicRoughnessTexture.index)
|
|
184
|
+
: undefined;
|
|
185
|
+
const emissiveTextureId =
|
|
186
|
+
mat.emissiveTexture !== undefined ? textureMap.get(mat.emissiveTexture.index) : undefined;
|
|
187
|
+
const occlusionTextureId =
|
|
188
|
+
mat.occlusionTexture !== undefined ? textureMap.get(mat.occlusionTexture.index) : undefined;
|
|
189
|
+
|
|
190
|
+
// The Galfus 3D shader computes emissive as (material.emissive_color.rgb + tex_emissive.rgb).
|
|
191
|
+
// When an emissive texture is present, avoid adding constant white factor which would blow out the surface.
|
|
192
|
+
const emissiveColor: [number, number, number, number] =
|
|
193
|
+
mat.emissiveFactor && emissiveTextureId === undefined
|
|
194
|
+
? [mat.emissiveFactor[0], mat.emissiveFactor[1], mat.emissiveFactor[2], 1]
|
|
195
|
+
: [0, 0, 0, 1];
|
|
196
|
+
|
|
197
|
+
const baseTextureIndex = pbr?.baseColorTexture?.index ?? specGloss?.diffuseTexture?.index;
|
|
198
|
+
const alphaMode =
|
|
199
|
+
resolveMaterialAlphaMode?.(
|
|
200
|
+
mat,
|
|
201
|
+
mIdx,
|
|
202
|
+
baseTextureIndex === undefined ? undefined : textureBytes.get(baseTextureIndex)
|
|
203
|
+
) ?? mat.alphaMode;
|
|
204
|
+
const surfaceType =
|
|
205
|
+
alphaMode === 'BLEND' ? 'transparent' : alphaMode === 'MASK' ? 'masked' : 'opaque';
|
|
206
|
+
|
|
207
|
+
const alphaCutoff = mat.alphaCutoff;
|
|
208
|
+
const ior = mat.extensions?.KHR_materials_ior?.ior ?? 1.5;
|
|
209
|
+
const reflectance = ior === 0 ? 2.5 : (2.5 * (ior - 1)) / (ior + 1);
|
|
210
|
+
|
|
211
|
+
const { materialId } = realm.createMaterial({
|
|
212
|
+
baseColor,
|
|
213
|
+
metallic,
|
|
214
|
+
roughness,
|
|
215
|
+
refractionIndex: ior,
|
|
216
|
+
reflectance,
|
|
217
|
+
emissiveColor,
|
|
218
|
+
albedoTextureId: baseColorTextureId,
|
|
219
|
+
normalTextureId,
|
|
220
|
+
normalScale: mat.normalTexture?.scale,
|
|
221
|
+
metallicRoughnessTextureId,
|
|
222
|
+
emissiveTextureId,
|
|
223
|
+
occlusionTextureId,
|
|
224
|
+
occlusionStrength: mat.occlusionTexture?.strength,
|
|
225
|
+
textureUvSets: [
|
|
226
|
+
pbr?.baseColorTexture ?? specGloss?.diffuseTexture,
|
|
227
|
+
mat.normalTexture,
|
|
228
|
+
pbr?.metallicRoughnessTexture,
|
|
229
|
+
mat.occlusionTexture,
|
|
230
|
+
mat.emissiveTexture
|
|
231
|
+
].map((info) => (info?.texCoord === 1 ? 'uv1' : 'uv0')),
|
|
232
|
+
side: mat.doubleSided ? 'double' : 'front',
|
|
233
|
+
surfaceType,
|
|
234
|
+
alphaCutoff
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
materialMap.set(mIdx, materialId);
|
|
238
|
+
createdMaterialIds.push(materialId);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 3. Upload Geometries
|
|
243
|
+
if (gltf.meshes) {
|
|
244
|
+
for (let meshIdx = 0; meshIdx < gltf.meshes.length; meshIdx++) {
|
|
245
|
+
const mesh = gltf.meshes[meshIdx]!;
|
|
246
|
+
|
|
247
|
+
for (let primIdx = 0; primIdx < mesh.primitives.length; primIdx++) {
|
|
248
|
+
const prim: GlTFPrimitive = mesh.primitives[primIdx]!;
|
|
249
|
+
let positions: Float32Array;
|
|
250
|
+
let normals: Float32Array | undefined;
|
|
251
|
+
let uvs: Float32Array | undefined;
|
|
252
|
+
let uv1: Float32Array | undefined;
|
|
253
|
+
let tangents: Float32Array | undefined;
|
|
254
|
+
let colors: Float32Array | undefined;
|
|
255
|
+
let joints0: Uint32Array | Uint16Array | undefined;
|
|
256
|
+
let weights0: Float32Array | undefined;
|
|
257
|
+
let joints1: Uint32Array | Uint16Array | undefined;
|
|
258
|
+
let weights1: Float32Array | undefined;
|
|
259
|
+
let indices: Uint32Array | Uint16Array | undefined;
|
|
260
|
+
|
|
261
|
+
// Check for Draco mesh compression extension
|
|
262
|
+
if (prim.extensions?.KHR_draco_mesh_compression && decoders.draco) {
|
|
263
|
+
const dracoExt = prim.extensions.KHR_draco_mesh_compression;
|
|
264
|
+
const bv = gltf.bufferViews?.[dracoExt.bufferView];
|
|
265
|
+
if (bv) {
|
|
266
|
+
const buffer = buffers[bv.buffer];
|
|
267
|
+
if (buffer) {
|
|
268
|
+
const start = bv.byteOffset ?? 0;
|
|
269
|
+
const dracoBytes = buffer.subarray(start, start + bv.byteLength);
|
|
270
|
+
const decoded = await decoders.draco.decode(dracoBytes, dracoExt.attributes);
|
|
271
|
+
positions = decoded.position ?? new Float32Array(0);
|
|
272
|
+
normals = decoded.normal;
|
|
273
|
+
uvs = decoded.uv;
|
|
274
|
+
uv1 = decoded.uv1;
|
|
275
|
+
tangents = decoded.tangent;
|
|
276
|
+
colors = decoded.color;
|
|
277
|
+
joints0 = decoded.joints;
|
|
278
|
+
weights0 = decoded.weights;
|
|
279
|
+
indices = decoded.indices;
|
|
280
|
+
} else {
|
|
281
|
+
positions = new Float32Array(0);
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
positions = new Float32Array(0);
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
// Standard uncompressed accessors
|
|
288
|
+
const posAcc = prim.attributes['POSITION'];
|
|
289
|
+
if (posAcc === undefined) continue;
|
|
290
|
+
positions = readAccessor(gltf, buffers, posAcc) as Float32Array;
|
|
291
|
+
|
|
292
|
+
if (prim.attributes['NORMAL'] !== undefined) {
|
|
293
|
+
normals = readAccessor(gltf, buffers, prim.attributes['NORMAL']) as Float32Array;
|
|
294
|
+
}
|
|
295
|
+
if (prim.attributes['TEXCOORD_0'] !== undefined) {
|
|
296
|
+
uvs = readAccessor(gltf, buffers, prim.attributes['TEXCOORD_0']) as Float32Array;
|
|
297
|
+
}
|
|
298
|
+
if (prim.attributes['TEXCOORD_1'] !== undefined) {
|
|
299
|
+
uv1 = readAccessor(gltf, buffers, prim.attributes['TEXCOORD_1']) as Float32Array;
|
|
300
|
+
}
|
|
301
|
+
if (prim.attributes['TANGENT'] !== undefined) {
|
|
302
|
+
tangents = readAccessor(gltf, buffers, prim.attributes['TANGENT']) as Float32Array;
|
|
303
|
+
}
|
|
304
|
+
if (prim.attributes['COLOR_0'] !== undefined) {
|
|
305
|
+
colors = readAccessor(gltf, buffers, prim.attributes['COLOR_0']) as Float32Array;
|
|
306
|
+
if (gltf.accessors?.[prim.attributes['COLOR_0']]?.type === 'VEC3') {
|
|
307
|
+
const rgba = new Float32Array((colors.length / 3) * 4);
|
|
308
|
+
for (let i = 0; i < colors.length / 3; i++) {
|
|
309
|
+
rgba.set(colors.subarray(i * 3, i * 3 + 3), i * 4);
|
|
310
|
+
rgba[i * 4 + 3] = 1;
|
|
311
|
+
}
|
|
312
|
+
colors = rgba;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (prim.attributes['JOINTS_0'] !== undefined) {
|
|
316
|
+
joints0 = readAccessor(gltf, buffers, prim.attributes['JOINTS_0']) as
|
|
317
|
+
Uint32Array | Uint16Array;
|
|
318
|
+
}
|
|
319
|
+
if (prim.attributes['WEIGHTS_0'] !== undefined) {
|
|
320
|
+
weights0 = readAccessor(gltf, buffers, prim.attributes['WEIGHTS_0']) as Float32Array;
|
|
321
|
+
}
|
|
322
|
+
if (prim.attributes['JOINTS_1'] !== undefined) {
|
|
323
|
+
joints1 = readAccessor(gltf, buffers, prim.attributes['JOINTS_1']) as
|
|
324
|
+
Uint32Array | Uint16Array;
|
|
325
|
+
}
|
|
326
|
+
if (prim.attributes['WEIGHTS_1'] !== undefined) {
|
|
327
|
+
weights1 = readAccessor(gltf, buffers, prim.attributes['WEIGHTS_1']) as Float32Array;
|
|
328
|
+
}
|
|
329
|
+
if (prim.indices !== undefined) {
|
|
330
|
+
indices = readAccessor(gltf, buffers, prim.indices) as Uint32Array | Uint16Array;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (prim.mode === 2 || prim.mode === 6) {
|
|
335
|
+
const src = indices ?? Uint32Array.from({ length: positions.length / 3 }, (_, i) => i);
|
|
336
|
+
if (prim.mode === 2) indices = Uint32Array.from([...src, src[0]!]);
|
|
337
|
+
else {
|
|
338
|
+
const triangles: number[] = [];
|
|
339
|
+
for (let i = 1; i + 1 < src.length; i++) triangles.push(src[0]!, src[i]!, src[i + 1]!);
|
|
340
|
+
indices = new Uint32Array(triangles);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const { geometryId } = realm.createGeometry({
|
|
344
|
+
positions,
|
|
345
|
+
normals,
|
|
346
|
+
uvs,
|
|
347
|
+
uv1,
|
|
348
|
+
tangents,
|
|
349
|
+
colors,
|
|
350
|
+
joints0,
|
|
351
|
+
weights0,
|
|
352
|
+
joints1,
|
|
353
|
+
weights1,
|
|
354
|
+
indices,
|
|
355
|
+
topology: mapGltfPrimitiveTopology(prim.mode)
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
if (prim.targets?.length) {
|
|
359
|
+
const targets = prim.targets.map((target) => ({
|
|
360
|
+
positions:
|
|
361
|
+
target['POSITION'] === undefined
|
|
362
|
+
? undefined
|
|
363
|
+
: (readAccessor(gltf, buffers, target['POSITION']) as Float32Array),
|
|
364
|
+
normals:
|
|
365
|
+
target['NORMAL'] === undefined
|
|
366
|
+
? undefined
|
|
367
|
+
: (readAccessor(gltf, buffers, target['NORMAL']) as Float32Array),
|
|
368
|
+
tangents:
|
|
369
|
+
target['TANGENT'] === undefined
|
|
370
|
+
? undefined
|
|
371
|
+
: (readAccessor(gltf, buffers, target['TANGENT']) as Float32Array)
|
|
372
|
+
}));
|
|
373
|
+
realm.setGeometryMorphTargets(geometryId, targets, positions.length / 3);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const key = `${meshIdx}_${primIdx}`;
|
|
377
|
+
geometryMap.set(key, geometryId);
|
|
378
|
+
createdGeometryIds.push(geometryId);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
geometryMap,
|
|
385
|
+
materialMap,
|
|
386
|
+
textureMap,
|
|
387
|
+
createdGeometryIds,
|
|
388
|
+
createdMaterialIds,
|
|
389
|
+
createdTextureIds
|
|
390
|
+
};
|
|
391
|
+
}
|