首页 > 解决方案 > Unity - 将大量顶点加载到 ComputeShader 以进行光线跟踪的高性能选项?

问题描述

我一直在使用 Daerst 出色的 Unity Raytracing 教程(来自http://blog.three-eyed-games.com/2018/05/03/gpu-ray-tracing-in-unity-第 1 部分/ )。

我目前正在扩展光线跟踪器以接受任意网格对象,这样我就可以可视化其他对象,而不仅仅是球体和平面。我使用 Wikipedia implementation of a guide 编写了自己的 Moeller-Trombore 实现,并且对于少量三角形(订单 700)按预期工作。

但是,我发现将具有 80,000 个顶点的网格写入缓冲区需要花费几分钟的时间。我知道按照渲染标准,这个顶点数非常低,所以我认为我处理这个问题的方式肯定有一些东西导致了性能问题。

澄清一下,加载网格后,我的性能问题与 FPS 无关——就我的目的而言,任何大于 0.1FPS 的东西都很棒!它正在加载感觉速度太慢的网格。

这是我的 ComputeShader 代码:

#pragma kernel CSMain

RWTexture2D<float4> Result;

float4x4 _CameraToWorld;
float4x4 _CameraInverseProjection;

float4 _DirectionalLight;

float2 _PixelOffset;

Texture2D<float4> _SkyboxTexture;
SamplerState sampler_SkyboxTexture;

static const float PI = 3.14159265f;

//-------------------------------------
//- UTILITY

float sdot(float3 x, float3 y, float f = 1.0f)
{
    return saturate(dot(x, y) * f);
}

float energy(float3 color)
{
    return dot(color, 1.0f / 3.0f); 
}

//-------------------------------------
//- RANDOMNESS

float2 _Pixel;
float _Seed;

float rand()
{
    float result = frac(sin(_Seed / 100.0f * dot(_Pixel, float2(12.9898f, 78.233f))) * 43758.5453f);
    _Seed += 1.0f;
    return result;
}


struct Geometry
{
    uint type; //4;
    float smoothness; //8;
    float3 albedo; //20;
    float3 specular; //32;
    float3 emission; //44;
    int3 verts; //56
};
StructuredBuffer<Geometry> _Geometries;
StructuredBuffer<float3> _Vertices;


//-------------------------------------
//- RAY

struct Ray
{
    float3 origin;
    float3 direction;
    float3 energy;
};

Ray CreateRay(float3 origin, float3 direction)
{
    Ray ray;
    ray.origin = origin;
    ray.direction = direction;
    ray.energy = float3(1.0f, 1.0f, 1.0f);
    return ray;
}

Ray CreateCameraRay(float2 uv)
{
    // Transform the camera origin to world space
    float3 origin = mul(_CameraToWorld, float4(0.0f, 0.0f, 0.0f, 1.0f)).xyz;

    // Invert the perspective projection of the view-space position
    float3 direction = mul(_CameraInverseProjection, float4(uv, 0.0f, 1.0f)).xyz;
    // Transform the direction from camera to world space and normalize
    direction = mul(_CameraToWorld, float4(direction, 0.0f)).xyz;
    direction = normalize(direction);

    return CreateRay(origin, direction);
}


//-------------------------------------
//- RAYHIT

struct RayHit
{
    float3 position;
    float distance;
    float3 normal;
    float3 albedo;
    float3 specular;
    float smoothness;
    float3 emission;
};

RayHit CreateRayHit()
{
    RayHit hit;
    hit.position = float3(0.0f, 0.0f, 0.0f);
    hit.distance = 1.#INF;
    hit.normal = float3(0.0f, 0.0f, 0.0f);
    hit.albedo = float3(0.0f, 0.0f, 0.0f);
    hit.specular = float3(0.0f, 0.0f, 0.0f);
    hit.smoothness = 0.0f;
    hit.emission = float3(0.0f, 0.0f, 0.0f);
    return hit;
}



//-------------------------------------
//- POLYGONS

struct Triangle
{
    float3 vertexA; //12
    float3 vertexB; //24
    float3 vertexC; //36
    float3 albedo; //48
    float3 specular; //60
    float smoothness; //64
    float3 emission; //76
};

float3 GetTriangleNormal(float3 vA, float3 vB, float3 vC)
{
    return cross(vB-vA, vC-vA);
}


Triangle TriangleFromGeometry(Geometry geometry)
{
    Triangle tri;
    tri.albedo = geometry.albedo;
    tri.specular = geometry.specular;
    tri.smoothness = geometry.smoothness;
    tri.emission = geometry.emission;

    tri.vertexA = _Vertices[geometry.verts[0]];
    tri.vertexB = _Vertices[geometry.verts[1]];
    tri.vertexC = _Vertices[geometry.verts[2]];
    return tri;
}

void IntersectTriangle(Ray ray, inout RayHit bestHit, Triangle tri) 
{
    float epsilon = 0.0000001;
    float3 pA = tri.vertexA;
    float3 pB = tri.vertexB;
    float3 pC = tri.vertexC;
    float3 edge1 = pB - pA;
    float3 edge2 = pC - pA;
    float3 rayVector = ray.direction;// - ray.origin;
    float3 h = cross(rayVector, edge2);
    float a = dot(edge1, h);
    if (a > -epsilon && a < epsilon)
    {
        return;
    }
    float f = 1/a;
    float3 s = ray.origin - pA;
    float u = f * dot(s, h);
    if (u < 0.0f || u> 1.0f)
    {
        return;
    }

    float3 q = cross(s, edge1);
    float v = f * dot(rayVector, q);
    if (v < 0.0 || u + v > 1.0)
    {
        return;
    }
    float t = f * dot(edge2, q);
    if (t > epsilon && t < bestHit.distance)
    {
        bestHit.distance = t;
        bestHit.position = ray.origin + rayVector * t;
        bestHit.normal = GetTriangleNormal(pA, pB, pC);
        bestHit.albedo = tri.albedo;
        bestHit.specular = tri.specular;
        bestHit.smoothness = tri.smoothness;
        bestHit.emission = tri.emission;
    }
}

void IntersectGeometry(Ray ray, inout RayHit bestHit, Geometry geometry) {
    if (geometry.type == 1) {
        Triangle tri = TriangleFromGeometry(geometry);
        IntersectTriangle(ray, bestHit,tri);
    }
}

//-------------------------------------
//- TRACE

RayHit Trace(Ray ray)
{
    RayHit bestHit = CreateRayHit();

    uint numGeometries, geometryStride;
    _Geometries.GetDimensions(numGeometries, geometryStride);
    for (uint i = 0; i < numGeometries; i++) 
    {
        IntersectGeometry(ray, bestHit, _Geometries[i]);
    }

    return bestHit;
}


//-------------------------------------
//- SAMPLING

float3x3 GetTangentSpace(float3 normal)
{
    // Choose a helper vector for the cross product
    float3 helper = float3(1, 0, 0);
    if (abs(normal.x) > 0.99f)
        helper = float3(0, 0, 1);

    // Generate vectors
    float3 tangent = normalize(cross(normal, helper));
    float3 binormal = normalize(cross(normal, tangent));
    return float3x3(tangent, binormal, normal);
}

float3 SampleHemisphere(float3 normal, float alpha)
{
    // Sample the hemisphere, where alpha determines the kind of the sampling
    float cosTheta = pow(rand(), 1.0f / (alpha + 1.0f));
    float sinTheta = sqrt(1.0f - cosTheta * cosTheta);
    float phi = 2 * PI * rand();
    float3 tangentSpaceDir = float3(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta);

    // Transform direction to world space
    return mul(tangentSpaceDir, GetTangentSpace(normal));
}

//-------------------------------------
//- SHADE

float SmoothnessToPhongAlpha(float s)
{
    return pow(1000.0f, s * s);
}

float3 Shade(inout Ray ray, RayHit hit)
{
    if (hit.distance < 1.#INF)
    {
        // Calculate chances of diffuse and specular reflection
        hit.albedo = min(1.0f - hit.specular, hit.albedo);
        float specChance = energy(hit.specular);
        float diffChance = energy(hit.albedo);

        // Roulette-select the ray's path
        float roulette = rand();
        if (roulette < specChance)
        {
            // Specular reflection
            ray.origin = hit.position + hit.normal * 0.001f;
            float alpha = SmoothnessToPhongAlpha(hit.smoothness);
            ray.direction = SampleHemisphere(reflect(ray.direction, hit.normal), alpha);
            float f = (alpha + 2) / (alpha + 1);
            ray.energy *= (1.0f / specChance) * hit.specular * sdot(hit.normal, ray.direction, f);
        }
        else if (diffChance > 0 && roulette < specChance + diffChance)
        {
            // Diffuse reflection
            ray.origin = hit.position + hit.normal * 0.001f;
            ray.direction = SampleHemisphere(hit.normal, 1.0f);
            ray.energy *= (1.0f / diffChance) * hit.albedo;
        }
        else
        {
            // Terminate ray
            ray.energy = 0.0f;
        }

        return hit.emission;
    }
    else
    {
        // Erase the ray's energy - the sky doesn't reflect anything
        ray.energy = 0.0f;

        // Sample the skybox and write it
        float theta = acos(ray.direction.y) / -PI;
        float phi = atan2(ray.direction.x, -ray.direction.z) / -PI * 0.5f;
        return _SkyboxTexture.SampleLevel(sampler_SkyboxTexture, float2(phi, theta), 0).xyz;
    }
}






//-------------------------------------
//- KERNEL

[numthreads(16,16,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    _Pixel = id.xy;

    // Get the dimensions of the RenderTexture
    uint width, height;
    Result.GetDimensions(width, height);

    // Transform pixel to [-1,1] range
    float2 uv = float2((id.xy + _PixelOffset) / float2(width, height) * 2.0f - 1.0f);

    // Get a ray for the UVs
    Ray ray = CreateCameraRay(uv);

    // Trace and shade the ray
    float3 result = float3(0, 0, 0);
    for (int i = 0; i < 4; i++)
    {
        RayHit hit = Trace(ray);
        result += ray.energy * Shade(ray, hit);

        if (!any(ray.energy))
            break;
    }

    Result[id.xy] = float4(result, 1);
}

这是将信息写入着色器的 C# 类:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;

public class RayTracingMaster : MonoBehaviour
{
    public ComputeShader RayTracingShader;
    private RenderTexture _target;
    public Texture SkyboxTexture;
    private uint _currentSample = 0;
    private Material _addMaterial;

    private ComputeBuffer _geometryBuffer;
    private ComputeBuffer _vertexBuffer;
    private Camera _camera;
    public Light DirectionalLight;
    private RenderTexture _converged;
    public int SphereSeed = 2018;
    public List<Geometry> geometries = new List<Geometry>();
    private List<Vector3> _vertices = new List<Vector3>();


    private void Awake()
    {
        _camera = GetComponent<Camera>();
    }


    private void Update()
    {
        if (transform.hasChanged)
        {
            _currentSample = 0;
            transform.hasChanged = false;
        }

        if (DirectionalLight.transform.hasChanged)
        {
            _currentSample = 0;
            DirectionalLight.transform.hasChanged = false;
        }
    }
    private void OnEnable()
    {
        _currentSample = 0;
        SetUpScene();
    }
    private void OnDisable()
    {
        if (_geometryBuffer != null)
            _geometryBuffer.Release();
    }
    private void SetUpScene()
    {
        Random.InitState(SphereSeed);
        Profiler.BeginSample("Geometry Buffer Creation");
        _geometryBuffer = new ComputeBuffer(geometries.Count, 56);
        _geometryBuffer.SetData(geometries);
        _vertexBuffer = new ComputeBuffer(_vertices.Count, 12);
        _vertexBuffer.SetData(_vertices);
        Profiler.EndSample();

    }

    private void SetShaderParameters()
    {
        RayTracingShader.SetMatrix("_CameraToWorld", _camera.cameraToWorldMatrix);
        RayTracingShader.SetMatrix("_CameraInverseProjection", _camera.projectionMatrix.inverse);

        RayTracingShader.SetVector("_PixelOffset", new Vector2(Random.value, Random.value));
        Vector3 l = DirectionalLight.transform.forward;
        RayTracingShader.SetVector("_DirectionalLight", new Vector4(l.x, l.y, l.z, DirectionalLight.intensity));
        RayTracingShader.SetInt("numGeometry",_geometryBuffer.count);
        Debug.Log("Geometries: " + _geometryBuffer.count);
        RayTracingShader.SetBuffer(0, "_Geometries", _geometryBuffer);
        RayTracingShader.SetBuffer(0, "_Vertices", _vertexBuffer);
        RayTracingShader.SetFloat("_Seed", Random.value);
        RayTracingShader.SetTexture(0, "_SkyboxTexture", SkyboxTexture);
    }

    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        SetShaderParameters();
        Render(destination);
    }
    private void Render(RenderTexture destination)
    {
        // Make sure we have a current render target
        InitRenderTexture();
        // Set the target and dispatch the compute shader
        RayTracingShader.SetTexture(0, "Result", _target);
        int threadGroupsX = Mathf.CeilToInt(Screen.width / 8.0f);
        int threadGroupsY = Mathf.CeilToInt(Screen.height / 8.0f);
        RayTracingShader.Dispatch(0, threadGroupsX, threadGroupsY, 1);
        // Blit the result texture to the screen
        if (_addMaterial == null)
            _addMaterial = new Material(Shader.Find("Hidden/AddShader"));
        _addMaterial.SetFloat("_Sample", _currentSample);
        Graphics.Blit(_target, destination, _addMaterial);
        _currentSample++;
    }
    private void InitRenderTexture()
    {
        if ((_target == null || _target.width != Screen.width || _target.height != Screen.height) || (_converged == null || _converged.width != Screen.width || _converged.height != Screen.height))
        {
            // Release render texture if we already have one
            if (_target != null)
                _target.Release();
            // Get a render target for Ray Tracing
            _target = new RenderTexture(Screen.width, Screen.height, 0,
                RenderTextureFormat.ARGBFloat, RenderTextureReadWrite.Linear);
            _target.enableRandomWrite = true;
            _target.Create();

            if (_converged != null)
            {
                _converged.Release();
            }
            _converged = new RenderTexture(Screen.width, Screen.height, 0, RenderTextureFormat.ARGBFloat,RenderTextureReadWrite.Linear);
            _converged.enableRandomWrite = true;
            _converged.Create();
            _currentSample = 0;
        }
    }

    public Geometry[] ShaderGeometryFromMesh(Mesh mesh, Vector3 albedo, Vector3 specular, float smoothness, Vector3 emission, Vector3 worldScale, Quaternion worldRotation) {
       Profiler.BeginSample("Shader Geometry Creation");
        Geometry[] geometry = new Geometry[mesh.triangles.Length/3];
        for (int i = 0; i < geometry.Length; i++)
        {
            int[] vertIndices = new int[3];
            for (int j = 0; j < 3; j++)
            {
                Vector3 vert = mesh.vertices[mesh.triangles[i * 3 + j]];
                if (!_vertices.Contains(vert))
                {
                    _vertices.Add(vert);
                }
                vertIndices[j] = _vertices.IndexOf(vert);
            }
            geometry[i] = new Geometry(albedo, specular, smoothness, emission,vertIndices);
        }
        Profiler.EndSample();
        return geometry;

    }





    public struct Geometry
    {
        public uint type;
        public Vector3 albedo;
        public Vector3 specular;
        public float smoothness;
        public Vector3 emission;

        public Vector3Int verts;

        public Geometry( Vector3 albedo, Vector3 specular, float smoothness, Vector3 emission, int[] verts, uint type = 1) {
            this.albedo = albedo;
            this.specular = specular;
            this.smoothness = smoothness;
            this.emission = emission;
            this.verts = new Vector3Int(verts[0],verts[1],verts[2]);
            this.type = type;
        }
    }
}

标签: c#unity3draytracingcompute-shader

解决方案


事实证明,我根本没有遇到我认为的问题!我的瓶颈是 ShaderGeometryFromMesh() 方法试图通过检查每个添加的顶点是否唯一来避免重复的顶点。显然,随着列表扩展到数千或数万个项目,这需要大量时间。上传到缓冲区相对简单。


推荐阅读