@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,721 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {DeflateCompression} from '@loaders.gl/compression';
|
|
6
|
+
import type {MeshArrowTable} from '@loaders.gl/schema';
|
|
7
|
+
import type {GaussianSplats, SplatsLoaderOptions} from '../types';
|
|
8
|
+
import {
|
|
9
|
+
parseRADChunkHeader,
|
|
10
|
+
roundUpToEight,
|
|
11
|
+
type RADChunkMetadata,
|
|
12
|
+
type RADChunkProperty,
|
|
13
|
+
type RADSplatEncoding
|
|
14
|
+
} from './parse-rad';
|
|
15
|
+
import {makeGaussianSplatsArrowTable} from './splats-arrow-table';
|
|
16
|
+
import {decodeFloat16, normalizeQuaternion, SH_C0} from './splat-utils';
|
|
17
|
+
|
|
18
|
+
const RAD_PROPERTY_DEFLATE = new DeflateCompression();
|
|
19
|
+
const RAD_PROPERTY_RAW_DEFLATE = new DeflateCompression({raw: true});
|
|
20
|
+
|
|
21
|
+
/** Options for decoding one Spark RADC chunk payload. */
|
|
22
|
+
export type RADChunkDecodeOptions = SplatsLoaderOptions & {
|
|
23
|
+
radChunk?: {
|
|
24
|
+
/** Optional source-level splat encoding metadata used as a fallback for chunk-local metadata. */
|
|
25
|
+
splatEncoding?: RADSplatEncoding;
|
|
26
|
+
/** Whether decoded LoD child metadata is retained in `loaderData`. */
|
|
27
|
+
includeLoDTree?: boolean;
|
|
28
|
+
/** Whether decoded SH rest coefficients are included in Arrow table columns. */
|
|
29
|
+
includeSphericalHarmonics?: boolean;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Parses a Spark `.radc` chunk into a Mesh Arrow table. */
|
|
34
|
+
export function parseRADChunk(
|
|
35
|
+
data: ArrayBuffer | ArrayBufferView,
|
|
36
|
+
options?: RADChunkDecodeOptions
|
|
37
|
+
): MeshArrowTable {
|
|
38
|
+
return makeGaussianSplatsArrowTable(parseRADChunkToGaussianSplats(data, options));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Parses a Spark `.radc` chunk into decoded Gaussian splat values. */
|
|
42
|
+
export function parseRADChunkToGaussianSplats(
|
|
43
|
+
data: ArrayBuffer | ArrayBufferView,
|
|
44
|
+
options?: RADChunkDecodeOptions
|
|
45
|
+
): GaussianSplats {
|
|
46
|
+
const bytes = getUint8Array(data);
|
|
47
|
+
const metadata = getRADChunkDecodeMetadata(parseRADChunkHeader(bytes), options);
|
|
48
|
+
if (bytes.byteLength < metadata.chunkByteLength) {
|
|
49
|
+
throw new Error('RADLoader: RADC chunk payload is incomplete.');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const splatCount = metadata.count;
|
|
53
|
+
const positions = decodeRADFloatProperty(
|
|
54
|
+
bytes,
|
|
55
|
+
metadata,
|
|
56
|
+
requireRADProperty(metadata, 'center'),
|
|
57
|
+
3
|
|
58
|
+
);
|
|
59
|
+
const opacities = decodeRADAlpha(bytes, metadata, splatCount);
|
|
60
|
+
const rgb = decodeRADRgb(bytes, metadata, splatCount);
|
|
61
|
+
const scales = decodeRADScales(bytes, metadata, splatCount);
|
|
62
|
+
const rotations = decodeRADRotations(bytes, metadata, splatCount);
|
|
63
|
+
const {colors, sphericalHarmonicDcs} = convertRgbToColorColumns(rgb);
|
|
64
|
+
const includeLoDTree = options?.radChunk?.includeLoDTree ?? true;
|
|
65
|
+
const childCounts = includeLoDTree
|
|
66
|
+
? decodeOptionalRADUint16(bytes, metadata, 'child_count')
|
|
67
|
+
: null;
|
|
68
|
+
const childStarts = includeLoDTree
|
|
69
|
+
? decodeOptionalRADUint32(bytes, metadata, 'child_start')
|
|
70
|
+
: null;
|
|
71
|
+
const includeSphericalHarmonics = options?.radChunk?.includeSphericalHarmonics ?? false;
|
|
72
|
+
const sphericalHarmonics = includeSphericalHarmonics
|
|
73
|
+
? decodeRADSphericalHarmonics(bytes, metadata)
|
|
74
|
+
: null;
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
format: 'rad',
|
|
78
|
+
splatCount,
|
|
79
|
+
positions,
|
|
80
|
+
scales,
|
|
81
|
+
rotations,
|
|
82
|
+
colors,
|
|
83
|
+
sphericalHarmonicDcs,
|
|
84
|
+
opacities,
|
|
85
|
+
sphericalHarmonics: sphericalHarmonics?.values,
|
|
86
|
+
sphericalHarmonicsComponentCount: sphericalHarmonics?.componentCount,
|
|
87
|
+
loaderData: {
|
|
88
|
+
format: 'rad',
|
|
89
|
+
base: metadata.base,
|
|
90
|
+
count: metadata.count,
|
|
91
|
+
maxSh: metadata.maxSh ?? 0,
|
|
92
|
+
lodTree: metadata.lodTree ?? false,
|
|
93
|
+
splatEncoding: metadata.splatEncoding,
|
|
94
|
+
childCounts: childCounts ?? undefined,
|
|
95
|
+
childStarts: childStarts ?? undefined
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Decodes the chunk alpha property, falling back to fully opaque splats. */
|
|
101
|
+
function decodeRADAlpha(
|
|
102
|
+
bytes: Uint8Array,
|
|
103
|
+
metadata: RADChunkMetadata,
|
|
104
|
+
splatCount: number
|
|
105
|
+
): Float32Array {
|
|
106
|
+
const property = findRADProperty(metadata, 'alpha');
|
|
107
|
+
if (!property) {
|
|
108
|
+
return new Float32Array(splatCount).fill(1);
|
|
109
|
+
}
|
|
110
|
+
const opacities = decodeRADFloatProperty(bytes, metadata, property, 1);
|
|
111
|
+
for (let splatIndex = 0; splatIndex < opacities.length; splatIndex++) {
|
|
112
|
+
opacities[splatIndex] = decodeRADOpacity(opacities[splatIndex], metadata, property);
|
|
113
|
+
}
|
|
114
|
+
return opacities;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Decodes normal or Spark LoD opacity into the renderer opacity domain. */
|
|
118
|
+
function decodeRADOpacity(
|
|
119
|
+
opacity: number,
|
|
120
|
+
metadata: RADChunkMetadata,
|
|
121
|
+
property: RADChunkProperty
|
|
122
|
+
): number {
|
|
123
|
+
const normalizedOpacity = Math.max(opacity, 0);
|
|
124
|
+
if (
|
|
125
|
+
!metadata.splatEncoding?.lodOpacity ||
|
|
126
|
+
(property.encoding !== 'r8' && property.encoding !== 'r8_delta')
|
|
127
|
+
) {
|
|
128
|
+
return normalizedOpacity;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return normalizedOpacity * 2;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Decodes the chunk RGB property, falling back to white splats. */
|
|
135
|
+
function decodeRADRgb(
|
|
136
|
+
bytes: Uint8Array,
|
|
137
|
+
metadata: RADChunkMetadata,
|
|
138
|
+
splatCount: number
|
|
139
|
+
): Float32Array {
|
|
140
|
+
const property = findRADProperty(metadata, 'rgb');
|
|
141
|
+
if (!property) {
|
|
142
|
+
return new Float32Array(splatCount * 3).fill(1);
|
|
143
|
+
}
|
|
144
|
+
return decodeRADFloatProperty(bytes, metadata, property, 3);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Decodes the chunk scale property, falling back to unit splats. */
|
|
148
|
+
function decodeRADScales(
|
|
149
|
+
bytes: Uint8Array,
|
|
150
|
+
metadata: RADChunkMetadata,
|
|
151
|
+
splatCount: number
|
|
152
|
+
): Float32Array {
|
|
153
|
+
const property = findRADProperty(metadata, 'scales');
|
|
154
|
+
if (!property) {
|
|
155
|
+
return new Float32Array(splatCount * 3).fill(1);
|
|
156
|
+
}
|
|
157
|
+
return decodeRADFloatProperty(bytes, metadata, property, 3);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Decodes the chunk orientation property, falling back to identity rotations. */
|
|
161
|
+
function decodeRADRotations(
|
|
162
|
+
bytes: Uint8Array,
|
|
163
|
+
metadata: RADChunkMetadata,
|
|
164
|
+
splatCount: number
|
|
165
|
+
): Float32Array {
|
|
166
|
+
const property = findRADProperty(metadata, 'orientation');
|
|
167
|
+
if (!property) {
|
|
168
|
+
return makeIdentityRotations(splatCount);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const propertyBytes = getRADPropertyBytes(bytes, metadata, property);
|
|
172
|
+
switch (property.encoding) {
|
|
173
|
+
case 'oct88r8':
|
|
174
|
+
return decodeRADOct88R8Rotations(propertyBytes, splatCount);
|
|
175
|
+
case 'f32':
|
|
176
|
+
case 'f16': {
|
|
177
|
+
const xyz = decodeRADFloatProperty(bytes, metadata, property, 3);
|
|
178
|
+
const rotations = new Float32Array(splatCount * 4);
|
|
179
|
+
for (let splatIndex = 0; splatIndex < splatCount; splatIndex++) {
|
|
180
|
+
const xyzOffset = splatIndex * 3;
|
|
181
|
+
const rotationOffset = splatIndex * 4;
|
|
182
|
+
const x = xyz[xyzOffset + 0];
|
|
183
|
+
const y = xyz[xyzOffset + 1];
|
|
184
|
+
const z = xyz[xyzOffset + 2];
|
|
185
|
+
const w = Math.sqrt(Math.max(0, 1 - x * x - y * y - z * z));
|
|
186
|
+
const normalized = normalizeQuaternion(w, x, y, z);
|
|
187
|
+
rotations.set(normalized, rotationOffset);
|
|
188
|
+
}
|
|
189
|
+
return rotations;
|
|
190
|
+
}
|
|
191
|
+
default:
|
|
192
|
+
throw new Error(`RADLoader: unsupported orientation encoding ${property.encoding}.`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Decodes optional spherical harmonic rest properties from a chunk. */
|
|
197
|
+
function decodeRADSphericalHarmonics(
|
|
198
|
+
bytes: Uint8Array,
|
|
199
|
+
metadata: RADChunkMetadata
|
|
200
|
+
): {values: Float32Array; componentCount: number} | null {
|
|
201
|
+
const shProperties = [
|
|
202
|
+
{property: findRADProperty(metadata, 'sh1'), dimensions: 9},
|
|
203
|
+
{property: findRADProperty(metadata, 'sh2'), dimensions: 15},
|
|
204
|
+
{property: findRADProperty(metadata, 'sh3'), dimensions: 21}
|
|
205
|
+
].filter(
|
|
206
|
+
(entry): entry is {property: RADChunkProperty; dimensions: number} =>
|
|
207
|
+
entry.property !== undefined
|
|
208
|
+
);
|
|
209
|
+
if (shProperties.length === 0) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const decodedProperties = shProperties.map(entry => ({
|
|
214
|
+
values: decodeRADFloatProperty(bytes, metadata, entry.property, entry.dimensions),
|
|
215
|
+
dimensions: entry.dimensions
|
|
216
|
+
}));
|
|
217
|
+
const componentCount = decodedProperties.reduce((total, entry) => total + entry.dimensions, 0);
|
|
218
|
+
const values = new Float32Array(metadata.count * componentCount);
|
|
219
|
+
for (let splatIndex = 0; splatIndex < metadata.count; splatIndex++) {
|
|
220
|
+
let outputOffset = splatIndex * componentCount;
|
|
221
|
+
for (const decodedProperty of decodedProperties) {
|
|
222
|
+
const inputOffset = splatIndex * decodedProperty.dimensions;
|
|
223
|
+
values.set(
|
|
224
|
+
decodedProperty.values.subarray(inputOffset, inputOffset + decodedProperty.dimensions),
|
|
225
|
+
outputOffset
|
|
226
|
+
);
|
|
227
|
+
outputOffset += decodedProperty.dimensions;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return {values, componentCount};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Decodes one numeric RADC property into row-major Float32 values. */
|
|
234
|
+
function decodeRADFloatProperty(
|
|
235
|
+
bytes: Uint8Array,
|
|
236
|
+
metadata: RADChunkMetadata,
|
|
237
|
+
property: RADChunkProperty,
|
|
238
|
+
dimensions: number
|
|
239
|
+
): Float32Array {
|
|
240
|
+
const propertyBytes = getRADPropertyBytes(bytes, metadata, property);
|
|
241
|
+
switch (property.encoding) {
|
|
242
|
+
case 'f32':
|
|
243
|
+
return decodePlanarFloat32(propertyBytes, dimensions, metadata.count);
|
|
244
|
+
case 'f16':
|
|
245
|
+
return decodePlanarFloat16(propertyBytes, dimensions, metadata.count);
|
|
246
|
+
case 'f32_lebytes':
|
|
247
|
+
return decodeLeBytesFloat32(propertyBytes, dimensions, metadata.count);
|
|
248
|
+
case 'f16_lebytes':
|
|
249
|
+
return decodeLeBytesFloat16(propertyBytes, dimensions, metadata.count);
|
|
250
|
+
case 'r8':
|
|
251
|
+
return decodeR8(
|
|
252
|
+
propertyBytes,
|
|
253
|
+
dimensions,
|
|
254
|
+
metadata.count,
|
|
255
|
+
getPropertyMin(metadata, property),
|
|
256
|
+
getPropertyMax(metadata, property)
|
|
257
|
+
);
|
|
258
|
+
case 'r8_delta':
|
|
259
|
+
return decodeR8Delta(
|
|
260
|
+
propertyBytes,
|
|
261
|
+
dimensions,
|
|
262
|
+
metadata.count,
|
|
263
|
+
getPropertyMin(metadata, property),
|
|
264
|
+
getPropertyMax(metadata, property)
|
|
265
|
+
);
|
|
266
|
+
case 's8':
|
|
267
|
+
return decodeS8(
|
|
268
|
+
propertyBytes,
|
|
269
|
+
dimensions,
|
|
270
|
+
metadata.count,
|
|
271
|
+
getPropertyMax(metadata, property)
|
|
272
|
+
);
|
|
273
|
+
case 's8_delta':
|
|
274
|
+
return decodeS8Delta(
|
|
275
|
+
propertyBytes,
|
|
276
|
+
dimensions,
|
|
277
|
+
metadata.count,
|
|
278
|
+
getPropertyMax(metadata, property)
|
|
279
|
+
);
|
|
280
|
+
case 'ln_0r8':
|
|
281
|
+
return decodeScaleR8(
|
|
282
|
+
propertyBytes,
|
|
283
|
+
dimensions,
|
|
284
|
+
metadata.count,
|
|
285
|
+
getPropertyMin(metadata, property),
|
|
286
|
+
getPropertyMax(metadata, property)
|
|
287
|
+
);
|
|
288
|
+
case 'ln_f16':
|
|
289
|
+
return decodeLogFloat16(propertyBytes, dimensions, metadata.count);
|
|
290
|
+
default:
|
|
291
|
+
throw new Error(`RADLoader: unsupported ${property.property} encoding ${property.encoding}.`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Decodes one optional unsigned 16-bit RADC property. */
|
|
296
|
+
function decodeOptionalRADUint16(
|
|
297
|
+
bytes: Uint8Array,
|
|
298
|
+
metadata: RADChunkMetadata,
|
|
299
|
+
propertyName: string
|
|
300
|
+
): Uint16Array | null {
|
|
301
|
+
const property = findRADProperty(metadata, propertyName);
|
|
302
|
+
if (!property) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
if (property.encoding !== 'u16') {
|
|
306
|
+
throw new Error(`RADLoader: unsupported ${propertyName} encoding ${property.encoding}.`);
|
|
307
|
+
}
|
|
308
|
+
const propertyBytes = getRADPropertyBytes(bytes, metadata, property);
|
|
309
|
+
const values = new Uint16Array(metadata.count);
|
|
310
|
+
for (let splatIndex = 0; splatIndex < metadata.count; splatIndex++) {
|
|
311
|
+
values[splatIndex] = propertyBytes[splatIndex * 2] | (propertyBytes[splatIndex * 2 + 1] << 8);
|
|
312
|
+
}
|
|
313
|
+
return values;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Decodes one optional unsigned 32-bit RADC property. */
|
|
317
|
+
function decodeOptionalRADUint32(
|
|
318
|
+
bytes: Uint8Array,
|
|
319
|
+
metadata: RADChunkMetadata,
|
|
320
|
+
propertyName: string
|
|
321
|
+
): Uint32Array | null {
|
|
322
|
+
const property = findRADProperty(metadata, propertyName);
|
|
323
|
+
if (!property) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
if (property.encoding !== 'u32') {
|
|
327
|
+
throw new Error(`RADLoader: unsupported ${propertyName} encoding ${property.encoding}.`);
|
|
328
|
+
}
|
|
329
|
+
const propertyBytes = getRADPropertyBytes(bytes, metadata, property);
|
|
330
|
+
const dataView = new DataView(
|
|
331
|
+
propertyBytes.buffer,
|
|
332
|
+
propertyBytes.byteOffset,
|
|
333
|
+
propertyBytes.byteLength
|
|
334
|
+
);
|
|
335
|
+
const values = new Uint32Array(metadata.count);
|
|
336
|
+
for (let splatIndex = 0; splatIndex < metadata.count; splatIndex++) {
|
|
337
|
+
values[splatIndex] = dataView.getUint32(splatIndex * 4, true);
|
|
338
|
+
}
|
|
339
|
+
return values;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Returns decompressed bytes for one RADC property. */
|
|
343
|
+
function getRADPropertyBytes(
|
|
344
|
+
bytes: Uint8Array,
|
|
345
|
+
metadata: RADChunkMetadata,
|
|
346
|
+
property: RADChunkProperty
|
|
347
|
+
): Uint8Array {
|
|
348
|
+
const paddedByteLength = roundUpToEight(property.bytes);
|
|
349
|
+
const byteOffset = metadata.payloadByteOffset + property.offset;
|
|
350
|
+
const byteEnd = byteOffset + paddedByteLength;
|
|
351
|
+
if (byteEnd > bytes.byteLength || property.offset + paddedByteLength > metadata.payloadBytes) {
|
|
352
|
+
throw new Error(`RADLoader: property ${property.property} exceeds RADC payload byte length.`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const propertyBytes = bytes.subarray(byteOffset, byteOffset + property.bytes);
|
|
356
|
+
if (!property.compression) {
|
|
357
|
+
return propertyBytes;
|
|
358
|
+
}
|
|
359
|
+
if (property.compression !== 'gz') {
|
|
360
|
+
throw new Error(`RADLoader: unsupported property compression ${property.compression}.`);
|
|
361
|
+
}
|
|
362
|
+
return decompressRADProperty(propertyBytes, property.property);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Inflates one Spark RADC property payload. */
|
|
366
|
+
function decompressRADProperty(bytes: Uint8Array, propertyName: string): Uint8Array {
|
|
367
|
+
const arrayBuffer = copyToArrayBuffer(bytes);
|
|
368
|
+
try {
|
|
369
|
+
return new Uint8Array(RAD_PROPERTY_DEFLATE.decompressSync(arrayBuffer));
|
|
370
|
+
} catch {
|
|
371
|
+
try {
|
|
372
|
+
return new Uint8Array(RAD_PROPERTY_RAW_DEFLATE.decompressSync(arrayBuffer));
|
|
373
|
+
} catch {
|
|
374
|
+
throw new Error(`RADLoader: failed to decompress RADC property ${propertyName}.`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Decodes planar Float32 values emitted by Spark RADC. */
|
|
380
|
+
function decodePlanarFloat32(bytes: Uint8Array, dimensions: number, count: number): Float32Array {
|
|
381
|
+
const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
382
|
+
const values = new Float32Array(count * dimensions);
|
|
383
|
+
for (let rowIndex = 0; rowIndex < count; rowIndex++) {
|
|
384
|
+
for (let dimension = 0; dimension < dimensions; dimension++) {
|
|
385
|
+
values[rowIndex * dimensions + dimension] = dataView.getFloat32(
|
|
386
|
+
(dimension * count + rowIndex) * 4,
|
|
387
|
+
true
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return values;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Decodes planar Float16 values emitted by Spark RADC. */
|
|
395
|
+
function decodePlanarFloat16(bytes: Uint8Array, dimensions: number, count: number): Float32Array {
|
|
396
|
+
const values = new Float32Array(count * dimensions);
|
|
397
|
+
for (let rowIndex = 0; rowIndex < count; rowIndex++) {
|
|
398
|
+
for (let dimension = 0; dimension < dimensions; dimension++) {
|
|
399
|
+
const byteOffset = (dimension * count + rowIndex) * 2;
|
|
400
|
+
values[rowIndex * dimensions + dimension] = decodeFloat16(
|
|
401
|
+
bytes[byteOffset] | (bytes[byteOffset + 1] << 8)
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return values;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Decodes Spark's byte-plane reordered Float32 values. */
|
|
409
|
+
function decodeLeBytesFloat32(bytes: Uint8Array, dimensions: number, count: number): Float32Array {
|
|
410
|
+
const values = new Float32Array(count * dimensions);
|
|
411
|
+
const stride = count * dimensions;
|
|
412
|
+
const scratch = new Uint8Array(4);
|
|
413
|
+
const scratchView = new DataView(scratch.buffer);
|
|
414
|
+
for (let rowIndex = 0; rowIndex < count; rowIndex++) {
|
|
415
|
+
for (let dimension = 0; dimension < dimensions; dimension++) {
|
|
416
|
+
const byteOffset = dimension * count + rowIndex;
|
|
417
|
+
for (let byteIndex = 0; byteIndex < 4; byteIndex++) {
|
|
418
|
+
scratch[byteIndex] = bytes[byteOffset + stride * byteIndex];
|
|
419
|
+
}
|
|
420
|
+
values[rowIndex * dimensions + dimension] = scratchView.getFloat32(0, true);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return values;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Decodes Spark's byte-plane reordered Float16 values. */
|
|
427
|
+
function decodeLeBytesFloat16(bytes: Uint8Array, dimensions: number, count: number): Float32Array {
|
|
428
|
+
const values = new Float32Array(count * dimensions);
|
|
429
|
+
const stride = count * dimensions;
|
|
430
|
+
for (let rowIndex = 0; rowIndex < count; rowIndex++) {
|
|
431
|
+
for (let dimension = 0; dimension < dimensions; dimension++) {
|
|
432
|
+
const byteOffset = dimension * count + rowIndex;
|
|
433
|
+
values[rowIndex * dimensions + dimension] = decodeFloat16(
|
|
434
|
+
bytes[byteOffset] | (bytes[byteOffset + stride] << 8)
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return values;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** Decodes R8 quantized planar float values. */
|
|
442
|
+
function decodeR8(
|
|
443
|
+
bytes: Uint8Array,
|
|
444
|
+
dimensions: number,
|
|
445
|
+
count: number,
|
|
446
|
+
min: number,
|
|
447
|
+
max: number
|
|
448
|
+
): Float32Array {
|
|
449
|
+
const values = new Float32Array(count * dimensions);
|
|
450
|
+
const range = max - min;
|
|
451
|
+
for (let rowIndex = 0; rowIndex < count; rowIndex++) {
|
|
452
|
+
for (let dimension = 0; dimension < dimensions; dimension++) {
|
|
453
|
+
values[rowIndex * dimensions + dimension] =
|
|
454
|
+
(bytes[dimension * count + rowIndex] / 255) * range + min;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return values;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** Decodes delta-compressed R8 quantized planar float values. */
|
|
461
|
+
function decodeR8Delta(
|
|
462
|
+
bytes: Uint8Array,
|
|
463
|
+
dimensions: number,
|
|
464
|
+
count: number,
|
|
465
|
+
min: number,
|
|
466
|
+
max: number
|
|
467
|
+
): Float32Array {
|
|
468
|
+
const values = new Float32Array(count * dimensions);
|
|
469
|
+
const range = max - min;
|
|
470
|
+
const last = new Uint8Array(dimensions);
|
|
471
|
+
for (let rowIndex = 0; rowIndex < count; rowIndex++) {
|
|
472
|
+
for (let dimension = 0; dimension < dimensions; dimension++) {
|
|
473
|
+
const byteOffset = dimension * count + rowIndex;
|
|
474
|
+
const value = (last[dimension] + bytes[byteOffset]) & 0xff;
|
|
475
|
+
last[dimension] = value;
|
|
476
|
+
values[rowIndex * dimensions + dimension] = (value / 255) * range + min;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return values;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** Decodes S8 quantized planar float values. */
|
|
483
|
+
function decodeS8(bytes: Uint8Array, dimensions: number, count: number, max: number): Float32Array {
|
|
484
|
+
const values = new Float32Array(count * dimensions);
|
|
485
|
+
for (let rowIndex = 0; rowIndex < count; rowIndex++) {
|
|
486
|
+
for (let dimension = 0; dimension < dimensions; dimension++) {
|
|
487
|
+
const byte = bytes[dimension * count + rowIndex];
|
|
488
|
+
values[rowIndex * dimensions + dimension] = (((byte << 24) >> 24) / 127) * max;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return values;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Decodes delta-compressed S8 quantized planar float values. */
|
|
495
|
+
function decodeS8Delta(
|
|
496
|
+
bytes: Uint8Array,
|
|
497
|
+
dimensions: number,
|
|
498
|
+
count: number,
|
|
499
|
+
max: number
|
|
500
|
+
): Float32Array {
|
|
501
|
+
const values = new Float32Array(count * dimensions);
|
|
502
|
+
const last = new Uint8Array(dimensions);
|
|
503
|
+
for (let rowIndex = 0; rowIndex < count; rowIndex++) {
|
|
504
|
+
for (let dimension = 0; dimension < dimensions; dimension++) {
|
|
505
|
+
const byteOffset = dimension * count + rowIndex;
|
|
506
|
+
const value = (last[dimension] + bytes[byteOffset]) & 0xff;
|
|
507
|
+
last[dimension] = value;
|
|
508
|
+
values[rowIndex * dimensions + dimension] = (((value << 24) >> 24) / 127) * max;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return values;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** Decodes Spark scale8 values to linear Gaussian standard deviations. */
|
|
515
|
+
function decodeScaleR8(
|
|
516
|
+
bytes: Uint8Array,
|
|
517
|
+
dimensions: number,
|
|
518
|
+
count: number,
|
|
519
|
+
min: number,
|
|
520
|
+
max: number
|
|
521
|
+
): Float32Array {
|
|
522
|
+
const values = new Float32Array(count * dimensions);
|
|
523
|
+
for (let rowIndex = 0; rowIndex < count; rowIndex++) {
|
|
524
|
+
for (let dimension = 0; dimension < dimensions; dimension++) {
|
|
525
|
+
values[rowIndex * dimensions + dimension] = decodeScale8(
|
|
526
|
+
bytes[dimension * count + rowIndex],
|
|
527
|
+
min,
|
|
528
|
+
max
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return values;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** Decodes Spark log-Float16 scale values to linear Gaussian standard deviations. */
|
|
536
|
+
function decodeLogFloat16(bytes: Uint8Array, dimensions: number, count: number): Float32Array {
|
|
537
|
+
const logValues = decodePlanarFloat16(bytes, dimensions, count);
|
|
538
|
+
for (let valueIndex = 0; valueIndex < logValues.length; valueIndex++) {
|
|
539
|
+
logValues[valueIndex] = Math.exp(logValues[valueIndex]);
|
|
540
|
+
}
|
|
541
|
+
return logValues;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** Decodes Spark oct88r8 quaternion values into `[w, x, y, z]` rotations. */
|
|
545
|
+
function decodeRADOct88R8Rotations(bytes: Uint8Array, count: number): Float32Array {
|
|
546
|
+
const rotations = new Float32Array(count * 4);
|
|
547
|
+
for (let splatIndex = 0; splatIndex < count; splatIndex++) {
|
|
548
|
+
const byteOffset = splatIndex * 3;
|
|
549
|
+
const [x, y, z, w] = decodeQuatOct888(
|
|
550
|
+
bytes[byteOffset],
|
|
551
|
+
bytes[byteOffset + 1],
|
|
552
|
+
bytes[byteOffset + 2]
|
|
553
|
+
);
|
|
554
|
+
rotations.set(normalizeQuaternion(w, x, y, z), splatIndex * 4);
|
|
555
|
+
}
|
|
556
|
+
return rotations;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** Converts decoded RGB floats to loaders.gl color and SH DC columns. */
|
|
560
|
+
function convertRgbToColorColumns(rgb: Float32Array): {
|
|
561
|
+
colors: Uint8Array;
|
|
562
|
+
sphericalHarmonicDcs: Float32Array;
|
|
563
|
+
} {
|
|
564
|
+
const colors = new Uint8Array(rgb.length);
|
|
565
|
+
const sphericalHarmonicDcs = new Float32Array(rgb.length);
|
|
566
|
+
for (let componentIndex = 0; componentIndex < rgb.length; componentIndex++) {
|
|
567
|
+
const component = rgb[componentIndex];
|
|
568
|
+
colors[componentIndex] = normalizeColorByte(component);
|
|
569
|
+
sphericalHarmonicDcs[componentIndex] = (component - 0.5) / SH_C0;
|
|
570
|
+
}
|
|
571
|
+
return {colors, sphericalHarmonicDcs};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** Returns an identity rotation array. */
|
|
575
|
+
function makeIdentityRotations(count: number): Float32Array {
|
|
576
|
+
const rotations = new Float32Array(count * 4);
|
|
577
|
+
for (let splatIndex = 0; splatIndex < count; splatIndex++) {
|
|
578
|
+
rotations[splatIndex * 4] = 1;
|
|
579
|
+
}
|
|
580
|
+
return rotations;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Decodes Spark's scale8 byte encoding. */
|
|
584
|
+
function decodeScale8(scale: number, min: number, max: number): number {
|
|
585
|
+
if (scale === 0) {
|
|
586
|
+
return 0;
|
|
587
|
+
}
|
|
588
|
+
const logScaleStep = (max - min) / 254;
|
|
589
|
+
return Math.exp(min + (scale - 1) * logScaleStep);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Decodes Spark's oct888 quaternion encoding into `[x, y, z, w]`. */
|
|
593
|
+
function decodeQuatOct888(u: number, v: number, r: number): [number, number, number, number] {
|
|
594
|
+
let x = (u / 255) * 2 - 1;
|
|
595
|
+
let y = (v / 255) * 2 - 1;
|
|
596
|
+
const z = 1 - Math.abs(x) - Math.abs(y);
|
|
597
|
+
const t = Math.max(-z, 0);
|
|
598
|
+
x = x >= 0 ? x - t : x + t;
|
|
599
|
+
y = y >= 0 ? y - t : y + t;
|
|
600
|
+
const length = Math.hypot(x, y, z);
|
|
601
|
+
const axisX = length > Number.EPSILON ? x / length : 1;
|
|
602
|
+
const axisY = length > Number.EPSILON ? y / length : 0;
|
|
603
|
+
const axisZ = length > Number.EPSILON ? z / length : 0;
|
|
604
|
+
const halfTheta = (r / 255) * 0.5 * Math.PI;
|
|
605
|
+
const sinHalfTheta = Math.sin(halfTheta);
|
|
606
|
+
return [axisX * sinHalfTheta, axisY * sinHalfTheta, axisZ * sinHalfTheta, Math.cos(halfTheta)];
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** Returns a named property or throws. */
|
|
610
|
+
function requireRADProperty(metadata: RADChunkMetadata, propertyName: string): RADChunkProperty {
|
|
611
|
+
const property = findRADProperty(metadata, propertyName);
|
|
612
|
+
if (!property) {
|
|
613
|
+
throw new Error(`RADLoader: RADC chunk is missing ${propertyName} property.`);
|
|
614
|
+
}
|
|
615
|
+
return property;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** Returns a named property when present. */
|
|
619
|
+
function findRADProperty(
|
|
620
|
+
metadata: RADChunkMetadata,
|
|
621
|
+
propertyName: string
|
|
622
|
+
): RADChunkProperty | undefined {
|
|
623
|
+
return metadata.properties.find(property => property.property === propertyName);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** Merges source-level splat encoding with chunk-local encoding. */
|
|
627
|
+
function getRADChunkDecodeMetadata(
|
|
628
|
+
metadata: RADChunkMetadata,
|
|
629
|
+
options?: RADChunkDecodeOptions
|
|
630
|
+
): RADChunkMetadata {
|
|
631
|
+
const splatEncoding = options?.radChunk?.splatEncoding;
|
|
632
|
+
if (!splatEncoding) {
|
|
633
|
+
return metadata;
|
|
634
|
+
}
|
|
635
|
+
return {
|
|
636
|
+
...metadata,
|
|
637
|
+
splatEncoding: {
|
|
638
|
+
...splatEncoding,
|
|
639
|
+
...metadata.splatEncoding
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** Returns a required quantization minimum. */
|
|
645
|
+
function getPropertyMin(metadata: RADChunkMetadata, property: RADChunkProperty): number {
|
|
646
|
+
if (property.min === undefined) {
|
|
647
|
+
const fallback = getSplatEncodingMinimum(metadata.splatEncoding, property.property);
|
|
648
|
+
if (fallback !== undefined) {
|
|
649
|
+
return fallback;
|
|
650
|
+
}
|
|
651
|
+
throw new Error(`RADLoader: RADC property ${property.property} is missing min.`);
|
|
652
|
+
}
|
|
653
|
+
return property.min;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** Returns a required quantization maximum. */
|
|
657
|
+
function getPropertyMax(metadata: RADChunkMetadata, property: RADChunkProperty): number {
|
|
658
|
+
if (property.max === undefined) {
|
|
659
|
+
const fallback = getSplatEncodingMaximum(metadata.splatEncoding, property.property);
|
|
660
|
+
if (fallback !== undefined) {
|
|
661
|
+
return fallback;
|
|
662
|
+
}
|
|
663
|
+
throw new Error(`RADLoader: RADC property ${property.property} is missing max.`);
|
|
664
|
+
}
|
|
665
|
+
return property.max;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** Returns a source-level minimum value for a quantized RAD property. */
|
|
669
|
+
function getSplatEncodingMinimum(
|
|
670
|
+
splatEncoding: RADSplatEncoding | undefined,
|
|
671
|
+
propertyName: string
|
|
672
|
+
): number | undefined {
|
|
673
|
+
switch (propertyName) {
|
|
674
|
+
case 'rgb':
|
|
675
|
+
return splatEncoding?.rgbMin;
|
|
676
|
+
case 'scales':
|
|
677
|
+
return splatEncoding?.lnScaleMin;
|
|
678
|
+
default:
|
|
679
|
+
return undefined;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** Returns a source-level maximum value for a quantized RAD property. */
|
|
684
|
+
function getSplatEncodingMaximum(
|
|
685
|
+
splatEncoding: RADSplatEncoding | undefined,
|
|
686
|
+
propertyName: string
|
|
687
|
+
): number | undefined {
|
|
688
|
+
switch (propertyName) {
|
|
689
|
+
case 'rgb':
|
|
690
|
+
return splatEncoding?.rgbMax;
|
|
691
|
+
case 'scales':
|
|
692
|
+
return splatEncoding?.lnScaleMax;
|
|
693
|
+
case 'sh1':
|
|
694
|
+
return splatEncoding?.sh1Max;
|
|
695
|
+
case 'sh2':
|
|
696
|
+
return splatEncoding?.sh2Max;
|
|
697
|
+
case 'sh3':
|
|
698
|
+
return splatEncoding?.sh3Max;
|
|
699
|
+
default:
|
|
700
|
+
return undefined;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** Clamps a normalized color component into an unorm8 byte. */
|
|
705
|
+
function normalizeColorByte(value: number): number {
|
|
706
|
+
return Math.round(Math.min(Math.max(value, 0), 1) * 255);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** Copies a byte view into a standalone ArrayBuffer. */
|
|
710
|
+
function copyToArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
711
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
712
|
+
copy.set(bytes);
|
|
713
|
+
return copy.buffer;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/** Converts an ArrayBuffer-like value into a Uint8Array view. */
|
|
717
|
+
function getUint8Array(data: ArrayBuffer | ArrayBufferView): Uint8Array {
|
|
718
|
+
return data instanceof ArrayBuffer
|
|
719
|
+
? new Uint8Array(data)
|
|
720
|
+
: new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
721
|
+
}
|