高阶Shader
Unity的渲染管线
着色器定义了两点:1. 对象自身的外观(其材质属性);2. 对象对光照的反应。由于光照计算必须内置在着色器中,并且有很多可能的光源和阴影类型,因此编写“好用”的高质量着色器将是一项复杂的任务。为了简化这一任务,Unity 提供了表面着色器;在表面着色器中,所有光照、阴影、光照贴图、前向渲染和延迟渲染任务均自动完成。
渲染路径
如何应用光照以及使用着色器的哪些通道取决于使用的渲染路径。着色器中的每个通道均通过通道标签来表达其光照类型。
在前向渲染中,将使用 ForwardBase 和 ForwardAdd 通道。
在延迟着色中,将使用 Deferred 通道。
在旧版延迟光照中,将使用 PrepassBase 和 PrepassFinal 通道。
在旧版顶点光照中,将使用 Vertex、VertexLMRGBM 和 VertexLM 通道。
在上述任何情况中,要渲染阴影或深度纹理,都将使用 ShadowCaster 通道。
前项渲染 ForwardBase 通道可一次性渲染环境光、光照贴图、主方向光和不重要的(顶点/SH)光源。ForwardAdd 通道用于任何附加的每像素光源;针对此类光源照亮的每个对象进行一次调用。请参阅前向渲染以了解详细信息。
如果使用前向渲染,但着色器没有适合前向渲染的通道(即,ForwardBase 和 ForwardAdd 通道类型均不存在),则会按照在顶点光照通道中的方式来渲染该对象
- 延迟着色路径 Deferred 通道将渲染光照需要的所有信息(在内置着色器中:漫射颜色、镜面反射颜色、平滑度、 世界空间法线、发光)。它还在发光通道中增加光照贴图、反射探针和环境光照。有关详细信息,请参阅延迟着色。
着色器性能
- 着色器代码需要执行的计算和处理越多,它对游戏性能的影响就越大。
- 计算的频率也会影响游戏的性能。通常,与顶点数(顶点着色器执行次数)相比,渲染的像素数会更多(因此像素着色器执行次数也更多),而渲染的顶点数比渲染的对象更多。在可能的情况下,可将计算从像素着色器代码移动到顶点着色器代码中,或者将它们完全移出着色器并在脚本中设置值。
优化的表面着色器
表面着色器非常适合编写与光照交互的着色器。但是,它们的默认选项已调整为涵盖大量的一般情况。可针对特定情况调整这些选项以使着色器运行速度更快,或至少让着色器变得更小巧:
- 使用视图方向(即镜面反射)的着色器的
approxview指令使视图方向按照顶点(而不是按像素)进行标准化。这是近似值,但通常足够好。 - 适用于镜面反射着色器类型的
halfasview速度更快。半矢量(光照方向和视图矢量之间)按照顶点进行计算和标准化,并且光照函数接受半矢量作为参数,而不是视图矢量。 noforwardadd使着色器仅完全支持前向渲染中的单方向光。其余的光源仍然可提供每顶点光源或球谐函数光源的效果。这样可以使着色器更小并确保它始终在一个通道中渲染,即使存在多个光源也是如此。noambient在着色器中禁用环境光照和球谐函数光源。这样可以稍稍提高性能。
计算精度
用 Cg/HLSL 编写着色器时,有三种基本数字类型:float、half 和 fixed(请参阅数据类型和精度)。
为获得良好性能,请始终使用尽可能低的精度。这在移动平台(如 iOS 和 Android)上尤为重要。重要的经验法则如下:
- 对于世界空间位置和纹理坐标,请使用 float 精度。
- 对于所有其他情况(矢量、HDR 颜色等),请首先尝试 half 精度。仅在必要的情况下再提高精度。
- 要对纹理数据进行非常简单的运算,请使用 fixed 精度。
实际上,具体应该使用哪种数字类型取决于平台和 GPU。
Alpha 测试
固定函数 AlphaTest(或者其可编程的等效函数 clip())在不同平台上具有不同的性能特征:
- 通常,在使用该函数来移除大多数平台上的完全透明像素时,可获得少量优势。
- 但是,在 iOS 和某些 Android 设备的 PowerVR GPU 上,Alpha 测试是资源密集型任务。不要试图在这些平台上使用这种测试进行性能优化,因为它会导致游戏运行速度比平常慢。
颜色遮罩 (Color Mask)
在某些平台(主要是 iOS 和 Android 设备的移动端 GPU)上,使用 ColorMask 省略一些通道(例如 ColorMask RGB)可能是资源密集型的操作,所以除非绝对需要,否则请不要使用。
使用替换的着色器进行渲染
有些渲染效果需要使用一组不同的着色器来渲染场景。例如,良好的边缘检测要求纹理具有场景法线,这样才能检测出表面方向不同的边缘。其他效果可能要求纹理具有场景深度,诸如此类。为此,可使用所有对象的替换着色器来渲染场景。
应通过脚本使用函数 Camera.RenderWithShader 或 Camera.SetReplacementShader 来实现着色器替换。这两个函数均采用 shader 和 replacementTag。
工作方式如下:摄像机按正常方式渲染场景,对象仍使用自己的材质,但要更改最终使用的实际着色器:
- 如果 replacementTag 为空,则使用指定的替换着色器来渲染场景中的所有对象。
- 如果 replacementTag 不为空,则对于将要渲染的每个对象:
- 查询真实对象的着色器以获取标签值。
- 如果没有该标签,则不渲染对象。
- 在替换着色器中找到一个子着色器,并且该子着色器的一个给定标签具有找到的值。如果找不到此类子着色器,则不渲染对象。
- 现在,使用该子着色器来渲染对象。
因此,比如说,如果所有着色器都要有一个值为“Opaque”、“Transparent”、“Background”或“Overlay”的“RenderType”标签,则可编写一个替换着色器,该着色器只使用一个具有 RenderType = Solid 标签的子着色器来渲染实体对象。在替换着色器中找不到其他标签类型,因此不会渲染对象。或者您也可以为不同“RenderType”标签值编写若干子着色器。顺便提一下,Unity 的所有内置着色器都设置了一个“RenderType”标签。
光照着色器替换
使用着色器替换时,将使用摄像机上配置的渲染路径来渲染场景。这意味着用于替换的着色器可以包含阴影和光照通道(您可以使用表面着色器进行着色器替换)。这对于渲染特殊效果和场景调试很有用。
Unity 内置着色器中的着色器替换标签
Unity 的所有内置着色器都设置了一个“RenderType”标签,可以在使用替换着色器进行渲染时使用此标签。标签值如下:
- Opaque:大部分着色器(法线、自发光、反射和地形着色器)。
- Transparent:大部分半透明着色器(透明、粒子、字体和地形附加通道着色器)。
- TransparentCutout:遮罩透明度着色器(透明镂空、两个通道植被着色器)。
- Background:天空盒着色器。
- Overlay:GUI 纹理、光环、光晕着色器。
- TreeOpaque:地形引擎树皮。
- TreeTransparentCutout:地形引擎树叶。
- TreeBillboard:地形引擎公告牌树。
- Grass:地形引擎草。
- GrassBillboard:地形引擎公告牌草。
内置场景深度/法线纹理
摄像机内置了渲染深度或深度+法线纹理的功能(可能在某些效果中需要该功能)。请参阅摄像机深度纹理页面。请注意,在某些情况下(取决于硬件),可以使用着色器替换方法在内部渲染深度和深度+法线纹理。因此,务必在着色器中设置正确的“RenderType”标签。
自定义着色器 GUI
使用深度纹理
深度纹理中的像素值介于 0 和 1 之间,具有非线性分布。精度通常为 32 或 16 位,具体取决于所使用的配置和平台。从深度纹理读取时,将返回 0 到 1范围内的高精度值。如果您需要获取与摄像机之间的距离或其他 0 到 1 之间的线性值,
Shader "Render Depth" {
SubShader {
Tags { "RenderType"="Opaque" }
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f {
float4 pos : SV_POSITION;
float2 depth : TEXCOORD0;
};
v2f vert (appdata_base v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
UNITY_TRANSFER_DEPTH(o.depth);
return o;
}
half4 frag(v2f i) : SV_Target {
UNITY_OUTPUT_DEPTH(i.depth);
}
ENDCG
}
}
}
摄像机的深度纹理
TODO
平台特定的渲染差异
渲染纹理坐标
- Direct3D 类:顶部坐标为 0 并向下增加。此类型适用于 Direct3D、Metal 和游戏主机。
- OpenGL 类:底部坐标为 0 并向上增加。此类适用于 OpenGL 和 OpenGL ES。
图像效果
// 翻转纹理的采样:
// 主纹理的
// 纹理像素大小将具有负 Y。
# if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
uv.y = 1-uv.y;
# endif
在 UV 空间中渲染
在纹理坐标 (UV) 空间中渲染特殊效果或工具时,您可能需要调整着色器,以便在 Direct3D 类和 OpenGL 类系统之间进行一致渲染。您还可能需要在渲染到屏幕和渲染到纹理之间进行渲染调整。为进行此类调整,应上下翻转 Direct3D 类投影,使其坐标与 OpenGL 类投影坐标相匹配。
内置变量 ProjectionParams.x 包含值 +1 或 –1。-1 表示投影已上下翻转以匹配 OpenGL 类投影坐标,而 +1 表示尚未翻转。 您可以在着色器中检查此值,然后执行不同的操作。下面的示例将检查是否已翻转投影,如果已翻转,则再次进行翻转,然后返回 UV 坐标以便匹配。
float4 vert(float2 uv : TEXCOORD0) : SV_POSITION
{
float4 pos;
pos.xy = uv;
// 此示例使用上下翻转的投影进行渲染,
// 因此也翻转垂直 UV 坐标
if (_ProjectionParams.x < 0)
pos.y = 1 - pos.y;
pos.z = 0;
pos.w = 1;
return pos;
}
裁剪空间坐标
与纹理坐标类似,裁剪空间坐标(也称为投影后空间坐标)在 Direct3D 类和 OpenGL 类平台之间有所不同:
Direct3D 类:裁剪空间深度从近平面的 0.0 到远平面的 +1.0。此类型适用于 Direct3D、Metal 和游戏主机。
OpenGL 类:裁剪空间深度从近平面的 –1.0 到远平面的 +1.0。此类适用于 OpenGL 和 OpenGL ES。
在着色器代码内,可使用内置宏 UNITY_NEAR_CLIP_VALUE 来获取基于平台的近平面值。
着色器计算的精度
要避免精度问题,请确保在目标平台上测试着色器。移动设备和 PC 中的 GPU 在处理浮点类型方面有所不同。
着色器中的 const 声明
const 的使用在 Microsoft HSL(请参阅 msdn.microsoft.com)和 OpenGL 的 GLSL(请参阅 Wikipedia)着色器语言之间有所不同。
Microsoft 的 HLSL const 与 C# 和 C++ 中的含义大致相同:声明的变量在其作用域内是只读的,但可按任何方式初始化。
OpenGL 的 GLSL const 表示变量实际上是编译时常量,因此必须使用编译时约束(文字值或其他对于 const 的计算)进行初始化。
最好是遵循 OpenGL 的 GLSL 语义,并且只有当变量真正不变时才将变量声明为 const。避免使用其他一些可变值初始化 const 变量(例如,作为函数中的局部变量)。这一原则也适用于 Microsoft 的 HLSL,因此以这种方式使用 const 可以避免在某些平台上混淆错误
提取深度缓冲区
如果要手动提取深度 (Z) 缓冲区值,则可能需要检查缓冲区方向。以下是执行此操作的示例:
float z = tex2D(_CameraDepthTexture, uv);
# if defined(UNITY_REVERSED_Z)
z = 1.0f - z;
# endif
使用裁剪空间
如果要手动使用裁剪空间 (Z) 深度,则可能还需要使用以下宏来抽象化平台差异:
float clipSpaceRange01 = UNITY_Z_0_FAR_FROM_CLIPSPACE(rawClipSpace);
注意:此宏不会改变 OpenGL 或 OpenGL ES 平台上的裁剪空间,因此在这些平台上,此宏返回“-near”1(近平面)到 far(远平面)之间的值。
投影矩阵
如果处于深度 (Z) 发生反转的平台上,则 GL.GetGPUProjectionMatrix() 返回一个还原了 z 的矩阵。 但是,如果要手动从投影矩阵中进行合成(例如,对于自定义阴影或深度渲染),您需要通过脚本按需自行还原深度 (Z) 方向。
以下是执行此操作的示例:
var shadowProjection = Matrix4x4.Ortho(...); //阴影摄像机投影矩阵
var shadowViewMat = ... //阴影摄像机视图矩阵
var shadowSpaceMatrix = ... //从裁剪空间到阴影贴图纹理空间
//当引擎通过摄像机投影计算设备投影矩阵时,
//"m_shadowCamera.projectionMatrix"被隐式反转
m_shadowCamera.projectionMatrix = shadowProjection;
//"shadowProjection"在连接到"m_shadowMatrix"之前被手动翻转,
//因为它被视为着色器的其他矩阵。
if(SystemInfo.usesReversedZBuffer)
{
shadowProjection[2, 0] = -shadowProjection[2, 0];
shadowProjection[2, 1] = -shadowProjection[2, 1];
shadowProjection[2, 2] = -shadowProjection[2, 2];
shadowProjection[2, 3] = -shadowProjection[2, 3];
}
m_shadowMatrix = shadowSpaceMatrix * shadowProjection * shadowViewMat;
深度 (Z) 偏差
Unity 自动处理深度 (Z) 偏差,以确保其与 Unity 的深度 (Z) 方向匹配。但是,如果要使用本机代码渲染插件,则需要在 C 或 C++ 代码中消除(反转)深度 (Z) 偏差。
深度 (Z) 方向检查工具
- 使用 SystemInfo.usesReversedZBuffer 可确认所在平台是否使用反转深度 (Z)。
着色器细节级别
仅当使用的着色器或子着色器的细节级别 (LOD) 值低于指定数字时,着色器 LOD 才有效。
默认情况下,允许的 LOD 级别无限大,因此可以使用用户硬件支持的所有着色器。但在某些情况下,即使硬件可以支持更高的着色器细节级别,您也可能希望降低细节级别。例如,一些廉价的显卡可能支持所有功能,但使用起来会导致响应过慢。因此,您可能不希望在这些显卡上使用视差法线贴图。
可为各个着色器分别设置着色器 LOD(使用 Shader.maximumLOD),也可为所有着色器进行全局设置(使用 Shader.globalMaximumLOD)。
Unity 中的内置着色器按以下方式设置 LOD:
- 顶点光照 (VertexLit) 类型着色器 = 100
- 贴花、反光顶点光照 = 150
- 漫射 = 200
- 漫射细节、反光凹凸无光照、反光凹凸顶点光照 = 250
- 凹凸、镜面反射 = 300
- 凹凸镜面反射 = 400
- 视差 = 500
- 视差镜面反射 = 600
纹理数组
类似于常规 2D 纹理(Texture2D 类,着色器中的 sampler2D__)、立方体贴图(Cubemap 类,着色器中的 samplerCUBE__)和 3D 纹理(Texture3D 类,着色器中的 sampler3D),Unity 也支持 2D 纹理数组。
纹理数组是具有相同大小/格式/标记的 2D 纹理的集合,这些纹理对于 GPU 而言像是单个对象,并可在着色器中使用纹理元素索引进行采样。它们可以用于实现自定义地形渲染系统或其他特殊效果,让您高效访问大量相同大小和格式的纹理。2D 纹理数组的元素也称为切片或图层。
平台支持
纹理数组需要受到底层图形 API 和 GPU 的支持。纹理数组在以下平台上可用:
- Direct3D 11/12(Windows、Xbox One)
- OpenGL Core(Mac OS X、Linux)
- Metal(iOS、Mac OS X)
- OpenGL ES 3.0(Android、iOS、WebGL 2.0)
- PlayStation 4
其他平台(OpenGL ES 2.0 或 WebGL 1.0)不支持纹理数组。可使用
SystemInfo.supports2DArrayTextures
在运行时确定纹理数组支持情况。
创建和填充纹理数组
由于纹理数组没有纹理导入管线,必须在脚本中创建纹理数组。可使用 Texture2DArray 类来创建和填充纹理数组。请注意,纹理数组可序列化为资源,因此可以借助 Editor 脚本中的数据创建和填充纹理数组。
通常,纹理数组完全是在 GPU 内存中使用,但您可以使用 Graphics.CopyTexture、Texture2DArray.GetPixels 和 Texture2DArray.SetPixels 与系统内存之间双向传输像素。
将纹理数组用作渲染目标
纹理数组元素也可用作渲染目标。使用 RenderTexture.dimension 提前指定渲染目标是否是 2D 纹理数组。Graphics.SetRenderTarget 的 depthSlice 参数可指定要渲染到的 Mipmap 级别或立方体贴图面。在支持“分层渲染”(例如,几何着色器)的平台上,可将 depthSlice 参数设置为 –1 以便将整个纹理数组设置为渲染目标。此外还可使用几何着色器来渲染到个别元素中。
在着色器中使用纹理数组
由于纹理数组并非适用于所有平台,因此着色器需要使用适当的编译目标或功能要求来访问纹理数组。支持纹理数组的最低着色器模型编译目标为 3.5,功能名称为 2darray。
使用以下宏可声明和采样纹理数组:
- UNITY_DECLARE_TEX2DARRAY(name) 在 HLSL 代码中声明纹理数组采样器变量。
- UNITY_SAMPLE_TEX2DARRAY(name,uv) 使用 float3 UV 采样纹理数组;坐标的 z 分量是数组元素索引。
- UNITY_SAMPLE_TEX2DARRAY_LOD(name,uv,lod) 使用显式 Mipmap 级别采样纹理数组。
Shader "Example/Sample2DArrayTexture"
{
Properties
{
_MyArr ("Tex", 2DArray) = "" {}
_SliceRange ("Slices", Range(0,16)) = 6
_UVScale ("UVScale", Float) = 1.0
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 纹理数组并非在任何地方都可用,
// 只能在它们所在的平台上编译着色器
#pragma require 2darray
#include "UnityCG.cginc"
struct v2f
{
float3 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
float _SliceRange;
float _UVScale;
v2f vert (float4 vertex : POSITION)
{
v2f o;
o.vertex = mul(UNITY_MATRIX_MVP, vertex);
o.uv.xy = (vertex.xy + 0.5) * _UVScale;
o.uv.z = (vertex.z + 0.5) * _SliceRange;
return o;
}
UNITY_DECLARE_TEX2DARRAY(_MyArr);
half4 frag (v2f i) : SV_Target
{
return UNITY_SAMPLE_TEX2DARRAY(_MyArr, i.uv);
}
ENDCG
}
}
}