@loaders.gl/splats 5.0.0-alpha.1 → 5.0.0-alpha.11
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 +14 -3
- package/dist/dist.dev.js +36757 -17782
- package/dist/dist.min.js +68 -7
- package/dist/index.cjs +2769 -838
- package/dist/index.cjs.map +3 -4
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/ksplat-loader-types.js +1 -1
- package/dist/ksplat-loader-types.js.map +1 -1
- package/dist/lib/parse-rad-chunk.js +3 -3
- package/dist/lib/parse-rad-chunk.js.map +1 -1
- package/dist/lib/parse-rad.d.ts +23 -7
- package/dist/lib/parse-rad.d.ts.map +1 -1
- package/dist/lib/parse-rad.js +10 -135
- package/dist/lib/parse-rad.js.map +1 -1
- package/dist/lib/parse-spz.d.ts.map +1 -1
- package/dist/lib/parse-spz.js +153 -4
- package/dist/lib/parse-spz.js.map +1 -1
- package/dist/rad-loader-types.js +1 -1
- package/dist/rad-loader-types.js.map +1 -1
- package/dist/rad-metadata.schema.json +133 -0
- package/dist/rad-source-loader.js +1 -1
- package/dist/rad-source-loader.js.map +1 -1
- package/dist/rad-zod-schema.cjs +81 -0
- package/dist/rad-zod-schema.cjs.map +6 -0
- package/dist/rad-zod-schema.d.ts +88 -0
- package/dist/rad-zod-schema.d.ts.map +1 -0
- package/dist/rad-zod-schema.js +70 -0
- package/dist/rad-zod-schema.js.map +1 -0
- package/dist/radc-metadata.schema.json +112 -0
- package/dist/splat-loader-types.js +1 -1
- package/dist/splat-loader-types.js.map +1 -1
- package/dist/spz-loader-types.js +1 -1
- package/dist/spz-loader-types.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +17 -8
- package/src/index.ts +4 -1
- package/src/lib/parse-rad-chunk.ts +3 -3
- package/src/lib/parse-rad.ts +46 -167
- package/src/lib/parse-spz.ts +190 -5
- package/src/rad-zod-schema.ts +83 -0
- package/src/types.ts +7 -0
package/src/lib/parse-spz.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
// Copyright (c) vis.gl contributors
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {GZipDecompressor, ZstdDecompressor} from '@loaders.gl/compression';
|
|
6
6
|
import type {MeshArrowTable} from '@loaders.gl/schema';
|
|
7
|
-
import type {GaussianSplats, SplatsLoaderOptions} from '../types';
|
|
7
|
+
import type {GaussianSplats, SPZCoordinateSystem, SplatsLoaderOptions} from '../types';
|
|
8
8
|
import {makeGaussianSplatsArrowTable} from './splats-arrow-table';
|
|
9
9
|
import {normalizeQuaternion} from './splat-utils';
|
|
10
10
|
|
|
@@ -46,20 +46,205 @@ export async function parseSPZToGaussianSplats(
|
|
|
46
46
|
data: ArrayBuffer,
|
|
47
47
|
options?: SplatsLoaderOptions
|
|
48
48
|
): Promise<GaussianSplats> {
|
|
49
|
+
const version = detectSPZVersion(data);
|
|
50
|
+
if (version === 0) {
|
|
51
|
+
return await parseLegacySPZ(data, options);
|
|
52
|
+
}
|
|
53
|
+
if (version !== SPZ_VERSION) {
|
|
54
|
+
throw new Error(`SPZLoader: version ${version} is not supported.`);
|
|
55
|
+
}
|
|
49
56
|
const header = parseSPZHeader(data);
|
|
50
57
|
const streamInfos = parseSPZStreamInfos(data, header, getSPZStreamByteLengths(header));
|
|
51
58
|
const compressedStreams = streamInfos.map(streamInfo =>
|
|
52
59
|
data.slice(streamInfo.compressedOffset, streamInfo.compressedOffset + streamInfo.compressedSize)
|
|
53
60
|
);
|
|
54
|
-
const
|
|
61
|
+
const decompressor = new ZstdDecompressor({modules: options?.modules});
|
|
55
62
|
const streams = await Promise.all(
|
|
56
63
|
compressedStreams.map((compressedStream, streamIndex) =>
|
|
57
|
-
|
|
64
|
+
decompressor.decompress(compressedStream, streamInfos[streamIndex].uncompressedSize)
|
|
58
65
|
)
|
|
59
66
|
);
|
|
60
67
|
|
|
61
68
|
validateSPZStreamLengths(streams, streamInfos);
|
|
62
|
-
return
|
|
69
|
+
return applySPZCoordinateSystem(
|
|
70
|
+
decodeSPZStreams(header, streams, data),
|
|
71
|
+
options?.splats?.sourceCoordinateSystem ?? 'RUB',
|
|
72
|
+
options?.splats?.targetCoordinateSystem ?? options?.splats?.sourceCoordinateSystem ?? 'RUB'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Detects the SPZ generation from either the legacy gzip or v4 plaintext header. */
|
|
77
|
+
function detectSPZVersion(data: ArrayBuffer): number {
|
|
78
|
+
if (data.byteLength < 8) {
|
|
79
|
+
throw new Error('SPZLoader: file must contain at least an 8-byte header.');
|
|
80
|
+
}
|
|
81
|
+
const bytes = new Uint8Array(data);
|
|
82
|
+
if (bytes[0] === 0x1f && bytes[1] === 0x8b) {
|
|
83
|
+
// Legacy files carry their 16-byte header inside the gzip stream. The
|
|
84
|
+
// version is therefore read by parseLegacySPZ after decompression.
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
if (data.byteLength < SPZ_HEADER_BYTE_LENGTH) {
|
|
88
|
+
throw new Error('SPZLoader: file must contain a 32-byte header.');
|
|
89
|
+
}
|
|
90
|
+
return new DataView(data).getUint32(4, true);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Decodes the single-stream SPZ v2/v3 container. */
|
|
94
|
+
async function parseLegacySPZ(
|
|
95
|
+
data: ArrayBuffer,
|
|
96
|
+
options?: SplatsLoaderOptions
|
|
97
|
+
): Promise<GaussianSplats> {
|
|
98
|
+
const decompressed = await new GZipDecompressor({
|
|
99
|
+
modules: options?.modules,
|
|
100
|
+
useNative: true
|
|
101
|
+
}).decompress(data);
|
|
102
|
+
if (decompressed.byteLength < 16) {
|
|
103
|
+
throw new Error('SPZLoader: legacy payload must contain a 16-byte header.');
|
|
104
|
+
}
|
|
105
|
+
const dataView = new DataView(decompressed);
|
|
106
|
+
const header = {
|
|
107
|
+
magic: dataView.getUint32(0, true),
|
|
108
|
+
version: dataView.getUint32(4, true),
|
|
109
|
+
numPoints: dataView.getUint32(8, true),
|
|
110
|
+
shDegree: dataView.getUint8(12),
|
|
111
|
+
fractionalBits: dataView.getUint8(13),
|
|
112
|
+
flags: dataView.getUint8(14)
|
|
113
|
+
};
|
|
114
|
+
if (header.magic !== SPZ_MAGIC) {
|
|
115
|
+
throw new Error('SPZLoader: NGSP magic header not found.');
|
|
116
|
+
}
|
|
117
|
+
if (header.version !== 2 && header.version !== 3) {
|
|
118
|
+
throw new Error(`SPZLoader: version ${header.version} is not supported.`);
|
|
119
|
+
}
|
|
120
|
+
if (dataView.getUint8(15) !== 0) {
|
|
121
|
+
throw new Error('SPZLoader: legacy header reserved byte must be zero.');
|
|
122
|
+
}
|
|
123
|
+
if (header.shDegree > 3) {
|
|
124
|
+
throw new Error(`SPZLoader: spherical harmonics degree ${header.shDegree} is not supported.`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const count = header.numPoints;
|
|
128
|
+
const shComponentCount = getSphericalHarmonicsComponentCount(header.shDegree);
|
|
129
|
+
const positionByteLength = count * 9;
|
|
130
|
+
const alphaByteLength = count;
|
|
131
|
+
const colorByteLength = count * 3;
|
|
132
|
+
const scaleByteLength = count * 3;
|
|
133
|
+
const rotationByteLength = count * (header.version === 2 ? 3 : 4);
|
|
134
|
+
const shByteLength = count * shComponentCount;
|
|
135
|
+
const payloadByteLength =
|
|
136
|
+
positionByteLength +
|
|
137
|
+
alphaByteLength +
|
|
138
|
+
colorByteLength +
|
|
139
|
+
scaleByteLength +
|
|
140
|
+
rotationByteLength +
|
|
141
|
+
shByteLength;
|
|
142
|
+
if (decompressed.byteLength - 16 !== payloadByteLength) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`SPZLoader: legacy payload has ${decompressed.byteLength - 16} bytes, expected ${payloadByteLength}.`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let offset = 16;
|
|
149
|
+
const positions = new Float32Array(count * 3);
|
|
150
|
+
const opacities = new Float32Array(count);
|
|
151
|
+
const colors = new Uint8Array(count * 3);
|
|
152
|
+
const scales = new Float32Array(count * 3);
|
|
153
|
+
const rotations = new Float32Array(count * 4);
|
|
154
|
+
const sphericalHarmonicDcs = new Float32Array(count * 3);
|
|
155
|
+
const sphericalHarmonics = shComponentCount
|
|
156
|
+
? new Float32Array(count * shComponentCount)
|
|
157
|
+
: undefined;
|
|
158
|
+
decodeSPZPositions(
|
|
159
|
+
new Uint8Array(decompressed, offset, positionByteLength),
|
|
160
|
+
positions,
|
|
161
|
+
header.fractionalBits
|
|
162
|
+
);
|
|
163
|
+
offset += positionByteLength;
|
|
164
|
+
decodeSPZOpacities(new Uint8Array(decompressed, offset, alphaByteLength), opacities);
|
|
165
|
+
offset += alphaByteLength;
|
|
166
|
+
decodeSPZColors(
|
|
167
|
+
new Uint8Array(decompressed, offset, colorByteLength),
|
|
168
|
+
colors,
|
|
169
|
+
sphericalHarmonicDcs
|
|
170
|
+
);
|
|
171
|
+
offset += colorByteLength;
|
|
172
|
+
decodeSPZScales(new Uint8Array(decompressed, offset, scaleByteLength), scales);
|
|
173
|
+
offset += scaleByteLength;
|
|
174
|
+
if (header.version === 2) {
|
|
175
|
+
decodeLegacySPZRotations(new Uint8Array(decompressed, offset, rotationByteLength), rotations);
|
|
176
|
+
} else {
|
|
177
|
+
decodeSPZRotations(new Uint8Array(decompressed, offset, rotationByteLength), rotations);
|
|
178
|
+
}
|
|
179
|
+
offset += rotationByteLength;
|
|
180
|
+
if (sphericalHarmonics) {
|
|
181
|
+
decodeSPZSphericalHarmonics(
|
|
182
|
+
new Uint8Array(decompressed, offset, shByteLength),
|
|
183
|
+
sphericalHarmonics
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return applySPZCoordinateSystem(
|
|
188
|
+
{
|
|
189
|
+
format: 'spz',
|
|
190
|
+
splatCount: count,
|
|
191
|
+
positions,
|
|
192
|
+
scales,
|
|
193
|
+
rotations,
|
|
194
|
+
colors,
|
|
195
|
+
sphericalHarmonicDcs,
|
|
196
|
+
opacities,
|
|
197
|
+
sphericalHarmonics,
|
|
198
|
+
sphericalHarmonicsComponentCount: shComponentCount || undefined,
|
|
199
|
+
loaderData: {...header, legacy: true}
|
|
200
|
+
},
|
|
201
|
+
options?.splats?.sourceCoordinateSystem ?? 'RUB',
|
|
202
|
+
options?.splats?.targetCoordinateSystem ?? options?.splats?.sourceCoordinateSystem ?? 'RUB'
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Decodes the three signed bytes used by SPZ v2 rotations. */
|
|
207
|
+
function decodeLegacySPZRotations(rotationStream: Uint8Array, rotations: Float32Array): void {
|
|
208
|
+
for (let splatIndex = 0; splatIndex < rotations.length / 4; splatIndex++) {
|
|
209
|
+
const byteOffset = splatIndex * 3;
|
|
210
|
+
const x = signedByte(rotationStream[byteOffset]) / 127;
|
|
211
|
+
const y = signedByte(rotationStream[byteOffset + 1]) / 127;
|
|
212
|
+
const z = signedByte(rotationStream[byteOffset + 2]) / 127;
|
|
213
|
+
const w = Math.sqrt(Math.max(0, 1 - x * x - y * y - z * z));
|
|
214
|
+
const normalized = normalizeQuaternion(w, x, y, z);
|
|
215
|
+
rotations.set(normalized, splatIndex * 4);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Converts an unsigned byte containing a signed int8 value. */
|
|
220
|
+
function signedByte(value: number): number {
|
|
221
|
+
return value > 127 ? value - 256 : value;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Applies the explicitly requested SPZ coordinate conversion. */
|
|
225
|
+
function applySPZCoordinateSystem(
|
|
226
|
+
splats: GaussianSplats,
|
|
227
|
+
source: SPZCoordinateSystem,
|
|
228
|
+
target: SPZCoordinateSystem
|
|
229
|
+
): GaussianSplats {
|
|
230
|
+
if (source === target || source === 'UNSPECIFIED' || target === 'UNSPECIFIED') {
|
|
231
|
+
return splats;
|
|
232
|
+
}
|
|
233
|
+
if (source === 'RUB' && target === 'LUF') {
|
|
234
|
+
for (let index = 0; index < splats.positions.length; index += 3) {
|
|
235
|
+
splats.positions[index] = -splats.positions[index];
|
|
236
|
+
splats.positions[index + 2] = -splats.positions[index + 2];
|
|
237
|
+
}
|
|
238
|
+
for (let index = 0; index < splats.rotations.length; index += 4) {
|
|
239
|
+
splats.rotations[index + 1] = -splats.rotations[index + 1];
|
|
240
|
+
splats.rotations[index + 3] = -splats.rotations[index + 3];
|
|
241
|
+
}
|
|
242
|
+
return splats;
|
|
243
|
+
}
|
|
244
|
+
if (source === 'LUF' && target === 'RUB') {
|
|
245
|
+
return applySPZCoordinateSystem(splats, 'RUB', 'LUF');
|
|
246
|
+
}
|
|
247
|
+
throw new Error(`SPZLoader: coordinate conversion ${source} -> ${target} is not supported.`);
|
|
63
248
|
}
|
|
64
249
|
|
|
65
250
|
/** Parses and validates the SPZ v4 plaintext header. */
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {z} from 'zod';
|
|
6
|
+
import type {
|
|
7
|
+
RADChunkMetadataJSON,
|
|
8
|
+
RADChunkProperty,
|
|
9
|
+
RADChunkRange,
|
|
10
|
+
RADMetadataJSON,
|
|
11
|
+
RADSplatEncoding
|
|
12
|
+
} from './lib/parse-rad';
|
|
13
|
+
|
|
14
|
+
const nonNegativeSafeIntegerSchema = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
15
|
+
|
|
16
|
+
/** Zod schema for Spark RAD splat quantization and shader decode ranges. */
|
|
17
|
+
export const RADSplatEncodingSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
rgbMin: z.number().finite().optional(),
|
|
20
|
+
rgbMax: z.number().finite().optional(),
|
|
21
|
+
lnScaleMin: z.number().finite().optional(),
|
|
22
|
+
lnScaleMax: z.number().finite().optional(),
|
|
23
|
+
sh1Max: z.number().finite().optional(),
|
|
24
|
+
sh2Max: z.number().finite().optional(),
|
|
25
|
+
sh3Max: z.number().finite().optional(),
|
|
26
|
+
lodOpacity: z.boolean().optional()
|
|
27
|
+
})
|
|
28
|
+
.passthrough() satisfies z.ZodType<RADSplatEncoding>;
|
|
29
|
+
|
|
30
|
+
/** Zod schema for one chunk-table entry in top-level RAD metadata. */
|
|
31
|
+
export const RADChunkRangeSchema = z
|
|
32
|
+
.object({
|
|
33
|
+
offset: nonNegativeSafeIntegerSchema,
|
|
34
|
+
bytes: nonNegativeSafeIntegerSchema,
|
|
35
|
+
base: nonNegativeSafeIntegerSchema.optional(),
|
|
36
|
+
count: nonNegativeSafeIntegerSchema.optional(),
|
|
37
|
+
filename: z.string().optional()
|
|
38
|
+
})
|
|
39
|
+
.passthrough() satisfies z.ZodType<RADChunkRange>;
|
|
40
|
+
|
|
41
|
+
/** Zod schema for raw JSON metadata stored in a Spark RAD header. */
|
|
42
|
+
export const RADMetadataJSONSchema = z
|
|
43
|
+
.object({
|
|
44
|
+
version: z.literal(1),
|
|
45
|
+
type: z.literal('gsplat'),
|
|
46
|
+
count: nonNegativeSafeIntegerSchema,
|
|
47
|
+
maxSh: nonNegativeSafeIntegerSchema.optional(),
|
|
48
|
+
lodTree: z.boolean().optional(),
|
|
49
|
+
chunkSize: nonNegativeSafeIntegerSchema.optional(),
|
|
50
|
+
allChunkBytes: nonNegativeSafeIntegerSchema.optional(),
|
|
51
|
+
chunks: z.array(RADChunkRangeSchema),
|
|
52
|
+
splatEncoding: RADSplatEncodingSchema.optional(),
|
|
53
|
+
shCodeCount: nonNegativeSafeIntegerSchema.optional(),
|
|
54
|
+
comment: z.string().optional()
|
|
55
|
+
})
|
|
56
|
+
.passthrough() satisfies z.ZodType<RADMetadataJSON>;
|
|
57
|
+
|
|
58
|
+
/** Zod schema for one property-table entry in RADC metadata. */
|
|
59
|
+
export const RADChunkPropertySchema = z
|
|
60
|
+
.object({
|
|
61
|
+
offset: nonNegativeSafeIntegerSchema,
|
|
62
|
+
bytes: nonNegativeSafeIntegerSchema,
|
|
63
|
+
property: z.string(),
|
|
64
|
+
encoding: z.string(),
|
|
65
|
+
compression: z.string().optional(),
|
|
66
|
+
min: z.number().finite().optional(),
|
|
67
|
+
max: z.number().finite().optional()
|
|
68
|
+
})
|
|
69
|
+
.passthrough() satisfies z.ZodType<RADChunkProperty>;
|
|
70
|
+
|
|
71
|
+
/** Zod schema for raw JSON metadata stored in a Spark RADC chunk header. */
|
|
72
|
+
export const RADChunkMetadataJSONSchema = z
|
|
73
|
+
.object({
|
|
74
|
+
version: z.literal(1),
|
|
75
|
+
base: nonNegativeSafeIntegerSchema,
|
|
76
|
+
count: nonNegativeSafeIntegerSchema,
|
|
77
|
+
payloadBytes: nonNegativeSafeIntegerSchema,
|
|
78
|
+
maxSh: nonNegativeSafeIntegerSchema.optional(),
|
|
79
|
+
lodTree: z.boolean().optional(),
|
|
80
|
+
splatEncoding: RADSplatEncodingSchema.optional(),
|
|
81
|
+
properties: z.array(RADChunkPropertySchema)
|
|
82
|
+
})
|
|
83
|
+
.passthrough() satisfies z.ZodType<RADChunkMetadataJSON>;
|
package/src/types.ts
CHANGED
|
@@ -9,9 +9,16 @@ export type SplatsLoaderOptions = LoaderOptions & {
|
|
|
9
9
|
splats?: {
|
|
10
10
|
/** Output shape. V1 supports loaders.gl Mesh Arrow tables. */
|
|
11
11
|
shape?: 'arrow-table';
|
|
12
|
+
/** Coordinate frame stored in the source SPZ payload. */
|
|
13
|
+
sourceCoordinateSystem?: SPZCoordinateSystem;
|
|
14
|
+
/** Coordinate frame expected by the caller. Defaults to the source frame. */
|
|
15
|
+
targetCoordinateSystem?: SPZCoordinateSystem;
|
|
12
16
|
};
|
|
13
17
|
};
|
|
14
18
|
|
|
19
|
+
/** Coordinate-system labels used by the SPZ container and glTF bridge. */
|
|
20
|
+
export type SPZCoordinateSystem = 'RUB' | 'LUF' | 'RUF' | 'UNSPECIFIED';
|
|
21
|
+
|
|
15
22
|
/** Linear Gaussian splat values before Arrow table construction. */
|
|
16
23
|
export type GaussianSplats = {
|
|
17
24
|
/** Source format identifier. */
|