@loaders.gl/splats 5.0.0-alpha.1
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/LICENSE +41 -0
- package/README.md +3 -0
- package/dist/bundled.d.ts +2 -0
- package/dist/bundled.d.ts.map +1 -0
- package/dist/bundled.js +5 -0
- package/dist/bundled.js.map +1 -0
- package/dist/dist.dev.js +20876 -0
- package/dist/dist.min.js +18 -0
- package/dist/index.cjs +2046 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/ksplat-loader-types.d.ts +42 -0
- package/dist/ksplat-loader-types.d.ts.map +1 -0
- package/dist/ksplat-loader-types.js +26 -0
- package/dist/ksplat-loader-types.js.map +1 -0
- package/dist/ksplat-loader.d.ts +22 -0
- package/dist/ksplat-loader.d.ts.map +1 -0
- package/dist/ksplat-loader.js +13 -0
- package/dist/ksplat-loader.js.map +1 -0
- package/dist/lib/parse-ksplat.d.ts +7 -0
- package/dist/lib/parse-ksplat.d.ts.map +1 -0
- package/dist/lib/parse-ksplat.js +312 -0
- package/dist/lib/parse-ksplat.js.map +1 -0
- package/dist/lib/parse-rad-chunk.d.ts +19 -0
- package/dist/lib/parse-rad-chunk.d.ts.map +1 -0
- package/dist/lib/parse-rad-chunk.js +514 -0
- package/dist/lib/parse-rad-chunk.js.map +1 -0
- package/dist/lib/parse-rad.d.ts +120 -0
- package/dist/lib/parse-rad.d.ts.map +1 -0
- package/dist/lib/parse-rad.js +235 -0
- package/dist/lib/parse-rad.js.map +1 -0
- package/dist/lib/parse-splat.d.ts +7 -0
- package/dist/lib/parse-splat.d.ts.map +1 -0
- package/dist/lib/parse-splat.js +58 -0
- package/dist/lib/parse-splat.js.map +1 -0
- package/dist/lib/parse-spz.d.ts +7 -0
- package/dist/lib/parse-spz.d.ts.map +1 -0
- package/dist/lib/parse-spz.js +268 -0
- package/dist/lib/parse-spz.js.map +1 -0
- package/dist/lib/splat-utils.d.ts +12 -0
- package/dist/lib/splat-utils.d.ts.map +1 -0
- package/dist/lib/splat-utils.js +38 -0
- package/dist/lib/splat-utils.js.map +1 -0
- package/dist/lib/splats-arrow-table.d.ts +5 -0
- package/dist/lib/splats-arrow-table.d.ts.map +1 -0
- package/dist/lib/splats-arrow-table.js +132 -0
- package/dist/lib/splats-arrow-table.js.map +1 -0
- package/dist/rad-loader-types.d.ts +36 -0
- package/dist/rad-loader-types.d.ts.map +1 -0
- package/dist/rad-loader-types.js +22 -0
- package/dist/rad-loader-types.js.map +1 -0
- package/dist/rad-loader.d.ts +21 -0
- package/dist/rad-loader.d.ts.map +1 -0
- package/dist/rad-loader.js +15 -0
- package/dist/rad-loader.js.map +1 -0
- package/dist/rad-source-loader.d.ts +115 -0
- package/dist/rad-source-loader.d.ts.map +1 -0
- package/dist/rad-source-loader.js +338 -0
- package/dist/rad-source-loader.js.map +1 -0
- package/dist/splat-loader-types.d.ts +42 -0
- package/dist/splat-loader-types.d.ts.map +1 -0
- package/dist/splat-loader-types.js +26 -0
- package/dist/splat-loader-types.js.map +1 -0
- package/dist/splat-loader.d.ts +22 -0
- package/dist/splat-loader.d.ts.map +1 -0
- package/dist/splat-loader.js +13 -0
- package/dist/splat-loader.js.map +1 -0
- package/dist/splats-format.d.ts +43 -0
- package/dist/splats-format.d.ts.map +1 -0
- package/dist/splats-format.js +46 -0
- package/dist/splats-format.js.map +1 -0
- package/dist/spz-loader-types.d.ts +44 -0
- package/dist/spz-loader-types.d.ts.map +1 -0
- package/dist/spz-loader-types.js +26 -0
- package/dist/spz-loader-types.js.map +1 -0
- package/dist/spz-loader.d.ts +23 -0
- package/dist/spz-loader.d.ts.map +1 -0
- package/dist/spz-loader.js +12 -0
- package/dist/spz-loader.js.map +1 -0
- package/dist/types.d.ts +34 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/unbundled.d.ts +2 -0
- package/dist/unbundled.d.ts.map +1 -0
- package/dist/unbundled.js +5 -0
- package/dist/unbundled.js.map +1 -0
- package/package.json +85 -0
- package/src/bundled.ts +5 -0
- package/src/index.ts +26 -0
- package/src/ksplat-loader-types.ts +32 -0
- package/src/ksplat-loader.ts +18 -0
- package/src/lib/parse-ksplat.ts +538 -0
- package/src/lib/parse-rad-chunk.ts +721 -0
- package/src/lib/parse-rad.ts +439 -0
- package/src/lib/parse-splat.ts +77 -0
- package/src/lib/parse-spz.ts +368 -0
- package/src/lib/splat-utils.ts +49 -0
- package/src/lib/splats-arrow-table.ts +188 -0
- package/src/rad-loader-types.ts +27 -0
- package/src/rad-loader.ts +24 -0
- package/src/rad-source-loader.ts +504 -0
- package/src/splat-loader-types.ts +32 -0
- package/src/splat-loader.ts +18 -0
- package/src/splats-format.ts +51 -0
- package/src/spz-loader-types.ts +32 -0
- package/src/spz-loader.ts +18 -0
- package/src/types.ts +39 -0
- package/src/unbundled.ts +5 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {ZstdCompression} from '@loaders.gl/compression';
|
|
6
|
+
import type {MeshArrowTable} from '@loaders.gl/schema';
|
|
7
|
+
import type {GaussianSplats, SplatsLoaderOptions} from '../types';
|
|
8
|
+
import {makeGaussianSplatsArrowTable} from './splats-arrow-table';
|
|
9
|
+
import {normalizeQuaternion} from './splat-utils';
|
|
10
|
+
|
|
11
|
+
const SPZ_HEADER_BYTE_LENGTH = 32;
|
|
12
|
+
const SPZ_MAGIC = 0x5053474e;
|
|
13
|
+
const SPZ_VERSION = 4;
|
|
14
|
+
const SPZ_EXTENSION_FLAG = 0x2;
|
|
15
|
+
const SPZ_COLOR_SCALE = 0.15;
|
|
16
|
+
const SPZ_ROTATION_COMPONENT_SCALE = Math.SQRT1_2;
|
|
17
|
+
const SPZ_ROTATION_COMPONENT_MASK = (1 << 9) - 1;
|
|
18
|
+
|
|
19
|
+
type SPZHeader = {
|
|
20
|
+
magic: number;
|
|
21
|
+
version: number;
|
|
22
|
+
numPoints: number;
|
|
23
|
+
shDegree: number;
|
|
24
|
+
fractionalBits: number;
|
|
25
|
+
flags: number;
|
|
26
|
+
numStreams: number;
|
|
27
|
+
tocByteOffset: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type SPZStreamInfo = {
|
|
31
|
+
compressedSize: number;
|
|
32
|
+
uncompressedSize: number;
|
|
33
|
+
compressedOffset: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Parses a Niantic Spatial `.spz` ArrayBuffer into a Mesh Arrow table. */
|
|
37
|
+
export async function parseSPZ(
|
|
38
|
+
data: ArrayBuffer,
|
|
39
|
+
options?: SplatsLoaderOptions
|
|
40
|
+
): Promise<MeshArrowTable> {
|
|
41
|
+
return makeGaussianSplatsArrowTable(await parseSPZToGaussianSplats(data, options));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Parses a Niantic Spatial `.spz` ArrayBuffer into decoded Gaussian splat values. */
|
|
45
|
+
export async function parseSPZToGaussianSplats(
|
|
46
|
+
data: ArrayBuffer,
|
|
47
|
+
options?: SplatsLoaderOptions
|
|
48
|
+
): Promise<GaussianSplats> {
|
|
49
|
+
const header = parseSPZHeader(data);
|
|
50
|
+
const streamInfos = parseSPZStreamInfos(data, header, getSPZStreamByteLengths(header));
|
|
51
|
+
const compressedStreams = streamInfos.map(streamInfo =>
|
|
52
|
+
data.slice(streamInfo.compressedOffset, streamInfo.compressedOffset + streamInfo.compressedSize)
|
|
53
|
+
);
|
|
54
|
+
const compression = new ZstdCompression({modules: options?.modules});
|
|
55
|
+
const streams = await Promise.all(
|
|
56
|
+
compressedStreams.map((compressedStream, streamIndex) =>
|
|
57
|
+
compression.decompress(compressedStream, streamInfos[streamIndex].uncompressedSize)
|
|
58
|
+
)
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
validateSPZStreamLengths(streams, streamInfos);
|
|
62
|
+
return decodeSPZStreams(header, streams, data);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Parses and validates the SPZ v4 plaintext header. */
|
|
66
|
+
function parseSPZHeader(data: ArrayBuffer): SPZHeader {
|
|
67
|
+
if (data.byteLength < SPZ_HEADER_BYTE_LENGTH) {
|
|
68
|
+
throw new Error(`SPZLoader: file must contain a ${SPZ_HEADER_BYTE_LENGTH}-byte header.`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const dataView = new DataView(data);
|
|
72
|
+
const header: SPZHeader = {
|
|
73
|
+
magic: dataView.getUint32(0, true),
|
|
74
|
+
version: dataView.getUint32(4, true),
|
|
75
|
+
numPoints: dataView.getUint32(8, true),
|
|
76
|
+
shDegree: dataView.getUint8(12),
|
|
77
|
+
fractionalBits: dataView.getUint8(13),
|
|
78
|
+
flags: dataView.getUint8(14),
|
|
79
|
+
numStreams: dataView.getUint8(15),
|
|
80
|
+
tocByteOffset: dataView.getUint32(16, true)
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (header.magic !== SPZ_MAGIC) {
|
|
84
|
+
throw new Error('SPZLoader: NGSP magic header not found.');
|
|
85
|
+
}
|
|
86
|
+
if (header.version !== SPZ_VERSION) {
|
|
87
|
+
throw new Error(`SPZLoader: version ${header.version} is not supported.`);
|
|
88
|
+
}
|
|
89
|
+
if (header.shDegree > 4) {
|
|
90
|
+
throw new Error(`SPZLoader: spherical harmonics degree ${header.shDegree} is not supported.`);
|
|
91
|
+
}
|
|
92
|
+
if (header.tocByteOffset < SPZ_HEADER_BYTE_LENGTH || header.tocByteOffset > data.byteLength) {
|
|
93
|
+
throw new Error('SPZLoader: invalid table of contents byte offset.');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return header;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Parses the SPZ stream table of contents and validates expected stream sizes. */
|
|
100
|
+
function parseSPZStreamInfos(
|
|
101
|
+
data: ArrayBuffer,
|
|
102
|
+
header: SPZHeader,
|
|
103
|
+
expectedUncompressedSizes: number[]
|
|
104
|
+
): SPZStreamInfo[] {
|
|
105
|
+
if (header.numStreams !== expectedUncompressedSizes.length) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`SPZLoader: expected ${expectedUncompressedSizes.length} streams, found ${header.numStreams}.`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const tocByteLength = header.numStreams * 16;
|
|
112
|
+
const tocEnd = header.tocByteOffset + tocByteLength;
|
|
113
|
+
if (tocEnd > data.byteLength) {
|
|
114
|
+
throw new Error('SPZLoader: table of contents exceeds file byte length.');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const dataView = new DataView(data);
|
|
118
|
+
let compressedOffset = tocEnd;
|
|
119
|
+
const streamInfos: SPZStreamInfo[] = [];
|
|
120
|
+
for (let streamIndex = 0; streamIndex < header.numStreams; streamIndex++) {
|
|
121
|
+
const tocOffset = header.tocByteOffset + streamIndex * 16;
|
|
122
|
+
const compressedSize = readSafeUint64(dataView, tocOffset);
|
|
123
|
+
const uncompressedSize = readSafeUint64(dataView, tocOffset + 8);
|
|
124
|
+
const expectedUncompressedSize = expectedUncompressedSizes[streamIndex];
|
|
125
|
+
|
|
126
|
+
if (uncompressedSize !== expectedUncompressedSize) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`SPZLoader: stream ${streamIndex} has ${uncompressedSize} uncompressed bytes, expected ${expectedUncompressedSize}.`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (compressedOffset + compressedSize > data.byteLength) {
|
|
132
|
+
throw new Error(`SPZLoader: stream ${streamIndex} exceeds file byte length.`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
streamInfos.push({compressedSize, uncompressedSize, compressedOffset});
|
|
136
|
+
compressedOffset += compressedSize;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (compressedOffset !== data.byteLength) {
|
|
140
|
+
throw new Error('SPZLoader: compressed stream byte lengths do not match file byte length.');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return streamInfos;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Converts a little-endian uint64 to a safe JavaScript number. */
|
|
147
|
+
function readSafeUint64(dataView: DataView, byteOffset: number): number {
|
|
148
|
+
const value = dataView.getBigUint64(byteOffset, true);
|
|
149
|
+
if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
150
|
+
throw new Error('SPZLoader: stream byte length exceeds Number.MAX_SAFE_INTEGER.');
|
|
151
|
+
}
|
|
152
|
+
return Number(value);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Returns expected SPZ v4 stream byte lengths, skipping zero-length SH data. */
|
|
156
|
+
function getSPZStreamByteLengths(header: SPZHeader): number[] {
|
|
157
|
+
return [
|
|
158
|
+
header.numPoints * 9,
|
|
159
|
+
header.numPoints,
|
|
160
|
+
header.numPoints * 3,
|
|
161
|
+
header.numPoints * 3,
|
|
162
|
+
header.numPoints * 4,
|
|
163
|
+
header.numPoints * getSphericalHarmonicsComponentCount(header.shDegree)
|
|
164
|
+
].filter(byteLength => byteLength > 0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Validates that decompressed stream lengths match the TOC. */
|
|
168
|
+
function validateSPZStreamLengths(streams: ArrayBuffer[], streamInfos: SPZStreamInfo[]): void {
|
|
169
|
+
for (let streamIndex = 0; streamIndex < streams.length; streamIndex++) {
|
|
170
|
+
if (streams[streamIndex].byteLength !== streamInfos[streamIndex].uncompressedSize) {
|
|
171
|
+
throw new Error(`SPZLoader: decompressed stream ${streamIndex} has unexpected byte length.`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Decodes decompressed SPZ streams into shared Gaussian splat arrays. */
|
|
177
|
+
function decodeSPZStreams(
|
|
178
|
+
header: SPZHeader,
|
|
179
|
+
streams: ArrayBuffer[],
|
|
180
|
+
sourceData: ArrayBuffer
|
|
181
|
+
): GaussianSplats {
|
|
182
|
+
const splatCount = header.numPoints;
|
|
183
|
+
const positions = new Float32Array(splatCount * 3);
|
|
184
|
+
const scales = new Float32Array(splatCount * 3);
|
|
185
|
+
const rotations = new Float32Array(splatCount * 4);
|
|
186
|
+
const colors = new Uint8Array(splatCount * 3);
|
|
187
|
+
const sphericalHarmonicDcs = new Float32Array(splatCount * 3);
|
|
188
|
+
const opacities = new Float32Array(splatCount);
|
|
189
|
+
const sphericalHarmonicsComponentCount = getSphericalHarmonicsComponentCount(header.shDegree);
|
|
190
|
+
const sphericalHarmonics = sphericalHarmonicsComponentCount
|
|
191
|
+
? new Float32Array(splatCount * sphericalHarmonicsComponentCount)
|
|
192
|
+
: undefined;
|
|
193
|
+
const [
|
|
194
|
+
positionStream,
|
|
195
|
+
alphaStream,
|
|
196
|
+
colorStream,
|
|
197
|
+
scaleStream,
|
|
198
|
+
rotationStream,
|
|
199
|
+
sphericalHarmonicsStream
|
|
200
|
+
] = streams.map(stream => new Uint8Array(stream));
|
|
201
|
+
|
|
202
|
+
decodeSPZPositions(positionStream, positions, header.fractionalBits);
|
|
203
|
+
decodeSPZScales(scaleStream, scales);
|
|
204
|
+
decodeSPZRotations(rotationStream, rotations);
|
|
205
|
+
decodeSPZColors(colorStream, colors, sphericalHarmonicDcs);
|
|
206
|
+
decodeSPZOpacities(alphaStream, opacities);
|
|
207
|
+
if (sphericalHarmonics && sphericalHarmonicsStream) {
|
|
208
|
+
decodeSPZSphericalHarmonics(sphericalHarmonicsStream, sphericalHarmonics);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
format: 'spz',
|
|
213
|
+
splatCount,
|
|
214
|
+
positions,
|
|
215
|
+
scales,
|
|
216
|
+
rotations,
|
|
217
|
+
colors,
|
|
218
|
+
sphericalHarmonicDcs,
|
|
219
|
+
opacities,
|
|
220
|
+
sphericalHarmonics,
|
|
221
|
+
sphericalHarmonicsComponentCount,
|
|
222
|
+
loaderData: {
|
|
223
|
+
format: 'spz',
|
|
224
|
+
...header,
|
|
225
|
+
antialiased: Boolean(header.flags & 0x1),
|
|
226
|
+
extensionByteLength: getSPZExtensionByteLength(header),
|
|
227
|
+
extensionBytes: getSPZExtensionBytes(sourceData, header)
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Decodes 24-bit signed fixed-point SPZ positions. */
|
|
233
|
+
function decodeSPZPositions(
|
|
234
|
+
positionStream: Uint8Array,
|
|
235
|
+
positions: Float32Array,
|
|
236
|
+
fractionalBits: number
|
|
237
|
+
): void {
|
|
238
|
+
const scale = 1 / 2 ** fractionalBits;
|
|
239
|
+
for (let componentIndex = 0; componentIndex < positions.length; componentIndex++) {
|
|
240
|
+
const byteOffset = componentIndex * 3;
|
|
241
|
+
let fixed32 =
|
|
242
|
+
positionStream[byteOffset] |
|
|
243
|
+
(positionStream[byteOffset + 1] << 8) |
|
|
244
|
+
(positionStream[byteOffset + 2] << 16);
|
|
245
|
+
fixed32 |= fixed32 & 0x800000 ? 0xff000000 : 0;
|
|
246
|
+
positions[componentIndex] = fixed32 * scale;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Decodes SPZ log-encoded scales to linear standard deviations. */
|
|
251
|
+
function decodeSPZScales(scaleStream: Uint8Array, scales: Float32Array): void {
|
|
252
|
+
for (let componentIndex = 0; componentIndex < scales.length; componentIndex++) {
|
|
253
|
+
scales[componentIndex] = Math.exp(scaleStream[componentIndex] / 16 - 10);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Decodes SPZ smallest-three quaternion rotations into `[w, x, y, z]` order. */
|
|
258
|
+
function decodeSPZRotations(rotationStream: Uint8Array, rotations: Float32Array): void {
|
|
259
|
+
for (let splatIndex = 0; splatIndex < rotations.length / 4; splatIndex++) {
|
|
260
|
+
const byteOffset = splatIndex * 4;
|
|
261
|
+
const component =
|
|
262
|
+
rotationStream[byteOffset] |
|
|
263
|
+
(rotationStream[byteOffset + 1] << 8) |
|
|
264
|
+
(rotationStream[byteOffset + 2] << 16) |
|
|
265
|
+
(rotationStream[byteOffset + 3] << 24);
|
|
266
|
+
const xyzw = decodeSPZQuaternion(component >>> 0);
|
|
267
|
+
const [w, x, y, z] = normalizeQuaternion(xyzw[3], xyzw[0], xyzw[1], xyzw[2]);
|
|
268
|
+
const rotationOffset = splatIndex * 4;
|
|
269
|
+
rotations[rotationOffset + 0] = w;
|
|
270
|
+
rotations[rotationOffset + 1] = x;
|
|
271
|
+
rotations[rotationOffset + 2] = y;
|
|
272
|
+
rotations[rotationOffset + 3] = z;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Decodes one SPZ smallest-three quaternion into `[x, y, z, w]` order. */
|
|
277
|
+
function decodeSPZQuaternion(component: number): [number, number, number, number] {
|
|
278
|
+
const rotation: [number, number, number, number] = [0, 0, 0, 0];
|
|
279
|
+
const largestComponent = component >>> 30;
|
|
280
|
+
let remainingBits = component;
|
|
281
|
+
let sumSquares = 0;
|
|
282
|
+
|
|
283
|
+
for (let componentIndex = 3; componentIndex >= 0; componentIndex--) {
|
|
284
|
+
if (componentIndex !== largestComponent) {
|
|
285
|
+
const magnitude = remainingBits & SPZ_ROTATION_COMPONENT_MASK;
|
|
286
|
+
const isNegative = (remainingBits >>> 9) & 0x1;
|
|
287
|
+
remainingBits >>>= 10;
|
|
288
|
+
let value = (SPZ_ROTATION_COMPONENT_SCALE * magnitude) / SPZ_ROTATION_COMPONENT_MASK;
|
|
289
|
+
if (isNegative) {
|
|
290
|
+
value = -value;
|
|
291
|
+
}
|
|
292
|
+
rotation[componentIndex] = value;
|
|
293
|
+
sumSquares += value * value;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
rotation[largestComponent] = Math.sqrt(Math.max(0, 1 - sumSquares));
|
|
298
|
+
return rotation;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Decodes SPZ color bytes into display RGB bytes and direct SH DC coefficients. */
|
|
302
|
+
function decodeSPZColors(
|
|
303
|
+
colorStream: Uint8Array,
|
|
304
|
+
colors: Uint8Array,
|
|
305
|
+
sphericalHarmonicDcs: Float32Array
|
|
306
|
+
): void {
|
|
307
|
+
for (let componentIndex = 0; componentIndex < colors.length; componentIndex++) {
|
|
308
|
+
sphericalHarmonicDcs[componentIndex] =
|
|
309
|
+
(colorStream[componentIndex] / 255 - 0.5) / SPZ_COLOR_SCALE;
|
|
310
|
+
colors[componentIndex] = normalizeColorByte(sphericalHarmonicDcs[componentIndex]);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Decodes SPZ alpha bytes into linear opacity. */
|
|
315
|
+
function decodeSPZOpacities(alphaStream: Uint8Array, opacities: Float32Array): void {
|
|
316
|
+
for (let splatIndex = 0; splatIndex < opacities.length; splatIndex++) {
|
|
317
|
+
opacities[splatIndex] = alphaStream[splatIndex] / 255;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Decodes SPZ quantized SH rest coefficients. */
|
|
322
|
+
function decodeSPZSphericalHarmonics(
|
|
323
|
+
sphericalHarmonicsStream: Uint8Array,
|
|
324
|
+
sphericalHarmonics: Float32Array
|
|
325
|
+
): void {
|
|
326
|
+
for (let componentIndex = 0; componentIndex < sphericalHarmonics.length; componentIndex++) {
|
|
327
|
+
sphericalHarmonics[componentIndex] = (sphericalHarmonicsStream[componentIndex] - 128) / 128;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Converts an SH DC coefficient to a fallback RGB byte for preview paths. */
|
|
332
|
+
function normalizeColorByte(sphericalHarmonicDc: number): number {
|
|
333
|
+
return Math.min(
|
|
334
|
+
Math.max(Math.round((sphericalHarmonicDc * SPZ_COLOR_SCALE + 0.5) * 255), 0),
|
|
335
|
+
255
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Returns the number of SH rest coefficients per splat for an SPZ degree. */
|
|
340
|
+
function getSphericalHarmonicsComponentCount(shDegree: number): number {
|
|
341
|
+
switch (shDegree) {
|
|
342
|
+
case 0:
|
|
343
|
+
return 0;
|
|
344
|
+
case 1:
|
|
345
|
+
return 9;
|
|
346
|
+
case 2:
|
|
347
|
+
return 24;
|
|
348
|
+
case 3:
|
|
349
|
+
return 45;
|
|
350
|
+
case 4:
|
|
351
|
+
return 72;
|
|
352
|
+
default:
|
|
353
|
+
throw new Error(`SPZLoader: spherical harmonics degree ${shDegree} is not supported.`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Returns the plaintext SPZ extension byte count. */
|
|
358
|
+
function getSPZExtensionByteLength(header: SPZHeader): number {
|
|
359
|
+
return header.flags & SPZ_EXTENSION_FLAG ? header.tocByteOffset - SPZ_HEADER_BYTE_LENGTH : 0;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Returns a copy of plaintext SPZ extension bytes when extension records are present. */
|
|
363
|
+
function getSPZExtensionBytes(data: ArrayBuffer, header: SPZHeader): Uint8Array | undefined {
|
|
364
|
+
const extensionByteLength = getSPZExtensionByteLength(header);
|
|
365
|
+
return extensionByteLength
|
|
366
|
+
? new Uint8Array(data.slice(SPZ_HEADER_BYTE_LENGTH, header.tocByteOffset))
|
|
367
|
+
: undefined;
|
|
368
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
export const SH_C0 = 0.28209479177387814;
|
|
6
|
+
|
|
7
|
+
/** Converts a normalized RGB value to a spherical harmonic DC coefficient. */
|
|
8
|
+
export function convertColorByteToSphericalHarmonicDc(colorByte: number): number {
|
|
9
|
+
return (colorByte / 255 - 0.5) / SH_C0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Converts an unorm8 alpha byte to linear opacity. */
|
|
13
|
+
export function convertAlphaByteToLinearOpacity(alphaByte: number): number {
|
|
14
|
+
return Math.min(Math.max(alphaByte / 255, 0), 1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Returns a normalized quaternion, falling back to identity when invalid. */
|
|
18
|
+
export function normalizeQuaternion(
|
|
19
|
+
w: number,
|
|
20
|
+
x: number,
|
|
21
|
+
y: number,
|
|
22
|
+
z: number
|
|
23
|
+
): [number, number, number, number] {
|
|
24
|
+
const length = Math.hypot(w, x, y, z);
|
|
25
|
+
if (!Number.isFinite(length) || length <= Number.EPSILON) {
|
|
26
|
+
return [1, 0, 0, 0];
|
|
27
|
+
}
|
|
28
|
+
return [w / length, x / length, y / length, z / length];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Decodes the GaussianSplats3D uint8 quaternion component encoding. */
|
|
32
|
+
export function decodeQuaternionByte(byte: number): number {
|
|
33
|
+
return (byte - 128) / 128;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Decodes an IEEE 754 binary16 value. */
|
|
37
|
+
export function decodeFloat16(value: number): number {
|
|
38
|
+
const sign = value & 0x8000 ? -1 : 1;
|
|
39
|
+
const exponent = (value >> 10) & 0x1f;
|
|
40
|
+
const fraction = value & 0x03ff;
|
|
41
|
+
|
|
42
|
+
if (exponent === 0) {
|
|
43
|
+
return sign * Math.pow(2, -14) * (fraction / 1024);
|
|
44
|
+
}
|
|
45
|
+
if (exponent === 0x1f) {
|
|
46
|
+
return fraction ? Number.NaN : sign * Number.POSITIVE_INFINITY;
|
|
47
|
+
}
|
|
48
|
+
return sign * Math.pow(2, exponent - 15) * (1 + fraction / 1024);
|
|
49
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import * as arrow from 'apache-arrow';
|
|
6
|
+
import type {Field, MeshArrowTable, Schema} from '@loaders.gl/schema';
|
|
7
|
+
import type {GaussianSplats} from '../types';
|
|
8
|
+
import {convertColorByteToSphericalHarmonicDc} from './splat-utils';
|
|
9
|
+
|
|
10
|
+
/** Builds a Mesh Arrow table from decoded Gaussian splat values. */
|
|
11
|
+
export function makeGaussianSplatsArrowTable(splats: GaussianSplats): MeshArrowTable {
|
|
12
|
+
const columns: Record<string, arrow.Vector> = {};
|
|
13
|
+
const fields: arrow.Field[] = [];
|
|
14
|
+
const schemaFields: Field[] = [];
|
|
15
|
+
|
|
16
|
+
addColumn(
|
|
17
|
+
columns,
|
|
18
|
+
fields,
|
|
19
|
+
schemaFields,
|
|
20
|
+
'POSITION',
|
|
21
|
+
makeFixedSizeListVector(splats.positions, 3),
|
|
22
|
+
{
|
|
23
|
+
type: 'fixed-size-list',
|
|
24
|
+
listSize: 3,
|
|
25
|
+
children: [{name: 'value', type: 'float32'}]
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const colorCoefficients = getColorCoefficients(splats);
|
|
30
|
+
addScalarColumn(columns, fields, schemaFields, 'f_dc_0', colorCoefficients[0], {
|
|
31
|
+
'loaders_gl.gaussian_splats.semantic': 'spherical_harmonic_dc',
|
|
32
|
+
'loaders_gl.gaussian_splats.component': '0',
|
|
33
|
+
'loaders_gl.gaussian_splats.encoding': 'coefficient'
|
|
34
|
+
});
|
|
35
|
+
addScalarColumn(columns, fields, schemaFields, 'f_dc_1', colorCoefficients[1], {
|
|
36
|
+
'loaders_gl.gaussian_splats.semantic': 'spherical_harmonic_dc',
|
|
37
|
+
'loaders_gl.gaussian_splats.component': '1',
|
|
38
|
+
'loaders_gl.gaussian_splats.encoding': 'coefficient'
|
|
39
|
+
});
|
|
40
|
+
addScalarColumn(columns, fields, schemaFields, 'f_dc_2', colorCoefficients[2], {
|
|
41
|
+
'loaders_gl.gaussian_splats.semantic': 'spherical_harmonic_dc',
|
|
42
|
+
'loaders_gl.gaussian_splats.component': '2',
|
|
43
|
+
'loaders_gl.gaussian_splats.encoding': 'coefficient'
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
addScalarColumn(columns, fields, schemaFields, 'opacity', splats.opacities, {
|
|
47
|
+
'loaders_gl.gaussian_splats.semantic': 'opacity',
|
|
48
|
+
'loaders_gl.gaussian_splats.encoding': 'linear'
|
|
49
|
+
});
|
|
50
|
+
addVectorComponents(columns, fields, schemaFields, 'scale', splats.scales, 3, {
|
|
51
|
+
'loaders_gl.gaussian_splats.semantic': 'scale',
|
|
52
|
+
'loaders_gl.gaussian_splats.encoding': 'linear'
|
|
53
|
+
});
|
|
54
|
+
addVectorComponents(columns, fields, schemaFields, 'rot', splats.rotations, 4, {
|
|
55
|
+
'loaders_gl.gaussian_splats.semantic': 'rotation',
|
|
56
|
+
'loaders_gl.gaussian_splats.encoding': 'quaternion'
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (splats.sphericalHarmonics && splats.sphericalHarmonicsComponentCount) {
|
|
60
|
+
addSphericalHarmonicRestColumns(
|
|
61
|
+
columns,
|
|
62
|
+
fields,
|
|
63
|
+
schemaFields,
|
|
64
|
+
splats.sphericalHarmonics,
|
|
65
|
+
splats.sphericalHarmonicsComponentCount
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const metadata = new Map<string, string>([
|
|
70
|
+
['loaders_gl.semantic_type', 'gaussian-splats'],
|
|
71
|
+
['loaders_gl.gaussian_splats.version', '1'],
|
|
72
|
+
['loaders_gl.gaussian_splats.source_format', splats.format],
|
|
73
|
+
['topology', 'point-list'],
|
|
74
|
+
['mode', '0']
|
|
75
|
+
]);
|
|
76
|
+
const arrowSchema = new arrow.Schema(fields, metadata);
|
|
77
|
+
const schema: Schema = {
|
|
78
|
+
fields: schemaFields,
|
|
79
|
+
metadata: Object.fromEntries(metadata)
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
shape: 'arrow-table',
|
|
84
|
+
schema,
|
|
85
|
+
data: new arrow.Table(arrowSchema, columns),
|
|
86
|
+
topology: 'point-list',
|
|
87
|
+
loaderData: splats.loaderData
|
|
88
|
+
} as MeshArrowTable;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Adds one scalar Arrow column per component of an interleaved vector array. */
|
|
92
|
+
function addVectorComponents(
|
|
93
|
+
columns: Record<string, arrow.Vector>,
|
|
94
|
+
fields: arrow.Field[],
|
|
95
|
+
schemaFields: Field[],
|
|
96
|
+
prefix: string,
|
|
97
|
+
values: Float32Array,
|
|
98
|
+
size: number,
|
|
99
|
+
metadata: Record<string, string>
|
|
100
|
+
): void {
|
|
101
|
+
for (let component = 0; component < size; component++) {
|
|
102
|
+
const componentValues = new Float32Array(values.length / size);
|
|
103
|
+
for (let rowIndex = 0; rowIndex < componentValues.length; rowIndex++) {
|
|
104
|
+
componentValues[rowIndex] = values[rowIndex * size + component];
|
|
105
|
+
}
|
|
106
|
+
addScalarColumn(columns, fields, schemaFields, `${prefix}_${component}`, componentValues, {
|
|
107
|
+
...metadata,
|
|
108
|
+
'loaders_gl.gaussian_splats.component': String(component)
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Adds one scalar Arrow column per spherical harmonic rest component. */
|
|
114
|
+
function addSphericalHarmonicRestColumns(
|
|
115
|
+
columns: Record<string, arrow.Vector>,
|
|
116
|
+
fields: arrow.Field[],
|
|
117
|
+
schemaFields: Field[],
|
|
118
|
+
sphericalHarmonics: Float32Array,
|
|
119
|
+
componentCount: number
|
|
120
|
+
): void {
|
|
121
|
+
for (let component = 0; component < componentCount; component++) {
|
|
122
|
+
const componentValues = new Float32Array(sphericalHarmonics.length / componentCount);
|
|
123
|
+
for (let rowIndex = 0; rowIndex < componentValues.length; rowIndex++) {
|
|
124
|
+
componentValues[rowIndex] = sphericalHarmonics[rowIndex * componentCount + component];
|
|
125
|
+
}
|
|
126
|
+
addScalarColumn(columns, fields, schemaFields, `f_rest_${component}`, componentValues, {
|
|
127
|
+
'loaders_gl.gaussian_splats.semantic': 'spherical_harmonic_rest',
|
|
128
|
+
'loaders_gl.gaussian_splats.component': String(component),
|
|
129
|
+
'loaders_gl.gaussian_splats.encoding': 'coefficient'
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Adds a Float32 scalar Arrow column and matching loaders.gl schema field. */
|
|
135
|
+
function addScalarColumn(
|
|
136
|
+
columns: Record<string, arrow.Vector>,
|
|
137
|
+
fields: arrow.Field[],
|
|
138
|
+
schemaFields: Field[],
|
|
139
|
+
name: string,
|
|
140
|
+
values: Float32Array,
|
|
141
|
+
metadata: Record<string, string>
|
|
142
|
+
): void {
|
|
143
|
+
addColumn(columns, fields, schemaFields, name, arrow.makeVector(values), 'float32', metadata);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Adds an Arrow column, Arrow field, and loaders.gl schema field. */
|
|
147
|
+
function addColumn(
|
|
148
|
+
columns: Record<string, arrow.Vector>,
|
|
149
|
+
fields: arrow.Field[],
|
|
150
|
+
schemaFields: Field[],
|
|
151
|
+
name: string,
|
|
152
|
+
column: arrow.Vector,
|
|
153
|
+
schemaType: Field['type'],
|
|
154
|
+
metadata: Record<string, string> = {}
|
|
155
|
+
): void {
|
|
156
|
+
columns[name] = column;
|
|
157
|
+
fields.push(new arrow.Field(name, column.type, false, new Map(Object.entries(metadata))));
|
|
158
|
+
schemaFields.push({name, type: schemaType, nullable: false, metadata});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Builds a FixedSizeList Arrow vector from an interleaved Float32 array. */
|
|
162
|
+
function makeFixedSizeListVector(value: Float32Array, size: number): arrow.Vector {
|
|
163
|
+
const values = arrow.makeVector(value);
|
|
164
|
+
const child = values.data[0];
|
|
165
|
+
const type = new arrow.FixedSizeList(size, new arrow.Field('value', child.type, false));
|
|
166
|
+
const data = new arrow.Data(type, 0, value.length / size, 0, {}, [child]);
|
|
167
|
+
return new arrow.Vector([data]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Returns separate SH DC coefficient arrays from direct coefficients or RGB bytes. */
|
|
171
|
+
function getColorCoefficients(splats: GaussianSplats): [Float32Array, Float32Array, Float32Array] {
|
|
172
|
+
const rowCount = splats.splatCount;
|
|
173
|
+
const fdc0 = new Float32Array(rowCount);
|
|
174
|
+
const fdc1 = new Float32Array(rowCount);
|
|
175
|
+
const fdc2 = new Float32Array(rowCount);
|
|
176
|
+
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
|
177
|
+
if (splats.sphericalHarmonicDcs) {
|
|
178
|
+
fdc0[rowIndex] = splats.sphericalHarmonicDcs[rowIndex * 3 + 0];
|
|
179
|
+
fdc1[rowIndex] = splats.sphericalHarmonicDcs[rowIndex * 3 + 1];
|
|
180
|
+
fdc2[rowIndex] = splats.sphericalHarmonicDcs[rowIndex * 3 + 2];
|
|
181
|
+
} else {
|
|
182
|
+
fdc0[rowIndex] = convertColorByteToSphericalHarmonicDc(splats.colors[rowIndex * 3 + 0]);
|
|
183
|
+
fdc1[rowIndex] = convertColorByteToSphericalHarmonicDc(splats.colors[rowIndex * 3 + 1]);
|
|
184
|
+
fdc2[rowIndex] = convertColorByteToSphericalHarmonicDc(splats.colors[rowIndex * 3 + 2]);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return [fdc0, fdc1, fdc2];
|
|
188
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {Loader, LoaderOptions} from '@loaders.gl/loader-utils';
|
|
6
|
+
import {RADFormat} from './splats-format';
|
|
7
|
+
import type {RADMetadata} from './lib/parse-rad';
|
|
8
|
+
|
|
9
|
+
// __VERSION__ is injected by babel-plugin-version-inline
|
|
10
|
+
// @ts-ignore TS2304: Cannot find name '__VERSION__'.
|
|
11
|
+
const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'latest';
|
|
12
|
+
|
|
13
|
+
/** Preloads the parser-bearing RAD loader implementation. */
|
|
14
|
+
async function preload() {
|
|
15
|
+
const {RADLoaderWithParser} = await import('./rad-loader');
|
|
16
|
+
return RADLoaderWithParser;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Metadata-only loader for Spark `.rad` paged LoD Gaussian splat containers. */
|
|
20
|
+
export const RADLoader = {
|
|
21
|
+
dataType: null as unknown as RADMetadata,
|
|
22
|
+
batchType: null as never,
|
|
23
|
+
...RADFormat,
|
|
24
|
+
version: VERSION,
|
|
25
|
+
options: {},
|
|
26
|
+
preload
|
|
27
|
+
} as const satisfies Loader<RADMetadata, never, LoaderOptions>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {LoaderOptions, LoaderWithParser} from '@loaders.gl/loader-utils';
|
|
6
|
+
import {parseRADHeader} from './lib/parse-rad';
|
|
7
|
+
import type {RADMetadata} from './lib/parse-rad';
|
|
8
|
+
import {RADLoader as RADLoaderMetadata} from './rad-loader-types';
|
|
9
|
+
|
|
10
|
+
export {parseRADChunkHeader, parseRADHeader, tryParseRADHeader} from './lib/parse-rad';
|
|
11
|
+
export {
|
|
12
|
+
parseRADChunk,
|
|
13
|
+
parseRADChunkToGaussianSplats,
|
|
14
|
+
type RADChunkDecodeOptions
|
|
15
|
+
} from './lib/parse-rad-chunk';
|
|
16
|
+
|
|
17
|
+
const {preload: _RADLoaderPreload, ...RADLoaderMetadataWithoutPreload} = RADLoaderMetadata;
|
|
18
|
+
|
|
19
|
+
/** Parser-bearing loader for Spark `.rad` paged LoD Gaussian splat container metadata. */
|
|
20
|
+
export const RADLoaderWithParser = {
|
|
21
|
+
...RADLoaderMetadataWithoutPreload,
|
|
22
|
+
parse: async (arrayBuffer: ArrayBuffer) => parseRADHeader(arrayBuffer),
|
|
23
|
+
parseSync: (arrayBuffer: ArrayBuffer) => parseRADHeader(arrayBuffer)
|
|
24
|
+
} as const satisfies LoaderWithParser<RADMetadata, never, LoaderOptions>;
|