Complete V1 performance and OOM release gates
This commit is contained in:
@@ -11,9 +11,14 @@ import {
|
||||
probeNanoVDBWebGPU,
|
||||
renderNanoVDBFloat32WebGPU,
|
||||
uploadNanoVDBFloat32GridPaged,
|
||||
type NanoVDBMaterialGridUploadsIR,
|
||||
type NanoVDBViewAxis,
|
||||
type NanoVDBWebGPUCapabilityIR,
|
||||
type NanoVDBWebGPUGrid,
|
||||
} from "../render/nanovdb-volume-renderer";
|
||||
import { createResumableHttpNanoVDBRangeSource } from "./nanovdb-stream";
|
||||
|
||||
export const NANOVDB_VIEWPORT_PREVIEW_SIZE = 64;
|
||||
import type { NanoVDBRangeSource } from "./nanovdb-stream";
|
||||
import {
|
||||
commitNanoVDBToOPFS,
|
||||
@@ -32,6 +37,7 @@ export interface NanoVDBViewportAssetIR {
|
||||
manifest: NanoVDBBundleManifestIR;
|
||||
grids: NanoVDBViewportGridPayloadIR[];
|
||||
material?: NanoVDBMaterialIR;
|
||||
viewAxis?: NanoVDBViewAxis;
|
||||
}
|
||||
|
||||
export interface NanoVDBViewportRenderResultIR {
|
||||
@@ -42,6 +48,7 @@ export interface NanoVDBViewportRenderResultIR {
|
||||
width: number;
|
||||
height: number;
|
||||
capability: NanoVDBWebGPUCapabilityIR;
|
||||
viewAxis: NanoVDBViewAxis;
|
||||
}
|
||||
|
||||
export interface NanoVDBViewportProjectContextIR {
|
||||
@@ -49,9 +56,75 @@ export interface NanoVDBViewportProjectContextIR {
|
||||
sourceBlendSha256: string;
|
||||
}
|
||||
|
||||
function residentBytes(payloadBytes: number, pageBytes: number, maxResidentBytes: number): number {
|
||||
const pageCount = Math.ceil(payloadBytes / pageBytes);
|
||||
return Math.min(pageCount, Math.max(1, Math.floor(maxResidentBytes / pageBytes))) * pageBytes;
|
||||
export interface NanoVDBGridResidencyPlanIR {
|
||||
totalResidentBytes: number;
|
||||
maxResidentBytes: number;
|
||||
gridResidentBytes: Readonly<Record<string, number>>;
|
||||
}
|
||||
|
||||
export function planNanoVDBGridResidency(
|
||||
manifestValue: NanoVDBBundleManifestIR,
|
||||
materialValue?: NanoVDBMaterialIR,
|
||||
): NanoVDBGridResidencyPlanIR {
|
||||
const manifest = validateNanoVDBBundleManifest(manifestValue);
|
||||
const grids = materialGridDefinitions(manifest, materialValue ?? manifest.material);
|
||||
const pageBytes = manifest.gpu.pageByteLength;
|
||||
const availableSlots = Math.floor(manifest.gpu.maxResidentBytes / pageBytes);
|
||||
if (availableSlots < grids.length) throw new Error("NANOVDB_GPU_BUDGET_EXCEEDED: material grids require more resident slots than the manifest budget");
|
||||
const requiredSlots = grids.map((grid) => Math.ceil(grid.byteLength / pageBytes));
|
||||
const assignedSlots = grids.map(() => 1);
|
||||
let remainingSlots = availableSlots - grids.length;
|
||||
while (remainingSlots > 0) {
|
||||
let assigned = false;
|
||||
for (let index = 0; index < grids.length && remainingSlots > 0; index++) {
|
||||
if (assignedSlots[index] >= requiredSlots[index]) continue;
|
||||
assignedSlots[index]++;
|
||||
remainingSlots--;
|
||||
assigned = true;
|
||||
}
|
||||
if (!assigned) break;
|
||||
}
|
||||
const gridResidentBytes = Object.fromEntries(grids.map((grid, index) => [grid.name, assignedSlots[index] * pageBytes]));
|
||||
return {
|
||||
totalResidentBytes: Object.values(gridResidentBytes).reduce((sum, bytes) => sum + bytes, 0),
|
||||
maxResidentBytes: manifest.gpu.maxResidentBytes,
|
||||
gridResidentBytes,
|
||||
};
|
||||
}
|
||||
|
||||
function uploadMaterialGrids(
|
||||
device: GPUDevice,
|
||||
asset: NanoVDBViewportAssetIR,
|
||||
material: NanoVDBMaterialIR,
|
||||
residency: NanoVDBGridResidencyPlanIR,
|
||||
uploads: NanoVDBMaterialGridUploadsIR,
|
||||
): void {
|
||||
const uploadedByName = new Map<string, NanoVDBWebGPUGrid>();
|
||||
const upload = (field: "temperature" | "color" | "emission", name: string | undefined): void => {
|
||||
if (!name) return;
|
||||
const existing = uploadedByName.get(name);
|
||||
if (existing) {
|
||||
uploads[field] = existing;
|
||||
return;
|
||||
}
|
||||
const gridPayload = asset.grids.find((candidate) => candidate.name === name);
|
||||
if (!gridPayload) throw new Error(`NANOVDB_STREAM_INCOMPLETE: viewport ${field} payload is missing`);
|
||||
const uploaded = uploadNanoVDBFloat32GridPaged(
|
||||
device,
|
||||
gridPayload.data,
|
||||
asset.manifest.gpu.pageByteLength,
|
||||
residency.gridResidentBytes[name],
|
||||
);
|
||||
uploadedByName.set(name, uploaded);
|
||||
uploads[field] = uploaded;
|
||||
};
|
||||
upload("temperature", material.temperatureGrid);
|
||||
upload("color", material.colorGrid);
|
||||
upload("emission", material.emissionGrid);
|
||||
}
|
||||
|
||||
function disposeMaterialGrids(uploads: NanoVDBMaterialGridUploadsIR): void {
|
||||
for (const uploaded of new Set(Object.values(uploads))) uploaded?.dispose();
|
||||
}
|
||||
|
||||
/** Keeps the WebGPU device alive for a production viewport and rebuilds it after loss. */
|
||||
@@ -69,24 +142,34 @@ export class NanoVDBViewportRenderSession {
|
||||
const asset = validateNanoVDBViewportAsset(value);
|
||||
const grid = densityGrid(asset.manifest);
|
||||
const payload = asset.grids.find((candidate) => candidate.name === grid.name)!.data;
|
||||
const requiredBytes = residentBytes(payload.byteLength, asset.manifest.gpu.pageByteLength, asset.manifest.gpu.maxResidentBytes);
|
||||
const material = asset.material ?? asset.manifest.material;
|
||||
const residency = planNanoVDBGridResidency(asset.manifest, material);
|
||||
const requiredBytes = Math.max(...Object.values(residency.gridResidentBytes));
|
||||
const requiredStorageBuffers = 3 + 2 * [material.temperatureGrid, material.colorGrid, material.emissionGrid].filter(Boolean).length;
|
||||
let device = this.deviceSession.device;
|
||||
if (!device || this.deviceSession.status !== "ready") device = await this.deviceSession.open(requiredBytes);
|
||||
if (!device || this.deviceSession.status !== "ready") device = await this.deviceSession.open(requiredBytes, requiredStorageBuffers);
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
const uploaded = uploadNanoVDBFloat32GridPaged(device, payload, asset.manifest.gpu.pageByteLength, asset.manifest.gpu.maxResidentBytes);
|
||||
const activeDevice = device;
|
||||
const uploaded = uploadNanoVDBFloat32GridPaged(activeDevice, payload, asset.manifest.gpu.pageByteLength, residency.gridResidentBytes[grid.name]);
|
||||
const materialUploads: NanoVDBMaterialGridUploadsIR = {};
|
||||
try {
|
||||
const pixels = await renderNanoVDBFloat32WebGPU(device, uploaded, grid, asset.material ?? asset.manifest.material, width, height);
|
||||
return { dataId: asset.dataId, grid, material: asset.material ?? asset.manifest.material, pixels, width, height, capability: {
|
||||
uploadMaterialGrids(activeDevice, asset, material, residency, materialUploads);
|
||||
const viewAxis = asset.viewAxis ?? "Z";
|
||||
const pixels = await renderNanoVDBFloat32WebGPU(activeDevice, uploaded, grid, material, width, height, materialUploads, viewAxis);
|
||||
return { dataId: asset.dataId, grid, material, pixels, width, height, viewAxis, capability: {
|
||||
available: true,
|
||||
maxStorageBufferBindingSize: Number(device.limits.maxStorageBufferBindingSize),
|
||||
maxBufferSize: Number(device.limits.maxBufferSize),
|
||||
maxStorageBufferBindingSize: Number(activeDevice.limits.maxStorageBufferBindingSize),
|
||||
maxBufferSize: Number(activeDevice.limits.maxBufferSize),
|
||||
} };
|
||||
}
|
||||
catch (error) {
|
||||
if (this.deviceSession.status !== "lost" || attempt !== 0) throw error;
|
||||
device = await this.deviceSession.recover(requiredBytes);
|
||||
device = await this.deviceSession.recover(requiredBytes, requiredStorageBuffers);
|
||||
}
|
||||
finally {
|
||||
uploaded.dispose();
|
||||
disposeMaterialGrids(materialUploads);
|
||||
}
|
||||
finally { uploaded.dispose(); }
|
||||
}
|
||||
throw new Error("VOLUME_SHADER_UNAVAILABLE: WebGPU device recovery exhausted");
|
||||
}
|
||||
@@ -107,12 +190,25 @@ function densityGrid(manifest: NanoVDBBundleManifestIR): NanoVDBGridIR {
|
||||
return grid;
|
||||
}
|
||||
|
||||
async function loadDensityPayload(
|
||||
function materialGridDefinitions(manifest: NanoVDBBundleManifestIR, material = manifest.material): NanoVDBGridIR[] {
|
||||
const fields = [material.densityGrid, material.temperatureGrid, material.colorGrid, material.emissionGrid].filter((name): name is string => Boolean(name));
|
||||
return [...new Set(fields)].map((name) => {
|
||||
const grid = manifest.grids.find((candidate) => candidate.name === name);
|
||||
if (!grid) throw new Error(`NANOVDB_MANIFEST_INVALID: material grid ${name} is missing`);
|
||||
if ((grid.semantic === "DENSITY" || grid.semantic === "TEMPERATURE" || grid.semantic === "EMISSION") && grid.valueType !== "FLOAT32") {
|
||||
throw new Error(`NANOVDB_GRID_UNSUPPORTED: ${grid.semantic} viewport grid must be Float32`);
|
||||
}
|
||||
if (grid.semantic === "COLOR" && grid.valueType !== "VEC3F32") throw new Error("NANOVDB_GRID_UNSUPPORTED: COLOR viewport grid must be Vec3f32");
|
||||
return grid;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadGridPayload(
|
||||
manifest: NanoVDBBundleManifestIR,
|
||||
grid: NanoVDBGridIR,
|
||||
source: NanoVDBRangeSource,
|
||||
signal: AbortSignal,
|
||||
): Promise<ArrayBuffer> {
|
||||
const grid = densityGrid(manifest);
|
||||
const payload = new Uint8Array(grid.byteLength);
|
||||
let copiedBytes = 0;
|
||||
for (const chunk of manifest.chunks) {
|
||||
@@ -130,19 +226,58 @@ async function loadDensityPayload(
|
||||
payload.set(new Uint8Array(data, sourceOffset, overlapLength), targetOffset);
|
||||
copiedBytes += overlapLength;
|
||||
}
|
||||
if (copiedBytes !== grid.byteLength) throw new Error("NANOVDB_STREAM_INCOMPLETE: density grid ranges are incomplete");
|
||||
if (copiedBytes !== grid.byteLength) throw new Error(`NANOVDB_STREAM_INCOMPLETE: ${grid.name} grid ranges are incomplete`);
|
||||
return payload.buffer;
|
||||
}
|
||||
|
||||
export async function loadNanoVDBGridPage(
|
||||
manifestValue: NanoVDBBundleManifestIR,
|
||||
gridName: string,
|
||||
pageIndex: number,
|
||||
source: NanoVDBRangeSource,
|
||||
signal: AbortSignal,
|
||||
): Promise<ArrayBuffer> {
|
||||
const manifest = validateNanoVDBBundleManifest(manifestValue);
|
||||
const grid = manifest.grids.find((candidate) => candidate.name === gridName);
|
||||
if (!grid) throw new Error(`NANOVDB_MANIFEST_INVALID: grid ${gridName} is missing`);
|
||||
const pageBytes = manifest.gpu.pageByteLength;
|
||||
const pageCount = Math.ceil(grid.byteLength / pageBytes);
|
||||
if (!Number.isSafeInteger(pageIndex) || pageIndex < 0 || pageIndex >= pageCount) {
|
||||
throw new Error("NANOVDB_STREAM_INCOMPLETE: requested GPU page is outside the grid");
|
||||
}
|
||||
const localStart = pageIndex * pageBytes;
|
||||
const localEnd = Math.min(grid.byteLength, localStart + pageBytes);
|
||||
const globalStart = grid.byteOffset + localStart;
|
||||
const globalEnd = grid.byteOffset + localEnd;
|
||||
const output = new Uint8Array(localEnd - localStart);
|
||||
let copiedBytes = 0;
|
||||
for (const chunk of manifest.chunks) {
|
||||
const chunkEnd = chunk.byteOffset + chunk.byteLength;
|
||||
const overlapStart = Math.max(globalStart, chunk.byteOffset);
|
||||
const overlapEnd = Math.min(globalEnd, chunkEnd);
|
||||
if (overlapEnd <= overlapStart) continue;
|
||||
if (signal.aborted) throw new DOMException("NanoVDB page load cancelled", "AbortError");
|
||||
const range: NanoVDBRangeIR = { chunkIndex: chunk.index, start: chunk.byteOffset, endExclusive: chunkEnd, sha256: chunk.sha256 };
|
||||
const data = await source(range, signal);
|
||||
await verifyNanoVDBChunk(chunk, data);
|
||||
output.set(new Uint8Array(data, overlapStart - chunk.byteOffset, overlapEnd - overlapStart), overlapStart - globalStart);
|
||||
copiedBytes += overlapEnd - overlapStart;
|
||||
}
|
||||
if (copiedBytes !== output.byteLength) throw new Error(`NANOVDB_STREAM_INCOMPLETE: ${grid.name} GPU page is incomplete`);
|
||||
return output.buffer;
|
||||
}
|
||||
|
||||
export function validateNanoVDBViewportAsset(value: NanoVDBViewportAssetIR): NanoVDBViewportAssetIR {
|
||||
if (!value.dataId || value.dataId.length > 256) throw new Error("NANOVDB_MANIFEST_INVALID: viewport dataId");
|
||||
const manifest = validateNanoVDBBundleManifest(value.manifest);
|
||||
const grid = densityGrid(manifest);
|
||||
const payload = value.grids.find((candidate) => candidate.name === grid.name);
|
||||
if (!payload || payload.data.byteLength !== grid.byteLength) {
|
||||
throw new Error("NANOVDB_STREAM_INCOMPLETE: viewport density payload does not match the manifest");
|
||||
densityGrid(manifest);
|
||||
for (const grid of materialGridDefinitions(manifest, value.material ?? manifest.material)) {
|
||||
const payload = value.grids.find((candidate) => candidate.name === grid.name);
|
||||
if (!payload || payload.data.byteLength !== grid.byteLength) {
|
||||
throw new Error(`NANOVDB_STREAM_INCOMPLETE: viewport ${grid.semantic.toLowerCase()} payload does not match the manifest`);
|
||||
}
|
||||
}
|
||||
return { dataId: value.dataId, manifest, grids: value.grids, material: value.material ?? manifest.material };
|
||||
return { dataId: value.dataId, manifest, grids: value.grids, material: value.material ?? manifest.material, viewAxis: value.viewAxis ?? "Z" };
|
||||
}
|
||||
|
||||
export function cloneNanoVDBViewportAssets(assets: readonly NanoVDBViewportAssetIR[]): NanoVDBViewportAssetIR[] {
|
||||
@@ -168,13 +303,14 @@ export async function loadNanoVDBViewportAsset(
|
||||
const response = await fetcher(manifestUrl, { signal, cache: "no-store" });
|
||||
if (!response.ok) throw new Error(`NANOVDB_STREAM_INCOMPLETE: manifest request returned ${response.status}`);
|
||||
const manifest = validateNanoVDBBundleManifest(await response.json() as NanoVDBBundleManifestIR);
|
||||
const grid = densityGrid(manifest);
|
||||
const grids = materialGridDefinitions(manifest);
|
||||
const source = createResumableHttpNanoVDBRangeSource(bundleUrl, manifest.bundleByteLength, { fetcher, retries: 2, requireStableEtag: true });
|
||||
const payload = await loadDensityPayload(manifest, source, signal);
|
||||
const payloads: NanoVDBViewportGridPayloadIR[] = [];
|
||||
for (const grid of grids) payloads.push({ name: grid.name, data: await loadGridPayload(manifest, grid, source, signal) });
|
||||
return validateNanoVDBViewportAsset({
|
||||
dataId,
|
||||
manifest,
|
||||
grids: [{ name: grid.name, data: payload }],
|
||||
grids: payloads,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -200,11 +336,13 @@ export async function reopenNanoVDBViewportAssetFromOPFS(
|
||||
lastBlockedCode = opened.bindingStatus?.code ?? lastBlockedCode;
|
||||
continue;
|
||||
}
|
||||
const grid = densityGrid(opened.manifest);
|
||||
const grids = materialGridDefinitions(opened.manifest);
|
||||
const payloads: NanoVDBViewportGridPayloadIR[] = [];
|
||||
for (const grid of grids) payloads.push({ name: grid.name, data: await loadGridPayload(opened.manifest, grid, opened.source, signal) });
|
||||
return validateNanoVDBViewportAsset({
|
||||
dataId,
|
||||
manifest: opened.manifest,
|
||||
grids: [{ name: grid.name, data: await loadDensityPayload(opened.manifest, opened.source, signal) }],
|
||||
grids: payloads,
|
||||
});
|
||||
}
|
||||
throw new Error(`${lastBlockedCode}: no current NanoVDB project binding is available`);
|
||||
@@ -240,17 +378,27 @@ export async function renderNanoVDBViewportAsset(
|
||||
const asset = validateNanoVDBViewportAsset(value);
|
||||
const grid = densityGrid(asset.manifest);
|
||||
const payload = asset.grids.find((candidate) => candidate.name === grid.name)!.data;
|
||||
const probe = await probeNanoVDBWebGPU(residentBytes(payload.byteLength, asset.manifest.gpu.pageByteLength, asset.manifest.gpu.maxResidentBytes));
|
||||
const material = asset.material ?? asset.manifest.material;
|
||||
const residency = planNanoVDBGridResidency(asset.manifest, material);
|
||||
const requiredStorageBuffers = 3 + 2 * [material.temperatureGrid, material.colorGrid, material.emissionGrid].filter(Boolean).length;
|
||||
const probe = await probeNanoVDBWebGPU(
|
||||
Math.max(...Object.values(residency.gridResidentBytes)),
|
||||
requiredStorageBuffers,
|
||||
);
|
||||
if (!probe.capability.available || !probe.device) {
|
||||
throw new Error(`VOLUME_SHADER_UNAVAILABLE: ${probe.capability.reason ?? "WebGPU device unavailable"}`);
|
||||
}
|
||||
const uploaded = uploadNanoVDBFloat32GridPaged(probe.device, payload, asset.manifest.gpu.pageByteLength, asset.manifest.gpu.maxResidentBytes);
|
||||
const uploaded = uploadNanoVDBFloat32GridPaged(probe.device, payload, asset.manifest.gpu.pageByteLength, residency.gridResidentBytes[grid.name]);
|
||||
const materialUploads: NanoVDBMaterialGridUploadsIR = {};
|
||||
try {
|
||||
const pixels = await renderNanoVDBFloat32WebGPU(probe.device, uploaded, grid, asset.material ?? asset.manifest.material, width, height);
|
||||
return { dataId: asset.dataId, grid, material: asset.material ?? asset.manifest.material, pixels, width, height, capability: probe.capability };
|
||||
uploadMaterialGrids(probe.device, asset, material, residency, materialUploads);
|
||||
const viewAxis = asset.viewAxis ?? "Z";
|
||||
const pixels = await renderNanoVDBFloat32WebGPU(probe.device, uploaded, grid, material, width, height, materialUploads, viewAxis);
|
||||
return { dataId: asset.dataId, grid, material, pixels, width, height, viewAxis, capability: probe.capability };
|
||||
}
|
||||
finally {
|
||||
uploaded.dispose();
|
||||
disposeMaterialGrids(materialUploads);
|
||||
probe.device.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,18 +23,15 @@ export interface PrincipledVolumeMappingInputIR {
|
||||
|
||||
export interface VolumeMaterialMappingLossIR {
|
||||
code:
|
||||
| "VOLUME_COLOR_GRID_UNSUPPORTED"
|
||||
| "VOLUME_TEMPERATURE_BLACKBODY_UNSUPPORTED"
|
||||
| "VOLUME_EMISSION_GRID_UNSUPPORTED"
|
||||
| "VOLUME_VELOCITY_RENDER_UNSUPPORTED";
|
||||
field: "colorGrid" | "temperatureGrid" | "emissionGrid" | "velocityGrid";
|
||||
field: "velocityGrid";
|
||||
fallback: string;
|
||||
}
|
||||
|
||||
export interface VolumeMaterialMappingResultIR {
|
||||
material: NanoVDBMaterialIR;
|
||||
losses: VolumeMaterialMappingLossIR[];
|
||||
supportedSemantics: Array<"DENSITY_GRID" | "CONSTANT_COLOR" | "CONSTANT_EMISSION" | "ANISOTROPY" | "INTERPOLATION">;
|
||||
supportedSemantics: Array<"DENSITY_GRID" | "COLOR_GRID" | "TEMPERATURE_GRID_BLACKBODY" | "EMISSION_GRID" | "CONSTANT_COLOR" | "CONSTANT_EMISSION" | "ANISOTROPY" | "INTERPOLATION">;
|
||||
}
|
||||
|
||||
function finite(value: number, minimum: number, maximum: number, name: string): number {
|
||||
@@ -48,10 +45,11 @@ function color(value: [number, number, number] | undefined, fallback: [number, n
|
||||
return [...result];
|
||||
}
|
||||
|
||||
function requireGrid(manifest: NanoVDBBundleManifestIR, name: string | undefined, semantic: NanoVDBGridSemantic, field: string): string | undefined {
|
||||
function requireGrid(manifest: NanoVDBBundleManifestIR, name: string | undefined, semantic: NanoVDBGridSemantic, field: string, valueType: "FLOAT32" | "VEC3F32"): string | undefined {
|
||||
if (name === undefined) return undefined;
|
||||
const grid = manifest.grids.find((candidate) => candidate.name === name);
|
||||
if (!grid || grid.semantic !== semantic) throw new Error(`NANOVDB_MANIFEST_INVALID: ${field} must reference a ${semantic} grid`);
|
||||
if (grid.valueType !== valueType) throw new Error(`NANOVDB_GRID_UNSUPPORTED: ${field} requires ${valueType}`);
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -60,16 +58,16 @@ export function mapPrincipledVolumeToNanoVDB(
|
||||
input: PrincipledVolumeMappingInputIR,
|
||||
): VolumeMaterialMappingResultIR {
|
||||
const manifest = validateNanoVDBBundleManifest(sourceManifest);
|
||||
const densityGrid = requireGrid(manifest, input.densityGrid, "DENSITY", "densityGrid");
|
||||
const densityGrid = requireGrid(manifest, input.densityGrid, "DENSITY", "densityGrid", "FLOAT32");
|
||||
if (!densityGrid) throw new Error("NANOVDB_MANIFEST_INVALID: a density grid is required");
|
||||
const colorGrid = requireGrid(manifest, input.colorGrid, "COLOR", "colorGrid");
|
||||
const temperatureGrid = requireGrid(manifest, input.temperatureGrid, "TEMPERATURE", "temperatureGrid");
|
||||
const emissionGrid = requireGrid(manifest, input.emissionGrid, "EMISSION", "emissionGrid");
|
||||
const velocityGrid = requireGrid(manifest, input.velocityGrid, "VELOCITY", "velocityGrid");
|
||||
const colorGrid = requireGrid(manifest, input.colorGrid, "COLOR", "colorGrid", "VEC3F32");
|
||||
const temperatureGrid = requireGrid(manifest, input.temperatureGrid, "TEMPERATURE", "temperatureGrid", "FLOAT32");
|
||||
const emissionGrid = requireGrid(manifest, input.emissionGrid, "EMISSION", "emissionGrid", "FLOAT32");
|
||||
const velocityGrid = requireGrid(manifest, input.velocityGrid, "VELOCITY", "velocityGrid", "VEC3F32");
|
||||
const material: NanoVDBMaterialIR = {
|
||||
densityGrid,
|
||||
...(colorGrid ? { colorGrid } : {}),
|
||||
...(temperatureGrid ? { temperatureGrid } : {}),
|
||||
...(temperatureGrid && input.blackbodyEnabled ? { temperatureGrid } : {}),
|
||||
...(emissionGrid ? { emissionGrid } : {}),
|
||||
...(velocityGrid ? { velocityGrid } : {}),
|
||||
densityScale: finite(input.densityScale, 0, 1_000_000, "densityScale"),
|
||||
@@ -82,13 +80,10 @@ export function mapPrincipledVolumeToNanoVDB(
|
||||
};
|
||||
if (material.interpolation !== "NEAREST" && material.interpolation !== "LINEAR") throw new Error("NANOVDB_MANIFEST_INVALID: interpolation");
|
||||
const losses: VolumeMaterialMappingLossIR[] = [];
|
||||
if (colorGrid) losses.push({ code: "VOLUME_COLOR_GRID_UNSUPPORTED", field: "colorGrid", fallback: "constant color" });
|
||||
if (temperatureGrid && input.blackbodyEnabled) losses.push({ code: "VOLUME_TEMPERATURE_BLACKBODY_UNSUPPORTED", field: "temperatureGrid", fallback: "constant emission color" });
|
||||
if (emissionGrid) losses.push({ code: "VOLUME_EMISSION_GRID_UNSUPPORTED", field: "emissionGrid", fallback: "constant emission color and scale" });
|
||||
if (velocityGrid) losses.push({ code: "VOLUME_VELOCITY_RENDER_UNSUPPORTED", field: "velocityGrid", fallback: "velocity metadata retained without motion rendering" });
|
||||
return {
|
||||
material,
|
||||
losses,
|
||||
supportedSemantics: ["DENSITY_GRID", "CONSTANT_COLOR", "CONSTANT_EMISSION", "ANISOTROPY", "INTERPOLATION"],
|
||||
supportedSemantics: ["DENSITY_GRID", "COLOR_GRID", "TEMPERATURE_GRID_BLACKBODY", "EMISSION_GRID", "CONSTANT_COLOR", "CONSTANT_EMISSION", "ANISOTROPY", "INTERPOLATION"],
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user