
1. 为什么我说引擎架构是所有游戏技术的基石我入行那会儿赶上公司自研引擎还在中期阶段Leader扔给我一份引擎核心模块的代码让我先读着玩玩。我对着那一堆抽象类、回调注册、消息分发看了整整一周脑子里的感觉就是每个文件单独拎出来都认识合在一起完全不知道它们是怎么协作的。后来带我的老大哥跟我说了一句让我记到现在的话——你不需要背住每一个类你需要先弄清楚一件事一个游戏帧从输入到画面到底在引擎里走了哪条路。这句话点醒了我。游戏引擎架构说到底就是回答这么几个问题游戏世界里成千上万的物件由谁管理每帧每秒钟上百万次的逻辑判断和渲染调用如何被组织起来内存、时间、资源这些稀缺东西由谁分配、由谁回收所有看似炫酷的画面效果、物理交互、AI行为底层都是这一套基础架构在撑着。所以这个系列我把第一篇文章留给基础架构是有原因的。不管你是想读Unity、Unreal源码还是打算自己动手写一个小型引擎或者只是想搞清楚为什么我们项目总是卡在加载界面你都得从这一层开始。这篇文章我会把自己在这几年里读代码、写引擎、调性能踩过的坑和经验一并整理出来尽量用大白话把底层的逻辑讲清楚。我这里说的引擎基础架构不是指某一个开源引擎的代码目录而是指游戏引擎作为一个软件系统通用的骨架。它通常由这样几块构成底层工具库、核心运行时模块渲染、物理、动画、音频、AI、网络、游戏世界组织层场景、实体、组件、以及最顶层的应用层。下面我会一步步拆开讲。2. 引擎的分层设计为什么所有引擎长得都差不多如果你把Unity、Unreal、Godot、CryEngine放在一起对比会发现它们的架构图出奇地相似。这不是巧合而是游戏引擎要解决的问题决定了它的分层方式。就像所有建筑都需要地基、承重墙和屋顶一样引擎的分层是被需求逼出来的。2.1 平台抽象层引擎跨平台的起点引擎最底下一层不是渲染也不是物理而是平台抽象层。它负责屏蔽操作系统、编译器、硬件平台的差异。你看Unity能一键导出Windows、macOS、Linux、iOS、Android、WebGL等多种平台靠的就是这一层。平台抽象层具体要管什么至少包括这几类窗口创建与事件循环你要在Windows上创建窗口在Android上是Activity在浏览器里是Canvas这部分差异极大。图形API封装DirectX、Metal、Vulkan、OpenGL不同API的初始化、渲染管线和资源创建方式完全不同。输入设备抽象键盘鼠标、手柄、触摸屏、VR控制器引擎需要把它们统一成一套输入事件系统。文件系统抽象桌面平台直接用路径读文件移动平台需要经过沙盒有些平台资源还要先解压或加密。内存分配和线程接口C标准的new/delete在移动平台上不一定可靠引擎通常自己封装内存池这层也要处理平台差异。我自己写引擎的时候最头疼的就是图形API封装。为了兼容Vulkan和OpenGL我发现最好的办法是定义一套引擎自己的渲染API接口然后为每个后端写实现。这就是典型的接口隔离原则——上层模块只知道创建顶点缓冲这个操作不关心底层是Vulkan还是Metal。2.2 基础工具库被忽视却最重要的底盘很多人一讲到引擎架构就只盯着渲染和物理忽略了底层工具库。但说实话我在实际开发中调试时间花得最多的往往就是这些不起眼的部分。基础工具库包括数学库向量、矩阵、四元数、包围盒、射线。游戏引擎的数学库几乎都是自研的因为通用数学库比如Eigen性能不够、内存布局不适合SIMD优化而且游戏开发最常用的是浮点运算精度要求特殊。容器与数据结构引擎内部很少直接用标准库容器。原因很现实标准库的std::vector在频繁增删下会不断分配和释放内存造成碎片化而游戏引擎常常需要在极短时间内创建和销毁大量对象子弹、特效必须用自定义的内存池容器。字符串与哈希游戏里查找资源、给对象定位如果每次用字符串比较效率太低。引擎通常会把字符串哈希成ID用整数来做快速查找。内存分配器这是引擎架构里的隐藏主角。后面我会专门用一节来讲。这里我必须说一个反常识的观点引擎架构的好坏很大程度由它的内存管理和工具库决定而不是由渲染多炫酷决定。渲染效果差可以后期调内存管理差会整个系统陷入卡顿和崩溃的泥潭基本没法救。2.3 核心运行时模块各司其职的部门基础工具库之上就是引擎的核心运行时模块。我习惯把它们理解成公司里的各个部门模块职责典型数据流渲染模块将场景几何体、材质、灯光转换为屏幕像素场景数据 - 剔除 - 排序 - 提交DrawCall - GPU执行物理模块模拟刚体运动、碰撞检测、关节约束物理世界 - 步进计算 - 碰撞回调 - 游戏逻辑处理动画模块驱动骨骼动画、状态机、IK动画状态 - 采样骨骼姿势 - 提交给渲染音频模块声音播放、3D定位、混音事件触发 - 播放实例 - DSP处理 - 输出设备AI模块寻路、行为树、感知世界状态 - 决策 - 输出行为指令网络模块同步状态、RPC、消息序列化本地输入 - 序列化 - 网络发送 - 远端应用这些模块之间不是完全独立的它们之间有大量协作。比如物理模块检测到角色踩到地面要通知动画模块播放对应的动画AI模块决定敌人转向要告诉渲染模块更新模型朝向网络模块收到远端玩家位置要写到场景状态中让物理和渲染都能读到。模块协作的关键是数据共享而不是函数调用。我刚写引擎时总喜欢让模块之间直接互相调用比如物理模块直接调用渲染模块的某个函数。后来发现这样耦合太严重——调试物理问题牵扯渲染代码改渲染还要担心物理那边受影响。好的做法是模块之间通过共享的场景状态位置、速度、标记来解耦谁需要数据就主动去读而不是靠函数调用链。3. 游戏世界的组织方式实体、组件与场景图引擎模块是能力提供方但真正承载游戏内容的是游戏世界这一层。这一层怎么设计直接决定了你写玩法时的体验和效率。3.1 从继承万物到组合优先早期引擎也包括很多新手自研引擎喜欢用深度继承树组织游戏对象。比如Actor是基类Character继承自ActorPlayer继承自CharacterEnemy又继承自Character。表面看很合理但实际项目里你会遇到一种典型悲剧玩家角色需要有可被物理推动的能力而宠物小怪也有这个需求。于是你发现这个能力到底该放在基类还是子类怎么放都别扭。这就是所谓的继承树困境。现代引擎普遍采用**实体组件系统ECS或近似变体**来解决这个问题。基本思路是游戏对象实体本身只是一个ID所有能力都由挂在它身上的组件提供。玩家有PlayerController组件、CharacterMotor组件、Health组件宠物有AIBehavior组件、CharacterMotor组件、Health组件。撞车能力通过共享组件复用了但实体之间完全解耦。以Unity为例GameObject上挂Component的做法就是组件的经典实践。Unreal虽然保留了Actor继承体系但近年也在大量使用Component来组合能力。国内很多自研引擎直接走纯ECS路线比如使用开源框架EnTT或者干脆自己实现一套。3.2 场景图谁是谁的子物体场景图Scene Graph解决的是层级变换关系问题。举个例子一个角色手里拿了一把剑剑的位置必须跟着手掌骨骼走。如果引擎只能管理绝对坐标你要么每帧手动同步剑的位置要么就得建立父子关系——剑是手的子节点手移动时剑自动跟随。场景图本质上就是一棵树父节点的变换会传递给所有子节点。它极大地简化了物体跟随的逻辑。但场景图也有性能陷阱——每帧更新整个树会产生大量矩阵乘法和数据遍历。引擎通常采用脏标记机制只有变换发生变化的节点才重新计算并只向下传播到受影响的子树。我实际经验是场景图层级不要超过10层越深性能越差。美术同事很喜欢把所有东西都在编辑器里层层嵌套挂父子关系结果调试时发现一个位置异常要一层层找。后来我写了编辑器检查规则嵌套超过8层直接报警开发效率反而高了。3.3 组件之间怎么通信有了实体和组件还缺一个关键问题不同组件如何得知其他组件的变化比如血条UI组件要监听玩家受伤事件。有三种常见方案直接引用组件持有另一个组件的指针直接调用。最快但耦合严重。适合关系稳定的场景角色移动组件与动画组件经常这样。事件总线全局事件中心组件发布事件其他组件订阅。解耦好但事件多了难调试而且全局事件容易产生幽灵订阅组件销毁忘了取消订阅导致内存泄漏和莫名其妙的行为。共享数据状态两个组件同时读写某个共享数据结构比如Transform组件的世界坐标靠数据变化驱动逻辑。这是ECS架构的核心思路性能最好但思维转换成本高。我个人的建议小项目用直接引用少量事件总线的组合最省心。大型项目如果团队对ECS理解到位可以考虑全面数据驱动否则强行上ECS反而会让团队效率大幅下降。4. 帧循环与游戏时间驱动一切的心脏所有游戏引擎的核心都有一个无限循环在运转处理输入、更新逻辑、渲染画面、等待/计时、回到起点。这个循环就是引擎的心跳。4.1 固定步长与可变步长帧循环里最容易犯的错误就是让游戏逻辑直接依赖帧率。如果你写角色移动速度 每帧移动10像素那么60帧每秒时角色一秒移动600像素30帧每秒时一秒只有300像素。性能不稳定的机器上角色忽快忽慢。正确的做法是引入时间步长DeltaTime每帧计算实际经过的时间所有逻辑用速度 x DeltaTime来计算位移。这样无论帧率是30还是60角色每秒移动的距离都一致。这里有两个流派可变步长每帧用真实DeltaTime驱动逻辑。实现简单但物理模拟容易不稳定因为碰撞检测在步长过大时可能漏掉穿透。固定步长逻辑层固定使用1/60秒步进或1/30每帧可能执行0次、1次、多次逻辑更新然后插值渲染。物理和网络同步场景必须用固定步长否则会出现两台机器模拟结果不一致的严重bug。我在做网络同步联机逻辑时被可变步长坑到怀疑人生同一局游戏我本机测试敌人AI永远正常跟同事联机后敌人AI飘忽不定。后来才明白是两台机器帧率不同导致AI更新节奏不同。换成固定步长渲染插值之后问题立刻消失。4.2 渲染插值固定逻辑步长下的流畅感补丁固定步长解决了模拟稳定性但也会带来一个问题逻辑更新频率固定60Hz而屏幕刷新率是144Hz渲染拿到的物体位置可能每隔16.6ms才更新一次显示出来会有微小的卡顿感。标准解法是渲染插值保存物体上一帧逻辑位置和当前逻辑位置渲染时根据实际渲染时刻在这两个位置之间做线性插值把时间上的离散变成空间上的连续。这个概念其实不复杂效果却非常明显。我在项目里做完插值优化后玩家反馈画面明显顺滑了而帧率数字压根没变。插值需要注意插值只能用于视觉表现不能回写逻辑状态。否则会污染物理和网络同步数据造成判断错乱。4.3 主循环里各阶段的顺序为什么重要一个典型的引擎主循环大概是这个顺序1. 处理输入事件 2. 更新游戏世界逻辑、AI、物理 3. 处理网络收发 4. 渲染准备剔除、排序、生成渲染命令 5. 提交GPU执行 6. 计算本帧耗时休眠到目标帧时长这个顺序不是随便定的。输入必须最先处理因为它是所有逻辑的起点物理更新必须在逻辑更新之前或内部因为物理反馈比如碰撞事件往往会影响逻辑决策渲染放到最后因为它读取的是所有逻辑更新完成后的最终状态。如果渲染和逻辑并行运行多线程引擎就需要帧同步机制。一个经典做法是双缓冲逻辑线程写本帧数据渲染线程读上一帧数据两者通过同步点交换。这样可以大幅提升性能但也会引入至少一帧的输入延迟——FPS玩家能感受到。所以竞技类游戏往往牺牲一些画质换更低的输入延迟就是这个道理。5. 内存管理引擎架构里最容易翻车的隐形战场我见过太多人写游戏代码时完全不关心内存上来就是new一个对象、delete一个对象。这在工具软件里可能没问题在游戏引擎里就是灾难。原因有三每帧可能创建成千上万个临时对象堆分配和垃圾回收的耗时抖动会造成帧率波动内存碎片化会让手机系统直接杀进程。5.1 为什么引擎很少用裸new和delete这里要理解一个背景操作系统的堆分配malloc/new虽然功能完备但速度不稳定最坏情况需要几百个时钟周期。游戏引擎每帧要分配的角色、特效、子弹、UI控件动辄几千上万个对象如果用通用堆分配光分配和释放就能吃掉几毫秒再加上碎片化导致的性能衰减游戏就没法玩了。引擎的解决方案通常是内存池事先向系统申请一块大内存然后程序内部自己管理这块内存的分配与回收。分配时只需要从池里取一块固定大小的内存速度极快且无碎片。我最早写的那个像素游戏开局加载完毕只占用了系统分配的一小块内存后面运行期间完全不向操作系统申请新内存。所有子弹、敌人、特效全部在预分配池中周转。这样帧率曲线是一条直线没有一丝抖动。5.2 对象池之外栈式分配与双缓冲分配器内存池适合固定大小对象的复用但有时我们需要更灵活的方案栈式分配器按顺序分配按顺序释放类似函数的调用栈。适合生命周期嵌套清晰的对象比如场景内一次型临时数据。它的性能极高——分配只是移动栈顶指针。双缓冲分配器维护两个栈式分配器一帧使用A下一帧使用B隔帧交替。适合每帧创建、下一帧必须清理的临时数据。渲染模块的临时命令缓冲区就适合这种。块分配器大块内存再切分成不同大小的小块按2的幂次分桶。每个对象分配和释放都精确落在自己的桶里几乎无碎片。内存管理原则有一条铁律分配器应该分层使用不要全局统一一套。比如每帧临时对象用栈式分配器持久对象用对象池UI对象用块分配器。我见过某个项目把所有分配都塞进一个通用池结果UI面板开合频繁的内存碎片把物理模块拖慢定位了大半天。5.3 缓存友好性内存布局决定了性能上限现代CPU的运算速度远超内存读取速度CPU内核靠Cache缓解这种差距。如果你频繁访问的内存地址很分散Cache命中率就会暴跌CPU被迫等待从内存拉数据这就是所谓的Cache Miss。游戏引擎里很多迷之卡顿根因就在这里。经典案例组件遍历。如果你用面向对象的方式存储所有物体及其组件每个组件分散在不同内存地址遍历全部物体时CPU要跳来跳去读内存。但如果用ECS的思路——把同一类组件数据连续存放比如所有速度存在一个数组里遍历时CPU按顺序读取Cache命中率极高速度可能快出一个数量级。我做过多线程性能优化项目把怪物AI决策从遍历所有怪物对象改成遍历怪物组件数组后同样数量的怪物决策耗时下降了65%。这就是数据布局的威力。所以说ECS的流行不只是因为架构清爽背后还有内存性能的硬核支撑。6. 从单线程到多线程现代引擎的并行化演进早期的游戏引擎是单线程的——所有模块在一个线程中依次执行。逻辑简单调试方便。但现代游戏动辄几十万物体、复杂光照和物理模拟单线程性能远远不够。于是引擎架构开始向并行化演进这个过程的每一步都踩了不少坑。6.1 并行化从分线程开始专业模块各自为战第一阶段的并行化是模块级分线程。比如音频开一个线程播放和混音网络开一个线程收发数据物理计算也分配到单独线程。主线程只负责整体调度和渲染提交。这个阶段相对容易实现因为模块之间通过队列传递数据天然解耦。我把音频和网络拆成独立线程后主线程的压力立即下降。不过这种方案的瓶颈也很明显模块内部仍然依赖主线程返回结果。比如物理计算如果耗时10ms逻辑线程这10ms就得干等。这时候需要**Job系统任务调度系统**来解决更细粒度的并行。6.2 Job系统把工作切碎让所有核心忙起来Job系统的核心思想是把原本串行的计算任务分解成很多小任务Job每个Job没有互相依赖时才可并行。引擎通过一个任务调度器把这些Job分配到各个线程执行。举个例子渲染模块的遮挡剔除以前是一整块代码在主线程算。拆成Job后场景按空间划分为多个区域每个区域单独一个Job做剔除N个核心可以同时处理N个区域性能接近线性提升。但要小心任务依赖Job之间可能形成依赖链比如先做完物理结算才能生成渲染命令。调度器需要处理这种依赖关系避免死锁。而且Job内不能访问主线程的数据结构通信都要走线程安全队列。这些限制让Job系统的调试难度直线上升。我调试过一个诡异的crash两个线程同时访问同一个实体的变换数据一个读一个写切到Release模式后问题随机出现排查了两天才靠TSan线程检测工具定位到。从那以后我再也不在Job间直接共享裸指针一律走数据拷贝或原子操作。6.3 ECS与并行化的结合为什么ECS适合多线程前文提到了ECS的内存布局优势其实它在并行方面还有天然优势因为组件数据是独立的、连续存储的系统System迭代组件时不会产生数据竞争。只要确保两个系统不同时写同一个组件类型就可以放心并行。这就是为什么现在很多新引擎框架都在推ECS比如Unity的DOTS、Bevy的多线程调度。ECS配合Job系统可以让一个8核CPU同时干8份活而不用担心数据冲突。但代价是你必须抛弃万物皆对象的直觉写代码方式学习用数据处理逻辑思考。如果你不想完全进入ECS混合架构也是一条路核心高频逻辑用ECS游戏玩法的业务逻辑用传统组件方式。我当前的项目就是这个路线效果很稳。7. 资源的生命周期从磁盘到GPU的漫长旅程一个模型要能在屏幕上显示中间要经历的步骤远比大多数人想象的多。资源管理的设计不合理会导致游戏加载慢、卡顿、内存暴涨。这也是引擎基础架构里的重头戏。7.1 资源加载的完整链路以一个FBX模型文件为例读取磁盘文件IO线程读取文件二进制内容。反序列化解析解析FBX格式提取网格顶点、UV、法线、材质引用关系。转换为引擎内部格式把引擎不认识的格式转换为基础数据比如生成GPU友好的顶点缓冲布局。上传GPU把顶点数据拷贝到显存Vulkan/OGL/D3D各有不同。材质关联加载模型引用的贴图资源生成材质实例。注册到场景创建渲染实体挂载Mesh和Material。这6步每一步都可能有性能瓶颈。很多游戏的卡加载本质上是第2步的解析和第4步的上传大量串行执行导致的。7.2 同步加载 vs 异步加载最朴素的加载方式是同步加载需要资源时就读取文件、解析、创建全部完成才继续往下走。好处是代码简单坏处是加载期间整个游戏冻结。玩家最能直观感受到的就是过场动画里那个转圈图标。现代引擎全部采用异步加载加载请求提交给后台线程后台解析资源和创建数据完成后通知主线程资源就绪了游戏继续运行不受影响。开放世界的无缝加载就是靠这一套才可能实现。异步加载有几个关键细节引用计数一个贴图可能被多个模型引用加载状态要记录引用数没人引用时自动卸载。加载优先级玩家视野内的资源优先加载远处的可以延后。比如角色切换皮肤时新皮肤资源必须在若干毫秒内到位否则会出现模型变成白模的尴尬。流式加载Streaming超大世界不可能一次全部加载需要在运行时按玩家位置动态加载和卸载资源块。Unity的Addressables和Unreal的World Partition都是干这个的。我在一个开放世界项目里被流式加载折磨了一个季度玩家开摩托艇高速移动时地块的加载速度跟不上远处地形偶尔会露底。后来优化了资源分块大小从每块256米改为512米、加了Tile级LOD和预加载半径才完全解决。7.3 资源包与热更新引擎架构里的最后一公里资源加载的后半段还涉及资源打包和分发。引擎通常不会直接把几百MB的散文件丢给玩家而是打包成压缩包。手游的AB包AssetBundle和PC游戏的Pak包都是这种方案。热更新机制实际上就是让引擎可以按需从服务器下载新资源包并替换本地文件。这涉及版本管理、加密校验、断点续传、资源差异比对。基础架构设计得好热更新实现起来顺畅如果设计时没考虑资源分包和版本号后期加热更新会非常痛苦。我见过的反面教材某个项目早期没做资源版本管理上线后想修一个贴图运营要求出热更包开发只能全量打包几百MB的资源包更新体验很差。后续强制在引擎基础层加了资源Hash校验和分块差分下载才把更新包控制在几十KB级别。8. 引擎可扩展性为什么你需要一套插件友好的架构游戏引擎不仅是给游戏用的多数大厂引擎还承载着编辑器工具、自定义美术管线、AI离线工具链等功能。一个可扩展性差的引擎会在项目中期成为开发效率的绞刑架。8.1 业务逻辑与引擎核心的边界谈到可扩展性最关键的分界线是引擎核心代码和游戏业务代码必须分层隔离。渲染、物理、动画这些属于引擎核心通常你写完就不想再动了而技能系统、任务系统、背包系统属于业务逻辑每周都在变。如果业务代码跟引擎核心纠缠在一起改一个技能特效可能触发渲染模块的重新编译这种痛苦我深有体会。好的架构会提供一套扩展接口比如引擎定义好如何注册一个自定义组件类型如何在渲染流程中插入自定义Pass如何监听事件总线。业务部门写代码时只跟接口打交道不碰核心实现。Unreal的GameplayAbilitySystem和Unity的ScriptableObject都是在这个方向上做得比较出色的案例。国内自研引擎也有不少用C模板和注册表做扩展框架。8.2 代码生成与数据驱动扩展性的另一种形态纯手写扩展接口还是不够优雅因为业务类型一多注册代码会变成一大堆重复的样板代码。于是现代引擎普遍引入代码生成你在配置文件或标记中声明这是一个技能组件有3个属性引擎工具自动生成注册代码和数据表结构。数据驱动最有价值的场景在技能和数值系统。我在一个ARPG项目里所有技能效果都是用数据表配置的——伤害、范围、冷却、动画、音效、Buff效果、触发条件全部在表格里。策划改技能不做代码开发只看表格和编辑器蓝图。引擎基础架构只要提供一个读取配置 - 构造成对象 - 挂到实体组件上的通用机制就够了。代码生成和反射机制密切相关。反射是指程序在运行时能读取自身类型信息的能力。C原生没有反射引擎通常自己做一套类型注册系统或干脆用脚本语言如Lua/Python/C#做业务层。这一层设计得好编辑器的Inspector面板能自动展示任何类型的所有属性程序员和策划都会感激你。8.3 模块化的代价别把引擎变成过度工程我见过一些团队引擎基础架构写得很完美抽象了几百个接口每个模块之间完全隔离。结果呢新人上手要三个月改一个小功能要同时改五层抽象。这里要泼一盆冷水模块化和过度工程之间只有一线之隔。引擎架构的设计应当以团队对问题域的理解深度为基准。如果你团队只有5个人做一个小游戏定制化自研引擎的架构远不如直接用成熟引擎少量中间件来得划算。就算要做自研起步阶段也不用追求极致的抽象先让游戏能跑起来再在重构中逐步引入模块边界。我在自己写引擎时遵循一个原则每个模块先写出最小可用版本只有当两个模块之间真的出现重复逻辑或强耦合时才做抽象提取。这样既保持了架构演进的能力又不会一开始就把自己锁死在过度设计里。9. 给想学引擎架构的人一条我踩出来的路如果你看到这里说明你对游戏引擎的底层机制是真的感兴趣。最后这段我分享几条实操层面的建议都是我自己以及我带过的新人验证过有效的路径。第一步别急着读Unreal源码。Unreal太庞大直接读源码容易被海量宏定义和反射系统劝退。先从Unity这个黑盒使用的引擎入手用Profiler观察每一帧CPU和GPU分别花在哪就会对引擎循环里到底发生了什么建立感性认识。第二步带着问题去读一个小型开源引擎。我推荐两个Godot的源码结构清晰有很好的场景树和信号系统或者用手写的方式复刻一个简化版。这个阶段目标不是读完全部代码而是找到三个问题的答案主循环在哪物理线程怎么跟渲染同步的资源加载是怎么做到的第三步动手改/写一个小引擎。哪怕是实现一个只显示三角形旋转的微型引擎你也会被迫弄懂窗口创建、图形API初始化、顶点缓冲、Shader编译、游戏循环、输入处理这些全套细节。把这些串起来之后你对架构的理解会从一个抽象概念变成一个具身经验。我自己在写微型引擎时最大的收获其实不是掌握了某套API而是建立了一种用架构的眼光看问题的思维方式。做任何功能先问它属于哪一层它的数据在哪里被读和写它跟其他模块的耦合点在哪这个问题思考熟练了基本就不太会写出让后续维护者崩溃的代码了。这条路的终点不是把引擎架构背下来而是能在新项目里自主判断什么样的架构选择最适合当前的场景。我希望这个系列的第一篇已经为你架好了这张思考的地图。下一篇我会深入渲染架构聊聊从场景数据到屏幕像素之间那条更长的路我们下次继续。