CSM级联阴影
需要预先学习: 阴影 ShadowMap与PCF、PCSS CSM 级联阴影 ShadowMap与PCF、PCSS 中实现了单张阴影贴图:一张正交投影的深度图罩住整个场景。它有一个根本性的问题——分辨率分配与视觉需求不匹配。CSM(Cascaded Shadow Maps,级联阴影贴图)把视锥按深度切成多段,每段单独渲染一张阴影贴图,让近处获得高分辨率、远处用低分辨率,是实时渲染中大范围场景阴影的标准方案。 分辨率困境 透视投影下,屏幕空间里"近大远小":近处的物体占满屏幕的大片像素,远处的物体挤在屏幕的一角。而阴影贴图是均匀的——正交投影把整个场景等分进固定数量的纹素。于是: 近处:屏幕上一大片像素对应的场景区域,在阴影贴图里只占几个纹素,阴影边缘糊成一片块状锯齿; 远处:阴影贴图的大半纹素被远方的场景占用,但它们在屏幕上只占几行像素,分辨率被浪费。 提高贴图分辨率只能缓解,不能解决:要让近处阴影清晰,需要的分辨率高到远处完全用不完;照顾远处,近处又回到锯齿。正确思路是按需分配——把阴影贴图拆成多张,近处的贴图只覆盖视锥近...
引用与指针的区别
需要预先学习: 指针 指针常量与常量指针 引用与左右值 语法层面 指针的值是地址,需要通过解引用操作符*来访问指针所指向的对象,而引用是一个已存在变量的别名,可以直接使用引用名来访问变量。 int num = 10; int* ptr = # int& ref = num; std::cout << *ptr << std::endl; std::cout << ref << std::endl; 资源持有层面 引用天然地不支持更换对象,而指针需要在*后加const修饰才能实现类似语义: int& ref = num; // ref 永远绑定 num ref = num2; // 这是把 num2 的值赋给 num,而不是让 ref 绑定 num2 // 相当于: int* const ptr = # // ptr 永远指向 num 内存层面 引用在一些实现中不占用额外的内存空间,而指针是一个变量,会占用一定的...
GOC 架构
需要预先学习: 类与对象 多态 模版基础 智能指针与RAII GOC 架构 用继承树表达游戏对象,很快会撞上功能组合的墙——ECS 架构 开头详细描述了这个困境:继承表达 is-a(是一个)关系,而游戏实体需要 has-a(有一个)关系,一辆车有发动机、有轮子,而不是"是一类特殊的可驾驶物"。 对这条原则最直接的落地是 GOC 架构(GameObject-Component,游戏对象-组件):GameObject 是一个轻量的容器,本身只有身份与层级信息;所有功能——变换、网格渲染、刚体、光源、音源——做成一个个 Component,按需挂到 GameObject 上。想要一辆会飞的车?给同一个对象把 MeshRenderer、RigidBody、FlyingControl 都挂上即可,不需要任何新类型。 这套思路是商业引擎的主流选择:Unity 的 GameObject + MonoBehaviour、Unreal 的 Actor + Component、Godot 的 Node,内核都是 GOC。本篇以 ID ...
全局光照与局部光照
需要预先学习: 着色与渲染管线 光线追踪 渲染方程与重要性采样 局部光照(Local Illumination) 光照模型(illumination model)描述表面颜色如何由光源与材质决定。着色与渲染管线 里的着色是局部的:每个着色点独立计算颜色,只考虑光源与表面本身的属性(法线、材质系数),完全不考虑场景中其他物体的存在。 Blinn-Phong 模型是典型的局部光照模型(local illumination model),它把表面颜色拆成环境光、漫反射、高光三项之和。局部光照的"局部"体现在两点: 逐点独立:每个点的着色只依赖该点的法线、位置与光源,与其他点互不相干; 无视遮挡:着色点与光源之间有什么东西、有没有东西挡着,一概不知。 这两点决定了局部光照的固有局限。 局部光照的局限 因为每个点只知道自己和光源,局部光照算不出一系列依赖"场景其他部分"的效果: 效果 为什么局部光照做不到 补救手段 阴影 不知道光源与点之间是否有物体遮挡 ShadowMapping([[Sha...
垃圾回收
需要预先了解: 内存分配与逃逸分析 垃圾回收 Go 的垃圾回收器(GC,Garbage Collector)负责回收不再被引用的堆对象。它的设计目标很明确:把停顿时间压到亚毫秒级,让服务端程序不必为 GC 停顿做特殊设计。这一篇讲清楚三色标记、写屏障、触发时机和调优手段。 为什么需要 GC 堆上的内存生命周期无法在编译期确定,只能运行时判断。手动管理会带来两类问题: 忘记释放:内存越用越多,最终耗尽; 释放过早:对象还在被引用就被回收,后续访问到已失效的内存。 垃圾回收器把"什么时候释放"这件事交给运行时:它周期性地从根对象(栈上的变量、全局变量、寄存器)出发,找出所有还能访问到的对象,其余的就是垃圾。 GC 演进时间线 Go 的 GC 从"全程暂停"演进到"几乎不停顿",每一步都是被上一步的问题推动的: 阶段 做法 问题 STW 标记清扫 暂停所有 goroutine,标记垃圾并清扫 停顿与堆大小成正比,堆大了没法用 并发标记清扫 标记阶段与用户代码并发执行 并发时对象引用会变化...
法线贴图与TBN矩阵
需要预先学习: 纹理 纹理映射 Phong模型与Blinn-Phong模型 凹凸感的来源 Phong模型与Blinn-Phong模型 的光照计算只依赖一个输入描述表面形状:法线(normal)。一个表面看起来是平的还是坑坑洼洼的,本质上取决于法线在表面上如何变化——砖墙缝隙处法线突然翻转,凹陷处法线朝向内倾,人眼把这些明暗变化解读为"凹凸"。 要让一面砖墙真的有凹凸,最直接的办法是建出缝隙的几何:每个砖块、每条沟壑都用三角形描述。代价是三角形数量爆炸——一面墙几十万三角形,剔除总结 与 前向渲染与延迟渲染 里讨论的所有开销跟着翻倍。而仔细想想,这些三角形只是提供了"法线在像素间变化"这一件事。 法线贴图(normal mapping)的思路:几何保持平滑(低多边形),把逐像素的法线存进一张纹理,着色时采样它代替插值法线。凹凸感来自法线变化,而不是几何变化——一张图就能骗过光照计算。 方案 三角形数量 细节层级 代价 几何建模凹凸 极多 顶点级 顶点处理、带宽、内存 顶点...
DrawCall 开销与优化
需要预先学习: RenderPass与RenderGraph 缓冲对象与顶点 DrawCall 是什么 Draw Call(绘制调用)是 CPU 向 GPU 提交的一次绘制命令——在 OpenGL 里就是 glDrawElements、glDrawArrays 这类调用。一次 draw call 绘制一个网格(或一批相同状态的物体),RenderPass与RenderGraph 里一个 Pass 内的每次 draw_batch 提交,对应一次 draw call。 一帧画面通常由成百上千次 draw call 组成:场景里每个物体一次、阴影贴图渲染一次、后处理若干次。现代游戏场景中物体数量动辄上万,draw call 数量是渲染性能的关键指标之一。 一次 draw call 的旅程 调用 glDrawElements 之后发生了什么: CPU 把渲染状态(绑定的 shader、纹理、缓冲、绘制参数)打包进一条命令; 命令写入命令缓冲区(command buffer),GPU 异步读取执行; GPU 按命令切换状态、发起顶点处理,执行绘制。 关键...
ECS 架构
需要预先学习: 类与对象 多态 模版基础 游戏对象的表示:继承的困境 写游戏时第一个遇到的问题往往不是渲染,而是"一个游戏里的东西怎么用代码表示"。最自然的想法来自面向对象:万物皆对象,公共的东西放进基类,特化的东西放进派生类: class GameObject { /* 位置、名字、Update() 虚函数 */ }; class Character : public GameObject { /* 生命值、移动 */ }; class Player : public Character { /* 输入控制 */ }; class Enemy : public Character { /* AI */ }; class Prop : public GameObject { /* 可交互物体 */ }; 游戏小的时候这套结构工作得很好。但游戏的本质是功能的自由组合:策划想要一辆会游泳的飞行汽车、一只会拾取道具的鸟、一个能被玩家附身的...
渲染方程与重要性采样
需要预先学习: 全局光照与局部光照 光线追踪 渲染方程 从局部到全局的数学表达 全局光照与局部光照 把光照分为局部与全局两个层次。渲染方程(rendering equation)把"全局"写成了数学形式:一个表面点向观察方向反射的总亮度,等于它自身发出的光,加上从法线半球所有方向射来的光经 BRDF 反射后的总和: Lo(p,ωo)=Le(p,ωo)+∫Ωfr(p,ωi,ωo) Li(p,ωi) (n⋅ωi) dωiL_o(p, \omega_o) = L_e(p, \omega_o) + \int_{\Omega} f_r(p, \omega_i, \omega_o)\, L_i(p, \omega_i)\, (\mathbf{n} \cdot \omega_i)\, d\omega_i Lo(p,ωo)=Le(p,ωo)+∫Ωfr(p,ωi,ωo)Li(p,ωi)(n⋅ωi)dωi 方程中各项: 符号 含义 Lo(p,ωo)L_o(p, \omega_o)Lo(p,ωo) 点 ppp...
调度器:GMP 模型深入
需要预先了解: goroutine 与并发初探 channel 调度器:GMP 模型深入 goroutine 与并发初探 里给出了 GMP 的概览。这一篇展开内部结构:G 里存了什么、调度器怎么选下一个 G、work stealing 和 hand-off 如何工作、抢占是怎么实现的,以及为什么网络 IO 不会占住线程。 G、M、P 的职责 G G G G G = goroutine(任务:栈 + 状态 + 调度信息) | | | | v v v v +---------------+ P = 逻辑处理器(本地队列 + 资源配额) | P runq | +---------------+ | v +---------------+ M = 内核线程(真正执行指令) | M | +---------------+ 结构 内容 G 栈指...





