这里是全站文章的归档入口,下面是按时间倒序排列的全部文章。若想按主题浏览,请从首页的专题分类进入。
《游戏引擎实现原理》8.2 矩阵、行/列主序与左/右手坐标系
同一组 16 个数字,按行主序读和按列主序读会得到两个不同的变换。本节讲清主序、手性与转置陷阱,说明 WebGPU / WGSL 的列主序约定与 CPU 到 GPU 的统一口径做法,给出 helio 可编译的列主序 Mat4,并用实测数字展示同一矩阵两种解释下的结果差异。
posts
这里是全站文章的归档入口,下面是按时间倒序排列的全部文章。若想按主题浏览,请从首页的专题分类进入。
同一组 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。
体积云与大气散射是实时渲染中最具「物理感」的效果之一。本文从体积渲染方程出发,讲解 Raymarching 积分、Beer-Lambert 吸收定律、Henyey-Greenstein 与 Mie 相位函数、Precomputed Atmospheric Scattering 的 LUT 方案、Worley/Perlin 噪声构造云形,以及降采样、时间重投影、蓝噪声抖动等性能优化手段,给出可落地的工程实现路径。
凭直觉优化性能几乎总是错的。本节把「先测量、再优化」落成可执行的流程:用 --cpu-prof 与 --prof 采集 CPU 剖析、用自顶向下与自底向上两种视角读火焰图、用堆快照定位内存泄漏,并解释采样开销、JIT 预热、异步栈与 source map 四个让剖析失真的陷阱。读完你能独立定位热点函数并验证优化是否真的有效。
类型擦除不是一句「类型都没了」就能带过:本节逐条对照 .ts 与编译产物,看清哪些语法被彻底删除、哪些留下了运行时代码。重点讲 enum、namespace、参数属性、装饰器与类字段的真实编译结果,以及为什么 private 与 #private 在运行时完全不同。读完你能预判任意一段 TS 会编译成什么,并知道何时必须补运行时校验。
本节把 TypeScript 的类型声明放到一边,转而看真正执行代码的那台机器:V8 如何用 Ignition 采集类型反馈、用隐藏类与内联缓存加速属性访问,再由 TurboFan 做推测式优化并在假设破裂时去优化。读完你能读懂 --trace-deopt 日志,知道哪些写法会让函数永远留在解释器里,并写出对 JIT 友好的代码。
本节讨论契约改动之后的事:先给出破坏性变更的判定清单,把「加字段安全、改语义危险」这类直觉变成可检查的规则;再对比 URL、Header、内容协商三种版本化策略的代价;然后用 expand-contract 三步法演示字段改名如何做到不打断旧客户端;最后讲弃用头、下线指标与消费者驱动契约测试的落地方式。
本节讲清「schema 先行」这条契约路线的完整闭环:如何从 TypeScript 服务端实现自动导出 OpenAPI 文档,如何用 openapi-typescript 与 orval 把文档变成带类型的客户端 SDK,以及 GraphQL 如何借助 codegen 生成 TypedDocumentNode,最后给出三条路线的选型判据与 CI 门禁。