63 lines
2.9 KiB
TypeScript
63 lines
2.9 KiB
TypeScript
import type { ShapeKeyIR, SkinWeightsIR } from "./scene-ir";
|
|
import type { SkinSimplifyPolicy } from "./simplify";
|
|
|
|
export interface NormalizedSkinWeights {
|
|
indices: number[];
|
|
weights: number[];
|
|
droppedInfluences: number;
|
|
}
|
|
|
|
export interface SkinSimplifyGate {
|
|
allowed: boolean;
|
|
reason?: "MISSING_SKIN_ATTRIBUTES" | "INVALID_SKIN_ATTRIBUTES" | "SHAPE_KEYS_REJECTED" | "SHAPE_KEY_DATA_INVALID";
|
|
message?: string;
|
|
normalized?: NormalizedSkinWeights;
|
|
}
|
|
|
|
function finiteWeight(value: number): boolean {
|
|
return Number.isFinite(value) && value >= 0;
|
|
}
|
|
|
|
export function normalizeSkinWeights(vertexCount: number, skin: SkinWeightsIR, policy: SkinSimplifyPolicy): NormalizedSkinWeights {
|
|
if (!Number.isInteger(vertexCount) || vertexCount <= 0 || skin.indices.length !== vertexCount * 4 || skin.weights.length !== vertexCount * 4) {
|
|
throw new Error("skin arrays must contain four influences per vertex");
|
|
}
|
|
const indices: number[] = [];
|
|
const weights: number[] = [];
|
|
let droppedInfluences = 0;
|
|
for (let vertex = 0; vertex < vertexCount; vertex++) {
|
|
const influences = [0, 1, 2, 3].map((slot) => ({
|
|
index: skin.indices[vertex * 4 + slot],
|
|
weight: skin.weights[vertex * 4 + slot],
|
|
})).filter((influence) => finiteWeight(influence.weight) && influence.weight >= policy.minWeight)
|
|
.sort((left, right) => right.weight - left.weight)
|
|
.slice(0, policy.maxInfluences);
|
|
droppedInfluences += 4 - influences.length;
|
|
const total = influences.reduce((sum, influence) => sum + influence.weight, 0);
|
|
if (total <= 0) throw new Error(`vertex ${vertex} has no surviving skin influence`);
|
|
for (let slot = 0; slot < 4; slot++) {
|
|
const influence = influences[slot];
|
|
indices.push(influence?.index ?? 0);
|
|
weights.push(influence ? influence.weight / total : 0);
|
|
}
|
|
}
|
|
return { indices, weights, droppedInfluences };
|
|
}
|
|
|
|
export function evaluateSkinSimplifyGate(vertexCount: number, skin: SkinWeightsIR | undefined, shapeKeys: readonly ShapeKeyIR[] | undefined, policy: SkinSimplifyPolicy): SkinSimplifyGate {
|
|
if (!skin) return { allowed: false, reason: "MISSING_SKIN_ATTRIBUTES", message: "Skin simplification requires exported bone indices, weights and bind matrix" };
|
|
try {
|
|
const normalized = normalizeSkinWeights(vertexCount, skin, policy);
|
|
if (shapeKeys && shapeKeys.length > 0 && policy.shapeKeys === "REJECT") {
|
|
return { allowed: false, reason: "SHAPE_KEYS_REJECTED", message: "Shape keys must be explicitly rejected before skin simplification" };
|
|
}
|
|
if (shapeKeys?.some((shape) => shape.positions.length !== vertexCount * 3)) {
|
|
return { allowed: false, reason: "SHAPE_KEY_DATA_INVALID", message: "Shape key positions do not match the source vertex count" };
|
|
}
|
|
return { allowed: true, normalized };
|
|
}
|
|
catch (error) {
|
|
return { allowed: false, reason: "INVALID_SKIN_ATTRIBUTES", message: error instanceof Error ? error.message : "Invalid skin attributes" };
|
|
}
|
|
}
|