@myned-ai/gsplat-flame-avatar-renderer 1.0.1 → 1.0.4

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.
Files changed (66) hide show
  1. package/README.md +4 -29
  2. package/dist/gsplat-flame-avatar-renderer.cjs.js +12875 -0
  3. package/dist/{gsplat-flame-avatar-renderer.umd.js.map → gsplat-flame-avatar-renderer.cjs.js.map} +1 -1
  4. package/dist/gsplat-flame-avatar-renderer.esm.js +1 -1
  5. package/package.json +5 -3
  6. package/src/api/index.js +7 -0
  7. package/src/buffers/SplatBuffer.js +1394 -0
  8. package/src/buffers/SplatBufferGenerator.js +41 -0
  9. package/src/buffers/SplatPartitioner.js +110 -0
  10. package/src/buffers/UncompressedSplatArray.js +106 -0
  11. package/src/buffers/index.js +11 -0
  12. package/src/core/SplatGeometry.js +48 -0
  13. package/src/core/SplatMesh.js +2620 -0
  14. package/src/core/SplatScene.js +43 -0
  15. package/src/core/SplatTree.js +200 -0
  16. package/src/core/Viewer.js +2895 -0
  17. package/src/core/index.js +13 -0
  18. package/src/enums/EngineConstants.js +58 -0
  19. package/src/enums/LogLevel.js +13 -0
  20. package/src/enums/RenderMode.js +11 -0
  21. package/src/enums/SceneFormat.js +21 -0
  22. package/src/enums/SceneRevealMode.js +11 -0
  23. package/src/enums/SplatRenderMode.js +10 -0
  24. package/src/enums/index.js +13 -0
  25. package/src/flame/FlameAnimator.js +271 -0
  26. package/src/flame/FlameConstants.js +21 -0
  27. package/src/flame/FlameTextureManager.js +293 -0
  28. package/src/flame/index.js +22 -0
  29. package/src/flame/utils.js +50 -0
  30. package/src/index.js +39 -0
  31. package/src/loaders/DirectLoadError.js +14 -0
  32. package/src/loaders/INRIAV1PlyParser.js +223 -0
  33. package/src/loaders/PlyLoader.js +261 -0
  34. package/src/loaders/PlyParser.js +19 -0
  35. package/src/loaders/PlyParserUtils.js +311 -0
  36. package/src/loaders/index.js +13 -0
  37. package/src/materials/SplatMaterial.js +1065 -0
  38. package/src/materials/SplatMaterial2D.js +358 -0
  39. package/src/materials/SplatMaterial3D.js +278 -0
  40. package/src/materials/index.js +11 -0
  41. package/src/raycaster/Hit.js +37 -0
  42. package/src/raycaster/Ray.js +123 -0
  43. package/src/raycaster/Raycaster.js +175 -0
  44. package/src/raycaster/index.js +10 -0
  45. package/src/renderer/AnimationManager.js +574 -0
  46. package/src/renderer/AppConstants.js +101 -0
  47. package/src/renderer/GaussianSplatRenderer.js +695 -0
  48. package/src/renderer/index.js +24 -0
  49. package/src/utils/LoaderUtils.js +65 -0
  50. package/src/utils/Util.js +375 -0
  51. package/src/utils/index.js +9 -0
  52. package/src/worker/SortWorker.js +284 -0
  53. package/src/worker/index.js +8 -0
  54. package/dist/gsplat-flame-avatar-renderer.esm.min.js +0 -2
  55. package/dist/gsplat-flame-avatar-renderer.esm.min.js.map +0 -1
  56. package/dist/gsplat-flame-avatar-renderer.umd.js +0 -12876
  57. package/dist/gsplat-flame-avatar-renderer.umd.min.js +0 -2
  58. package/dist/gsplat-flame-avatar-renderer.umd.min.js.map +0 -1
  59. package/dist/gsplat-flame-avatar.esm.js +0 -12755
  60. package/dist/gsplat-flame-avatar.esm.js.map +0 -1
  61. package/dist/gsplat-flame-avatar.esm.min.js +0 -2
  62. package/dist/gsplat-flame-avatar.esm.min.js.map +0 -1
  63. package/dist/gsplat-flame-avatar.umd.js +0 -12876
  64. package/dist/gsplat-flame-avatar.umd.js.map +0 -1
  65. package/dist/gsplat-flame-avatar.umd.min.js +0 -2
  66. package/dist/gsplat-flame-avatar.umd.min.js.map +0 -1
@@ -0,0 +1,261 @@
1
+ /**
2
+ * PlyLoader
3
+ *
4
+ * Derived from @mkkellogg/gaussian-splats-3d (MIT License)
5
+ * https://github.com/mkkellogg/GaussianSplats3D
6
+ *
7
+ * Simplified for FLAME avatar - only supports INRIAV1 PLY format.
8
+ */
9
+
10
+ import { Vector3 } from 'three';
11
+ import { SplatBuffer } from '../buffers/SplatBuffer.js';
12
+ import { UncompressedSplatArray } from '../buffers/UncompressedSplatArray.js';
13
+ import { SplatBufferGenerator } from '../buffers/SplatBufferGenerator.js';
14
+ import { PlyParser } from './PlyParser.js';
15
+ import { PlyParserUtils } from './PlyParserUtils.js';
16
+ import { INRIAV1PlyParser } from './INRIAV1PlyParser.js';
17
+ import { Constants, InternalLoadType, LoaderStatus } from '../enums/EngineConstants.js';
18
+ import { fetchWithProgress, delayedExecute, nativePromiseWithExtractedComponents } from '../utils/Util.js';
19
+
20
+ /**
21
+ * Helper function to store chunks into a single buffer
22
+ */
23
+ function storeChunksInBuffer(chunks, buffer) {
24
+ let inBytes = 0;
25
+ for (let chunk of chunks) inBytes += chunk.sizeBytes;
26
+
27
+ if (!buffer || buffer.byteLength < inBytes) {
28
+ buffer = new ArrayBuffer(inBytes);
29
+ }
30
+
31
+ let offset = 0;
32
+ for (let chunk of chunks) {
33
+ new Uint8Array(buffer, offset, chunk.sizeBytes).set(chunk.data);
34
+ offset += chunk.sizeBytes;
35
+ }
36
+
37
+ return buffer;
38
+ }
39
+
40
+ /**
41
+ * Helper function to finalize splat data
42
+ */
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());
51
+ }
52
+ }
53
+
54
+ export class PlyLoader {
55
+
56
+ static loadFromURL(fileName, onProgress, loadDirectoToSplatBuffer, onProgressiveLoadSectionProgress,
57
+ minimumAlpha, compressionLevel, optimizeSplatData = true, outSphericalHarmonicsDegree = 0,
58
+ headers, sectionSize, sceneCenter, blockSize, bucketSize) {
59
+
60
+ let internalLoadType = loadDirectoToSplatBuffer ? InternalLoadType.DirectToSplatBuffer : InternalLoadType.DirectToSplatArray;
61
+ if (optimizeSplatData) internalLoadType = InternalLoadType.DirectToSplatArray;
62
+
63
+ const directLoadSectionSizeBytes = Constants.ProgressiveLoadSectionSize;
64
+ const splatDataOffsetBytes = SplatBuffer.HeaderSizeBytes + SplatBuffer.SectionHeaderSizeBytes;
65
+ const sectionCount = 1;
66
+
67
+ let directLoadBufferIn;
68
+ let directLoadBufferOut;
69
+ let directLoadSplatBuffer;
70
+ let maxSplatCount = 0;
71
+ let splatCount = 0;
72
+
73
+ let headerLoaded = false;
74
+ let readyToLoadSplatData = false;
75
+
76
+ const loadPromise = nativePromiseWithExtractedComponents();
77
+
78
+ let numBytesStreamed = 0;
79
+ let numBytesParsed = 0;
80
+ let numBytesDownloaded = 0;
81
+ let headerText = '';
82
+ let header = null;
83
+ let chunks = [];
84
+
85
+ let standardLoadUncompressedSplatArray;
86
+
87
+ const textDecoder = new TextDecoder();
88
+ const inriaV1PlyParser = new INRIAV1PlyParser();
89
+
90
+ const localOnProgress = (percent, percentLabel, chunkData) => {
91
+ const loadComplete = percent >= 100;
92
+
93
+ if (chunkData) {
94
+ chunks.push({
95
+ 'data': chunkData,
96
+ 'sizeBytes': chunkData.byteLength,
97
+ 'startBytes': numBytesDownloaded,
98
+ 'endBytes': numBytesDownloaded + chunkData.byteLength
99
+ });
100
+ numBytesDownloaded += chunkData.byteLength;
101
+ }
102
+
103
+ if (internalLoadType === InternalLoadType.DownloadBeforeProcessing) {
104
+ if (loadComplete) {
105
+ loadPromise.resolve(chunks);
106
+ }
107
+ } else {
108
+ if (!headerLoaded) {
109
+ headerText += textDecoder.decode(chunkData);
110
+ 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);
117
+
118
+ const shDescriptor = SplatBuffer.CompressionLevels[0].SphericalHarmonicsDegrees[outSphericalHarmonicsDegree];
119
+ const splatBufferSizeBytes = splatDataOffsetBytes + shDescriptor.BytesPerSplat * maxSplatCount;
120
+
121
+ if (internalLoadType === InternalLoadType.DirectToSplatBuffer) {
122
+ directLoadBufferOut = new ArrayBuffer(splatBufferSizeBytes);
123
+ SplatBuffer.writeHeaderToBuffer({
124
+ versionMajor: SplatBuffer.CurrentMajorVersion,
125
+ versionMinor: SplatBuffer.CurrentMinorVersion,
126
+ maxSectionCount: sectionCount,
127
+ sectionCount: sectionCount,
128
+ maxSplatCount: maxSplatCount,
129
+ splatCount: splatCount,
130
+ compressionLevel: 0,
131
+ sceneCenter: new Vector3()
132
+ }, directLoadBufferOut);
133
+ } else {
134
+ standardLoadUncompressedSplatArray = new UncompressedSplatArray(outSphericalHarmonicsDegree);
135
+ }
136
+
137
+ numBytesStreamed = header.headerSizeBytes;
138
+ numBytesParsed = header.headerSizeBytes;
139
+ headerLoaded = true;
140
+ }
141
+ }
142
+
143
+ if (headerLoaded && readyToLoadSplatData) {
144
+
145
+ if (chunks.length > 0) {
146
+
147
+ directLoadBufferIn = storeChunksInBuffer(chunks, directLoadBufferIn);
148
+
149
+ const bytesLoadedSinceLastStreamedSection = numBytesDownloaded - numBytesStreamed;
150
+ if (bytesLoadedSinceLastStreamedSection > directLoadSectionSizeBytes || loadComplete) {
151
+ const numBytesToProcess = numBytesDownloaded - numBytesParsed;
152
+ const addedSplatCount = Math.floor(numBytesToProcess / header.bytesPerSplat);
153
+ const numBytesToParse = addedSplatCount * header.bytesPerSplat;
154
+ const numBytesLeftOver = numBytesToProcess - numBytesToParse;
155
+ const newSplatCount = splatCount + addedSplatCount;
156
+ const parsedDataViewOffset = numBytesParsed - chunks[0].startBytes;
157
+ const dataToParse = new DataView(directLoadBufferIn, parsedDataViewOffset, numBytesToParse);
158
+
159
+ const shDescriptor = SplatBuffer.CompressionLevels[0].SphericalHarmonicsDegrees[outSphericalHarmonicsDegree];
160
+ const outOffset = splatCount * shDescriptor.BytesPerSplat + splatDataOffsetBytes;
161
+
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);
170
+ }
171
+
172
+ splatCount = newSplatCount;
173
+
174
+ if (internalLoadType === InternalLoadType.DirectToSplatBuffer) {
175
+ if (!directLoadSplatBuffer) {
176
+ SplatBuffer.writeSectionHeaderToBuffer({
177
+ maxSplatCount: maxSplatCount,
178
+ splatCount: splatCount,
179
+ bucketSize: 0,
180
+ bucketCount: 0,
181
+ bucketBlockSize: 0,
182
+ compressionScaleRange: 0,
183
+ storageSizeBytes: 0,
184
+ fullBucketCount: 0,
185
+ partiallyFilledBucketCount: 0,
186
+ sphericalHarmonicsDegree: outSphericalHarmonicsDegree
187
+ }, 0, directLoadBufferOut, SplatBuffer.HeaderSizeBytes);
188
+ directLoadSplatBuffer = new SplatBuffer(directLoadBufferOut, false);
189
+ }
190
+ directLoadSplatBuffer.updateLoadedCounts(1, splatCount);
191
+ if (onProgressiveLoadSectionProgress) {
192
+ onProgressiveLoadSectionProgress(directLoadSplatBuffer, loadComplete);
193
+ }
194
+ }
195
+
196
+ numBytesStreamed += directLoadSectionSizeBytes;
197
+ numBytesParsed += numBytesToParse;
198
+
199
+ if (numBytesLeftOver === 0) {
200
+ chunks = [];
201
+ } else {
202
+ let keepChunks = [];
203
+ let keepSize = 0;
204
+ for (let i = chunks.length - 1; i >= 0; i--) {
205
+ const chunk = chunks[i];
206
+ keepSize += chunk.sizeBytes;
207
+ keepChunks.unshift(chunk);
208
+ if (keepSize >= numBytesLeftOver) break;
209
+ }
210
+ chunks = keepChunks;
211
+ }
212
+ }
213
+ }
214
+
215
+ if (loadComplete) {
216
+ if (internalLoadType === InternalLoadType.DirectToSplatBuffer) {
217
+ loadPromise.resolve(directLoadSplatBuffer);
218
+ } else {
219
+ loadPromise.resolve(standardLoadUncompressedSplatArray);
220
+ }
221
+ }
222
+ }
223
+ }
224
+
225
+ if (onProgress) onProgress(percent, percentLabel, LoaderStatus.Downloading);
226
+ };
227
+
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);
233
+ if (internalLoadType === InternalLoadType.DownloadBeforeProcessing) {
234
+ 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
+ });
239
+ } else if (internalLoadType === InternalLoadType.DirectToSplatBuffer) {
240
+ return splatData;
241
+ } else {
242
+ return delayedExecute(() => {
243
+ return finalize$1(splatData, optimizeSplatData, minimumAlpha, compressionLevel,
244
+ sectionSize, sceneCenter, blockSize, bucketSize);
245
+ });
246
+ }
247
+ });
248
+ });
249
+ }
250
+
251
+ static loadFromFileData(plyFileData, minimumAlpha, compressionLevel, optimizeSplatData, outSphericalHarmonicsDegree = 0,
252
+ sectionSize, sceneCenter, blockSize, bucketSize) {
253
+ return delayedExecute(() => {
254
+ return PlyParser.parseToUncompressedSplatArray(plyFileData, outSphericalHarmonicsDegree);
255
+ })
256
+ .then((splatArray) => {
257
+ return finalize$1(splatArray, optimizeSplatData, minimumAlpha, compressionLevel,
258
+ sectionSize, sceneCenter, blockSize, bucketSize);
259
+ });
260
+ }
261
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * PlyParser
3
+ *
4
+ * Derived from @mkkellogg/gaussian-splats-3d (MIT License)
5
+ * https://github.com/mkkellogg/GaussianSplats3D
6
+ *
7
+ * Simplified for FLAME avatar - only supports INRIAV1 PLY format.
8
+ */
9
+
10
+ import { INRIAV1PlyParser } from './INRIAV1PlyParser.js';
11
+
12
+ export class PlyParser {
13
+
14
+ static parseToUncompressedSplatArray(plyBuffer, outSphericalHarmonicsDegree = 0) {
15
+ // FLAME avatars use INRIAV1 PLY format
16
+ return new INRIAV1PlyParser().parseToUncompressedSplatArray(plyBuffer, outSphericalHarmonicsDegree);
17
+ }
18
+
19
+ }
@@ -0,0 +1,311 @@
1
+ /**
2
+ * PlyParserUtils and related constants
3
+ *
4
+ * Derived from @mkkellogg/gaussian-splats-3d (MIT License)
5
+ * https://github.com/mkkellogg/GaussianSplats3D
6
+ *
7
+ * Simplified for FLAME avatar - only supports INRIAV1 format.
8
+ */
9
+
10
+ // Ply format types (simplified - FLAME uses INRIAV1)
11
+ export const PlyFormat = {
12
+ 'INRIAV1': 0
13
+ };
14
+
15
+ // Field size IDs
16
+ export const FieldSizeIdDouble = 0;
17
+ export const FieldSizeIdInt = 1;
18
+ export const FieldSizeIdUInt = 2;
19
+ export const FieldSizeIdFloat = 3;
20
+ export const FieldSizeIdShort = 4;
21
+ export const FieldSizeIdUShort = 5;
22
+ export const FieldSizeIdUChar = 6;
23
+
24
+ export const FieldSizeStringMap = {
25
+ 'double': FieldSizeIdDouble,
26
+ 'int': FieldSizeIdInt,
27
+ 'uint': FieldSizeIdUInt,
28
+ 'float': FieldSizeIdFloat,
29
+ 'short': FieldSizeIdShort,
30
+ 'ushort': FieldSizeIdUShort,
31
+ 'uchar': FieldSizeIdUChar,
32
+ };
33
+
34
+ export const FieldSize = {
35
+ [FieldSizeIdDouble]: 8,
36
+ [FieldSizeIdInt]: 4,
37
+ [FieldSizeIdUInt]: 4,
38
+ [FieldSizeIdFloat]: 4,
39
+ [FieldSizeIdShort]: 2,
40
+ [FieldSizeIdUShort]: 2,
41
+ [FieldSizeIdUChar]: 1,
42
+ };
43
+
44
+ export class PlyParserUtils {
45
+
46
+ static HeaderEndToken = 'end_header';
47
+
48
+ constructor() {
49
+ }
50
+
51
+ decodeSectionHeader(headerLines, fieldNameIdMap, headerStartLine = 0) {
52
+
53
+ const extractedLines = [];
54
+
55
+ let processingSection = false;
56
+ let headerEndLine = -1;
57
+ let vertexCount = 0;
58
+ let endOfHeader = false;
59
+ let sectionName = null;
60
+
61
+ const fieldIds = [];
62
+ const fieldTypes = [];
63
+ const allFieldNames = [];
64
+ const usedFieldNames = [];
65
+ const fieldTypesByName = {};
66
+
67
+ for (let i = headerStartLine; i < headerLines.length; i++) {
68
+ const line = headerLines[i].trim();
69
+ if (line.startsWith('element')) {
70
+ if (processingSection) {
71
+ headerEndLine--;
72
+ break;
73
+ } else {
74
+ processingSection = true;
75
+ headerStartLine = i;
76
+ headerEndLine = i;
77
+ const lineComponents = line.split(' ');
78
+ let validComponents = 0;
79
+ for (let lineComponent of lineComponents) {
80
+ const trimmedComponent = lineComponent.trim();
81
+ if (trimmedComponent.length > 0) {
82
+ validComponents++;
83
+ if (validComponents === 2) {
84
+ sectionName = trimmedComponent;
85
+ } else if (validComponents === 3) {
86
+ vertexCount = parseInt(trimmedComponent);
87
+ }
88
+ }
89
+ }
90
+ }
91
+ } else if (line.startsWith('property')) {
92
+ const fieldMatch = line.match(/(\w+)\s+(\w+)\s+(\w+)/);
93
+ if (fieldMatch) {
94
+ const fieldTypeStr = fieldMatch[2];
95
+ const fieldName = fieldMatch[3];
96
+ allFieldNames.push(fieldName);
97
+ const fieldId = fieldNameIdMap[fieldName];
98
+ fieldTypesByName[fieldName] = fieldTypeStr;
99
+ const fieldType = FieldSizeStringMap[fieldTypeStr];
100
+ if (fieldId !== undefined) {
101
+ usedFieldNames.push(fieldName);
102
+ fieldIds.push(fieldId);
103
+ fieldTypes[fieldId] = fieldType;
104
+ }
105
+ }
106
+ }
107
+ if (line === PlyParserUtils.HeaderEndToken) {
108
+ endOfHeader = true;
109
+ break;
110
+ }
111
+ if (processingSection) {
112
+ extractedLines.push(line);
113
+ headerEndLine++;
114
+ }
115
+ }
116
+
117
+ const fieldOffsets = [];
118
+ let bytesPerVertex = 0;
119
+ for (let fieldName of allFieldNames) {
120
+ const fieldType = fieldTypesByName[fieldName];
121
+ if (Object.hasOwn(fieldTypesByName, fieldName)) {
122
+ const fieldId = fieldNameIdMap[fieldName];
123
+ if (fieldId !== undefined) {
124
+ fieldOffsets[fieldId] = bytesPerVertex;
125
+ }
126
+ }
127
+ bytesPerVertex += FieldSize[FieldSizeStringMap[fieldType]];
128
+ }
129
+
130
+ const sphericalHarmonics = this.decodeSphericalHarmonicsFromSectionHeader(allFieldNames, fieldNameIdMap);
131
+
132
+ return {
133
+ 'headerLines': extractedLines,
134
+ 'headerStartLine': headerStartLine,
135
+ 'headerEndLine': headerEndLine,
136
+ 'fieldTypes': fieldTypes,
137
+ 'fieldIds': fieldIds,
138
+ 'fieldOffsets': fieldOffsets,
139
+ 'bytesPerVertex': bytesPerVertex,
140
+ 'vertexCount': vertexCount,
141
+ 'dataSizeBytes': bytesPerVertex * vertexCount,
142
+ 'endOfHeader': endOfHeader,
143
+ 'sectionName': sectionName,
144
+ 'sphericalHarmonicsDegree': sphericalHarmonics.degree,
145
+ 'sphericalHarmonicsCoefficientsPerChannel': sphericalHarmonics.coefficientsPerChannel,
146
+ 'sphericalHarmonicsDegree1Fields': sphericalHarmonics.degree1Fields,
147
+ 'sphericalHarmonicsDegree2Fields': sphericalHarmonics.degree2Fields
148
+ };
149
+ }
150
+
151
+ decodeSphericalHarmonicsFromSectionHeader(fieldNames, fieldNameIdMap) {
152
+ let sphericalHarmonicsFieldCount = 0;
153
+ let coefficientsPerChannel = 0;
154
+ for (let fieldName of fieldNames) {
155
+ if (fieldName.startsWith('f_rest')) sphericalHarmonicsFieldCount++;
156
+ }
157
+ coefficientsPerChannel = sphericalHarmonicsFieldCount / 3;
158
+ let degree = 0;
159
+ if (coefficientsPerChannel >= 3) degree = 1;
160
+ if (coefficientsPerChannel >= 8) degree = 2;
161
+
162
+ let degree1Fields = [];
163
+ let degree2Fields = [];
164
+
165
+ for (let rgb = 0; rgb < 3; rgb++) {
166
+ if (degree >= 1) {
167
+ for (let i = 0; i < 3; i++) {
168
+ degree1Fields.push(fieldNameIdMap['f_rest_' + (i + coefficientsPerChannel * rgb)]);
169
+ }
170
+ }
171
+ if (degree >= 2) {
172
+ for (let i = 0; i < 5; i++) {
173
+ degree2Fields.push(fieldNameIdMap['f_rest_' + (i + coefficientsPerChannel * rgb + 3)]);
174
+ }
175
+ }
176
+ }
177
+
178
+ return {
179
+ 'degree': degree,
180
+ 'coefficientsPerChannel': coefficientsPerChannel,
181
+ 'degree1Fields': degree1Fields,
182
+ 'degree2Fields': degree2Fields
183
+ };
184
+ }
185
+
186
+ static getHeaderSectionNames(headerLines) {
187
+ const sectionNames = [];
188
+ for (let headerLine of headerLines) {
189
+ if (headerLine.startsWith('element')) {
190
+ const lineComponents = headerLine.split(' ');
191
+ let validComponents = 0;
192
+ for (let lineComponent of lineComponents) {
193
+ const trimmedComponent = lineComponent.trim();
194
+ if (trimmedComponent.length > 0) {
195
+ validComponents++;
196
+ if (validComponents === 2) {
197
+ sectionNames.push(trimmedComponent);
198
+ }
199
+ }
200
+ }
201
+ }
202
+ }
203
+ return sectionNames;
204
+ }
205
+
206
+ static checkTextForEndHeader(endHeaderTestText) {
207
+ if (endHeaderTestText.includes(PlyParserUtils.HeaderEndToken)) {
208
+ return true;
209
+ }
210
+ return false;
211
+ }
212
+
213
+ static checkBufferForEndHeader(buffer, searchOfset, chunkSize, decoder) {
214
+ const endHeaderTestChunk = new Uint8Array(buffer, Math.max(0, searchOfset - chunkSize), chunkSize);
215
+ const endHeaderTestText = decoder.decode(endHeaderTestChunk);
216
+ return PlyParserUtils.checkTextForEndHeader(endHeaderTestText);
217
+ }
218
+
219
+ static extractHeaderFromBufferToText(plyBuffer) {
220
+ const decoder = new TextDecoder();
221
+ let headerOffset = 0;
222
+ let headerText = '';
223
+ const readChunkSize = 100;
224
+
225
+ while (true) {
226
+ if (headerOffset + readChunkSize >= plyBuffer.byteLength) {
227
+ throw new Error('End of file reached while searching for end of header');
228
+ }
229
+ const headerChunk = new Uint8Array(plyBuffer, headerOffset, readChunkSize);
230
+ headerText += decoder.decode(headerChunk);
231
+ headerOffset += readChunkSize;
232
+
233
+ if (PlyParserUtils.checkBufferForEndHeader(plyBuffer, headerOffset, readChunkSize * 2, decoder)) {
234
+ break;
235
+ }
236
+ }
237
+
238
+ return headerText;
239
+ }
240
+
241
+ readHeaderFromBuffer(plyBuffer) {
242
+ const decoder = new TextDecoder();
243
+ let headerOffset = 0;
244
+ let headerText = '';
245
+ const readChunkSize = 100;
246
+
247
+ while (true) {
248
+ if (headerOffset + readChunkSize >= plyBuffer.byteLength) {
249
+ throw new Error('End of file reached while searching for end of header');
250
+ }
251
+ const headerChunk = new Uint8Array(plyBuffer, headerOffset, readChunkSize);
252
+ headerText += decoder.decode(headerChunk);
253
+ headerOffset += readChunkSize;
254
+
255
+ if (PlyParserUtils.checkBufferForEndHeader(plyBuffer, headerOffset, readChunkSize * 2, decoder)) {
256
+ break;
257
+ }
258
+ }
259
+
260
+ return headerText;
261
+ }
262
+
263
+ static convertHeaderTextToLines(headerText) {
264
+ const headerLines = headerText.split('\n');
265
+ const prunedLines = [];
266
+ for (let i = 0; i < headerLines.length; i++) {
267
+ const line = headerLines[i].trim();
268
+ prunedLines.push(line);
269
+ if (line === PlyParserUtils.HeaderEndToken) {
270
+ break;
271
+ }
272
+ }
273
+ return prunedLines;
274
+ }
275
+
276
+ static determineHeaderFormatFromHeaderText(_headerText) {
277
+ // Simplified - FLAME avatars always use INRIAV1 format
278
+ return PlyFormat.INRIAV1;
279
+ }
280
+
281
+ static determineHeaderFormatFromPlyBuffer(plyBuffer) {
282
+ const headertText = PlyParserUtils.extractHeaderFromBufferToText(plyBuffer);
283
+ return PlyParserUtils.determineHeaderFormatFromHeaderText(headertText);
284
+ }
285
+
286
+ static readVertex(vertexData, header, row, dataOffset, fieldsToRead, rawVertex, normalize = true) {
287
+ const offset = row * header.bytesPerVertex + dataOffset;
288
+ const fieldOffsets = header.fieldOffsets;
289
+ const fieldTypes = header.fieldTypes;
290
+ for (let fieldId of fieldsToRead) {
291
+ const fieldType = fieldTypes[fieldId];
292
+ if (fieldType === FieldSizeIdFloat) {
293
+ rawVertex[fieldId] = vertexData.getFloat32(offset + fieldOffsets[fieldId], true);
294
+ } else if (fieldType === FieldSizeIdShort) {
295
+ rawVertex[fieldId] = vertexData.getInt16(offset + fieldOffsets[fieldId], true);
296
+ } else if (fieldType === FieldSizeIdUShort) {
297
+ rawVertex[fieldId] = vertexData.getUint16(offset + fieldOffsets[fieldId], true);
298
+ } else if (fieldType === FieldSizeIdInt) {
299
+ rawVertex[fieldId] = vertexData.getInt32(offset + fieldOffsets[fieldId], true);
300
+ } else if (fieldType === FieldSizeIdUInt) {
301
+ rawVertex[fieldId] = vertexData.getUint32(offset + fieldOffsets[fieldId], true);
302
+ } else if (fieldType === FieldSizeIdUChar) {
303
+ if (normalize) {
304
+ rawVertex[fieldId] = vertexData.getUint8(offset + fieldOffsets[fieldId]) / 255.0;
305
+ } else {
306
+ rawVertex[fieldId] = vertexData.getUint8(offset + fieldOffsets[fieldId]);
307
+ }
308
+ }
309
+ }
310
+ }
311
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * gsplat-flame-avatar - Loaders Module
3
+ * File format loaders for PLY (INRIAV1 format).
4
+ *
5
+ * Derived from @mkkellogg/gaussian-splats-3d (MIT License)
6
+ * Simplified for FLAME avatar usage.
7
+ */
8
+
9
+ export { PlyLoader } from './PlyLoader.js';
10
+ export { PlyParser } from './PlyParser.js';
11
+ export { PlyParserUtils, PlyFormat, FieldSize, FieldSizeStringMap } from './PlyParserUtils.js';
12
+ export { INRIAV1PlyParser } from './INRIAV1PlyParser.js';
13
+ export { DirectLoadError } from './DirectLoadError.js';