@myned-ai/gsplat-flame-avatar-renderer 1.0.4 → 1.0.6
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 +128 -40
- package/dist/gsplat-flame-avatar-renderer.cjs.js +3478 -722
- package/dist/gsplat-flame-avatar-renderer.cjs.min.js +2 -0
- package/dist/gsplat-flame-avatar-renderer.cjs.min.js.map +1 -0
- package/dist/gsplat-flame-avatar-renderer.esm.js +3439 -724
- package/dist/gsplat-flame-avatar-renderer.esm.min.js +2 -0
- package/dist/gsplat-flame-avatar-renderer.esm.min.js.map +1 -0
- package/package.json +11 -14
- package/src/core/SplatMesh.js +53 -46
- package/src/core/Viewer.js +47 -196
- package/src/errors/ApplicationError.js +185 -0
- package/src/errors/index.js +17 -0
- package/src/flame/FlameAnimator.js +282 -57
- package/src/loaders/PlyLoader.js +302 -44
- package/src/materials/SplatMaterial.js +13 -10
- package/src/materials/SplatMaterial3D.js +72 -27
- package/src/renderer/AnimationManager.js +8 -5
- package/src/renderer/GaussianSplatRenderer.js +668 -217
- package/src/utils/BlobUrlManager.js +294 -0
- package/src/utils/EventEmitter.js +349 -0
- package/src/utils/LoaderUtils.js +2 -1
- package/src/utils/Logger.js +171 -0
- package/src/utils/ObjectPool.js +248 -0
- package/src/utils/RenderLoop.js +306 -0
- package/src/utils/Util.js +59 -18
- package/src/utils/ValidationUtils.js +331 -0
- package/src/utils/index.js +10 -1
- package/dist/gsplat-flame-avatar-renderer.cjs.js.map +0 -1
- package/dist/gsplat-flame-avatar-renderer.esm.js.map +0 -1
package/src/loaders/PlyLoader.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PlyLoader
|
|
3
|
-
*
|
|
2
|
+
* PlyLoader - Loads and parses PLY format Gaussian Splat files
|
|
3
|
+
*
|
|
4
4
|
* Derived from @mkkellogg/gaussian-splats-3d (MIT License)
|
|
5
5
|
* https://github.com/mkkellogg/GaussianSplats3D
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* Simplified for FLAME avatar - only supports INRIAV1 PLY format.
|
|
8
|
+
* Provides both progressive streaming and file-based loading.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import { Vector3 } from 'three';
|
|
@@ -16,18 +17,35 @@ import { PlyParserUtils } from './PlyParserUtils.js';
|
|
|
16
17
|
import { INRIAV1PlyParser } from './INRIAV1PlyParser.js';
|
|
17
18
|
import { Constants, InternalLoadType, LoaderStatus } from '../enums/EngineConstants.js';
|
|
18
19
|
import { fetchWithProgress, delayedExecute, nativePromiseWithExtractedComponents } from '../utils/Util.js';
|
|
20
|
+
import { getLogger } from '../utils/Logger.js';
|
|
21
|
+
import { ValidationError, NetworkError, ParseError, AssetLoadError } from '../errors/index.js';
|
|
22
|
+
import { validateUrl, validateCallback, validateArrayBuffer } from '../utils/ValidationUtils.js';
|
|
23
|
+
|
|
24
|
+
const logger = getLogger('PlyLoader');
|
|
19
25
|
|
|
20
26
|
/**
|
|
21
|
-
*
|
|
27
|
+
* Store data chunks into a single ArrayBuffer
|
|
28
|
+
*
|
|
29
|
+
* Combines multiple downloaded chunks into a contiguous buffer for parsing.
|
|
30
|
+
* Reallocates buffer if needed to fit all chunks.
|
|
31
|
+
*
|
|
32
|
+
* @private
|
|
33
|
+
* @param {Array<{data: Uint8Array, sizeBytes: number}>} chunks - Array of data chunks
|
|
34
|
+
* @param {ArrayBuffer} [buffer] - Existing buffer to reuse (will reallocate if too small)
|
|
35
|
+
* @returns {ArrayBuffer} Buffer containing all chunk data
|
|
22
36
|
*/
|
|
23
37
|
function storeChunksInBuffer(chunks, buffer) {
|
|
24
38
|
let inBytes = 0;
|
|
25
|
-
for (let chunk of chunks)
|
|
39
|
+
for (let chunk of chunks) {
|
|
40
|
+
inBytes += chunk.sizeBytes;
|
|
41
|
+
}
|
|
26
42
|
|
|
43
|
+
// Reallocate if buffer doesn't exist or is too small
|
|
27
44
|
if (!buffer || buffer.byteLength < inBytes) {
|
|
28
45
|
buffer = new ArrayBuffer(inBytes);
|
|
29
46
|
}
|
|
30
47
|
|
|
48
|
+
// Copy all chunks into buffer sequentially
|
|
31
49
|
let offset = 0;
|
|
32
50
|
for (let chunk of chunks) {
|
|
33
51
|
new Uint8Array(buffer, offset, chunk.sizeBytes).set(chunk.data);
|
|
@@ -38,25 +56,103 @@ function storeChunksInBuffer(chunks, buffer) {
|
|
|
38
56
|
}
|
|
39
57
|
|
|
40
58
|
/**
|
|
41
|
-
*
|
|
59
|
+
* Finalize splat data into a SplatBuffer
|
|
60
|
+
*
|
|
61
|
+
* Converts UncompressedSplatArray into final optimized SplatBuffer format.
|
|
62
|
+
* Applies compression and optimization if requested.
|
|
63
|
+
*
|
|
64
|
+
* @private
|
|
65
|
+
* @param {UncompressedSplatArray} splatData - Parsed splat data
|
|
66
|
+
* @param {boolean} optimizeSplatData - Whether to optimize/compress the data
|
|
67
|
+
* @param {number} minimumAlpha - Minimum alpha threshold for splat culling
|
|
68
|
+
* @param {number} compressionLevel - Compression level (0-2)
|
|
69
|
+
* @param {number} sectionSize - Section size for partitioning
|
|
70
|
+
* @param {Vector3} sceneCenter - Center point of the scene
|
|
71
|
+
* @param {number} blockSize - Block size for spatial partitioning
|
|
72
|
+
* @param {number} bucketSize - Bucket size for sorting
|
|
73
|
+
* @returns {SplatBuffer} Finalized splat buffer ready for rendering
|
|
74
|
+
* @throws {ParseError} If splat data is invalid or finalization fails
|
|
42
75
|
*/
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
76
|
+
function finalizeSplatData(splatData, optimizeSplatData, minimumAlpha, compressionLevel, sectionSize, sceneCenter, blockSize, bucketSize) {
|
|
77
|
+
try {
|
|
78
|
+
if (optimizeSplatData) {
|
|
79
|
+
const splatBufferGenerator = SplatBufferGenerator.getStandardGenerator(
|
|
80
|
+
minimumAlpha,
|
|
81
|
+
compressionLevel,
|
|
82
|
+
sectionSize,
|
|
83
|
+
sceneCenter,
|
|
84
|
+
blockSize,
|
|
85
|
+
bucketSize
|
|
86
|
+
);
|
|
87
|
+
return splatBufferGenerator.generateFromUncompressedSplatArray(splatData);
|
|
88
|
+
} else {
|
|
89
|
+
return SplatBuffer.generateFromUncompressedSplatArrays([splatData], minimumAlpha, 0, new Vector3());
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
throw new ParseError(
|
|
93
|
+
`Failed to finalize splat data: ${error.message}`,
|
|
94
|
+
'splatData',
|
|
95
|
+
error
|
|
96
|
+
);
|
|
51
97
|
}
|
|
52
98
|
}
|
|
53
99
|
|
|
100
|
+
/**
|
|
101
|
+
* PlyLoader - Loads and parses PLY format Gaussian Splat files
|
|
102
|
+
*
|
|
103
|
+
* Supports both progressive streaming and complete file loading.
|
|
104
|
+
* Optimized for INRIAV1 PLY format used in FLAME avatars.
|
|
105
|
+
*/
|
|
54
106
|
export class PlyLoader {
|
|
55
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Load PLY file from URL with progressive streaming support
|
|
110
|
+
*
|
|
111
|
+
* Downloads and parses PLY data progressively, enabling render during load.
|
|
112
|
+
* Supports both direct-to-buffer and array-based loading modes.
|
|
113
|
+
*
|
|
114
|
+
* @static
|
|
115
|
+
* @param {string} fileName - URL to the PLY file
|
|
116
|
+
* @param {Function} [onProgress] - Progress callback (percent, percentLabel, status)
|
|
117
|
+
* @param {boolean} [loadDirectoToSplatBuffer=false] - Load directly to SplatBuffer (faster but less flexible)
|
|
118
|
+
* @param {Function} [onProgressiveLoadSectionProgress] - Callback for progressive section updates
|
|
119
|
+
* @param {number} [minimumAlpha=1] - Minimum alpha threshold for splat culling
|
|
120
|
+
* @param {number} [compressionLevel=0] - Compression level (0=none, 1=medium, 2=high)
|
|
121
|
+
* @param {boolean} [optimizeSplatData=true] - Whether to optimize/compress splat data
|
|
122
|
+
* @param {number} [outSphericalHarmonicsDegree=0] - Spherical harmonics degree (0-3)
|
|
123
|
+
* @param {Object} [headers] - HTTP headers for fetch request
|
|
124
|
+
* @param {number} [sectionSize] - Section size for partitioning
|
|
125
|
+
* @param {Vector3} [sceneCenter] - Center point of the scene
|
|
126
|
+
* @param {number} [blockSize] - Block size for spatial partitioning
|
|
127
|
+
* @param {number} [bucketSize] - Bucket size for sorting
|
|
128
|
+
* @returns {Promise<SplatBuffer>} Loaded and parsed splat buffer
|
|
129
|
+
* @throws {ValidationError} If parameters are invalid
|
|
130
|
+
* @throws {NetworkError} If file download fails
|
|
131
|
+
* @throws {ParseError} If PLY parsing fails
|
|
132
|
+
* @throws {AssetLoadError} If asset loading fails
|
|
133
|
+
*/
|
|
56
134
|
static loadFromURL(fileName, onProgress, loadDirectoToSplatBuffer, onProgressiveLoadSectionProgress,
|
|
57
135
|
minimumAlpha, compressionLevel, optimizeSplatData = true, outSphericalHarmonicsDegree = 0,
|
|
58
136
|
headers, sectionSize, sceneCenter, blockSize, bucketSize) {
|
|
59
137
|
|
|
138
|
+
// Validate required parameters
|
|
139
|
+
try {
|
|
140
|
+
validateUrl(fileName);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
logger.error('Invalid URL provided to loadFromURL', { fileName, error });
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Validate optional callbacks
|
|
147
|
+
if (onProgress) {
|
|
148
|
+
validateCallback(onProgress, 'onProgress', false);
|
|
149
|
+
}
|
|
150
|
+
if (onProgressiveLoadSectionProgress) {
|
|
151
|
+
validateCallback(onProgressiveLoadSectionProgress, 'onProgressiveLoadSectionProgress', false);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
logger.info('Loading PLY from URL', { fileName, optimizeSplatData, outSphericalHarmonicsDegree });
|
|
155
|
+
|
|
60
156
|
let internalLoadType = loadDirectoToSplatBuffer ? InternalLoadType.DirectToSplatBuffer : InternalLoadType.DirectToSplatArray;
|
|
61
157
|
if (optimizeSplatData) internalLoadType = InternalLoadType.DirectToSplatArray;
|
|
62
158
|
|
|
@@ -108,12 +204,28 @@ export class PlyLoader {
|
|
|
108
204
|
if (!headerLoaded) {
|
|
109
205
|
headerText += textDecoder.decode(chunkData);
|
|
110
206
|
if (PlyParserUtils.checkTextForEndHeader(headerText)) {
|
|
111
|
-
// FLAME avatars use INRIAV1 format
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
207
|
+
// FLAME avatars use INRIAV1 format - parse header
|
|
208
|
+
try {
|
|
209
|
+
header = inriaV1PlyParser.decodeHeaderText(headerText);
|
|
210
|
+
maxSplatCount = header.splatCount;
|
|
211
|
+
readyToLoadSplatData = true;
|
|
212
|
+
|
|
213
|
+
logger.debug('PLY header decoded', {
|
|
214
|
+
splatCount: maxSplatCount,
|
|
215
|
+
sphericalHarmonicsDegree: header.sphericalHarmonicsDegree
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
outSphericalHarmonicsDegree = Math.min(outSphericalHarmonicsDegree, header.sphericalHarmonicsDegree);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
const parseError = new ParseError(
|
|
221
|
+
`Failed to decode PLY header: ${error.message}`,
|
|
222
|
+
'headerText',
|
|
223
|
+
error
|
|
224
|
+
);
|
|
225
|
+
logger.error('Header parsing failed', parseError);
|
|
226
|
+
loadPromise.reject(parseError);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
117
229
|
|
|
118
230
|
const shDescriptor = SplatBuffer.CompressionLevels[0].SphericalHarmonicsDegrees[outSphericalHarmonicsDegree];
|
|
119
231
|
const splatBufferSizeBytes = splatDataOffsetBytes + shDescriptor.BytesPerSplat * maxSplatCount;
|
|
@@ -159,14 +271,30 @@ export class PlyLoader {
|
|
|
159
271
|
const shDescriptor = SplatBuffer.CompressionLevels[0].SphericalHarmonicsDegrees[outSphericalHarmonicsDegree];
|
|
160
272
|
const outOffset = splatCount * shDescriptor.BytesPerSplat + splatDataOffsetBytes;
|
|
161
273
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
274
|
+
// Parse splat data with error handling
|
|
275
|
+
try {
|
|
276
|
+
if (internalLoadType === InternalLoadType.DirectToSplatBuffer) {
|
|
277
|
+
inriaV1PlyParser.parseToUncompressedSplatBufferSection(
|
|
278
|
+
header, 0, addedSplatCount - 1, dataToParse,
|
|
279
|
+
0, directLoadBufferOut, outOffset,
|
|
280
|
+
outSphericalHarmonicsDegree
|
|
281
|
+
);
|
|
282
|
+
} else {
|
|
283
|
+
inriaV1PlyParser.parseToUncompressedSplatArraySection(
|
|
284
|
+
header, 0, addedSplatCount - 1, dataToParse,
|
|
285
|
+
0, standardLoadUncompressedSplatArray,
|
|
286
|
+
outSphericalHarmonicsDegree
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
} catch (error) {
|
|
290
|
+
const parseError = new ParseError(
|
|
291
|
+
`Failed to parse splat data section: ${error.message}`,
|
|
292
|
+
'splatData',
|
|
293
|
+
error
|
|
294
|
+
);
|
|
295
|
+
logger.error('Splat data parsing failed', { splatCount, addedSplatCount, error });
|
|
296
|
+
loadPromise.reject(parseError);
|
|
297
|
+
return;
|
|
170
298
|
}
|
|
171
299
|
|
|
172
300
|
splatCount = newSplatCount;
|
|
@@ -222,40 +350,170 @@ export class PlyLoader {
|
|
|
222
350
|
}
|
|
223
351
|
}
|
|
224
352
|
|
|
225
|
-
|
|
353
|
+
// Progress callback with error isolation
|
|
354
|
+
if (onProgress) {
|
|
355
|
+
try {
|
|
356
|
+
onProgress(percent, percentLabel, LoaderStatus.Downloading);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
logger.warn('Error in onProgress callback', error);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
226
361
|
};
|
|
227
362
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
363
|
+
// Initial progress callback
|
|
364
|
+
if (onProgress) {
|
|
365
|
+
try {
|
|
366
|
+
onProgress(0, '0%', LoaderStatus.Downloading);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
logger.warn('Error in onProgress callback', error);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Fetch and process the PLY file
|
|
373
|
+
return fetchWithProgress(fileName, localOnProgress, false, headers)
|
|
374
|
+
.then(() => {
|
|
375
|
+
if (onProgress) {
|
|
376
|
+
try {
|
|
377
|
+
onProgress(0, '0%', LoaderStatus.Processing);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
logger.warn('Error in onProgress callback', error);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return loadPromise.promise;
|
|
383
|
+
})
|
|
384
|
+
.then((splatData) => {
|
|
385
|
+
if (onProgress) {
|
|
386
|
+
try {
|
|
387
|
+
onProgress(100, '100%', LoaderStatus.Done);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
logger.warn('Error in onProgress callback', error);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
logger.debug('PLY data loaded successfully', {
|
|
394
|
+
internalLoadType,
|
|
395
|
+
splatCount: splatData?.splatCount || 'unknown'
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Process based on load type
|
|
233
399
|
if (internalLoadType === InternalLoadType.DownloadBeforeProcessing) {
|
|
234
400
|
const chunkDatas = chunks.map((chunk) => chunk.data);
|
|
235
|
-
return new Blob(chunkDatas).arrayBuffer()
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
401
|
+
return new Blob(chunkDatas).arrayBuffer()
|
|
402
|
+
.then((plyFileData) => {
|
|
403
|
+
return PlyLoader.loadFromFileData(
|
|
404
|
+
plyFileData, minimumAlpha, compressionLevel, optimizeSplatData,
|
|
405
|
+
outSphericalHarmonicsDegree, sectionSize, sceneCenter, blockSize, bucketSize
|
|
406
|
+
);
|
|
407
|
+
})
|
|
408
|
+
.catch((error) => {
|
|
409
|
+
throw new AssetLoadError(
|
|
410
|
+
`Failed to process downloaded PLY data: ${error.message}`,
|
|
411
|
+
fileName,
|
|
412
|
+
error
|
|
413
|
+
);
|
|
414
|
+
});
|
|
239
415
|
} else if (internalLoadType === InternalLoadType.DirectToSplatBuffer) {
|
|
240
416
|
return splatData;
|
|
241
417
|
} else {
|
|
242
418
|
return delayedExecute(() => {
|
|
243
|
-
return
|
|
244
|
-
|
|
419
|
+
return finalizeSplatData(
|
|
420
|
+
splatData, optimizeSplatData, minimumAlpha, compressionLevel,
|
|
421
|
+
sectionSize, sceneCenter, blockSize, bucketSize
|
|
422
|
+
);
|
|
245
423
|
});
|
|
246
424
|
}
|
|
425
|
+
})
|
|
426
|
+
.catch((error) => {
|
|
427
|
+
// Re-throw custom errors as-is
|
|
428
|
+
if (error instanceof ValidationError ||
|
|
429
|
+
error instanceof NetworkError ||
|
|
430
|
+
error instanceof ParseError ||
|
|
431
|
+
error instanceof AssetLoadError) {
|
|
432
|
+
logger.error('PLY loading failed', { fileName, errorCode: error.code });
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Wrap unexpected errors
|
|
437
|
+
logger.error('Unexpected error loading PLY', { fileName, error });
|
|
438
|
+
throw new AssetLoadError(
|
|
439
|
+
`Unexpected error loading PLY file: ${error.message}`,
|
|
440
|
+
fileName,
|
|
441
|
+
error
|
|
442
|
+
);
|
|
247
443
|
});
|
|
248
|
-
});
|
|
249
444
|
}
|
|
250
445
|
|
|
446
|
+
/**
|
|
447
|
+
* Load PLY file from raw ArrayBuffer data
|
|
448
|
+
*
|
|
449
|
+
* Parses PLY data that has already been downloaded. Useful for loading
|
|
450
|
+
* from local files or when data is provided directly.
|
|
451
|
+
*
|
|
452
|
+
* @static
|
|
453
|
+
* @param {ArrayBuffer} plyFileData - Raw PLY file data as ArrayBuffer
|
|
454
|
+
* @param {number} [minimumAlpha=1] - Minimum alpha threshold for splat culling
|
|
455
|
+
* @param {number} [compressionLevel=0] - Compression level (0=none, 1=medium, 2=high)
|
|
456
|
+
* @param {boolean} [optimizeSplatData=true] - Whether to optimize/compress splat data
|
|
457
|
+
* @param {number} [outSphericalHarmonicsDegree=0] - Spherical harmonics degree (0-3)
|
|
458
|
+
* @param {number} [sectionSize] - Section size for partitioning
|
|
459
|
+
* @param {Vector3} [sceneCenter] - Center point of the scene
|
|
460
|
+
* @param {number} [blockSize] - Block size for spatial partitioning
|
|
461
|
+
* @param {number} [bucketSize] - Bucket size for sorting
|
|
462
|
+
* @returns {Promise<SplatBuffer>} Parsed and finalized splat buffer
|
|
463
|
+
* @throws {ValidationError} If plyFileData is invalid
|
|
464
|
+
* @throws {ParseError} If parsing fails
|
|
465
|
+
*/
|
|
251
466
|
static loadFromFileData(plyFileData, minimumAlpha, compressionLevel, optimizeSplatData, outSphericalHarmonicsDegree = 0,
|
|
252
467
|
sectionSize, sceneCenter, blockSize, bucketSize) {
|
|
468
|
+
// Validate input
|
|
469
|
+
try {
|
|
470
|
+
validateArrayBuffer(plyFileData, 'plyFileData');
|
|
471
|
+
} catch (error) {
|
|
472
|
+
logger.error('Invalid PLY file data', error);
|
|
473
|
+
return Promise.reject(error);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
logger.info('Loading PLY from file data', {
|
|
477
|
+
sizeBytes: plyFileData.byteLength,
|
|
478
|
+
optimizeSplatData,
|
|
479
|
+
outSphericalHarmonicsDegree
|
|
480
|
+
});
|
|
481
|
+
|
|
253
482
|
return delayedExecute(() => {
|
|
254
|
-
|
|
483
|
+
try {
|
|
484
|
+
return PlyParser.parseToUncompressedSplatArray(plyFileData, outSphericalHarmonicsDegree);
|
|
485
|
+
} catch (error) {
|
|
486
|
+
throw new ParseError(
|
|
487
|
+
`Failed to parse PLY file data: ${error.message}`,
|
|
488
|
+
'plyFileData',
|
|
489
|
+
error
|
|
490
|
+
);
|
|
491
|
+
}
|
|
255
492
|
})
|
|
256
493
|
.then((splatArray) => {
|
|
257
|
-
|
|
258
|
-
|
|
494
|
+
logger.debug('PLY parsed successfully', {
|
|
495
|
+
splatCount: splatArray?.splatCount || 'unknown'
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
return finalizeSplatData(
|
|
499
|
+
splatArray, optimizeSplatData, minimumAlpha, compressionLevel,
|
|
500
|
+
sectionSize, sceneCenter, blockSize, bucketSize
|
|
501
|
+
);
|
|
502
|
+
})
|
|
503
|
+
.catch((error) => {
|
|
504
|
+
// Re-throw custom errors as-is
|
|
505
|
+
if (error instanceof ValidationError || error instanceof ParseError) {
|
|
506
|
+
logger.error('PLY file data loading failed', { errorCode: error.code });
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Wrap unexpected errors
|
|
511
|
+
logger.error('Unexpected error loading PLY from file data', error);
|
|
512
|
+
throw new ParseError(
|
|
513
|
+
`Unexpected error parsing PLY data: ${error.message}`,
|
|
514
|
+
'plyFileData',
|
|
515
|
+
error
|
|
516
|
+
);
|
|
259
517
|
});
|
|
260
518
|
}
|
|
261
519
|
}
|
|
@@ -16,14 +16,9 @@ import { Constants } from '../enums/EngineConstants.js';
|
|
|
16
16
|
|
|
17
17
|
export class SplatMaterial {
|
|
18
18
|
|
|
19
|
-
static buildVertexShaderBase(dynamicMode = false, enableOptionalEffects = false, maxSphericalHarmonicsDegree = 0, customVars = ''
|
|
20
|
-
let vertexShaderSource =
|
|
21
|
-
|
|
22
|
-
vertexShaderSource += `#define USE_FLAME`;
|
|
23
|
-
} else {
|
|
24
|
-
vertexShaderSource += `#define USE_SKINNING`;
|
|
25
|
-
}
|
|
26
|
-
vertexShaderSource += `
|
|
19
|
+
static buildVertexShaderBase(dynamicMode = false, enableOptionalEffects = false, maxSphericalHarmonicsDegree = 0, customVars = '') {
|
|
20
|
+
let vertexShaderSource = `#define USE_SKINNING
|
|
21
|
+
|
|
27
22
|
precision highp float;
|
|
28
23
|
#include <common>
|
|
29
24
|
|
|
@@ -755,6 +750,14 @@ export class SplatMaterial {
|
|
|
755
750
|
'headBoneIndex': {
|
|
756
751
|
'type': 'f',
|
|
757
752
|
'value': -1.0
|
|
753
|
+
},
|
|
754
|
+
'eyeBlinkLeft': {
|
|
755
|
+
'type': 'f',
|
|
756
|
+
'value': 0.0
|
|
757
|
+
},
|
|
758
|
+
'eyeBlinkRight': {
|
|
759
|
+
'type': 'f',
|
|
760
|
+
'value': 0.0
|
|
758
761
|
}
|
|
759
762
|
};
|
|
760
763
|
for (let i = 0; i < Constants.MaxScenes; i++) {
|
|
@@ -815,7 +818,7 @@ class SplatMaterial3D {
|
|
|
815
818
|
* @return {THREE.ShaderMaterial}
|
|
816
819
|
*/
|
|
817
820
|
static build(dynamicMode = false, enableOptionalEffects = false, antialiased = false, maxScreenSpaceSplatSize = 2048,
|
|
818
|
-
splatScale = 1.0, pointCloudModeEnabled = false, maxSphericalHarmonicsDegree = 0, kernel2DSize = 0.3
|
|
821
|
+
splatScale = 1.0, pointCloudModeEnabled = false, maxSphericalHarmonicsDegree = 0, kernel2DSize = 0.3) {
|
|
819
822
|
|
|
820
823
|
const customVertexVars = `
|
|
821
824
|
uniform vec2 covariancesTextureSize;
|
|
@@ -834,7 +837,7 @@ class SplatMaterial3D {
|
|
|
834
837
|
`;
|
|
835
838
|
|
|
836
839
|
let vertexShaderSource = SplatMaterial.buildVertexShaderBase(dynamicMode, enableOptionalEffects,
|
|
837
|
-
maxSphericalHarmonicsDegree, customVertexVars
|
|
840
|
+
maxSphericalHarmonicsDegree, customVertexVars);
|
|
838
841
|
vertexShaderSource += SplatMaterial3D.buildVertexShaderProjection(antialiased, enableOptionalEffects,
|
|
839
842
|
maxScreenSpaceSplatSize, kernel2DSize);
|
|
840
843
|
const fragmentShaderSource = SplatMaterial3D.buildFragmentShader();
|
|
@@ -28,7 +28,7 @@ export class SplatMaterial3D {
|
|
|
28
28
|
* @return {THREE.ShaderMaterial}
|
|
29
29
|
*/
|
|
30
30
|
static build(dynamicMode = false, enableOptionalEffects = false, antialiased = false, maxScreenSpaceSplatSize = 2048,
|
|
31
|
-
splatScale = 1.0, pointCloudModeEnabled = false, maxSphericalHarmonicsDegree = 0, kernel2DSize = 0.3,
|
|
31
|
+
splatScale = 1.0, pointCloudModeEnabled = false, maxSphericalHarmonicsDegree = 0, kernel2DSize = 0.3, irisOcclusionConfig = null) {
|
|
32
32
|
|
|
33
33
|
const customVertexVars = `
|
|
34
34
|
uniform vec2 covariancesTextureSize;
|
|
@@ -46,11 +46,12 @@ export class SplatMaterial3D {
|
|
|
46
46
|
}
|
|
47
47
|
`;
|
|
48
48
|
|
|
49
|
+
// Add a varying for iris splats
|
|
49
50
|
let vertexShaderSource = SplatMaterial.buildVertexShaderBase(dynamicMode, enableOptionalEffects,
|
|
50
|
-
|
|
51
|
+
maxSphericalHarmonicsDegree, customVertexVars);
|
|
51
52
|
vertexShaderSource += SplatMaterial3D.buildVertexShaderProjection(antialiased, enableOptionalEffects,
|
|
52
|
-
|
|
53
|
-
const fragmentShaderSource = SplatMaterial3D.buildFragmentShader();
|
|
53
|
+
maxScreenSpaceSplatSize, kernel2DSize);
|
|
54
|
+
const fragmentShaderSource = SplatMaterial3D.buildFragmentShader(irisOcclusionConfig);
|
|
54
55
|
|
|
55
56
|
const uniforms = SplatMaterial.getUniforms(dynamicMode, enableOptionalEffects,
|
|
56
57
|
maxSphericalHarmonicsDegree, splatScale, pointCloudModeEnabled);
|
|
@@ -229,48 +230,92 @@ export class SplatMaterial3D {
|
|
|
229
230
|
return vertexShaderSource;
|
|
230
231
|
}
|
|
231
232
|
|
|
232
|
-
static buildFragmentShader() {
|
|
233
|
+
static buildFragmentShader(irisOcclusionConfig = null) {
|
|
233
234
|
let fragmentShaderSource = `
|
|
234
235
|
precision highp float;
|
|
235
236
|
#include <common>
|
|
236
|
-
|
|
237
|
-
uniform
|
|
237
|
+
|
|
238
|
+
uniform float eyeBlinkLeft;
|
|
239
|
+
uniform float eyeBlinkRight;
|
|
238
240
|
|
|
239
241
|
varying vec4 vColor;
|
|
240
242
|
varying vec2 vUv;
|
|
241
243
|
varying vec2 vPosition;
|
|
242
244
|
varying vec2 vSplatIndex;
|
|
243
|
-
|
|
244
245
|
`;
|
|
245
246
|
|
|
246
247
|
fragmentShaderSource += `
|
|
247
248
|
void main () {
|
|
248
|
-
// Compute the positional squared distance from the center of the splat to the current fragment.
|
|
249
249
|
float A = dot(vPosition, vPosition);
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
// defined by the rectangle formed by vPosition. It also means it's farther
|
|
253
|
-
// away than sqrt(8) standard deviations from the mean.
|
|
254
|
-
|
|
255
|
-
// if(vSplatIndex.x > 20000.0) discard;
|
|
256
|
-
// if (A > 8.0) discard;
|
|
257
|
-
vec3 color = vColor.rgb;
|
|
258
|
-
|
|
259
|
-
// Since the rendered splat is scaled by sqrt(8), the inverse covariance matrix that is part of
|
|
260
|
-
// the gaussian formula becomes the identity matrix. We're then left with (X - mean) * (X - mean),
|
|
261
|
-
// and since 'mean' is zero, we have X * X, which is the same as A:
|
|
262
|
-
float opacity = exp( -0.5*A) * vColor.a;
|
|
263
|
-
if(opacity < 1.0 / 255.0)
|
|
250
|
+
float opacity = exp(-0.5 * A) * vColor.a;
|
|
251
|
+
if (opacity < 1.0 / 255.0)
|
|
264
252
|
discard;
|
|
253
|
+
`;
|
|
254
|
+
|
|
255
|
+
// Generate iris occlusion code only if config exists
|
|
256
|
+
if (irisOcclusionConfig && (irisOcclusionConfig.right_iris || irisOcclusionConfig.left_iris)) {
|
|
257
|
+
fragmentShaderSource += `
|
|
258
|
+
float idx = vSplatIndex.x;
|
|
259
|
+
`;
|
|
260
|
+
|
|
261
|
+
// Generate right iris checks
|
|
262
|
+
if (irisOcclusionConfig.right_iris && irisOcclusionConfig.right_iris.length > 0) {
|
|
263
|
+
const rightConditions = irisOcclusionConfig.right_iris
|
|
264
|
+
.map(([start, end]) => `(idx >= ${start}.0 && idx <= ${end}.0)`)
|
|
265
|
+
.join(' ||\n ');
|
|
266
|
+
|
|
267
|
+
fragmentShaderSource += `
|
|
268
|
+
// Check if this splat is part of right iris
|
|
269
|
+
bool isRightIris = ${rightConditions};
|
|
270
|
+
`;
|
|
271
|
+
} else {
|
|
272
|
+
fragmentShaderSource += `
|
|
273
|
+
bool isRightIris = false;
|
|
274
|
+
`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Generate left iris checks
|
|
278
|
+
if (irisOcclusionConfig.left_iris && irisOcclusionConfig.left_iris.length > 0) {
|
|
279
|
+
const leftConditions = irisOcclusionConfig.left_iris
|
|
280
|
+
.map(([start, end]) => `(idx >= ${start}.0 && idx <= ${end}.0)`)
|
|
281
|
+
.join(' ||\n ');
|
|
282
|
+
|
|
283
|
+
fragmentShaderSource += `
|
|
284
|
+
// Check if this splat is part of left iris
|
|
285
|
+
bool isLeftIris = ${leftConditions};
|
|
286
|
+
`;
|
|
287
|
+
} else {
|
|
288
|
+
fragmentShaderSource += `
|
|
289
|
+
bool isLeftIris = false;
|
|
290
|
+
`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
fragmentShaderSource += `
|
|
294
|
+
float finalOpacity = opacity;
|
|
265
295
|
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
296
|
+
// Smooth fade: iris fades out as eye closes (blink increases)
|
|
297
|
+
// smoothstep(0.1, 0.5, blink) = 0 when blink<0.1, 1 when blink>0.5
|
|
298
|
+
if (isRightIris) {
|
|
299
|
+
float fadeFactor = 1.0 - smoothstep(0.1, 0.5, eyeBlinkRight);
|
|
300
|
+
finalOpacity = opacity * fadeFactor;
|
|
301
|
+
} else if (isLeftIris) {
|
|
302
|
+
float fadeFactor = 1.0 - smoothstep(0.1, 0.5, eyeBlinkLeft);
|
|
303
|
+
finalOpacity = opacity * fadeFactor;
|
|
304
|
+
}
|
|
270
305
|
|
|
306
|
+
if (finalOpacity < 1.0 / 255.0)
|
|
307
|
+
discard;
|
|
271
308
|
|
|
309
|
+
gl_FragColor = vec4(vColor.rgb, finalOpacity);
|
|
272
310
|
}
|
|
273
311
|
`;
|
|
312
|
+
} else {
|
|
313
|
+
// No iris occlusion - simple rendering
|
|
314
|
+
fragmentShaderSource += `
|
|
315
|
+
gl_FragColor = vec4(vColor.rgb, opacity);
|
|
316
|
+
}
|
|
317
|
+
`;
|
|
318
|
+
}
|
|
274
319
|
|
|
275
320
|
return fragmentShaderSource;
|
|
276
321
|
}
|