Files
ttstd-infinite-canvas/web/src/lib/canvas/director/director-resources.ts
echoD886 ad0775605d fix(canvas): 3D 导演台 - 完成 Issue #305 P0 稳定性治理 (#324)
* fix(canvas): 3D 导演台 - 修复状态回弹与保存恢复生命周期

* style(canvas): 3D 导演台 - 对齐 CI 格式门禁
2026-08-27 10:32:45 +08:00

144 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { AnimationMixer, Material, Mesh, Object3D, Texture } from "three";
type DisposableMaterial = Material & Record<string, unknown>;
// 材质上可能承载纹理的常见槽位;只释放本 owner 真正引用到的纹理。
const TEXTURE_SLOTS = [
"map",
"alphaMap",
"aoMap",
"bumpMap",
"displacementMap",
"emissiveMap",
"envMap",
"lightMap",
"metalnessMap",
"normalMap",
"roughnessMap",
"specularMap",
"clearcoatMap",
"clearcoatNormalMap",
"clearcoatRoughnessMap",
"sheenColorMap",
"sheenRoughnessMap",
"transmissionMap",
"thicknessMap",
"iridescenceMap",
"specularColorMap",
"specularIntensityMap",
] as const;
function collectTextures(material: DisposableMaterial, sink: Set<Texture>) {
TEXTURE_SLOTS.forEach((slot) => {
const value = material[slot];
// 必须真的是 Texture:仅凭「有 dispose」会把 RenderTarget 等误判成纹理并造成错误释放。
if (isDirectorTexture(value)) sink.add(value);
});
}
function isDirectorTexture(value: unknown): value is Texture {
if (!value || typeof value !== "object") return false;
const candidate = value as Partial<Texture>;
return candidate.isTexture === true && typeof candidate.dispose === "function";
}
/**
* GLTF 加载结果的所有权判定。
* SkeletonUtils.clone 与 source 共享 geometry/material/texture,
* 因此被采纳(adopt)时绝不能释放 source —— 共享资源随 owned clone 的最终 cleanup 一并释放。
* 只有未被采纳的晚到/失效 generation 才需要释放 source,避免孤儿泄漏。
*/
export function resolveDirectorLoadOwnership(input: { active: boolean; generation: number; currentGeneration: number }) {
const adopt = input.active && input.generation === input.currentGeneration;
return { adopt, disposeSource: !adopt };
}
/** 释放一组材质及其引用纹理;用于被替换掉的原材质,避免 actor 参考材质造成泄漏。 */
export function disposeDirectorMaterials(materials: Array<Material | null | undefined>) {
const owned = new Set<DisposableMaterial>();
materials.forEach((material) => {
if (material && typeof material.dispose === "function") owned.add(material as DisposableMaterial);
});
const textures = new Set<Texture>();
owned.forEach((material) => collectTextures(material, textures));
textures.forEach((texture) => texture.dispose());
owned.forEach((material) => material.dispose());
}
function collectObject3DResources(root: Object3D | null | undefined, geometries: Set<{ dispose: () => void }>, materials: Set<DisposableMaterial>) {
if (!root) return;
root.traverse((child) => {
const mesh = child as Mesh;
if (mesh.geometry && typeof mesh.geometry.dispose === "function") geometries.add(mesh.geometry);
if (!mesh.material) return;
const list = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
list.forEach((material) => {
if (!material || typeof material.dispose !== "function") return;
materials.add(material as DisposableMaterial);
});
});
}
function disposeCollected(geometries: Set<{ dispose: () => void }>, materials: Set<DisposableMaterial>) {
const textures = new Set<Texture>();
materials.forEach((material) => collectTextures(material, textures));
textures.forEach((texture) => texture.dispose());
materials.forEach((material) => material.dispose());
geometries.forEach((geometry) => geometry.dispose());
}
/**
* 释放一棵 owned Object3D 的 GPU 资源。
* 共享的 geometry/material/texture 用 Set 去重,保证同一资源只 dispose 一次。
* `<primitive object={...}>` 不会代为释放,必须由 owner 显式调用。
*/
export function disposeDirectorObject3D(root: Object3D | null | undefined) {
if (!root) return;
const geometries = new Set<{ dispose: () => void }>();
const materials = new Set<DisposableMaterial>();
collectObject3DResources(root, geometries, materials);
disposeCollected(geometries, materials);
}
/** SkeletonHelper 自带 geometry/material,必须单独释放。 */
export function disposeDirectorHelper(helper: Object3D | null | undefined) {
disposeDirectorObject3D(helper);
}
/** mixer 停止全部动作并解除 root 缓存,避免 clip 绑定泄漏。 */
export function disposeDirectorMixer(mixer: AnimationMixer | null | undefined, root?: Object3D | null) {
if (!mixer) return;
mixer.stopAllAction();
if (root) mixer.uncacheRoot(root as never);
}
/**
* 一次性释放某个模型 generation 拥有的全部资源。
* 只释放 owned clone 与 helper:clone 持有与 source 共享的 GPU 引用,
* source 的 Object3D 层级本身可被 GC,不需要(也不能)在这里再 dispose 一遍。
* model/helper 在同一次去重集合内收集,共享资源恰好 dispose 一次。
*/
export function disposeDirectorModelResources(input: { model?: Object3D | null; helper?: Object3D | null; mixer?: AnimationMixer | null }) {
disposeDirectorMixer(input.mixer, input.model);
const geometries = new Set<{ dispose: () => void }>();
const materials = new Set<DisposableMaterial>();
collectObject3DResources(input.model, geometries, materials);
collectObject3DResources(input.helper, geometries, materials);
disposeCollected(geometries, materials);
}
/**
* 采纳过程中途抛错时的精确清理,生产与测试共用。
* 二选一,绝不 double-dispose:
* - clone 已建成:它已持有与 source 共享的 GPU 引用,只释放 clone(+mixer),source 交给 GC。
* - clone 尚未建成:本次没有任何 owned 资源,只释放这份未被采纳的 source。
*/
export function disposeDirectorAdoptionFailure(input: { clone?: Object3D | null; mixer?: AnimationMixer | null; source?: Object3D | null }) {
if (input.clone) {
disposeDirectorModelResources({ model: input.clone, mixer: input.mixer });
return;
}
disposeDirectorMixer(input.mixer, null);
disposeDirectorObject3D(input.source);
}