1:I["d9f27c132951",[],"Layout",1] 2:I["37eafacc3f00",[],"Outlet",1] 3:I["37eafacc3f00",[],"UNSAFE_WithErrorBoundaryProps",1] 4:I["d9f27c132951",[],"ErrorBoundary",1] 5:I["11eb051da520",[],"ThemeSwitch",1] :HL["/assets/root-Bnzpi1uE.css","stylesheet"] 0:{"type":"render","basename":"/","clientVersion":"$undefined","routeDiscovery":{"mode":"lazy"},"actionData":null,"errors":null,"loaderData":{"root":null,"blog":null,"blog-page":null},"location":{"pathname":"/blog/pages/1","search":"","hash":"","state":null,"key":"default","mask":"$undefined"},"formState":"$undefined","matches":[{"clientAction":"$undefined","clientLoader":"$undefined","element":[[["$","link","css:/assets/root-Bnzpi1uE.css",{"rel":"stylesheet","precedence":"vite-rsc/importer-resources","href":"/assets/root-Bnzpi1uE.css","data-rsc-css-href":"/assets/root-Bnzpi1uE.css"}],"$undefined"],["$","$L1",null,{"children":[[["$","link","css:/assets/root-Bnzpi1uE.css",{"rel":"stylesheet","precedence":"vite-rsc/importer-resources","href":"/assets/root-Bnzpi1uE.css","data-rsc-css-href":"/assets/root-Bnzpi1uE.css"}],"$undefined"],["$","$L2",null,{}]]}]],"errorElement":[[["$","link","css:/assets/root-Bnzpi1uE.css",{"rel":"stylesheet","precedence":"vite-rsc/importer-resources","href":"/assets/root-Bnzpi1uE.css","data-rsc-css-href":"/assets/root-Bnzpi1uE.css"}],"$undefined"],["$","$L1",null,{"children":["$","$L3",null,{"children":["$","$L4",null,{}]}]}]],"handle":"$undefined","hasAction":false,"hasComponent":true,"hasLoader":false,"hydrateFallbackElement":"$undefined","id":"root","index":"$undefined","links":"$undefined","meta":"$undefined","params":{"index":"1"},"parentId":"$undefined","path":"","pathname":"/","pathnameBase":"/","shouldRevalidate":"$undefined"},{"clientAction":"$undefined","clientLoader":"$undefined","element":[["$","$L5",null,{}],["$","$L2",null,{}]],"errorElement":"$undefined","handle":"$undefined","hasAction":false,"hasComponent":true,"hasLoader":false,"hydrateFallbackElement":"$undefined","id":"blog","index":"$undefined","links":"$undefined","meta":"$undefined","params":"$0:matches:0:params","parentId":"root","path":"blog","pathname":"/blog","pathnameBase":"/blog","shouldRevalidate":"$undefined"},{"clientAction":"$undefined","clientLoader":"$undefined","element":"$L6","errorElement":"$undefined","handle":"$undefined","hasAction":false,"hasComponent":true,"hasLoader":false,"hydrateFallbackElement":"$undefined","id":"blog-page","index":"$undefined","links":"$undefined","meta":"$undefined","params":"$0:matches:0:params","parentId":"blog","path":"pages/:index","pathname":"/blog/pages/1","pathnameBase":"/blog/pages/1","shouldRevalidate":"$undefined"}],"patches":"$@7"} 7:[{"clientAction":"$undefined","clientLoader":"$undefined","handle":"$undefined","hasAction":false,"hasComponent":true,"errorElement":"$undefined","hasLoader":false,"id":"blog-index","parentId":"blog","path":"$undefined","index":true,"links":"$undefined","meta":"$undefined"},{"clientAction":"$undefined","clientLoader":"$undefined","handle":"$undefined","hasAction":false,"hasComponent":true,"errorElement":"$undefined","hasLoader":false,"id":"blog-post","parentId":"blog","path":":id","index":"$undefined","links":"$undefined","meta":"$undefined"}] 8:I["4d29546c44ff",[],"PrefetchCurrentRoute",1] 9:I["ba0f37e54449",[],"BackButton",1] 14:I["37eafacc3f00",[],"Link",1] 15:I["45dc6c57fe5c",[],"default",1] 6:["$","main",null,{"className":"mx-auto max-w-2xl px-5 pt-20 pb-16 sm:pt-24","children":[["$","title",null,{"children":"Blog"}],["$","$L8",null,{}],["$","$L9",null,{}],["$","header",null,{"className":"mb-8","children":[["$","h1",null,{"className":"text-3xl font-bold tracking-tight","children":"Blog"}],["$","p",null,{"className":"mt-2 text-sm text-muted-foreground","children":["共 ",13," 篇文章"]}]]}],["$","div",null,{"className":"flex flex-col gap-8","children":[["$","section","2026",{"aria-label":"2026 年","children":[["$","h2",null,{"className":"border-b pb-2 text-sm font-medium text-muted-foreground tabular-nums","children":2026}],["$","ul",null,{"className":"divide-y","children":["$La","$Lb","$Lc","$Ld","$Le","$Lf","$L10","$L11"]}]]}],["$","section","2025",{"aria-label":"2025 年","children":[["$","h2",null,{"className":"border-b pb-2 text-sm font-medium text-muted-foreground tabular-nums","children":2025}],["$","ul",null,{"className":"divide-y","children":["$L12","$L13"]}]]}]]}],["$","nav",null,{"aria-label":"分页","className":"mt-10 grid grid-cols-3 items-center border-t pt-6 text-sm","children":[["$","div",null,{"children":false}],["$","span",null,{"className":"text-center text-muted-foreground tabular-nums","children":[1," / ",2]}],["$","div",null,{"className":"flex justify-end","children":["$","$L14",null,{"to":"/blog/pages/2","prefetch":"intent","className":"flex items-center gap-1.5 rounded-md text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring","children":["下一页",["$","$L15",null,{"ref":"$undefined","icon":{"name":"arrow-right","size":24,"node":[["path",{"d":"M5 12h14","key":"1ays0h"}],["path",{"d":"m12 5 7 7-7 7","key":"xquz4c"}]]},"className":"size-4"}]]}]}]]}]]}] a:["$","li","h8gCJHpjiCaegoo1tc9s6",{"className":"$undefined","children":["$","$L14",null,{"to":"/blog/h8gCJHpjiCaegoo1tc9s6","prefetch":"intent","className":"group -mx-3 grid gap-1 rounded-lg px-3 py-5 outline-none sm:grid-cols-[5rem_1fr] sm:gap-6 focus-visible:ring-2 focus-visible:ring-ring","children":[["$","time",null,{"dateTime":"2026-10-30T17:53:46.545Z","className":"text-sm text-muted-foreground tabular-nums sm:pt-1","children":"10月30日"}],["$","div",null,{"className":"min-w-0","children":[["$","h3",null,{"className":"text-lg leading-snug font-semibold tracking-tight decoration-muted-foreground/50 underline-offset-4 group-hover:underline","children":""}],["$","p",null,{"className":"mt-1.5 line-clamp-2 text-sm leading-relaxed text-muted-foreground","children":" "}],false]}]]}]}] 16:T1117,从行走到连招:一次 MotionBricks 接入实验的反复排查 把 MotionBricks 接进引擎后,我先修左右脚步幅,再修待机站姿,最后补上攻击和三连击。自动检查陆续通过,实际操作依然让我失望。最难的部分逐渐变成:怎样确认一次修改真正改善了角色控制? 2026 年 10 月,我在自己的 HitagiEngine 中做了这次实验,希望角色根据操作和环境生成动作,减少为起步、转向、停步和跳跃手工组织过渡的工作。我一度撤回演示接入,后来又恢复代码,继续验证目标帧、步态相位和动作库的想法。 以下记录截至 10 月 31 日的排查过程。如果你也打算把生成模型接到玩家控制器上,相位和待机锚点这两轮实验,能说明条件构造与后处理怎样改变最终动作;最后的攻击实验则留下了尚未解决的操控问题。 第一阶段:先让模型输出驱动自己的角色 最初要打通的是从键盘操作到角色画面的完整路径。模型输出使用 SOMA 骨架,项目里使用的角色是 Sour Miku,两者之间还需要重定向。 实验使用 Windows Release 构建,通过 TensorRT 运行 FP32 SOMA 模型。 应用先把“继续往左走”“停下来”等键盘操作转换成目标和约束,交给模型。生成的动作经过骨架转换和接触修正,最终呈现在角色上。 这里先明确“预设”的含义:公开示例把参考姿态、目标放置和运行时参数组织成方便试用的配置。选择预设后,应用从中取出具体姿态并构造约束,再交给模型。模型实际消费的是姿态和运动约束;预设名称只属于应用层。 以本次使用的公开 SOMA ONNX 接口为例,一次调用包含以下内容: 这些输入最终编码为数值张量。详细字段以固定版本 SOMA 模型卡的输入表为准。 因此,行走、蹲走或坐下的差别,通过具体的身体姿态和运动约束表达。参考姿态可以从动作片段中提取,也可以手工制作。论文的 Smart Locomotion 将风格关键帧与操控产生的目标组合;Smart Object 则把交互关键帧绑定到物体,随物体的位置与朝向放置。公开预设提供了这套接口的使用示例。论文 §6 这也决定了后面的排错方向:需要检查预设最终构造出了什么条件,以及这些条件是否符合当前操作与环境。 图 1:本次接入的流程。蓝色区域是生成模型;历史反馈来自运行时播放的 SOMA 姿态,初音网格位于重定向和接触修正之后。 这条路径跑通后,角色能够响应移动操作。持续操作时,后半段却出现了像半蹲着走路的姿态。问题可能出在目标构造、生成姿态或重定向/IK,这次实验先从画面上最明显的腿部和接触处理查起。 第二阶段:从半蹲姿态查到接触修正 第一段录像之后,我的反馈是:后半段有一种突然半蹲着走路的感觉。 图 2:手动录像约 7.9 秒的姿态。结合下方视频的前后运动,可以观察动作是否突兀。 完整手动录像 A:12.184 秒。如果 Markdown 阅读器不支持内嵌视频,可以直接打开链接。建议先看完整片段,再回看约 7~9 秒。 检查膝盖和鞋底接触时,接入代码暴露出一个问题:落脚锚点滞留后,求解器会通过下压骨盆去满足它。这次修正限制了骨盆下压,并放松不可达的锚点。 接触处理的修正改善了部分表现。要解释剩下的异常,还需要看模型究竟收到了什么条件,以及输出在重定向前是什么样子。于是我提出:把模型真正收到的输入画出来。 第三阶段:把输入画出来,发现移动条件在减速 这一轮的重点是核对操作意图和模型条件是否一致。仅看最终角色,很难判断矛盾发生在哪一层。 第二段录像增加了诊断显示。黄色骨架代表应用提供的未来条件,蓝色骨架代表当前 SOMA 姿态;粉色和绿色分别显示原始输出与端点修正后的根轨迹。看清每一层的数据,成为这次排查的转折点。 图 3:手动录像 B,约 2.6 秒,第 589 次规划。黄色条件接近直立,位置仍在行进方向前方。正文插图已裁掉编辑器界面;黄色和蓝色骨架保留在场景中供对照。 b:["$","li","dLVutsVQou",{"className":"$undefined","children":["$","$L14",null,{"to":"/blog/dLVutsVQou","prefetch":"intent","className":"group -mx-3 grid gap-1 rounded-lg px-3 py-5 outline-none sm:grid-cols-[5rem_1fr] sm:gap-6 focus-visible:ring-2 focus-visible:ring-ring","children":[["$","time",null,{"dateTime":"2026-10-30T17:41:05.692Z","className":"text-sm text-muted-foreground tabular-nums sm:pt-1","children":"10月30日"}],["$","div",null,{"className":"min-w-0","children":[["$","h3",null,{"className":"text-lg leading-snug font-semibold tracking-tight decoration-muted-foreground/50 underline-offset-4 group-hover:underline","children":"从行走到连招:一次 MotionBricks 接入实验"}],["$","p",null,{"className":"mt-1.5 line-clamp-2 text-sm leading-relaxed text-muted-foreground","children":"$16"}],["$","ul",null,{"className":"mt-3 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground","children":[["$","li","4ziqXCREdYw5HfWi00Wvr",{"className":"flex items-center gap-1.5","children":[["$","span",null,{"aria-hidden":"true","className":"size-2 rounded-full","style":{"backgroundColor":"var(--affine-palette-line-magenta)"}}],"Engine"]}]]}]]}]]}]}] c:["$","li","KnYVgXGCg60BzfNbDedWu",{"className":"$undefined","children":["$","$L14",null,{"to":"/blog/KnYVgXGCg60BzfNbDedWu","prefetch":"intent","className":"group -mx-3 grid gap-1 rounded-lg px-3 py-5 outline-none sm:grid-cols-[5rem_1fr] sm:gap-6 focus-visible:ring-2 focus-visible:ring-ring","children":[["$","time",null,{"dateTime":"2026-10-09T11:47:15.837Z","className":"text-sm text-muted-foreground tabular-nums sm:pt-1","children":"10月9日"}],["$","div",null,{"className":"min-w-0","children":[["$","h3",null,{"className":"text-lg leading-snug font-semibold tracking-tight decoration-muted-foreground/50 underline-offset-4 group-hover:underline","children":"和 agent 一起优化 Hitagi Engine 的 C++ Modules 编译速度"}],["$","p",null,{"className":"mt-1.5 line-clamp-2 text-sm leading-relaxed text-muted-foreground","children":"重构 Hitagi Engine 的 gfx 时,我将资源创建从 Device 中分离,并用 module partition 按职责组织接口。随后,干净构建耗时从此前记录的约 97 秒增加到 123 秒。由于资源依赖和模块布局同时改变,这还不能直接归因于 partition。 这篇记录 2026 年 10 月 8 日到 9 日,我如何与 agent 一步步定位这次回退。后期一组 clang-cl 构建测到了 25.10 秒,gfx 分区仍然保留。不过,各阶段基线不同,不能将它们拼成一次严格的端到端对照 先确认构建系统实际做了什么 最初我让 agent 保存提交,再保持资源依赖设计不变,将接口分区合回主接口。分区版复测为 122.34 秒,合并版两次平均为 92.45 秒。agent 因此建议采用“单 cppm 接口 + 多个普通 cpp 实现”。 但我希望声明仍能按职责分散,而不是全部集中到主接口。于是继续追问:差距来自分区本身,还是当前构建方式? 这里需要区分文件扩展名和模块角色。export module gfx.dx12:command_list; 即使写在 .cpp 中,仍是需要生成 BMI(预编译模块接口)的接口分区;module gfx.dx12; 才是普通实现单元。真正影响构建的是这些单元的依赖和任务数量,而不是 .cppm 文件数。 agent 统计日志后发现:35 个分区加 6 个主接口,本应对应 41 个 gfx BMI,却产生了 164 次编译。engine、hitagi_editor、editor、unit_tests 各生成了一份。 我让它转向 XMake 源码,捕获复用检查中的实际参数差异。问题集中在三处:clang-cl 路径未正确过滤 /Fd、/external:I/W 等参数差异;公共包原始编译选项未完整继承;扫描器又将已复用的依赖源码加入普通编译队列。 修复只规范化复用比较,不删除源码编译所需的 include 路径,也不通过 reuse.nocheck 忽略语言、运行库及宏配置差异。普通实现文件当时被标为 public,也是工程侧的触发条件;后续已改为 private,仅接口和接口分区保持 public。 "}],["$","ul",null,{"className":"mt-3 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground","children":[["$","li","4ziqXCREdYw5HfWi00Wvr",{"className":"flex items-center gap-1.5","children":[["$","span",null,{"aria-hidden":"true","className":"size-2 rounded-full","style":{"backgroundColor":"var(--affine-palette-line-magenta)"}}],"Engine"]}],["$","li","eGJm3PFyC6D0oo59KJ_pN",{"className":"flex items-center gap-1.5","children":[["$","span",null,{"aria-hidden":"true","className":"size-2 rounded-full","style":{"backgroundColor":"var(--affine-palette-line-teal)"}}],"C++"]}]]}]]}]]}]}] 17:Tf71,Introduction 本文介绍了一种结合Animation Layering技术和MoE的角色动画生成模型。做到具有一定编辑能力的Learning Generative Model. 一般基于Learning的方法在编辑、控制这两个方面都有很大的不足。个人总结的原因如下 使用End2End的模型,控制方法就被定死了,想要替换控制方法必须retrain整个模型, 控制方式变化后,网络的结构,特别是网络的输入格式也要响应调整, 动画师对动画的调整一般是建立在已有动画的基础上反复修改,而现有的基于Laerning的模型,没有这种修改方式的切入点。 这些原因导致在实际使用Learning模型时,体验是非常糟糕的。 作者从这个切入点入手,将模型分为两个部分控制模块和生成模块分别训练。受Condition VAE 这篇论文 启发,生成模块的训练使得其代表了motion的流形,从而对于不同的输入都能有一个pose的输出。控制模块则是计算出控制信号所对应的生成模块的输入。同时为了解决3. 问题,作者在控制层面使用reference motion作为控制方式,而这个reference motion的来源一般可分为两类:原始数据、动画师编辑后的数据。 此篇论文的主要贡献如下 一个基于learning的animation layering动画框架,用于合成类型丰富的运动 一个用户透明的控制接口,使得可以对运动进行编辑,同时不必重新训练动作生成网络 一组控制模块用于对合成角色动画,甚至是有接触的多名角色动画 一个评估框架,用于评估不同应用下武术动作制作过程 Overview 为了解决基于学习的 Motion Synthesis 难以编辑,特别是动画师无法直观的编辑人物的动作,论文作者提出了如上图的架构。图中分为三个模块,分别是用户可以编辑动作的Control Modules,以及动作生成模块 Motion Generator。由于Control Modules可以有多个控制的方式,而Motion Generator只接受一个输入,因此这里还提出了一个Control Interface模块,用于将Control Modules的输出整合成Motion Generator的输入,具体将Control Interface中在阐述。 个人认为,这里的Motion Generator可以看作是MoCap数据库的表示。而它的输入是一些key joints的预测信息,以及当前姿势信息。Generator 将这些信息投影到流形上得到下一帧动作的变化信息。而这个输入的形式又可以在业界中常用的技术中找到类似的数据格式,比如motion matching使用一段预测的motion去数据库中找到类似的motion,又比如关键帧技术的插值后生成也是一段动画,而这段动画刚好作为动画师调整动画的参考。那么如果我们将一个motion片段作为generator的input,那么我们就可以和现有技术结合起来用。而这时motion generator就可以看成是motion数据库的一个压缩。 Motion Generator 这篇文章的Motion Generator和MANN 所用的生成模型基本一致,它的gating network输入是key joints在未来一秒(7个采样点)的速度大小,输出blending因子。姿势预测网络的输入是key joints在过去到未来两秒内(13个采样点)的位置、速度、朝向、以及路径积分、角度累积 ,同时还有当前帧关节的位置、角度、速度 。key joints的信息 作为控制信息,而当前帧的关节信息 则作为状态信息,两者通过Motion Generator共同决定下一帧的姿势。 个人认为key joints的选取,关系到Motion Generator能否生成合理的姿势。文章中选取的是hip, legs, arms, spine, and head。这些key joints都比较均匀分布在身体上。这里可以有两个解释 根据 local motion phase这篇文章可以知道,使用多个局部信息去指导 gating network 能够让输出更加好,gating network可能会从key joints的窗口中学到Phase的概念, d:["$","li","oA_J1ELvvf",{"className":"$undefined","children":["$","$L14",null,{"to":"/blog/oA_J1ELvvf","prefetch":"intent","className":"group -mx-3 grid gap-1 rounded-lg px-3 py-5 outline-none sm:grid-cols-[5rem_1fr] sm:gap-6 focus-visible:ring-2 focus-visible:ring-ring","children":[["$","time",null,{"dateTime":"2026-10-09T11:44:20.855Z","className":"text-sm text-muted-foreground tabular-nums sm:pt-1","children":"10月9日"}],["$","div",null,{"className":"min-w-0","children":[["$","h3",null,{"className":"text-lg leading-snug font-semibold tracking-tight decoration-muted-foreground/50 underline-offset-4 group-hover:underline","children":"⚓{0x05} Neural Animation Layering for Synthesizing Martial Arts Movements"}],["$","p",null,{"className":"mt-1.5 line-clamp-2 text-sm leading-relaxed text-muted-foreground","children":"$17"}],false]}]]}]}] 18:Te76,背景 Motion Matching 的发展 motion matching技术是最近新兴的角色动画系统的技术,主要解决不同动画序列之间的过渡。其核心思想是,建立一个庞大的小序列数据库,当需要进行动作的过渡时,算法计算出未来一小段时间的姿势、速度、轨迹等你所考虑的指标,然后在数据库中搜索相匹配的动画序列,然后将其播放。在实际使用的时候,可以根据需求调整匹配的具体形式,如速度的权重要大,轨迹的权重可以小一点等等。同时,可以所有动作序列进行分类,然后可以根据需求限制搜索空间。 那么显而易见的是,只要数据库的序列足够丰富,对动作之间的过渡进行人工干预的情况就会变少。因此,基于此技术可以非常方便的搭建其一个动画系统。但目前这种也有其局限性,比如对于一些非 locomotion 的动作来说,如何得到丰富的的小序列是比较困难的。以格斗游戏为例子,对于同一个连招,按下按钮的持续时间、按键之间的间隔不同会影响角色的动作快慢、力量感等等。而这些动画越是具有张力、艺术性,在现实世界就越难做出了,也就难以使用动作捕捉。而这部分还是会使用传统的方法来实现,比如动画师通过操纵骨骼、蒙皮来生产关键帧等等。当然,还有一些其它问题,比如当数据库的序列过多时,搜索算法就很容易成为性能瓶颈。 本文主要介绍 Holden 等人利用深度学习技术来去除Motion Matching的运行时数据库 。 总体框架 论文作者根据 Motion Matching 的流程,找到三个地方使用深度学习来优化,分别是搜索、动画数据,动画播放。 以一个最基础的motion matching运作流程为例,当一个特征 (姿势、速度、轨迹等)被输入后,利用 k-nn 算法找到数据库中临近的一个特征 ,然后再根据 找到对应的动画 (即一个键值对),最后不断播放下一帧直到有新的 输入进来。 Decompressor 作者首先对数据库这一部分下手,使用自编码的技术来压缩动画序列的数据库(但还保留了特征 的数据库)。具体做法如下 将 编码到隐变量 ,然后将 和对应的特征 一起作为Decoder的输入解码,得到 计算 ,计算梯度,优化网络参数 这个算是比较常用的自编码套路。当网络训练好后,我们就可以将 对应的 储存起来,也就是 压缩到了 。现在我们从存储 变成了存储 ,这样就达到了压缩动画序列的数据库了。在使用的时候,只需要额外的将 经过Decompressor网络生成对应的 即可。但现在还是存在两个数据库 和 ,接下来我们讲如何去掉运行时的这两个数据库,变成无数据库的技术。 Stepper 为了在运行时除去 和 两个数据库,同时还有保证可以正常工作,我们需要考虑一个问题:如何知道下一帧的动画?这就是一个典型的预测问题了。 文中介绍了一个名为 Stepper 的方法,用于计算下一帧的 ,然后就能计算出下一帧的 了。具体做法如下 对于一个窗口为 的动画 ,我们需要预测 ,那么 首先赋值 利用网络预测 然后赋值 如此进行完 窗口,得到 计算 ,梯度,优化网络 这个步骤的思想类似于 motion matching 中的搜索算法,即根据当前的输入,寻找符合预期的一段动画序列。 一旦网络训练好后,我们可以将当前的 喂给网络 ,得出 ,然后加回去后得到下一帧的 ,之后再使用Decompressor就能得到下一帧的动画 e:["$","li","p-hWsfRHvT",{"className":"$undefined","children":["$","$L14",null,{"to":"/blog/p-hWsfRHvT","prefetch":"intent","className":"group -mx-3 grid gap-1 rounded-lg px-3 py-5 outline-none sm:grid-cols-[5rem_1fr] sm:gap-6 focus-visible:ring-2 focus-visible:ring-ring","children":[["$","time",null,{"dateTime":"2026-10-09T11:44:20.780Z","className":"text-sm text-muted-foreground tabular-nums sm:pt-1","children":"10月9日"}],["$","div",null,{"className":"min-w-0","children":[["$","h3",null,{"className":"text-lg leading-snug font-semibold tracking-tight decoration-muted-foreground/50 underline-offset-4 group-hover:underline","children":"⚓{0x04} Learned Motion Matching"}],["$","p",null,{"className":"mt-1.5 line-clamp-2 text-sm leading-relaxed text-muted-foreground","children":"$18"}],false]}]]}]}] 19:T12c7,本文是对Motion Controller Using Motion VAEs 的阅读笔记 这一篇论文结合CVAE和MANN 来生成骨骼动画,并使用强化学习的方法来控制Decoder的输出。对于动画这种序列型数据,作者并没用考虑使用RNN等循环网络,他的解释是,骨骼动画具有马尔科夫性质——下一刻的骨骼位置如何变化取决于当前骨骼的所有物理参数。 生成模型 上图是此论文所用的生成模型CVAE+MANN,作者称之为MVAE,主要分为Encoder和Decoder两个部分,其中condition是上一帧的姿态 ,是马尔可夫性质的表现。从功能上来看和传统的CVAE有两个不同: 自回归性,因为我们的目的是预测,那么就需要将前一帧姿势作为输入 Decoder所用的网络是更为复杂的MANN,这里是为了更好的生成姿势,但不同之处在于blend后的网络的每一层输入加入了隐变量 ,用于在错误姿势的输入下,能够重构出正确的姿势。 Decoder 由于Encoder部分和传统的CVAE没什么不同,因此在这里不细讲。我们在这里着重讲一下Decoder部分,即MANN。 MANN其实是MoE(Mixture-of-Experts)概念运用在深度学习上的产物。它定义了一组专家网络(Expert Network),再不同输入下每个专家网络会按照不同的权重进行求和得出一个被用于实际处理输入的网络。比较形象地来看,专家网络就像一个智囊团,里面的每个人都是不同领域的专家,当向智囊团(一组专家网络)抛出一个问题时,每位专家都会对这个问题有不同的解答(专家网络的参数),最终智囊团会结合各位专家的意见给出答案(网络输出),显然对于此问题领域下的专家更具权威性(专家网络的权重)。 回到图中,需要实现这种模型,需要两部分: Gating Network,用于根据输入来计算权重,这里如果用一些周期函数来计算权重的话,就和PFNN 很像了, Expert Networks,一组专家网络用来加权求和,得出混合网络用来处理输入,得到输出。 预测动作的稳定性 由于这个模型是自回归的,那么对误差累积的考虑就不可避免了。这就会导致预测的动作非常不稳定,关节的位置过一会儿就“飞”走了。“飞”在这里比较形象,由于误差的累积,导致输入的姿势已经不在正常的姿态分布空间内,预测出来的肯定就不是正确的。 为了解决这个问题,作者使用了scheduled sampling的技术 ,先采取全监督的方式,然后逐步的变为自回归的方式,具体做法为, 首先在每一个epoch定义一个概率 ,用于每次forward的时候,以 的概率使用ground truth作为condition,以 的概率使用预测值作为condition 将整个训练过程分为三部分进行,全监督模式( )、scheduled sampling模式( )、自回归模式( ),每部分的epcoch设置为 在scheduled sampling模式下,随着每个epoch完成,概率 线性减小至 下图显示了运行时,模型生成动作的稳定性。图片上方是采用了scheduled sampling方式的生成结果,下方则是没有采用的生成结果。从上方可以看出就算初始状态将点聚集在一起,采用了scheduled sampling技术的生成模型还是有能力将其恢复。 为了使用shceduled sampling需要在训练上利用一些技巧,比较复杂,这里就不再赘述,感兴趣可以看原文的MVAE Training章节的后半部分。至于为何shceduled sampling有这种魔力,不过是让Decoder用正确的结果作为condition。同时由于Decoder中的prediction network的每个layer输入都带上了 ,使得Decoder更看重 控制模型 当生成模型MVAE被训练好后,我们就已经有一定的控制能力了,比如通过 可以控制输出,或者利用蒙特卡洛树搜索(给定第一帧,在隐空间 采样 次,并递归进行 次,从 个搜索路径选择最好的一条,并移动至路径开头的那一帧,如此反复),但这些方法的控制能力都很弱,因此作者提出了使用DRL的方法来做一个控制模型。 从图中来看,将Encoder部分替换为控制网络,Env根据condition和控制策略来生成目标 ,然后由网络 生成隐变量 ,简而言之就是控制隐变量 的生成,从而达到控制动作的预测。它的累积奖赏为累积折扣奖励 其中, 代表环境状态, 表示agent所作出的动作, 表示 帧的奖励,对于不同的控制方式有不同的计算方法,之后会详细展开 表示折扣因子。 结合上图来讲,agent会根据当前环境状态使用网络 来计算 ,这里 表示网络 的参数,括号内看作是在 环境下做出动作 的概率。 f:["$","li","NaEzXkbcZY",{"className":"$undefined","children":["$","$L14",null,{"to":"/blog/NaEzXkbcZY","prefetch":"intent","className":"group -mx-3 grid gap-1 rounded-lg px-3 py-5 outline-none sm:grid-cols-[5rem_1fr] sm:gap-6 focus-visible:ring-2 focus-visible:ring-ring","children":[["$","time",null,{"dateTime":"2026-10-09T11:44:20.721Z","className":"text-sm text-muted-foreground tabular-nums sm:pt-1","children":"10月9日"}],["$","div",null,{"className":"min-w-0","children":[["$","h3",null,{"className":"text-lg leading-snug font-semibold tracking-tight decoration-muted-foreground/50 underline-offset-4 group-hover:underline","children":"⚓{0x03} 论文笔记 Motion Controller Using Motion VAEs"}],["$","p",null,{"className":"mt-1.5 line-clamp-2 text-sm leading-relaxed text-muted-foreground","children":"$19"}],false]}]]}]}] 1a:Tc44,本文是Tutorial on Variational Autoencoders 一文的学习笔记。有人可能会问了:“编号为啥不是 0x01?” 这是因为我第一篇文章想大致讲一下目前利用深度学习来生成骨骼动画的研究进展。但随着工作推进,发现要讲好没那么简单,所以就搁置了。 变分自编码器(VAEs)是一种生成模型,主要思想是找到一种映射,将服从正态分布的空间映射至样本空间,即 ,此时在 上进行采样,就可以得到预测的值 。 关键在于如何优化 使得 可以很好地生成 。联想分类器的输出,其分量代表每种类别的可能性。那么在这里,我们可以这么思考,生成模型可以根据 可能生成不同的 ,关键在于让它尽量生成地生成我们想要的 。概率的表述:采样 ,将 喂给模型后生成和 相似的概率最大,利用全概率公式 公式中 对应采样, 对应模型在不同 下生成 的概率(相似度)。这里用模型这个词而不是 ,原因是全概率公式中使用的是条件分布,而 是一个确定的东西。如果直接使用的话,就退化成Autoencoders 在VAEs中,一般将 ,其中 是一个hyperparameter, 是单位矩阵。这样就能够巧妙的将确定的 ,变为一个以均值为 的分布,从而使用全概率公式。那么此时的模型就可以看成 How to train? 现在我们的目的就是最大化 ,怎么做? 直接做 首先采样大量的 ,然后计算出对应的 ,根据蒙特卡洛方法, 然后一般采用欧几里得距离来计算 ,即 这种方法并不合适,例如考虑图像“2”被偏移了一段距离作为预测,那么在像素计算距离时就会得到很大的数,从而被模型认为是一个“不好”的预测。而在人看来,它应该是一个好的预测。这是因为度量“相似”的欧几里得距离并不是一个足够好的方法。同时这种直接采样,会采样到大量的无贡献的点。 利用KL散度 观察公式 ,对于大多数给定的 , 在很多情况下都是 ,这是因为理想模型, 会大概率地对应到一个 ,而小概率地对应到其它地 。但是如果我们能够根据 来选取一个可以让 高概率地生成 的分布,那么训练效率就会大大增加,我们记这条件个分布为 ,它相对于 的空间更小。 我们将 式右边的 替换为 ,即 它与 的关系可以由KL散度(描述两个分布间的相似程度,通俗理解可看 )和应用贝叶斯公式来得到,即 在这个式子中,我们希望 能够足够的接近理想的采样分布 ,此时KL散度就会接近 。现在我们对上式整理一下,得到 由于这个 的分布是任意设定的,那么我们可以将 分布是依赖于 的取值,即改写为 ,此时 式变为 这个式子还是有些复杂,当关键点在两处 和 ,在具体代码实现上,可以看成Encoder和Decoder。前者在数据集中取出一个样本 ,由 这个分布生成 ,后者根据 来生成预测值 。随着训练正常进行,等式左侧的数值应该不断增加。 10:["$","li","8mnaNB_Q8u",{"className":"$undefined","children":["$","$L14",null,{"to":"/blog/8mnaNB_Q8u","prefetch":"intent","className":"group -mx-3 grid gap-1 rounded-lg px-3 py-5 outline-none sm:grid-cols-[5rem_1fr] sm:gap-6 focus-visible:ring-2 focus-visible:ring-ring","children":[["$","time",null,{"dateTime":"2026-10-09T11:44:20.614Z","className":"text-sm text-muted-foreground tabular-nums sm:pt-1","children":"10月9日"}],["$","div",null,{"className":"min-w-0","children":[["$","h3",null,{"className":"text-lg leading-snug font-semibold tracking-tight decoration-muted-foreground/50 underline-offset-4 group-hover:underline","children":"⚓{0x02} Variational Autoencoders"}],["$","p",null,{"className":"mt-1.5 line-clamp-2 text-sm leading-relaxed text-muted-foreground","children":"$1a"}],false]}]]}]}] 1b:Teba,最后更新时间: 2021年11月22日 12点55分 这一篇文章主要讲Taku Komura课题组基于Learning的方法在motion合成和控制方面所作的研究。具体路径如下,也可以在他们在GitHub的项目,AI4Animation中一窥究竟。 PFNN: Phase-functioned neural networks for character control MANN: Mode-adaptive neural networks for quadruped motion control. NSM: Neural state machine for character-scene interactions. Local motion phases for learning multi-contact character movements. 每一篇文章都是前者的演进,只要抓住Phase和MoE这两个概念,那么理解起来就会比较容易。 本文将分为四大部分,分别是背景,生成网络,控制模型,讨论和总结。其中涉及到的数据增强等一些细节内容不会在文中赘述,而是另开一篇文章总结这些细节中的技巧。 背景 问题:对于骨骼动画,我们如何在一定的控制条件下,生成自然的动作? 在上面4篇文章之前,已经有一些基于Learning的研究,比如CNN ,RNN 等,在Holden的CNN 这篇中,就已经可以看到phase的出现,即周期函数中相位的在动作中的表述。到了Phase-functioned nerual networks(下称PFNN)这篇文章,Phase被重度地使用。 Phase(相位)具体代表什么呢?这里我们举例说明,考虑一个简单的动作——走路,我们可以将其看成是一个双脚交替前进的周期运动,在一个周期开始时,我们指定Phase在左脚着地为0,右脚着地为 ,周期结束时的左脚着地为 ,双脚交替之间的姿势进行插值。也就是说,Phase代表了一个动作周期中的某刻的姿势。 然而在实际模型运行的时候,动作是多变的,有时快跑、有时慢跑、有时停下、有时拐弯,那么具体Phase的值就不是我们能预先知道的,因此我们让网络学习Phase的变化,然后不断更新Phase即可。 上面介绍了Phase和动作之间的关系,那么具体如何使用Phase来指导动作的生成呢?直觉是将Phase作为网络的输入,同时输出Phase的更新。而作者利用Phase来动态调整网络参数,之后再来用这个调整的网络进行motion的输出。 注意,分号后面的相位 不是 的输入,而是指相位 决定生成网络 的网络参数。 生成网络 上面提及的4篇论文,在Phase和动态这两个点上可以依次体现为 指出locomotion中的周期性,用显式的方法来指导网络的动态更新, 使用gating网络去隐式的学习Phase的概念来动态更新动作预测网络, 将Phase和gating网络的输入进行笛卡尔积,然后gating网络决定如何动态更新动作预测网络, 用和环境接触的关节进行Phase标记,这些不同的关节对应不同的Phase。 在开始讲解其中的细节之前,我们先对这4篇论文中生成网络的输入和输出做一个简单的分类,以便于体现Phase在这些网络中的影响。 输入 为了比较号的区分一些概念,本文所指的输入是直接喂给给网络的数据,不包括hyper-parameter。当然相位 也不是网络的输入(尽管它参与了网络的运算) 我们可以对输入的特点分为三类: Trajectory: 路径信息,如2D平面中的路径位置、速度、朝向,一般会使用过去到未来的一段窗口信息, Pose: 姿势相关的信息,关节的位置、速度、旋转角度,等等。一般只使用一帧信息,且坐标位于以根节点为原点的坐标系中, Control: 控制信息,根据控制策略的不同有所差异,比如有target位置、想要的速度、朝向、或者action的类型,gait类型等等。有可能使用一段窗口信息、也有可能使用一帧的信息。 11:["$","li","sTcx-FFaB7",{"className":"$undefined","children":["$","$L14",null,{"to":"/blog/sTcx-FFaB7","prefetch":"intent","className":"group -mx-3 grid gap-1 rounded-lg px-3 py-5 outline-none sm:grid-cols-[5rem_1fr] sm:gap-6 focus-visible:ring-2 focus-visible:ring-ring","children":[["$","time",null,{"dateTime":"2026-10-09T11:44:20.491Z","className":"text-sm text-muted-foreground tabular-nums sm:pt-1","children":"10月9日"}],["$","div",null,{"className":"min-w-0","children":[["$","h3",null,{"className":"text-lg leading-snug font-semibold tracking-tight decoration-muted-foreground/50 underline-offset-4 group-hover:underline","children":"⚓{0x01} Phase 与 MoE 的碰撞——基于动态网络的motion生成模型"}],["$","p",null,{"className":"mt-1.5 line-clamp-2 text-sm leading-relaxed text-muted-foreground","children":"$1b"}],false]}]]}]}] 1c:T73f,SoA和AoS In computing, array of structures (AoS), structure of arrays (SoA) and array of structures of arrays (AoSoA) refer to contrasting ways to arrange a sequence of records in memory, with regard to interleaving, and are of interest in SIMD and SIMT programming. 根据Wikipedia的介绍,SoA是一种并行友好的layout,这种数据结构是我在学习如何用OpenGL向GPU加载顶点数据时时了解到的。比如在编辑顶点数据时,我们常常会分别对顶点的不同属性单独处理,因此当使用SoA时,能保证缓存友好。打个比方,当我们想要批量修改顶点的法向量时,使用SoA这种layout,能够保证不会载入其它属性(位置、颜色)的数据到Cache中。 当然,除了顶点,还有许多地方也会用到,比如在ECS架构,大多数`System`只处理一种类型的`Component`,这类似于对顶点的属性进行处理。而`Entity`则类似于顶点的概念。 实现 这里我们想要实现SoA的容器,类似于google/filament中的SoA模板容器filament/StructureOfArrays.h at main · google/filament (github.com),模板参数为属性。这里大部分接口都是类似的,具体实现因为数据存储方式不同而有所区别。 在google/filament中,数据存在raw buffer中,并由Allocator进行分配容量,访问接口时,利用类型大小和位置计算偏移,并进行类型转换。这里为了利用 C++17 的 PMR 特性,我们尝试使用标准库的容器进行实现。 SoA一个大致的样子如下 template class SoA { /* Some things */ }; std::tuple 在实现时的第一个问题是,我们如何根据Types构建出struct。这里我们可以用std::tuple来表示struct。std::tuple - cppreference.com中的描述如下: Class templateis a fixed-size collection of heterogeneous values. 12:["$","li","k7lEPaHrHe",{"className":"$undefined","children":["$","$L14",null,{"to":"/blog/k7lEPaHrHe","prefetch":"intent","className":"group -mx-3 grid gap-1 rounded-lg px-3 py-5 outline-none sm:grid-cols-[5rem_1fr] sm:gap-6 focus-visible:ring-2 focus-visible:ring-ring","children":[["$","time",null,{"dateTime":"2025-05-02T04:58:56.273Z","className":"text-sm text-muted-foreground tabular-nums sm:pt-1","children":"5月2日"}],["$","div",null,{"className":"min-w-0","children":[["$","h3",null,{"className":"text-lg leading-snug font-semibold tracking-tight decoration-muted-foreground/50 underline-offset-4 group-hover:underline","children":"基于tuple的SoA容器实现"}],["$","p",null,{"className":"mt-1.5 line-clamp-2 text-sm leading-relaxed text-muted-foreground","children":"$1c"}],["$","ul",null,{"className":"mt-3 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground","children":[["$","li","eGJm3PFyC6D0oo59KJ_pN",{"className":"flex items-center gap-1.5","children":[["$","span",null,{"aria-hidden":"true","className":"size-2 rounded-full","style":{"backgroundColor":"var(--affine-palette-line-teal)"}}],"C++"]}]]}]]}]]}]}] 1d:T11d8,在昨天深夜的时候,Starke推了一条他们最新出炉的一篇siggraph。视频中的人物动画反倒是不怎么吸引我,重点是下方这个Phase Manifold。我第一眼看到的时候就觉得这肯定是近两年内角色动画中最漂亮的一篇工作。 文章和视频在这个GitHub repo中 看完这篇文章后,也非常冲动地先写下这篇blog,作者在文章中有很多想法在曾经出现在我的脑海中,这也是为什么编号是0x08。至于中间的这几篇文章为什么还没有填,还是课程比较繁重,所以打算暑假再填,(当然不是因为懒。大致的目录以及对应的文章如下 0x03 NSM, LMP对应的是19年和20年Strake的文章。没错,这人的文章属于那种能给人inspiration的文章(类似的还有Motion Field)。 Neural state machine for character-scene interactions. Local motion phases for learning multi-contact character movements. 0x04 MVAE,对应的是20年的 Character controllers using motion VAEs 0x05 Motion Graph 和 Motion Field,这两篇也是非常经典的文章,不管是业界还是学界都有应用和延申 0x06 Motion Matching,在业界中属于比较前沿的技术,属于简化版的Motion Field,由于它的简单好用,近些年频繁地出现在学界的研究中。(如果有时间的话,这里会写一个非常非常简单的demo,逃 0x07 这里会重新提起Phase这个概念在动画领域其他分支上的工作 DeepMimic, 在基于DRL的、物理的角色动画中使用Motion数据来给予奖励,属于让agent去track动画数据,一般称为motion tracking Robust motion in-betweening,由Transformer启发的深度补帧工作,在航海日志中属于一个小插曲,篇幅可能不多,但其中的一个position encoding可以联想到NSM中的Phase的计算。(这个联系我在前两个月中发现,在现在要讲的这篇DeepPhase中作者也有提及,第一次感觉和大牛离得辣么近 0x08 就是这篇文章啦,其实还有几篇关于Physiced的文章笔记,以后有时间再发把。废话不多说,我们进入正题 背景 截至2022年6月12日,虽然这个系列中间缺了几篇,但是如果看过的话,能够大致的清楚这篇文章想要做的是什么。 文章的标题其实指的就是一个编码器,能够从原始的motion数据中学习到Phase的表示。这个编码器有三个非常不错的特点: 无监督学习。更确切的说,在学习Phase的流形这个目的上,是无监督的。 对Phase这个latent space,引入“周期性”这一约束。这一设定的假设是,时间和空间的局部motion被认为是具有周期性的(笔者在前些天胡思乱想的时候也有这种感觉,可以用信号处理的方式来解释motion。在比较老的工作中,信号处理的方法更多用在motion的对齐以及编辑上,比较有代表的是Motion signal processing这一篇工作) 多个Phase通道,类似于LMP中的Local Phase。 为什么这些个特点非常不错?下面有几个原因 在 中提到,对于四肢动物来说,Phase不能像人类的行走奔跑这类locomotion那样基于左右脚接触地面来标记,因为四肢动物的触地是复杂的,我们无法通过某些规则来计算出来。更不用说复杂的动作如何计算(比如跳舞)。 在2020年,Starke的LMP这篇文章就介绍篮球这个运动的Phase提取,更确切地说,他通过了“关键”部位地接触信号的高低电平(触地和拍球),通过简单的信号处理方法,拟合到正弦曲线上,从而算出相位。然而,这对于MoCap原始数据来说,需要找到接触的时间点(timing),其中一些计算的参数阈值也要设置合适)。({0x03} 文章链接待更新) 先前基于Phase的工作,需要接触信息,但导致了其他没有接触的关节运动效果并不好,属于一种人为的局部attention,这也使得在舞蹈方面手部的运动效果不好 总的来说,DeepPhase非常像MANN和LMP的结合,但能够给我们更加high level的思考Phase的本质。值得一提的是,在{0x07 文章链接待更新} DeepMimic和Robust motion in-betweening中,也有类似的表达,前者使用简单的 来向agent指出当前的Motion Reference的Phase,而后者则采用类似于Transfomer的position encoding来指出当前插帧的在整段动画中的时间位置,而这个数学表达形式很像{0x03 文章链接待更新}中NSM的2D Phase,即都是以正弦余弦函数表达。 13:["$","li","UC-xpFOWty",{"className":"$undefined","children":["$","$L14",null,{"to":"/blog/UC-xpFOWty","prefetch":"intent","className":"group -mx-3 grid gap-1 rounded-lg px-3 py-5 outline-none sm:grid-cols-[5rem_1fr] sm:gap-6 focus-visible:ring-2 focus-visible:ring-ring","children":[["$","time",null,{"dateTime":"2025-05-02T04:29:09.571Z","className":"text-sm text-muted-foreground tabular-nums sm:pt-1","children":"5月2日"}],["$","div",null,{"className":"min-w-0","children":[["$","h3",null,{"className":"text-lg leading-snug font-semibold tracking-tight decoration-muted-foreground/50 underline-offset-4 group-hover:underline","children":"⚓{0x08}DeepPhase. 关于Phase的编码器"}],["$","p",null,{"className":"mt-1.5 line-clamp-2 text-sm leading-relaxed text-muted-foreground","children":"$1d"}],false]}]]}]}]