118 lines
7.1 KiB
TypeScript
118 lines
7.1 KiB
TypeScript
import type { NanoVDBFloat32TreeLayoutIR, NanoVDBGridIR } from "../../../protocol/volume-vdb";
|
|
|
|
export interface NanoVDBSampleIR { value: number; active: boolean }
|
|
|
|
type NanoVDBNearestLocation =
|
|
| { sample: NanoVDBSampleIR }
|
|
| { leaf: number; voxel: number };
|
|
|
|
export class NanoVDBFloat32Sampler {
|
|
private readonly view: DataView;
|
|
private readonly layout: NanoVDBFloat32TreeLayoutIR;
|
|
private readonly root: number;
|
|
|
|
constructor(payload: ArrayBuffer, grid: NanoVDBGridIR, layout: NanoVDBFloat32TreeLayoutIR | undefined) {
|
|
if (grid.valueType !== "FLOAT32" || !layout) throw new Error("NANOVDB_GRID_UNSUPPORTED: Float32 tree layout is required");
|
|
if (payload.byteLength !== grid.byteLength || payload.byteLength < layout.gridDataBytes + layout.treeDataBytes) throw new Error("NANOVDB_STREAM_INCOMPLETE: Float32 grid payload length mismatch");
|
|
this.view = new DataView(payload);
|
|
this.layout = layout;
|
|
if (this.u32(0) !== 0x6f6e614e || this.u32(4) !== 0x31424456) throw new Error("NANOVDB_MANIFEST_INVALID: NanoVDB grid magic mismatch");
|
|
if ((this.u32(16) >>> 21) !== 32) throw new Error("NANOVDB_GRID_UNSUPPORTED: NanoVDB major version is unsupported");
|
|
if (this.u64(32) !== BigInt(payload.byteLength)) throw new Error("NANOVDB_STREAM_INCOMPLETE: NanoVDB GridData size mismatch");
|
|
const tree = layout.gridDataBytes;
|
|
const rootOffset = this.i64(tree + layout.treeRootOffsetOffset);
|
|
if (rootOffset <= 0n || rootOffset > BigInt(payload.byteLength - layout.rootDataBytes)) throw new Error("NANOVDB_MANIFEST_INVALID: NanoVDB root offset is outside the payload");
|
|
this.root = tree + Number(rootOffset);
|
|
const tableSize = this.u32(this.root + layout.rootTableSizeOffset);
|
|
this.range(this.root + layout.rootDataBytes, tableSize * layout.rootTileBytes);
|
|
}
|
|
|
|
nearest(coord: readonly [number, number, number]): NanoVDBSampleIR {
|
|
const location = this.nearestLocation(coord);
|
|
if ("sample" in location) return location.sample;
|
|
return {
|
|
value: this.f32(location.leaf + this.layout.leafValuesOffset + location.voxel * 4),
|
|
active: this.mask(location.leaf + this.layout.leafValueMaskOffset, location.voxel),
|
|
};
|
|
}
|
|
|
|
leafByteOffset(coord: readonly [number, number, number]): number | null {
|
|
const location = this.nearestLocation(coord);
|
|
return "leaf" in location ? location.leaf : null;
|
|
}
|
|
|
|
private nearestLocation(coord: readonly [number, number, number]): NanoVDBNearestLocation {
|
|
if (coord.some((value) => !Number.isSafeInteger(value) || value < -0x8000_0000 || value > 0x7fff_ffff)) throw new Error("NANOVDB_MANIFEST_INVALID: sample coordinate is outside int32");
|
|
const tableSize = this.u32(this.root + this.layout.rootTableSizeOffset);
|
|
const key = this.rootKey(coord);
|
|
let low = 0;
|
|
let high = tableSize - 1;
|
|
let tile = -1;
|
|
while (low <= high) {
|
|
const middle = (low + high) >>> 1;
|
|
const address = this.root + this.layout.rootDataBytes + middle * this.layout.rootTileBytes;
|
|
const candidate = this.u64(address + this.layout.rootTileKeyOffset);
|
|
if (candidate === key) { tile = address; break; }
|
|
// NanoVDB root tiles are serialized in descending key order.
|
|
if (candidate > key) low = middle + 1;
|
|
else high = middle - 1;
|
|
}
|
|
if (tile < 0) return { sample: { value: this.view.getFloat32(this.root + 28, true), active: false } };
|
|
const child = this.i64(tile + this.layout.rootTileChildOffset);
|
|
if (child === 0n) return { sample: { value: this.f32(tile + this.layout.rootTileValueOffset), active: this.u32(tile + this.layout.rootTileStateOffset) !== 0 } };
|
|
const upper = this.child(this.root, child, this.layout.upperNodeBytes);
|
|
const upperOffset = (((coord[0] >>> 0 & 4095) >>> 7) << 10) | (((coord[1] >>> 0 & 4095) >>> 7) << 5) | ((coord[2] >>> 0 & 4095) >>> 7);
|
|
const upperSample = this.internal(upper, upperOffset, this.layout.upperValueMaskOffset, this.layout.upperChildMaskOffset, this.layout.upperTableOffset, this.layout.lowerNodeBytes);
|
|
if ("sample" in upperSample) return upperSample;
|
|
const lower = upperSample.child;
|
|
const lowerOffset = (((coord[0] >>> 0 & 127) >>> 3) << 8) | (((coord[1] >>> 0 & 127) >>> 3) << 4) | ((coord[2] >>> 0 & 127) >>> 3);
|
|
const lowerSample = this.internal(lower, lowerOffset, this.layout.lowerValueMaskOffset, this.layout.lowerChildMaskOffset, this.layout.lowerTableOffset, this.layout.leafNodeBytes);
|
|
if ("sample" in lowerSample) return lowerSample;
|
|
const leaf = lowerSample.child;
|
|
const voxel = ((coord[0] >>> 0 & 7) << 6) | ((coord[1] >>> 0 & 7) << 3) | (coord[2] >>> 0 & 7);
|
|
return { leaf, voxel };
|
|
}
|
|
|
|
linear(coord: readonly [number, number, number]): NanoVDBSampleIR {
|
|
const base = coord.map(Math.floor) as [number, number, number];
|
|
const fraction = coord.map((value, index) => value - base[index]) as [number, number, number];
|
|
let value = 0;
|
|
let active = false;
|
|
for (let x = 0; x < 2; x++) for (let y = 0; y < 2; y++) for (let z = 0; z < 2; z++) {
|
|
const sample = this.nearest([base[0] + x, base[1] + y, base[2] + z]);
|
|
const weight = (x ? fraction[0] : 1 - fraction[0]) * (y ? fraction[1] : 1 - fraction[1]) * (z ? fraction[2] : 1 - fraction[2]);
|
|
value += sample.value * weight;
|
|
active ||= sample.active;
|
|
}
|
|
return { value, active };
|
|
}
|
|
|
|
private internal(node: number, index: number, valueMaskOffset: number, childMaskOffset: number, tableOffset: number, childBytes: number): { child: number } | { sample: NanoVDBSampleIR } {
|
|
if (!this.mask(node + childMaskOffset, index)) return { sample: { value: this.f32(node + tableOffset + index * 8), active: this.mask(node + valueMaskOffset, index) } };
|
|
return { child: this.child(node, this.i64(node + tableOffset + index * 8), childBytes) };
|
|
}
|
|
|
|
private child(parent: number, offset: bigint, bytes: number): number {
|
|
if (offset <= 0n || offset > BigInt(this.view.byteLength)) throw new Error("NANOVDB_MANIFEST_INVALID: NanoVDB child offset is invalid");
|
|
const child = parent + Number(offset);
|
|
this.range(child, bytes);
|
|
return child;
|
|
}
|
|
|
|
private rootKey(coord: readonly number[]): bigint {
|
|
const x = BigInt(coord[0] >>> 0) >> 12n;
|
|
const y = BigInt(coord[1] >>> 0) >> 12n;
|
|
const z = BigInt(coord[2] >>> 0) >> 12n;
|
|
return z | (y << 21n) | (x << 42n);
|
|
}
|
|
|
|
private mask(address: number, index: number): boolean { return (this.u32(address + (index >>> 5) * 4) & (1 << (index & 31))) !== 0; }
|
|
private u32(address: number): number { this.range(address, 4); return this.view.getUint32(address, true); }
|
|
private f32(address: number): number { this.range(address, 4); return this.view.getFloat32(address, true); }
|
|
private u64(address: number): bigint { this.range(address, 8); return this.view.getBigUint64(address, true); }
|
|
private i64(address: number): bigint { this.range(address, 8); return this.view.getBigInt64(address, true); }
|
|
private range(address: number, bytes: number): void {
|
|
if (!Number.isSafeInteger(address) || !Number.isSafeInteger(bytes) || address < 0 || bytes < 0 || address > this.view.byteLength - bytes) throw new Error("NANOVDB_MANIFEST_INVALID: NanoVDB address is outside the grid payload");
|
|
}
|
|
}
|