
应用技术大学的计算机图形学实验课大概是整个课程体系里最看着简单、做着上头的一门。屏幕上一根线没画直、一个三角形颜色不对、纹理贴上去糊成一团你盯着代码看半小时也找不出问题——因为图形学的错误往往不在语法层而在数学层和管线层。我带过几届实验课也自己从固定管线一路写到着色器最深的体会是图形学实验的价值不在于把图渲染出来而在于你能不能说清楚每一个像素是怎么被算出来的。这篇内容面向正在做或者准备做计算机图形学实验的同学也适合已经能跑通Demo但讲不清原理的开发者。我会按实验课的真实推进顺序把光栅化、几何变换、光照着色、曲线曲面、纹理映射这几块拆开讲每一块都给出可复现的代码骨架、参数推导过程以及我自己踩过的坑。1. 实验课的第一道坎为什么图形学代码跑得动却讲不清图形学实验有个很典型的现象照着实验指导书敲完窗口弹出来了三角形也画出来了但老师一问这个顶点最后落在屏幕的哪个像素上人就卡住了。这不是学习态度问题而是图形学的知识结构和普通编程课完全不同——普通编程课是输入→处理→输出的线性思维图形学是建模坐标→变换到裁剪空间→光栅化成片元→着色→写帧缓冲的多级流水线思维中间任何一级理解断层代码照样能跑但你就失去了调试能力。1.1 把渲染管线当成一条传送带来理解我一般建议学生先别急着写代码拿一张纸把管线画成传送带传送带的起点是一堆三维顶点坐标终点是屏幕上的颜色像素。中间经过的每一站都要问三个问题——这一站输入什么、输出什么、做了什么运算。具体来说顶点数据先经过模型矩阵搬到世界空间再经过视图矩阵搬到相机空间再经过投影矩阵搬到裁剪空间然后做透视除法得到标准化设备坐标NDC最后经过视口变换映射到屏幕像素。这一串里前三个变换是矩阵乘法第四个是除以 w 分量第五个是线性缩放加平移。提示实验报告里如果只写使用了MVP矩阵基本拿不到过程分。要写清楚每个矩阵的维度、作用空间、以及为什么乘的顺序是 P×V×M 而不是 M×V×P。为什么顺序这么重要因为矩阵乘法不满足交换律。顶点是列向量时变换从右往左作用先乘模型矩阵再乘视图矩阵最后乘投影矩阵。你可以用一个小例子验证先平移(1,0,0)再放大2倍和先放大2倍再平移(1,0,0)结果差了一倍的距离。这个验证我让学生每个人都手算一遍比看十遍教材管用。1.2 建立像素级验证的调试习惯普通程序调试靠打印变量图形学调试要靠打印像素。我推荐的做法是写完一个绘制函数先别画复杂模型画一个已知坐标的三角形然后用代码把帧缓冲里某几个坐标的颜色值读出来打印。以常见的OpenGL为例可以用glReadPixels读取指定像素unsigned char pixel[4]; glReadPixels(320, 240, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel); printf(center pixel: R%d G%d B%d A%d\n, pixel[0], pixel[1], pixel[2], pixel[3]);这里有个坑必须提前说glReadPixels的坐标原点在左下角而大多数人习惯的屏幕坐标原点在左上角。我第一次做实验时读中心点颜色一直是黑的排查了四十分钟才发现是坐标系搞反了把 y 坐标用height - y换算一下就好了。这种坑在实验指导书里通常不会写但它能吃掉你半个下午。另一个习惯是给每个绘制阶段加可视化中间结果。比如把法线当成颜色输出法线 xyz 映射到 rgb你立刻能看出法线方向对不对把纹理坐标当成颜色输出能看出 UV 有没有超出 0 到 1 的范围。这两个技巧在后面的光照和纹理实验里会反复用到比任何断点调试都直观。2. 工具链选型固定管线和可编程管线实验环境怎么搭实验课第一件事是选技术栈而这个选择会直接影响你后面能学到什么。应用技术大学的教学场景通常有三种主流方案我按学习收益和上手成本做了个对比你可以对照自己的实验要求选。方案代表技术上手成本能接触到的底层细节适合的实验类型固定管线老式 OpenGLglBegin/glEnd低少大部分被封装入门绘制、变换演示可编程管线现代 OpenGL / WebGL GLSL中多着色器全流程可控光照、纹理、阴影高层封装Three.js / 图形库封装层很低很少偏应用快速出效果、课程展示2.1 为什么我建议至少用一次可编程管线固定管线的写法很诱人几行glBegin(GL_TRIANGLES)就能画图但它把顶点处理、光照计算全藏在驱动里你根本看不到矩阵怎么作用、法线怎么参与计算。应用到具体实验里学生做完固定管线实验往往只知道调了glLightfv就有光了但说不清光照公式。可编程管线的学习曲线陡一些要先写顶点着色器和片元着色器还要管理顶点缓冲对象VBO、顶点数组对象VAO但一旦跑通整个计算过程对你就是透明的。我通常建议入门实验用固定管线快速建立图形是画出来的这个直觉从光照实验开始必须切到可编程管线否则后面的内容全是黑盒。一个最小的顶点着色器长这样#version 330 core layout(location 0) in vec3 aPos; layout(location 1) in vec3 aNormal; uniform mat4 model; uniform mat4 view; uniform mat4 projection; uniform mat3 normalMatrix; out vec3 fragNormal; out vec3 fragPos; void main() { fragPos vec3(model * vec4(aPos, 1.0)); fragNormal normalMatrix * aNormal; gl_Position projection * view * vec4(fragPos, 1.0); }注意这里的normalMatrix它是模型矩阵左上角 3×3 部分的逆转置矩阵。为什么不能直接用模型矩阵变换法线因为法线是垂直于表面的方向向量当模型发生非均匀缩放比如 x 方向拉长 2 倍法线如果按同样方式缩放就不再垂直于变换后的表面了。这个细节是光照实验里最容易翻车的地方我在第 5 节会展开讲推导。2.2 环境搭建里最容易被忽略的两件事第一件是深度测试默认没开。很多同学画完两个三角形发现后面的盖住了前面的以为是绘制顺序问题其实是忘了glEnable(GL_DEPTH_TEST)。深度测试的原理是每个片元带一个深度值写入帧缓冲时和已有深度比较更近的才覆盖。开了之后还要记得每帧清空深度缓冲glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)少清一个就会看到上一帧的残影。第二件是视口和窗口尺寸不一致。窗口被拖拽改变大小时如果不重新调用glViewport画面会被拉伸变形或者只渲染在左下角一块。标准的处理是注册窗口尺寸回调在里面更新视口和投影矩阵的宽高比void framebuffer_size_callback(GLFWwindow* window, int width, int height) { glViewport(0, 0, width, height); float aspect (float)width / (float)height; projection glm::perspective(glm::radians(45.0f), aspect, 0.1f, 100.0f); }宽高比不更新带来的后果是画面横向拉胖学生在做相机实验时经常误判成投影矩阵参数写错了其实只是漏了这个回调。这类环境层的坑不涉及算法但会浪费大量排查时间我一般会在实验课开头统一演示一遍。3. 光栅化实验直线、圆与多边形的像素级拆解光栅化是图形学实验里最有手工感的一块也是最能体现数学到像素转换的环节。屏幕上没有真正的直线只有一排排发光的像素点你要做的事情是用整数运算决定这一列像素里哪一行该点亮。这个判断逻辑就是光栅化算法。3.1 从 DDA 到 Bresenham误差累积的两种处理思路DDA数字微分分析的思路很直观把直线参数化x 每次加 1y 就加一个斜率 k然后对 y 四舍五入取整。代码短但有个硬伤——每步都要做浮点加法和取整而且误差会随着步数累积。Bresenham 算法的精妙之处在于把浮点运算换成了整数误差项的符号判断。核心思想是维护一个决策变量表示当前点到理想直线的偏差根据偏差的正负决定下一个像素是取正右方还是右上方。以斜率在 0 到 1 之间的直线为例void bresenhamLine(int x0, int y0, int x1, int y1) { int dx x1 - x0; int dy y1 - y0; int d 2 * dy - dx; // 初始决策变量 int y y0; for (int x x0; x x1; x) { setPixel(x, y); if (d 0) { y; d 2 * (dy - dx); } else { d 2 * dy; } } }这个d的推导值得在实验报告里写清楚它本质上是真实直线在 x1 处的 y 值与当前像素行中心之差的 2 倍再乘 dx之所以乘 2 是为了把分数比较转成整数比较。我让学生自己推一遍这个式子推完之后对整数光栅化这件事的理解会完全不一样。注意Bresenham 的完整实现要分八象限讨论。斜率绝对值大于 1 时要交换 x 和 y 的角色否则循环步进方向会出错画出来的线会断开。这是实验里最常见的扣分点。3.2 圆和椭圆八分对称性的利用画圆实验里很多人第一反应是用参数方程逐角度采样结果圆看起来毛毛糙糙因为角度采样在圆周上分布不均匀。正确做法是利用圆的八分对称性只计算 0 到 45 度这一段第一象限中靠近 y 轴的部分其余七个点通过对称变换得到。中点画圆算法同样是整数决策变量的思路决策变量来自圆方程 F(x,y) x² y² - r² 在中点处的取值符号。当决策变量小于 0说明中点在圆内选正右方的像素否则选右下方的像素。void midpointCircle(int r) { int x 0, y r; int d 1 - r; // 初始决策变量 while (x y) { plot8(x, y); // 八分对称绘制 if (d 0) { d 2 * x 3; } else { d 2 * (x - y) 5; y--; } x; } }plot8会一次性画出 (x,y)、(y,x)、(-x,y)、(-y,x) 等八个对称点。这里我在实验课上见过一个反复出现的错误对称点画了但圆心没有加上去导致八个点分散在原点周围而不是围绕指定圆心。修法是把plot8里的每个点都加上圆心偏移(cx, cy)。这个错误看起来很低级但它恰好说明算法逻辑和坐标系统是两件事混在一起就会出错。多边形填充实验通常会引入扫描线算法或种子填充。扫描线填充的核心是求交点、排序、配对填充这里有个经典边界问题顶点恰好落在扫描线上时交点会被重复计算导致填充区域错位。标准解法是左闭右开规则即判断交点时把共享顶点只计入一侧的边。这个规则如果不讲清楚学生填出来的多边形总会在某些行多一个或少一个像素。4. 几何变换实验矩阵不是背出来的是验出来的变换实验是图形学里数学味最重的一块也是很多人第一次真正感受到线性代数有用的地方。但实验课的问题在于学生往往把矩阵当公式背背完能通过检查一换场景就不会用。我的做法是所有变换都要求学生先用两个具体点手算再用代码验证最后对比结果。4.1 齐次坐标到底解决了什么问题二维变换用 2×2 矩阵就够表示旋转和缩放但平移不行因为平移是加法不是乘法。齐次坐标的做法是给点补一个维度二维点 (x, y) 写成 (x, y, 1)这样平移就能写成矩阵乘法$$ \begin{bmatrix} 1 0 t_x \ 0 1 t_y \ 0 0 1 \end{bmatrix} \begin{bmatrix} x \ y \ 1 \end{bmatrix}\begin{bmatrix} x t_x \ y t_y \ 1 \end{bmatrix} $$这样一来旋转、缩放、平移全部统一成矩阵乘法多个变换可以预先乘成一个矩阵顶点只需要乘一次。这个合并矩阵优化在渲染大量顶点时意义重大也是实验里应该让学生实测的分别用三次矩阵乘法变换 10 万个点和用一个合并矩阵变换 10 万个点用系统计时器量一下耗时差异。我实测下来合并之后通常能快一到两倍具体倍数和硬件有关。提示齐次坐标中的 w 分量不是补位的 1在投影变换后它有意义。透视除法就是拿 xyz 除以 w这是实现近大远小的关键实验报告里要单独说明。4.2 旋转方向与矩阵顺序两个高频翻车点旋转方向的问题几乎每届都有人问为什么我按逆时针公式写的矩阵画出来是顺时针原因是屏幕坐标系里 y 轴方向和你数学课本上不一样。数学坐标系 y 轴向上屏幕坐标系 y 轴向下两者差一个镜像旋转的视觉方向就反了。解决办法要么在投影矩阵里翻转 y要么接受这个约定在实验报告里写明使用的坐标系。矩阵顺序的问题更隐蔽。假设你要把一个物体先缩放到 2 倍再绕原点旋转 90 度最后一个顶点 v 的变换是glm::mat4 S glm::scale(glm::mat4(1.0f), glm::vec3(2.0f)); glm::mat4 R glm::rotate(glm::mat4(1.0f), glm::radians(90.0f), glm::vec3(0,0,1)); glm::mat4 M R * S; // 先缩放再旋转 glm::vec3 result M * v;关键点列向量约定下右侧的矩阵先作用。所以先缩放再旋转写成R * S而不是S * R。这个顺序如果写反非均匀缩放的物体会被剪切变形看起来像斜了很多同学误以为是模型数据坏了其实是矩阵乘反了。我做实验时有个笨但有效的检查方法挑一个不在原点的点比如 (1, 0, 0)按你写的矩阵顺序手算出结果再和代码输出对比。手算和代码一致顺序就对了。这个方法虽然土但它把矩阵顺序这个抽象问题变成了可以验证的具体数字。4.3 三维相机的构造视图矩阵其实是手动搭坐标系视图矩阵的实验我一般放在变换实验的最后因为它综合了前面所有知识。相机的本质是定义一套新的坐标系相机位置是原点观察方向是 -z 轴上方向是 y 轴右方向是 x 轴。构造过程是观察方向 目标点 - 相机位置归一化得到 z 轴注意取反才是相机朝向右方向 上方向 × z 轴归一化再用 z 轴 × 右方向得到真正的上方向。这三个正交基向量组成旋转部分再和相机位置组合成平移部分glm::mat4 lookAt(glm::vec3 eye, glm::vec3 center, glm::vec3 up) { glm::vec3 f glm::normalize(center - eye); // 前方向 glm::vec3 s glm::normalize(glm::cross(f, up)); // 右方向 glm::vec3 u glm::cross(s, f); // 正交化后的上方向 glm::mat4 view(1.0f); view[0][0] s.x; view[1][0] s.y; view[2][0] s.z; view[0][1] u.x; view[1][1] u.y; view[2][1] u.z; view[0][2] -f.x; view[1][2] -f.y; view[2][2] -f.z; view[3][0] -glm::dot(s, eye); view[3][1] -glm::dot(u, eye); view[3][2] glm::dot(f, eye); return view; }注意u cross(s, f)这一步是在做正交化。如果输入的 up 向量和 f 不垂直很常见比如随手写个 (0,1,0)直接用它会让画面倾斜。用叉乘重新算出垂直的上方向这个问题就消除了。这个细节在实验指导书里往往一笔带过但它是画面看着有点歪的常见原因。5. 光照与着色实验从 Lambert 到 Phong 的逐步逼近光照实验是整个图形学实验里最有成就感的一块也是最能区分会调包和懂原理的一块。同一个球体用不同的光照模型渲染出来质感差异非常明显。我的建议是不要一上来就用完整的光照公式而是按 Lambert 漫反射、Phong 镜面反射、Blinn-Phong 优化的顺序逐步加每加一步都观察画面变化。5.1 法线变换为什么必须用逆转置矩阵在讲光照模型之前必须先解决法线变换的问题。顶点位置用模型矩阵变换法线却不能用同一个矩阵原因在于法线是垂直关系的载体而垂直关系在非均匀缩放下不保持。举个具体例子一条斜面法线是 (1, 1, 0)模型在 x 方向拉伸 2 倍如果法线也按同样方式变成 (2, 1, 0)它和变换后表面的夹角就不再是 90 度了。数学上可以证明保持垂直关系的正确变换是模型矩阵左上角 3×3 部分的逆转置。glm::mat3 normalMatrix glm::transpose(glm::inverse(glm::mat3(model)));实测下的现象很直观用错矩阵时拉伸变形区域的明暗分布会明显不对高光位置偏移看起来像光从错误的方向照过来。而在均匀缩放和纯旋转下逆转置刚好等于原矩阵所以有些同学在简单模型上没发现问题一换到非均匀缩放就翻车。5.2 Lambert 漫反射点积背后的物理直觉漫反射项的公式是max(dot(N, L), 0)乘以光照颜色和物体颜色其中 N 是归一化法线L 是从表面指向光源的归一化向量。这个点积的几何意义是法线越正对光源表面接收到的光越多法线与光线垂直时点积为 0表面不受光。vec3 N normalize(fragNormal); vec3 L normalize(lightPos - fragPos); float diff max(dot(N, L), 0.0); vec3 diffuse diff * lightColor * objectColor;这里必须强调归一化。如果 N 或 L 没有归一化点积的结果会超过 1 或者偏小画面不是过曝就是偏暗。我见过有同学把normalize写在顶点着色器里但片元里忘了插值之后法线长度不再为 1明暗过渡就变得很怪。正确做法是在片元着色器里重新归一化。max(..., 0.0)这一层也不能省点积为负说明光在表面背面物理上不应该有贡献不加 max 会出现背面发亮、明暗翻转的现象。5.3 Phong 与 Blinn-Phong半程向量的那次优化镜面反射项描述的是高光。Phong 模型用反射向量 R 和视线向量 V 的点积Blinn-Phong 则用半程向量 H光线方向和视线方向的平均和法线的点积vec3 V normalize(viewPos - fragPos); vec3 H normalize(L V); // Blinn-Phong 的半程向量 float spec pow(max(dot(N, H), 0.0), shininess); vec3 specular spec * lightColor * specularStrength;两者视觉上很接近但 Blinn-Phong 计算量更小而且在高光边缘更平滑。shininess参数控制高光范围值越大高光越集中、物体看起来越光滑。实验里建议让学生把 shininess 从 2 调到 256观察高光从一大片收缩成一个小点的过程这个连续变化的观察比看公式更能建立参数直觉。注意Phong 光照是逐片元计算的在片元着色器里不是逐顶点。逐顶点光照在低面数模型上会出现明显的马赫带效应高光断成一块一块。实验报告里如果写了逐顶点光照要说明它在什么情况下不够用。5.4 光照实验的调试顺序先孤立再组合光照实验最容易出的问题是画面不对但不知道哪一项不对。我的排查顺序是固定的只开环境光确认物体整体亮度正常没有全黑或全白只开漫反射把光源放在物体正前方确认最亮区域在正对光源的位置只开镜面反射把 shininess 设小一点确认高光能跟着相机移动三项一起开调权重直到视觉合理。这个顺序的价值在于每次只引入一个变量。我见过学生一上来就写完整公式结果画面一片黑然后开始乱改参数越改越乱。按孤立变量法排查十分钟就能定位到是法线没归一化还是光源位置传错。6. 曲线曲面与纹理映射那些看起来对但细节翻车的地方曲线曲面和纹理映射是实验课里见效快、翻车也快的两块。曲线实验画出来的曲线往往一眼看着没问题但控制点一动就发现曲线不经过某些点、或者曲率突变纹理实验贴上去能看但一转视角就开始拉伸、接缝、摩尔纹。这些问题的根源都在参数化细节上。6.1 Bézier 曲线de Casteljau 比多项式展开更好懂Bézier 曲线的标准公式是伯恩斯坦多项式求和直接展开写代码也能跑但学生往往不理解为什么曲线会被控制点拉过去。de Casteljau 算法用递归插值的几何方式构造曲线理解成本低得多在相邻控制点之间按参数 t 做线性插值得到新的一层点再对新点插值直到只剩一个点。glm::vec3 deCasteljau(const std::vectorglm::vec3 pts, float t) { std::vectorglm::vec3 tmp pts; while (tmp.size() 1) { for (size_t i 0; i 1 tmp.size(); i) tmp[i] glm::mix(tmp[i], tmp[i1], t); tmp.pop_back(); } return tmp[0]; }这个算法有三个性质值得在实验里验证曲线经过首尾控制点、曲线位于控制点凸包内、控制点整体平移时曲线同向平移。我让学生把控制点拖到共线观察曲线退化成直线再把中间控制点拉到很远观察曲线被拽的幅度。这些交互验证比静态截图更能说明问题。B 样条实验的难点在节点向量。均匀 B 样条好写但一旦要求曲线经过端点就要用准均匀或 clamped 节点向量即在首尾重复节点。实验里常见的错误是节点向量长度算错n1 个控制点、k 阶次数为 k-1的 B 样条节点向量长度应该是 nk1。这个公式记错了程序要么越界要么曲线形状完全不对。6.2 纹理坐标与采样拉伸、接缝、摩尔纹的三类成因纹理实验里最典型的现象是贴图看起来糊糊的或者边缘有缝。这三类问题对应三个不同的原因排查时可以分开看现象常见原因处理方式纹理拉伸变形UV 坐标范围或映射方式不对检查 UV 是否覆盖 0 到 1模型 UV 是否按比例分配模型接缝处有裂缝采样模式导致边缘颜色不连续设置 wrap 模式为 repeat 或 clamp to edge远处出现摩尔纹采样频率高于纹理分辨率开启 mipmap 并设置合适的过滤模式wrap 模式这块值得展开说。默认的GL_REPEAT在 UV 超出 0 到 1 时会循环平铺适合地面、墙面这种连续表面GL_CLAMP_TO_EDGE会把边缘像素向外延伸适合单张海报这种不希望重复的情况。选错了就会出现接缝处突然重复半张图的诡异效果。mipmap 的原理是预先生成一系列逐级缩小的纹理采样时根据屏幕上的像素密度选择合适的层级。远处物体一个屏幕像素可能覆盖纹理上几十个像素如果不做 mipmap采样点随机落在纹理的不同位置就会产生高频闪烁也就是摩尔纹。开启很简单glGenerateMipmap(GL_TEXTURE_2D); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);注意MIN_FILTER一定要设成带 mipmap 的选项只开GL_LINEAR是没用的这是个高频错误。另外纹理坐标的 y 轴方向在图像加载时经常和图形 API 约定相反加载出来的图上下颠倒解决办法是在加载时翻转或者在着色器里用1.0 - uv.y。这个坑我在不同项目里反复遇到建议加载纹理的封装函数里直接处理掉。6.3 多纹理混合时的透明度排序问题做完基础纹理之后实验通常会加一个混合blending环节画半透明的物体。这里有个原理性的坑透明渲染必须从远到近绘制否则会出现近处透明物体被远处物体遮挡的错误效果。原因是混合是逐片元按绘制顺序叠加的先画的已经在帧缓冲里后画的按 alpha 混合上去。如果近的先画远的后画远物体的片元会覆盖掉近物体已经混合好的颜色。正确做法是把透明物体单独收集按相机距离从远到近排序后再绘制。这个排序在实验用的少量物体场景里很好实现但在真实场景里开销不小这也是为什么很多引擎把透明物体和实体物体分开渲染。7. 实验报告与答辩把我做出来了变成我知道为什么最后说一个不那么技术、但直接决定实验得分的事报告怎么写。应用技术大学的实验评分通常分过程分和报告分过程分看代码能不能跑报告分看你能不能讲清楚。我见过太多代码写得不错但报告只有截图和运行成功四个字的作业得分很吃亏。7.1 报告里必须出现的四类内容第一类是参数推导。任何一个算法用了什么公式这个公式怎么来的关键参数取值的依据是什么都要写。比如 Bresenham 的决策变量推导、Phong 的 shininess 取值理由、mipmap 层级怎么算。第二类是对照实验。单纯的结果截图说明不了什么有对照组才有信息量。开了深度测试和没开深度测试的对比、用逆转置矩阵和用原矩阵的对比、有 mipmap 和没 mipmap 的对比这类图放上去老师一眼就知道你真的理解了。第三类是异常现象与排查过程。报告里写遇到了边缘接缝问题排查后定位为 wrap 模式设置不当比写程序运行成功有价值得多。排查过程本身就是能力证明。第四类是性能或精度数据。比如前面提到的合并矩阵优化前后耗时对比、不同面数模型下逐顶点和逐片元光照的视觉差异、纹理尺寸对加载时间的影响。有一两个具体数字报告的专业度立刻不一样。7.2 答辩时被追问的三个问题提前准备好根据我带实验课的经验老师最常追问的三个问题是你这个矩阵为什么这么乘、这个算法的时间复杂度是多少、如果模型更复杂你的方案还成立吗。第一个问题考的是变换顺序理解准备好手算过程就能答。第二个问题考的是算法意识Bresenham 是 O(n)n 是较长轴上的像素数扫描线填充是 O(边数 扫描线数 × 交点排序开销)能说出大致量级就行。第三个问题考的是边界意识比如逐顶点光照在低面数下失真、固定管线在复杂材质下不够用、透明排序在大量物体时开销大能说出局限比强行说没问题更让人信服。提示答辩时如果被问到不会的直接说这块我还没验证到我的推测是……比硬编一个答案好。图形学的细节太多没人指望你全懂但态度和思路是能看出来的。7.3 实验之外的延伸方向如果做完基础实验还有余力我建议往两个方向延伸。一个是把渲染结果和数学公式对起来比如写一个小工具输入任意点和光源位置实时显示法线点积的数值和对应像素颜色这样公式和画面就彻底打通了。另一个是做一次完整的场景把变换、光照、纹理、透明混合全部用上因为单独的实验都是玩具组合起来才会暴露排序、状态管理、资源释放这些工程问题。我自己在这些实验里折腾下来最大的收获其实不是学会了某个 API而是建立了一种从像素反推计算过程的思维方式。屏幕上任何一个颜色不对的像素背后一定有一个具体的矩阵、一次具体的归一化、一个具体的采样决策出了问题。顺着这条链路往回找几乎总能找到原因。这个习惯后来在处理任何图形相关的问题时都用得上比记住多少个函数签名有用得多。