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

为何我的OpenGL Compute Shader光线追踪仅30fps?求优化方案

OpenGL Compute Shader 光线追踪性能瓶颈分析与优化方案

瓶颈原因

  1. 线程粒度严重不匹配GPU架构
    当前设置local_size_x = 1, local_size_y = 1,意味着每个工作组仅包含1个线程。GPU是基于SIMD(单指令多数据)的并行架构,通常以32/64线程为一个执行单元(Wavefront/Warp),这种单线程工作组的设置会导致硬件利用率极低,大部分计算单元处于闲置状态,这是最核心的性能瓶颈。

  2. 暴力遍历无空间加速
    每个像素光线都遍历场景中所有三角形,即使仅20个三角形,若按1080p分辨率计算,总求交次数也达到1920*1080*20 ≈ 4000万次,且随着三角形数量增加,计算量会线性增长。没有任何空间剔除逻辑,大量时间浪费在与光线不相交的三角形求交上。

  3. 冗余计算开销

    • getBarycentricCoords函数中重复计算v0、v1,而Triangle结构体已经存储了edge1(vertex2-vertex1)和edge2(vertex3-vertex1),属于完全不必要的重复运算。
    • 使用distance(intersectionPoint, rayOrigin)计算交点距离,而由于rayDirection已经归一化,求交得到的t值本身就是光线原点到交点的距离,无需额外计算距离(包含开方操作)。
    • unProject函数声明了model、proj参数,但实际未使用,属于代码冗余,虽不直接影响性能,但增加了维护成本。
  4. 内存访问效率低下
    Triangle结构体采用std430布局,vec3类型会被自动对齐到vec4的字节大小,导致结构体内部存在大量内存填充(Padding),浪费内存带宽。同时,线程随机访问不同Triangle元素,无法利用GPU缓存的空间局部性,缓存命中率低。

优化措施

  1. 调整工作组大小适配GPU架构
    修改工作组尺寸为符合GPU硬件的并行单元大小,例如:

    layout (local_size_x = 16, local_size_y = 16, local_size_z = 1) in;
    

    16x16的工作组包含256个线程,适配绝大多数GPU的Wavefront/Warp规模,能充分利用GPU的SIMD并行能力,直接提升硬件利用率。

  2. 引入空间加速结构
    即使三角形数量少,也能通过简单的加速结构减少无效求交:

    • BVH(包围盒层级结构):在CPU端提前构建BVH,将三角形按空间位置分组,光线先与BVH节点的包围盒求交,仅遍历可能相交的三角形组,大幅减少求交次数。
    • 网格划分(Grid):将场景划分为均匀网格,光线仅与经过的网格内的三角形进行求交,避免遍历所有三角形。
  3. 消除冗余计算

    • 替换getBarycentricCoords中的重复计算,直接使用结构体已有的edge1和edge2:
      vec3 getBarycentricCoords(int triangleIndex, vec3 closestIntersectionPoint) {
          vec3 v0 = triangles[triangleIndex].edge1;
          vec3 v1 = triangles[triangleIndex].edge2;
          vec3 v2 = closestIntersectionPoint - triangles[triangleIndex].vertex1;
          float d00 = dot(v0, v0);
          float d01 = dot(v0, v1);
          float d11 = dot(v1, v1);
          float d20 = dot(v2, v0);
          float d21 = dot(v2, v1);
          float denom = d00 * d11 - d01 * d01;
          float b1 = (d11 * d20 - d01 * d21) / denom;
          float b2 = (d00 * d21 - d01 * d20) / denom;
          float b0 = 1.0f - b1 - b2;
          return vec3(b0, b1, b2);
      }
      
    • 用求交得到的t值替代distance计算,需要先修改rayTriangleIntersection函数返回t值:
      bool rayTriangleIntersection(vec3 rayOrigin, vec3 rayDirection, int triangleIndex, out vec3 intersectionPoint, out float t) {
          vec3 h = cross(rayDirection, triangles[triangleIndex].edge2);
          float a = dot(triangles[triangleIndex].edge1, h);
          if (a > -0.00001 && a < 0.00001) {
              return false;
          }
          float f = 1.0 / a;
          vec3 s = rayOrigin - triangles[triangleIndex].vertex1;
          float u = f * dot(s, h);
          if (u < 0.0 || u > 1.0) {
              return false;
          }
          vec3 q = cross(s, triangles[triangleIndex].edge1);
          float v = f * dot(rayDirection, q);
          if (v < 0.0 || u + v > 1.0) {
              return false;
          }
          t = f * dot(triangles[triangleIndex].edge2, q);
          if (t > 0.00001) {
              intersectionPoint = rayOrigin + rayDirection * t;
              return true;
          }
          return false;
      }
      
      之后在主逻辑中直接比较t:
      float t;
      if (rayTriangleIntersection(rayOrigin, rayDirection, triangleIndex, intersectionPoint, t)) {
          if (t < closestIntersectionDistance) {
              closestIntersectionDistance = t;
              closestIntersectionPoint = intersectionPoint;
              finalColor = getTriangleColor(triangleIndex, closestIntersectionPoint);
              intersectionFound = true;
          }
      }
      
    • 清理unProject函数的无效参数:
      vec3 unProject(vec3 win, vec4 viewport) {
          vec4 tmp = vec4(win, 1);
          tmp.x = (tmp.x - viewport[0]) / viewport[2];
          tmp.y = (tmp.y - viewport[1]) / viewport[3];
          tmp.x = tmp.x * 2 - 1;
          tmp.y = tmp.y * 2 - 1;
          vec4 obj = inverseViewProjection * tmp;
          obj /= obj.w;
          return obj.xyz;
      }
      
  4. 优化内存布局

    • 调整Triangle结构体的成员顺序,减少std430布局的内存填充,将同类型成员连续排列:
      struct Triangle {
          vec3 vertex1;
          vec3 vertex2;
          vec3 vertex3;
          vec3 edge1;
          vec3 edge2;
          vec3 color1;
          vec3 color2;
          vec3 color3;
          vec3 normal1;
          vec3 normal2;
          vec3 normal3;
      };
      
    • 拆分结构体为多个独立的缓冲区(如顶点缓冲区、边缓冲区、颜色缓冲区),让连续线程访问连续的内存地址,提升缓存命中率。
  5. 优化光线生成逻辑
    简化光线生成的计算,无需两次调用unProject:

    void main() {
        ivec2 pixelCoord = ivec2(gl_GlobalInvocationID.xy);
        vec2 imgSize = vec2(imageSize(outputImage));
        // 计算NDC坐标(加0.5是为了取像素中心)
        vec2 ndc = (vec2(pixelCoord) + 0.5) / imgSize * 2.0 - 1.0;
        vec4 clipPos = vec4(ndc.x, ndc.y, -1.0, 1.0);
        vec4 worldPos = inverseViewProjection * clipPos;
        worldPos /= worldPos.w;
        vec3 rayOrigin = cameraPosition;
        vec3 rayDirection = normalize(worldPos.xyz - rayOrigin);
        
        vec3 intersectionPoint;
        float closestIntersectionDistance = 999999999.0f;
        vec3 finalColor = vec3(0,0,0);
        bool intersectionFound = false;
        float t;
        
        for (int triangleIndex = 0; triangleIndex < numTriangles; triangleIndex++) {
            if (rayTriangleIntersection(rayOrigin, rayDirection, triangleIndex, intersectionPoint, t)) {
                if (t < closestIntersectionDistance) {
                    closestIntersectionDistance = t;
                    closestIntersectionPoint = intersectionPoint;
                    finalColor = getTriangleColor(triangleIndex, closestIntersectionPoint);
                    intersectionFound = true;
                }
            }
        }
        
        imageStore(outputImage, pixelCoord, intersectionFound ? vec4(finalColor, 1.0f) : vec4(0));
    }
    

    这种方式减少了一次矩阵乘法,同时避免了viewport相关的冗余计算。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 22:01:03