第 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做软边判断。最终遮蔽程度决定当前像素的明暗,模拟其被周围几何包围的程度。