跳到主要内容
← Math

Math

Spherical Coordinates

我的版本和gpt一起写的,所以更高级,考虑了局部空间到世界空间的转换,但是不能pass。

实时渲染 · 网格 GLSL

第 18 课:Spherical Coordinates

我的解:我的版本和gpt一起写的,所以更高级,考虑了局部空间到世界空间的转换,但是不能pass。

我的解

uniform sampler2D iDepthBuffer;      // 深度贴图:用来比较当前点与周围点的遮挡关系
uniform mat4 projectionMatrix;       // 投影矩阵:用于将 3D 坐标投影到屏幕空间
// uniform mat4 viewMatrix;          // 视图矩阵(默认存在,只是这题注释掉)

const float PI = 3.1415926;

varying vec3 vPos;                   // 当前片元在世界空间中的位置
varying vec3 vNormal;                // 当前片元在世界空间中的法线方向

void main() {
  float occlusion = 0.0;            // 最终遮挡因子(0 = 完全遮挡,1 = 完全开放)

  // ---------- Step 1:构建局部坐标系,以法线为“正上方向” ----------
  vec3 w = normalize(vNormal);       // 法线方向 = 局部坐标系中的“y轴”或“上方向”

  // 选择一个与 w 不平行的参考向量,避免交叉积为零向量
  vec3 up = abs(w.y) < 0.999 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);

  vec3 u = normalize(cross(up, w));  // 横向轴 = 用“上”和法线做叉乘,得到局部“x轴”
  vec3 v = cross(w, u);              // 前后轴 = 和法线、横向轴正交,构成局部“z轴”

  // ---------- Step 2:在这个局部半球中采样 64 个方向 ----------
  for (int i = 0; i < 4; ++i) {
    for (int j = 0; j < 16; ++j) {

      // θ 是从法线方向往外的“仰角”(越大越平行)
      float theta = float(i) * (PI * 0.5) / 4.0;  // θ ∈ [0, π/2]

      // φ 是在水平方向绕一圈的“方位角”
      float phi   = float(j) * (2.0 * PI) / 16.0;  // φ ∈ [0, 2π]

      // ---------- Step 3:将球坐标 (θ, φ) 转换为单位方向向量 ----------
      vec3 localDir = vec3(
        sin(phi) * sin(theta),  // x 分量(在平面上绕圈)
        cos(theta),             // y 分量(垂直方向,越靠近法线越大)
        cos(phi) * sin(theta)   // z 分量(在平面上绕圈)
      );

      // ---------- Step 4:把这个局部方向旋转到当前像素的世界空间 ----------
      vec3 worldDir = localDir.x * u + localDir.y * w + localDir.z * v;

      // ---------- Step 5:从当前像素位置出发,沿这个方向偏移一点 ----------
      vec3 samplePos = vPos + worldDir * 0.025;  // 0.025 是半径,表示采样距离

      // ---------- Step 6:将这个 sample 点投影到屏幕空间,用来取深度 ----------
      vec4 proj = projectionMatrix * viewMatrix * vec4(samplePos, 1.0);
      proj.xyz /= proj.w;                         // 齐次除法,变成 [-1, 1] 范围
      proj.xyz = proj.xyz * 0.5 + 0.5;            // 变换为 [0, 1] 贴图坐标范围

      // ---------- Step 7:在深度图中采样该点的深度 ----------
      float sampleDepth = texture(iDepthBuffer, proj.xy).r;  // 获取该点的深度

      float bias = 0.001;   // 偏移:防止浮点误差导致自己遮自己

      // ---------- Step 8:做平滑遮挡判断 ----------
      float rangeCheck = smoothstep(0.0, 1.0, 0.01 / abs(proj.z - sampleDepth));

      // 如果当前 sample 深度小于我们投射出的 sample 点,就说明被挡住了
      float occluded = (proj.z - bias > sampleDepth ? 1.0 : 0.0);

      // 累加遮挡值(乘以 rangeCheck 让远离的遮挡弱一些)
      occlusion += occluded * rangeCheck;
    }
  }

  // ---------- Step 9:归一化遮挡因子 ----------
  occlusion = 1.0 - occlusion / 64.0; // 采样 64 次,平均值;1 表示完全明亮

  // ---------- Step 10:用遮挡因子控制颜色输出 ----------
  gl_FragColor = vec4(vec3(occlusion) * vec3(1.0, 0.3, 0.3), 1.0);
  // 越遮越暗;为了调试视觉清晰,我们用了红色调混合
}

复盘笔记

  • 我的版本和gpt一起写的,所以更高级,考虑了局部空间到世界空间的转换,但是不能pass。
  • 在屏幕空间环境光遮蔽(SSAO)中,我们以当前像素的法线为“上”方向,在该点构建一个局部坐标系(right、up、forward)。通过对半球空间进行64次采样(两个嵌套循环,使用仰角 theta 和方位角 phi),我们将球坐标系方向(x,y,z)转换为单位方向向量,并用 worldDir = x * u + y * w + z * v 将其映射到世界空间。这一步不是旋转,而是将局部向量展开为世界空间中的方向。
  • 然后从当前像素位置 vPos 沿该方向偏移一个小距离,得出采样点 point,将其转换为屏幕空间坐标,再从深度图中取对应深度值。比较该点实际深度与深度图值,如果被遮挡(即深度图更近),则累加遮蔽值,并用 smoothstep 做软边判断。最终遮蔽程度决定当前像素的明暗,模拟其被周围几何包围的程度。