Files
workinf_Blender_Wasm/web/app/src/volume/volume-material-mapping.ts
2026-08-14 22:32:09 -04:00

90 lines
4.4 KiB
TypeScript

import {
validateNanoVDBBundleManifest,
type NanoVDBBundleManifestIR,
type NanoVDBGridSemantic,
type NanoVDBMaterialIR,
} from "../../../protocol/volume-vdb";
export interface PrincipledVolumeMappingInputIR {
densityGrid: string;
densityScale: number;
color?: [number, number, number];
colorGrid?: string;
temperatureGrid?: string;
temperatureScale?: number;
blackbodyEnabled?: boolean;
emissionGrid?: string;
emissionColor?: [number, number, number];
emissionScale?: number;
velocityGrid?: string;
anisotropy?: number;
interpolation?: "NEAREST" | "LINEAR";
}
export interface VolumeMaterialMappingLossIR {
code:
| "VOLUME_VELOCITY_RENDER_UNSUPPORTED";
field: "velocityGrid";
fallback: string;
}
export interface VolumeMaterialMappingResultIR {
material: NanoVDBMaterialIR;
losses: VolumeMaterialMappingLossIR[];
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 {
if (!Number.isFinite(value) || value < minimum || value > maximum) throw new Error(`NANOVDB_MANIFEST_INVALID: ${name}`);
return value;
}
function color(value: [number, number, number] | undefined, fallback: [number, number, number], name: string): [number, number, number] {
const result = value ?? fallback;
if (!Array.isArray(result) || result.length !== 3 || result.some((channel) => !Number.isFinite(channel) || channel < 0 || channel > 1_000_000)) throw new Error(`NANOVDB_MANIFEST_INVALID: ${name}`);
return [...result];
}
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;
}
export function mapPrincipledVolumeToNanoVDB(
sourceManifest: NanoVDBBundleManifestIR,
input: PrincipledVolumeMappingInputIR,
): VolumeMaterialMappingResultIR {
const manifest = validateNanoVDBBundleManifest(sourceManifest);
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", "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 && input.blackbodyEnabled ? { temperatureGrid } : {}),
...(emissionGrid ? { emissionGrid } : {}),
...(velocityGrid ? { velocityGrid } : {}),
densityScale: finite(input.densityScale, 0, 1_000_000, "densityScale"),
emissionScale: finite(input.emissionScale ?? 0, 0, 1_000_000, "emissionScale"),
temperatureScale: finite(input.temperatureScale ?? 1, 0, 1_000_000, "temperatureScale"),
anisotropy: finite(input.anisotropy ?? 0, -0.99, 0.99, "anisotropy"),
interpolation: input.interpolation ?? "LINEAR",
color: color(input.color, [0.72, 0.78, 0.86], "color"),
emissionColor: color(input.emissionColor, [1, 1, 1], "emissionColor"),
};
if (material.interpolation !== "NEAREST" && material.interpolation !== "LINEAR") throw new Error("NANOVDB_MANIFEST_INVALID: interpolation");
const losses: VolumeMaterialMappingLossIR[] = [];
if (velocityGrid) losses.push({ code: "VOLUME_VELOCITY_RENDER_UNSUPPORTED", field: "velocityGrid", fallback: "velocity metadata retained without motion rendering" });
return {
material,
losses,
supportedSemantics: ["DENSITY_GRID", "COLOR_GRID", "TEMPERATURE_GRID_BLACKBODY", "EMISSION_GRID", "CONSTANT_COLOR", "CONSTANT_EMISSION", "ANISOTROPY", "INTERPOLATION"],
};
}