跳到主要内容
pbr · 代码
查看代码目录
Graphics Learning Lab PBR / Shader source

Observe · Change · Recompute · Explain

画面使用的代码,就是这里的代码

Read-only · exact runtime source

代码页不是第二套示例。下面的 GLSL 直接由交互视口导入;教学步骤的“定位到 shader”也会跳到这些稳定章节。

这一课怎么学

  1. 观察
  2. 预测
  3. 改一个量
  4. 看证据
  5. 再读代码
阅读位置会自动记住;代码保持完整行,可左右滑动。
01 · vertex

顶点到世界空间

输出位置、法线和 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;
}
02 · fragment

切线空间法线

用屏幕导数重建 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);
}
03 · fragment

Lambert 漫反射

能量归一化的漫反射基线。

代码锚点:#diffuse · 本段第 48–53 行

按需展开 · 6 行代码 查看 Lambert 漫反射 的精确实现
// SECTION:diffuse — Lambert baseline
vec3 lambertDiffuse(vec3 baseColor) {
  return baseColor / PI;
}
04 · fragment

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);
}
05 · fragment

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);
}
06 · fragment

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);
}
07 · fragment

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;
08 · fragment

调试输出

将中间项直接变成可见证据。

代码锚点:#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>
}