1. 3D图形渲染的核心思路与整体设计
1.1 从“看起来像真的”到“算得足够快”:3D图形的本质问题
很多人第一次接触3D图形,脑子里想的都是“怎么把模型建得好看”。但真正做过一段时间的人会告诉你,3D图形最核心的矛盾从来不是“好不好看”,而是**“在有限算力下,如何让画面看起来足够可信”**。这个矛盾贯穿了整个3D图形管线,从顶点处理到光栅化,从纹理采样到后处理,每一个环节都在做取舍。
我拿一个最常见的场景举例:你要在屏幕上画一个旋转的立方体。听起来简单,但背后要做的事情包括——把三维坐标变换到二维屏幕空间、判断哪些面朝向摄像机、计算每个像素的颜色、处理遮挡关系。如果只是画一个立方体,CPU硬算也能跑,但一旦场景里有十万个三角形,事情就完全不一样了。这时候你就必须理解渲染管线的设计逻辑:为什么要有顶点着色器和片元着色器之分?为什么深度测试要放在片元着色器之后?这些不是随便定的,每一步都在解决特定的性能瓶颈。
3D图形的整体设计思路,说白了就是一条从数据到像素的流水线。输入是顶点数据、纹理、光照参数,输出是屏幕上的一帧画面。中间经过的每一步都有明确的职责划分,而且每一步都可以并行化。GPU之所以比CPU适合做图形,不是因为它单核多强,而是因为它有几千个核心可以同时处理几百万个顶点和像素。理解这一点,你才能明白为什么写Shader的时候要尽量避免分支、为什么要用纹理来存数据而不是用if-else。
1.2 渲染管线的选型逻辑:可编程与固定功能的边界在哪里
现代3D图形管线大致分为几个阶段:顶点着色、图元装配、光栅化、片元着色、逐片元操作。其中顶点着色和片元着色是可编程的,其他阶段基本是固定功能。这个划分不是随意的,而是经过几十年图形硬件演进沉淀下来的最优解。
为什么顶点着色和片元着色要可编程?因为这两个阶段的变化最多。顶点着色要处理骨骼动画、形变、粒子运动,片元着色要处理材质、光照、阴影、后处理效果。如果把这些都做成固定功能,那硬件就没法适应不同的渲染需求了。反过来,图元装配和光栅化为什么不做成可编程的?因为它们的逻辑非常固定——把三角形拆成像素,这个操作不需要灵活性,只需要快。硬件厂商把这些步骤做成专用电路,效率比通用计算高得多。
在实际项目里,选择什么样的渲染路径非常关键。如果你做的是移动端应用,那就要优先考虑前向渲染,因为移动GPU的带宽有限,延迟渲染的G-Buffer开销太大。如果你做的是PC端大型场景,那延迟渲染可能更合适,因为它能高效处理大量动态光源。我见过不少项目一开始没想清楚这个问题,做到一半发现帧率上不去,回头改管线,代价非常大。
还有一个容易被忽略的点:渲染管线的选择要和美术资产的生产流程匹配。比如你选了延迟渲染,那美术就不能用太多透明材质,因为延迟渲染对透明物体的处理很麻烦。这种约束如果在项目初期没有对齐,后期就会出现“技术说能做、美术说做不了”的扯皮局面。
1.3 坐标系与变换:为什么你的模型总是跑到屏幕外面
3D图形里最容易让人迷糊的就是坐标系变换。模型空间、世界空间、观察空间、裁剪空间、屏幕空间——五个坐标系来回转,稍不注意模型就飞到屏幕外面去了。我刚开始学的时候,经常遇到“明明顶点数据没问题,但画出来就是一片黑”的情况,后来才发现是投影矩阵的远裁剪面设错了。
这里我把整个变换链条捋一遍。假设你有一个立方体的顶点数据,坐标范围是-1到1,这是模型空间。然后你把它放到场景里的某个位置,比如平移(3, 0, 0),旋转45度,缩放2倍,这就变成了世界空间。接着你要用摄像机去看它,摄像机有位置和朝向,把世界空间的坐标转换到以摄像机为原点的观察空间。再然后,你要把观察空间里的点投影到一个标准立方体里,这就是裁剪空间,超出这个立方体的部分会被裁掉。最后,裁剪空间的坐标经过视口变换,映射到屏幕上的像素坐标,这就是屏幕空间。
每一步都有对应的矩阵:模型矩阵、视图矩阵、投影矩阵。通常我们会把这三个矩阵乘在一起,得到一个MVP矩阵,在顶点着色器里一次性完成变换。这样做的好处是减少矩阵乘法次数,因为顶点数量可能非常多,每个顶点少做两次矩阵乘法,整体性能提升就很可观。
注意:投影矩阵的near和far不要随便设。near太小会导致深度精度不够,出现Z-fighting;far太大同样会降低深度精度。一般建议near设为0.1到1之间,far根据场景大小来定,不要动辄设成10000。
2. 核心细节解析与实操要点
2.1 顶点数据布局:为什么你的模型加载出来是乱码
顶点数据是3D图形的原材料。一个顶点通常包含位置、法线、纹理坐标、切线、骨骼权重等信息。这些数据在内存里怎么排列,直接影响到GPU的读取效率。
最常见的两种布局是交错布局和分离布局。交错布局是把一个顶点的所有属性放在一起,比如位置(3个float)、法线(3个float)、纹理坐标(2个float),总共8个float,32个字节。下一个顶点紧接着放在后面。分离布局是把所有顶点的位置放在一块,所有顶点的法线放在另一块,以此类推。
从GPU缓存的角度看,交错布局通常更好,因为顶点着色器读取一个顶点时,所有属性都在相邻的内存地址上,缓存命中率高。分离布局在某些情况下也有优势,比如你只需要更新位置数据而不动其他属性时,分离布局更方便。但大多数引擎默认用交错布局,因为综合性能更好。
我在实际项目里踩过一个坑:从建模软件导出的FBX模型,顶点数据里包含了大量冗余信息,比如每个顶点都存了切线,但实际上很多模型根本用不到法线贴图。这些冗余数据不仅占内存,还会拖慢顶点着色器的读取速度。后来我写了一个预处理脚本,根据材质需求裁剪顶点属性,模型加载时间直接少了三分之一。
还有一个常见问题是顶点顺序。OpenGL默认是逆时针为正面,DirectX默认是顺时针为正面。如果你从OpenGL迁移到DirectX,或者用了不同引擎的模型,顶点顺序不对就会导致背面剔除出错,模型看起来像透明的一样。解决办法要么在导出时统一顶点顺序,要么在渲染时关闭背面剔除(但这样性能会差)。
2.2 纹理与采样:为什么你的贴图总是糊成一团
纹理是3D图形的“皮肤”,但纹理采样远不是“把图片贴上去”那么简单。这里涉及几个关键概念:纹理过滤、Mipmap、寻址模式、各向异性过滤。
纹理过滤解决的是“当纹理像素和屏幕像素不是一一对应时怎么办”的问题。最邻近采样速度快但会有锯齿,线性过滤平滑但会模糊。实际项目里通常用三线性过滤,也就是在Mipmap的两层之间再做一次线性插值。Mipmap是一系列预先缩小好的纹理,用来解决远处物体纹理闪烁的问题。没有Mipmap的话,远处物体的纹理采样会出现严重的摩尔纹。
各向异性过滤解决的是“斜着看纹理时模糊”的问题。比如你站在一条马路中间往前看,路面纹理在远处会被压扁,普通三线性过滤会让远处路面糊成一片。各向异性过滤会根据视角方向,在纹理的不同方向上采样多次,最多16次,效果提升非常明显。但代价是带宽消耗增加,移动端要谨慎使用。
实操心得:纹理的尺寸最好是2的幂次方,比如256、512、1024。虽然现代GPU支持非2的幂次方纹理,但在生成Mipmap和某些压缩格式下,2的幂次方兼容性更好。另外,UI纹理和3D纹理要分开管理,UI纹理通常不需要Mipmap,而且要用点采样保持清晰。
还有一个容易被忽略的点是纹理压缩。PC端常用BC系列格式,移动端常用ETC2或ASTC。压缩纹理不仅能减少显存占用,还能降低带宽消耗。但压缩是有损的,对于法线贴图这种对精度要求高的纹理,压缩后可能会出现明显的块状伪影。我的经验是:颜色贴图可以用高压缩比,法线贴图用低压缩比或者不压缩,具体要看项目对画质的要求。
2.3 光照模型:从Lambert到PBR的演进逻辑
光照是3D图形里最能体现“技术含量”的部分。最早的光照模型是Lambert漫反射,只考虑光线方向和法线方向的夹角,计算简单但看起来很平。后来出现了Phong模型,加了高光反射,看起来有光泽了。再后来是Blinn-Phong,优化了高光的计算方式。现在主流是PBR,基于物理的渲染。
PBR的核心思想是:用物理参数来描述材质,而不是用经验参数。传统光照模型里,你调一个“高光强度”参数,调大调小全凭感觉。PBR里,你用粗糙度和金属度来描述材质,粗糙度决定高光的扩散程度,金属度决定反射的颜色。这两个参数有明确的物理意义,而且在不同光照环境下表现一致。
PBR的另一个关键是能量守恒。简单说就是:反射的光不能比入射的光多。传统光照模型里,漫反射和高光反射是分开算的,加起来可能超过1,导致物体看起来过亮。PBR里,漫反射和高光反射共享同一个能量预算,高光反射多了,漫反射就少了,这样物体看起来才自然。
我在实际项目里发现,很多美术同学一开始不理解粗糙度和金属度的区别,经常把金属度调到0.5这种中间值。实际上,现实世界里要么是金属(金属度=1),要么是非金属(金属度=0),中间值只适用于过渡区域。这个观念需要反复沟通,最好在材质编辑器里加一个提示,告诉美术这两个参数的含义。
2.4 深度测试与混合:透明物体为什么总是画不对
深度测试是3D图形里解决遮挡关系的机制。每个像素在写入颜色缓冲区之前,会先比较它的深度值和深度缓冲区里的值。如果更近,就写入并更新深度缓冲区;如果更远,就丢弃。这个机制保证了近处的物体遮挡远处的物体。
但透明物体是个例外。透明物体需要混合,也就是把透明物体的颜色和背景颜色按一定比例混合。混合操作依赖于绘制顺序,所以透明物体必须从远到近绘制。如果顺序错了,就会出现“近处的透明物体被远处的透明物体遮挡”这种错误。
更麻烦的是,透明物体通常不写入深度缓冲区,只读取。这意味着多个透明物体之间的遮挡关系无法通过深度测试解决,只能靠排序。但排序也不是万能的,比如两个透明物体互相穿插,排序算法就无能为力了。这时候要么用深度剥离,要么用顺序无关的透明渲染,但这些方案都有性能代价。
实操心得:透明物体的渲染是性能杀手。移动端上,透明物体的overdraw非常致命。我的建议是:能不用透明就不用透明,能用Alpha Test就不用Alpha Blend。Alpha Test虽然会有硬边缘,但不需要排序,也不会有overdraw问题。如果必须用透明,尽量控制透明物体的数量和覆盖面积。
3. 实操过程与核心环节实现
3.1 从零搭建一个最小渲染循环
我拿OpenGL举例,搭建一个最小的3D渲染循环。虽然现在很多人用引擎,但理解底层流程对排查问题非常有帮助。
第一步是初始化窗口和上下文。用GLFW创建窗口,设置OpenGL版本(比如3.3 Core Profile),然后加载OpenGL函数指针。这一步在Windows上通常用GLAD或GLEW,在macOS上要注意Core Profile不支持固定管线。
第二步是编译着色器。顶点着色器和片元着色器分别编译,然后链接成一个程序。编译失败时要打印日志,否则你只能看到黑屏,不知道哪里错了。
// 顶点着色器 #version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aNormal; layout (location = 2) in vec2 aTexCoord; uniform mat4 model; uniform mat4 view; uniform mat4 projection; out vec3 FragPos; out vec3 Normal; out vec2 TexCoord; void main() { FragPos = vec3(model * vec4(aPos, 1.0)); Normal = mat3(transpose(inverse(model))) * aNormal; TexCoord = aTexCoord; gl_Position = projection * view * vec4(FragPos, 1.0); }// 片元着色器 #version 330 core in vec3 FragPos; in vec3 Normal; in vec2 TexCoord; out vec4 FragColor; uniform sampler2D texture1; uniform vec3 lightPos; uniform vec3 viewPos; void main() { // 环境光 float ambientStrength = 0.1; vec3 ambient = ambientStrength * vec3(1.0); // 漫反射 vec3 norm = normalize(Normal); vec3 lightDir = normalize(lightPos - FragPos); float diff = max(dot(norm, lightDir), 0.0); vec3 diffuse = diff * vec3(1.0); // 高光 float specularStrength = 0.5; vec3 viewDir = normalize(viewPos - FragPos); vec3 reflectDir = reflect(-lightDir, norm); float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32); vec3 specular = specularStrength * spec * vec3(1.0); vec3 result = (ambient + diffuse + specular) * texture(texture1, TexCoord).rgb; FragColor = vec4(result, 1.0); }第三步是准备顶点数据。创建VAO、VBO、EBO,把顶点数据上传到GPU。VAO记录顶点属性的布局,VBO存实际数据,EBO存索引。这一步的常见错误是属性指针设置不对,比如stride算错、offset算错,导致模型显示异常。
第四步是渲染循环。每一帧清屏、更新摄像机矩阵、绑定纹理、绘制。绘制时用glDrawElements而不是glDrawArrays,因为索引绘制能复用顶点,减少顶点着色器的调用次数。
3.2 摄像机控制:让用户能“走进”场景
摄像机控制是3D应用的基本交互。最简单的实现是轨道摄像机,围绕一个目标点旋转。复杂一点的是第一人称摄像机,用鼠标控制朝向,WASD控制移动。
轨道摄像机的核心是球坐标。用两个角度(方位角和俯仰角)和一个半径来确定摄像机位置。鼠标水平移动改变方位角,垂直移动改变俯仰角,滚轮改变半径。俯仰角要限制在-89度到89度之间,否则会出现万向节死锁。
第一人称摄像机的核心是欧拉角和视图矩阵。鼠标移动改变偏航角和俯仰角,然后根据这两个角度计算前向量、右向量、上向量,构建视图矩阵。移动时,前向量和右向量分别乘以速度,累加到摄像机位置。
注意:鼠标输入要处理边界情况。比如鼠标移到窗口边缘时,如果不做处理,摄像机会一直旋转。解决办法是限制鼠标在窗口内,或者用相对移动模式。另外,帧率波动会影响移动速度,所以移动量要乘以deltaTime。
3.3 模型加载与优化:从OBJ到glTF
OBJ是最简单的模型格式,只包含顶点、法线、纹理坐标和材质。但OBJ不支持动画、不支持PBR材质、文件体积大。glTF是现在主流的格式,支持PBR材质、骨骼动画、场景层级,而且可以用二进制格式(.glb)减少体积。
加载模型时,要注意几个问题。第一是坐标系差异,不同建模软件的坐标系可能不同,比如Blender是Z轴向上,Unity是Y轴向上,导入时要转换。第二是材质映射,OBJ的MTL文件和glTF的材质系统不一样,PBR参数要重新映射。第三是顶点去重,很多模型导出时顶点是重复的,加载后要去重以减少顶点数量。
我在项目里用过Assimp库加载模型,功能很全但体积也大。如果只需要加载glTF,用tinygltf更轻量。加载后的模型数据要转换成引擎内部的格式,比如把顶点数据打包成交错布局,把索引数据转成32位整数。
3.4 性能分析与调优:找到瓶颈在哪里
3D应用的性能瓶颈通常出现在三个地方:CPU端、GPU端、带宽。CPU端的问题通常是绘制调用太多,GPU端的问题通常是片元着色器太复杂,带宽的问题通常是纹理太大或顶点数据太多。
分析工具方面,PC端可以用RenderDoc抓帧,看每个绘制调用的耗时和状态。移动端可以用Xcode的GPU Capture或Android的GPU Inspector。我习惯先用工具定位瓶颈在CPU还是GPU,然后再深入。
如果瓶颈在CPU,优先考虑合批。把相同材质的物体合并成一个绘制调用,能大幅减少CPU开销。如果瓶颈在GPU,优先考虑降低片元着色器复杂度,比如减少光照计算、用更简单的材质。如果瓶颈在带宽,优先考虑纹理压缩和减少顶点属性。
实操心得:不要过早优化。我见过很多项目一开始就追求极致性能,结果代码复杂度飙升,开发效率下降。正确的做法是:先让功能跑起来,用工具找到真正的瓶颈,再针对性优化。80%的性能问题往往来自20%的代码,找到那20%比全面优化更有效。
4. 常见问题与排查技巧实录
4.1 黑屏问题速查表
黑屏是3D图形开发中最常见的问题,原因可能有很多。我整理了一个排查顺序,从简单到复杂。
| 排查步骤 | 检查内容 | 常见原因 |
|---|---|---|
| 1 | 着色器编译日志 | 语法错误、版本不匹配 |
| 2 | 顶点数据 | 属性指针错误、stride/offset算错 |
| 3 | 矩阵 | 投影矩阵near/far设错、矩阵乘法顺序错 |
| 4 | 摄像机 | 位置在物体内部、朝向反了 |
| 5 | 深度测试 | 深度函数设错、深度缓冲区没清 |
| 6 | 背面剔除 | 顶点顺序反了、剔除面设错 |
| 7 | 纹理 | 纹理没绑定、采样器单元错 |
| 8 | 视口 | 视口尺寸为0、视口设置错 |
我遇到最多的是第3和第4条。有一次调了半天,最后发现是投影矩阵的far设成了0.1,near设成了100,整个场景都被裁掉了。还有一次是摄像机的位置和看向的点重合了,视图矩阵退化,画面全黑。
4.2 画面撕裂与帧率不稳
画面撕裂是因为GPU在屏幕刷新时还在绘制,导致上半屏显示旧帧,下半屏显示新帧。解决办法是开启垂直同步,让GPU等待屏幕刷新。但垂直同步会引入输入延迟,竞技类游戏通常关闭垂直同步,改用自适应同步技术。
帧率不稳的原因通常是帧时间波动。可能是某一帧的绘制调用特别多,也可能是垃圾回收导致的卡顿。解决办法包括:对象池化减少GC、异步加载资源、限制每帧的绘制调用数量。
我在移动端项目里遇到过一个典型问题:帧率在60和30之间跳。排查后发现是纹理加载导致的。游戏在运行时动态加载纹理,加载的那一帧耗时特别长,触发了垂直同步的降帧机制。后来改成预加载所有纹理,帧率就稳定了。
4.3 内存泄漏与资源管理
3D应用的内存泄漏通常来自GPU资源没有释放。OpenGL的纹理、缓冲区、着色器程序,创建后如果不删除,就会一直占用显存。在长时间运行的应用里,这会导致显存耗尽,程序崩溃。
解决办法是RAII,也就是资源获取即初始化。用C++的智能指针或者自己封装资源类,在析构函数里释放GPU资源。另外,要定期检查显存占用,用工具查看是否有未释放的资源。
注意:OpenGL的删除操作是异步的,调用glDeleteTextures后,纹理不会立即释放,而是等GPU用完后再释放。所以不要频繁创建和删除纹理,最好用纹理池来复用。
4.4 跨平台兼容性问题
不同平台的GPU驱动行为可能不同。比如某些Android设备对浮点精度的处理不一致,导致着色器计算结果有差异。某些iOS设备不支持某些纹理格式。Windows上不同显卡厂商的驱动也有差异。
解决办法是在目标平台上尽早测试,不要等到开发后期才移植。着色器里避免使用高精度浮点,尽量用mediump。纹理格式选择要查目标平台的兼容性列表。如果必须用平台特有的功能,用宏来隔离。
我在项目里遇到过一个坑:在PC上跑得好好的着色器,到了移动端就花屏。排查后发现是移动端GPU不支持动态数组索引,而我在片元着色器里用了动态索引访问数组。改成静态索引后问题解决。这个教训是:移动端GPU的限制比PC多得多,写Shader时要时刻想着移动端的约束。
4.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 模型显示为纯色 | 纹理没绑定或采样器错 | 检查纹理绑定和uniform | 绑定纹理到正确的采样器单元 |
| 模型有锯齿 | 没开抗锯齿 | 检查MSAA设置 | 开启MSAA或FXAA |
| 远处纹理闪烁 | 没生成Mipmap | 检查纹理参数 | 设置GL_LINEAR_MIPMAP_LINEAR |
| 透明物体遮挡错误 | 绘制顺序错 | 检查透明物体排序 | 从远到近绘制透明物体 |
| 高光位置不对 | 法线空间错 | 检查法线是否变换到世界空间 | 用法线矩阵变换法线 |
| 帧率突然下降 | 绘制调用过多 | 用工具统计绘制调用 | 合批或实例化 |
| 显存持续增长 | 资源没释放 | 检查资源创建和删除 | 用RAII管理资源 |
| 画面颜色偏暗 | 颜色空间错 | 检查是否做了伽马校正 | 在输出时做伽马校正 |
5. 进阶方向与个人经验分享
5.1 后处理效果:让画面更有“电影感”
后处理是在场景渲染完成后,对整张画面做二次处理。常见的后处理包括泛光、景深、色调映射、抗锯齿、屏幕空间反射。
泛光的原理是:把画面中亮度超过阈值的部分提取出来,做模糊,再叠加回原画面。这样亮的地方会有光晕,看起来更柔和。景深是模拟摄像机焦距,近处和远处模糊,中间清晰。色调映射是把HDR的颜色范围映射到LDR的显示范围,避免过曝。
后处理的性能开销主要在模糊操作上。高斯模糊是分离的,先水平模糊再垂直模糊,复杂度从O(n²)降到O(n)。但即使这样,全屏模糊还是很耗带宽。移动端上通常用降采样,把画面缩小到一半或四分之一再做模糊,最后放大回去。
5.2 实例化渲染:画一万棵树也不卡
实例化渲染是解决“大量相同物体”的性能利器。传统做法是每个物体一次绘制调用,一万棵树就是一万次调用,CPU直接爆了。实例化渲染把模型数据上传一次,然后一次性绘制一万个实例,每个实例有不同的变换矩阵。
OpenGL里用glDrawElementsInstanced,顶点着色器里用gl_InstanceID来区分不同实例。实例数据可以放在另一个VBO里,用glVertexAttribDivisor设置更新频率。
我在项目里用实例化渲染做过草地,一万五千棵草,帧率稳定在60。如果用传统方式,一千棵草就卡了。实例化的代价是每个实例的数据要提前准备好,不能动态增删。如果物体数量变化频繁,实例化就不太合适。
5.3 计算着色器:把GPU当通用计算用
计算着色器是OpenGL 4.3引入的功能,允许在GPU上做通用计算。在3D图形里,计算着色器可以用来做粒子模拟、布料模拟、光照预计算、剔除。
比如粒子系统,传统做法是在CPU上更新粒子位置,然后上传到GPU。粒子多了之后,CPU和GPU之间的带宽就成了瓶颈。用计算着色器,粒子数据一直在GPU上,更新和渲染都在GPU完成,带宽消耗大幅降低。
计算着色器的难点是线程组和共享内存的管理。线程组的大小要匹配GPU的架构,太大或太小都会浪费。共享内存用来做线程间通信,但容量有限,用多了会限制并行度。我建议先从简单的计算任务开始,比如图像处理,熟悉了再往复杂的模拟上做。
5.4 个人踩坑经验汇总
最后分享几个我在3D图形开发中踩过的坑,希望能帮你省点时间。
第一个坑是过度依赖引擎。我刚开始做项目时,什么都用引擎现成的功能,结果遇到引擎不支持的需求就傻眼了。后来我花时间学了底层的OpenGL和Vulkan,再回头看引擎的源码,发现很多问题都能自己解决了。我的建议是:引擎要用,但底层原理也要懂,至少要知道引擎帮你做了什么。
第二个坑是忽视美术流程。技术再牛,如果美术资产生产不出来,项目也推进不下去。我见过一个项目,技术选型很先进,但美术工具链没跟上,美术同学做不出符合要求的资产,最后项目延期。后来我学乖了,技术方案确定之前,先和美术沟通,确保他们能理解并执行。
第三个坑是不重视性能测试。开发阶段用高端显卡跑得很流畅,一到低端设备就卡成幻灯片。后来我养成了习惯:每做一个功能,就在最低配的设备上测一遍。如果跑不动,要么优化,要么砍功能。性能问题越早发现越好解决,拖到后期就是灾难。
第四个坑是文档和注释不够。3D图形的代码逻辑复杂,矩阵变换、坐标系转换,过两个月自己都看不懂。我现在写Shader和渲染代码,一定会写清楚每个矩阵的含义、每个参数的取值范围、每个步骤的目的。这样别人接手或者自己回头改,都能快速理解。
3D图形这个领域,入门容易精通难。但只要你理解了渲染管线的设计逻辑,掌握了坐标系变换和光照模型,再通过实际项目不断积累经验,就能慢慢从“能跑”做到“跑得好”。我到现在还在学新东西,比如实时光线追踪、神经网络渲染,这个领域的变化很快,保持学习的心态比什么都重要。