《游戏引擎实现原理》9.2 四元数运算与最短弧插值
实现 helio 的四元数模块:乘法、共轭、归一化、向量旋转与 slerp。重点把插值里那个反直觉的分支跑出来——当两个四元数的点积为负时若不取反,t=0.5 会转 175° 的远弧;取反后只转 5°,两条轨迹相差整整 180°。两段轨迹的角度扫描一并给出真实数字。
category
实现 helio 的四元数模块:乘法、共轭、归一化、向量旋转与 slerp。重点把插值里那个反直觉的分支跑出来——当两个四元数的点积为负时若不取反,t=0.5 会转 175° 的远弧;取反后只转 5°,两条轨迹相差整整 180°。两段轨迹的角度扫描一并给出真实数字。
描述一个旋转最直觉的方式是三个角度——欧拉角。本节用可编译的 TypeScript 把它的失效点跑出来:pitch 转到 90° 时,两组完全不同的欧拉角给出逐元素相同的旋转矩阵,误差只有 6.12e-17。我们把死锁现场、丢掉的自由度与四元数的引入动机一次讲清。
把平移、旋转、缩放按固定顺序复合,就是每个物体的世界变换。本节给出 helio 的 TRS 复合与分量逆变换实现,用 vitest 断言 M 乘 M⁻¹ 约等于单位阵并写出实测误差,并说明非均匀缩放下法线为什么要用 R 乘 S⁻¹ 来变换。
同一组 16 个数字,按行主序读和按列主序读会得到两个不同的变换。本节讲清主序、手性与转置陷阱,说明 WebGPU / WGSL 的列主序约定与 CPU 到 GPU 的统一口径做法,给出 helio 可编译的列主序 Mat4,并用实测数字展示同一矩阵两种解释下的结果差异。
向量在引擎里怎么存,决定了批量运算能跑多快。本节把三维向量放进 Float32Array,对照 AoS 交错布局与 SoA 分离布局的字节分布,给出零分配的 addInto 约定,以及可编译的 Vec3Store 与 Vec3SoA 实现和实测求和结果。
把引擎里到处流动的名字压成整数:字符串驻留表让同名字符串只存一份、比较变成整数比较,资源 GUID 让文件路径确定性地映射成 64 位标识,名字表把组件类型、属性、系统登记成句柄。给出按字符串查 Map 与按句柄索引数组的 6.6 倍实测差距。
实现 helio 的三个零分配容器:用 Uint32Array 把 32 个布尔压进一个字的位集合、固定容量首尾相接的环形缓冲,以及把 next/prev 长在对象自己身上的侵入式链表。给出位集合与布尔数组的堆增量、遍历耗时实测,以及环形缓冲 1000 万次 push 的吞吐。
从零实现 helio 的两个基础容器:增长因子可配的动态数组,以及用 FNV-1a 加线性探测、Robin Hood 抢占与墓碑删除的开放寻址哈希表。给出 1.25×/1.5×/2× 三档因子的扩容与搬运实测,以及探测步数分布和墓碑退化数据,全部本机跑出。
把实体的创建、销毁、句柄复用收进一个实体注册表,复用第 6 章的世代句柄:删除实体后旧句柄立即失效,新实体沿用同一槽位却拿到新世代。给出可编译实现、断言用例、复用与不复用的实测对比,以及世代回绕撞上 NULL_HANDLE 这个真实边界与应对办法。
把「每种组件一个稀疏集」和「按组件集合分组的 archetype」两种组件存储都从零实现出来,用同一组操作对比:建 20 万个实体的耗时与堆净增量、以及查询并原地更新的耗时,把本机实测的真实数字写进正文,并给出各自的取舍边界与可操作的选择判据。
把「让物体动起来」用继承、组合、数据三种方式各写一遍,对照它们各自的代价;再在 6.2 的世代句柄之上从零搭出一套最小 ECS,用可编译的 MiniEcs 落地实体、组件与查询三件套,并用 vitest 断言「销毁实体后旧句柄立即失效」这一关键行为。
把 for (const e of entities) e.update(dt) 这样一段面向对象的更新循环,一步步改造成按组件列分组的批处理。本节给出改造前后两份可编译的 helio 源码与一份基准,在本机实测出耗时约 1.5 倍、堆增量 16.6 MB 对 0 MB 的差距,并说明哪些场景根本不该改。
同样 1M 个元素,AoS(对象数组)与 SoA(平行 TypedArray)在本机实测出 22.94 ms 对 14.47 ms、约 1.59 倍的差距,但堆增量是 62.6 MB 对 0 MB——TypedArray 的底层 ArrayBuffer 属外部内存,不进 JS 堆。本节讲清三种布局的真实差异,并给出「什么时候不该用数据导向」的判据。
缓存层级决定了一次访存要等多久:L1 一个周期,主存上百个周期。本节用 1M 元素的连续遍历与指针追逐、以及 4096 方阵的行主序与列主序求和做对照,在本机实测出 2.2 倍与 6.2 倍的差距,并说清在 V8 里数据导向的收益主要在分配与 GC 压力,而不在访存速度。
数值计算的另一类问题是边界而非精度:比较不能用等号、除法会炸出 Infinity、NaN 会顺着运算扩散。本节实现一组数值稳健工具——相对 epsilon 比较、saturate、clamp、safeDiv、sanitize、safeNormalize,并用 vitest 把每个边界行为钉成断言。
定点数把小数点钉死在固定位置,用纯整数运算换来跨平台逐位一致。本节实现 Q16.16:一个 int32 的低 16 位是小数、高 16 位是整数,乘法拆高低位避免溢出。实测同一段弹簧积分跑 10000 步:单精度与双精度相差 2.9e-7,而定点版本跑两遍结果逐位相同。
同一份浮点代码在不同平台上会算出不同结果,根源是加法不满足结合律、FMA 改变了舍入时机、单精度与双精度位宽不同。本节在 Node 里实测:(0.1+0.2)+0.3 与 0.1+(0.2+0.3) 相差 1.1e-16,一段积分跑 10 万步后单精度比双精度差了 0.2,有无 FMA 甚至能把 4.4e-16 变成 0。
《游戏引擎实现原理》卷一附录 D:汇总跟读本卷时最容易踩的坑与高频疑问。覆盖 erasable syntax 的写法替代、句柄失效的排查、内存布局的对齐陷阱、基准数字为何波动、Job System 在 Node 与浏览器的差异、反射为什么不用装饰器元数据,以及「怎么调试」这类实操问题,每条给出原因与解决方向。
《游戏引擎实现原理》卷一附录 B:跟读本卷所需的环境准备与代码复现方法。列出实测的 Node / TypeScript / vitest 版本,给出从零搭建 playground 的完整步骤、推荐的 tsconfig 与 package.json 并逐个解释编译选项、各层代码(L1 运行、L2 无头浏览器、L3 类型检查、B 基准)的跑通命令,以及常见环境问题的排查清单与目录结构约定。
《游戏引擎实现原理》卷一《引擎内核》全书导航页:说明本卷定位、读者画像与前置知识、学习节奏与三条阅读路径,交代贯穿项目《微光岛》与从零引擎 helio,讲清 L1–L4 可运行性口径,并按五个部分列出 18 章 54 节的完整章节索引,另含序、前言、附录 A–D 与常见问题速查表,方便读者按需定位、跳读与随时查阅。