threejs-compositions
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseThree.js in Editframe Compositions
在Editframe合成项目中使用Three.js
Drive Three.js scenes from Editframe's timeline via . The scene is a pure function of composition time — no internal clock — making it fully scrubable, seekable, and renderable to video.
addFrameTask通过由Editframe的时间轴驱动Three.js场景。场景是合成时间的纯函数——没有内部时钟——因此完全支持拖动时间轴预览、可跳转,并且可渲染为视频。
addFrameTaskArchitecture
架构
EFTimegroup.addFrameTask(({ ownCurrentTimeMs, durationMs }) => {
scene.update(ownCurrentTimeMs, durationMs);
})The Three.js renderer targets a inside the timegroup. The composition provides timing; the canvas provides visuals.
<canvas>EFTimegroup.addFrameTask(({ ownCurrentTimeMs, durationMs }) => {
scene.update(ownCurrentTimeMs, durationMs);
})Three.js渲染器以时间组内的为目标。合成项目提供时序控制;画布提供视觉内容。
<canvas>Scene Module Pattern
场景模块模式
Create a standalone module that exports a factory function:
typescript
// my-scene.ts
import * as THREE from "three";
export function createMyScene(canvas: HTMLCanvasElement) {
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: true,
alpha: true,
preserveDrawingBuffer: true, // REQUIRED for renderToVideo
});
// ... scene setup ...
function update(timeMs: number, durationMs: number) {
// Position everything deterministically based on timeMs
// NO Math.random() for positions (breaks scrubbing)
// NO internal clocks or requestAnimationFrame
renderer.render(scene, camera);
renderer.getContext().finish(); // REQUIRED for renderToVideo
}
function resize(w: number, h: number) {
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
function dispose() { /* clean up GPU resources */ }
return { update, resize, dispose };
}创建一个独立模块,导出工厂函数:
typescript
// my-scene.ts
import * as THREE from "three";
export function createMyScene(canvas: HTMLCanvasElement) {
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: true,
alpha: true,
preserveDrawingBuffer: true, // REQUIRED for renderToVideo
});
// ... 场景设置 ...
function update(timeMs: number, durationMs: number) {
// 根据timeMs确定性地设置所有元素位置
// 不要使用Math.random()设置位置(会破坏拖动预览功能)
// 不要使用内部时钟或requestAnimationFrame
renderer.render(scene, camera);
renderer.getContext().finish(); // REQUIRED for renderToVideo
}
function resize(w: number, h: number) {
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
function dispose() { /* 清理GPU资源 */ }
return { update, resize, dispose };
}Critical Requirements
关键要求
-
— Without this, the canvas buffer is cleared after presenting. Frame capture reads empty pixels.
preserveDrawingBuffer: true -
after render — Forces all GL operations to complete before the frame is captured. Without this, the capture may read stale content.
gl.finish() -
Pure function of time —must produce the same result for the same input. Use deterministic math (not
update(timeMs)for positions). Particle systems should derive positions fromMath.random(), not accumulated state.timeMs -
No internal animation loop — No. The composition's
requestAnimationFramedrives all updates.addFrameTask
-
— 没有此设置,画布缓冲区会在画面展示后被清除,帧捕获会读取到空像素。
preserveDrawingBuffer: true -
渲染后调用— 强制所有GL操作在帧捕获前完成。没有此设置,捕获可能会读取到过期内容。
gl.finish() -
基于时间的纯函数 —对于相同输入必须产生相同结果。使用确定性数学运算(不要用
update(timeMs)设置位置)。粒子系统应从Math.random()推导位置,而非累积状态。timeMs -
无内部动画循环 — 不要使用。合成项目的
requestAnimationFrame驱动所有更新。addFrameTask
Integration with React Components
与React组件集成
Prime Instance (live playback)
主实例(实时播放)
Set up the scene directly in a , after the dynamic import resolves:
useEffecttsx
useEffect(() => {
if (!isClient) return;
const container = containerRef.current;
if (!container) return;
let scene = null;
const setup = async () => {
const { createMyScene } = await import("./my-scene");
const canvas = container.querySelector("canvas");
const tg = container.querySelector("ef-timegroup");
if (!canvas || !tg) return;
scene = createMyScene(canvas);
const { width, height } = container.getBoundingClientRect();
scene.resize(width, height);
tg.addFrameTask(({ ownCurrentTimeMs, durationMs }) => {
scene.update(ownCurrentTimeMs, durationMs);
});
};
setup();
return () => scene?.dispose();
}, [isClient]);在中直接设置场景,等待动态导入完成后执行:
useEffecttsx
useEffect(() => {
if (!isClient) return;
const container = containerRef.current;
if (!container) return;
let scene = null;
const setup = async () => {
const { createMyScene } = await import("./my-scene");
const canvas = container.querySelector("canvas");
const tg = container.querySelector("ef-timegroup");
if (!canvas || !tg) return;
scene = createMyScene(canvas);
const { width, height } = container.getBoundingClientRect();
scene.resize(width, height);
tg.addFrameTask(({ ownCurrentTimeMs, durationMs }) => {
scene.update(ownCurrentTimeMs, durationMs);
});
};
setup();
return () => scene?.dispose();
}, [isClient]);Render Clones (for renderToVideo)
渲染克隆(用于renderToVideo)
renderToVideouseEffectinitializertsx
// Inside the setup function, AFTER creating the prime scene:
tg.initializer = (instance) => {
if (instance === tg) return; // skip prime, already set up
let cloneScene = null;
instance.addFrameTask(({ ownCurrentTimeMs, durationMs }) => {
if (!cloneScene) {
// Lazy creation (initializer must be <100ms)
const cvs = instance.querySelector("canvas");
if (!cvs) return;
cloneScene = createMyScene(cvs);
const rect = cvs.getBoundingClientRect();
cloneScene.resize(rect.width || cvs.clientWidth || 800,
rect.height || cvs.clientHeight || 500);
}
cloneScene.update(ownCurrentTimeMs, durationMs);
});
};Key points:
- The initializer runs on both prime and clones. Skip the prime with .
instance === tg - Create the scene lazily inside the frame task (not in the initializer body) to stay under the 100ms initializer time limit.
- The clone's canvas is offscreen; may return 0. Fall back to
getBoundingClientRect()or hardcoded defaults.clientWidth
renderToVideouseEffectinitializertsx
// 在setup函数内部,创建主场景之后:
tg.initializer = (instance) => {
if (instance === tg) return; // 跳过主实例,已完成设置
let cloneScene = null;
instance.addFrameTask(({ ownCurrentTimeMs, durationMs }) => {
if (!cloneScene) {
// 延迟创建(initializer执行时间必须<100ms)
const cvs = instance.querySelector("canvas");
if (!cvs) return;
cloneScene = createMyScene(cvs);
const rect = cvs.getBoundingClientRect();
cloneScene.resize(rect.width || cvs.clientWidth || 800,
rect.height || cvs.clientHeight || 500);
}
cloneScene.update(ownCurrentTimeMs, durationMs);
});
};关键点:
- initializer会在主实例和克隆实例上运行。通过跳过主实例。
instance === tg - 在帧任务内部延迟创建场景(不要在initializer主体中创建),以保持initializer执行时间在100ms限制内。
- 克隆的画布是离屏的;可能返回0。回退到
getBoundingClientRect()或硬编码默认值。clientWidth
JSX Structure
JSX结构
tsx
<Preview id={rootId} loop>
<Timegroup mode="fixed" duration="14s"
className="relative w-full overflow-hidden"
style={{ aspectRatio: "16/10", background: "#1e2233" }}>
<canvas style={{
position: "absolute", inset: 0,
width: "100%", height: "100%", display: "block",
}} />
{/* HTML overlays on top of the canvas */}
<div style={{ position: "absolute", ... }}>Text labels</div>
</Timegroup>
</Preview>tsx
<Preview id={rootId} loop>
<Timegroup mode="fixed" duration="14s"
className="relative w-full overflow-hidden"
style={{ aspectRatio: "16/10", background: "#1e2233" }}>
<canvas style={{
position: "absolute", inset: 0,
width: "100%", height: "100%", display: "block",
}} />
{/* 画布上方的HTML叠加层 */}
<div style={{ position: "absolute", ... }}>文本标签</div>
</Timegroup>
</Preview>Lighting & Materials for Visibility
可见性相关的光照与材质
Dark 3D scenes are the #1 problem. Objects that look fine in preview disappear in rendered output.
3D场景过暗是头号问题。在预览中看起来正常的对象在渲染输出中可能消失。
Minimum viable lighting
最低可行光照设置
typescript
scene.add(new THREE.AmbientLight(0xd0d8f0, 0.9)); // strong ambient
const key = new THREE.DirectionalLight(0xffffff, 1.8); // key light with shadows
const spot = new THREE.SpotLight(0xffffff, 2.0, 25); // specular catch
scene.add(new THREE.PointLight(0x82b1ff, 0.9, 25)); // rim/accenttypescript
scene.add(new THREE.AmbientLight(0xd0d8f0, 0.9)); // 强环境光
const key = new THREE.DirectionalLight(0xffffff, 1.8); // 带阴影的主光源
const spot = new THREE.SpotLight(0xffffff, 2.0, 25); // 镜面补光
scene.add(new THREE.PointLight(0x82b1ff, 0.9, 25)); // 轮廓/强调光Material recommendations
材质推荐
Use with clearcoat for visible specular highlights:
MeshPhysicalMaterialtypescript
new THREE.MeshPhysicalMaterial({
color: 0x448aff,
roughness: 0.12, // low = shiny
metalness: 0.15,
clearcoat: 1.0, // glossy lacquer layer
clearcoatRoughness: 0.15,
emissive: new THREE.Color(0x448aff),
emissiveIntensity: 0.1, // self-illumination for dark scenes
transparent: true,
opacity: 0, // start hidden, fade in
});MeshStandardMaterial使用带清漆层的以获得清晰的镜面高光:
MeshPhysicalMaterialtypescript
new THREE.MeshPhysicalMaterial({
color: 0x448aff,
roughness: 0.12, // 值越低=越亮面
metalness: 0.15,
clearcoat: 1.0, // 光泽清漆层
clearcoatRoughness: 0.15,
emissive: new THREE.Color(0x448aff),
emissiveIntensity: 0.1, // 暗场景中的自发光
transparent: true,
opacity: 0, // 初始隐藏,渐显
});MeshStandardMaterialTone mapping
色调映射
typescript
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.6–1.8; // push brighttypescript
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.6–1.8; // 提高亮度Sizing Rules
尺寸规则
Objects need to be much larger than expected for rendered video:
- Minimum object size: 5% of frame width to be visible
- Progress bars: height >= 3% of frame height
- Particles: (not 0.035)
size >= 0.08 - Floor grid: adds spatial grounding, use at 20-25% opacity
GridHelper
对象需要比预期大得多才能在渲染视频中可见:
- 最小对象尺寸:帧宽度的5%才能可见
- 进度条:高度>=帧高度的3%
- 粒子:(不要用0.035)
size >= 0.08 - 地面网格:增加空间定位感,使用并设置20-25%的透明度
GridHelper
Shadow-Opacity Sync
阴影与透明度同步
Transparent objects still cast full shadows. Toggle based on opacity:
castShadowtypescript
function setOpacity(mesh, opacity) {
mesh.material.opacity = opacity;
mesh.castShadow = opacity > 0.1; // no shadow when nearly invisible
}透明对象仍会投射完整阴影。根据透明度切换:
castShadowtypescript
function setOpacity(mesh, opacity) {
mesh.material.opacity = opacity;
mesh.castShadow = opacity > 0.1; // 几乎不可见时不投射阴影
}Camera Choreography
相机运镜
Camera position is the primary attention tool. Keyframe camera poses and interpolate:
typescript
const CAM_CLOSE = new THREE.Vector3(0, 0.8, 2.8); // hero shot, fills frame
const CAM_WIDE = new THREE.Vector3(0, 3.8, 10); // reveals full scene
const CAM_WIN = new THREE.Vector3(2, 3, 8); // orbits toward payoff
// In update():
const pullBack = easeInOut(progress(timeMs, startMs, endMs));
lerpV3(camPos, CAM_CLOSE, CAM_WIDE, pullBack);Rules:
- Start close (subject fills frame), pull back to reveal
- Pull back BEFORE adding new elements (so they don't appear off-screen)
- Orbit toward the "winner" in comparison scenes
- Snap zoom (200-300ms sine pulse on camPos.z) for emphasis on key metrics
相机位置是主要的注意力引导工具。为相机姿态设置关键帧并进行插值:
typescript
const CAM_CLOSE = new THREE.Vector3(0, 0.8, 2.8); // 特写镜头,填满画面
const CAM_WIDE = new THREE.Vector3(0, 3.8, 10); // 展示完整场景
const CAM_WIN = new THREE.Vector3(2, 3, 8); // 环绕移动至重点区域
// 在update()中:
const pullBack = easeInOut(progress(timeMs, startMs, endMs));
lerpV3(camPos, CAM_CLOSE, CAM_WIDE, pullBack);规则:
- 从特写开始(主体填满画面),向后拉远以展示更多内容
- 在添加新元素之前向后拉远(避免元素出现在画面外)
- 在对比场景中环绕移动至“重点对象”
- 快速缩放(在camPos.z上应用200-300ms的正弦脉冲)以强调关键指标
Environment
环境设置
Dark backgrounds need a sense of place, not a void:
typescript
const BG = 0x1e2233;
scene.background = new THREE.Color(BG);
scene.fog = new THREE.Fog(BG, 16, 35); // objects fade into background at distance
// Floor with grid
const floor = new THREE.Mesh(
new THREE.PlaneGeometry(50, 35),
new THREE.MeshStandardMaterial({ color: 0x2a2e42, roughness: 0.75, metalness: 0.1 }),
);
floor.rotation.x = -Math.PI / 2;
floor.position.y = -0.7;
floor.receiveShadow = true;
const grid = new THREE.GridHelper(30, 30, 0x3a3f58, 0x3a3f58);
grid.position.y = -0.69;
grid.material.transparent = true;
grid.material.opacity = 0.25;深色背景需要空间感,而非空洞:
typescript
const BG = 0x1e2233;
scene.background = new THREE.Color(BG);
scene.fog = new THREE.Fog(BG, 16, 35); // 对象在远处渐隐入背景
// 带网格的地面
const floor = new THREE.Mesh(
new THREE.PlaneGeometry(50, 35),
new THREE.MeshStandardMaterial({ color: 0x2a2e42, roughness: 0.75, metalness: 0.1 }),
);
floor.rotation.x = -Math.PI / 2;
floor.position.y = -0.7;
floor.receiveShadow = true;
const grid = new THREE.GridHelper(30, 30, 0x3a3f58, 0x3a3f58);
grid.position.y = -0.69;
grid.material.transparent = true;
grid.material.opacity = 0.25;Common Pitfalls
常见陷阱
-
Scene too dark — Push ambient to 0.9, key to 1.8, exposure to 1.6+. Check by rendering, not by preview.
-
Render clone shows only first frame — Eitherisn't advancing (check
ownCurrentTimeMsin seekForRender) or the initializer isn't registering the frame task on the clone._setLocalTimeMs -
Render output is blank — Missingor
preserveDrawingBuffer: true.gl.finish() -
Shadows visible before objects fade in — Togglewith opacity (threshold 0.1).
castShadow -
Particles invisible in render — Size too small. Use. Additive blending can disappear against bright backgrounds.
size >= 0.08 -
renderToVideo returns undefined —defaults to true, which uses File System Access API and returns undefined. Set
streamingwhen you need the buffer returned to your code.streaming: false -
Initializer exceeds 100ms — Scene creation is too heavy for the initializer. Create the scene lazily inside the frame task callback instead.
-
场景过暗 — 将环境光强度设为0.9,主光源设为1.8,曝光度设为1.6以上。通过渲染结果检查,而非预览。
-
渲染克隆仅显示第一帧 — 要么未推进(检查seekForRender中的
ownCurrentTimeMs),要么initializer未在克隆上注册帧任务。_setLocalTimeMs -
渲染输出为空 — 缺少或
preserveDrawingBuffer: true。gl.finish() -
对象渐显前阴影已可见 — 根据透明度切换(阈值0.1)。
castShadow -
粒子在渲染中不可见 — 尺寸过小。使用。 additive混合模式在亮背景下可能消失。
size >= 0.08 -
renderToVideo返回undefined —默认值为true,会使用文件系统访问API并返回undefined。当需要将缓冲区返回至代码时,设置
streaming。streaming: false -
Initializer执行时间超过100ms — 场景创建在initializer中过于繁重。改为在帧任务回调内部延迟创建场景。
Dependencies
依赖
bash
undefinedbash
undefinedInstall in telecine (via Docker scripts)
在telecine中安装(通过Docker脚本)
telecine/scripts/npm install three
telecine/scripts/npm install --save-dev @types/three
Dynamic import in the component avoids SSR issues:
```typescript
const { createMyScene } = await import("./my-scene");telecine/scripts/npm install three
telecine/scripts/npm install --save-dev @types/three
组件中的动态导入可避免SSR问题:
```typescript
const { createMyScene } = await import("./my-scene");React Three Fiber (R3F)
React Three Fiber (R3F)
For component-based 3D scenes with better developer experience, see r3f.md. R3F wraps Three.js in React components with hooks, declarative scene construction, and drei helpers. Use the vanilla approach (this file) for render clones; use R3F for the live interactive version.
如需拥有更好开发体验的组件化3D场景,请查看r3f.md。R3F通过钩子、声明式场景构建和drei助手将Three.js封装为React组件。渲染克隆使用原生方法(本文档内容);实时交互版本使用R3F。