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、临时消息 |
|
引擎引用 |
StudioPage |
engineRef, 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、临时消息 |
|
引擎引用 |
StudioPage |
engineRef, 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.tsx |
Next.js入口 —— 挂载所有提供者 + <StudioPage /> |
|
context/studio-context.ts |
文档 + 选择状态存储,useStudioSelector, 操作 |
|
context/playback-context.ts |
播放控制存储,选择器,操作,usePlaybackFrameRef |
|
components/studio.tsx |
StudioPage—— 布局、文件处理、菜单栏、导出 |
|
components/studio-status.tsx |
状态栏存储 + <StudioStatusFooter> |
|
components/engine-bridge.tsx |
与引擎相关的副作用(初始化、跳转、播放、rAF播放循环) |
|
components/timeline.tsx |
摄影表 + 曲线编辑器,命令式播放头 / 拖拽重绘 |
|
components/properties-inspector.tsx |
姿态滑块、表情权重、插值编辑器 |
|
components/axis-slider-row.tsx |
带有预览/提交分离 + 本地拖拽值的滑块行 |
