Unity通用渲染管线Shader日志输出工具

Unity通用渲染管线Shader日志输出工具

在Unity开发过程中,如果需要输出调试日志只需要在C#中调用Debug.Log即可,但是Shader由于硬件结构上的问题无法像C#一样轻松地输出调试日志。因此在Shader编码过程中调试就成了一个很困难的事情,比如想知道VS中某个中间变量结果是否正确等等。

我写的这个工具就是希望能把Shader中的变量能像C#一样输出,解决调试中遇到的困难。当然原理与C#中的日志输出是完全不同的,针对不同的Shader解决方法也是不同的。

一、开发环境

Unity 2019.3+URP

支持的调试Shader类型:VertexShader、FragmentShader、ComputeShader。

二、VertexShader中的日志输出

顶点着色器与像素着色器是两个必须的着色器,但是不要忘记,这两者还有一个可选的着色器:几何着色器(Geometry Shader)。

关于几何着色器就不详细阐述了,大家可以自行查阅相关资料。

由于几何着色器可以为模型添加新的顶点,并且还没有经过光栅化,因此我们可以将顶点着色器中需要输出的变量存储到纹理通道中,然后在几何着色阶段利用新增的顶点将这个变量的内容画到屏幕上。

1. 下面直接介绍使用方法:

以调试Lit.shader为例(工程中参见LitDebugVertex.shader)。

先看下效果:


对红圈内的模型Shader进行日志输出


调试过程中的模型会以Wireframe的模式渲染,点击某一个顶点会输出调试的日志

2. 对需要日志输出的Shader进行简单改造

1)在原先Fragment声明的地方插入如下代码,然后注释掉原先的声明。

#pragma vertex LitPassVertex
//#pragma fragment LitPassFragment
//1、VertexDebug: 在#pragma fragment xxx后前添加,同时注释掉此行
#pragma geometry geom       //关闭调试注释此行
#pragma fragment debugFrag  //关闭调试注释此行
#define VERTEX_DEBUG_ENABLE //关闭调试注释此行
#define VERTEX_DEBUG_INDEX 0    //选取的顶点所在三角形index(0,1,2,3-表示全部检测)
#include "Packages/com.seasun.graphics/Shaders/Debug/VertexDebug.hlsl"

如果想取消调试,恢复到正常的渲染模式,可以注释掉上述标记的3行代码,并恢复原先的Fragment函数声明。

2)因为替换了Fragment函数,所以需要修改原先Vertex函数的名称。

//2、VertexDebug: 修改Vert函数分布传入4个参数:返回类型,函数名,数据结构体名称,结构体实例
VERTEX_DEBUG_FUN(Varyings, LitPassVertex, Attributes, input)
//Varyings LitPassVertex(Attributes input)
{
    Varyings output = (Varyings)0;

    float4 mrtValue = 0;

    UNITY_SETUP_INSTANCE_ID(input);
    UNITY_TRANSFER_INSTANCE_ID(input, output);
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

3)初始化

 UNITY_SETUP_INSTANCE_ID(input);
    UNITY_TRANSFER_INSTANCE_ID(input, output);
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

    VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);

    //3、VertexDebug: 初始化,传递投影后的坐标值
    VERTEX_DEBUG_INIT(vertexInput.positionCS)

4)添加想要输出的变量

#if defined(_MAIN_LIGHT_SHADOWS) && !defined(_RECEIVE_SHADOWS_OFF)
    output.shadowCoord = GetShadowCoord(vertexInput);
#endif

    output.positionCS = vertexInput.positionCS;

    //4、VertexDebug: 根据屏幕采点,自动选择顶点(可选,也可以自己填写)
    if (VERTEX_DEBUG_AUTO_JUDGE)
    {
        //5、VertexDebug: 输入想要调试输出的变量,支持xy2个参数
        VERTEX_DEBUG_VALUE(xy, input.lightmapUV.xy)
    }

由于Shader是并行执行,在调试期间会有多个顶点执行同样的一段代码,因此这里有两种方法来指定某一个顶点输入。

一种是像示例中的一样使用这个宏,然后在场景的运行的时候按住Alt,用鼠标点击模型的顶点,然后就会输出选中顶点的日志(如果游戏顶点比较密集,会输出多个顶点的日志)。

另一种方式是自己设置约束,在Shader中指定某一个顶点输出日志。

5)改写返回

 //6、VertexDebug: 将原始输出结构放入宏中
    VERTEX_DEBUG_OUTPUT(output)
    //return output;

3. 开始调试

在调试场景中,找个任意一个GameObject,挂载VertexDebug.cs脚本,然后启动游戏。

按住Alt,用鼠标点击待调试模型的顶点。

调节摄像机的视角,使待调试的顶点进行放大,避免其他顶点的干扰。

三、FragmentShader中的日志输出

像素着色器不像顶点着色器那样,中间有几何着色器辅助输出,因此像素着色器中的调试信息只能存储到颜色缓冲区中。但是存储到颜色缓冲区中的内容不仅会影响最终的渲染结果,也会受到后期等因素的影响。假如我们使用MRT,就可以解决上述问题。

Unity中的MRT可参见延迟渲染:
https://docs.unity3d.com/Manual/RenderTech-DeferredShading.html

1. 对URP进行改造

URP由于使用正向渲染,因此并没有启用MRT,所以需要稍微改造,已到达支持的目的。具体内容这里就不阐述了,可以在工程中搜索宏FRAGMENG_DEBUG查看改造的内容。

2. 下面直接介绍使用方法:

以调试Lit.shader为例(工程中参见LitDebugFragment.shader)。

先看下效果:


木材Shader为需要调试的,插入代码后,渲染结果不会受到任何影响


按住Ctrl,点击需要显示输出内容的像素点,同时会在屏幕和Console中输出内容

3. 对需要日志输出的Shader进行简单改造

1)在HLSLPROGRAM前添加

//1、FragmentDebug:添加混合模式
    Blend 1 One Zero

指定SV_Target1的混合方式

2)在Fragment函数前添加

//--------------------------------------
    // GPU Instancing
    #pragma multi_compile_instancing

    //2、FragmentDebug: 在Fragment函数前添加
    #pragma multi_compile __ FRAGMENT_DEBUG_ENABLE
    #include "Packages/com.seasun.graphics/Shaders/Debug/FragmentDebug.hlsl"

    #include "ShaderPass/LitInput.hlsl"

3)改造Fragment函数名和初始化

// Used in Standard (Physically Based) shader
//half4 LitPassFragment(Varyings input) : SV_Target
//3、FragmentDebug: 修改Frag函数分别传入3个参数:函数名、v2f结构体名称、结构体实例
FRAGMENT_DEBUG_FUN(LitPassFragment, Varyings, input)
{
    //4、FragmentDebug: 初始化
    FRAGMENT_DEBUG_INIT

4)增添想输出的变量和改造返回

 half4 color = UniversalFragmentPBR(inputData, surfaceData.albedo, surfaceData.metallic, surfaceData.specular, surfaceData.smoothness, surfaceData.occlusion, surfaceData.emission, surfaceData.alpha);

    color.rgb = MixFog(color.rgb, inputData.fogCoord);

    //5、FragmentDebug: 输入想要调试输出的变量,支持xyz3个参数
    FRAGMENT_DEBUG_VALUE(xyz, surfaceData.albedo)

    //6、FragmentDebug: 将原始结果放入宏中
    FRAGMENT_DEBUG_OUTPUT(color)
    //return color;

4. 开始调试

在PlayerSetting中增添宏FRAGMENG_DEBUG,删除此宏会自动关掉全部功能,包括对URP的改造。

在调试场景中,找个任意一个GameObject,挂载FragmentDebug.cs脚本,然后启动游戏。

按住Ctrl,用鼠标点击待调试模型的像素点,会在Console和屏幕中输出日志内容。

Ctrl+D可以显示和隐藏屏幕中的调试窗口。

四、ComputeShader中的日志输出

ComputeShader与上面的VS与PS不同,是完全两套流水线,基于GPGPU设计,天然就支持数据从GPU回传数据到CPU。这个工具为了更方便地调试输出,只是对原本的方法进行了一些封装。

1. 下面直接介绍使用方法:

使用示例参见仓库中的CSTest.cs和CSDebug.compute。

2. 对执行脚本进行改造

由于ComputeShader的执行通常有两种,一种是直接执行,另一种是在CommandBuffer中执行。

针对这两种方法使用上略有差别:

1)直接执行

private void ExcuteCSManual()
    {
        CSDebug.ComputeShaderDebugSet("Debug1", m_ComputeShader, kernel);
        CSDebug.ComputeShaderDebugSet("Debug2", m_ComputeShader, kernel);

        m_ComputeShader.SetTexture(kernel, "Result", m_RenderTexture);
        m_ComputeShader.SetTexture(kernel, "Source", m_SrcTexture);
        m_ComputeShader.Dispatch(kernel, m_RenderTexture.width, m_RenderTexture.height, 1);

        Debug.Log("CS1 : " + CSDebug.ComputeShaderDebugGet("Debug1"));
        Debug.Log("CS2 : " + CSDebug.ComputeShaderDebugGet("Debug2"));

        CSDebug.ComputeShaderDebugRelease();
    }

在Dispatch之前设置变量名,可以根据实际情况设置多个,其中Debug1和Debug2为变量名。

在执行完Dispatch之后调用CSDebug.ComputeShaderDebugGet来获取ComputeShader中输出的数值。

最后执行CSDebug.ComputeShaderDebugRelease()来释放ComputeBuffer。

2)在CommandBuffer中执行

private void ExcuteCSCommand(ScriptableRenderContext context, Camera camera)
    {
        if (camera == Camera.main)
        {
            if (m_ExcuteCommand)
            {
                m_ExcuteCommand = false;
            }
            else
            {
                return;
            }
            Debug.Log("CS1 : " + CSDebug.ComputeShaderDebugGet("Debug1"));
            Debug.Log("CS2 : " + CSDebug.ComputeShaderDebugGet("Debug2"));
            CSDebug.ComputeShaderDebugRelease();

            CommandBuffer command = CommandBufferPool.Get("ExcuteCSCommand");
            CSDebug.ComputeShaderDebugSet("Debug1", m_ComputeShader, kernel);
            CSDebug.ComputeShaderDebugSet("Debug2", m_ComputeShader, kernel);

            command.SetComputeTextureParam(m_ComputeShader, kernel, "Result", m_RenderTexture);
            command.SetComputeTextureParam(m_ComputeShader, kernel, "Source", m_SrcTexture);
            command.DispatchCompute(m_ComputeShader, kernel, m_RenderTexture.width, m_RenderTexture.height, 1);

            context.ExecuteCommandBuffer(command);
            CommandBufferPool.Release(command);
        }
    }

与执行直接调用的三个函数一样,但是由于CommandBuffer不是立即执行,而是延迟执行的,因此DispatchCompute之后ComputeBuffer并没有真正执行,也就无法获取调试的内容。

CSDebug.ComputeShaderDebugSet使用的位置同直接执行,但是Get和Release两个方法需要放到Set之前。也就是说,每次Get出来的是上一次执行的结果,第一次执行输出的内容为0。

3. 对ComputeShader进行改造

#pragma kernel CSMain

//1) 在定义前添加
#include "Packages/com.seasun.graphics/Shaders/Debug/CSDebug.hlsl" 

RWTexture2D<float4> Result;
Texture2D Source;

//2)定义变量,其中变量名同C#中的定义
DEBUG_DEF(Debug1)
DEBUG_DEF(Debug2)

[numthreads(1, 1, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
    int i = id.x;
    int j = id.y;
    float c = Source[float2(i, j)].x * 0.3 + Source[float2(i, j)].y * 0.2 + Source[float2(i, j)].z * 0.5;
    Result[float2(i, j)] = float4(c, c, c, Source[float2(i, j)].w);

    if (i == 100 && j == 100)
    {
        //3)增添想要输出的变量
        DEBUG_VALUE(Debug1, Source[float2(i, j)].x)
        DEBUG_VALUE(Debug2, Source[float2(i, j)].y)
    }
}

一共3个步骤,这里就不再细说了。

4. 开始调试

在PlayerSetting中增添CS_DEBUG宏,然后运行场景。

由于ComputeShader不像普通的Shader一样支持宏编译和变体,因此ComputeShader中宏的实现采用文件替换的方式间接实现。每次修改完宏之后需要在编辑器模式下执行一次CSDebug中的任意方法才能真正生效(也可以在编辑器模式下调试一次即可)。


点击右上角的两个按钮进行测试,结果在Console中输出

五、仓库地址

欢迎大家Clone使用,提出改进意见。
https://github.com/zouchunyi/ShaderDebug


感谢作者邹春毅供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

作者主页:https://www.zhihu.com/people/zou-chun-yi-45,作者也是U Sparkle活动参与者,UWA欢迎更多开发朋友加入U Sparkle开发者计划,这个舞台有你更精彩!