@myned-ai/gsplat-flame-avatar-renderer 1.0.5 → 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.
@@ -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
- * Helper function to store chunks into a single buffer
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) inBytes += chunk.sizeBytes;
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
- * Helper function to finalize splat data
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 finalize$1(splatData, optimizeSplatData, minimumAlpha, compressionLevel, sectionSize, sceneCenter, blockSize, bucketSize) {
44
- if (optimizeSplatData) {
45
- const splatBufferGenerator = SplatBufferGenerator.getStandardGenerator(minimumAlpha, compressionLevel,
46
- sectionSize, sceneCenter,
47
- blockSize, bucketSize);
48
- return splatBufferGenerator.generateFromUncompressedSplatArray(splatData);
49
- } else {
50
- return SplatBuffer.generateFromUncompressedSplatArrays([splatData], minimumAlpha, 0, new Vector3());
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
- header = inriaV1PlyParser.decodeHeaderText(headerText);
113
- maxSplatCount = header.splatCount;
114
- readyToLoadSplatData = true;
115
-
116
- outSphericalHarmonicsDegree = Math.min(outSphericalHarmonicsDegree, header.sphericalHarmonicsDegree);
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
- if (internalLoadType === InternalLoadType.DirectToSplatBuffer) {
163
- inriaV1PlyParser.parseToUncompressedSplatBufferSection(header, 0, addedSplatCount - 1, dataToParse,
164
- 0, directLoadBufferOut, outOffset,
165
- outSphericalHarmonicsDegree);
166
- } else {
167
- inriaV1PlyParser.parseToUncompressedSplatArraySection(header, 0, addedSplatCount - 1, dataToParse,
168
- 0, standardLoadUncompressedSplatArray,
169
- outSphericalHarmonicsDegree);
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
- if (onProgress) onProgress(percent, percentLabel, LoaderStatus.Downloading);
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
- if (onProgress) onProgress(0, '0%', LoaderStatus.Downloading);
229
- return fetchWithProgress(fileName, localOnProgress, false, headers).then(() => {
230
- if (onProgress) onProgress(0, '0%', LoaderStatus.Processing);
231
- return loadPromise.promise.then((splatData) => {
232
- if (onProgress) onProgress(100, '100%', LoaderStatus.Done);
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().then((plyFileData) => {
236
- return PlyLoader.loadFromFileData(plyFileData, minimumAlpha, compressionLevel, optimizeSplatData,
237
- outSphericalHarmonicsDegree, sectionSize, sceneCenter, blockSize, bucketSize);
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 finalize$1(splatData, optimizeSplatData, minimumAlpha, compressionLevel,
244
- sectionSize, sceneCenter, blockSize, bucketSize);
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
- return PlyParser.parseToUncompressedSplatArray(plyFileData, outSphericalHarmonicsDegree);
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
- return finalize$1(splatArray, optimizeSplatData, minimumAlpha, compressionLevel,
258
- sectionSize, sceneCenter, blockSize, bucketSize);
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 = '', useFlame = true) {
20
- let vertexShaderSource = ``;
21
- if (useFlame == true) {
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, useFlame = true) {
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, useFlame);
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, useFlame = true) {
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
- maxSphericalHarmonicsDegree, customVertexVars, useFlame);
51
+ maxSphericalHarmonicsDegree, customVertexVars);
51
52
  vertexShaderSource += SplatMaterial3D.buildVertexShaderProjection(antialiased, enableOptionalEffects,
52
- maxScreenSpaceSplatSize, kernel2DSize);
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 vec3 debugColor;
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
- // Since the positional data in vPosition has been scaled by sqrt(8), the squared result will be
251
- // scaled by a factor of 8. If the squared result is larger than 8, it means it is outside the ellipse
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
- // uint a = uint(255);
267
- // vec3 c = vec3(vSplatIndex.x / 256.0 / 256.0, float(uint(vSplatIndex.x / 256.0 )% a) / 256.0, float(uint(vSplatIndex.x)% a) / 256.0);
268
- // gl_FragColor = vec4(c, 1.0);
269
- gl_FragColor = vec4(color, opacity);
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
  }