You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ShaderToy片段着色器水折射异常,折射值无法超过1.0

嘿,我碰到过类似的ShaderToy水面折射坑,来帮你拆解一下问题!

先排查折射值卡1.0的核心原因

你说折射值无法超过1.0,大概率是这几个环节出了问题:

  1. 斯涅尔定律的参数搞反了
    空气到水的折射率是n_air=1.0,n_water=1.33,折射公式里的eta应该是n_air/n_water≈0.75,而不是反过来。如果你误写成1.33,当入射角大于临界角(约48.8度)时会触发全反射,这时候折射向量会变成虚数,很多人会直接 clamp 或者取绝对值,导致折射方向被限制在1以内。
  2. 法线计算被限制/未归一化
    你的脊噪生成的水面高度场,法线是通过求导得到的吧?如果噪波的强度被压得太低,或者求导的步长eps太小,法线会接近垂直向上,折射效果就会极其微弱,看起来像是折射值没超过1.0。另外,法线一定要记得归一化,不然折射方向计算会完全跑偏。
  3. 代码里不小心加了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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 09:51:11