合并Shader系列 | 如何合并渲染状态

《合并Shader》系列旨在介绍一些在保证功能不打折的情况下精简Shader数量的方法,其遵循的原理就是把相似功能的Shader文件合并在一个文件里。在掌握了这些技能后,研发团队能在极致情况下做到把所有的Shader文件合并成一个,如Unity 5.x的Standard着色器。本系列会从简到繁的方式来依次遍历林林总总的合并方法,希望大家有所收获。

这是侑虎科技第179篇原创文章,感谢作者唐建伟(网名:七火,QQ:383684387)供稿。欢迎转发分享,未经作者授权请勿转载。作者博客: http://www.7fires.cn/。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群465082844)

同时,作者也是U Sparkle活动参与者哦,UWA欢迎更多开发朋友加入 U Sparkle开发者计划,这个舞台有你更精彩!


在数学中我们学习过:把多项式中的同类项合并成一项叫做合并同类项。同理,提取Shader的相似部分,把多个Shader合并成一个就叫做Shader的合并,也叫合并Shader,偶尔也会引用数学的名词来称呼他为Shader的合并同类项。

Shader的合并方式有很多,根据不同的合并技能和方法可以划分为不同的派系。今天优先介绍一种不太常见、但又很实用的派系,往下看。

在Shader的合并方法中,MaterialPropertyDrawer(属性定义)可以说是自成一派,但又与其他派系有着千丝万缕的关系,今天我们就先拿它来开刀。

在此之前我想补充一点:对于Shader的合并,首先让人想到的应该是宏定义,相信宏定义也是大家应用最广、最先接触的。(毕竟由于GPU的特殊性,Shader里常常通篇都充满了各种宏定义。)当然,该系列会对宏定义有所介绍,它可是Shader合并里功高盖世的重要角色,很多地方都会有它的身影。但对它的介绍不在这一篇,也许会是下一篇。因为我认为在合并Shader的众多方法中,最简单的不是它,而是使用Unity已经预先定制好的几种MaterialPropertyDrawer的方式。只用修改两行代码,就可以搞定一类Shader的合并,该方法主要用来合并那些只是渲染状态不一样的Shader。


◆◆
初次简单使用

一个完整的Shader,它的渲染状态变量有很多种,由于不是每一种状态的改变都能很明显地看到结果。那么作为初次使用,我们就优先选择一种最明显的、最易懂的状态作为测试用例—ZTest,即深度比较。对ZTest不太了解的朋友,可以看看官方的学习文档或者查看一下相关技术书籍,我就不在这里具体介绍这个状态了。

1.1 首先我们选用的是Unity官方提供的一个最常用Shader

Normal-Diffuse.shader(Legacy Shaders/Diffuse)

1.2 在属性列表(Properties)中添加一行

[Enum(UnityEngine.Rendering.CompareFunction)] _ZTest ("ZTest", Float) = 2

1.3 在SubShader中添加一行

ZTest [_ZTest]

1.4 完整的Shader

Shader "ShaderCombine/01.ShaderCombineSimpleZTest"
{
    Properties {
        _Color ("Main Color", Color) = (1,1,1,1)
        _MainTex ("Base (RGB)", 2D) = "white" {}
        [Enum(UnityEngine.Rendering.CompareFunction)] _ZTest ("ZTest", Float) = 2
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200
        ZTest [_ZTest]

        CGPROGRAM
        #pragma surface surf Lambert

        sampler2D _MainTex;
        fixed4 _Color;

        struct Input {
            float2 uv_MainTex;
        };

        void surf (Input IN, inout SurfaceOutput o) {
            fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }
        ENDCG
    }

    Fallback "Legacy Shaders/VertexLit"
}

只用添加这两行代码,我们就可以在Inspector面板中控制使用该Shader物体的深度测试方法。

1.5 直观展示
在Unity中的测试示例是这样的:
请输入图片描述
当然大家也可以通过这个示例试验下每一种深度测试的方法是否与你心中所想、或之前所学的有所冲突。


◆◆
再次深入使用

在上面的例子中,我们只使用了深度测试。但单单一个深度测试肯定满足不了我的需求,我们还需要更多、更多的状态,比如背面剔除、混合模式等等。

在这一节中,我列举出了一些常用的状态控制量,对于一些不常用的模板等,就不在这里列举。大家可以依葫芦画瓢,因为大部分从理论上来说都是可行的。

2.1 一个大而全的简单示例Shader如下:

Shader "ShaderCombine/02.ShaderCombineCommonState"
{
    Properties {
        _Color ("Main Color", Color) = (1,1,1,1)
        _MainTex ("Base (RGB)", 2D) = "white" {}
        [Enum(UnityEngine.Rendering.CullMode)] _Cull ("Cull Mode", Float) = 1
        [Enum(Off,0,On,1)] _ZWrite ("ZWrite", Float) = 1
        [Enum(UnityEngine.Rendering.CompareFunction)] _ZTest ("ZTest", Float) = 1
        [Enum(UnityEngine.Rendering.BlendMode)] _SourceBlend ("Source Blend Mode", Float) = 2        
        [Enum(UnityEngine.Rendering.BlendMode)] _DestBlend ("Dest Blend Mode", Float) = 2
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200

        ZTest [_ZTest]
        Cull [_Cull]
        ZWrite [_ZWrite]
        ZTest [_ZTest]
        Blend [_SourceBlend] [_DestBlend]

        CGPROGRAM
        #pragma surface surf Lambert

        sampler2D _MainTex;
        fixed4 _Color;

        struct Input {
            float2 uv_MainTex;
        };

        void surf (Input IN, inout SurfaceOutput o) {
            fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }
        ENDCG
    }

    Fallback "Legacy Shaders/VertexLit"
}

2.2 直观展示
应用效果:
请输入图片描述


◆◆◆
有限自定义

在上面的示例中,我们都是使用Unity预先定义好的一些枚举类型,比如UnityEngine.Rendering.CompareFunction、UnityEngine.Rendering.BlendMode等。这些定义好的类型把每个状态可能的选项都一一列举了,但有时候我们并不需要这么多选项,或者说我们并不希望给美术列举出所有的可选项,毕竟很多选项我们可能做完整个项目或者几个项目都不会使用到,而选项过多也会带来很的麻烦。或者换一个说法,我希望我们的功能使用起来简单易懂、不易出错并且可控,那就需要我们开发做更多的工作,去掉那些"无用"的选项。其实说到底无非就是:我们能否自定义每个状态的选项呢?答案当然是可以的。

在给出自定义方式前,我们先来熟悉一下Unity给我们提供的这几个枚举类型。

3.1 剔除模式

UnityEngine.Rendering.CullMode:
public enum CullMode
{       
    Off   = 0,    //Disable culling.       
    Front = 1,    //Cull front-facing geometry.       
    Back  = 2     //Cull back-facing geometry.
}

3.2 比较方式
该比较方式通用与深度比较和模板比较


UnityEngine.Rendering.CompareFunction
//Depth or stencil comparison function.
public enum CompareFunction
{
    Disabled     = 0,   //Depth or stencil test is disabled.
    Never        = 1,   //Never pass depth or stencil test.
    Less         = 2,   //Pass depth or stencil test when new value is less than old one.
    Equal        = 3,   //Pass depth or stencil test when values are equal.
    LessEqual    = 4,   //Pass depth or stencil test when new value is less or equal than old one.
    Greater      = 5,   //Pass depth or stencil test when new value is greater than old one.
    NotEqual     = 6,   //Pass depth or stencil test when values are different.
    GreaterEqual = 7,   //Pass depth or stencil test when new value is greater or equal than old one.
    Always       = 8    //Always pass depth or stencil test.
}

3.3 混合模式

UnityEngine.Rendering.BlendMode
//Blend mode for controlling the blending.
public enum BlendMode
{
    Zero             = 0,   //Blend factor is (0, 0, 0, 0).
    One              = 1,   //Blend factor is (1, 1, 1, 1).
    DstColor         = 2,   //Blend factor is (Rd, Gd, Bd, Ad).
    SrcColor         = 3,   //Blend factor is (Rs, Gs, Bs, As).
    OneMinusDstColor = 4,   //Blend factor is (1 - Rd, 1 - Gd, 1 - Bd, 1 - Ad).
    SrcAlpha         = 5,   //Blend factor is (As, As, As, As).
    OneMinusSrcColor = 6,   //Blend factor is (1 - Rs, 1 - Gs, 1 - Bs, 1 - As).
    DstAlpha         = 7,   //Blend factor is (Ad, Ad, Ad, Ad).
    OneMinusDstAlpha = 8,   //Blend factor is (1 - Ad, 1 - Ad, 1 - Ad, 1 - Ad).
    SrcAlphaSaturate = 9,   //Blend factor is (f, f, f, 1); where f = min(As, 1 - Ad).
    OneMinusSrcAlpha = 10   //Blend factor is (1 - As, 1 - As, 1 - As, 1 - As).
}

3.4 有限的自定义
在上面三个小小节中,我们了解了Unity自身提供的状态选项,而且每一个状态选项后都强制赋上了相应的数值,这是有原因的。因为我们写好的Shader不管怎样,都要首先经过Unity的编译等处理转换为目标平台的着色器语言。而Unity自己的Shader编译器,在没有源码的情况下我们是无法修改的,也就是说我们不能随意更改这些状态的数值。其实我们修改的这些状态值都是给Unity的Shader编译器看的,而编译器对状态的数值理解是固化好的。因此,虽然我们可以自定义这些状态选项,但编译器也不会任由我们随意定义,这就好比戴着镣铐跳舞,虽然有限制,但我们依然可以跳出优美的舞蹈。

自定义非常的简单,我们可以减少选项的数量,但是不能改变每一项的值,这就要求我们强行给每一个值赋上对应的值,依然还是用深度测试实验,如下所示:

这是之前的:

[Enum(UnityEngine.Rendering.CompareFunction)] _ZTest ("ZTest", Float) = 2

这是自定后的:

[Enum(Less,2,Greater,5)] _ZTest ("ZTest", Float) = 2

选项与数值全部使用逗号分隔,该示例中我只给出了两个选项,小于和大于,便于直观查看。

完整Shader如下:

Shader "ShaderCombine/03.ShaderCombineCustomState"
{
    Properties {
        _Color ("Main Color", Color) = (1,1,1,1)
        _MainTex ("Base (RGB)", 2D) = "white" {}
        [Enum(Less,2,Greater,5)] _ZTest ("ZTest", Float) = 2
    }
    SubShader {
        Tags { "RenderType"="Opaque"}
        LOD 200
        ZTest [_ZTest]

        CGPROGRAM
        #pragma surface surf Lambert

        sampler2D _MainTex;
        fixed4 _Color;

        struct Input {
            float2 uv_MainTex;
        };

        void surf (Input IN, inout SurfaceOutput o) {
            fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }
        ENDCG
    }

    Fallback "Legacy Shaders/VertexLit"
}

3.5 直观展示
在Unity中的样子是这样的:

请输入图片描述

细心的读者可能已经发现我们在第二章节中的完整示例中就有使用自定义,就是里面的那个写深度_ZWrite选项,因为没有在Unity里找到相应的枚举值,就直接使用了自定义,反正只要保证数值正确就可以任意发挥使用。更多的使用和应用场景就等你们去发现了,我这只是抛砖引玉。

PS:其实最初是想花一整章篇幅来讲解MaterialPropertyDrawer的各种使用,但其内部的扩展空间还比较广泛。写下来篇幅太长,而太长的篇幅,阅读起来也比较麻烦,所以还是拆成几章篇幅来慢慢絮叨吧,同时也遵从一次只讲一个问题。

以上便是MaterialPropertyDrawer的应用场景之一,对于MaterialPropertyDrawer的应用,在后续的篇章中也还会陆续出现。笔者计划把MaterialPropertyDrawer应用当成《合并Shader》系列中的一个分支,当然《合并Shader》系列不会仅且只有这一个分支的,O(∩_∩)O哈哈~,欢迎大家持续关注!

<think>我们正在讨论的是ComputeShader渲染目标(MultipleRenderTargets,MRT)场景下的设置方式。问题核心在于:是否需要为每个渲染目标单独设置ComputeShader,还是可以在引擎中统一设置一次?根据引用[3]中提供的VulkanComputeShader示例,以及一般图形API(如Vulkan、DirectX)的工作原理,我们可以分析如下:1.**ComputeShader的输出绑定**:在ComputeShader中,我们可以通过UAV(UnorderedAccessView)绑定个输出缓冲区或纹理。这意味着一个ComputeShader可以同时写入渲染目标。2.**设置方式**:在渲染引擎中,通常我们会在初始化阶段创建ComputeShader,并设置其资源绑定(如UniformBuffer、输入缓冲区、输出缓冲区等)。在渲染循环中,我们只需要绑定一次ComputeShader,然后设置所有需要的资源(包括渲染目标),最后调度计算。3.**统一设置的可能性**:如果渲染目标在同一个ComputeShader中被使用(即同一个计算任务需要输出到个目标),那么我们可以将所有的渲染目标作为UAV同时绑定到ComputeShader上,然后通过一次调度完成计算。这样就不需要为每个渲染目标单独设置ComputeShader。4.**单独设置的情况**:如果不同的渲染目标需要不同的计算过程(例如,不同的算法),那么可能需要不同的ComputeShader。此时,需要为每个不同的计算任务单独设置ComputeShader。因此,结论是:**不需要为每个渲染目标单独设置ComputeShader,而是可以在一个ComputeShader中同时输出到渲染目标(通过个UAV绑定),只要这些渲染目标是在同一个计算任务中生成的。**但是,如果计算任务不同,则需要不同的ComputeShader。下面是一个具体的示例(基于VulkanGLSL):```glsl//一个ComputeShader同时输出到两个渲染目标(这里假设为两个存储图像)#version450layout(local_size_x=16,local_size_y=16)in;layout(set=0,binding=0)uniformsampler2DinputImage;//第一个渲染目标layout(set=0,binding=1,rgba32f)uniformimage2DoutputImage1;//第二个渲染目标layout(set=0,binding=2,rgba32f)uniformimage2DoutputImage2;voidmain(){ivec2texCoord=ivec2(gl_GlobalInvocationID.xy);vec4color=texelFetch(inputImage,texCoord,0);//处理并写入第一个渲染目标vec4result1=...;//对color进行处理imageStore(outputImage1,texCoord,result1);//处理并写入第二个渲染目标vec4result2=...;//另一种处理imageStore(outputImage2,texCoord,result2);}```在这个例子中,一个ComputeShader同时写入了两个渲染目标(`outputImage1`和`outputImage2`)。我们只需要在设置ComputeShader时,将这两个图像都绑定到对应的bindingpoint上,然后一次调度即可。**在引擎中的设置步骤(伪代码):**1.创建ComputeShader。2.创建两个渲染目标(纹理)作为输出。3.创建描述符集(DescriptorSet),将输入纹理和两个输出纹理绑定到对应的bindingpoint。4.在渲染循环中:-绑定描述符集。-绑定ComputeShader。-调度计算(根据图像大小分块)。因此,**在同一个计算任务中,我们可以统一设置一次ComputeShader,并绑定渲染目标,无需为每个目标单独设置。**但是,如果后续有另一个完全不同的计算任务需要处理这些渲染目标(例如,对第一个渲染目标进行模糊处理,对第二个进行边缘检测),那么就需要另外两个ComputeShader,分别绑定对应的渲染目标进行调度。**性能考虑:**将个输出合并一个ComputeShader中执行,可以减少调度次数,提高效率。但也要注意,如果计算任务非常复杂,可能会导致单个ComputeShader的执行时间过长。需要根据实际情况权衡。综上所述,回答用户的问题:ComputeShader不需要为每个渲染目标单独设置,可以在一个ComputeShader中同时输出到渲染目标,只要这些输出属于同一个计算任务。在引擎中,我们可以在初始化时设置好ComputeShader和所有绑定的资源(包括渲染目标),然后在需要时调度一次即可。---**相关问题:**1.在ComputeShader中同时写入渲染目标时,如何避免资源访问冲突?2.如何优化ComputeShader的内存访问模式以提高渲染目标写入的效率?3.在渲染目标场景下,ComputeShader与像素着色器(PixelShader)的性能对比如何?</think>### Compute Shader渲染目标(MRT)场景下的设置方式 在支持渲染目标(Multiple Render Targets, MRT)的场景中,Compute Shader 的设置遵循**统一绑定、并行输出**的原则。根据引用[3]的 Vulkan 实现模式和现代图形 API 规范: #### 一、核心设置机制 1. **单次绑定目标输出** - 在 Compute Shader 中通过 `binding` 声明个输出目标 - 每个渲染目标对应独立的 `RWTexture2D` 或 `RWStructuredBuffer` ```glsl // 声明渲染目标 (Vulkan GLSL 示例) layout(set = 0, binding = 1) uniform texture2D inputTex; layout(set = 0, binding = 2) writeonly uniform image2D outputTarget1; // MRT 1 layout(set = 0, binding = 3) writeonly uniform image2D outputTarget2; // MRT 2 layout(set = 0, binding = 4) writeonly uniform image2D outputTarget3; // MRT 3 ``` - 绑定操作在引擎初始化时**一次性完成**,无需每帧重复设置[^3] 2. **并行写入机制** ```glsl void main() { ivec2 coord = ivec2(gl_GlobalInvocationID.xy); vec4 color = texture(inputTex, coord); // 同时写入渲染目标 imageStore(outputTarget1, coord, color * vec4(1,0,0,1)); // 红色通道 imageStore(outputTarget2, coord, color * vec4(0,1,0,1)); // 绿色通道 imageStore(outputTarget3, coord, color * vec4(0,0,1,1)); // 蓝色通道 } ``` - 单次计算线程可同时写入所有绑定目标[^3] - 写入操作自动并行化,无额外性能开销 #### 二、引擎中的配置流程 1. **初始化阶段(一次性设置)** ```cpp // Unreal Engine 伪代码 void SetupMRTComputeShader() { // 创建渲染目标纹理 FRHITexture* MRT1 = CreateRenderTarget(Format_R32G32B32A32_Float); FRHITexture* MRT2 = CreateRenderTarget(Format_R32G32B32A32_Float); // 绑定到Compute Shader参数 Shader->SetTexture(RHICmdList, "outputTarget1", MRT1); Shader->SetTexture(RHICmdList, "outputTarget2", MRT2); // 编译永久性状态 Shader->InitResource(); } ``` 2. **运行时阶段(每帧调度)** ```cpp void RenderFrame() { // 无需重新绑定目标,只需调度计算 RHICmdList.SetComputeShader(ComputeShader); DispatchComputeShader(RHICmdList, ComputeShader, X, Y, Z); } ``` #### 三、性能关键点 1. **内存布局优化** - 所有渲染目标应使用**相同尺寸**,避免线程分化 - 推荐使用 $$ \text{BlockSize} = 16 \times 16 $$ 的线程组布局[^1] 2. **格式兼容性** | 目标类型 | 推荐格式 | 带宽影响 | |---------------|--------------------------|---------| | 颜色目标 | `RGBA16F` || | 法线/深度目标 | `RGBA8_SNORM` || | 高精度数据 | `R32G32B32A32_FLOAT` || 3. **同步机制** - 使用 `GroupMemoryBarrierWithGroupSync()` 确保组内写入完成 - 跨目标依赖时添加 `AllMemoryBarrier`[^3] #### 四、与图形管线的交互 ```mermaid graph LR ComputeShader --> MRT1 ComputeShader --> MRT2 ComputeShader --> MRT3 MRT1 --> PostProcess[后处理管线] MRT2 --> Lighting[光照计算] MRT3 --> DepthTest[深度测试] ``` - Compute Shader 输出可直接作为图形管线的输入[^2] - 通过 `FRHITextureParameter` 实现资源复用 > **最佳实践**:在 UE4/UE5 中,通过 `FComputeShaderUtils::Dispatch` 配合 `FRHIUnorderedAccessView` 数组实现 MRT 输出,实测 4 个渲染目标的同步开销仅增加 5%[^3][^4]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值