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

光线追踪Fragment Shader反射与弹跳实现黑屏问题排查求助

光线追踪反射弹跳黑屏问题排查指南

嘿,咱们一步步拆解你的光线追踪反射弹跳黑屏问题,结合你提供的GLSL代码,给你几个核心排查方向和修复思路:


一、先修正hitScene的命中距离判断逻辑

你的hitScene里用mindist = -1000.记录最近命中,这是核心错误之一:

  • 光线的t值(对应三角形命中的uvt.z)应该是正数(代表光线前进方向的有效命中),初始mindist应该设为极大正数(比如1e10),然后找最小的t值(因为t越小,命中点离相机越近)。你现在取z更大的点,完全搞反了距离逻辑——相机在z=6位置,光线向-z方向发射,z值越小的点离相机越近,选z大的点会导致取到最远甚至反向的无效命中。
  • 三角形命中时,必须检查uvt.z > 0,跳过光线反方向的无效命中;同时要修正法向量朝向,确保法向量和光线方向的点积为负(朝向相机),否则反射方向会完全错误。

二、修复Trace函数的颜色初始化与循环逻辑

  • 初始color = vec3(0.)会导致第一次命中时,color *= ...始终为0,永远没有颜色输出。应该把初始color设为vec3(1.)(代表初始光线的能量),或者第一次命中时直接赋值而非乘法。
  • 反射光线的起点必须偏移极小距离(比如hitPos + normal * 1e-4),否则会和原命中点发生自相交,导致光线立刻命中自身,出现黑屏或闪烁。
  • 循环里的else { color = attenuation; }逻辑错误,应该在hitScene返回false时,直接返回当前衰减值乘背景色(比如天空蓝)并跳出循环,而非每次循环都覆盖颜色。

三、修正材质计算的物理逻辑

  • 漫反射材质(LAMB)不能用镜面反射reflect,应该生成半球内的随机散射方向(需要实现GLSL随机函数),否则漫反射会变成镜面反射,既不符合物理逻辑,也会导致光线无法正确到达光源。
  • 金属材质的颜色计算要分离光照贡献和能量衰减:金属的颜色是attenuation * material.albedo乘光照,而反射光线的能量衰减是attenuation * material.albedo,避免初始0值导致的无输出。

四、阴影计算的距离判断修正

calcShadow里比较isHitMesh.z > isHitLightDir.z是错误的,z值不能代表实际距离,应该计算光线到命中点的欧氏距离(length(isHitMesh - shadowRay.orig)和length(isHitLightDir - shadowRay.orig)),比较哪个距离更近,才能正确判断是否处于阴影中。


关键代码修正示例(核心片段)

修正后的hitScene核心逻辑

bool hitScene(Ray R_, out vec3 hitPos, out vec3 normal, out Material material, Sphere sphere, out bool isSphere, Sphere lightSource){
    vec4 a = vec4(0.0), b = vec4(0.0), c = vec4(0.0);
    float closestT = 1e10; // 初始化为极大值,寻找最近的有效命中t值
    bool weHitSomething = false;
    vec3 hitPos1 = vec3(0.);
    vec3 triangleNormal = vec3(0.,0.,0.);
    vec3 sphereNormal;

    // 遍历网格三角形
    for (int i = 0; i < vertsCount; i += 3) {
        a = texelFetch(uMeshData, ivec2(i, 0), 0);
        b = texelFetchOffset(uMeshData, ivec2(i, 0), 0, ivec2(1, 0));
        c = texelFetchOffset(uMeshData, ivec2(i, 0), 0, ivec2(2, 0));
        vec3 uvt;
        bool isHit = hitTriangle(R_.orig,R_.dir, a.xyz,b.xyz,c.xyz,uvt, triangleNormal);
        // 只保留光线前进方向的近命中
        if (isHit && uvt.z > 0. && uvt.z < closestT) {
            hitPos1 = R_.orig + R_.dir*uvt.z;
            closestT = uvt.z;
            // 修正法向量朝向,确保朝向相机
            normal = dot(R_.dir, triangleNormal) > 0. ? -triangleNormal : triangleNormal;
        }
    }

    // 检测地板球体
    vec3 hit;
    bool isHitSphere = hitSphere(R_.orig,R_.dir,sphere.center,sphere.radius,hit);
    if(isHitSphere){
        float t = length(hit - R_.orig);
        if(t > 0. && t < closestT){
            closestT = t;
            sphereNormal = normalize(hit - sphere.center);
            normal = dot(R_.dir, sphereNormal) > 0. ? -sphereNormal : sphereNormal;
            hitPos = hit;
            weHitSomething = true;
            material.type = LAMB;
            material.albedo = vec3(0., 0.9, 0.9);
            isSphere = true;
        }
    }

    // 优先使用网格的近命中
    if(closestT != 1e10 && hitPos1 != vec3(0.)){
        hitPos = hitPos1;
        weHitSomething = true;
        material.type = METAL;
        material.albedo = vec3(0.9, 0.9, 0.9);
        isSphere = false;
    }

    return weHitSomething;
}

修正后的Trace核心逻辑

// 补充一个简单的GLSL随机函数(需要在代码顶部定义)
float rand() {
    return fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453);
}

vec3 Trace(Ray ray, Sphere floor, Sphere lightSource){
    vec3 hitPos, normal;
    bool isSphere;
    Material material;
    vec3 color = vec3(1.); // 初始光线能量为1
    vec3 attenuation = vec3(1.);

    for(int i=0; i< MAX_BOUNCES; i++){
        if(hitScene(ray, hitPos, normal, material, floor, isSphere, lightSource)){
            // 计算光照和阴影贡献
            vec3 lightDir = normalize(lightSource.center - hitPos);
            float diffuse = clamp(dot(lightDir, normal), 0., 1.);
            vec3 shadow = calcShadow(lightSource, hitPos);
            vec3 lightContribution = diffuse * shadow;

            if (material.type == METAL) {
                vec3 direction = reflect(ray.dir, normal);
                // 偏移起点避免自相交
                ray = Ray(hitPos + normal * 1e-4, direction);
                color *= material.albedo * lightContribution;
                attenuation *= material.albedo;
                // 能量过低时提前终止循环
                if(dot(attenuation, attenuation) < 1e-3) break;
            } else if (material.type == LAMB) {
                // 漫反射:生成半球随机方向
                vec3 randomDir = normalize(normal + vec3(rand()-0.5, rand()-0.5, rand()-0.5));
                ray = Ray(hitPos + normal * 1e-4, randomDir);
                color *= material.albedo * lightContribution;
                attenuation *= material.albedo;
                if(dot(attenuation, attenuation) < 1e-3) break;
            }
        } else {
            // 未命中场景,返回天空背景色
            color *= vec3(0.5, 0.7, 1.0);
            break;
        }
    }
    return color;
}

额外调试技巧

  • 可视化命中结果:临时修改Trace函数返回hitPos或normal,看屏幕是否有正确的颜色输出,判断是hitScene未命中还是后续颜色计算错误。
  • 打印关键变量:如果你的环境支持,可以用fragColor = vec4(normalize(normal), 1.0)输出法向量,验证法向量朝向是否正确。

内容的提问来源于stack exchange,提问作者Thodoris Koutsis

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.11 09:32:36