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"], }; }