Files
ttstd-infinite-canvas/web/test/director-clay-materials.test.ts
VicoHu 5bda548fe9 fix(canvas): 3D 导演台 - 修复白膜视频导出渲染循环逐帧崩溃 (#439)
导出白膜视频得到的是 0 秒视频:文件实测仅 1 帧(ffprobe
nb_read_frames=1,duration=0.001s),却无任何报错并照常回写画布。

根因:applyClaySceneMaterials 把场景内所有非 actor mesh 的材质换成
clay MeshStandardMaterial,drei Grid 的 GridMaterial(带 uniforms 的
ShaderMaterial)也在其中。Grid 组件内部 useFrame 每帧读取
material.uniforms.worldCamProjPosition,材质被换后 uniforms 为
undefined,渲染循环逐帧抛 TypeError,R3F 不再产出新帧,
captureStream 只录到启动前手动 gl.render 的那一帧;5 秒后
setTimeout 照常 stop,残缺视频被当作成功结果。开启轨迹显示
(drei Line 同为 uniforms 材质)或视口白膜显示模式时同样触发。

修复:
- applyClaySceneMaterials 跳过带 uniforms 的 ShaderMaterial,
  并抽到 lib/canvas/director/director-clay-materials.ts 补回归测试;
- recordCanvas 加固:录制期间监听 window 未捕获错误,首个渲染
  错误立即中止并报错;录制结束用 <video> 探测真实时长(Chrome
  MediaRecorder 的 webm 无时长头,需 seek 到末尾),低于预期一半
  报「时长异常」,杜绝残缺视频再被静默回写。
2026-09-08 16:58:41 +08:00

57 lines
2.4 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 { describe, expect, test } from "bun:test";
import { Line, LineBasicMaterial, Mesh, MeshBasicMaterial, MeshStandardMaterial, PlaneGeometry, Scene, ShaderMaterial, type Material } from "three";
import { applyClaySceneMaterials } from "../src/lib/canvas/director/director-clay-materials";
const geometry = new PlaneGeometry(1, 1);
describe("导演台白膜材质切换", () => {
test("标准材质 mesh 换成 clay,恢复后还原为原材质", () => {
const scene = new Scene();
const original = new MeshStandardMaterial({ color: "#ff0000" });
const mesh = new Mesh(geometry, original);
scene.add(mesh);
const restore = applyClaySceneMaterials(scene);
expect(mesh.material).not.toBe(original);
expect(mesh.material).toBeInstanceOf(MeshStandardMaterial);
restore();
expect(mesh.material).toBe(original);
});
test("带 uniforms 的 shader 材质保持原样(Grid 逐帧崩溃回归)", () => {
const scene = new Scene();
const shader = new ShaderMaterial({ uniforms: { worldCamProjPosition: { value: null } } });
const grid = new Mesh(geometry, shader);
scene.add(grid);
const restore = applyClaySceneMaterials(scene);
expect(grid.material).toBe(shader);
restore();
expect(grid.material).toBe(shader);
});
test("directorActor mesh 与非 Mesh 对象不受影响", () => {
const scene = new Scene();
const actorMaterial = new MeshBasicMaterial();
const actor = new Mesh(geometry, actorMaterial);
actor.userData.directorActor = true;
const line = new Line(geometry, new LineBasicMaterial());
scene.add(actor, line);
const restore = applyClaySceneMaterials(scene);
expect(actor.material).toBe(actorMaterial);
expect(line.material).toBeInstanceOf(LineBasicMaterial);
restore();
expect(actor.material).toBe(actorMaterial);
});
test("材质数组混入 uniforms 材质时整组跳过", () => {
const scene = new Scene();
const materials: Material[] = [new MeshStandardMaterial(), new ShaderMaterial({ uniforms: {} })];
const mesh = new Mesh(geometry, materials);
scene.add(mesh);
const restore = applyClaySceneMaterials(scene);
expect(mesh.material).toBe(materials);
restore();
expect(mesh.material).toBe(materials);
});
});