ShaderToy片段着色器水折射异常,折射值无法超过1.0
嘿,我碰到过类似的ShaderToy水面折射坑,来帮你拆解一下问题!
先排查折射值卡1.0的核心原因
你说折射值无法超过1.0,大概率是这几个环节出了问题:
- 斯涅尔定律的参数搞反了
空气到水的折射率是n_air=1.0,n_water=1.33,折射公式里的eta应该是n_air/n_water≈0.75,而不是反过来。如果你误写成1.33,当入射角大于临界角(约48.8度)时会触发全反射,这时候折射向量会变成虚数,很多人会直接 clamp 或者取绝对值,导致折射方向被限制在1以内。 - 法线计算被限制/未归一化
你的脊噪生成的水面高度场,法线是通过求导得到的吧?如果噪波的强度被压得太低,或者求导的步长eps太小,法线会接近垂直向上,折射效果就会极其微弱,看起来像是折射值没超过1.0。另外,法线一定要记得归一化,不然折射方向计算会完全跑偏。 - 代码里不小心加了clamp限制
检查下你计算折射向量的代码,是不是无意中写了clamp(refractedDir, -1.0, 1.0)?这种错误很容易犯,直接把折射方向的分量锁死在1以内了。
给你几个具体的修复步骤
1. 修正斯涅尔定律的实现
用GLSL内置的refract函数最稳妥,自己实现的话要注意全反射的处理:
vec3 getRefractedRay(vec3 incident, vec3 normal, float eta) { // eta = 入射介质折射率 / 折射介质折射率,空气到水就是1.0/1.33 float cosThetaI = dot(-incident, normal); // 处理全反射:当cosThetaT为虚数时,返回反射方向 float sinThetaT2 = eta * eta * (1.0 - cosThetaI * cosThetaI); if (sinThetaT2 > 1.0) { return reflect(incident, normal); } float cosThetaT = sqrt(1.0 - sinThetaT2); return eta * incident + (eta * cosThetaI - cosThetaT) * normal; }
2. 确保水面法线正确
你的脊噪高度场求法线的代码应该类似这样,记得归一化:
vec3 getWaterNormal(vec2 uv, float time) { float eps = 0.02; // 步长别太小,不然噪波细节会被抹平 float h = ridgeNoise(uv, time); float hx = ridgeNoise(uv + vec2(eps, 0.0), time); float hy = ridgeNoise(uv + vec2(0.0, eps), time); vec3 normal = vec3(hx - h, hy - h, eps); return normalize(normal); }
可以先把法线可视化出来调试:fragColor = vec4(normal * 0.5 + 0.5, 1.0);,看看水面的起伏是否符合预期。
3. 调试折射光线的步进逻辑
暂时关掉球体和软阴影,只保留水面和棋盘格平面,简化场景更容易定位问题:
- 当光线击中水面时,计算折射方向,然后用新的光线步进去采样棋盘格
- 给折射后的棋盘格加个蓝色 tint,模拟水下效果,这样更容易看到折射的畸变
简化的测试代码片段
// 你的脊噪函数(示例) float ridgeNoise(vec2 uv, float time) { float noise = 0.0; float amp = 0.5; float freq = 2.0; for(int i=0; i<4; i++) { float n = abs(sin(uv.x * freq + time) * cos(uv.y * freq + time*0.7)); noise += n * amp; uv *= 2.0; amp *= 0.5; freq *= 1.5; } return noise; } // 光线步进:简化版,只处理水面和棋盘格 vec2 rayMarch(vec3 ro, vec3 rd, out vec3 hitPos) { float t = 0.0; for(int i=0; i<64; i++) { vec3 p = ro + rd * t; // 水面:y=0的平面加噪波高度 float waterHeight = ridgeNoise(p.xz, iTime); float distWater = p.y - waterHeight; if(distWater < 0.001) { hitPos = p; return vec2(t, 0.0); // 0=水面材质 } // 棋盘格平面:y=-2.0 float distPlane = p.y + 2.0; if(distPlane < 0.001) { hitPos = p; return vec2(t, 1.0); // 1=棋盘格材质 } t += min(distWater, distPlane); } hitPos = ro + rd * t; return vec2(t, -1.0); // 无命中 } // 棋盘格图案 vec3 checker(vec2 uv) { vec2 grid = fract(uv * 0.5); float check = step(0.5, grid.x) ^ step(0.5, grid.y); return mix(vec3(0.9), vec3(0.1), check); } void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = (fragCoord - 0.5*iResolution.xy)/iResolution.y; vec3 ro = vec3(0.0, 2.0, -5.0); vec3 rd = normalize(vec3(uv, 1.0)); vec3 hitPos; vec2 march = rayMarch(ro, rd, hitPos); vec3 col = vec3(0.0); if(march.y == 0.0) { // 水面命中:计算折射 vec3 normal = getWaterNormal(hitPos.xz, iTime); float eta = 1.0/1.33; vec3 refractedRd = getRefractedRay(rd, normal, eta); // 步进折射光线到棋盘格 vec3 planeHit; vec2 planeMarch = rayMarch(hitPos + normal*0.001, refractedRd, planeHit); if(planeMarch.y == 1.0) { col = checker(planeHit.xz); // 水下颜色衰减 col *= vec3(0.2, 0.5, 0.8); } } else if(march.y == 1.0) { // 直接命中棋盘格 col = checker(hitPos.xz); } fragColor = vec4(col, 1.0); }
最后几个调试小技巧
- 输出折射向量的分量:
col = vec3(refractedRd.x, refractedRd.y, refractedRd.z)*0.5 + 0.5;,看看是否有超过1.0的情况(如果全反射的话会变成反射向量) - 调整脊噪的强度和八度数量,让水面起伏更明显,折射效果更容易观察
- 检查光线步进时的偏移:击中水面后,折射光线要稍微偏移一点(比如
hitPos + normal*0.001),避免重新击中水面
内容的提问来源于stack exchange,提问作者Krupip
相关产品推荐
相关产品推荐

