CommandBuffer携带一系列的渲染命令,依赖相机,用来拓展渲染管线的渲染效果。而且可以指定在相机渲染的某个点执行本身的拓展渲染。Command buffers也可以结合屏幕后期效果使用。
简单来说就是可以在渲染流程中插入一些自定义操作,开发者得到了更多的权限。
左图绿点处就是可以让开发者进行操作的地方,右图蓝色是蓝色线框部分。
1.在unity里首先创建一个C#脚本和一个Shader脚本还有一个默认材质球。
2.将材质赋给场景中的测试物体。
3.进行脚本编写
C#脚本
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
[ExecuteInEditMode]
public class CommandBufferTest01 : MonoBehaviour
{
public Shader shader;
public Renderer render;
public CameraEvent cameraEvent;
private void OnEnable()
{
CommandBuffer tBuffer = new CommandBuffer();
//设置渲染
tBuffer.DrawRenderer(render, new Material(shader));
//不透明物体渲染完后执行
Camera.main.AddCommandBuffer(cameraEvent,tBuffer);
}
private void OnDisable()
{
Camera.main.RemoveAllCommandBuffers();
}
}
Shader脚本:
shader脚本只改下颜色即可,后面作比对用。
然后就可以看到效果:
相机在渲染完不透明物体后又渲染了一遍我们自己添加Buffer。导致Game窗口下物体为Shader中返回的颜色。
camera中也可以看到所添加的Command Buffer。
这样一个最简单的CommandBuffer应用就搞定了。
官方Demo效果:
自己实现:
步骤:
1.先渲染不透明物体
2.抓取屏幕
3.渲染玻璃材质
4.合成
官方代码:
CommandBufferBlurRefraction.cs
using UnityEngine;
using UnityEngine.Rendering;
using System.Collections.Generic;
[ExecuteInEditMode]
public class CommandBufferBlurRefraction : MonoBehaviour
{
public Shader m_BlurShader;
private Material m_Material;
private Camera m_Cam;
// We'll want to add a command buffer on any camera that renders us,
// so have a dictionary of them.
private Dictionary m_Cameras = new Dictionary();
// Remove command buffers from all cameras we added into
private void Cleanup()
{
foreach (var cam in m_Cameras)
{
if (cam.Key)
{
cam.Key.RemoveCommandBuffer (CameraEvent.AfterSkybox, cam.Value);
}
}
m_Cameras.Clear();
Object.DestroyImmediate (m_Material);
}
public void OnEnable()
{
Cleanup();
}
public void OnDisable()
{
Cleanup();
}
// Whenever any camera will render us, add a command buffer to do the work on it
public void OnWillRenderObject()
{
var act = gameObject.activeInHierarchy && enabled;
if (!act)
{
Cleanup();
return;
}
var cam = Camera.current;
if (!cam)
return;
CommandBuffer buf = null;
// Did we already add the command buffer on this camera? Nothing to do then.
if (m_Cameras.ContainsKey(cam))
return;
if (!m_Material)
{
m_Material = new Material(m_BlurShader);
m_Material.hideFlags = HideFlags.HideAndDontSave;
}
buf = new CommandBuffer();
buf.name = "Grab screen and blur";
m_Cameras[cam] = buf;
// copy screen into temporary RT
//获取渲染纹理ID
int screenCopyID = Shader.PropertyToID("_ScreenCopyTexture");
//用screenCopyID标识,获取一个模板纹理。
buf.GetTemporaryRT (screenCopyID, -1, -1, 0, FilterMode.Bilinear);
//BuiltinRenderTextureType.CurrentActive也就是当前屏幕的纹理标识,将其赋值进screenCopyID标识的纹理。
buf.Blit (BuiltinRenderTextureType.CurrentActive, screenCopyID);
// get two smaller RTs
//获取2个模板纹理,后面将它们2的效果轮流混合。
int blurredID = Shader.PropertyToID("_Temp1");
int blurredID2 = Shader.PropertyToID("_Temp2");
buf.GetTemporaryRT (blurredID, -2, -2, 0, FilterMode.Bilinear);
buf.GetTemporaryRT (blurredID2, -2, -2, 0, FilterMode.Bilinear);
// downsample screen copy into smaller RT, release screen RT
//将抓屏纹理_ScreenCopyTexture采样填充到_Temp1后释放。
buf.Blit (screenCopyID, blurredID);
buf.ReleaseTemporaryRT (screenCopyID);
// horizontal blur
//修改SeparableBlur.shader的全局参数offsets,使其产生各个方向的模糊效果来叠加增强效果。
//(如果着色器不在 Properties模块中暴露某个参数,将使用全局属性)
buf.SetGlobalVector("offsets", new Vector4(2.0f/Screen.width,0,0,0));
//使用m_Material渲染blurredID结果赋值给blurredID2。
buf.Blit (blurredID, blurredID2, m_Material);
// vertical blur
buf.SetGlobalVector("offsets", new Vector4(0,2.0f/Screen.height,0,0));
buf.Blit (blurredID2, blurredID, m_Material);
// horizontal blur
buf.SetGlobalVector("offsets", new Vector4(4.0f/Screen.width,0,0,0));
buf.Blit (blurredID, blurredID2, m_Material);
// vertical blur
buf.SetGlobalVector("offsets", new Vector4(0,4.0f/Screen.height,0,0));
buf.Blit (blurredID2, blurredID, m_Material);
//得到透明效果纹理后,赋值给GlassWithoutGrab.shader的_GrabBlurTexture混合得出模糊透明。
buf.SetGlobalTexture("_GrabBlurTexture", blurredID);
cam.AddCommandBuffer (CameraEvent.AfterSkybox, buf);
}
}
SeparableBlur.shader
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Hidden/SeparableGlassBlur" {
Properties {
_MainTex ("Base (RGB)", 2D) = "" {}
}
CGINCLUDE
#include "UnityCG.cginc"
struct v2f {
float4 pos : POSITION;
float2 uv : TEXCOORD0;
float4 uv01 : TEXCOORD1;
float4 uv23 : TEXCOORD2;
float4 uv45 : TEXCOORD3;
};
float4 offsets;
sampler2D _MainTex;
//偏移定点uv,使其读取到周围颜色值进行混合。
v2f vert (appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord.xy;
o.uv01 = v.texcoord.xyxy + offsets.xyxy * float4(1,1, -1,-1);
o.uv23 = v.texcoord.xyxy + offsets.xyxy * float4(1,1, -1,-1) * 2.0;
o.uv45 = v.texcoord.xyxy + offsets.xyxy * float4(1,1, -1,-1) * 3.0;
return o;
}
//混合颜色值。
half4 frag (v2f i) : COLOR {
half4 color = float4 (0,0,0,0);
color += 0.40 * tex2D (_MainTex, i.uv);
color += 0.15 * tex2D (_MainTex, i.uv01.xy);
color += 0.15 * tex2D (_MainTex, i.uv01.zw);
color += 0.10 * tex2D (_MainTex, i.uv23.xy);
color += 0.10 * tex2D (_MainTex, i.uv23.zw);
color += 0.05 * tex2D (_MainTex, i.uv45.xy);
color += 0.05 * tex2D (_MainTex, i.uv45.zw);
return color;
}
ENDCG
Subshader {
Pass {
ZTest Always Cull Off ZWrite Off
Fog { Mode off }
CGPROGRAM
#pragma fragmentoption ARB_precision_hint_fastest
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
Fallback off
GlassWithoutGrab.shader
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
// Similar to regular FX/Glass/Stained BumpDistort shader
// from standard Effects package, just without grab pass,
// and samples a texture with a different name.
Shader "FX/Glass/Stained BumpDistort (no grab)" {
Properties {
_BumpAmt ("Distortion", range (0,64)) = 10
_TintAmt ("Tint Amount", Range(0,1)) = 0.1
_MainTex ("Tint Color (RGB)", 2D) = "white" {}
_BumpMap ("Normalmap", 2D) = "bump" {}
}
Category {
// We must be transparent, so other objects are drawn before this one.
Tags { "Queue"="Transparent" "RenderType"="Opaque" }
SubShader {
Pass {
Name "BASE"
Tags { "LightMode" = "Always" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata_t {
float4 vertex : POSITION;
float2 texcoord: TEXCOORD0;
};
struct v2f {
float4 vertex : POSITION;
float4 uvgrab : TEXCOORD0;
float2 uvbump : TEXCOORD1;
float2 uvmain : TEXCOORD2;
UNITY_FOG_COORDS(3)
};
float _BumpAmt;
half _TintAmt;
float4 _BumpMap_ST;
float4 _MainTex_ST;
v2f vert (appdata_t v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
#if UNITY_UV_STARTS_AT_TOP
float scale = -1.0;
#else
float scale = 1.0;
#endif
//将顶点裁剪空间下的坐标,计算NDC空间上的位置后,映射到模糊纹理的uv。
o.uvgrab.xy = (float2(o.vertex.x, o.vertex.y*scale) + o.vertex.w) * 0.5;
o.uvgrab.zw = o.vertex.zw;
o.uvbump = TRANSFORM_TEX( v.texcoord, _BumpMap );
o.uvmain = TRANSFORM_TEX( v.texcoord, _MainTex );
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
//抓屏得到的模糊纹理会赋值到此。(因为Properties没有定义_GrabBlurTexture,所以默认使用全局的参数)
sampler2D _GrabBlurTexture;
float4 _GrabBlurTexture_TexelSize;
sampler2D _BumpMap;
sampler2D _MainTex;
half4 frag (v2f i) : SV_Target
{
// calculate perturbed coordinates
// we could optimize this by just reading the x & y without reconstructing the Z
half2 bump = UnpackNormal(tex2D( _BumpMap, i.uvbump )).rg;
float2 offset = bump * _BumpAmt * _GrabBlurTexture_TexelSize.xy;
i.uvgrab.xy = offset * i.uvgrab.z + i.uvgrab.xy;
//对模糊纹理采样
half4 col = tex2Dproj (_GrabBlurTexture, UNITY_PROJ_COORD(i.uvgrab));
half4 tint = tex2D(_MainTex, i.uvmain);
//模糊纹理与主纹理混合得出模糊透明效果
col = lerp (col, tint, _TintAmt);
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
}
注意点:
虽然Shader.PropertyToID返回的是一个int类型,但是Blit操作用到的ID会强转成RenderTargetIdentifier,以供Blit需要。
在其源码中可以看到
经过1、2、3步的操作后就应该对Command Buffer有个大致的了解,下面对Comman Buffer相关的代码进行一个梳理,只列举一些关键、常用的变量和函数。
关系图
先知道一些基础的概念,然后再继续深入。
变量、函数名 |
作用 |
示例图 |
CommandBuffer.BeginSample CommandBuffer.EndSample |
将对应的Pass框选出来,方便在Profile里进行分析。但是在URP的源码里经常会出现其他的写法,使用using(newProfilingScope(CommandBuffer cmd,ProfilingSampler sampler)) |
|
CommandBuffer.GetTemporayRT |
申请一张临时的RT,配合RenderIdentifier使用 |
|
CommandBuffer.SetRenderTarget |
设置渲染目标,添加一个激活渲染目标的指令到CommandBuffer中。 指定一张RenderTexture作为RenderTarget可以使用好几种方法: 1.直接使用RenderTexture 2.GetTemporaryRT申请的RT的Id(int)类型 3.在渲染周期内生成的内置(Built-In)RenderTexture[BuiltinRenderTextureType] 这些全部都会被隐式转换成RenderTargetIdentifier这个结构体。 |
|
RenderBufferLoadAction |
Load:当RenderBuffer被激活时,载入原本已保存的内容,这个加载读取的操作对Tile base架构的GPU比较占用带宽。 Clear:当RenderBuffer被激活时,会清理掉原本Buffer里的内容。目前不能直接使用,需要使用CommandBuffer.CleanRenderTarget作为代替。 DontCare:当RenderBuffer被激活时,GPU不会把RenderBuffer的内容加载进内存,从而减少带宽传输压力。 unity CommandBuffer.SetTarget()几个重载都是用的Load |
|
RenderBufferStoreAction |
跟RenderBufferLoadAction类似,但这个枚举类型参数主要是控制RenderBuffer存储时的策略。 Store:RenderBuffer的内容需要存储到RAM中。如果启用了MSAA,这是就会存储未Resolve(non-resolved)的着色结果。 Resolve:GPU将多重采样数据(Multisampled data)重新解析(resolve)为每像素一个采样点,并将数据存储到RenderTexture中,丢弃了多采样数据)。目前不能直接设置,只能调用API进行Resolve StoreAndResolve: 将多重采样数据(Multisampled data)重新解析(resolve)为每像素一个采样点,并将数据存储到RenderTexture中,并且存储多重采样数据。目前不能直接使用,只能调用API进行Resolve。 DontCare:RenderBuffer的内容是不需要的,可以被丢弃。Tile-base架构的GPU将完全跳过存储阶段,有次减少带宽压力,提高性能。[RenderTextureMemoryless需要设置对象的模式] unity CommandBuffer.SetTarget()几个重载都是用的Resolve |
|
RenderTargetBinding |
把一个或者多个ColorBuffer以及一个深度/模板缓冲[8/16/24/32bit]如何绑定到RenderTarget上,并且他们各自Load和Store的策略。 |
|
CommandBuffer.DrawMesh |
mesh:绘制使用的网格 matrix:绘制时使用的模型空间矩阵(M) material:绘制时使用的材质 submeshindex:绘制的子网格的索引值 shaderPass:Material对应的shader使用Pass的索引值,默认为-1,渲染所有Pass properties:MaterialPropertyBlock不需要额外创建材质实例就可以覆写当前绘制时的材质属性值。 |
|
CommandBuffer.DrawMeshInstanced |
使用实例化绘制多个相同Mesh,看到matrices和count这两个参数,说明绘制时需要提供模型空间矩阵(M)数组,以及绘制的数量。 需要注意的是:绘制时的Material对应的shader一定得是支持Instance绘制。 |
|
CommandBuffer. DrawMeshInstanceProcedural |
绘制Instance时的InstancePath是Instance Procedural。 跟之前的DrawMeshInstanced相比,区别就是,不支持LightMap和LightProbe需要手动在Procedural的函数实现,从Instance和CBuffer中通过InstanceID获取每个实例所对应的的LightMapST,LightMapIndex,LightProbe的SH,Occlusion数值。 |
|
CommandBuffer. DrawMeshInstanceIndirect |
绘制Instance时,除了需要提供绘制Instance的Mesh之外,还需要通过MaterialPropertyBlock设置绘制时所需要用到的Compute Shader计算的数据。 另外需要用BufferArgs控制绘制的顶点数以及实例数量,绘制时开始的Index位置,开始绘制时的Instance位置。 可以看到BufferArgs的可控性更强了,但是一般而言是会配合着Compute Shader进行控制BufferArgs的数值,然后再通过BufferArgs绘制Instance。 一般来说,DrawMeshInstanceIndirect会用来制作视锥裁剪的四叉树地形,草地等等。 |
|
CommandBuffer.DrawProcedural |
选择绘制的拓扑结构进行绘制Mesh,不需要用vertex/index buffer,用Compute shader生成顶点数据,然后再调用DrawProcedural的时候用Vertex Shader对顶点数据进行描绘。 需要注意的是Compute Shader的计算不属于图元装配阶段。 Compute Shader - OpenGL Wiki Compute Shader介绍(一) |
|
CommandBuffer. DrawProceduralIndirect |
不需要用vertex/index buffer,同时也和 DrawMeshInstanceIndirect一样,需要提供BufferArgs控制实例化的过程参数。 在Shader里也同样能够使用InstanceID。 |
|
CommandBuffer.DrawRenderer |
最为常用的用法还是写在Gameloop里用脚本获取常经理的Renderer,在管线里new一个Renderer来渲染貌似不是一个好主意。 |
|
CommandBuffer.Blit |
在实际使用的时候,为了以防万一尽量使用CommandBuffer.SetGlobalTexture配合Shader.PropertyToID设置参数。 另外,最好还是得手动SetRenderTarget设置下LoadAction以及StoreAction节省带宽 |
--------待补充--------
此处开始全是在URP下进行(我用的版本Unity2020.3f1c1 URP Linear空间)
Edit->Project Setting->Quality
这个UniversalRP-HighQuality就是我们以后要替换的URPAsset。
点击这个Asset可以在Inspector面板上看到RendererList。
点击ForwardRenderer(Forward Renderer Data)。
Renderer Feature就是后面我们自定义的东西,上面的Post Process Data也是可以自定义的,先不管。
--------待补充--------
Unity自带的渲染管线创建如下图。
点击Pipeine Asset(Forward Renderer),会创建两个默认的配置文件,就是上面流程梳理里面的两个文件。
不管这两个文件,下面开始创建自己的渲染管线。
首先创建两个脚本分别叫做CustomRenderPipline和CustomRenderPiplineAsset。
分别继承RenderPipeline和RenderPipelineAsset如下。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
public class CustomRenderPipeline : RenderPipeline
{
protected override void Render(ScriptableRenderContext context, Camera[] cameras)
{
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
[CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline")]
public class CustomRenderPipelineAsset : RenderPipelineAsset
{
protected override RenderPipeline CreatePipeline()
{
return new CustomRenderPipeline();
}
}
然后右键创建对应的RP Asset,将原本的Asset替换成我们自己创建的RP Asset,发现窗口什么都渲染不出。
因为还没有指定渲染相机。
创建一个新的自定义相机渲染脚本CustomCameraRenderer.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
public class CustomCameraRenderer
{
private Camera _camera;
private ScriptableRenderContext _context;
public void Render(ScriptableRenderContext sContext, Camera rCamera)
{
this._camera = rCamera;
this._context = sContext;
}
}
然后将CustomRenderPipeline设置返回值。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
public class CustomRenderPipeline : RenderPipeline
{
private CustomCameraRenderer _renderer = new CustomCameraRenderer();
protected override void Render(ScriptableRenderContext context, Camera[] cameras)
{
foreach (Camera camera in cameras)
{
_renderer.Render(context,camera);
}
}
}
camera renderer 大致相当于通用RP的scriptable renderer。
这种方法能让每个相机在未来更容易支持不同的渲染方法。例如一个渲染第一人称视图,一个渲染三维地图,或前向和延迟渲染的区别。但现在我们会用同样的方式渲染所有的摄像机。
ScriptableRenderContext包含了一些渲染相关的数据,具体可以查看源码。
--------------------------------后续借用Catlike里面的截图----------------------------------
(_camera->camera 、_context->context)
在CustomCameraRenderer中对可见物体进行绘制Context.DrawSkybox(camera);
将缓存中的命令提交到执行队列里。
需要提前计算VP矩阵,使最终得到的图像正常。
上下文会延迟实际的渲染,直到我们提交它为止。在此之前,我们对其进行配置并向其添加命令以供后续的执行。某些任务(例如绘制天空盒)提供了专属方法,但其他命令则必须通过单独的命令缓冲区(command buffer)间接执行。我们需要用这样的缓冲区来绘制场景中的其他几何图形。
为了获得缓冲区,我们必须创建一个新的CommandBuffer对象实例。一般只需要一个缓冲区,因此默认情况下为CameraRenderer创建一个缓冲区,并将对它的引用存储在字段中。给缓冲区起一个名字,以便我们在frame debugger中识别它。就叫Render Camera好了。
CustomCameraRenderer里添加CommandBuffer,如下图所示。
我们可以使用命令缓冲区注入给Profiler样本,这些样本将同时显示在Profiler和frame debugger中。通过在适当的位置插入BeginSample和EndSample就可以完成。在本例中,在Setup和Submit的开头添加。注意两个方法必须提供相同的样本名称,为此我们直接使用缓冲区的名称。
要执行缓冲区,在上下文中调用ExecuteCommandBuffer,并把缓冲区作为一个参数。这是从缓冲区中复制命令,但并不清除它,如果我们想重新使用它,我们必须在事后明确地这样做。因为执行和清除总是一起进行的,所以添加一个方法来做这两件事是很方便的。
简单来说就是我们可以手动控制渲染顺序,并且在Profiler和Frame Debug上面进行单独框选命名(方便查看)。
我们画的东西最终会被渲染到摄像机的渲染目标上,默认情况下是帧缓冲区,但也可以是渲染纹理。之前绘制到该目标上的东西仍然存在,这可能会干扰我们现在正在渲染的图像。为了保证正常的渲染,我们必须清除渲染目标,以摆脱它的旧内容。这可以通过在命令缓冲区上调用ClearRenderTarget来实现,它属于Setup方法。
帧调试器现在显示了清除的Draw GL条目,它显示嵌套在Render Camera的一个额外层次中。这是因为ClearRenderTarget用命令缓冲区的名字将清除动作包裹在一个样本中。我们可以通过在开始我们自己的样本之前进行清除来摆脱多余的嵌套。这导致了两个相邻的Render Camera样本范围,它们被合并了。
Draw GL条目表示用Hidden/InternalClear着色器绘制全屏四边形,该着色器会写入渲染目标,这并不是最有效的清除方式。使用这种方法是因为我们是在设置摄像机属性之前进行清除。如果我们把这两个步骤的顺序对调一下,就可以得到快速的清除方式。
现在我们看到Clear(color+Z+stencil),这表明颜色和深度缓冲区都被清除了。Z(深度缓冲区)和模版数据是整个缓冲区的一部分。
剔除这部分就是要剔除相机看不到的地方,为此我们可以使用ScriptableCullingParameters结构。
我们可以在摄像机上调用TryGetCullingParameters,用它来返回一个bool值用作后续的渲染。
实际的剔除是通过在上下文中调用Cull来完成的,它会产生一个CullingResults结构。如果成功的话就在Cull中进行,并将结果存储在一个字段中。在这种情况下,我们必须把剔除参数作为一个引用参数来传递,在它前面写上ref。
一旦我们知道什么是可见的,我们就可以继续渲染这些东西了。这可以通过在上下文中调用DrawRenderers来完成,并将剔除结果作为一个参数,告诉它要使用哪些渲染器。
除此之外,我们还必须提供绘制设置和过滤设置。两者都是结构体--绘图设置(DrawingSettings)和过滤设置(FilteringSettings),我们最初会使用它们的默认构造函数。两者都必须通过引用来传递。
在绘制天空盒之前,在DrawVisibleGeometry中这样做。
我们还没有看到任何东西,因为我们还必须指出哪种着色器通道是允许的。由于我们在本教程中只支持非亮光着色器,我们必须为SRPDefaultUnlit通道获取着色器标签ID,我们可以做一次并将其缓存在一个静态字段中。
将其作为DrawingSettings构造函数的第一个参数,同时提供一个新的SortingSettings结构值。将相机传递给排序设置的构造函数,因为它被用来确定是否适用正交或基于距离的排序。
除此之外,我们还必须指出哪些渲染队列是允许的。将RenderQueueRange.all传递给FilteringSettings构造函数,这样我们就包括了所有的东西。
可以看到现在透明与不透明物体的渲染顺序杂乱无章,下面要再对排序进行处理。
此处为语雀视频卡片,点击链接查看:bandicam 2022-01-25 14-00-57-377.mp4
这样渲染顺序就是正常的。
此处为语雀视频卡片,点击链接查看:bandicam 2022-01-25 14-10-58-873.mp4
目前天空盒会遮挡住透明物体,因此我们需要调整不透明物体与天空盒绘制的顺序。
我们可以通过切换到RenderQueueRange.opaque来消除初始DrawRenderers调用中的透明对象。
然后重新设置下绘制相关参数,重新进行绘制。