跳到主要内容
normal-spaces · 代码
查看代码目录
Graphics Learning Lab Normal Spaces / Shader source

Observe · Change · Recompute · Explain

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

Read-only · exact runtime source

代码页不提供第二套示例。下面的 GLSL 直接由交互视口导入,并按 decode、TBN、normal matrix、handedness、lighting 和 debug 分段。

这一课怎么学

  1. 观察
  2. 预测
  3. 改一个量
  4. 看证据
  5. 再读代码
01 · vertex

顶点与空间输入

传递位置、UV、T/B/N 与 handedness。

// SECTION:vertex - carry geometry, tangent and space data
attribute vec4 tangent;

varying vec3 vWorldPosition;
varying vec2 vUv;
varying vec3 vObjectNormal;
varying vec3 vObjectTangent;
varying vec3 vObjectBitangent;
varying vec3 vWorldNormal;
varying vec3 vNaiveWorldNormal;
varying vec3 vWorldTangent;
varying vec3 vWorldBitangent;
varying vec3 vViewNormal;
varying vec3 vViewTangent;
varying vec3 vViewBitangent;

void main() {
  vec3 objectNormal = normalize(normal);
  vec3 fallbackAxis = abs(objectNormal.y) < 0.95
    ? vec3(0.0, 1.0, 0.0)
    : vec3(1.0, 0.0, 0.0);
  vec3 fallbackTangent = normalize(cross(fallbackAxis, objectNormal));
  bool hasTangent = length(tangent.xyz) > 0.1;
  vec3 objectTangent = hasTangent ? normalize(tangent.xyz) : fallbackTangent;
  float tangentHandedness = hasTangent ? (tangent.w < 0.0 ? -1.0 : 1.0) : 1.0;
  vec3 objectBitangent = normalize(cross(objectNormal, objectTangent)) * tangentHandedness;

  vec4 worldPosition = modelMatrix * vec4(position, 1.0);
02 · fragment

把 RGB 解码成方向

从 [0, 1] 还原到局部切线空间的 [-1, 1]。

precision highp float;

uniform int uSpace;
uniform int uPattern;
uniform float uNormalStrength;
uniform bool uMirrorUv;
uniform bool uShowRawNormal;
uniform bool uShowPerturbedNormal;
uniform int uDebugMode;

varying vec3 vWorldPosition;
varying vec2 vUv;
varying vec3 vObjectNormal;
varying vec3 vObjectTangent;
varying vec3 vObjectBitangent;
varying vec3 vWorldNormal;
varying vec3 vNaiveWorldNormal;
varying vec3 vWorldTangent;
varying vec3 vWorldBitangent;
varying vec3 vViewNormal;
varying vec3 vViewTangent;
varying vec3 vViewBitangent;

// SECTION:decode - sample a procedural tangent-space normal
vec3 proceduralNormalMap(vec2 uv) {
  float x = 0.0;
  float y = 0.0;
  if (uPattern == 0) {
    x = sin(uv.x * 32.0) * cos(uv.y * 22.0);
    y = cos(uv.x * 18.0 + uv.y * 27.0);
  } else if (uPattern == 1) {
    float cell = step(0.5, fract(uv.x * 8.0)) * 2.0 - 1.0;
    float row = step(0.5, fract(uv.y * 8.0)) * 2.0 - 1.0;
    x = cell * 0.72;
    y = row * 0.72;
  } else {
    vec2 centered = fract(uv) - 0.5;
    float radius = length(centered);
    x = centered.x / max(radius, 0.08) * sin(radius * 34.0);
    y = centered.y / max(radius, 0.08) * sin(radius * 34.0);
  }

  vec3 normalMap = vec3(
    0.5 + x * 0.5 * uNormalStrength,
    0.5 + y * 0.5 * uNormalStrength,
    0.98
  );
  return normalize(normalMap * 2.0 - 1.0);
}

float mirrorBitangentSign();
03 · fragment

TBN 展开

把 tangent-space normal 搬到选定的目标空间。

// SECTION:tbn - expand a local direction through the selected basis
vec3 worldFromTangent(vec3 tangentNormal) {
  vec3 bitangent = vWorldBitangent * mirrorBitangentSign();
  return normalize(
    vWorldTangent * tangentNormal.x +
    bitangent * tangentNormal.y +
    vWorldNormal * tangentNormal.z
  );
}

vec3 objectFromTangent(vec3 tangentNormal) {
  vec3 bitangent = vObjectBitangent * mirrorBitangentSign();
  return normalize(
    vObjectTangent * tangentNormal.x +
    bitangent * tangentNormal.y +
    vObjectNormal * tangentNormal.z
  );
}

vec3 viewFromTangent(vec3 tangentNormal) {
  vec3 bitangent = vViewBitangent * mirrorBitangentSign();
  return normalize(
    vViewTangent * tangentNormal.x +
    bitangent * tangentNormal.y +
    vViewNormal * tangentNormal.z
  );
}

vec3 geometricWorldNormal() {
  return normalize(vWorldNormal);
}
04 · vertex

逆转置法线矩阵

解释非均匀缩放下为什么要保持切平面正交。

// SECTION:normal-matrix - non-uniform scale preserves the normal constraint
  mat3 normalMatrix = mat3(transpose(inverse(modelMatrix)));
  mat3 modelBasis = mat3(modelMatrix);
  vec3 worldNormal = normalize(normalMatrix * objectNormal);
  vec3 naiveWorldNormal = normalize(modelBasis * objectNormal);
  vec3 worldTangent = normalize(modelBasis * objectTangent);
  worldTangent = normalize(worldTangent - worldNormal * dot(worldNormal, worldTangent));
  vec3 worldBitangent = normalize(cross(worldNormal, worldTangent)) * tangentHandedness;

  vWorldPosition = worldPosition.xyz;
  vUv = uv;
  vObjectNormal = objectNormal;
  vObjectTangent = objectTangent;
  vObjectBitangent = objectBitangent;
  vWorldNormal = worldNormal;
  vNaiveWorldNormal = naiveWorldNormal;
  vWorldTangent = worldTangent;
  vWorldBitangent = worldBitangent;
  vViewNormal = normalize(mat3(viewMatrix) * worldNormal);
  vViewTangent = normalize(mat3(viewMatrix) * worldTangent);
  vViewBitangent = normalize(mat3(viewMatrix) * worldBitangent);
  gl_Position = projectionMatrix * viewMatrix * worldPosition;
}
05 · fragment

镜像 UV 与副切线

通过 ±1 符号修正镜像 UV 的 B 轴方向。

// SECTION:handedness - mirrored UVs flip the bitangent sign
float mirrorBitangentSign() {
  return uMirrorUv ? -1.0 : 1.0;
}

// Select the transformed basis for the requested display space.
vec3 perturbedWorldNormal(vec3 tangentNormal) {
  return worldFromTangent(tangentNormal);
}

vec3 selectedSpaceNormal(vec3 tangentNormal, vec3 perturbedNormal) {
  if (uSpace == 0) return tangentNormal;
  if (uSpace == 1) return objectFromTangent(tangentNormal);
  if (uSpace == 3) return viewFromTangent(tangentNormal);
  return perturbedNormal;
}

vec3 selectedSpaceTangent() {
  if (uSpace == 0) return vec3(1.0, 0.0, 0.0);
  if (uSpace == 1) return normalize(vObjectTangent);
  if (uSpace == 3) return normalize(vViewTangent);
  return normalize(vWorldTangent);
}

vec3 selectedSpaceBitangent() {
  if (uSpace == 0) return vec3(0.0, mirrorBitangentSign(), 0.0);
  if (uSpace == 1) return normalize(vObjectBitangent * mirrorBitangentSign());
  if (uSpace == 3) return normalize(vViewBitangent * mirrorBitangentSign());
  return normalize(vWorldBitangent * mirrorBitangentSign());
}

vec3 selectedSpaceGeometricNormal() {
  if (uSpace == 0) return vec3(0.0, 0.0, 1.0);
  if (uSpace == 1) return normalize(vObjectNormal);
  if (uSpace == 3) return normalize(vViewNormal);
  return geometricWorldNormal();
}
06 · fragment

N dot L 与高光

展示扰动法线如何影响光照而不改变几何轮廓。

// SECTION:lighting - the normal changes N dot L, not the silhouette
vec3 finalLighting(vec3 perturbedNormal, vec3 geometricNormal) {
  vec3 lightDirection = normalize(vec3(1.8, 2.4, 3.2) - vWorldPosition);
  vec3 viewDirection = normalize(cameraPosition - vWorldPosition);
  float dotGeometric = max(dot(geometricNormal, lightDirection), 0.0);
  float dotPerturbed = max(dot(perturbedNormal, lightDirection), 0.0);
  float specular = pow(max(dot(reflect(-lightDirection, perturbedNormal), viewDirection), 0.0), 34.0);
  vec3 baseColor = vec3(0.26, 0.52, 0.65);
  return baseColor * (0.12 + dotPerturbed * 0.92) + vec3(specular * 0.42) + vec3(dotGeometric * 0.04);
}
07 · fragment

调试输出

将空间向量和中间量直接编码成可观察证据。

// SECTION:debug - expose the evidence behind the final image
vec3 encodeNormal(vec3 normalValue) {
  return normalValue * 0.5 + 0.5;
}

void main() {
  vec3 tangentNormal = proceduralNormalMap(vUv);
  vec3 geometricNormal = geometricWorldNormal();
  vec3 perturbedNormal = perturbedWorldNormal(tangentNormal);
  vec3 selectedNormal = selectedSpaceNormal(tangentNormal, perturbedNormal);
  vec3 color = finalLighting(perturbedNormal, geometricNormal);

  if (uDebugMode == 1) color = encodeNormal(selectedNormal);
  if (uDebugMode == 2) color = encodeNormal(selectedSpaceTangent());
  if (uDebugMode == 3) color = encodeNormal(selectedSpaceBitangent());
  if (uDebugMode == 4) color = encodeNormal(selectedSpaceGeometricNormal());
  if (uDebugMode == 5) color = encodeNormal(selectedNormal);
  if (uDebugMode == 7) color = encodeNormal(vNaiveWorldNormal);
  if (uDebugMode == 6) {
    vec3 lightDirection = normalize(vec3(1.8, 2.4, 3.2) - vWorldPosition);
    color = vec3(max(dot(perturbedNormal, lightDirection), 0.0));
  }

  if (uShowRawNormal) color = mix(color, encodeNormal(selectedSpaceGeometricNormal()), 0.55);
  if (uShowPerturbedNormal) color = mix(color, encodeNormal(selectedNormal), 0.35);

  gl_FragColor = vec4(color, 1.0);
  #include <tonemapping_fragment>
  #include <colorspace_fragment>
}