@mapcomponents/three 1.7.2
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/.babelrc +12 -0
- package/.storybook/main.ts +20 -0
- package/.storybook/preview.ts +0 -0
- package/README.md +54 -0
- package/cypress.config.ts +13 -0
- package/eslint.config.mjs +12 -0
- package/package.json +24 -0
- package/project.json +15 -0
- package/public/assets/3D/godzilla_simple.glb +0 -0
- package/public/assets/splats/output.splat +0 -0
- package/src/components/MlThreeModelLayer/MlThreeModelLayer.cy.tsx +63 -0
- package/src/components/MlThreeModelLayer/MlThreeModelLayer.meta.json +21 -0
- package/src/components/MlThreeModelLayer/MlThreeModelLayer.stories.tsx +161 -0
- package/src/components/MlThreeModelLayer/MlThreeModelLayer.tsx +153 -0
- package/src/components/MlThreeSplatLayer/MlThreeSplatLayer.cy.tsx +62 -0
- package/src/components/MlThreeSplatLayer/MlThreeSplatLayer.meta.json +21 -0
- package/src/components/MlThreeSplatLayer/MlThreeSplatLayer.stories.tsx +151 -0
- package/src/components/MlThreeSplatLayer/MlThreeSplatLayer.tsx +158 -0
- package/src/components/MlTransformControls.tsx +112 -0
- package/src/components/ThreeContext.tsx +26 -0
- package/src/components/ThreeObjectControls.tsx +197 -0
- package/src/components/ThreeProvider.tsx +149 -0
- package/src/cypress/support/commands.ts +1 -0
- package/src/cypress/support/component-index.html +13 -0
- package/src/cypress/support/component.ts +13 -0
- package/src/decorators/ThreejsContextDecorator.tsx +42 -0
- package/src/decorators/style.css +33 -0
- package/src/index.ts +7 -0
- package/src/lib/ThreejsSceneHelper.ts +250 -0
- package/src/lib/ThreejsSceneRenderer.ts +73 -0
- package/src/lib/ThreejsUtils.ts +62 -0
- package/src/lib/splats/GaussianSplattingMesh.ts +848 -0
- package/src/lib/splats/GaussianSplattingShaders.ts +266 -0
- package/src/lib/splats/loaders/PlySplatLoader.ts +537 -0
- package/src/lib/splats/loaders/SplatLoader.ts +52 -0
- package/src/lib/utils/coroutine.ts +121 -0
- package/tsconfig.json +21 -0
- package/tsconfig.lib.json +27 -0
- package/tsconfig.storybook.json +24 -0
- package/vite.config.ts +49 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of mapbox-3d-tiles.
|
|
3
|
+
* Copyright (c) 2024 Jianshun Yang
|
|
4
|
+
* Licensed under the MIT License.
|
|
5
|
+
* Source: https://github.com/yangjs6/mapbox-3d-tiles
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Vector3, FileLoader, Loader, Quaternion, MathUtils } from 'three';
|
|
9
|
+
import { GaussianSplattingMesh } from '../GaussianSplattingMesh';
|
|
10
|
+
import { createYieldingScheduler, runCoroutineAsync } from '../../utils/coroutine';
|
|
11
|
+
|
|
12
|
+
const unpackUnorm = (value: number, bits: number): number => {
|
|
13
|
+
const t = (1 << bits) - 1;
|
|
14
|
+
return (value & t) / t;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const unpack111011 = (value: number, result: Vector3): void => {
|
|
18
|
+
result.x = unpackUnorm(value >>> 21, 11);
|
|
19
|
+
result.y = unpackUnorm(value >>> 11, 10);
|
|
20
|
+
result.z = unpackUnorm(value, 11);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const unpack8888 = (value: number, result: Uint8ClampedArray): void => {
|
|
24
|
+
result[0] = unpackUnorm(value >>> 24, 8) * 255;
|
|
25
|
+
result[1] = unpackUnorm(value >>> 16, 8) * 255;
|
|
26
|
+
result[2] = unpackUnorm(value >>> 8, 8) * 255;
|
|
27
|
+
result[3] = unpackUnorm(value, 8) * 255;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const unpackRot = (value: number, result: Quaternion): void => {
|
|
31
|
+
const norm = 1.0 / (Math.sqrt(2) * 0.5);
|
|
32
|
+
const a = (unpackUnorm(value >>> 20, 10) - 0.5) * norm;
|
|
33
|
+
const b = (unpackUnorm(value >>> 10, 10) - 0.5) * norm;
|
|
34
|
+
const c = (unpackUnorm(value, 10) - 0.5) * norm;
|
|
35
|
+
const m = Math.sqrt(1.0 - (a * a + b * b + c * c));
|
|
36
|
+
|
|
37
|
+
switch (value >>> 30) {
|
|
38
|
+
case 0:
|
|
39
|
+
result.set(m, a, b, c);
|
|
40
|
+
break;
|
|
41
|
+
case 1:
|
|
42
|
+
result.set(a, m, b, c);
|
|
43
|
+
break;
|
|
44
|
+
case 2:
|
|
45
|
+
result.set(a, b, m, c);
|
|
46
|
+
break;
|
|
47
|
+
case 3:
|
|
48
|
+
result.set(a, b, c, m);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
interface CompressedPLYChunk {
|
|
54
|
+
min: Vector3;
|
|
55
|
+
max: Vector3;
|
|
56
|
+
minScale: Vector3;
|
|
57
|
+
maxScale: Vector3;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const enum PLYType {
|
|
61
|
+
FLOAT,
|
|
62
|
+
INT,
|
|
63
|
+
UINT,
|
|
64
|
+
DOUBLE,
|
|
65
|
+
UCHAR,
|
|
66
|
+
UNDEFINED,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const enum PLYValue {
|
|
70
|
+
MIN_X,
|
|
71
|
+
MIN_Y,
|
|
72
|
+
MIN_Z,
|
|
73
|
+
MAX_X,
|
|
74
|
+
MAX_Y,
|
|
75
|
+
MAX_Z,
|
|
76
|
+
MIN_SCALE_X,
|
|
77
|
+
MIN_SCALE_Y,
|
|
78
|
+
MIN_SCALE_Z,
|
|
79
|
+
MAX_SCALE_X,
|
|
80
|
+
MAX_SCALE_Y,
|
|
81
|
+
MAX_SCALE_Z,
|
|
82
|
+
PACKED_POSITION,
|
|
83
|
+
PACKED_ROTATION,
|
|
84
|
+
PACKED_SCALE,
|
|
85
|
+
PACKED_COLOR,
|
|
86
|
+
X,
|
|
87
|
+
Y,
|
|
88
|
+
Z,
|
|
89
|
+
SCALE_0,
|
|
90
|
+
SCALE_1,
|
|
91
|
+
SCALE_2,
|
|
92
|
+
DIFFUSE_RED,
|
|
93
|
+
DIFFUSE_GREEN,
|
|
94
|
+
DIFFUSE_BLUE,
|
|
95
|
+
OPACITY,
|
|
96
|
+
F_DC_0,
|
|
97
|
+
F_DC_1,
|
|
98
|
+
F_DC_2,
|
|
99
|
+
F_DC_3,
|
|
100
|
+
ROT_0,
|
|
101
|
+
ROT_1,
|
|
102
|
+
ROT_2,
|
|
103
|
+
ROT_3,
|
|
104
|
+
UNDEFINED,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface PlyProperty {
|
|
108
|
+
value: PLYValue;
|
|
109
|
+
type: PLYType;
|
|
110
|
+
offset: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface PLYHeader {
|
|
114
|
+
vertexCount: number;
|
|
115
|
+
chunkCount: number;
|
|
116
|
+
rowVertexLength: number;
|
|
117
|
+
rowChunkLength: number;
|
|
118
|
+
vertexProperties: PlyProperty[];
|
|
119
|
+
chunkProperties: PlyProperty[];
|
|
120
|
+
dataView: DataView;
|
|
121
|
+
buffer: ArrayBuffer;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Loader for .ply Gaussian Splatting files
|
|
126
|
+
*/
|
|
127
|
+
export class PlySplatLoader extends Loader {
|
|
128
|
+
private static readonly ROW_OUTPUT_LENGTH = 3 * 4 + 3 * 4 + 4 + 4;
|
|
129
|
+
private static readonly SH_C0 = 0.28209479177387814;
|
|
130
|
+
private static readonly PLY_CONVERSION_BATCH_SIZE = 32768;
|
|
131
|
+
|
|
132
|
+
override load(
|
|
133
|
+
url: string,
|
|
134
|
+
onLoad: (mesh: GaussianSplattingMesh) => void,
|
|
135
|
+
onProgress?: (event: ProgressEvent) => void,
|
|
136
|
+
onError?: (event: unknown) => void
|
|
137
|
+
): void {
|
|
138
|
+
const loader = new FileLoader(this.manager);
|
|
139
|
+
loader.setPath(this.path);
|
|
140
|
+
loader.setResponseType('arraybuffer');
|
|
141
|
+
loader.setRequestHeader(this.requestHeader);
|
|
142
|
+
loader.setWithCredentials(this.withCredentials);
|
|
143
|
+
|
|
144
|
+
loader.load(
|
|
145
|
+
url,
|
|
146
|
+
(buffer) => this.parse(buffer as ArrayBuffer, onLoad, onError),
|
|
147
|
+
onProgress,
|
|
148
|
+
onError
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
parse(
|
|
153
|
+
plyBuffer: ArrayBuffer,
|
|
154
|
+
onLoad: (mesh: GaussianSplattingMesh) => void,
|
|
155
|
+
onError?: (error: unknown) => void
|
|
156
|
+
): void {
|
|
157
|
+
PlySplatLoader.ConvertPLYToSplatAsync(plyBuffer)
|
|
158
|
+
.then((splatsData) => {
|
|
159
|
+
const mesh = new GaussianSplattingMesh();
|
|
160
|
+
return mesh.loadDataAsync(splatsData).then(() => onLoad(mesh));
|
|
161
|
+
})
|
|
162
|
+
.catch((error) => {
|
|
163
|
+
if (onError) {
|
|
164
|
+
onError(error);
|
|
165
|
+
} else {
|
|
166
|
+
console.error(error);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
static async ConvertPLYToSplatAsync(data: ArrayBuffer): Promise<ArrayBuffer> {
|
|
172
|
+
return runCoroutineAsync(
|
|
173
|
+
PlySplatLoader.ConvertPLYToSplat(data, true),
|
|
174
|
+
createYieldingScheduler()
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
static *ConvertPLYToSplat(
|
|
179
|
+
data: ArrayBuffer,
|
|
180
|
+
useCoroutine = false
|
|
181
|
+
): Generator<void, ArrayBuffer, void> {
|
|
182
|
+
const header = PlySplatLoader.ParseHeader(data);
|
|
183
|
+
if (!header) return data;
|
|
184
|
+
|
|
185
|
+
const offset = { value: 0 };
|
|
186
|
+
const compressedChunks = PlySplatLoader.getCompressedChunks(header, offset);
|
|
187
|
+
|
|
188
|
+
for (let i = 0; i < header.vertexCount; i++) {
|
|
189
|
+
PlySplatLoader.getSplat(header, i, compressedChunks, offset);
|
|
190
|
+
if (useCoroutine && i % PlySplatLoader.PLY_CONVERSION_BATCH_SIZE === 0) {
|
|
191
|
+
yield;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return header.buffer;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private static getCompressedChunks(
|
|
199
|
+
header: PLYHeader,
|
|
200
|
+
offset: { value: number }
|
|
201
|
+
): CompressedPLYChunk[] | null {
|
|
202
|
+
if (!header.chunkCount) return null;
|
|
203
|
+
|
|
204
|
+
const { dataView, chunkProperties, rowChunkLength, chunkCount } = header;
|
|
205
|
+
const chunks: CompressedPLYChunk[] = [];
|
|
206
|
+
|
|
207
|
+
for (let i = 0; i < chunkCount; i++) {
|
|
208
|
+
const chunk: CompressedPLYChunk = {
|
|
209
|
+
min: new Vector3(),
|
|
210
|
+
max: new Vector3(),
|
|
211
|
+
minScale: new Vector3(),
|
|
212
|
+
maxScale: new Vector3(),
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
for (const prop of chunkProperties) {
|
|
216
|
+
if (prop.type !== PLYType.FLOAT) continue;
|
|
217
|
+
const value = dataView.getFloat32(prop.offset + offset.value, true);
|
|
218
|
+
|
|
219
|
+
switch (prop.value) {
|
|
220
|
+
case PLYValue.MIN_X:
|
|
221
|
+
chunk.min.x = value;
|
|
222
|
+
break;
|
|
223
|
+
case PLYValue.MIN_Y:
|
|
224
|
+
chunk.min.y = value;
|
|
225
|
+
break;
|
|
226
|
+
case PLYValue.MIN_Z:
|
|
227
|
+
chunk.min.z = value;
|
|
228
|
+
break;
|
|
229
|
+
case PLYValue.MAX_X:
|
|
230
|
+
chunk.max.x = value;
|
|
231
|
+
break;
|
|
232
|
+
case PLYValue.MAX_Y:
|
|
233
|
+
chunk.max.y = value;
|
|
234
|
+
break;
|
|
235
|
+
case PLYValue.MAX_Z:
|
|
236
|
+
chunk.max.z = value;
|
|
237
|
+
break;
|
|
238
|
+
case PLYValue.MIN_SCALE_X:
|
|
239
|
+
chunk.minScale.x = value;
|
|
240
|
+
break;
|
|
241
|
+
case PLYValue.MIN_SCALE_Y:
|
|
242
|
+
chunk.minScale.y = value;
|
|
243
|
+
break;
|
|
244
|
+
case PLYValue.MIN_SCALE_Z:
|
|
245
|
+
chunk.minScale.z = value;
|
|
246
|
+
break;
|
|
247
|
+
case PLYValue.MAX_SCALE_X:
|
|
248
|
+
chunk.maxScale.x = value;
|
|
249
|
+
break;
|
|
250
|
+
case PLYValue.MAX_SCALE_Y:
|
|
251
|
+
chunk.maxScale.y = value;
|
|
252
|
+
break;
|
|
253
|
+
case PLYValue.MAX_SCALE_Z:
|
|
254
|
+
chunk.maxScale.z = value;
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
chunks.push(chunk);
|
|
260
|
+
offset.value += rowChunkLength;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return chunks;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private static getSplat(
|
|
267
|
+
header: PLYHeader,
|
|
268
|
+
index: number,
|
|
269
|
+
compressedChunks: CompressedPLYChunk[] | null,
|
|
270
|
+
offset: { value: number }
|
|
271
|
+
): void {
|
|
272
|
+
const q = new Quaternion();
|
|
273
|
+
const temp3 = new Vector3();
|
|
274
|
+
|
|
275
|
+
const { buffer, dataView, vertexProperties, rowVertexLength } = header;
|
|
276
|
+
const rowLen = PlySplatLoader.ROW_OUTPUT_LENGTH;
|
|
277
|
+
|
|
278
|
+
const position = new Float32Array(buffer, index * rowLen, 3);
|
|
279
|
+
const scale = new Float32Array(buffer, index * rowLen + 12, 3);
|
|
280
|
+
const rgba = new Uint8ClampedArray(buffer, index * rowLen + 24, 4);
|
|
281
|
+
const rot = new Uint8ClampedArray(buffer, index * rowLen + 28, 4);
|
|
282
|
+
|
|
283
|
+
const chunkIndex = index >> 8;
|
|
284
|
+
let r0 = 255,
|
|
285
|
+
r1 = 0,
|
|
286
|
+
r2 = 0,
|
|
287
|
+
r3 = 0;
|
|
288
|
+
|
|
289
|
+
for (const prop of vertexProperties) {
|
|
290
|
+
let value: number;
|
|
291
|
+
switch (prop.type) {
|
|
292
|
+
case PLYType.FLOAT:
|
|
293
|
+
value = dataView.getFloat32(offset.value + prop.offset, true);
|
|
294
|
+
break;
|
|
295
|
+
case PLYType.INT:
|
|
296
|
+
value = dataView.getInt32(offset.value + prop.offset, true);
|
|
297
|
+
break;
|
|
298
|
+
case PLYType.UINT:
|
|
299
|
+
value = dataView.getUint32(offset.value + prop.offset, true);
|
|
300
|
+
break;
|
|
301
|
+
case PLYType.DOUBLE:
|
|
302
|
+
value = dataView.getFloat64(offset.value + prop.offset, true);
|
|
303
|
+
break;
|
|
304
|
+
case PLYType.UCHAR:
|
|
305
|
+
value = dataView.getUint8(offset.value + prop.offset);
|
|
306
|
+
break;
|
|
307
|
+
default:
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const chunk = compressedChunks?.[chunkIndex];
|
|
312
|
+
|
|
313
|
+
switch (prop.value) {
|
|
314
|
+
case PLYValue.PACKED_POSITION:
|
|
315
|
+
unpack111011(value, temp3);
|
|
316
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
317
|
+
position[0] = MathUtils.lerp(chunk!.min.x, chunk!.max.x, temp3.x);
|
|
318
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
319
|
+
position[1] = -MathUtils.lerp(chunk!.min.y, chunk!.max.y, temp3.y);
|
|
320
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
321
|
+
position[2] = MathUtils.lerp(chunk!.min.z, chunk!.max.z, temp3.z);
|
|
322
|
+
break;
|
|
323
|
+
case PLYValue.PACKED_ROTATION:
|
|
324
|
+
unpackRot(value, q);
|
|
325
|
+
r0 = q.w;
|
|
326
|
+
r1 = q.z;
|
|
327
|
+
r2 = q.y;
|
|
328
|
+
r3 = q.x;
|
|
329
|
+
break;
|
|
330
|
+
case PLYValue.PACKED_SCALE:
|
|
331
|
+
unpack111011(value, temp3);
|
|
332
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
333
|
+
scale[0] = Math.exp(MathUtils.lerp(chunk!.minScale.x, chunk!.maxScale.x, temp3.x));
|
|
334
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
335
|
+
scale[1] = Math.exp(MathUtils.lerp(chunk!.minScale.y, chunk!.maxScale.y, temp3.y));
|
|
336
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
337
|
+
scale[2] = Math.exp(MathUtils.lerp(chunk!.minScale.z, chunk!.maxScale.z, temp3.z));
|
|
338
|
+
break;
|
|
339
|
+
case PLYValue.PACKED_COLOR:
|
|
340
|
+
unpack8888(value, rgba);
|
|
341
|
+
break;
|
|
342
|
+
case PLYValue.X:
|
|
343
|
+
position[0] = value;
|
|
344
|
+
break;
|
|
345
|
+
case PLYValue.Y:
|
|
346
|
+
position[1] = value;
|
|
347
|
+
break;
|
|
348
|
+
case PLYValue.Z:
|
|
349
|
+
position[2] = value;
|
|
350
|
+
break;
|
|
351
|
+
case PLYValue.SCALE_0:
|
|
352
|
+
scale[0] = Math.exp(value);
|
|
353
|
+
break;
|
|
354
|
+
case PLYValue.SCALE_1:
|
|
355
|
+
scale[1] = Math.exp(value);
|
|
356
|
+
break;
|
|
357
|
+
case PLYValue.SCALE_2:
|
|
358
|
+
scale[2] = Math.exp(value);
|
|
359
|
+
break;
|
|
360
|
+
case PLYValue.DIFFUSE_RED:
|
|
361
|
+
rgba[0] = value;
|
|
362
|
+
break;
|
|
363
|
+
case PLYValue.DIFFUSE_GREEN:
|
|
364
|
+
rgba[1] = value;
|
|
365
|
+
break;
|
|
366
|
+
case PLYValue.DIFFUSE_BLUE:
|
|
367
|
+
rgba[2] = value;
|
|
368
|
+
break;
|
|
369
|
+
case PLYValue.F_DC_0:
|
|
370
|
+
rgba[0] = (0.5 + PlySplatLoader.SH_C0 * value) * 255;
|
|
371
|
+
break;
|
|
372
|
+
case PLYValue.F_DC_1:
|
|
373
|
+
rgba[1] = (0.5 + PlySplatLoader.SH_C0 * value) * 255;
|
|
374
|
+
break;
|
|
375
|
+
case PLYValue.F_DC_2:
|
|
376
|
+
rgba[2] = (0.5 + PlySplatLoader.SH_C0 * value) * 255;
|
|
377
|
+
break;
|
|
378
|
+
case PLYValue.F_DC_3:
|
|
379
|
+
rgba[3] = (0.5 + PlySplatLoader.SH_C0 * value) * 255;
|
|
380
|
+
break;
|
|
381
|
+
case PLYValue.OPACITY:
|
|
382
|
+
rgba[3] = (1 / (1 + Math.exp(-value))) * 255;
|
|
383
|
+
break;
|
|
384
|
+
case PLYValue.ROT_0:
|
|
385
|
+
r0 = value;
|
|
386
|
+
break;
|
|
387
|
+
case PLYValue.ROT_1:
|
|
388
|
+
r1 = value;
|
|
389
|
+
break;
|
|
390
|
+
case PLYValue.ROT_2:
|
|
391
|
+
r2 = value;
|
|
392
|
+
break;
|
|
393
|
+
case PLYValue.ROT_3:
|
|
394
|
+
r3 = value;
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
q.set(r1, r2, r3, r0).normalize();
|
|
400
|
+
rot[0] = q.w * 128 + 128;
|
|
401
|
+
rot[1] = q.x * 128 + 128;
|
|
402
|
+
rot[2] = q.y * 128 + 128;
|
|
403
|
+
rot[3] = q.z * 128 + 128;
|
|
404
|
+
offset.value += rowVertexLength;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private static typeNameToEnum(name: string): PLYType {
|
|
408
|
+
switch (name) {
|
|
409
|
+
case 'float':
|
|
410
|
+
return PLYType.FLOAT;
|
|
411
|
+
case 'int':
|
|
412
|
+
return PLYType.INT;
|
|
413
|
+
case 'uint':
|
|
414
|
+
return PLYType.UINT;
|
|
415
|
+
case 'double':
|
|
416
|
+
return PLYType.DOUBLE;
|
|
417
|
+
case 'uchar':
|
|
418
|
+
return PLYType.UCHAR;
|
|
419
|
+
default:
|
|
420
|
+
return PLYType.UNDEFINED;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private static valueNameToEnum(name: string): PLYValue {
|
|
425
|
+
const map: Record<string, PLYValue> = {
|
|
426
|
+
min_x: PLYValue.MIN_X,
|
|
427
|
+
min_y: PLYValue.MIN_Y,
|
|
428
|
+
min_z: PLYValue.MIN_Z,
|
|
429
|
+
max_x: PLYValue.MAX_X,
|
|
430
|
+
max_y: PLYValue.MAX_Y,
|
|
431
|
+
max_z: PLYValue.MAX_Z,
|
|
432
|
+
min_scale_x: PLYValue.MIN_SCALE_X,
|
|
433
|
+
min_scale_y: PLYValue.MIN_SCALE_Y,
|
|
434
|
+
min_scale_z: PLYValue.MIN_SCALE_Z,
|
|
435
|
+
max_scale_x: PLYValue.MAX_SCALE_X,
|
|
436
|
+
max_scale_y: PLYValue.MAX_SCALE_Y,
|
|
437
|
+
max_scale_z: PLYValue.MAX_SCALE_Z,
|
|
438
|
+
packed_position: PLYValue.PACKED_POSITION,
|
|
439
|
+
packed_rotation: PLYValue.PACKED_ROTATION,
|
|
440
|
+
packed_scale: PLYValue.PACKED_SCALE,
|
|
441
|
+
packed_color: PLYValue.PACKED_COLOR,
|
|
442
|
+
x: PLYValue.X,
|
|
443
|
+
y: PLYValue.Y,
|
|
444
|
+
z: PLYValue.Z,
|
|
445
|
+
scale_0: PLYValue.SCALE_0,
|
|
446
|
+
scale_1: PLYValue.SCALE_1,
|
|
447
|
+
scale_2: PLYValue.SCALE_2,
|
|
448
|
+
diffuse_red: PLYValue.DIFFUSE_RED,
|
|
449
|
+
red: PLYValue.DIFFUSE_RED,
|
|
450
|
+
diffuse_green: PLYValue.DIFFUSE_GREEN,
|
|
451
|
+
green: PLYValue.DIFFUSE_GREEN,
|
|
452
|
+
diffuse_blue: PLYValue.DIFFUSE_BLUE,
|
|
453
|
+
blue: PLYValue.DIFFUSE_BLUE,
|
|
454
|
+
f_dc_0: PLYValue.F_DC_0,
|
|
455
|
+
f_dc_1: PLYValue.F_DC_1,
|
|
456
|
+
f_dc_2: PLYValue.F_DC_2,
|
|
457
|
+
f_dc_3: PLYValue.F_DC_3,
|
|
458
|
+
opacity: PLYValue.OPACITY,
|
|
459
|
+
rot_0: PLYValue.ROT_0,
|
|
460
|
+
rot_1: PLYValue.ROT_1,
|
|
461
|
+
rot_2: PLYValue.ROT_2,
|
|
462
|
+
rot_3: PLYValue.ROT_3,
|
|
463
|
+
};
|
|
464
|
+
return map[name] ?? PLYValue.UNDEFINED;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
static ParseHeader(data: ArrayBuffer): PLYHeader | null {
|
|
468
|
+
const ubuf = new Uint8Array(data);
|
|
469
|
+
const headerText = new TextDecoder().decode(ubuf.slice(0, 1024 * 10));
|
|
470
|
+
const headerEnd = 'end_header\n';
|
|
471
|
+
const headerEndIndex = headerText.indexOf(headerEnd);
|
|
472
|
+
|
|
473
|
+
if (headerEndIndex < 0) return null;
|
|
474
|
+
|
|
475
|
+
const vertexMatch = /element vertex (\d+)\n/.exec(headerText);
|
|
476
|
+
if (!vertexMatch) return null;
|
|
477
|
+
const vertexCount = parseInt(vertexMatch[1]);
|
|
478
|
+
|
|
479
|
+
const chunkMatch = /element chunk (\d+)\n/.exec(headerText);
|
|
480
|
+
const chunkCount = chunkMatch ? parseInt(chunkMatch[1]) : 0;
|
|
481
|
+
|
|
482
|
+
const offsets: Record<string, number> = {
|
|
483
|
+
double: 8,
|
|
484
|
+
int: 4,
|
|
485
|
+
uint: 4,
|
|
486
|
+
float: 4,
|
|
487
|
+
short: 2,
|
|
488
|
+
ushort: 2,
|
|
489
|
+
uchar: 1,
|
|
490
|
+
list: 0,
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const enum ElementMode {
|
|
494
|
+
Vertex,
|
|
495
|
+
Chunk,
|
|
496
|
+
}
|
|
497
|
+
let mode = ElementMode.Chunk;
|
|
498
|
+
let rowVertexOffset = 0;
|
|
499
|
+
let rowChunkOffset = 0;
|
|
500
|
+
|
|
501
|
+
const vertexProperties: PlyProperty[] = [];
|
|
502
|
+
const chunkProperties: PlyProperty[] = [];
|
|
503
|
+
|
|
504
|
+
for (const line of headerText.slice(0, headerEndIndex).split('\n')) {
|
|
505
|
+
if (line.startsWith('property ')) {
|
|
506
|
+
const [, typeName, name] = line.split(' ');
|
|
507
|
+
const property: PlyProperty = {
|
|
508
|
+
value: PlySplatLoader.valueNameToEnum(name),
|
|
509
|
+
type: PlySplatLoader.typeNameToEnum(typeName),
|
|
510
|
+
offset: mode === ElementMode.Chunk ? rowChunkOffset : rowVertexOffset,
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
if (mode === ElementMode.Chunk) {
|
|
514
|
+
chunkProperties.push(property);
|
|
515
|
+
rowChunkOffset += offsets[typeName] ?? 0;
|
|
516
|
+
} else {
|
|
517
|
+
vertexProperties.push(property);
|
|
518
|
+
rowVertexOffset += offsets[typeName] ?? 0;
|
|
519
|
+
}
|
|
520
|
+
} else if (line.startsWith('element ')) {
|
|
521
|
+
const [, type] = line.split(' ');
|
|
522
|
+
mode = type === 'chunk' ? ElementMode.Chunk : ElementMode.Vertex;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
vertexCount,
|
|
528
|
+
chunkCount,
|
|
529
|
+
rowVertexLength: rowVertexOffset,
|
|
530
|
+
rowChunkLength: rowChunkOffset,
|
|
531
|
+
vertexProperties,
|
|
532
|
+
chunkProperties,
|
|
533
|
+
dataView: new DataView(data, headerEndIndex + headerEnd.length),
|
|
534
|
+
buffer: new ArrayBuffer(PlySplatLoader.ROW_OUTPUT_LENGTH * vertexCount),
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of mapbox-3d-tiles.
|
|
3
|
+
* Copyright (c) 2024 Jianshun Yang
|
|
4
|
+
* Licensed under the MIT License.
|
|
5
|
+
* Source: https://github.com/yangjs6/mapbox-3d-tiles
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { FileLoader, Loader } from 'three';
|
|
9
|
+
import { GaussianSplattingMesh } from '../GaussianSplattingMesh';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Loader for .splat Gaussian Splatting files
|
|
13
|
+
*/
|
|
14
|
+
export class SplatLoader extends Loader {
|
|
15
|
+
override load(
|
|
16
|
+
url: string,
|
|
17
|
+
onLoad: (mesh: GaussianSplattingMesh) => void,
|
|
18
|
+
onProgress?: (event: ProgressEvent) => void,
|
|
19
|
+
onError?: (event: unknown) => void
|
|
20
|
+
): void {
|
|
21
|
+
const loader = new FileLoader(this.manager);
|
|
22
|
+
loader.setPath(this.path);
|
|
23
|
+
loader.setResponseType('arraybuffer');
|
|
24
|
+
loader.setRequestHeader(this.requestHeader);
|
|
25
|
+
loader.setWithCredentials(this.withCredentials);
|
|
26
|
+
|
|
27
|
+
loader.load(
|
|
28
|
+
url,
|
|
29
|
+
(buffer) => this.parse(buffer as ArrayBuffer, onLoad, onError),
|
|
30
|
+
onProgress,
|
|
31
|
+
onError
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
parse(
|
|
36
|
+
buffer: ArrayBuffer,
|
|
37
|
+
onLoad: (mesh: GaussianSplattingMesh) => void,
|
|
38
|
+
onError?: (error: unknown) => void
|
|
39
|
+
): void {
|
|
40
|
+
const mesh = new GaussianSplattingMesh();
|
|
41
|
+
mesh
|
|
42
|
+
.loadDataAsync(buffer)
|
|
43
|
+
.then(() => onLoad(mesh))
|
|
44
|
+
.catch((error) => {
|
|
45
|
+
if (onError) {
|
|
46
|
+
onError(error);
|
|
47
|
+
} else {
|
|
48
|
+
console.error(error);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derived from mapbox-3d-tiles by Jianshun Yang (MIT License)
|
|
3
|
+
* https://github.com/yangjs6/mapbox-3d-tiles
|
|
4
|
+
*
|
|
5
|
+
* Coroutine utilities for non-blocking async operations using generators.
|
|
6
|
+
* Allows long-running tasks to yield control periodically to prevent UI freezing.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type Coroutine<T> = Iterator<void, T, void> & IterableIterator<void>;
|
|
10
|
+
export type AsyncCoroutine<T> = Iterator<void | Promise<void>, T, void> &
|
|
11
|
+
IterableIterator<void | Promise<void>>;
|
|
12
|
+
export type CoroutineStep<T> = IteratorResult<void, T>;
|
|
13
|
+
export type CoroutineScheduler<T> = (
|
|
14
|
+
coroutine: AsyncCoroutine<T>,
|
|
15
|
+
onStep: (result: CoroutineStep<T>) => void,
|
|
16
|
+
onError: (error: any) => void
|
|
17
|
+
) => void;
|
|
18
|
+
|
|
19
|
+
export function inlineScheduler<T>(
|
|
20
|
+
coroutine: AsyncCoroutine<T>,
|
|
21
|
+
onStep: (result: CoroutineStep<T>) => void,
|
|
22
|
+
onError: (error: any) => void
|
|
23
|
+
): void {
|
|
24
|
+
try {
|
|
25
|
+
const step = coroutine.next();
|
|
26
|
+
|
|
27
|
+
if (step.done || !step.value) {
|
|
28
|
+
onStep(step as CoroutineStep<T>);
|
|
29
|
+
} else {
|
|
30
|
+
(step.value as Promise<void>).then(
|
|
31
|
+
() => onStep({ done: step.done, value: undefined } as CoroutineStep<T>),
|
|
32
|
+
onError
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
onError(error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createYieldingScheduler<T>(yieldAfterMs = 25): CoroutineScheduler<T> {
|
|
41
|
+
let startTime: number | undefined;
|
|
42
|
+
|
|
43
|
+
return (coroutine, onStep, onError) => {
|
|
44
|
+
const now = performance.now();
|
|
45
|
+
|
|
46
|
+
if (startTime === undefined || now - startTime > yieldAfterMs) {
|
|
47
|
+
startTime = now;
|
|
48
|
+
setTimeout(() => inlineScheduler(coroutine, onStep, onError), 0);
|
|
49
|
+
} else {
|
|
50
|
+
inlineScheduler(coroutine, onStep, onError);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function runCoroutine<T>(
|
|
56
|
+
coroutine: AsyncCoroutine<T>,
|
|
57
|
+
scheduler: CoroutineScheduler<T>,
|
|
58
|
+
onSuccess: (result: T) => void,
|
|
59
|
+
onError: (error: any) => void,
|
|
60
|
+
abortSignal?: AbortSignal
|
|
61
|
+
): void {
|
|
62
|
+
const resume = () => {
|
|
63
|
+
let shouldContinue: boolean | undefined;
|
|
64
|
+
|
|
65
|
+
const onStep = (result: CoroutineStep<T>) => {
|
|
66
|
+
if (result.done) {
|
|
67
|
+
onSuccess(result.value);
|
|
68
|
+
} else if (shouldContinue === undefined) {
|
|
69
|
+
shouldContinue = true;
|
|
70
|
+
} else {
|
|
71
|
+
resume();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
do {
|
|
76
|
+
shouldContinue = undefined;
|
|
77
|
+
|
|
78
|
+
if (abortSignal?.aborted) {
|
|
79
|
+
onError(new DOMException('Aborted', 'AbortError'));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
scheduler(coroutine, onStep, onError);
|
|
84
|
+
|
|
85
|
+
if (shouldContinue === undefined) {
|
|
86
|
+
shouldContinue = false;
|
|
87
|
+
}
|
|
88
|
+
} while (shouldContinue);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
resume();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function runCoroutineSync<T>(coroutine: Coroutine<T>, abortSignal?: AbortSignal): T {
|
|
95
|
+
let result: T | undefined;
|
|
96
|
+
|
|
97
|
+
runCoroutine(
|
|
98
|
+
coroutine,
|
|
99
|
+
inlineScheduler,
|
|
100
|
+
(r) => {
|
|
101
|
+
result = r;
|
|
102
|
+
},
|
|
103
|
+
(e) => {
|
|
104
|
+
throw e;
|
|
105
|
+
},
|
|
106
|
+
abortSignal
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
110
|
+
return result!;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function runCoroutineAsync<T>(
|
|
114
|
+
coroutine: AsyncCoroutine<T>,
|
|
115
|
+
scheduler: CoroutineScheduler<T>,
|
|
116
|
+
abortSignal?: AbortSignal
|
|
117
|
+
): Promise<T> {
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
runCoroutine(coroutine, scheduler, resolve, reject, abortSignal);
|
|
120
|
+
});
|
|
121
|
+
}
|