【问题标题】:Writing to render texture in Unity using Compute Shaders is slow?使用计算着色器在 Unity 中编写渲染纹理很慢?
【发布时间】:2017-06-06 23:32:43
【问题描述】:

我只是在学习计算着色器并尝试使用 Unity(我对着色器不太熟悉),并且我正在尝试做一些简单的光线投射并从计算着色器写入渲染纹理。一切都很完美,我得到了想要的结果。射线与三角形的交点发生得非常快——不到半秒。但是,目前我尝试将新颜色应用于渲染纹理,性能下降。所需时间跃升至 5 秒。我无法在不进一步恶化性能的情况下摆脱循环。我什至不能在循环中使用 bool 标志,如果它在循环中设置为 true,我可以在循环外使用它来更新纹理颜色。

性能变得非常糟糕。我将如何更新渲染纹理颜色?

这是着色器代码:感谢任何帮助。

//--------------------------------------------------------------------
#pragma kernel MainCS

//--------------------------------------------------------------------
struct Triangle
{
    float3 v0;
    float3 v1;
    float3 v2;
    float3 n;
};

// Precomputed and set from C# script
struct Pixel
{
    float3   position;
    float3   direction;
    int      index;
    float    pixelColor;
};

//-----------------------------------------------------------------------------
#define blocksize 8

// variables
int imageSize;

// buffers
RWStructuredBuffer<Pixel>        pixels    : register(u0); // UAV
RWTexture2D<float4>              rendTex   : register(u1); // UAV
const StructuredBuffer<Triangle> tris      : register(t0); // SRV


// This kernel writes some color in the current pixel if there is ray intersection with some of the triangles from the tris buffer.  In general works well but slow. The intersection part without writing to the render texture is SUPER FAST. When i attempt to write to the texture - gets SUPER SLOW. Render Texture random write is enabled from the C# script

[numthreads(blocksize,blocksize,1)]
void MainCS (uint3 id : SV_DispatchThreadID, uint3 Gid : SV_GroupID, uint3 GTid : SV_GroupThreadID, uint GI : SV_GroupIndex )
{
    // Get the current pixel ID - pixels is 1D array
    int pixelID = (int)(id.y * imageSize + id.x);

    // Ray
    float3 rayO = pixels[pixelID].position;
    float3 rayD = pixels[pixelID].direction;

    // Intersection variables
    float3 pt0, pt1, pt2, edge0, edge1, edge2, cross1, cross2, cross3, n;
    float angle1, angle2, angle3;
    float r, _a, b;
    float3 w0, I;

    bool bIntersect = false;

    [loop][allow_uav_condition]
    for (uint tr = 0; tr < tris.Length; tr++)
    {
        // Somecalculations
        pt0 = tris[tr].v0; pt1 = tris[tr].v1; pt2 = tris[tr].v2;
        edge0 = rayO - pt0; edge1 = rayO - pt1; edge2 = rayO - pt2;

        // First check - is the ray intersecting the triangle
        if (dot(rayD, cross(edge0, edge1)) >= 0.0 ||
            dot(rayD, cross(edge1, edge2)) >= 0.0 ||
            dot(rayD, cross(edge2, edge0)) >= 0.0) continue;

        // Fiding the intersection point
        n = normalize(cross(pt0 - pt1, pt0 - pt2));
        w0 = rayO - pt0;
        _a = -dot(n, w0);
        b  =  dot(n, rayD);
        r  = _a / b;
        I = rayO + rayD * r;

        // Second check - before validate the hitpoint
        if (_a < 0.0)
        {
            // Here i would want to update texture colors

            // ==============================================
            // Variant 1 =======================================
            // Only update the texture without break;
            // Gives proper result but is SLOW - 3 seconds
            rendTex[id.xy] = float4(1.0, 0.0, 0.0, 1.0);
            // if add break; - MUCH SLOWER
            break;

            // ===============================================
            // Variant 2 - Part 1 ==================================
            // rising flag to true - fast
            if(!bIntersect)
            {
                bIntersect = true;
            }
        }
    }

// Variant 2 - Part 2 - When using the flag - updating Render texture colror is SUPER SLOW but acurate
    if(bIntersect)
        rendTex[id.xy] = float4(1.0, 0.0, 0.0, 1.0);
}

【问题讨论】:

    标签: unity3d


    【解决方案1】:

    在 GPU 上编程时,动态分支非常昂贵。

    这是因为 GPU 的设计方式。 CPU 工作原理的简化视图:获取指令,对其进行解码,然后在 ALU 上执行它。 GPU 获取一条指令,对其进行解码,然后同时在一堆 ALU 上执行它。它同时单步执行其每个线程上的每一行,即使只有一个线程必须执行不同的指令,它也需要为所有这些像素再次运行程序。

    基本上,尽可能避免动态分支(if 语句)。当您使用条件中断执行 for 循环时,您会创建很多分支,这是 GPU 的致命弱点。该标志更快,因为 GPU 无论如何都能够在每个线程上执行所有这些指令。尝试让尽可能多的线程执行相同的代码行。

    【讨论】:

      【解决方案2】:

      我假设您正在尝试制作类似于绘图工具的东西,可以让您在表面上绘图。我之前已经构建了其中一个,但这是通过直接从 Unity 绘制纹理来完成的,而不是从着色器中绘制。此外,除非您尝试捕获另一个相机的渲染然后在此基础上进行合成,否则您不需要将其作为渲染纹理。

      着色器通常非常快,因为它们可以将许多像素一次并行绘制到绘图缓冲区。然而,写入纹理内存要慢得多。您的性能问题很可能是由于着色器不断更新每一帧的每个像素上的纹理。许多非常小的写操作。想象一下,通过打开一个文本文件,更新一个字符,然后反复关闭它来写一部小说。

      我的建议是使用Texture2D.setPixels() 在 Unity 中直接绘制纹理。它允许您通过接受 Unity Color 对象数组并仅在您对纹理调用 texture.Apply() 时发送那些修改后的像素来批量写入纹理内存。

      另外,如果需要获取纹理空间中的UV坐标,还有RaycastHit.textureCoord

      这是 Unity 文档中提供的示例,用于根据光线投射到对象表面的位置绘制到纹理。

      using UnityEngine;
      using System.Collections;
      
      public class ExampleClass : MonoBehaviour {
          public Camera cam;
          void Start() {
              cam = GetComponent<Camera>();
          }
          void Update() {
              if (!Input.GetMouseButton(0))
                  return;
      
              RaycastHit hit;
              if (!Physics.Raycast(cam.ScreenPointToRay(Input.mousePosition), out hit))
                  return;
      
              Renderer rend = hit.transform.GetComponent<Renderer>();
              MeshCollider meshCollider = hit.collider as MeshCollider;
              if (rend == null || rend.sharedMaterial == null || rend.sharedMaterial.mainTexture == null || meshCollider == null)
                  return;
      
              Texture2D tex = rend.material.mainTexture as Texture2D;
              Vector2 pixelUV = hit.textureCoord;
              pixelUV.x *= tex.width;
              pixelUV.y *= tex.height;
              tex.SetPixel((int)pixelUV.x, (int)pixelUV.y, Color.black);
              tex.Apply();
          }
      }
      

      【讨论】:

      • 感谢您的建议。但我实际上正在尝试制作一个 GPU RayTracer - 至少在它的基本级别上 - 只拍摄主光线并检测三角形。所需的所有数据都在 C# 脚本中预先计算,并使用缓冲区和 RWTexture2D 上传到计算着色器。我做了一些研究并认为使用渲染纹理最适合此目的,同时它允许随机读写访问 - 在将渲染纹理上传到 GPU 内存之前,在 C# 脚本中启用了此选项。这不是使用 ComputeShader 将信息写入纹理的正确方法吗?
      猜你喜欢
      • 2015-02-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-02-17
      • 2019-06-06
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多