mirror of
https://github.com/storytold/spark.git
synced 2026-10-09 00:09:53 +00:00
2f42e2e29c
* Replace for-of loops to reduce memory allocations in hot-path for ply loading * Use temporary THREE.Quaternion and THREE.Vector instances to avoid allocations
922 lines
25 KiB
TypeScript
922 lines
25 KiB
TypeScript
// PLY file format reader
|
|
|
|
export type PlyPropertyType =
|
|
| "char"
|
|
| "uchar"
|
|
| "short"
|
|
| "ushort"
|
|
| "int"
|
|
| "uint"
|
|
| "float"
|
|
| "double";
|
|
|
|
export type PlyElement = {
|
|
name: string;
|
|
count: number;
|
|
properties: Record<string, PlyProperty>;
|
|
};
|
|
|
|
export type PlyProperty = {
|
|
isList: boolean;
|
|
type: PlyPropertyType;
|
|
countType?: PlyPropertyType;
|
|
};
|
|
|
|
// Callback for parseSplats base Gsplat data
|
|
export type SplatCallback = (
|
|
index: number,
|
|
x: number,
|
|
y: number,
|
|
z: number,
|
|
scaleX: number,
|
|
scaleY: number,
|
|
scaleZ: number,
|
|
quatX: number,
|
|
quatY: number,
|
|
quatZ: number,
|
|
quatW: number,
|
|
opacity: number,
|
|
r: number,
|
|
g: number,
|
|
b: number,
|
|
) => void;
|
|
|
|
// Callback for parseSplats SH coefficients
|
|
export type SplatShCallback = (
|
|
index: number,
|
|
sh1: Float32Array,
|
|
sh2?: Float32Array,
|
|
sh3?: Float32Array,
|
|
) => void;
|
|
|
|
// A PlyReader is used to parse PLY files for Gsplat data.
|
|
// It takes a Uint8Array/ArrayBuffer as input fileBytes, parses the text header,
|
|
// and provides a method parseData to iterate over the entire binary data
|
|
// efficiently, or parseSplats to iterate over Gsplat data.
|
|
|
|
export class PlyReader {
|
|
fileBytes: Uint8Array;
|
|
header = "";
|
|
littleEndian = true;
|
|
elements: Record<string, PlyElement> = {};
|
|
comments: string[] = [];
|
|
data: DataView | null = null;
|
|
static defaultPointScale = 0.001;
|
|
|
|
numSplats = 0;
|
|
|
|
// Create a PlyReader from a Uint8Array/ArrayBuffer, no parsing done yet
|
|
constructor({ fileBytes }: { fileBytes: Uint8Array | ArrayBuffer }) {
|
|
this.fileBytes =
|
|
fileBytes instanceof ArrayBuffer ? new Uint8Array(fileBytes) : fileBytes;
|
|
}
|
|
|
|
// Identify and parse the PLY text header (assumed to be <64KB in size).
|
|
// this.elements will contain all the elements in the file, typically
|
|
// "vertex" contains the Gsplat data.
|
|
async parseHeader() {
|
|
const bufferStream = new ReadableStream({
|
|
start: (controller: ReadableStreamController<Uint8Array>) => {
|
|
// Assume the header is less than 64KB
|
|
controller.enqueue(this.fileBytes.slice(0, 65536));
|
|
controller.close();
|
|
},
|
|
});
|
|
const decoder = bufferStream
|
|
.pipeThrough(new TextDecoderStream())
|
|
.getReader();
|
|
|
|
// Find the end of the text section of the PLY file
|
|
this.header = "";
|
|
const headerTerminator = "end_header\n";
|
|
while (true) {
|
|
const { value, done } = await decoder.read();
|
|
if (done) {
|
|
throw new Error("Failed to read header");
|
|
}
|
|
|
|
this.header += value as string;
|
|
const endHeader = this.header.indexOf(headerTerminator);
|
|
if (endHeader >= 0) {
|
|
this.header = this.header.slice(0, endHeader + headerTerminator.length);
|
|
break;
|
|
}
|
|
}
|
|
// Partition the file into header and binary data
|
|
const headerLen = new TextEncoder().encode(this.header).length;
|
|
this.data = new DataView(this.fileBytes.buffer, headerLen);
|
|
|
|
this.elements = {};
|
|
let curElement: PlyElement | null = null;
|
|
this.comments = [];
|
|
|
|
this.header
|
|
.trim()
|
|
.split("\n")
|
|
.forEach((line: string, lineIndex: number) => {
|
|
const trimmedLine = line.trim();
|
|
if (lineIndex === 0) {
|
|
if (trimmedLine !== "ply") {
|
|
throw new Error("Invalid PLY header");
|
|
}
|
|
return;
|
|
}
|
|
if (trimmedLine.length === 0) {
|
|
return; // Skip empty lines
|
|
}
|
|
|
|
const fields = trimmedLine.split(" ");
|
|
switch (fields[0]) {
|
|
case "format":
|
|
if (fields[1] === "binary_little_endian") {
|
|
this.littleEndian = true;
|
|
} else if (fields[1] === "binary_big_endian") {
|
|
this.littleEndian = false;
|
|
} else {
|
|
// ascii formats not supported
|
|
throw new Error(`Unsupported PLY format: ${fields[1]}`);
|
|
}
|
|
if (fields[2] !== "1.0") {
|
|
throw new Error(`Unsupported PLY version: ${fields[2]}`);
|
|
}
|
|
break;
|
|
case "end_header":
|
|
break;
|
|
case "comment":
|
|
this.comments.push(trimmedLine.slice("comment ".length));
|
|
break;
|
|
case "element": {
|
|
const name = fields[1];
|
|
curElement = {
|
|
name,
|
|
count: Number.parseInt(fields[2]),
|
|
properties: {},
|
|
};
|
|
this.elements[name] = curElement;
|
|
break;
|
|
}
|
|
case "property":
|
|
if (curElement == null) {
|
|
throw new Error("Property must be inside an element");
|
|
}
|
|
if (fields[1] === "list") {
|
|
curElement.properties[fields[4]] = {
|
|
isList: true,
|
|
type: fields[3] as PlyPropertyType,
|
|
countType: fields[2] as PlyPropertyType,
|
|
};
|
|
} else {
|
|
curElement.properties[fields[2]] = {
|
|
isList: false,
|
|
type: fields[1] as PlyPropertyType,
|
|
};
|
|
}
|
|
break;
|
|
default:
|
|
// console.warn(`Skipping unsupported PLY keyword: ${fields[0]}`);
|
|
}
|
|
});
|
|
|
|
if (this.elements.vertex) {
|
|
this.numSplats = this.elements.vertex.count;
|
|
}
|
|
}
|
|
|
|
parseData(
|
|
elementCallback: (
|
|
element: PlyElement,
|
|
) =>
|
|
| null
|
|
| ((index: number, item: Record<string, number | number[]>) => void),
|
|
) {
|
|
// Go through the entire binary data of the PLY file, starting at offset 0
|
|
let offset = 0;
|
|
const data = this.data;
|
|
if (data == null) {
|
|
throw new Error("No data to parse");
|
|
}
|
|
|
|
for (const elementName in this.elements) {
|
|
const element = this.elements[elementName];
|
|
const { count, properties } = element;
|
|
const item: Record<string, number | number[]> = {};
|
|
// Construct an array of parser function to parse each property in an item
|
|
const parsers = [];
|
|
for (const [propertyName, property] of Object.entries(properties)) {
|
|
if (!property.isList) {
|
|
item[propertyName] = 0;
|
|
parsers.push(() => {
|
|
item[propertyName] = PARSE_FIELD[property.type](
|
|
data,
|
|
offset,
|
|
this.littleEndian,
|
|
);
|
|
offset += FIELD_BYTES[property.type];
|
|
});
|
|
} else {
|
|
// Property is a list, so parse the count first
|
|
item[propertyName] = [];
|
|
parsers.push(() => {
|
|
const list = item[propertyName] as number[];
|
|
list.length = PARSE_FIELD[property.countType as PlyPropertyType](
|
|
data,
|
|
offset,
|
|
this.littleEndian,
|
|
);
|
|
offset += FIELD_BYTES[property.countType as PlyPropertyType];
|
|
for (let i = 0; i < list.length; i++) {
|
|
list[i] = PARSE_FIELD[property.type](
|
|
data,
|
|
offset,
|
|
this.littleEndian,
|
|
);
|
|
offset += FIELD_BYTES[property.type];
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Parse all the items in the element
|
|
const callback = elementCallback(element) ?? (() => {});
|
|
for (let index = 0; index < count; index++) {
|
|
for (let parserIndex = 0; parserIndex < parsers.length; parserIndex++) {
|
|
parsers[parserIndex]();
|
|
}
|
|
callback(index, item);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse all the Gsplat data in the PLY file in go, invoking the given
|
|
// callbacks for each Gsplat.
|
|
parseSplats(splatCallback: SplatCallback, shCallback?: SplatShCallback) {
|
|
if (this.elements.vertex == null) {
|
|
throw new Error("No vertex element found");
|
|
}
|
|
|
|
let isSuperSplat = false;
|
|
const ssChunks: SSChunk[] = [];
|
|
|
|
let numSh = 0;
|
|
let sh1Props: string[] = [];
|
|
let sh2Props: string[] = [];
|
|
let sh3Props: string[] = [];
|
|
let sh1: Float32Array | undefined = undefined;
|
|
let sh2: Float32Array | undefined = undefined;
|
|
let sh3: Float32Array | undefined = undefined;
|
|
|
|
function prepareSh() {
|
|
// Prepare SH coefficient names and arrays for numSh total SH levels
|
|
const num_f_rest = NUM_SH_TO_NUM_F_REST[numSh];
|
|
sh1Props = new Array(3)
|
|
.fill(null)
|
|
.flatMap((_, k) =>
|
|
[0, 1, 2].map((_, d) => `f_rest_${k + (d * num_f_rest) / 3}`),
|
|
);
|
|
sh2Props = new Array(5)
|
|
.fill(null)
|
|
.flatMap((_, k) =>
|
|
[0, 1, 2].map((_, d) => `f_rest_${3 + k + (d * num_f_rest) / 3}`),
|
|
);
|
|
sh3Props = new Array(7)
|
|
.fill(null)
|
|
.flatMap((_, k) =>
|
|
[0, 1, 2].map((_, d) => `f_rest_${8 + k + (d * num_f_rest) / 3}`),
|
|
);
|
|
sh1 = numSh >= 1 ? new Float32Array(3 * 3) : undefined;
|
|
sh2 = numSh >= 2 ? new Float32Array(5 * 3) : undefined;
|
|
sh3 = numSh >= 3 ? new Float32Array(7 * 3) : undefined;
|
|
}
|
|
|
|
function ssShCallback(
|
|
index: number,
|
|
item: Record<string, number | number[]>,
|
|
) {
|
|
// Decode SH for SuperSplat compressed data
|
|
if (!sh1) {
|
|
throw new Error("Missing sh1");
|
|
}
|
|
for (let i = 0; i < sh1Props.length; i++) {
|
|
sh1[i] = ((item[sh1Props[i]] as number) * 8) / 255 - 4;
|
|
}
|
|
if (sh2) {
|
|
for (let i = 0; i < sh2Props.length; i++) {
|
|
sh2[i] = ((item[sh2Props[i]] as number) * 8) / 255 - 4;
|
|
}
|
|
}
|
|
if (sh3) {
|
|
for (let i = 0; i < sh3Props.length; i++) {
|
|
sh3[i] = ((item[sh3Props[i]] as number) * 8) / 255 - 4;
|
|
}
|
|
}
|
|
shCallback?.(index, sh1, sh2, sh3);
|
|
}
|
|
|
|
function initSuperSplat(element: PlyElement) {
|
|
const {
|
|
min_x,
|
|
min_y,
|
|
min_z,
|
|
max_x,
|
|
max_y,
|
|
max_z,
|
|
min_scale_x,
|
|
min_scale_y,
|
|
min_scale_z,
|
|
max_scale_x,
|
|
max_scale_y,
|
|
max_scale_z,
|
|
} = element.properties;
|
|
if (
|
|
!min_x ||
|
|
!min_y ||
|
|
!min_z ||
|
|
!max_x ||
|
|
!max_y ||
|
|
!max_z ||
|
|
!min_scale_x ||
|
|
!min_scale_y ||
|
|
!min_scale_z ||
|
|
!max_scale_x ||
|
|
!max_scale_y ||
|
|
!max_scale_z
|
|
) {
|
|
throw new Error("Missing PLY chunk properties");
|
|
}
|
|
|
|
// SuperSplat chunks are used to quantize splat data, so we need to store them
|
|
isSuperSplat = true;
|
|
return (index: number, item: Record<string, number | number[]>) => {
|
|
const {
|
|
min_x,
|
|
min_y,
|
|
min_z,
|
|
max_x,
|
|
max_y,
|
|
max_z,
|
|
min_scale_x,
|
|
min_scale_y,
|
|
min_scale_z,
|
|
max_scale_x,
|
|
max_scale_y,
|
|
max_scale_z,
|
|
min_r,
|
|
min_g,
|
|
min_b,
|
|
max_r,
|
|
max_g,
|
|
max_b,
|
|
} = item as Record<string, number>;
|
|
ssChunks.push({
|
|
min_x,
|
|
min_y,
|
|
min_z,
|
|
max_x,
|
|
max_y,
|
|
max_z,
|
|
min_scale_x,
|
|
min_scale_y,
|
|
min_scale_z,
|
|
max_scale_x,
|
|
max_scale_y,
|
|
max_scale_z,
|
|
min_r,
|
|
min_g,
|
|
min_b,
|
|
max_r,
|
|
max_g,
|
|
max_b,
|
|
});
|
|
};
|
|
}
|
|
|
|
function decodeSuperSplat(element: PlyElement) {
|
|
// Decode SuperSplat compressed data in vertex and sh elements
|
|
if (shCallback && element.name === "sh") {
|
|
numSh = getNumSh(element.properties);
|
|
prepareSh();
|
|
return ssShCallback;
|
|
}
|
|
if (element.name !== "vertex") {
|
|
return null;
|
|
}
|
|
|
|
const { packed_position, packed_rotation, packed_scale, packed_color } =
|
|
element.properties;
|
|
if (
|
|
!packed_position ||
|
|
!packed_rotation ||
|
|
!packed_scale ||
|
|
!packed_color
|
|
) {
|
|
throw new Error(
|
|
"Missing PLY properties: packed_position, packed_rotation, packed_scale, packed_color",
|
|
);
|
|
}
|
|
|
|
const SQRT2 = Math.sqrt(2);
|
|
|
|
return (index: number, item: Record<string, number | number[]>) => {
|
|
// SuperSplat data are quantized within chunks with 256 Gsplats each
|
|
const chunk = ssChunks[index >>> 8];
|
|
if (chunk == null) {
|
|
throw new Error("Missing PLY chunk");
|
|
}
|
|
const {
|
|
min_x,
|
|
min_y,
|
|
min_z,
|
|
max_x,
|
|
max_y,
|
|
max_z,
|
|
min_scale_x,
|
|
min_scale_y,
|
|
min_scale_z,
|
|
max_scale_x,
|
|
max_scale_y,
|
|
max_scale_z,
|
|
min_r,
|
|
min_g,
|
|
min_b,
|
|
max_r,
|
|
max_g,
|
|
max_b,
|
|
} = chunk;
|
|
const { packed_position, packed_rotation, packed_scale, packed_color } =
|
|
item as Record<string, number>;
|
|
|
|
const x =
|
|
(((packed_position >>> 21) & 2047) / 2047) * (max_x - min_x) + min_x;
|
|
const y =
|
|
(((packed_position >>> 11) & 1023) / 1023) * (max_y - min_y) + min_y;
|
|
const z = ((packed_position & 2047) / 2047) * (max_z - min_z) + min_z;
|
|
|
|
const r0 = (((packed_rotation >>> 20) & 1023) / 1023 - 0.5) * SQRT2;
|
|
const r1 = (((packed_rotation >>> 10) & 1023) / 1023 - 0.5) * SQRT2;
|
|
const r2 = ((packed_rotation & 1023) / 1023 - 0.5) * SQRT2;
|
|
const rr = Math.sqrt(Math.max(0, 1.0 - r0 * r0 - r1 * r1 - r2 * r2));
|
|
|
|
const rOrder = packed_rotation >>> 30;
|
|
const quatX = rOrder === 0 ? r0 : rOrder === 1 ? rr : r1;
|
|
const quatY = rOrder <= 1 ? r1 : rOrder === 2 ? rr : r2;
|
|
const quatZ = rOrder <= 2 ? r2 : rr;
|
|
const quatW = rOrder === 0 ? rr : r0;
|
|
|
|
const scaleX = Math.exp(
|
|
(((packed_scale >>> 21) & 2047) / 2047) *
|
|
(max_scale_x - min_scale_x) +
|
|
min_scale_x,
|
|
);
|
|
const scaleY = Math.exp(
|
|
(((packed_scale >>> 11) & 1023) / 1023) *
|
|
(max_scale_y - min_scale_y) +
|
|
min_scale_y,
|
|
);
|
|
const scaleZ = Math.exp(
|
|
((packed_scale & 2047) / 2047) * (max_scale_z - min_scale_z) +
|
|
min_scale_z,
|
|
);
|
|
|
|
const r =
|
|
(((packed_color >>> 24) & 255) / 255) *
|
|
((max_r ?? 1) - (min_r ?? 0)) +
|
|
(min_r ?? 0);
|
|
const g =
|
|
(((packed_color >>> 16) & 255) / 255) *
|
|
((max_g ?? 1) - (min_g ?? 0)) +
|
|
(min_g ?? 0);
|
|
const b =
|
|
(((packed_color >>> 8) & 255) / 255) * ((max_b ?? 1) - (min_b ?? 0)) +
|
|
(min_b ?? 0);
|
|
const opacity = (packed_color & 255) / 255;
|
|
|
|
splatCallback(
|
|
index,
|
|
x,
|
|
y,
|
|
z,
|
|
scaleX,
|
|
scaleY,
|
|
scaleZ,
|
|
quatX,
|
|
quatY,
|
|
quatZ,
|
|
quatW,
|
|
opacity,
|
|
r,
|
|
g,
|
|
b,
|
|
);
|
|
};
|
|
}
|
|
|
|
const elementCallback = (element: PlyElement) => {
|
|
if (element.name === "chunk") {
|
|
// "chunk" could conceivably be used for other formats, and we would
|
|
// ideally check for the comment: Generated by SuperSplat 2.*
|
|
// but gsplat also outputs this format without such a comment.
|
|
// In order to support both, let's assume a "chunk" element should
|
|
// be interpreted as this format.
|
|
return initSuperSplat(element);
|
|
}
|
|
if (isSuperSplat) {
|
|
return decodeSuperSplat(element);
|
|
}
|
|
|
|
if (element.name !== "vertex") {
|
|
return null;
|
|
}
|
|
|
|
const {
|
|
x,
|
|
y,
|
|
z,
|
|
scale_0,
|
|
scale_1,
|
|
scale_2,
|
|
rot_0,
|
|
rot_1,
|
|
rot_2,
|
|
rot_3,
|
|
opacity,
|
|
f_dc_0,
|
|
f_dc_1,
|
|
f_dc_2,
|
|
red,
|
|
green,
|
|
blue,
|
|
alpha,
|
|
} = element.properties;
|
|
|
|
if (!x || !y || !z) {
|
|
throw new Error("Missing PLY properties: x, y, z");
|
|
}
|
|
// Pure point cloud PLY files have no scales or rotations
|
|
const hasScales = scale_0 && scale_1 && scale_2;
|
|
const hasRots = rot_0 && rot_1 && rot_2 && rot_3;
|
|
// Quantization scale factor for argb values
|
|
const alphaDiv = alpha != null ? FIELD_SCALE[alpha.type] : 1;
|
|
const redDiv = red != null ? FIELD_SCALE[red.type] : 1;
|
|
const greenDiv = green != null ? FIELD_SCALE[green.type] : 1;
|
|
const blueDiv = blue != null ? FIELD_SCALE[blue.type] : 1;
|
|
|
|
numSh = getNumSh(element.properties);
|
|
prepareSh();
|
|
|
|
return (index: number, item: Record<string, number | number[]>) => {
|
|
const scaleX = hasScales
|
|
? Math.exp(item.scale_0 as number)
|
|
: PlyReader.defaultPointScale;
|
|
const scaleY = hasScales
|
|
? Math.exp(item.scale_1 as number)
|
|
: PlyReader.defaultPointScale;
|
|
const scaleZ = hasScales
|
|
? Math.exp(item.scale_2 as number)
|
|
: PlyReader.defaultPointScale;
|
|
|
|
const quatX = hasRots ? (item.rot_1 as number) : 0;
|
|
const quatY = hasRots ? (item.rot_2 as number) : 0;
|
|
const quatZ = hasRots ? (item.rot_3 as number) : 0;
|
|
const quatW = hasRots ? (item.rot_0 as number) : 1;
|
|
|
|
const op =
|
|
opacity != null
|
|
? 1.0 / (1.0 + Math.exp(-item.opacity as number))
|
|
: alpha != null
|
|
? (item.alpha as number) / alphaDiv
|
|
: 1.0;
|
|
const r =
|
|
f_dc_0 != null
|
|
? (item.f_dc_0 as number) * SH_C0 + 0.5
|
|
: red != null
|
|
? (item.red as number) / redDiv
|
|
: 1.0;
|
|
const g =
|
|
f_dc_1 != null
|
|
? (item.f_dc_1 as number) * SH_C0 + 0.5
|
|
: green != null
|
|
? (item.green as number) / greenDiv
|
|
: 1.0;
|
|
const b =
|
|
f_dc_2 != null
|
|
? (item.f_dc_2 as number) * SH_C0 + 0.5
|
|
: blue != null
|
|
? (item.blue as number) / blueDiv
|
|
: 1.0;
|
|
|
|
splatCallback(
|
|
index,
|
|
item.x as number,
|
|
item.y as number,
|
|
item.z as number,
|
|
scaleX,
|
|
scaleY,
|
|
scaleZ,
|
|
quatX,
|
|
quatY,
|
|
quatZ,
|
|
quatW,
|
|
op,
|
|
r,
|
|
g,
|
|
b,
|
|
);
|
|
|
|
if (shCallback && sh1) {
|
|
if (sh1) {
|
|
for (const [i, key] of sh1Props.entries()) {
|
|
sh1[i] = item[key] as number;
|
|
}
|
|
}
|
|
if (sh2) {
|
|
for (const [i, key] of sh2Props.entries()) {
|
|
sh2[i] = item[key] as number;
|
|
}
|
|
}
|
|
if (sh3) {
|
|
for (const [i, key] of sh3Props.entries()) {
|
|
sh3[i] = item[key] as number;
|
|
}
|
|
}
|
|
shCallback(index, sh1, sh2, sh3);
|
|
}
|
|
};
|
|
};
|
|
|
|
this.parseData(elementCallback);
|
|
}
|
|
|
|
// Inject RGBA values into original PLY file, which can be used to modify
|
|
// the color/opacity of the Gsplats and write out the modified PLY file.
|
|
injectRgba(rgba: Uint8Array) {
|
|
// Go through the entire binary data of the PLY file, starting at offset 0
|
|
let offset = 0;
|
|
const data = this.data;
|
|
if (data == null) {
|
|
throw new Error("No parsed data");
|
|
}
|
|
if (rgba.length !== this.numSplats * 4) {
|
|
throw new Error("Invalid RGBA array length");
|
|
}
|
|
|
|
for (const elementName in this.elements) {
|
|
const element = this.elements[elementName];
|
|
const { count, properties } = element;
|
|
const parsers = [];
|
|
|
|
let rgbaOffset = 0;
|
|
const isVertex = elementName === "vertex";
|
|
if (isVertex) {
|
|
for (const name of ["opacity", "f_dc_0", "f_dc_1", "f_dc_2"]) {
|
|
if (!properties[name] || properties[name].type !== "float") {
|
|
throw new Error(`Can't injectRgba due to property: ${name}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const [propertyName, property] of Object.entries(properties)) {
|
|
if (!property.isList) {
|
|
if (isVertex) {
|
|
if (
|
|
propertyName === "f_dc_0" ||
|
|
propertyName === "f_dc_1" ||
|
|
propertyName === "f_dc_2"
|
|
) {
|
|
const component = Number.parseInt(
|
|
propertyName.slice("f_dc_".length),
|
|
);
|
|
parsers.push(() => {
|
|
// Inject DC coefficients
|
|
const value =
|
|
(rgba[rgbaOffset + component] / 255 - 0.5) / SH_C0;
|
|
SET_FIELD[property.type](
|
|
data,
|
|
offset,
|
|
this.littleEndian,
|
|
value,
|
|
);
|
|
});
|
|
} else if (propertyName === "opacity") {
|
|
parsers.push(() => {
|
|
// Inject opacity sigmoid, clamped to [-100, 100]
|
|
const value = Math.max(
|
|
-100,
|
|
Math.min(
|
|
100,
|
|
-Math.log(1.0 / (rgba[rgbaOffset + 3] / 255) - 1.0),
|
|
),
|
|
);
|
|
SET_FIELD[property.type](
|
|
data,
|
|
offset,
|
|
this.littleEndian,
|
|
value,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
parsers.push(() => {
|
|
offset += FIELD_BYTES[property.type];
|
|
});
|
|
} else {
|
|
parsers.push(() => {
|
|
const length = PARSE_FIELD[property.countType as PlyPropertyType](
|
|
data,
|
|
offset,
|
|
this.littleEndian,
|
|
);
|
|
offset += FIELD_BYTES[property.countType as PlyPropertyType];
|
|
offset += length * FIELD_BYTES[property.type];
|
|
});
|
|
}
|
|
}
|
|
|
|
for (let index = 0; index < count; index++) {
|
|
// Go through all the data and field parsers to compute offset
|
|
for (const parser of parsers) {
|
|
parser();
|
|
}
|
|
if (isVertex) {
|
|
rgbaOffset += 4;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export const SH_C0 = 0.28209479177387814;
|
|
|
|
type FieldParser = (
|
|
data: DataView,
|
|
offset: number,
|
|
littleEndian: boolean,
|
|
) => number;
|
|
type FieldSetter = (
|
|
data: DataView,
|
|
offset: number,
|
|
littleEndian: boolean,
|
|
value: number,
|
|
) => void;
|
|
|
|
const PARSE_FIELD: Record<PlyPropertyType, FieldParser> = {
|
|
char: (data: DataView, offset: number, littleEndian: boolean) => {
|
|
return data.getInt8(offset);
|
|
},
|
|
uchar: (data: DataView, offset: number, littleEndian: boolean) => {
|
|
return data.getUint8(offset);
|
|
},
|
|
short: (data: DataView, offset: number, littleEndian: boolean) => {
|
|
return data.getInt16(offset, littleEndian);
|
|
},
|
|
ushort: (data: DataView, offset: number, littleEndian: boolean) => {
|
|
return data.getUint16(offset, littleEndian);
|
|
},
|
|
int: (data: DataView, offset: number, littleEndian: boolean) => {
|
|
return data.getInt32(offset, littleEndian);
|
|
},
|
|
uint: (data: DataView, offset: number, littleEndian: boolean) => {
|
|
return data.getUint32(offset, littleEndian);
|
|
},
|
|
float: (data: DataView, offset: number, littleEndian: boolean) => {
|
|
return data.getFloat32(offset, littleEndian);
|
|
},
|
|
double: (data: DataView, offset: number, littleEndian: boolean) => {
|
|
return data.getFloat64(offset, littleEndian);
|
|
},
|
|
};
|
|
|
|
const SET_FIELD: Record<PlyPropertyType, FieldSetter> = {
|
|
char: (
|
|
data: DataView,
|
|
offset: number,
|
|
littleEndian: boolean,
|
|
value: number,
|
|
) => {
|
|
data.setInt8(offset, value);
|
|
},
|
|
uchar: (
|
|
data: DataView,
|
|
offset: number,
|
|
littleEndian: boolean,
|
|
value: number,
|
|
) => {
|
|
data.setUint8(offset, value);
|
|
},
|
|
short: (
|
|
data: DataView,
|
|
offset: number,
|
|
littleEndian: boolean,
|
|
value: number,
|
|
) => {
|
|
data.setInt16(offset, value, littleEndian);
|
|
},
|
|
ushort: (
|
|
data: DataView,
|
|
offset: number,
|
|
littleEndian: boolean,
|
|
value: number,
|
|
) => {
|
|
data.setUint16(offset, value, littleEndian);
|
|
},
|
|
int: (
|
|
data: DataView,
|
|
offset: number,
|
|
littleEndian: boolean,
|
|
value: number,
|
|
) => {
|
|
data.setInt32(offset, value, littleEndian);
|
|
},
|
|
uint: (
|
|
data: DataView,
|
|
offset: number,
|
|
littleEndian: boolean,
|
|
value: number,
|
|
) => {
|
|
data.setUint32(offset, value, littleEndian);
|
|
},
|
|
float: (
|
|
data: DataView,
|
|
offset: number,
|
|
littleEndian: boolean,
|
|
value: number,
|
|
) => {
|
|
data.setFloat32(offset, value, littleEndian);
|
|
},
|
|
double: (
|
|
data: DataView,
|
|
offset: number,
|
|
littleEndian: boolean,
|
|
value: number,
|
|
) => {
|
|
data.setFloat64(offset, value, littleEndian);
|
|
},
|
|
};
|
|
|
|
const FIELD_BYTES: Record<PlyPropertyType, number> = {
|
|
char: 1,
|
|
uchar: 1,
|
|
short: 2,
|
|
ushort: 2,
|
|
int: 4,
|
|
uint: 4,
|
|
float: 4,
|
|
double: 8,
|
|
};
|
|
|
|
const FIELD_SCALE: Record<PlyPropertyType, number> = {
|
|
char: 127,
|
|
uchar: 255,
|
|
short: 32767,
|
|
ushort: 65535,
|
|
int: 2147483647,
|
|
uint: 4294967295,
|
|
float: 1,
|
|
double: 1,
|
|
};
|
|
|
|
const NUM_F_REST_TO_NUM_SH: Record<number, number> = {
|
|
0: 0,
|
|
9: 1,
|
|
24: 2,
|
|
45: 3,
|
|
};
|
|
const NUM_SH_TO_NUM_F_REST: Record<number, number> = {
|
|
0: 0,
|
|
1: 9,
|
|
2: 24,
|
|
3: 45,
|
|
};
|
|
|
|
function getNumSh(properties: Record<string, PlyProperty>) {
|
|
let num_f_rest = 0;
|
|
while (properties[`f_rest_${num_f_rest}`]) {
|
|
num_f_rest += 1;
|
|
}
|
|
const numSh = NUM_F_REST_TO_NUM_SH[num_f_rest];
|
|
if (numSh == null) {
|
|
throw new Error(`Unsupported number of SH coefficients: ${num_f_rest}`);
|
|
}
|
|
return numSh;
|
|
}
|
|
|
|
type SSChunk = {
|
|
min_x: number;
|
|
min_y: number;
|
|
min_z: number;
|
|
max_x: number;
|
|
max_y: number;
|
|
max_z: number;
|
|
min_scale_x: number;
|
|
min_scale_y: number;
|
|
min_scale_z: number;
|
|
max_scale_x: number;
|
|
max_scale_y: number;
|
|
max_scale_z: number;
|
|
min_r?: number;
|
|
min_g?: number;
|
|
min_b?: number;
|
|
max_r?: number;
|
|
max_g?: number;
|
|
max_b?: number;
|
|
};
|