平行光、点光源与软边聚光
平行光、点光源与软边聚光:保留 LearnOpenGL 3.3 Core 正文机制,以 context—资源—结果合同、GPU 轨迹和章专属单故障完成可重放验收。
学习目标
- 能比较平行光、点光源、聚光的字段、lightDir 来源与额外强度系数
- 能实现点光源的距离衰减并根据目标覆盖范围选择 Kc、Kl、Kq
- 能实现硬边聚光和内外锥软边聚光,并解释余弦切光角的比较方向
- 能修改 light.position/direction/cutOff,把相机绑定的手电改成固定舞台追光
- 能判断方向取反、衰减常数、切光角单位或内外锥顺序错误造成的画面问题
为什么太阳、灯泡和手电筒需要三种光源模型
前几章里,照亮立方体的那束光来历很含糊——我们只给了它一个位置或一个颜色,就开算了。可现实里的「灯」分好多种:晴天的太阳,光铺天盖地地平行打下来,你走到院子哪个角落都一样亮;屋里一只灯泡,凑近了刺眼、走远了昏暗,光是从那个点向四面八方散开的;手里一支手电筒,只照亮正前方一个圆圈,圈外漆黑一片。
这三种灯,照出来的味道天差地别,可前几章那套算法对它们一视同仁——结果就是不管想模拟太阳还是手电,画面都一个样,假得很。这章要解决的就是:怎么让同一套光照算法,分别演好太阳、灯泡、手电这三种灯。
好消息是,三块怎么算(环境光、漫反射、镜面那一套)一点没变,变的只是「光是怎么来到这块表面的」——是平行铺过来的,还是从一个点散开、走远了就弱,还是被收束成一个锥形。把这三种「光的来路」讲清楚,你的场景里就能同时立起太阳、台灯和手电筒。
投光物:光是怎么投到物体上的
把「一盏灯以什么方式把光投向物体」这件事,图形学里统称 ↡对「光怎么投射到物体上」的统称。同一套 Phong 光照算法,可以接上不同类型的投光物来模拟不同的灯:平行光(太阳)、点光源(灯泡)、聚光(手电筒)。它们只改变「光从哪儿、以什么方式来」,不改变环境光/漫反射/镜面三块的算法。(light caster)。本章讲三种最常用的投光物,它们的区别只在「光的来路」:
- 平行光:光线全都平行,像无限远处的太阳,整个场景一个方向、一样亮;
- 点光源:从一个点向四面八方发光,像灯泡,离得越远越暗(衰减);
- 聚光:从一个点沿一个方向射出一个锥形,像手电筒,锥内才亮、锥外全黑。
下面这张图把三者的几何差别并排画清楚——注意它们的箭头形态完全不同:
要强调的是:这三种投光物都不改动 Phong 三块的算法。环境光、漫反射 N·L、镜面 pow(R·V, n) 全照旧;它们唯一动的,是「怎么得到那个指向光源的方向 lightDir」,以及「要不要再乘一个随距离 / 随角度变化的系数」。接下来三节,一种一种拆。
平行光:只有方向,没有位置
第一种最省事。↡一种光线全部互相平行、来自无限远处的投光物,用来模拟太阳。它只有一个「方向」、没有具体位置,因此场景里每个片段收到的光方向都一样、强度也不随距离衰减。(directional light)模拟的是太阳那样的光源:因为离得无穷远,打到地面的所有光线几乎完全平行,朝向都一致。这带来一个关键简化——平行光没有「位置」,只有「方向」。
回忆基础光照章:漫反射要的是从表面指向光源的方向 lightDir。点光源那时是用「光源位置减表面位置」算出来的。可平行光没有位置,那 lightDir 哪来?答案是它直接由光源的 ↡平行光自带的一个方向向量,表示光「射出去」的朝向(比如太阳光斜着往下打就是一个朝下偏的向量)。注意它是「光射出的方向」,而漫反射要的是「指向光源的方向」,所以用前要取负:lightDir = normalize(-light.direction)。给出。注意有个取负的小弯:光源给的 direction 是「光往哪儿射」,而我们要的是「从表面望向光源」,两者正好相反,所以 lightDir = normalize(-light.direction)。没有了位置,平行光对场景里所有片段都用同一个 lightDir,而且完全不衰减——太阳照院子,这头那头一样亮。
点光源:有位置,离得越远越暗
第二种是你在前几章其实已经用过的 ↡一种位于场景中某个具体位置、向四面八方发光的投光物,用来模拟灯泡、火把这类有限光源。它有位置,光的强度随片段到它的距离增大而减弱(衰减)。(point light):它在场景里有一个具体位置,向四面八方发光,本章主 Demo 里那个绕着立方体转的小亮块就是它。和平行光不同,它的 lightDir 仍是「光源位置减表面位置再归一化」。
点光源真正的新东西是 ↡点光源(和聚光)的强度随距离减弱的现象。离光源越远越暗,用公式 1/(Kc + Kl·d + Kq·d²) 算出一个 0~1 的系数(d 是片段到光源的距离),把它乘到漫反射和镜面上。让灯泡有「近处刺眼、远处昏暗」的真实感。(attenuation):现实里灯泡近处刺眼、走远了昏暗,光强随距离掉下去。我们用一个随距离变小的系数来模拟它——离得越远,系数越小,乘到漫反射和镜面上,那块表面就越暗。这个系数怎么算(那条 1/(Kc + Kl·d + Kq·d²) 公式)是本章数学的重头,留到 §4 细讲,先记住直觉:点光源 = 有位置的灯 + 一个「越远越暗」的乘数。等下在 Demo 里你就能亲手把光源转远,看立方体暗下去。
聚光:只照亮一个锥形
第三种最有戏剧性。↡一种位于某个位置、只沿一个方向、把光收束成一个锥形射出的投光物,用来模拟手电筒、舞台追光灯。锥内才被照亮、锥外全黑,由「切光角」决定锥有多宽。(spotlight)模拟手电筒、舞台追光灯:它既有位置、又有一个照射方向,但光被收束成一个锥形——只有落在锥内的片段才被照亮,锥外一片黑。
怎么判断一个片段「在不在锥内」?靠角度。聚光有一个 ↡聚光锥的「半张角」,决定光锥有多宽。实际代码里通常不存角度本身,而是存这个角的余弦值,再拿「从光源指向片段的方向」和「聚光朝向」的点乘(也是个余弦)去和它比较——点乘更大说明夹角更小、更靠近锥中心,就在锥内。(cutoff)——光锥的半张角。算的时候:取「从聚光指向这个片段的方向」和「聚光自己的朝向」的夹角,夹角小于切光角(更靠近锥中心轴)就在锥内、照亮,否则在锥外、全黑。这里有个工程上的常用技巧:不直接比角度,而是比角度的余弦(点乘天然就给余弦,省一次反三角函数),§4 会把这个「比余弦」的方向掰开讲。
内外圆锥:别让光锥边缘是一刀切的硬边
如果只用一个切光角「一刀切」,锥内全亮、锥外全黑,边缘会是一道生硬刺眼的圆边——一看就假。真手电的光圈边缘是慢慢暗下去的。解决办法是用 ↡聚光的两个切光角:内圆锥(cutOff)以内全亮,外圆锥(outerCutOff)以外全黑,两者之间的环形过渡带里亮度从 1 平滑降到 0。靠 clamp((θ-外锥)/(内锥-外锥), 0, 1) 算出这个 0~1 的强度系数,消除生硬的光圈硬边。:再加一个更大的「外圆锥」切光角,规定内圆锥以内全亮、外圆锥以外全黑,两者之间的环带里亮度从 1 平滑过渡到 0。这样光圈边缘就柔和了。具体那条 clamp 公式见 §6,直觉很简单:在过渡带里,越靠内越亮、越靠外越暗,线性插一下、夹到 0~1。
两个公式:衰减和「比余弦」
本章数学不多,就两处:点光源的衰减公式,和聚光「为什么比余弦不比角度」。先约定本节符号:
- :片段到光源的距离(用
length(光源位置 - 片段位置)算出) - :衰减的常数项(constant),通常取 1
- :衰减的一次项系数(linear,线性衰减)
- :衰减的二次项系数(quadratic,二次衰减)
- :从光源指向片段的方向与聚光朝向之间的夹角
- :切光角(光锥的半张角)
点光源衰减:
点光源先由位置差同时得到未归一化方向、距离与单位光线方向:
这个式子在说:不要先把位置差归一化再求距离,否则长度已经变成 1。保留原始向量 q 求 length(q),再 normalize(q) 得到只表达方向的 lightDir。
这个式子在说:拿 1 去除以「一个常数项 + 一个随距离线性增长的项 + 一个随距离平方增长的项」。距离 越大,分母越大,整个分数越小——这就是「越远越暗」。三项各管一段:(常数)保证 时分母不为 0、系数不会爆掉(通常就设 1,使近处约等于满亮); 让中距离平稳变暗; 在远处发力——因为平方涨得快,它让远处衰减得又快又狠,正是真实灯光「先慢后陡」那种落法的来源。下面这张图把这条曲线的形状画了出来:
这三个常数不用自己硬凑,社区有一张按「希望照多远」给出的经验取值表,直接查用:
| 想覆盖的距离 | |||
|---|---|---|---|
| 约 32 | 1.0 | 0.14 | 0.07 |
| 约 50 | 1.0 | 0.09 | 0.032 |
| 约 100 | 1.0 | 0.045 | 0.0075 |
想让灯照得更远,就选距离更大那一行(、 更小,衰减更慢)。
聚光为什么「比余弦」:聚光要判断片段在不在锥内,本质是比「指向片段的方向」与「聚光朝向」的夹角 和切光角 谁大谁小—— 就在锥内。但代码里我们不直接比角度,而是比它们的余弦:
这个式子在说:在 0°~90° 这段里,余弦是单调递减的(角度越大、余弦越小),所以「夹角更小」完全等价于「余弦更大」。好处是: 不用反三角函数现算——它正是「指向片段方向」与「聚光朝向」两个单位向量的点乘(基础光照章讲过,单位向量点乘 = 夹角余弦);而 在 CPU 侧对切光角取一次 cos 预先算好传进来即可。于是片段着色器里只要一句点乘、一次比较,省掉了昂贵的 acos。这就是 §3 说的「比余弦不比角度」的来历。
内外锥之间的软边强度为:
这个式子在说:内锥余弦更大、外锥余弦更小;片段从外锥走向内锥时分子从 0 增到完整区间,强度平滑地从 0 变 1。若直接传角度或把内外顺序颠倒,分母符号就会错误。
动手:把点光源转远,看它暗下去
在完整 Demo 前,先把三种光按“只换光的来路”的顺序逐项实现:
平行光:只验证固定方向
使用 normalize(-light.direction),不访问 position,也不计算衰减。场景中所有片段共享同一个 lightDir。
下面这个 Demo 你在光照篇里见过——一个带法线的立方体,被一个绕着它公转的点光源照亮(那个小亮块就是光源位置)。它正是本章三种投光物里的点光源:有位置、向四面八方发光。这一节就借它把「衰减」看个明白。
Demo 默认就是点光源的完整 Phong。点画布下方的「从头分步看」可把这套 Phong 拆开逐项回顾:环境 → 漫反射 → 镜面——这正是三种投光物共用的底座,等会儿代码里平行光、聚光跟它的差别只在「怎么得到 lightDir」和「要不要乘衰减」。
猜一猜:拖光源方位让那个小亮块绕到立方体最远的一侧,朝你这面会变亮还是变暗?再想一步——如果这盏灯的「越远越暗」掉得更狠(二次项更大),同样转到远处,会比现在更黑还是更亮?先动手拖一拖,再看下面三种光的代码。
光照演示加载中…
把光源转到远侧,朝你这面会变暗:距离 变大,衰减系数 变小,乘到漫反射和镜面上,这块表面就暗了——这就是点光源区别于平行光(太阳那样远近一个样)的关键。注意这个 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,最后相加
// 平行光不衰减,到此为止
}#version 300 es
precision highp float;
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 / quadratic。lightDir 回到「位置减位置」的老办法;新增的是先用 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;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) 就是符号表里的 ,attenuation 就是 §4 那条分式,把它乘到三块上,灯就有了「越远越暗」。三个常数由 CPU 按 §4 取值表传进来:
聚光:先比切光角,再用内外锥平滑
聚光结构体最复杂:既有 position、又有照射方向 direction,外加两个切光角 cutOff(内锥)、outerCutOff(外锥)——注意这俩存的是角度的余弦值,不是角度本身。算法是:先和点光源一样求 lightDir,再求「指向片段方向」与「聚光朝向」的点乘 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;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 的 ;因为 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(问答题) 某人做聚光,只用了一个切光角 cutOff:if (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 项”
冻结输入: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
平行光、点光源与软边聚光:五段轨迹
当前观测:光源类型、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 次沿用同一 context、资源、uniform 与 draw 输入
保持其余输入不变,仅注入“把角度值直接与点积比较且内外 cutoff 反置,软边区间符号错误”
方向光不读距离;点光衰减有限;聚光 inner cosine 大于 outer cosine
光源类型、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 结果不同,必须保留差异,不能用最终截图相似掩盖中间状态错误。