当前位置: 首页 > news >正文

Custom SRP - Draw Calls

https://catlikecoding.com/unity/tutorials/custom-srp/draw-calls/

目标:

  • 书写我们的 hlsl shader

  • 支持 SRP batcher, Gpu Instancing, dynamic batching

  • 配置每个对象的材质属性,并随机渲染大量物体

  • 创建透明和镂空材质

1. Shaders

1.1 Unlit

我们的第一个例子是纯色,无光照的 shader。

通过 Assets/Create/Shader/UnlitShader ,在 Custom RP/Shaders/ 下创建 shader。我们要从头开始写我们的 shader,因此删除所有内容。

Shader "Custom RP/Unlit"
{Properties { }SubShader{Pass{}}
}
  • Shader 定义,后面的字符串,是在编辑器材质编辑时,选择 shader 的下拉列表框的路径

  • Properties 块定义材质属性,这些属性可以在材质编辑器中编辑

  • SubShader 块,定义一个 Pass

    • Pass 定义一种渲染方式

上面代码块都是空的,Unity 会给一个默认实现,将其渲染成实心白色,并且默认渲染队列为 2000,是默认不透明集合体的渲染队列。同时还有双面渲染开关。

1.2 HLSL

Unity shader 用 HLSL 书写,在 Pass 中,HLSLPROGRAM and ENDHLSL 关键字之间。

Shader "Custom RP/Unlit"
{Properties { }SubShader{Pass{}}
}

Shader 主要有2种

  • vertex 顶点变换,将顶点变换到设备空间。通过 #pragma vertex vertex_func_name 声明

  • fragment 渲染像素。通过 #pragma fragment fragment_func_name 声明

声明的 vertex/fragment 函数,可以直接写在 HLSLPROGRAM/ENDHLSL 之间,也可以写在其它的 .hlsl 文件中,并通过 #include “xxx.hlsl" 包含进来。

HLSLPROGRAM
#pragma vertex UnlitPassVertex
#pragma fragment UnlitPassFragment
#include "UnliePass.hlsl"
ENDHLSL

下面简单实现我们的 shader:

#ifndef CUSTOM_UNLIT_PASS_INCLUDED
#define CUSTOM_UNLIT_PASS_INCLUDEDfloat4 UnlitPassVertex() : SV_POSITION 
{return .0f;
}float4 UnlitPassFragment() : SV_TARGET
{return .0f;
}#endif
  • hlsl 文件可能被个文件引用包含,为了保证多次包含,只引入一次代码,需要用到宏来确保

  • shader 入口函数的返回值,需要有“语义”来修饰,以告诉GPU这些值用来干什么,比如上面:

    • SV_POSITION 告诉 GPU 返回值是齐次空间的顶点位置。

    • SV_TARGET 告诉 GPU 将颜色合并到 render target。

1.3 Space Transformation

vertex shader 主要工作就是将顶点变换到正确的空间。因此 shader 需要输入一个位置参数,位置参数用 POSITION 语义修饰。

顶点变换需要一些矩阵,由CPU在渲染对象时传入。这些输入都是类似的,为了后面复用,我们把这些输入定义到一个单独的文件 ShaderLibrary/UnityInput.hlsl 中。

#ifndef UNITY_INPUT_INCLUDED
#define UNITY_INPUT_INCLUDEDfloat4x4 unity_ObjectToWorld;
float4x4 unity_MatrixVP;#endif

我们需要一些变换函数,将顶点变换到对应的空间。这些函数也是通用的,因此放到 ShaderLibrary/Common.hlsl 中

#ifndef COMMON_INCLUDED
#define COMMON_INCLUDED#include "UnityInput.hlsl"float3 TransformObjectToWorld(float3 positionOS)
{return mul(unity_ObjectToWorld, float4(positionOS, 1.0f)).xyz;}float4 TransformWorldToHClip(float3 positionWS)
{return mul(unity_MatrixVP, float4(positionWS, 1.0f));
}#endif

Unlit.hlsl 现在变成

#ifndef CUSTOM_UNLIT_PASS_INCLUDED
#define CUSTOM_UNLIT_PASS_INCLUDED#include "../ShaderLibrary/Common.hlsl"float4 UnlitPassVertex(float3 positionOS : POSITION) : SV_POSITION
{float3 positionWS = TransformObjectToWorld(positionOS);return TransformWorldToHClip(positionWS);
}float4 UnlitPassFragment() : SV_TARGET
{return .0f;
}#endif

1.4 core library

我们定义的两个变换函数,实际上已经在 Core RP Library package 中定义了,同时还定义了很多很有用的必须函数或其它定义。安装这个 package。

在我们的 Common.hlsl 中包含 Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl

SpaceTransforms.hlsl 中使用的是用宏定义的常量,所以我们定义这些宏。后面会讨论用宏的原因。

Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl 中定义了一些类型,如 real4,根据不同平台,可能是 float4 或 half4。

我们的 shader 现在是这样的:

UnityInput.hlsl

#ifndef UNITY_INPUT_INCLUDED
#define UNITY_INPUT_INCLUDEDfloat4x4 unity_ObjectToWorld;
float4x4 unity_WorldToObject;
real4 unity_WorldTransformParams;float4x4 unity_MatrixVP;
float4x4 unity_MatrixV;
float4x4 unity_MatrixInvV;
float4x4 unity_prev_MatrixM;
float4x4 unity_prev_MatrixIM;
float4x4 glstate_matrix_projection;#endif

Common.hlsl

#ifndef COMMON_INCLUDED
#define COMMON_INCLUDED#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"#include "UnityInput.hlsl"#define UNITY_MATRIX_M unity_ObjectToWorld
#define UNITY_MATRIX_I_M unity_WorldToObject
#define UNITY_MATRIX_V unity_MatrixV
#define UNITY_MATRIX_I_V unity_MatrixInvV
#define UNITY_MATRIX_VP unity_MatrixVP
#define UNITY_PREV_MATRIX_M unity_prev_MatrixM
#define UNITY_PREV_MATRIX_I_M unity_prev_MatrixIM
#define UNITY_MATRIX_P glstate_matrix_projection#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"//float3 TransformObjectToWorld(float3 positionOS)
//{
//    return mul(unity_ObjectToWorld, float4(positionOS, 1.0f)).xyz;//}//float4 TransformWorldToHClip(float3 positionWS)
//{
//    return mul(unity_MatrixVP, float4(positionWS, 1.0f));
//}#endif

1.5 Color

  • 通过定义 shader 常量,来定义材质的颜色,像素着色时直接返回这个颜色

float _BaseColor;float4 UnlitPassFragment() : SV_TARGET
{return _BaseColor;
}

常量前面的下划线,是告诉 shader,该常量将会被当作材质属性。

  • 通过 .shader 的 Properties ,可以在材质面板上编辑这个属性

    Properties { _BaseColor("Color", Color) = (1.0,1.0,1.0,1.0)}

属性语法规则:常量的名字("在材质面板上显示的名字", 属性类型) = 默认值

1. Batching

每次绘制,都需要 CPU 和 GPU 之间的异步操作。如果CPU向GPU传递的数据太多,就会导致浪费时间在等待(传递完成)上。同时,CPU就没有时间处理其它任务了。这最终都会导致 FPS 降低。

创建一个有80个小球的场景,分别用4个我们 shader 创建的材质:红,绿,黄,蓝色。这需要82次 draw call,80个是小球渲染,一个渲染天空盒,还有一个清理 render target。

2.1 SRP Batcher

Batching 是用来合并 draw call(按照这里的上下文,draw call 不是指 api 级别的 draw call,而是指的 unity 定义的 draw call:准备 material/object constant buffer,提交到GPU,绑定,draw),降低CPU/GPU同步的时间消耗的。

通过开启 SRP batcher 可以做到这一点,但是必须按照 SRP batcher 的要求来定义我们的 shader,否则会提示不兼容:

要兼容 SRP batcher,需要将我们的 constant buffer 定义成结构体,并且使用其命名规范:

  • UnityPerMaerial 每个材质的常量

  • UnityPerDraw 每个对象的常量

cbuffer UnityPerMaterial{float _BaseColor;
}

这里有个问题:不是所有的硬件/API都支持 constant buffer,为了兼容这种情况,unity 提供了一组宏来定义 cbuffer:

CBUFFER_START(UnityPerMaterial)float4 _BaseColor;
CBUFFER_END

对于我们的 shader,需要把对象绘制常量做类似的修改:

CBUFFER_START(UnityPerDraw)float4x4 unity_ObjectToWorld;float4x4 unity_WorldToObject;float4 unity_LODFade;    // 后面有用,先写上real4 unity_WorldTransformParams;
CBUFFER_END

如此改完后,我们的 shader 就是兼容 SRP batcher 的了。通过在Project面板中,选中我们的 Unlit.shader 可以看到。

但这还不够,还要在代码中开启 batching:

public CustomRenderPipeline(){GraphicsSettings.useScriptableRenderPipelineBatching = true;
}

最后,在 FrameDebugger 中可以看到只有一个 SRP Batch:

原理:

PerMaterial/PerDraw constant 的数据被整理到一个 GPU Buffer 上,然后提交到GPU,只要在这之后常量没有发生变化,就不需要更新/提交。唯一的限制是 constant buffer layout 必须要一致,也就是说它们是一个 shader 变体。

2.2 MaterialPropertyBlock

如果我们希望每个小球都有自己的颜色,那么我们需要为每个小球创建一个材质,这工作量很大,其实不需要这样,我们可以利用 MaterialPropertyBlock:

创建一个 PerObjectMaterialProperties 的脚本来定义每个小球的颜色,并在合适的时机,通过 MaterialPropertyBlock 进行应用。

[DisallowMultipleComponent]
public class PerObjectMaterialProperties : MonoBehaviour
{static int baseColorId = Shader.PropertyToID("_BaseColor");[SerializeField]private Color baseColor = Color.white;static MaterialPropertyBlock matPropBlock;private void Awake(){OnValidate();}private void OnValidate(){if (matPropBlock == null)matPropBlock = new MaterialPropertyBlock();matPropBlock.SetColor(baseColorId, baseColor);GetComponent<Renderer>().SetPropertyBlock(matPropBlock);}
}

OnValidate 仅在编辑器,该脚本被加载,以及脚本属性被修改时调用。因此需要在 Awake 中主动调用。

不幸的是,应用了 MaterialPropertyBlock 之后,SRP batcher 将失效。

2.3 GPU Instancing

GPU Instancing 是管线将 mesh 相同,材质也相同的 draw call ,将对象的变换和材质属性收集起来,放到一个数组中提交给 GPU,通过一次 draw call,GPU 遍历每个对象的数据进行渲染。

启用 GPU Instancing,需要在 .shader 中,声明 vertex/fragment shader 前,声明 multi_compile_instancing:

#pragma multi_compile_instancing
#pragma vertex UnlitPassVertex
#pragma fragment UnlitPassFragment

hlsl 中,instancing 相关的定义是在 UnityInstancing.hlsl 中的,因此需要在我们的 Common.hlsl 中,在 SpaceTransforms.hlsl 之前将其包含进来:

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"

UnityInstancing.hlsl 主要是定义了 instancing 相关的一些宏:

  • UNITY_VERTEX_INPUT_INSTANCE_ID 为 vertex/fragment input 声明 instance id

  • UNITY_SETUP_INSTANCE_ID 准备,使 instance id 有效

  • UNITY_TRANSFER_INSTANCE_ID 将 instance id 传递到下个结构体

  • UNITY_ACCESS_INSTANCED_PROP 根据UNITY_SETUP_INSTANCE_ID 准备好的 instance id,访问当前 instance 的属性。

  • UNITY_INSTANCING_BUFFER_START/UNITY_INSTANCING_BUFFER_END

  • UNITY_DEFINE_INSTANCED_PROP

首先,将 BaseColor 声明到 instancing buffer 中:

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)float4 _BaseColor;
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

将顶点输入数据,定义成结构体,并声明 instance id 输入:

struct Attributes{float3 positionOS : POSITION;UNITY_VERTEX_INPUT_INSTANCE_ID
};

顶点返回,传递给 fragment 的值,也定义到结构体中,并声明 instance id 输入:

struct Varyings{float4 positionCS : SV_POSITION;UNITY_VERTEX_INPUT_INSTANCE_ID
};

修改 vertex shader:

Varying UnlitPassVertex(Attributes input){Varyings output;UNITY_SETUP_INSTANCE_ID(input);UNITY_TRANSFER_INSTANCE_ID(input, output);float3 positionWS = TransformObjectToWorld(input.positionOS);output.positionCS = TransformWorldToHClip(positionWS);return output;
}

最后修改 fragment shader:

float4 UnlitPassFragment(Varyings input) : SV_TARGET{UNITY_SETUP_INSTANCE_ID(input);return UNITY_ACCESS_INSTANCE_PROP(UnityPerMaterial, _BaseColor);
}

最后,要看到效果,记得先把 SRP batching 关掉。

2.4 Graphics.DrawMeshInstanced

GPU Instancing 在绘制成百的对象时有巨大的提升,但是在场景中编辑这么多对象不太现实。在某些情况下,可能需要通过一种程序化的方式,创建,渲染大量对象。

下面的例子,随机生成了1023个球,并且将它们是变换,颜色,分别收集起来,通过 MaterialPropertyBlock 应用这些球的颜色,最后通过 Graphics.DrawMeshInstanced 进行渲染:

public class MeshBall : MonoBehaviour
{[SerializeField]Mesh mesh = default;[SerializeField]Material material = default;static int colorID = Shader.PropertyToID("_BaseColor");MaterialPropertyBlock matPropBlock = new MaterialPropertyBlock();Matrix4x4[] matrices = new Matrix4x4[1023];Vector4[] baseColors = new Vector4[1023];private void Awake(){for (int i = 0; i < matrices.Length; i++){matrices[i] = Matrix4x4.TRS(Random.insideUnitSphere * 10f, Quaternion.identity, Vector3.one);baseColors[i] =new Vector4(Random.value, Random.value, Random.value, 1f);}matPropBlock.SetVectorArray(colorID, baseColors);}void Update(){Graphics.DrawMeshInstanced(mesh, 0, material, matrices, 1023, matPropBlock);}
}

这些小球以创建的顺序渲染,而且无法被裁剪,因为我们直接调用了 Graphics 的接口。

2.5 Dynamic Batching

对于那些使用同一个材质,且面数很低的模型,可以将这些对象的 mesh 合并成一个 mesh,一次 draw call 完成渲染。

通过配置 DrawingSettings 启用该功能:

var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings){enableDynamicBatching = true,enableInstancing = false};

同时禁用 SRP batcher:

GraphicsSettings.useScriptableRenderPipelineBatching = false;

还有 static batching,原理同 dynamic batching,却别是离线进行合并。

2.6 Configuring Batching

我们希望在我们的RP中,将这些 batching 策略,作为选项进行配置。

首先 DrawVisibleGeometry 支持这些开关:

void DrawVisibleGeometry(bool useDynamicBatching, bool useGPUInstancing)
{// 渲染不透明物体var sortingSettings = new SortingSettings(camera){ criteria = SortingCriteria.CommonOpaque };var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings){ enableDynamicBatching = useDynamicBatching, enableInstancing = useGPUInstancing};...
}

然后在 pipeline asset 声明对应的属性,以便用户编辑,创建管线实例时将参数传递进去,就可以了:

[CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline")]
public class CustomRenderPipelineAsset : RenderPipelineAsset 
{[SerializeField] bool useDynamicBatching = false;[SerializeField] bool useGPUInstancing = false;protected override RenderPipeline CreatePipeline(){return new CustomRenderPipeline(useDynamicBatching, useGPUInstancing);}
}

3. 透明

可以改变材质的 Render Queue 为 Transparent 使材质在透明阶段渲染,但是仅仅这样还没有效果,还需要让 shader 支持混合。

3.1 Blend Mode

首先要在 Pass 定义中声明混合:

Pass {Blend [_SrcBlend] [_DstBlend]HLSLPROGRAM…ENDHLSL}

其次,定义材质属性。这里利用unity定义的枚举定义属性:

[Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend ("Src Blend", Float) = 1
[Enum(UnityEngine.Rendering.BlendMode)] _DstBlend ("Dst Blend", Float) = 0

然后在材质编辑面板中,就可以看到并编辑混合模式了:

Src 指的是当前像素着色器计算的颜色

Dst 指的是当前 render target 上的颜色。

开启混合时,Src Blend 和Dst Blend 分别指定为 SrcAlpha 和 OneMinusSrcAlpha,指示混合公式为 SrcColor.rgb * SrcColor.a + DstColor.rgb * (1-SrcColor.a)。

可以看到效果(下面的不透明的,上面的半透明的):

注意:

  • 记得要把材质中的颜色的 alpha 值,改为128(0.5)

  • 对于GPU Instancing,由于透明物体是排序渲染的,因此合批是否成功依赖于距离摄像机的距离,所以根据视角的不同,合批结果也会不同。

3.2 Not Writting Depth

半透明渲染不需要写深度,因此我们需要给材质一个开关,当配置为半透明渲染时,禁止写深度。

[Enum(UnityEngine.Rendering.BlendMode)] _DstBlend ("Dst Blend", Float) = 0
[Enum(Off, 0, On, 1)] _ZWrite ("Z Write", Float) = 1
...
Blend [_SrcBlend] [_DstBlend]
ZWrite [_ZWrite]

然后在材质编辑面板关闭

3.3 Texturing

该节介绍如何采样贴图,并使用贴图中的alpha的值作为透明度。

  • 首先在材质属性中定义贴图属性:

    _BaseMap("Texture", 2D) = "white" {}

贴图名为"Texture",类型是 2D,默认是Unity 系统提供的 "white" 白色贴图。后面的 {} 是历史遗留特性,没用,但是不能没有,避免出现奇怪的错误。修改后,材质属性面板显示为:

  • 然后修改hlsl:

    • 声明全局贴图及其采样器变量。采样器变量名是在贴图变量名前加sampler

      TEXTURE2D(_BaseMap);
      SAMPLER(sampler_BaseMap);UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
      UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
    • 采样贴图,需要顶点提供贴图坐标

      struct Attributes {float3 positionOS : POSITION;float2 baseUV : TEXCOORD0;UNITY_VERTEX_INPUT_INSTANCE_ID
      };
    • 在材质中,可以配置贴图坐标的缩放和偏移

      UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)UNITY_DEFINE_INSTANCED_PROP(float4, _BaseMap_ST)UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
      UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
    • 材质坐标在顶点着色器中应用缩放和偏移后,交给光栅化器进行插值

      struct Varyings {float4 positionCS : SV_POSITION;float2 baseUV : VAR_BASE_UV;UNITY_VERTEX_INPUT_INSTANCE_ID
      };Varyings UnlitPassVertex (Attributes input) {…float4 baseST = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseMap_ST);output.baseUV = input.baseUV * baseST.xy + baseST.zw;return output;
      }
  • 最后在片段着色器中完成采样

    float4 UnlitPassFragment (Varyings input) : SV_TARGET {UNITY_SETUP_INSTANCE_ID(input);float4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.baseUV);float4 baseColor = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);return baseMap * baseColor;
    }

效果如下图

3.4 Alpha Clipping

定义一个阈值,在像素着色时,对于那些 alpha 值小于这个阈值的像素,直接丢弃,最终渲染出”镂空“的效果。

  • 首先在材质属性中定义阈值 _Cutoff:

    _Cutoff("Alpha Cutoff", Range(0.0,1.0) = 0.5
  • _Cutoff 是材质参数,因此定义到 UnityPerMaterial 中:

    UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
    UNITY_DEFINE_INSTANCED_PROP(float, _Cutoff)
  • 在像素着色器中,比较 a 的值,如果小于 _Cutoff ,则 clip:

    float4 base = baseColor * baseMap;
    clip(base.a - UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Cutoff);

编辑材质,设置一个 _Cutoff 参数。

同时clipping渲染是在 AlphaTest 队列中渲染的,该队列在不透明物体渲染完后渲染。

3.5 Shader Features

一个材质,半透明和 alpha test,不能同时存在,因此需要一个开关。同时需要让 hlsl 根据开关执行不同的逻辑。

Shader Features 可以实现该特性。

  • 首先在材质属性中添加一个 Feature Toggle 开关,名字为“Alpha Clipping",定义了一个宏关键字:_CLIPPING

    [Toggle(_CLIPPING)] _Clipping("Alpha Clipping", Float) = 0
  • 在 .shader 中声明 shader feature:

    #pragma shader_feature _CLIPPING
  • 在 hlsl 中,用宏将 clip 的代码包起来:

    #if defined(_CLIPPING)
    clip(base.a - UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Cutoff);
    #endif

最后,看看效果:

http://www.lryc.cn/news/596195.html

相关文章:

  • Linux异常与信号处理
  • 11.【C语言学习笔记】指针(三)(回调函数、qsort排序函数、sizeof关键字和strlen函数)
  • Mixed Content错误:“mixed block“ 问题
  • 西门子 S7-1500分布式 I/O通信 :PROFINET IO 与 PROFIBUS DP核心技术详解(上)
  • 知识库搭建之Meilisearch‘s 搜索引擎-创建搜索引擎项目 测评-东方仙盟测评师
  • 【Godot4】状态栏组件StatusBar
  • python中 tqdm ,itertuples 是什么
  • RabbitMQ--批量处理
  • halcon手眼标定z方向实操矫正
  • VUE 中父级组件使用JSON.stringify 序列化子组件传递循环引用错误
  • 机器人氩弧焊保护气降成本的方法
  • Apache Ignite 的 SQL 功能和分布式查询机制
  • 50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | ImageCarousel(图片轮播组件)
  • 深度学习篇---车道线循迹
  • FPGA自学——存储器模型
  • Kafka单条消息长度限制详解及Java实战指南
  • Apache Ignite 中 WHERE 子句中的子查询(Subqueries in WHERE Clause)的执行方式
  • Android 中 实现日期选择功能(DatePickerDialog/MaterialDatePicker)
  • 【无标题】buuctf-re3
  • JAVA中的IO流(四)数据流
  • 一个电脑抓包工具
  • 黄仁勋强调:首先,我是中国人
  • Python进阶第三方库之Numpy
  • 用手机当外挂-图文并茂做报告纪要
  • 云祺容灾备份系统Hadoop备份与恢复实操手册
  • 如何在 Windows 10 下部署多个 PHP 版本7.4,8.2
  • WIFI路由器长期不重启,手机连接时提示无IP分配
  • Android接入RocketMQ的文章链接
  • Spring Boot 使用Jasypt加密
  • Cy3-COOH 花菁染料Cy3-羧基