mirror of
https://github.com/storytold/spark.git
synced 2026-10-22 08:16:06 +00:00
Initial commit
This commit is contained in:
+930
@@ -0,0 +1,930 @@
|
||||
// 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 (const parser of parsers) {
|
||||
parser();
|
||||
}
|
||||
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 (const [i, key] of sh1Props.entries()) {
|
||||
sh1[i] = ((item[key] as number) * 8) / 255 - 4;
|
||||
}
|
||||
if (sh2) {
|
||||
for (const [i, key] of sh2Props.entries()) {
|
||||
sh2[i] = ((item[key] as number) * 8) / 255 - 4;
|
||||
}
|
||||
}
|
||||
if (sh3) {
|
||||
for (const [i, key] of sh3Props.entries()) {
|
||||
sh3[i] = ((item[key] 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,
|
||||
min_r,
|
||||
min_g,
|
||||
min_b,
|
||||
max_r,
|
||||
max_g,
|
||||
max_b,
|
||||
} = 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 ||
|
||||
!min_r ||
|
||||
!min_g ||
|
||||
!min_b ||
|
||||
!max_r ||
|
||||
!max_g ||
|
||||
!max_b
|
||||
) {
|
||||
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 - min_r) + min_r;
|
||||
const g =
|
||||
(((packed_color >>> 16) & 255) / 255) * (max_g - min_g) + min_g;
|
||||
const b =
|
||||
(((packed_color >>> 8) & 255) / 255) * (max_b - min_b) + min_b;
|
||||
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") {
|
||||
if (
|
||||
this.comments.some((comment) =>
|
||||
comment.toLowerCase().includes("supersplat"),
|
||||
)
|
||||
) {
|
||||
// SuperSplat compressed format detected
|
||||
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;
|
||||
};
|
||||
Reference in New Issue
Block a user