Web3D 在线3D模型骨骼动画编辑器 Reze Studio
「32 Web3D 在线3D模型骨骼动画编辑器THreeJS」
/~f4543YKLSZ~:/
链接:https://pan.quark.cn/s/c83b484c466b
预览: https://reze-studio.vercel.app/
一个现代化的、原生Web的MMD动画编辑器------专为手动关键帧编辑.vmd片段而设计的独立时间轴和曲线编辑器,摆脱了仅限Windows的桌面安装限制。它并非一个完全的MMD替代品(目前不支持MME风格的着色器或视频导出),也无意成为Maya或Blender;它是一个专注的、跨平台工具,旨在出色地完成动画编辑工作。通过WebGPU和reze引擎(集成Ammo.js物理引擎、IK解算器)直接在GPU上运行渲染,可在从iPad到游戏笔记本的任何设备上提供高帧率播放和流畅交互。功能特性
PMX模型和VMD动画加载与渲染,支持IK和物理效果
时间轴,包含摄影表和逐通道曲线编辑器
贝塞尔插值曲线编辑
在播放头位置插入/删除关键帧
VMD导入/导出
从本地文件夹加载用户的PMX模型
骨骼列表,支持分组层级结构
表情(Morph)列表
旋转/平移滑块,支持直接数字输入
表情权重关键帧设置
针对片段编辑的撤销/重做
轨道操作:简化(关键帧精简)、清空
键盘快捷键
标签页关闭/刷新时的未保存更改警告
视口骨骼拾取(双击)+ 3D变换控件(Gizmo)拖动
在材质面板中拾取材质并显示高亮轮廓
支持混合权重和骨骼蒙版的动画层
支持静音/独奏切换的自定义骨骼组
片段操作:剪切、复制、粘贴、镜像粘贴(左↔右)、导入、时间拉伸
动作捕捉导入(视频 → VMD)
Overleaf风格的实时协作
AI辅助动画(生成式补间、动作重定向)
快速开始
打开 reze.studio------ 会加载一个默认的Reze模型和示例片段,因此您可以立即开始编辑。
(可选)加载您自己的模型:
文件→加载PMX文件夹...,选择包含.pmx文件的文件夹(纹理文件必须与.pmx文件位于同一目录)。(可选)加载现有片段,或从头开始:
文件→加载VMD...导入现有的 .vmd 文件,或选择文件→新建以清空时间轴,在已加载的模型上自行设置关键帧动画。播放动画:按
空格键或点击播放按钮。保存您的编辑:
文件→导出VMD...。没有服务器参与------所有操作都在您的浏览器中完成,因此请在关闭标签页前导出。动画编辑流程简介
如果您从未手动设置过关键帧动画,可以这样理解:一个片段是每个骨骼(和每个表情)的关键帧 (keyframe) 列表------即"在第N帧,骨骼处于此姿态"的快照。引擎会在关键帧之间进行插值,使角色动作平滑流畅。编辑片段意味着移动、添加或调整这些关键帧。
在 Reze Studio 中的典型工作流程:
选择一个骨骼。在左侧面板、摄影表(dope sheet)中点击它,或在视口中双击模型。右侧的"属性检查器"(Properties Inspector)会显示其旋转/平移值以及该骨骼上的所有关键帧,并且在3D视图中该骨骼位置会出现一个环形/轴控制器(Gizmo)。
跳转到某一帧 。在时间轴中拖动播放头,或使用
←/→键逐帧步进。视口会实时更新。调整骨骼姿态。在检查器中拖动旋转/平移滑块、直接输入数字,或在视口中拖动Gizmo(环形控制旋转,轴控制平移)。这两种方式都会写入当前帧的关键帧------如果该帧不存在关键帧,则会自动插入一个。每次拖动操作将被记录为一个独立的可撤销编辑单元。
调整关键帧间的运动曲线。在摄影表中选择一个关键帧,然后打开曲线编辑器选项卡。每个通道(rotX, rotY, rotZ, tX, tY, tZ)都有其独立的贝塞尔曲线------拖动手柄可以改变缓动效果。这是将"僵硬"动画变得"生动"的关键。
删除/微移/拖拽关键帧。在摄影表中,您可以左右拖动菱形关键帧以调整时间,或选择并删除。方向键可逐帧微调。
清理轨道 。在属性检查器中,
简化功能可移除选定骨骼上的冗余关键帧(即被相邻关键帧间的贝塞尔曲线在指定的旋转/平移容差内精确复现的关键帧)。清空则会完全清除该轨道。两者都可撤销。撤销错误操作 。
Ctrl/⌘+Z撤销上一次片段编辑;Ctrl/⌘+Shift+Z(或⌘+Y)重做。历史记录保留最近的100次编辑。加载新的VMD或PMX文件不会进入历史堆栈------否则会导致加载的模型与之不同步。检查材质。打开"材质"选项卡(右侧面板),点击材质名称可在视口中高亮显示------便于确认网格对应关系。点击同一名称或列表中的任意空白区域可清除高亮。材质选择与骨骼/表情选择互斥。
对所有骨骼重复上述步骤,直到姿态流畅。最后导出为VMD文件。
键盘快捷键
按键 功能 空格键播放 / 暂停 ←/→后退 / 前进一帧 Home跳转到第一帧 End跳转到最后一帧 Ctrl/⌘+Z撤销上一次片段编辑 Ctrl/⌘+Shift+Z,⌘+Y重做 在帧数输入框内使用 ←/→递减 / 递增播放头所在帧 Shift+ 鼠标滚轮缩放数值 / Y轴 Ctrl/⌘+ 鼠标滚轮缩放时间 / X轴 技术栈
引擎: reze-engine ------ WebGPU渲染器,Ammo.js物理引擎,IK解算器
编辑器: Next.js 16, React 19, TypeScript, shadcn/ui, Tailwind
架构
除了作为MMD编辑器,此仓库也是对"如何在React中实现响应迅速的时间轴编辑器"的一项研究。时间轴编辑器是对框架的极限压力测试:它需要高帧率的播放头、多轴拖拽、数千个关键帧,以及绝不能卡顿的WebGPU画布------所有这些都与常规的React UI共享同一棵树。本节记录了Reze Studio如何实现这一目标。
简而言之 ------ React工程要点
分离外部状态存储 。文档/选择状态存在于
<Studio>中;播放控制(播放头、播放状态)存在于<Playback>中。播放控制以rAF(requestAnimationFrame)频率更新,不会触发撤销/重做目标的无效更新。
useSyncExternalStore+ 选择器模式 。组件订阅单一数据片段(useStudioSelector(s => s.field)),仅在数据片段变化时重新渲染。操作包(useStudioActions())保持稳定,不会导致重新渲染。热路径完全绕过React。播放、关键帧拖拽和姿态滑块拖拽都通过命令式操作直接修改引用/对象,通过命令式句柄重绘画布,并且仅在释放时与React交互一次。
currentFrameRef逃生舱。播放控制存储持有一个引用,EngineBridge的rAF循环直接向其写入。非订阅的消费者(检查器采样器、PMX交换快照)无需触发重新渲染即可读取实时播放头。具备快照桥接撤销功能的类Reducer核心结构 。由于预览期间的编辑会直接修改当前片段,存储还会维护一个不可变的
clipSnapshot(在最后一次提交/撤销/重做时进行的深拷贝)。commit()将该快照推入历史记录(past)------而不是被修改的片段------因此历史记录永远不会捕获到拖拽中的中间状态。提供者树结构
<Studio> // 外部存储 ------ 片段 + 选择状态(撤销/重做目标) └─ <Playback> // 外部存储 ------ 当前帧、播放状态(rAF循环更新不触发重渲染) └─ <StudioStatusProvider> // 外部存储 ------ PMX名称、FPS、消息(与页面重渲染隔离) └─ <StudioPage> // 布局外壳 + 文件处理器 ├─ <EngineBridge> // 无头组件 ------ 所有与引擎相关的副作用,返回 null ├─ <StudioLeftPanel> // 被记忆化 ------ 骨骼列表、表情列表、文件菜单 ├─ <StudioViewport> // 被记忆化 ------ WebGPU <canvas> ├─ <Timeline> // 切片订阅 ------ 摄影表 + 曲线编辑器 │ └─ <TimelineCanvas> // 命令式播放头 + 拖拽重绘句柄 ├─ <PropertiesInspector> // 切片订阅 ------ 姿态滑块、表情权重(播放期间通过rAF自行采样) └─ <StudioStatusFooter> // 切片订阅 ------ PMX名称、FPS、片段名称状态分层
层级 所在位置 说明 文档 context/studio-context.ts外部存储,切片订阅,撤销/重做目标 选择状态 context/studio-context.ts骨骼、表情、关键帧 播放控制 context/playback-context.ts外部存储; currentFrame,playing;存储自身拥有的currentFrameRef供rAF消费者使用(见下文注释)状态栏 components/studio-status.tsx外部存储;PMX文件名、FPS、临时消息 引擎引用 StudioPageengineRef,modelRef,canvasRef视图 Timeline中的本地 useState缩放、滚动、选项卡 界面 StudioPage中的本地 useState菜单栏、文件选择对话框 播放控制注释 :
currentFrameRef通过usePlaybackFrameRef()共享。EngineBridge 的 rAF 循环直接将实时播放头写入.current而不经过set(),因此非订阅的消费者无需任何React工作即可读取实时帧。订阅模型
Studio(文档/选择状态)、Playback(播放控制)和StudioStatus(状态栏)都是基于useSyncExternalStore的外部存储。组件通过useStudioSelector(s => s.field)/usePlaybackSelector(...)/useStudioStatusSelector(...)读取,因此每个组件仅在其依赖的数据片段变化时重新渲染,并通过use*Actions()写入,这些操作返回稳定的操作包,不会导致重新渲染。包装存储内部的set()也是撤销/重做的挂载点------commit()将快照推入历史记录,replaceClip()(用于VMD/PMX加载和"新建")清除历史记录,选择状态的变化从不触及撤销堆栈。热路径 ------ 交互时零React更新
三种高频交互(播放、关键帧拖拽、姿态滑块拖拽)都遵循相同的模式:命令式地修改引用/对象,通过命令式句柄重绘画布,并在释放时仅与React交互一次。
播放 ------
<EngineBridge>的 rAF 循环读取引擎时钟,将实时帧写入播放控制存储的currentFrameRef(通过usePlaybackFrameRef()共享的唯一引用),并调用playheadDrawRef.current(frame)------ 这是<TimelineCanvas>暴露的一个句柄,用于直接重绘播放头叠加层。没有每次tick的setCurrentFrame,因此不会重新渲染,但任何非订阅的消费者(检查器姿态采样、PMX交换快照)仍然可以通过该引用看到实时帧。自动滚动(当播放头移出视口时翻页)位于相同的命令式路径中,仅在罕见的翻页边界处触及React。暂停时,最终帧会通过setCurrentFrame刷新,以便暂停视图与最后绘制的内容一致。实时姿态/表情读数 ------
<PropertiesInspector>在隔离的叶子子组件中采样所选骨骼的姿态和表情权重。暂停时,它订阅currentFrame并在变化时重新采样;播放时,它运行自己的小型 rAF 循环,直接读取modelRef.current的runtimeSkeleton/getMorphWeights(),并通过相等性检查进行门控,以防止未变化的帧触发协调。这将逐帧的工作保持在父检查器和<StudioPage>之外。关键帧拖拽 ------
<Timeline>的移动回调命令式地修改关键帧的frame/ 通道值 / 轨道顺序,并触发dragRedrawRef.current(),这会增加一个用于静态层缓存失效检查的内部拖拽版本号,并重绘画布。selectedKeyframes条目被命令式地修改,使高亮跟随拖拽。在鼠标弹起时,一个单独的commit()克隆轨道 Maps 并创建撤销/重做快照,同时通过<EngineBridge>执行一次model.loadClip。姿态滑块拖拽 ------
<PropertiesInspector>的apply*Axis/applyMorphWeight函数在每次拖拽tick中以"预览"模式运行:原地修改匹配的关键帧(或插入一个),然后执行model.loadClip+ 跳转以更新3D视口。没有commit(),因此时间轴保持静态,检查器也不会协调。<AxisSliderRow>在拖拽期间保持本地的滑块值,以防止Radix控件回弹到过期的受控属性值。在onValueCommit时,触发一个单独的克隆 +commit()操作 ------ 只有这个commit()会进入撤销历史,因此一次拖拽是一个可撤销的单元,而不是数百个预览帧。
Reze Studio
一个现代化的、原生Web的MMD动画编辑器------专为手动关键帧编辑.vmd片段而设计的独立时间轴和曲线编辑器,摆脱了仅限Windows的桌面安装限制。它并非一个完全的MMD替代品(目前不支持MME风格的着色器或视频导出),也无意成为Maya或Blender;它是一个专注的、跨平台工具,旨在出色地完成动画编辑工作。通过WebGPU和reze引擎(集成Ammo.js物理引擎、IK解算器)直接在GPU上运行渲染,可在从iPad到游戏笔记本的任何设备上提供高帧率播放和流畅交互。
功能特性
PMX模型和VMD动画加载与渲染,支持IK和物理效果
时间轴,包含摄影表和逐通道曲线编辑器
贝塞尔插值曲线编辑
在播放头位置插入/删除关键帧
VMD导入/导出
从本地文件夹加载用户的PMX模型
骨骼列表,支持分组层级结构
表情(Morph)列表
旋转/平移滑块,支持直接数字输入
表情权重关键帧设置
针对片段编辑的撤销/重做
轨道操作:简化(关键帧精简)、清空
键盘快捷键
标签页关闭/刷新时的未保存更改警告
视口骨骼拾取(双击)+ 3D变换控件(Gizmo)拖动
在材质面板中拾取材质并显示高亮轮廓
支持混合权重和骨骼蒙版的动画层
支持静音/独奏切换的自定义骨骼组
片段操作:剪切、复制、粘贴、镜像粘贴(左↔右)、导入、时间拉伸
动作捕捉导入(视频 → VMD)
Overleaf风格的实时协作
AI辅助动画(生成式补间、动作重定向)
快速开始
打开 reze.studio------ 会加载一个默认的Reze模型和示例片段,因此您可以立即开始编辑。
(可选)加载您自己的模型:
文件→加载PMX文件夹...,选择包含.pmx文件的文件夹(纹理文件必须与.pmx文件位于同一目录)。(可选)加载现有片段,或从头开始:
文件→加载VMD...导入现有的 .vmd 文件,或选择文件→新建以清空时间轴,在已加载的模型上自行设置关键帧动画。播放动画:按
空格键或点击播放按钮。保存您的编辑:
文件→导出VMD...。没有服务器参与------所有操作都在您的浏览器中完成,因此请在关闭标签页前导出。动画编辑流程简介
如果您从未手动设置过关键帧动画,可以这样理解:一个片段是每个骨骼(和每个表情)的关键帧 (keyframe) 列表------即"在第N帧,骨骼处于此姿态"的快照。引擎会在关键帧之间进行插值,使角色动作平滑流畅。编辑片段意味着移动、添加或调整这些关键帧。
在 Reze Studio 中的典型工作流程:
选择一个骨骼。在左侧面板、摄影表(dope sheet)中点击它,或在视口中双击模型。右侧的"属性检查器"(Properties Inspector)会显示其旋转/平移值以及该骨骼上的所有关键帧,并且在3D视图中该骨骼位置会出现一个环形/轴控制器(Gizmo)。
跳转到某一帧 。在时间轴中拖动播放头,或使用
←/→键逐帧步进。视口会实时更新。调整骨骼姿态。在检查器中拖动旋转/平移滑块、直接输入数字,或在视口中拖动Gizmo(环形控制旋转,轴控制平移)。这两种方式都会写入当前帧的关键帧------如果该帧不存在关键帧,则会自动插入一个。每次拖动操作将被记录为一个独立的可撤销编辑单元。
调整关键帧间的运动曲线。在摄影表中选择一个关键帧,然后打开曲线编辑器选项卡。每个通道(rotX, rotY, rotZ, tX, tY, tZ)都有其独立的贝塞尔曲线------拖动手柄可以改变缓动效果。这是将"僵硬"动画变得"生动"的关键。
删除/微移/拖拽关键帧。在摄影表中,您可以左右拖动菱形关键帧以调整时间,或选择并删除。方向键可逐帧微调。
清理轨道 。在属性检查器中,
简化功能可移除选定骨骼上的冗余关键帧(即被相邻关键帧间的贝塞尔曲线在指定的旋转/平移容差内精确复现的关键帧)。清空则会完全清除该轨道。两者都可撤销。撤销错误操作 。
Ctrl/⌘+Z撤销上一次片段编辑;Ctrl/⌘+Shift+Z(或⌘+Y)重做。历史记录保留最近的100次编辑。加载新的VMD或PMX文件不会进入历史堆栈------否则会导致加载的模型与之不同步。检查材质。打开"材质"选项卡(右侧面板),点击材质名称可在视口中高亮显示------便于确认网格对应关系。点击同一名称或列表中的任意空白区域可清除高亮。材质选择与骨骼/表情选择互斥。
对所有骨骼重复上述步骤,直到姿态流畅。最后导出为VMD文件。
键盘快捷键
按键 功能 空格键播放 / 暂停 ←/→后退 / 前进一帧 Home跳转到第一帧 End跳转到最后一帧 Ctrl/⌘+Z撤销上一次片段编辑 Ctrl/⌘+Shift+Z,⌘+Y重做 在帧数输入框内使用 ←/→递减 / 递增播放头所在帧 Shift+ 鼠标滚轮缩放数值 / Y轴 Ctrl/⌘+ 鼠标滚轮缩放时间 / X轴 技术栈
引擎: reze-engine ------ WebGPU渲染器,Ammo.js物理引擎,IK解算器
编辑器: Next.js 16, React 19, TypeScript, shadcn/ui, Tailwind
架构
除了作为MMD编辑器,此仓库也是对"如何在React中实现响应迅速的时间轴编辑器"的一项研究。时间轴编辑器是对框架的极限压力测试:它需要高帧率的播放头、多轴拖拽、数千个关键帧,以及绝不能卡顿的WebGPU画布------所有这些都与常规的React UI共享同一棵树。本节记录了Reze Studio如何实现这一目标。
简而言之 ------ React工程要点
分离外部状态存储 。文档/选择状态存在于
<Studio>中;播放控制(播放头、播放状态)存在于<Playback>中。播放控制以rAF(requestAnimationFrame)频率更新,不会触发撤销/重做目标的无效更新。
useSyncExternalStore+ 选择器模式 。组件订阅单一数据片段(useStudioSelector(s => s.field)),仅在数据片段变化时重新渲染。操作包(useStudioActions())保持稳定,不会导致重新渲染。热路径完全绕过React。播放、关键帧拖拽和姿态滑块拖拽都通过命令式操作直接修改引用/对象,通过命令式句柄重绘画布,并且仅在释放时与React交互一次。
currentFrameRef逃生舱。播放控制存储持有一个引用,EngineBridge的rAF循环直接向其写入。非订阅的消费者(检查器采样器、PMX交换快照)无需触发重新渲染即可读取实时播放头。具备快照桥接撤销功能的类Reducer核心结构 。由于预览期间的编辑会直接修改当前片段,存储还会维护一个不可变的
clipSnapshot(在最后一次提交/撤销/重做时进行的深拷贝)。commit()将该快照推入历史记录(past)------而不是被修改的片段------因此历史记录永远不会捕获到拖拽中的中间状态。提供者树结构
<Studio> // 外部存储 ------ 片段 + 选择状态(撤销/重做目标) └─ <Playback> // 外部存储 ------ 当前帧、播放状态(rAF循环更新不触发重渲染) └─ <StudioStatusProvider> // 外部存储 ------ PMX名称、FPS、消息(与页面重渲染隔离) └─ <StudioPage> // 布局外壳 + 文件处理器 ├─ <EngineBridge> // 无头组件 ------ 所有与引擎相关的副作用,返回 null ├─ <StudioLeftPanel> // 被记忆化 ------ 骨骼列表、表情列表、文件菜单 ├─ <StudioViewport> // 被记忆化 ------ WebGPU <canvas> ├─ <Timeline> // 切片订阅 ------ 摄影表 + 曲线编辑器 │ └─ <TimelineCanvas> // 命令式播放头 + 拖拽重绘句柄 ├─ <PropertiesInspector> // 切片订阅 ------ 姿态滑块、表情权重(播放期间通过rAF自行采样) └─ <StudioStatusFooter> // 切片订阅 ------ PMX名称、FPS、片段名称状态分层
层级 所在位置 说明 文档 context/studio-context.ts外部存储,切片订阅,撤销/重做目标 选择状态 context/studio-context.ts骨骼、表情、关键帧 播放控制 context/playback-context.ts外部存储; currentFrame,playing;存储自身拥有的currentFrameRef供rAF消费者使用(见下文注释)状态栏 components/studio-status.tsx外部存储;PMX文件名、FPS、临时消息 引擎引用 StudioPageengineRef,modelRef,canvasRef视图 Timeline中的本地 useState缩放、滚动、选项卡 界面 StudioPage中的本地 useState菜单栏、文件选择对话框 播放控制注释 :
currentFrameRef通过usePlaybackFrameRef()共享。EngineBridge 的 rAF 循环直接将实时播放头写入.current而不经过set(),因此非订阅的消费者无需任何React工作即可读取实时帧。订阅模型
Studio(文档/选择状态)、Playback(播放控制)和StudioStatus(状态栏)都是基于useSyncExternalStore的外部存储。组件通过useStudioSelector(s => s.field)/usePlaybackSelector(...)/useStudioStatusSelector(...)读取,因此每个组件仅在其依赖的数据片段变化时重新渲染,并通过use*Actions()写入,这些操作返回稳定的操作包,不会导致重新渲染。包装存储内部的set()也是撤销/重做的挂载点------commit()将快照推入历史记录,replaceClip()(用于VMD/PMX加载和"新建")清除历史记录,选择状态的变化从不触及撤销堆栈。热路径 ------ 交互时零React更新
三种高频交互(播放、关键帧拖拽、姿态滑块拖拽)都遵循相同的模式:命令式地修改引用/对象,通过命令式句柄重绘画布,并在释放时仅与React交互一次。
播放 ------
<EngineBridge>的 rAF 循环读取引擎时钟,将实时帧写入播放控制存储的currentFrameRef(通过usePlaybackFrameRef()共享的唯一引用),并调用playheadDrawRef.current(frame)------ 这是<TimelineCanvas>暴露的一个句柄,用于直接重绘播放头叠加层。没有每次tick的setCurrentFrame,因此不会重新渲染,但任何非订阅的消费者(检查器姿态采样、PMX交换快照)仍然可以通过该引用看到实时帧。自动滚动(当播放头移出视口时翻页)位于相同的命令式路径中,仅在罕见的翻页边界处触及React。暂停时,最终帧会通过setCurrentFrame刷新,以便暂停视图与最后绘制的内容一致。实时姿态/表情读数 ------
<PropertiesInspector>在隔离的叶子子组件中采样所选骨骼的姿态和表情权重。暂停时,它订阅currentFrame并在变化时重新采样;播放时,它运行自己的小型 rAF 循环,直接读取modelRef.current的runtimeSkeleton/getMorphWeights(),并通过相等性检查进行门控,以防止未变化的帧触发协调。这将逐帧的工作保持在父检查器和<StudioPage>之外。关键帧拖拽 ------
<Timeline>的移动回调命令式地修改关键帧的frame/ 通道值 / 轨道顺序,并触发dragRedrawRef.current(),这会增加一个用于静态层缓存失效检查的内部拖拽版本号,并重绘画布。selectedKeyframes条目被命令式地修改,使高亮跟随拖拽。在鼠标弹起时,一个单独的commit()克隆轨道 Maps 并创建撤销/重做快照,同时通过<EngineBridge>执行一次model.loadClip。姿态滑块拖拽 ------
<PropertiesInspector>的apply*Axis/applyMorphWeight函数在每次拖拽tick中以"预览"模式运行:原地修改匹配的关键帧(或插入一个),然后执行model.loadClip+ 跳转以更新3D视口。没有commit(),因此时间轴保持静态,检查器也不会协调。<AxisSliderRow>在拖拽期间保持本地的滑块值,以防止Radix控件回弹到过期的受控属性值。在onValueCommit时,触发一个单独的克隆 +commit()操作 ------ 只有这个commit()会进入撤销历史,因此一次拖拽是一个可撤销的单元,而不是数百个预览帧。简化轨道(关键帧精简)
MMD的插值模型使得经典的Ramer--Douglas--Peucker算法不太适用:每一帧存储一条完整的记录,旋转是一个由单个贝塞尔曲线控制的slerp-t插值(因此rotX/rotY/rotZ共享一个线段,而非独立的曲线),而平移具有三个独立的每轴贝塞尔曲线。Reze Studio 使用了一种专为此模型设计的、类似Schneider的自顶向下拟合方法,而不是逐个丢弃关键帧:
在
[第一帧, 最后一帧]范围内,对原始轨道在每一整数帧进行密集采样。尝试用一条VMD段覆盖整个区间------包含四个独立的贝塞尔曲线(旋转slerp-t + tX/tY/tZ)。对于每条曲线,根据密集采样点匹配端点速度来初始化手柄,然后通过在127参数空间的粗糙5^4网格搜索以及围绕最佳结果的局部5^4搜索进行优化。
如果最大逐点误差 ≤ ε(旋转为测地角,平移为每轴距离),则输出一个关键帧,并折叠其间的所有中间关键帧。
否则,在最接近最大偏差帧的原始关键帧处分割,并对两部分递归执行上述操作。
相邻的原始关键帧会被原样保留,包括其手动设置的插值。
较早的贪心"如果可容忍则丢弃"算法存在一个细微缺陷:丢弃一个关键帧会继承保留关键帧的贝塞尔手柄,而这些手柄是为较短的线段设计的------将它们拉伸到更长的区间会扭曲速度曲线,即使点对点误差 ε 很严格,也会产生可见的抖动。为每个输出线段进行自定义拟合避免了这个问题。固定的容差为0.5° / 0.01单位,不提供用户调节旋钮。整个操作作为一次
commit()提交,因此一次简化是一个撤销步骤。各部分职责所在文件
文件 职责 app/page.tsxNext.js入口 ------ 挂载所有提供者 + <StudioPage />context/studio-context.ts文档 + 选择状态存储, useStudioSelector, 操作context/playback-context.ts播放控制存储,选择器,操作, usePlaybackFrameRefcomponents/studio.tsxStudioPage------ 布局、文件处理、菜单栏、导出components/studio-status.tsx状态栏存储 + <StudioStatusFooter>components/engine-bridge.tsx与引擎相关的副作用(初始化、跳转、播放、rAF播放循环) components/timeline.tsx摄影表 + 曲线编辑器,命令式播放头 / 拖拽重绘 components/properties-inspector.tsx姿态滑块、表情权重、插值编辑器 components/axis-slider-row.tsx带有预览/提交分离 + 本地拖拽值的滑块行
