Observe · Change · Recompute · Explain
画面使用的代码,就是这里的代码
Read-only · exact runtime source
代码页不是第二套示例。下面的 GLSL 直接由交互视口导入;教学步骤的“定位到 shader”也会跳到这些稳定章节。
这一课怎么学
- 观察
- 预测
- 改一个量
- 看证据
- 再读代码
阅读位置会自动记住;代码保持完整行,可左右滑动。
顶点到世界空间
输出位置、法线和 UV。
代码锚点:#vertex · 本段第 1–14 行
当前起点 · 14 行代码 查看 顶点到世界空间 的精确实现
// SECTION:vertex — world-space inputs
varying vec3 vWorldPosition;
varying vec3 vWorldNormal;
varying vec2 vUv;
void main() {
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
vWorldPosition = worldPosition.xyz;
vWorldNormal = normalize(mat3(modelMatrix) * normal);
vUv = uv;
gl_Position = projectionMatrix * viewMatrix * worldPosition;
}切线空间法线
用屏幕导数重建 TBN。
代码锚点:#normal · 本段第 1–48 行
按需展开 · 48 行代码 查看 切线空间法线 的精确实现
precision highp float;
#define PI 3.14159265359
uniform vec3 uBaseColor;
uniform float uMetallic;
uniform float uRoughness;
uniform float uLightIntensity;
uniform float uNormalStrength;
uniform vec3 uLightPosition;
uniform vec3 uLightColor;
uniform int uDebugMode;
uniform bool uEnableDiffuse;
uniform bool uEnableNdf;
uniform bool uEnableGeometry;
uniform bool uEnableFresnel;
uniform bool uEnableNormal;
varying vec3 vWorldPosition;
varying vec3 vWorldNormal;
varying vec2 vUv;
// SECTION:normal — tangent frame and procedural normal detail
mat3 cotangentFrame(vec3 n, vec3 p, vec2 uv) {
vec3 dp1 = dFdx(p);
vec3 dp2 = dFdy(p);
vec2 duv1 = dFdx(uv);
vec2 duv2 = dFdy(uv);
vec3 dp2Perp = cross(dp2, n);
vec3 dp1Perp = cross(n, dp1);
vec3 tangent = dp2Perp * duv1.x + dp1Perp * duv2.x;
vec3 bitangent = dp2Perp * duv1.y + dp1Perp * duv2.y;
float inverseScale = inversesqrt(max(max(dot(tangent, tangent), dot(bitangent, bitangent)), 0.0001));
return mat3(tangent * inverseScale, bitangent * inverseScale, n);
}
vec3 getShadingNormal(vec3 geometricNormal) {
if (!uEnableNormal || uNormalStrength <= 0.0) return geometricNormal;
vec2 wave = vec2(
sin(vUv.x * 42.0) * cos(vUv.y * 28.0),
cos(vUv.x * 36.0 + vUv.y * 16.0)
);
vec3 tangentNormal = normalize(vec3(wave * uNormalStrength, 1.0));
return normalize(cotangentFrame(geometricNormal, vWorldPosition, vUv) * tangentNormal);
}Lambert 漫反射
能量归一化的漫反射基线。
代码锚点:#diffuse · 本段第 48–53 行
按需展开 · 6 行代码 查看 Lambert 漫反射 的精确实现
// SECTION:diffuse — Lambert baseline
vec3 lambertDiffuse(vec3 baseColor) {
return baseColor / PI;
}GGX 法线分布 D
统计朝向半程向量的微表面。
代码锚点:#ndf · 本段第 53–62 行
按需展开 · 10 行代码 查看 GGX 法线分布 D 的精确实现
// SECTION:ndf — Trowbridge-Reitz GGX normal distribution
float distributionGGX(vec3 n, vec3 h, float roughness) {
float alpha = max(roughness * roughness, 0.0025);
float alphaSquared = alpha * alpha;
float nDotH = clamp(dot(n, h), 0.0, 1.0);
float d = nDotH * nDotH * (alphaSquared - 1.0) + 1.0;
return alphaSquared / max(PI * d * d, 1e-12);
}Smith 几何项 G
估计微表面遮蔽和阴影。
代码锚点:#geometry · 本段第 62–75 行
按需展开 · 14 行代码 查看 Smith 几何项 G 的精确实现
// SECTION:geometry — Schlick-GGX and Smith masking-shadowing
float geometrySchlickGGX(float nDotDirection, float roughness) {
float r = roughness + 1.0;
float k = (r * r) / 8.0;
return nDotDirection / max(nDotDirection * (1.0 - k) + k, 0.0001);
}
float geometrySmith(vec3 n, vec3 v, vec3 l, float roughness) {
float nDotV = max(dot(n, v), 0.0);
float nDotL = max(dot(n, l), 0.0);
return geometrySchlickGGX(nDotV, roughness) * geometrySchlickGGX(nDotL, roughness);
}Schlick 菲涅尔 F
合成看 V·H,debug 以 N·V 显示掠射角轮廓趋势。
代码锚点:#fresnel · 本段第 75–80 行
按需展开 · 6 行代码 查看 Schlick 菲涅尔 F 的精确实现
// SECTION:fresnel — Schlick angle-dependent reflectance
vec3 fresnelSchlick(float cosTheta, vec3 f0) {
return f0 + (1.0 - f0) * pow(1.0 - clamp(cosTheta, 0.0, 1.0), 5.0);
}Cook–Torrance 合成
组合直接光照并分配能量。
代码锚点:#compose · 本段第 80–112 行
按需展开 · 33 行代码 查看 Cook–Torrance 合成 的精确实现
// SECTION:compose — energy-aware Cook-Torrance direct lighting
void main() {
vec3 n = getShadingNormal(normalize(vWorldNormal));
vec3 v = normalize(cameraPosition - vWorldPosition);
vec3 lightVector = uLightPosition - vWorldPosition;
float lightDistance = length(lightVector);
vec3 l = lightVector / max(lightDistance, 0.0001);
vec3 h = normalize(v + l);
float nDotL = max(dot(n, l), 0.0);
float nDotV = max(dot(n, v), 0.0);
vec3 f0 = mix(vec3(0.04), uBaseColor, uMetallic);
float ndf = uEnableNdf ? distributionGGX(n, h, uRoughness) : 1.0;
float geometry = uEnableGeometry ? geometrySmith(n, v, l, uRoughness) : 1.0;
// Keep the physical Fresnel sample available for debug output even when the
// final composition bypasses it. The BRDF uses V·H; the rim debug below uses
// N·V because it is the view-facing signal artists recognize as an edge.
vec3 fresnel = fresnelSchlick(max(dot(h, v), 0.0), f0);
vec3 fresnelForSpecular = uEnableFresnel ? fresnel : vec3(1.0);
vec3 specular = (ndf * geometry * fresnelForSpecular) / max(4.0 * nDotV * nDotL, 0.0001);
// When F is bypassed, do not treat the neutral specular multiplier (1.0) as
// “all energy is reflected”. Keep kD open so a dielectric still has diffuse.
vec3 kS = uEnableFresnel ? fresnel : vec3(0.0);
vec3 kD = (vec3(1.0) - kS) * (1.0 - uMetallic);
vec3 diffuse = uEnableDiffuse ? kD * lambertDiffuse(uBaseColor) : vec3(0.0);
float attenuation = 1.0 / max(lightDistance * lightDistance, 0.25);
vec3 radiance = uLightColor * uLightIntensity * attenuation;
vec3 direct = (diffuse + specular) * radiance * nDotL;
vec3 color = direct + uBaseColor * 0.018;调试输出
将中间项直接变成可见证据。
代码锚点:#debug · 本段第 112–125 行
按需展开 · 14 行代码 查看 调试输出 的精确实现
// SECTION:debug — isolate the evidence behind the final image
vec3 fresnelDebug = fresnelSchlick(nDotV, f0);
if (uDebugMode == 1) color = diffuse * nDotL;
if (uDebugMode == 2) color = specular * nDotL;
if (uDebugMode == 3) color = vec3(clamp(ndf * 0.18, 0.0, 1.0));
if (uDebugMode == 4) color = vec3(geometry);
if (uDebugMode == 5) color = fresnelDebug;
if (uDebugMode == 6) color = n * 0.5 + 0.5;
gl_FragColor = vec4(color, 1.0);
#include <tonemapping_fragment>
#include <colorspace_fragment>
}