all lessons / computer_graphics / 04 · rasterization lesson 4 / 15

Rasterization — triangles to fragments & the z-buffer光栅化——三角形到片元与深度缓冲

Lesson 01 sketched the rasterization loop and lesson 02 delivered triangles into screen space. Now we actually turn a screen-space triangle into covered pixels — and resolve which surface wins when triangles overlap. Everything here rests on one tiny signed quantity, the edge function, computed three times per pixel: it decides coverage, hands us interpolation weights for free, breaks ties on shared edges, and even tells us which way a triangle faces. Then a single per-pixel number, depth, replaces a whole family of broken sorting schemes. 第 01 课勾勒了光栅化循环,第 02 课把三角形送进了屏幕空间。现在我们真正把一个屏幕空间三角形变成被覆盖的像素——并在三角形重叠时决出哪个表面胜出。这里的一切都立在一个小小的有符号量上,即边函数,每个像素算三次:它判定覆盖、免费送上插值权重、在共享边上打破平局,甚至还告诉我们三角形朝向哪边。随后,单个逐像素的数——深度——取代了一整族有缺陷的排序方案。

The plan本课计划

Five moves. (1) Ask the core question — which pixels does a triangle cover? — and answer it with the edge function, a signed area that is a half-plane test; a point is inside iff three edge functions share sign. (2) Notice those same three numbers are the barycentric coordinates — the interpolation weights lesson 05 will lean on. (3) Fix a subtle bug at shared edges with the top-left / fill rule, so no pixel is drawn twice or skipped. (4) Solve visibility the right way: see why the painter's algorithm fails, then adopt the z-buffer — one depth per pixel, order-independent — and confront z-fighting from nonlinear screen-z. (5) Skip work early with back-face culling (from the winding sign) and near-plane clipping (which must happen before the divide). Then lesson 05 fills these covered pixels with color.

五步走。(1) 提出核心问题——三角形覆盖哪些像素?——并用边函数回答它,那是一个有符号面积、亦即半平面测试;一个点在内部当且仅当三个边函数同号。(2) 注意到这同样的三个数就是重心坐标——第 05 课要倚重的插值权重。(3) 用左上 / 填充规则修掉共享边上的一个微妙 bug,使得没有像素被画两次或被跳过。(4) 用正确的方式解可见性:看清画家算法为何失败,再采用z-buffer——每像素一个深度、与顺序无关——并直面由非线性屏幕 z 引起的z-fighting。(5) 提前省下工作量:背面剔除(靠环绕符号)与近平面裁剪(必须在除法之前)。随后第 05 课会给这些被覆盖的像素填上颜色。

1 · The core question: which pixels does a triangle cover?核心问题:三角形覆盖哪些像素?

Lesson 02 left us with a triangle whose three vertices are already in screen space — plain 2D pixel coordinates, the perspective divide behind us. The rasterizer's job now is a coverage query: for each candidate pixel, is its center inside the triangle? Everything downstream (shading, texturing, depth) only happens at pixels the triangle actually covers, so this test runs an enormous number of times and must be cheap.

第 02 课给我们留下了一个三角形,它的三个顶点已在屏幕空间——就是普通的二维像素坐标,透视除法已在身后。光栅化器现在的活儿是一次覆盖查询:对每个候选像素,它的中心是否在三角形内?下游的一切(着色、纹理、深度)只在三角形真正覆盖的像素上发生,所以这个测试要跑极多次,必须便宜。

Here is the one primitive that does it. Given a directed edge from A to B and a query point P, the edge function is

下面就是完成它的那一个基本运算。给定一条从 A 指向 B 的有向边,以及查询点 P边函数

E(A, B, P) = (Px − Ax)(By − Ay) − (Py − Ay)(Bx − Ax).

Read it two ways. Algebraically it is the 2D cross product of (B − A) with (P − A), so its sign says which side of the infinite line AB the point lies on — the half-plane test. Geometrically its magnitude is twice the signed area of triangle ABP. One expression answers both "which side?" and "how much area?", and we will use both facts.

用两种方式来读它。代数上,它是 (B − A)(P − A) 的二维叉积,因此它的符号说明该点落在无限直线 AB 的哪一侧——即半平面测试。几何上,它的大小是三角形 ABP有符号面积的两倍。一个表达式同时回答“哪一侧?”与“多少面积?”,这两个事实我们都会用到。

A triangle is the intersection of three half-planes, one per edge. Walk its three vertices in a consistent order (say counter-clockwise) and evaluate the edge function for each directed edge against P. If the three results all share the same sign, P is on the interior side of every edge, so it is inside; if any sign disagrees, P is outside. That is the entire inside test — three multiply-adds and a sign check:

一个三角形是三个半平面的交集,每条边一个。以一致的顺序(比如逆时针)走过它的三个顶点,对每条有向边就 P 求边函数。如果三个结果同号,则 P 在每条边的内侧,故在内部;只要有一个符号不一致,P 就在外部。这就是整个内部测试——三次乘加加一次符号检查:

w0 = E(B, C, P) inside ⟺ (w0 ≥ 0 && w1 ≥ 0 && w2 ≥ 0) // CCW triangle w1 = E(C, A, P) ⟺ the three edge functions share sign w2 = E(A, B, P) w0 + w1 + w2 = 2 · Area(ABC) (a constant, > 0 for CCW)

One more thing keeps this from being wasteful. We do not test every pixel on screen against every triangle — that would be pixels × triangles, the very cost lesson 01 warned about. Instead we compute the triangle's bounding box: x from min to max of the three vertices, likewise y, clamped to the framebuffer. We scan only that rectangle. A triangle covering 40 pixels inside a 60-pixel box costs 60 coverage tests, not the whole screen — the widget below counts exactly these two numbers.

还有一件事让它不至于浪费。我们不会拿屏幕上每个像素去测试每个三角形——那会是像素 × 三角形,正是第 01 课警告过的开销。取而代之,我们计算三角形的包围盒x 取三顶点的 minmaxy 同理,并裁到帧缓冲范围内。我们只扫描那个矩形。一个在 60 像素盒子里覆盖 40 像素的三角形,花费 60 次覆盖测试,而非整屏——下面的控件正是数这两个数。

2 · Barycentric coordinates fall out for free重心坐标免费掉出来

We paid for three edge functions to decide coverage. Here is the bonus: those same three numbers, normalized, are the point's barycentric coordinates — the weights that write P as a blend of the three vertices. Recall each edge function is twice a signed area. The edge E(B,C,P) is twice the area of the sub-triangle PBC — the one opposite vertex A. Divide the three sub-areas by the whole triangle's area and you get three weights that sum to one:

我们为判定覆盖付了三个边函数的账。这里是赠品:这同样的三个数,归一化之后,就是该点的重心坐标——把 P 写成三个顶点的混合的那组权重。回忆每个边函数是有符号面积的两倍。边 E(B,C,P) 是子三角形 PBC 面积的两倍——就是对着顶点 A 的那个。把三个子面积各除以整个三角形的面积,就得到三个求和为一的权重:

α = w0 / (w0+w1+w2),   β = w1 / (w0+w1+w2),   γ = w2 / (w0+w1+w2),    α + β + γ = 1.
P = α·A + β·B + γ·C ,    and   α, β, γ ≥ 0  ⟺  P inside the triangle.

Read the two facts together. The sign test of section 1 is exactly the statement "all three barycentrics are non-negative" — coverage and interpolation are the same computation seen from two angles. And once P is covered, any per-vertex quantity — a color, a texture coordinate, a normal, a depth — is reconstructed at P by the same weights: v_P = α v_A + β v_B + γ v_C. A vertex with weight 1 and the others 0 reproduces that vertex; the centroid has all three equal to 1/3. This is the interpolation machinery lesson 05 needs — and lesson 05 will show it must be applied to 1/w, not screen values, to stay perspective-correct. For now: coverage buys us the weights, for free, at every covered pixel.

把这两个事实合起来读。第 1 节的符号测试恰恰就是“三个重心坐标都非负”这句话——覆盖与插值是同一计算的两个视角。而一旦 P 被覆盖,任何逐顶点的量——颜色、纹理坐标、法线、深度——都可用同一组权重在 P 处重建:v_P = α v_A + β v_B + γ v_C。某顶点权重为 1、其余为 0 就复现该顶点;重心处三者都等于 1/3。这正是第 05 课需要的插值机制——而第 05 课会说明它必须作用在 1/w 上、而非屏幕值上,才能保持透视校正。眼下只需记住:覆盖在每个被覆盖像素处免费买来了这组权重。

3 · The top-left rule — one edge, one owner左上规则——一条边,一个归属

Real meshes are watertight: adjacent triangles share edges exactly. That creates a boundary problem the inside test alone gets wrong. Consider a pixel center that lands precisely on the shared edge between two triangles. With the plain ≥ 0 test, both triangles report it inside — because the shared edge has edge-function value exactly 0 for each. The consequences are visible bugs:

真实网格是密封的:相邻三角形恰好共享边。这带来一个仅靠内部测试会出错的边界问题。考虑一个像素中心恰好落在两个三角形之间的共享边上。用朴素的 ≥ 0 测试,两个三角形都报告它在内——因为那条共享边对各自而言边函数值恰为 0。后果是可见的 bug:

The top-left rule (also called a fill rule) is a deterministic tie-break that gives every shared edge to exactly one of the two triangles. The convention: a pixel exactly on an edge is considered inside only if that edge is a top edge (horizontal, with the triangle below it) or a left edge (going downward, with the triangle to its right). For a covered-boundary case we accept the edge function when it is strictly positive, or zero on a top/left edge; we reject zero on any other edge. Because two triangles sharing an edge traverse it in opposite directions, exactly one of them sees it as top/left — so the pixel is claimed once. No gaps, no doubles, and — critically — the decision depends only on the edge's geometry, not on draw order, so the result is deterministic and consistent everywhere the edge appears.

左上规则(也称填充规则)是一个确定性的平局判定,把每条共享边恰好判给两个三角形中的一个。约定是:恰好落在边上的像素被视为在内,仅当该边是上边(水平、三角形在其下方)或左边(向下走、三角形在其右侧)。对覆盖到边界的情形,当边函数严格为正、在上/左边上为零时我们接受它;在任何其他边上为零则拒绝。因为共享一条边的两个三角形沿相反方向遍历它,其中恰有一个把它看作上/左边——于是该像素被认领一次。没有裂缝、没有重复,而且——关键在于——该判定只依赖边的几何,不依赖绘制顺序,所以结果是确定的,且在这条边出现的每一处都一致。

Why it must be deterministic为什么它必须是确定的

The top-left rule is not about which triangle "looks nicer" on the seam — it is about the two triangles agreeing without talking to each other. The rasterizer processes one triangle at a time and cannot know its neighbor exists. A rule based purely on edge orientation guarantees that when the neighbor is later rasterized, it independently reaches the opposite verdict on the shared pixel. This is the same theme as the z-buffer in section 4: replace a global, order-dependent decision with a purely local one.

左上规则的重点不在于接缝处哪个三角形"更好看"——而在于两个三角形在互不通信的前提下达成一致。光栅化器一次只处理一个三角形,根本不知道邻居的存在。一条纯粹基于边朝向的规则保证:当邻居随后被光栅化时,它会独立地对那个共享像素做出相反的裁决。这与第 4 节的深度缓冲是同一个主题:把一个全局的、依赖顺序的决策,换成一个纯局部的决策。

4 · Visibility with a depth buffer用深度缓冲解决可见性

Coverage tells us which pixels a triangle touches; it does not tell us which triangle the eye actually sees when several cover the same pixel. That is the visibility question from lesson 01, and rasterization needs an answer that fits its triangle-by-triangle loop.

覆盖告诉我们一个三角形碰到哪些像素;它没告诉我们当多个三角形覆盖同一像素时,眼睛实际看到的是哪一个。这就是第 01 课的可见性问题,而光栅化需要一个契合其“逐三角形”循环的答案。

The tempting classical answer is the painter's algorithm: sort all triangles back-to-front and draw them in that order, so nearer triangles paint over farther ones — like a painter laying down background before foreground. It is simple and it fails, for reasons that are structural, not fixable by a better sort:

诱人的经典答案是画家算法:把所有三角形按从远到近排序、再依序绘制,于是近的三角形盖在远的之上——就像画家先铺背景再画前景。它简单,却会失败,原因是结构性的,靠更好的排序补不了:

The fix is to make visibility a per-pixel, order-independent decision — the z-buffer (depth buffer). Allocate, alongside the color framebuffer, a second buffer holding one depth per pixel, all initialized to +∞ (the far plane). When a triangle covers a pixel, interpolate its depth there (barycentrically, from section 2) and compare against the stored depth. Keep the fragment — write its color and its depth — iff the new depth is smaller (nearer). Otherwise discard it. That is the whole algorithm:

解决办法是把可见性变成一个逐像素、与顺序无关的决定——即 z-buffer(深度缓冲)。在颜色帧缓冲之外,再分配一个每像素存一个深度的缓冲,全部初始化为 +∞(远平面)。当一个三角形覆盖某像素时,在该处插值它的深度(用第 2 节的重心坐标),并与已存深度比较。当且仅当新深度更小(更近)时保留该片元——写入它的颜色和深度;否则丢弃它。这就是全部算法:

for each triangle T:                       // any order — order does not matter
  for each pixel P in T's bounding box:
    if covered(T, P):                      // three edge functions share sign
      z = interpolate_depth(T, P)          // barycentric α,β,γ
      if z < zbuf[P]:                      // nearer than what's already there?
        color[P] = shade(T, P)             // keep this fragment
        zbuf[P]  = z

Read why it beats the painter. The comparison is O(1) per fragment and depends only on the pixel's own stored depth, so triangles may arrive in any order and the final image is identical — cyclic overlaps and interpenetration resolve automatically because the contest is settled independently at each pixel, exactly where order truly is per-pixel. The cost is memory (a full-resolution depth buffer) and bandwidth (a read-compare-maybe-write per fragment), both of which GPUs are built to absorb. The widget below runs precisely this loop and, when you disable the depth test, shows the "last-drawn wins" bug the z-buffer cures.

读懂它为何胜过画家算法。比较对每个片元是 O(1),且只依赖该像素自己存的深度,所以三角形可以按任意顺序到来而最终图像相同——循环遮挡与相互穿插会自动解决,因为这场竞争在每个像素处被独立裁定,恰好就在“顺序真正是逐像素”的地方。代价是内存(一个全分辨率深度缓冲)与带宽(每片元一次“读—比较—或许写”),二者 GPU 生来就能吸收。下面的控件正是跑这个循环,而当你关掉深度测试时,会显示 z-buffer 所治愈的“后画者胜”的 bug。

One catch remains, and it is the source of a classic artifact. The depth we store is screen-space z after the perspective divide, and that value is nonlinear in true camera distance: dividing by w ≈ Z packs most of the [0,1] depth range into the region right in front of the camera. So depth precision is lavish near the camera and starved far away. When two surfaces are nearly coplanar in the distance, their quantized depths can collide and the winner flickers pixel-to-pixel as the camera moves — z-fighting.

还剩一个陷阱,也是一个经典瑕疵的根源。我们存的深度是透视除法之后的屏幕空间 z,而这个值对真实相机距离是非线性的:除以 w ≈ Z[0,1] 深度范围的大部分挤到了相机正前方那一片。于是深度精度在近处极其充裕、在远处却匮乏。当远处两个表面近乎共面时,它们量化后的深度会撞车,随着相机移动,胜者在像素间闪烁——即 z-fighting

FixWhat it doesWhy it helps
Pull near/far togetherRaise the near plane, lower the far planePrecision is dominated by the near/far ratio; a tighter frustum spends bits where you look
Reversed-zMap near → 1, far → 0 and store with a float depth bufferFloat density near 0 cancels the 1/Z crowding, giving near-uniform precision
Depth bias / polygon offsetNudge coplanar geometry a hair in depthForces a deterministic winner for decals/shadow-map surfaces that are meant to coincide
修法做什么为何有效
拉近 near/far抬高近平面、压低远平面精度由 near/far 比值主导;更紧的视锥把比特花在你真正看的地方
反转 z(reversed-z)把 near → 1、far → 0,并用浮点深度缓冲存储浮点在接近 0 处的密度抵消了 1/Z 的拥挤,给出近乎均匀的精度
深度偏置 / 多边形偏移把共面几何在深度上轻推一丝为本就意在重合的贴花 / 阴影贴图表面强制出一个确定的胜者
Rasterize a triangle: edge functions, coverage, and the z-buffer光栅化一个三角形:边函数、覆盖与 z-buffer
A pixel grid. Triangle A (blue) has one movable vertex — drag vx, vy — and two fixed vertices; its bounding box (dashed) is the only region scanned. For each cell center the three edge functions run; covered cells fill blue. Enable the z-buffer to bring in a second, fixed triangle B (orange) at a nearer constant depth: where they overlap, the nearer surface wins. Turn the depth test off and B is simply drawn last, painting over A even where A is nearer — the painter's-algorithm bug. Counts are exact; nothing is random.一张像素网格。三角形 A(蓝)有一个可移动顶点——拖动 vx, vy——和两个固定顶点;它的包围盒(虚线)是唯一被扫描的区域。对每个格心运行三个边函数;被覆盖的格子填蓝。开启 z-buffer 会引入第二个固定的三角形 B(橙),处于更近的恒定深度:它们重叠处,更近的表面胜出。把深度测试关掉,B 只是被最后绘制,即使在 A 更近的地方也盖过 A——这就是画家算法的 bug。计数是精确的;没有任何随机。
Covered pixels (A)
Coverage tests (bbox)
Bbox fill ratio
Overlap winner
Show the core JS查看核心 JS
// Edge function: signed area / half-plane test (lesson 01, made real here).
function edge(ax,ay,bx,by,px,py){ return (px-ax)*(by-ay) - (py-ay)*(bx-ax); }

// Scan ONLY the bounding box, not the whole grid.
var minx = Math.floor(Math.min(Ax,Bx,Cx)), maxx = Math.ceil(Math.max(Ax,Bx,Cx));
var miny = Math.floor(Math.min(Ay,By,Cy)), maxy = Math.ceil(Math.max(Ay,By,Cy));
for (var y=miny; y<maxy; y++) for (var x=minx; x<maxx; x++){
  tests++;
  var px=x+0.5, py=y+0.5;                 // pixel CENTER
  var w0=edge(Bx,By,Cx,Cy,px,py), w1=edge(Cx,Cy,Ax,Ay,px,py), w2=edge(Ax,Ay,Bx,By,px,py);
  var inside = (w0>=0&&w1>=0&&w2>=0)||(w0<=0&&w1<=0&&w2<=0);
  if (!inside) continue;
  // z-buffer: keep the nearer surface, order-independent. Depth test OFF ⇒ last drawn wins.
  if (!depthTest || zA < zbuf[y*GW+x]) { color[y*GW+x] = colA; zbuf[y*GW+x] = zA; }
}

5 · Culling & clipping — skip work before it starts剔除与裁剪——在开工前就跳过

Two guards run before the coverage loop, each removing triangles that would otherwise waste it — and each reuses machinery we already have.

两道防线跑在覆盖循环之前,各自剔除本会白费循环的三角形——而且各自复用了我们已有的机制。

Back-face culling. A closed opaque object shows you only its front-facing triangles; the ones on the far side point away and are always hidden by the near side. We can detect them for free from the sign we already computed. In screen space, a triangle whose vertices we authored counter-clockwise has a positive total edge-function sum (twice its area); if that sum comes out negative, the triangle has been flipped by projection — it is facing away — and we discard it before scanning a single pixel. This is one comparison that can cull roughly half of a mesh's triangles. (The winding convention is a choice; you set which sign means "front" once, consistently.)

背面剔除。一个封闭不透明物体只让你看到它朝前的三角形;背面的那些朝向别处,总被近侧挡住。我们能从已经算好的符号里免费探出它们。在屏幕空间,我们按逆时针编写顶点的三角形有的边函数总和(其面积的两倍);如果这个和算出来是的,说明该三角形被投影翻了面——它朝向别处——于是在扫描任何一个像素之前就丢弃它。这是一次比较,能剔掉一个网格大约一半的三角形。(环绕约定是个选择;你一次性、一致地设定哪个符号代表“正面”即可。)

Near-plane clipping. Here is a subtler hazard tied straight to lesson 02's transform chain. The perspective divide is a division by w, and w is (essentially) the camera-space depth. A vertex behind or exactly on the camera plane has w ≤ 0. Divide by that and you get a division by zero or, worse, a sign flip that folds geometry from behind the eye into the visible image as a garbage triangle. So we must clip against the near plane in clip space, before the divide — cut each triangle along w = (a small positive) ε, producing one or two new triangles that lie entirely in front of the camera, and only then divide. This is exactly why lesson 02 insisted clip space exist as a distinct stage: it is the last place the divide is still safe to defer.

近平面裁剪。这里有个更隐蔽、直接系于第 02 课变换链的隐患。透视除法是除以 w,而 w(本质上)就是相机空间深度。位于相机平面之后或恰好之上的顶点有 w ≤ 0。除以它,你会得到除以零,或者更糟——一次符号翻转,把眼睛背后的几何折叠成一个垃圾三角形塞进可见图像。所以我们必须在裁剪空间、除法之前,对近平面做裁剪——沿 w =(一个小正数)ε 切开每个三角形,产出一或两个完全落在相机前方的新三角形,然后才做除法。这正是第 02 课坚持让裁剪空间作为独立阶段存在的原因:那是最后一处仍能安全推迟除法的地方。

The other four frustum planes (left, right, top, bottom) could also be clipped this way, but doing so for every triangle is wasteful when most are already on-screen. GPUs instead use a guard band: they clip only against the near plane (correctness — the divide demands it) and a region generously larger than the screen, then simply let the rasterizer's bounding-box clamp discard the off-screen pixels of anything within the guard band. Full clipping is reserved for triangles that also cross the guard band. Correctness where it is mandatory, cheap scissoring everywhere else.

其余四个视锥平面(左、右、上、下)也可以这样裁剪,但当大多数三角形本就在屏内时,对每个都这么做很浪费。GPU 转而使用护栏带(guard band):只对近平面(正确性——除法要求如此)以及一个比屏幕慷慨得多的区域做裁剪,然后干脆让光栅化器的包围盒裁剪去丢弃护栏带内之物的屏外像素。完整裁剪只留给同时越过护栏带的三角形。在必须之处保证正确,在其余处用廉价的裁切。

Where this points next接下来指向何处

We can now take a screen-space triangle and turn it into a set of covered pixels — fragments — each carrying interpolated barycentric weights and a depth that a z-buffer uses to keep the nearest surface, with the top-left rule sealing the seams and culling/clipping trimming the input. But a fragment so far is just a covered location with a flat color. Its real job is to sample surface detail: a color from a texture image, a normal from a normal map, a coordinate that varies smoothly across the triangle. Lesson 05 picks up the interpolation thread exactly where section 2 dropped it — and reveals the catch it flagged: interpolating screen-space values linearly is wrong under perspective, so we must interpolate attribute/w and 1/w and divide, giving perspective-correct interpolation and texture mapping.

现在我们能把一个屏幕空间三角形变成一组被覆盖的像素——片元——每个都带着插值好的重心权重和一个深度,z-buffer 用它保留最近的表面,左上规则封住接缝,剔除/裁剪修剪输入。但迄今为止一个片元只是一个带纯色的被覆盖位置。它真正的活儿是采样表面细节:从纹理图像取一个颜色、从法线贴图取一个法线、一个在三角形上平滑变化的坐标。第 05 课正好从第 2 节松手之处接起插值这条线——并揭示它标记过的那个陷阱:在透视下对屏幕空间值做线性插值是错的,所以我们必须插值 属性/w1/w 再相除,得到透视校正插值与纹理映射

Takeaway要点

Rasterization turns a screen-space triangle into covered pixels via the edge function E(A,B,P)=(Px−Ax)(By−Ay)−(Py−Ay)(Bx−Ax) — a signed area / half-plane test; a point is inside iff its three edge functions share sign, and we scan only the bounding box. Normalized, those three numbers are the barycentric coordinates α,β,γ ≥ 0, Σ = 1, the interpolation weights lesson 05 uses. The top-left rule gives each shared edge to exactly one triangle — deterministically, without triangles consulting each other — so no gaps or double-shading. For visibility, the painter's algorithm fails (cyclic overlaps, interpenetration, per-pixel order); the z-buffer keeps a fragment iff its depth is smaller, an O(1), order-independent per-pixel decision. Screen-z is nonlinear (crowded near the camera), causing z-fighting, fixed by tighter near/far or reversed-z. Finally, back-face culling reads the winding sign to drop away-facing triangles, and near-plane clipping must happen before the divide, because you cannot divide by w ≤ 0.

光栅化通过边函数 E(A,B,P)=(Px−Ax)(By−Ay)−(Py−Ay)(Bx−Ax) 把屏幕空间三角形变成被覆盖的像素——那是一个有符号面积 / 半平面测试;一个点在内部当且仅当它的三个边函数同号,而我们只扫描包围盒。归一化后,这三个数就是重心坐标 α,β,γ ≥ 0, Σ = 1,即第 05 课要用的插值权重。左上规则把每条共享边恰好判给一个三角形——确定地、无需三角形彼此商量——于是没有裂缝或重复着色。对于可见性,画家算法会失败(循环遮挡、相互穿插、逐像素顺序);z-buffer 当且仅当片元深度更小时保留它,是一个 O(1)、与顺序无关的逐像素决定。屏幕 z非线性的(在相机近处拥挤),引起 z-fighting,可用更紧的 near/far 或 reversed-z 修复。最后,背面剔除读环绕符号丢弃朝外的三角形,而近平面裁剪必须发生在除法之前,因为你无法除以 w ≤ 0

Interview prompts面试题