Multi-Pass,又称传统双通道模式。该模式是先完成左眼的渲染,然后再做右眼的渲染。这种模式虽然能很快速适配VR/MR,但是两眼之间会有一定的时滞与延迟,体验不佳。因为在这种模式下, Unity会为左右眼各分配一个Render Texture做渲染, 目的是和非VR模式下的渲染方式尽可能的兼容。
该模式可以完美支持自定义shader和后处理,即自定义shader和后处理按照传统的方法实现,不需要额外添加代码。但渲染效率并不高。
Single-Pass-Instanced,又称单通道实例化模式。在该模式下,利用渲染目标数组来执行单个绘制调用,该调用将实例实例化为每只眼睛的适当渲染目标。此外,此模式允许所有渲染都在渲染管道的一次执行中完成。
因此,选择Single Pass Instanced渲染作为混合现实应用程序的渲染路径可以节省CPU和GPU上的大量时间,并且是推荐的渲染配置。
但是,为了对每只眼睛的每个网格发出单个绘制调用,所有着色器都必须支持GPU Instancing。实例化使GPU可以在两只眼睛之间复用绘图调用。默认情况下,Unity内置着色器以及MRTK标准着色器在着色器代码中包含必要的实例化指令。如果为Unity编写自定义着色器,则可能需要更新这些着色器以支持Single Pass Instanced渲染。
设置方法如下:
File ——> Build Settings ——> Player Settings ——> XR Settings ——> Stereo Rendering Mode
上一篇已经说到,默认的着色器以及MRTK标准着色器在着色器代码中包含必要的实例化指令。官方建议使用MRTK标准着色器,相关的技术文档可见https://microsoft.github.io/MixedRealityToolkit-Unity/Documentation/README_MRTKStandardShader.html
如果需要自定义shader,可以通过下面的方法为自定义的shader添加实例化指令。
1、在预处理指令那里添加GPU Instance预处理指令,shader会根据是否开启GPU Instance生成不同的shader变体。
//添加GPU Instance预处理指令
#pragma multi_compile_instancing
2、在顶点输入输出结构体添加以下宏
//为顶点实例化一个ID
UNITY_VERTEX_INPUT_INSTANCE_ID
3、针对XR的Single-Pass-Instanced,还需要再顶点输出结构体添加以下宏
//在顶点着色器声明立体目标眼睛字段输出结构
//与普通的GPU Instancing不一样的地方,XR的Single-Pass-Instanced需要添加这个宏
UNITY_VERTEX_OUTPUT_STEREO
4、为顶点着色器添加以下宏
//初始化顶点信息
//这个宏必须在Vertex Shader的最开始调用
//如果你需要在Fragment Shader里访问Instanced属性,则需要在Fragment Shader的开始也用一下
//这个宏的目的在于让Instance ID在Shader函数里也能够被访问到。
UNITY_SETUP_INSTANCE_ID(v);
//在顶点程序中,将实例ID从输入结构复制到输出结构
//在Vertex Shader中把Instance ID从输入结构拷贝至输出结构中
//只有当你需要在Fragment Shader中访问每个Instance独有的属性时才需要写这个宏。
UNITY_TRANSFER_INSTANCE_ID(v,o);
//分配立体目标的眼睛
//与普通的GPU Instancing不一样的地方,XR的Single-Pass-Instanced需要添加这个宏
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
5、为片元着色器添加以下宏
//初始化顶点信息
UNITY_SETUP_INSTANCE_ID(i);
完整参考shader代码见下:
Shader "Unlit/GPUInstancing"
{
Properties
{
_Color ("Color", color) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
//添加GPU Instance预处理指令
#pragma multi_compile_instancing
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
//为顶点实例化一个ID
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
//为顶点实例化一个ID
UNITY_VERTEX_INPUT_INSTANCE_ID
//在顶点着色器声明立体目标眼睛字段输出结构(与普通的GPU Instancing不一样的地方,XR的Single-Pass-Instanced需要添加这个宏)
UNITY_VERTEX_OUTPUT_STEREO
};
float4 _Color;
v2f vert (appdata v)
{
//初始化顶点信息
//这个宏必须在Vertex Shader的最开始调用,如果你需要在Fragment Shader里访问Instanced属性,则需要在Fragment Shader的开始也用一下。这个宏的目的在于让Instance ID在Shader函数里也能够被访问到。
UNITY_SETUP_INSTANCE_ID(v);
v2f o;
//在顶点程序中,将实例ID从输入结构复制到输出结构
//在Vertex Shader中把Instance ID从输入结构拷贝至输出结构中。只有当你需要在Fragment Shader中访问每个Instance独有的属性时才需要写这个宏。
UNITY_TRANSFER_INSTANCE_ID(v,o);
//分配立体目标的眼睛(与普通的GPU Instancing不一样的地方,XR的Single-Pass-Instanced需要添加这个宏)
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//初始化顶点信息
UNITY_SETUP_INSTANCE_ID(i);
fixed4 col = _Color;
return col;
}
ENDCG
}
}
}
后处理的写法跟自定义shader的写法一样,可以参考“为自定义的shader添加实例化指令”
唯一不同的是后处理是通过Graphics.Blit(src, dest, material)的方法给material传入src到_MainTex的,所以_MainTex的声明及采样需要做出一些修改:
1、_MainTex的声明:UNITY_DECLARE_SCREENSPACE_TEXTURE(_MainTex);
2、_MainTex的采样:UNITY_SAMPLE_SCREENSPACE_TEXTURE(_MainTex, i.uv);
完整参考shader代码见下:
Shader "Unlit/ScreenColor"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_H("H",Range(0,1)) = 0
}
SubShader
{
Tags { "RenderType"="Opaque" }
/* 后处理的shader必须要添加 ZTest Always 和 ZWrite Off */
ZTest Always
ZWrite Off
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
//添加GPU Instance预处理指令
#pragma multi_compile_instancing
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
//为顶点实例化一个ID
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
//为顶点实例化一个ID
UNITY_VERTEX_INPUT_INSTANCE_ID
//在顶点着色器声明立体目标眼睛字段输出结构(与普通的GPU Instancing不一样的地方,XR的Single-Pass-Instanced需要添加这个宏)
UNITY_VERTEX_OUTPUT_STEREO
};
//后处理_MainTex的声明,以便2D纹理数组将被正确声明
UNITY_DECLARE_SCREENSPACE_TEXTURE(_MainTex);
uniform float _H;
float3 RGBToHSV( float3 Color ){
float4 p = lerp(float4(Color.bg, -1.0,2.0 / 3.0), float4(Color.gb, 0.0, -1.0 / 3.0), step(Color.b, Color.g));
float4 q = lerp(float4(p.xyw, Color.r), float4(Color.r, p.yzx), step(p.x, Color.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return float3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
float3 HSVToRGB( float3 Color ){
return lerp(float3(1,1,1),saturate(3.0*abs(1.0-2.0*frac(Color.r+float3(0.0,-1.0/3.0,1.0/3.0)))-1),Color.g)*Color.b;
}
v2f vert (appdata v)
{
//初始化顶点信息
//这个宏必须在Vertex Shader的最开始调用,如果你需要在Fragment Shader里访问Instanced属性,则需要在Fragment Shader的开始也用一下。这个宏的目的在于让Instance ID在Shader函数里也能够被访问到。
UNITY_SETUP_INSTANCE_ID(v);
v2f o;
//在顶点程序中,将实例ID从输入结构复制到输出结构
//在Vertex Shader中把Instance ID从输入结构拷贝至输出结构中。只有当你需要在Fragment Shader中访问每个Instance独有的属性时才需要写这个宏。
UNITY_TRANSFER_INSTANCE_ID(v,o);
//分配立体目标的眼睛(与普通的GPU Instancing不一样的地方,XR的Single-Pass-Instanced需要添加这个宏)
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//初始化顶点信息
UNITY_SETUP_INSTANCE_ID(i);
//后处理_MainTex的采样方式,以便于在Single Pass模式下给双眼取样
fixed4 col = UNITY_SAMPLE_SCREENSPACE_TEXTURE(_MainTex, i.uv);
float3 colToHSV = RGBToHSV( col.rgb );
colToHSV.r += _Time.y * 0.5;
colToHSV.r = frac(colToHSV.r);
return fixed4( HSVToRGB(colToHSV),col.a );
}
ENDCG
}
}
}
1、在shader上使用UNITY_DECLARE_SCREENSPACE_TEXTURE(tex) 声明纹理会出现不可预计的问题,如纹理加载不了,采样不了等问题(但后处理的_MainTex必须要这样声明)
2、尽量在每个材质球上的Enable GPU Instancing 上打勾,如果不打勾,这个材质球使用到的这个shader的GPU Instancing变体shader并不会打包出去导致Single-Pass-Instanced实例化出问题。
3、后处理的脚本请通过材质赋值,有些程序的习惯是获取shader,再通过
material = new Material(shader);
来动态创建材质,导致这个后处理shader的GPU Instancing变体shader并不会打包出去而渲染出错。
建议方法:
public Material material;
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (material != null)
Graphics.Blit(src, dest, material);
else
Graphics.Blit(src, dest);
}
``