平行光、点光源与软边聚光

平行光、点光源与软边聚光:保留 LearnOpenGL 3.3 Core 正文机制,以 context—资源—结果合同、GPU 轨迹和章专属单故障完成可重放验收。

学习目标

  • 能比较平行光、点光源、聚光的字段、lightDir 来源与额外强度系数
  • 能实现点光源的距离衰减并根据目标覆盖范围选择 Kc、Kl、Kq
  • 能实现硬边聚光和内外锥软边聚光,并解释余弦切光角的比较方向
  • 能修改 light.position/direction/cutOff,把相机绑定的手电改成固定舞台追光
  • 能判断方向取反、衰减常数、切光角单位或内外锥顺序错误造成的画面问题

为什么太阳、灯泡和手电筒需要三种光源模型

前几章里,照亮立方体的那束光来历很含糊——我们只给了它一个位置或一个颜色,就开算了。可现实里的「灯」分好多种:晴天的太阳,光铺天盖地地平行打下来,你走到院子哪个角落都一样亮;屋里一只灯泡,凑近了刺眼、走远了昏暗,光是从那个点向四面八方散开的;手里一支手电筒,只照亮正前方一个圆圈,圈外漆黑一片。

这三种灯,照出来的味道天差地别,可前几章那套算法对它们一视同仁——结果就是不管想模拟太阳还是手电,画面都一个样,假得很。这章要解决的就是:怎么让同一套光照算法,分别演好太阳、灯泡、手电这三种灯

好消息是,三块怎么算(环境光、漫反射、镜面那一套)一点没变,变的只是「光是怎么来到这块表面的」——是平行铺过来的,还是从一个点散开、走远了就弱,还是被收束成一个锥形。把这三种「光的来路」讲清楚,你的场景里就能同时立起太阳、台灯和手电筒。

投光物:光是怎么投到物体上的

把「一盏灯以什么方式把光投向物体」这件事,图形学里统称 (light caster)。本章讲三种最常用的投光物,它们的区别只在「光的来路」:

  • 平行光:光线全都平行,像无限远处的太阳,整个场景一个方向、一样亮;
  • 点光源:从一个点向四面八方发光,像灯泡,离得越远越暗(衰减);
  • 聚光:从一个点沿一个方向射出一个锥形,像手电筒,锥内才亮、锥外全黑。

下面这张图把三者的几何差别并排画清楚——注意它们的箭头形态完全不同:

要强调的是:这三种投光物都不改动 Phong 三块的算法。环境光、漫反射 N·L、镜面 pow(R·V, n) 全照旧;它们唯一动的,是「怎么得到那个指向光源的方向 lightDir」,以及「要不要再乘一个随距离 / 随角度变化的系数」。接下来三节,一种一种拆。

平行光:只有方向,没有位置

第一种最省事。(directional light)模拟的是太阳那样的光源:因为离得无穷远,打到地面的所有光线几乎完全平行,朝向都一致。这带来一个关键简化——平行光没有「位置」,只有「方向」

回忆基础光照章:漫反射要的是从表面指向光源的方向 lightDir。点光源那时是用「光源位置减表面位置」算出来的。可平行光没有位置,那 lightDir 哪来?答案是它直接由光源的 给出。注意有个取负的小弯:光源给的 direction 是「光往哪儿射」,而我们要的是「从表面望向光源」,两者正好相反,所以 lightDir = normalize(-light.direction)。没有了位置,平行光对场景里所有片段都用同一个 lightDir,而且完全不衰减——太阳照院子,这头那头一样亮。

点光源:有位置,离得越远越暗

第二种是你在前几章其实已经用过的 (point light):它在场景里有一个具体位置,向四面八方发光,本章主 Demo 里那个绕着立方体转的小亮块就是它。和平行光不同,它的 lightDir 仍是「光源位置减表面位置再归一化」。

点光源真正的新东西是 (attenuation):现实里灯泡近处刺眼、走远了昏暗,光强随距离掉下去。我们用一个随距离变小的系数来模拟它——离得越远,系数越小,乘到漫反射和镜面上,那块表面就越暗。这个系数怎么算(那条 1/(Kc + Kl·d + Kq·d²) 公式)是本章数学的重头,留到 §4 细讲,先记住直觉:点光源 = 有位置的灯 + 一个「越远越暗」的乘数。等下在 Demo 里你就能亲手把光源转远,看立方体暗下去。

聚光:只照亮一个锥形

第三种最有戏剧性。(spotlight)模拟手电筒、舞台追光灯:它既有位置、又有一个照射方向,但光被收束成一个锥形——只有落在锥内的片段才被照亮,锥外一片黑。

怎么判断一个片段「在不在锥内」?靠角度。聚光有一个 (cutoff)——光锥的半张角。算的时候:取「从聚光指向这个片段的方向」和「聚光自己的朝向」的夹角,夹角小于切光角(更靠近锥中心轴)就在锥内、照亮,否则在锥外、全黑。这里有个工程上的常用技巧:不直接比角度,而是比角度的余弦(点乘天然就给余弦,省一次反三角函数),§4 会把这个「比余弦」的方向掰开讲。

内外圆锥:别让光锥边缘是一刀切的硬边

如果只用一个切光角「一刀切」,锥内全亮、锥外全黑,边缘会是一道生硬刺眼的圆边——一看就假。真手电的光圈边缘是慢慢暗下去的。解决办法是用 :再加一个更大的「外圆锥」切光角,规定内圆锥以内全亮、外圆锥以外全黑,两者之间的环带里亮度从 1 平滑过渡到 0。这样光圈边缘就柔和了。具体那条 clamp 公式见 §6,直觉很简单:在过渡带里,越靠内越亮、越靠外越暗,线性插一下、夹到 0~1。

两个公式:衰减和「比余弦」

本章数学不多,就两处:点光源的衰减公式,和聚光「为什么比余弦不比角度」。先约定本节符号:

  • dd:片段到光源的距离(用 length(光源位置 - 片段位置) 算出)
  • KcK_c:衰减的常数项(constant),通常取 1
  • KlK_l:衰减的一次项系数(linear,线性衰减)
  • KqK_q:衰减的二次项系数(quadratic,二次衰减)
  • θ\theta:从光源指向片段的方向与聚光朝向之间的夹角
  • ϕ\phi:切光角(光锥的半张角)

点光源衰减

点光源先由位置差同时得到未归一化方向、距离与单位光线方向:

q=plightpfrag,d=q,L^=qd\vec{q}=\vec{p}_{light}-\vec{p}_{frag},\qquad d=\lVert\vec q\rVert,\qquad \hat L=\frac{\vec q}{d}

这个式子在说:不要先把位置差归一化再求距离,否则长度已经变成 1。保留原始向量 qlength(q),再 normalize(q) 得到只表达方向的 lightDir

attenuation=1Kc+Kld+Kqd2\text{attenuation} = \frac{1}{K_c + K_l \cdot d + K_q \cdot d^2}

这个式子在说:拿 1 去除以「一个常数项 + 一个随距离线性增长的项 + 一个随距离平方增长的项」。距离 dd 越大,分母越大,整个分数越小——这就是「越远越暗」。三项各管一段:KcK_c(常数)保证 d=0d=0 时分母不为 0、系数不会爆掉(通常就设 1,使近处约等于满亮);KldK_l \cdot d 让中距离平稳变暗;Kqd2K_q \cdot d^2 在远处发力——因为平方涨得快,它让远处衰减得又快又狠,正是真实灯光「先慢后陡」那种落法的来源。下面这张图把这条曲线的形状画了出来:

这三个常数不用自己硬凑,社区有一张按「希望照多远」给出的经验取值表,直接查用:

想覆盖的距离KcK_cKlK_lKqK_q
约 321.00.140.07
约 501.00.090.032
约 1001.00.0450.0075

想让灯照得更远,就选距离更大那一行(KlK_lKqK_q 更小,衰减更慢)。

聚光为什么「比余弦」:聚光要判断片段在不在锥内,本质是比「指向片段的方向」与「聚光朝向」的夹角 θ\theta 和切光角 ϕ\phi 谁大谁小——θ<ϕ\theta < \phi 就在锥内。但代码里我们不直接比角度,而是比它们的余弦

cosθ>cosϕ    θ<ϕ(当 θ,ϕ[0°,90°])\cos\theta > \cos\phi \iff \theta < \phi \quad (\text{当 } \theta, \phi \in [0°, 90°])

这个式子在说:在 0°~90° 这段里,余弦是单调递减的(角度越大、余弦越小),所以「夹角更小」完全等价于「余弦更大」。好处是:cosθ\cos\theta 不用反三角函数现算——它正是「指向片段方向」与「聚光朝向」两个单位向量的点乘(基础光照章讲过,单位向量点乘 = 夹角余弦);而 cosϕ\cos\phi 在 CPU 侧对切光角取一次 cos 预先算好传进来即可。于是片段着色器里只要一句点乘、一次比较,省掉了昂贵的 acos。这就是 §3 说的「比余弦不比角度」的来历。

内外锥之间的软边强度为:

Icone=clamp ⁣(cosθcosϕoutercosϕinnercosϕouter,0,1)I_{cone}=\operatorname{clamp}\!\left(\frac{\cos\theta-\cos\phi_{outer}}{\cos\phi_{inner}-\cos\phi_{outer}},0,1\right)

这个式子在说:内锥余弦更大、外锥余弦更小;片段从外锥走向内锥时分子从 0 增到完整区间,强度平滑地从 0 变 1。若直接传角度或把内外顺序颠倒,分母符号就会错误。

动手:把点光源转远,看它暗下去

在完整 Demo 前,先把三种光按“只换光的来路”的顺序逐项实现:

分步1 / 3

平行光:只验证固定方向

使用 normalize(-light.direction),不访问 position,也不计算衰减。场景中所有片段共享同一个 lightDir。

下面这个 Demo 你在光照篇里见过——一个带法线的立方体,被一个绕着它公转的点光源照亮(那个小亮块就是光源位置)。它正是本章三种投光物里的点光源:有位置、向四面八方发光。这一节就借它把「衰减」看个明白。

Demo 默认就是点光源的完整 Phong。点画布下方的「从头分步看」可把这套 Phong 拆开逐项回顾:环境 → 漫反射 → 镜面——这正是三种投光物共用的底座,等会儿代码里平行光、聚光跟它的差别只在「怎么得到 lightDir」和「要不要乘衰减」。

猜一猜:拖光源方位让那个小亮块绕到立方体最远的一侧,朝你这面会变亮还是变暗?再想一步——如果这盏灯的「越远越暗」掉得更狠(二次项更大),同样转到远处,会比现在更黑还是更亮?先动手拖一拖,再看下面三种光的代码。

可交互

光照演示加载中…

把光源转到远侧,朝你这面会变暗:距离 dd 变大,衰减系数 1/(Kc+Kld+Kqd2)1/(K_c + K_l d + K_q d^2) 变小,乘到漫反射和镜面上,这块表面就暗了——这就是点光源区别于平行光(太阳那样远近一个样)的关键。注意这个 Demo 是点光源;平行光会让整个立方体不论远近一律均匀受光,聚光则只在一个锥形圈内才亮——这两种在下面的代码里就能看出差在哪几行。

代码逐段拆解:三种光差在哪

三种投光物共用同一套 Phong(环境 + 漫反射 + 镜面,见基础光照章),差别只在怎么得到 lightDir、以及要不要再乘一个系数。下面三段把这三处差异单独拎出来对照,C++/OpenGL 与 WebGL2/TS 互为镜像。

平行光:lightDir 直接来自方向,取负即可

平行光的 Light 结构体里没有 position,只有 direction。片段着色器算 lightDir 时不再去减位置,而是直接把光的射出方向取负-light.direction)得到「指向光源」的方向,再归一化。算完 Phong 三块就结束——不乘任何衰减

#version 330 core
struct Light {
  vec3 direction;   // 平行光:只有方向,没有 position!
  vec3 ambient, diffuse, specular;
};
uniform Light light;
// …in FragPos/Normal、uniform viewPos 等照旧…
void main() {
  vec3 norm = normalize(Normal);
  vec3 lightDir = normalize(-light.direction);  // 取负 = 指向光源
  // …接着照常算 ambient / diffuse(N·L) / specular,最后相加
  // 平行光不衰减,到此为止
}

唯一要点是那个取负light.direction 是光「射出去」的方向,漫反射要的却是「望向光源」的方向,两者相反。CPU 侧把方向当 uniform 传进来即可(如太阳光 (-0.2, -1.0, -0.3),斜着往下打)。两端 GLSL 只差版本声明:

点光源:照常求方向,再额外算距离与衰减

点光源的结构体把 direction 换回 position,并多带三个衰减常数 constant / linear / quadraticlightDir 回到「位置减位置」的老办法;新增的是先用 length 求出距离 d,套 §4 公式算出 attenuation,最后把它乘到漫反射和镜面上(环境光通常也乘,让整盏灯一起变暗)。

struct Light {
  vec3 position;                       // 点光源:有位置
  vec3 ambient, diffuse, specular;
  float constant, linear, quadratic;   // 衰减三常数 Kc/Kl/Kq
};
// …main 里:norm、lightDir 照常…
vec3 lightDir = normalize(light.position - FragPos);  // 位置减位置
// 距离 + 衰减系数(§4 那条公式)
float dist = length(light.position - FragPos);
float attenuation = 1.0 / (light.constant
                  + light.linear * dist
                  + light.quadratic * dist * dist);
// 算完 ambient/diffuse/specular 后,整体乘衰减:
// ambient *= attenuation; diffuse *= attenuation; specular *= attenuation;

这一段两端一字不差——纯计算,没有任何 API 差异。length(light.position - FragPos) 就是符号表里的 ddattenuation 就是 §4 那条分式,把它乘到三块上,灯就有了「越远越暗」。三个常数由 CPU 按 §4 取值表传进来:

聚光:先比切光角,再用内外锥平滑

聚光结构体最复杂:既有 position、又有照射方向 direction,外加两个切光角 cutOff(内锥)、outerCutOff(外锥)——注意这俩存的是角度的余弦值,不是角度本身。算法是:先和点光源一样求 lightDir,再求「指向片段方向」与「聚光朝向」的点乘 theta(即 cosθ\cos\theta),用它和两个切光角的余弦比,clamp 出一个 0~1 的强度,乘到光照上。

struct Light {
  vec3 position, direction;            // 聚光:位置 + 朝向
  float cutOff, outerCutOff;           // 内锥、外锥(存的是余弦值!)
  // …ambient/diffuse/specular、衰减常数照旧…
};
vec3 lightDir = normalize(light.position - FragPos);
// theta = cosθ:指向片段方向 vs 聚光朝向(-direction 取负转成「望向锥顶」)
float theta = dot(lightDir, normalize(-light.direction));
// 内外锥平滑:环带里把强度从 1 线性插值到 0,clamp 夹住
float epsilon = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);
// 把 intensity 乘到 diffuse / specular 上(环境光通常不乘,锥外仍留点底光)
// diffuse *= intensity; specular *= intensity;

这段两端也完全一样theta 是 §4 的 cosθ\cos\theta;因为 cutOff / outerCutOff 存的是余弦、且余弦在锥内更大,所以 theta > cutOff 就是「在内锥里」。epsilon 是内外锥余弦之差,(theta - outerCutOff)/epsilon 把过渡带映到 0~1,clamp 把内锥里超过 1 的、外锥外低于 0 的都夹住——这正是 §3 说的「越靠内越亮、越靠外越暗」。关键的 CPU 侧动作:

三段对比下来一目了然:变的只有 lightDir 怎么来、以及乘不乘衰减 / 锥形强度,Phong 三块原封不动。把三者写进同一个着色器、按光源类型分支选用,就能在一个场景里同时点亮太阳、灯泡和手电——下一章「多光源」正是干这个。完整可运行源码将在章末提供(M5)。

容易踩的坑

小结

  • 投光物决定「光怎么来」;三种共用同一套 Phong,只换 lightDir 的来路和要不要乘系数
  • 平行光(太阳)只有方向、没位置、不衰减,lightDir = normalize(-light.direction)(记得取负)
  • 点光源(灯泡)有位置、随距离衰减:系数 1/(Kc + Kl·d + Kq·d²),乘到漫反射 / 镜面上,越远越暗
  • 衰减三常数:Kc 保底(取 1)、Kl 线性、Kq 二次(远处衰减更狠);按覆盖距离查取值表
  • 聚光(手电)有位置 + 方向 + 切光角,比余弦判锥内外,再用内外圆锥 clamp 平滑边缘,消硬边

练习

问题 1(改 Demo 代码 / 观察题) 在上面的点光源 Demo 里,把光源转到离立方体最远的一侧,观察朝你这面的明暗。然后回答:这个「越远越暗」的效果,在着色器里是哪一个量在起作用?如果想让它在同样距离下变得更暗(衰减更狠),该调大 §4 公式里的哪个常数?为什么是它?

问题 2(独立实现题) 不看上面的代码,写出平行光片段着色器里求 lightDir 的那一行,以及点光源多出来的「距离 + 衰减」那几行(已知结构体 light 含相应字段、FragPos 有)。说清两者关键差异。

问题 3(问答题) 某人做聚光,只用了一个切光角 cutOffif (theta > light.cutOff) { 正常照亮 } else { 全黑 }。画面里光圈边缘出现了什么毛病?为什么?正确做法是什么?

名词解释

本章出现的专业名词,用大白话再讲一遍。

投光物

对「光怎么投射到物体上」的统称。同一套 Phong 光照算法可以接上不同类型的投光物来模拟不同的灯:平行光(太阳)、点光源(灯泡)、聚光(手电筒)。它们只改变「光从哪儿、以什么方式来」,不改变环境光 / 漫反射 / 镜面三块的算法。详见本章「投光物」一节。

平行光

光线全部互相平行、来自无限远处的投光物,用来模拟太阳。它只有一个「方向」、没有具体位置,因此场景里每个片段收到的光方向都一样、强度也不随距离衰减。代码里 lightDir = normalize(-light.direction)(注意取负)。详见本章「平行光」一节。

光线方向

平行光自带的一个方向向量,表示光「射出去」的朝向(比如太阳光斜着往下打就是一个朝下偏的向量)。注意它是「光射出的方向」,而漫反射要的是「指向光源的方向」,正好相反,所以用前要取负normalize(-light.direction)。详见本章「平行光」一节。

点光源

位于场景中某个具体位置、向四面八方发光的投光物,用来模拟灯泡、火把这类有限光源。它有位置(lightDir 用「光源位置减表面位置」算),光的强度随片段到它的距离增大而衰减。详见本章「点光源」一节。

衰减

点光源(和聚光)的强度随距离减弱的现象。离光源越远越暗,用公式 1/(Kc + Kl·d + Kq·d²) 算出一个 0~1 的系数(d 是片段到光源的距离),把它乘到漫反射和镜面上。Kc 常数保底、Kl 线性、Kq 二次(远处衰减更狠)。详见本章「点光源」一节与 §4。

聚光

位于某个位置、只沿一个方向、把光收束成一个锥形射出的投光物,用来模拟手电筒、舞台追光灯。锥内才被照亮、锥外全黑,由「切光角」决定锥有多宽。详见本章「聚光」一节。

切光角

聚光锥的「半张角」,决定光锥有多宽。实际代码里通常不存角度本身,而是存这个角的余弦值,再拿「从光源指向片段的方向」和「聚光朝向」的点乘(也是个余弦)去和它比较——点乘更大说明夹角更小、更靠近锥中心,就在锥内。详见本章「聚光」一节与 §4。

内外圆锥

聚光的两个切光角:内圆锥(cutOff)以内全亮、外圆锥(outerCutOff)以外全黑,两者之间的环形过渡带里亮度从 1 平滑降到 0。靠 clamp((θ - 外锥) / (内锥 - 外锥), 0, 1) 算出这个 0~1 的强度系数,消除生硬的光圈硬边。详见本章「内外圆锥」一节。

版本、来源与运行边界

本章以 Joey de Vries 的 LearnOpenGL 原章 为授权改编依据,教学运行基线是 OpenGL 3.3 Core Profile。Khronos 当前发布的规范参照是 OpenGL 4.6 Core Profile;这里用 4.6 规范核查术语和状态合同,但不把 4.6 API 偷偷倒填为原教程内容。GLFW、GLAD、Assimp 与驱动版本都属于运行环境,不能拿“编译通过”替代对 context、资源和 framebuffer 结果的验证。

正式概念与状态责任

  • directional light:在“平行光、点光源与软边聚光”中由当前 Light 结构体与每片段光源求值函数负责解释其输入、受控状态和可观察结果;运行时以光源类型、L、距离、cutoff cosine、attenuation、intensity 与像素定位它的第一处变化。
  • point light:在“平行光、点光源与软边聚光”中由当前 Light 结构体与每片段光源求值函数负责解释其输入、受控状态和可观察结果;运行时以光源类型、L、距离、cutoff cosine、attenuation、intensity 与像素定位它的第一处变化。
  • spotlight:在“平行光、点光源与软边聚光”中由当前 Light 结构体与每片段光源求值函数负责解释其输入、受控状态和可观察结果;运行时以光源类型、L、距离、cutoff cosine、attenuation、intensity 与像素定位它的第一处变化。
  • attenuation:在“平行光、点光源与软边聚光”中由当前 Light 结构体与每片段光源求值函数负责解释其输入、受控状态和可观察结果;运行时以光源类型、L、距离、cutoff cosine、attenuation、intensity 与像素定位它的第一处变化。

章专属 OpenGL 状态实验

先预测“按光源类型构造 L,应用距离衰减或锥角强度,再计算 Phong 项”发生后,当前 Light 结构体与每片段光源求值函数应怎样改变direction/position、距离、衰减系数、内外 cutoff 和光照强度;再操作三个实验。实验不生成变化率或正确率等虚构总分,只显示真实 GL 状态、资源、命令和可观察结果。

实验一:Context—资源—结果合同

选择任一正式概念与基线/单故障场景,核对它是否进入本章状态合同。正式概念只有同时出现在解释、可视状态和交付证据中才算覆盖。

Context · resource · observable result

平行光、点光源与软边聚光:状态合同

在同一 Phong 核心上区分平行光、点光与软边聚光的方向和衰减合同

验证场景

官方教程正式概念

logl-13 · 基线帧

directional light固定 context、资源内容与输入事件,执行“按光源类型构造 L,应用距离衰减或锥角强度,再计算 Phong 项”

状态所有者当前 Light 结构体与每片段光源求值函数
受控状态/资源direction/position、距离、衰减系数、内外 cutoff 和光照强度
触发命令按光源类型构造 L,应用距离衰减或锥角强度,再计算 Phong 项

冻结输入:directional light

当前 Light 结构体与每片段光源求值函数记录direction/position、距离、衰减系数、内外 cutoff 和光照强度

结果:得到可重复的初始 GL 状态与资源身份

观测:光源类型、L、距离、cutoff cosine、attenuation、intensity 与像素中的初始快照

预期:当前 Light 结构体与每片段光源求值函数得到可复查结果,并持续满足“方向光不读距离;点光衰减有限;聚光 inner cosine 大于 outer cosine”

实验二:CPU 命令到 GPU 结果的五段轨迹

逐段执行“按光源类型构造 L,应用距离衰减或锥角强度,再计算 Phong 项”,在每一步记录资源身份、状态变化与第一个可观察结果,并持续核对“方向光不读距离;点光衰减有限;聚光 inner cosine 大于 outer cosine”。

CPU command · GL state · GPU result

平行光、点光源与软边聚光:五段轨迹

选择一段命令—资源—结果1 / 5

当前观测:光源类型、L、距离、cutoff cosine、attenuation、intensity 与像素中的初始快照

不变量:方向光不读距离;点光衰减有限;聚光 inner cosine 大于 outer cosine

实验三:单故障与同输入恢复

注入“把角度值直接与点积比较且内外 cutoff 反置,软边区间符号错误”,保存首个分岔;撤销后沿用完全相同的 context、资源内容、uniform 和 draw 输入重放。只有光源类型、L、距离、cutoff cosine、attenuation、intensity 与像素一起恢复才算修复。

Single fault · first divergence · replay

平行光、点光源与软边聚光:反例与恢复

故障:把角度值直接与点积比较且内外 cutoff 反置,软边区间符号错误

1. 冻结输入一致

第 1 次沿用同一 context、资源、uniform 与 draw 输入

2. 注入单故障一致

保持其余输入不变,仅注入“把角度值直接与点积比较且内外 cutoff 反置,软边区间符号错误”

3. 定位首差一致

方向光不读距离;点光衰减有限;聚光 inner cosine 大于 outer cosine

4. 清理并重放一致

光源类型、L、距离、cutoff cosine、attenuation、intensity 与像素

最小可重放检查

unit: logl-13
owner: 当前 Light 结构体与每片段光源求值函数
state_or_resource: direction/position、距离、衰减系数、内外 cutoff 和光照强度
command: 按光源类型构造 L,应用距离衰减或锥角强度,再计算 Phong 项
pass_invariant: 方向光不读距离;点光衰减有限;聚光 inner cosine 大于 outer cosine
single_fault: 把角度值直接与点积比较且内外 cutoff 反置,软边区间符号错误
required_evidence: 光源类型、L、距离、cutoff cosine、attenuation、intensity 与像素

复核者先仅依据以上合同写出预期,再运行基线、单故障和清理后重放。若两次基线的资源身份、首个状态变化或 framebuffer 结果不同,必须保留差异,不能用最终截图相似掩盖中间状态错误。

出处声明

本文为改编重写,改编自 Joey de Vries 的 LearnOpenGL。原文:learnopengl.com

原作及译作以 CC BY-NC 4.0 协议授权,本改编版同样遵循该协议(署名—非商业性使用)。

讨论

评论区加载中…