免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

HyperFrames 中的 Three.js 适配器:用 WebGL 构建确定性视频合成

HyperFrames 中的 Three.js 适配器:用 WebGL 构建确定性视频合成 HyperFrames 中的 Three.js 适配器用 WebGL 构建确定性视频合成【免费下载链接】hyperframesWrite HTML. Render video. Built for agents.项目地址: https://gitcode.com/GitHub_Trending/hy/hyperframesHyperFrames 通过内置的three运行时适配器支持 Three.js 场景渲染其核心设计是适配器不拥有你的场景只负责发布时间。本文基于 three 适配器文档 及仓库源码完整讲解hf-seek事件驱动渲染模式、data-duration必需性的底层原因、GLTF/AnimationMixer 时间轴驱动、Addons 加载方式以及资源就绪门控与 lint 校验读完即可写出一段可被 HyperFrames 引擎逐帧捕获、可复现渲染的 Three.js 合成。适配器契约场景归你时间归引擎HyperFrames 对 GPU 类合成的统一策略是把何时渲染哪一帧的决定权完全交给引擎而把如何根据时间画出这一帧留给你的 Three.js 代码。three运行时适配器只做两件事在每次 seek 时把当前时间写入window.__hfThreeTime派发一个携带{ detail: { time } }的CustomEvent(hf-seek)。对应的实现位于 three.tsseek: (ctx) { forcedTime Math.max(0, Number(ctx.time) || 0); lastForcedTime forcedTime; window.__hfThreeTime forcedTime; dispatchSeekEvent(forcedTime); },注意Math.max(0, ...)这一行负数时间会被钳制为 0three.test.ts 中用seek({ time: -10 })验证了__hfThreeTime最终为 0。因此在你的渲染函数里不需要再防御负时间。由此产生四条硬性契约违反任何一条都会破坏确定性渲染场景、相机、渲染器、材质、资产尽量同步创建从 HyperFrames 时间渲染而不是从墙钟时间渲染Date.now()、performance.now()、时钟增量都不允许监听hf-seek渲染事件给出的精确时间模型、纹理、HDR 必须在 render-critical 的 seek 之前加载完毕绝不能在 seek 时异步拉取。另外不要把requestAnimationFrame或renderer.setAnimationLoop当作渲染关键动效的时间源——它可以用于纯装饰性的非关键内容但逐帧捕获依赖的是hf-seek驱动的那次显式renderer.render()。为什么 Three.js 合成必须写>div >canvas idthree-layer/canvas script typemodule import * as THREE from https://cdn.jsdelivr.net/npm/three0.181.2/esm; const canvas document.getElementById(three-layer); const renderer new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true }); // 尺寸必须与合成帧尺寸一致第三个参数 false 表示不修改 canvas 样式尺寸 renderer.setSize(1920, 1080, false); renderer.setPixelRatio(1); // 视频渲染必须固定 pixelRatio避免 DPR 依赖 const scene new THREE.Scene(); const camera new THREE.PerspectiveCamera(35, 1920 / 1080, 0.1, 100); camera.position.set(0, 0, 6); const mesh new THREE.Mesh( new THREE.IcosahedronGeometry(1.4, 4), new THREE.MeshStandardMaterial({ color: 0x64d2ff, roughness: 0.38 }), ); scene.add(mesh); scene.add(new THREE.HemisphereLight(0xffffff, 0x223344, 2)); function renderAt(time) { mesh.rotation.y time * 0.7; mesh.rotation.x Math.sin(time * 0.6) * 0.16; renderer.render(scene, camera); } window.addEventListener(hf-seek, (event) { renderAt(event.detail.time); }); // 首次加载时兜底渲染一帧避免白屏 renderAt(window.__hfThreeTime || 0); /script配套 CSS#three-layer { width: 100%; height: 100%; display: block; }三个关键点旋转由时间推导mesh.rotation.y time * 0.7是纯函数形式同样的时间必然得到同样的姿态这是可逐帧捕获、可复现渲染的前提事件里取时间event.detail.time就是适配器从dispatchSeekEvent传入的值见 seek-dispatch.ts 中HfSeekEventDetail的定义首次兜底渲染renderAt(window.__hfThreeTime || 0)保证在第一次hf-seek到来之前画面上已有内容。hf-seek 事件机制去重与 waitUntil 屏障事件派发并不是简单的dispatchEventseek-dispatch.ts 里有一个按时间值去重的逻辑如果本次要派发的时间与上次完全相同就跳过避免 Three.js 与 TypeGPU 两个 GPU 适配器同时在场时对一次 seek 派发两次事件、造成每帧渲染工作翻倍。更重要的是waitUntil机制事件detail上附带一个waitUntil(promise)函数它必须在监听器内部同步调用调用时机超出同步栈会抛错。监听器可以把异步渲染工作如纹理上传、计算以 Promise 形式注册进去引擎通过waitForSeekCompletion()seek-dispatch.ts等待这些 Promise 全部 settle 后才认为该帧渲染完成。这一屏障对视频纹理注入后重渲染场景尤为重要引擎在注入解码后的__render_frame__图像后会通过window.__hfReseekGpu(time)见 init.ts对同一个时间强制再派发一次hf-seekforceDispatchSeekEvent绕过去重让 WebGL 场景从已经就位的视频帧重新上传纹理——这就是为什么不要在 seek 时才去拉远程资源这条纪律如此重要。加载 Addonsimportmap 与版本固定凡是用到three/addons/下的模块GLTFLoader、OrbitControls、RGBELoader等使用 importmap 让裸标识符正确解析script typeimportmap { imports: { three: https://cdn.jsdelivr.net/npm/three0.181.2/build/three.module.js, three/addons/: https://cdn.jsdelivr.net/npm/three0.181.2/examples/jsm/ } } /script script typemodule import * as THREE from three; import { GLTFLoader } from three/addons/loaders/GLTFLoader.js; import { OrbitControls } from three/addons/controls/OrbitControls.js; // ... /scriptHyperFrames 的 lint 同时识别两种形式上面这种 importmap 写法以及基础模式里内联的esm导入。两条铁律map 里的两个条目必须固定到同一个版本three与three/addons/的版本号不一致会导致裸导入与插件模块来自不同构建出现难以排查的静默崩溃不要混用importmap 的裸标识符导入与esm内联导入不要在同一次渲染路径里交叉引用同一份 three 实例避免实例身份分裂。AnimationMixer 模式直接 seek 混合器对于 GLTF 自带或手动创作的动画片段正确的做法是让AnimationMixer从 HyperFrames 时间直接 seek而不是让它自己跑时钟function renderAt(time) { mixer.setTime(time); renderer.render(scene, camera); }mixer.setTime(time)会把动画推进到指定时间点AnimationClip会自动循环/钳制到 clip 时长内这是确定性的如果有多个 mixer例如多个 GLTF 模型各自带动画用同一个time全部 seek 一遍保证它们彼此帧对齐资产必须本地化并在校验完成前加载完毕——这正是下面就绪门控要解决的问题。资源就绪门控DefaultLoadingManager 挂钩渲染引擎不会在 Three.js 还在加载模型/纹理时就发布window.__renderReady true去抓空帧。three适配器实现了getReadyPromisetypes.ts 定义了该可选钩子的契约实现方式是挂钩THREE.DefaultLoadingManager的itemsLoaded/itemsTotal计数three.tsdiscover()会幂等地包装 LoadingManager 的onStart因此在 discover 之后新排队的加载任务也会被捕获——这修复了用户脚本在 HF 运行时挂载后才开始加载资产、导致就绪判断漏掉这批任务的竞态源码注释提及该问题对应 PR-1543包装onLoad生成一个 pending Promise队列排空时 resolve同时保留并恢复用户自己的onStart/onLoad回调保证多资产合成的用户回调照常触发getReadyPromise()在每个就绪评估周期都会被调用itemsTotal itemsLoaded说明队列已排空返回null否则返回或按需重建pending Promise让引擎继续等待。这个机制解释了契约里加载模型、纹理、HDR 必须在 render-critical 的 seek 之前完成的底层保障即使加载是异步的引擎也会等到加载队列清空后才开始逐帧捕获。推荐用法与明确禁忌Good uses适合用 Three.js 适配器确定性 3D 物体产品旋转展示、基于种子数据的粒子系统、shader 面板shader plates由time推导的相机运镜资产本地化且在校验完成前已加载的 GLTF 动画片段。Avoid会导致帧不稳定或渲染失败用Date.now()、performance.now()或时钟增量更新场景状态——同一帧时间得到不同画面破坏确定性把 render-critical 的工作留在自由运行的动画循环里——引擎捕获的帧与循环状态对不上在渲染时加载远程模型或纹理——seek 是同步路径异步加载会造成空帧或错帧输出依赖设备像素比DPR——视频渲染必须固定renderer.setSize与setPixelRatio依赖上一帧历史的后处理管线——除非你能仅凭时间重建状态例如把上一帧的 buffer 存入按时间索引的缓存。校验与验证编辑完 Three.js 合成后在仓库根目录运行npx hyperframes lint npx hyperframes checklint会静态扫描根元素data-duration、GSAP 时间轴注册情况以及/\bTHREE\./使用痕迹见 composition.ts 的检测逻辑注释剥离后才会判定是否存在时长来源避免注释掉的代码误过关check则对合成做更完整的运行前检查。深入阅读适配器核心实现packages/core/src/runtime/adapters/three.ts适配器对象、LoadingManager 挂钩、seek 路径适配器单元测试packages/core/src/runtime/adapters/three.test.ts时间钳制、事件派发、pause/play/revert 状态事件派发与去重packages/core/src/runtime/adapters/seek-dispatch.ts时长兜底与 GPU 重渲染packages/core/src/runtime/init.ts、packages/core/src/runtime/init.ts适配器接口契约packages/core/src/runtime/types.tslint 时长规则packages/lint/src/rules/composition.ts 及对应测试 packages/lint/src/rules/composition.test.ts本主题配套的 TypeGPU 适配器skills/hyperframes-animation/adapters/typegpu.md共享同一hf-seek机制【免费下载链接】hyperframesWrite HTML. Render video. Built for agents.项目地址: https://gitcode.com/GitHub_Trending/hy/hyperframes创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表