第三章 进阶应用
一、深度与模板测试 Z Test & Stencil Test
-
模板测试
1.模板测试概述:
①从渲染管线角度(逐片元操作流程):
PixelOwnershipTest:控制对当前屏幕像素的使用权限,没有使用权限的屏幕上的像素会被裁剪。
Scissor Test:可以控制需要渲染的部分。
Alpha Test:通过设置一个阈值(0到1),对比数值进行渲染设置。只能实现不透明或全透明效果。
Blending:实现半透明效果。
②从逻辑的角度:
通过一定条件来判断是对该片元或片元属性执行抛弃操作还是保留操作。
③从书面的角度:
模板测试:模板缓冲区可以为屏幕上的每个像素点保存一个无符号整数值(通常的话是个8位整数)。这个值的具体意义视程序的具体应用而定。在渲染的过程中,可以用这个值与一个预先设定的参考值相比较,根据比较的结果来决定是否更新相应的像素点的颜色值。
模板测试发生在透明度测试(alpha test)之后,深度测试(depth test)之前。
如果模板测试通过,则相应的像素点更新,否则不更新。
2.举例:
*共同点:分为三层结构(门外场景、门、门内场景)
3.Unity Shader中的模板测试具体表现:
①模板测试结构:
stencil{
//给当前的片元设定一个参考值 0~25 用来和模板缓冲区的值作比较
Ref referenceValue
//读取遮罩值
ReadMask readMask
//写遮罩值
WriteMask writeMask
//设置比较操作函数
Comp comparisonFunction
//模板测试通过了 模板缓冲区对应的操作
Pass stencilOperation
//模板测试失败 模板缓冲区对应的操作
Fail stencilOperation
//模板测试通过 但深度测试没通过 模板缓冲区对应的操作
ZFail stencilOperation
}
②ComparisonFunction:
③StencilOperation 更新值:
4.案例效果:
①3D卡牌效果:
*Unity中模板缓冲区默认都是0
StencilMask shader代码:
Shader "mask_stencil"
{
Properties
{
_ID("Mask ID", Int) = 1
}
SubShader
{
//在默认的不透明物体后进行蒙版渲染
Tags { "RenderType"="Opaque" "Queue" = "Geometry+1"}
//颜色遮罩 RGBA(无遮罩) 0(不输出颜色)
ColorMask 0
//关闭深度写入,防止被深度剔除
ZWrite off
Stencil{
Ref[_ID]
Comp always
Pass replace
}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return half4(1,1,1,1);
}
ENDCG
}
}
}
物体Shader代码:
Properties
{
_ID("Mask ID", Int) = 1
}
SubShader
{
//渲染队列在模板遮罩后
Tags { "RenderType"="Opaque" "Queue" = "Geometry+2"}
Stencil{
Ref[_ID]
Comp equal//如果ID值和当前模板缓冲区的值一样才会渲染,否则就抛弃。
}
}
②盒子不同面显示不同场景:
渲染顺序是Queue越小,越先渲染,先渲染好Queue小的物体,那么Queue大的物体覆盖已经渲染好的Queue小物体的,表现效果就是Queue大的物体在Queue小的物体“前面”,符合引擎渲染实际情况。
案例实现:
代码:
*模板测试总结:
1.使用模板缓冲区最重要的两个值:当前模板缓冲值(stencilBufferValue)和模板参考值(referenceValue)。
2.模板测试主要就是对这两个值使用特定的比较操作:Never,Always,Less ,LEqual,Greater,Equal等等。
3.模板测试之后要对模板缓冲区的值(stencilBufferValue)进行更新操作,更新操作包括:Keep,Zero,Replace,IncrSat,DecrSat,Invert等等。
4.模板测试之后可以根据结果对模板缓冲区做不同的更新操作,比如模板测试成功操作Pass,模板测试失败操作Fail,深度测试失败操作ZFail,还有正对正面和背面精确更新操作PassBack,PassFront,FailBack等等。
5.模板测试拓展:
参考:https://blog.csdn.net/liu_if_else/article/details/86316361
①描边:
Shader "OutLineStencil"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Stencil{
Ref 0
Comp Equal
Pass IncrSat
//第一个pass渲染前屏幕上像素的Stencil为0,第一个pass结束,全部通过测试,Stencil+1
//第二个pass渲染时,在物体放大的部分stencil为0的屏幕像素区域渲染
Fail keep
ZFail keep
}
Pass //第一个pass渲染物体
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
float4 posW : TEXCOORD0;
float3 nDir : TEXCOORD1;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.nDir = UnityObjectToWorldNormal(v.normal);
o.vertex = UnityObjectToClipPos(v.vertex);
o.posW = mul(unity_ObjectToWorld,v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 lDir = normalize(_WorldSpaceLightPos0.xyz);
float3 nDir = i.nDir;
float halfLambert = dot(lDir,nDir)*0.5+0.5;
float3 finalCol = tex2D(_MainTex,float2(halfLambert,0.2));
return float4(finalCol,1.0);
}
ENDCG
}
pass //渲染轮廓线
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float4 normal: NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = v.vertex + normalize(v.normal)*0.05f; //把顶点往法线方向扩大
o.vertex = UnityObjectToClipPos(o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return float4(1.0,1.0,1.0,1.0);
}
ENDCG
}
}
}
-
深度测试
1.深度测试概述:
①从渲染管线来说(逐片元操作流程): 模板测试 → 深度测试 → 透明度混合
②从逻辑上来说:
③从书面上来说:
所谓深度测试,就是针对当前对象在屏幕上(更准确的说是frame buffer)对应的像素点,将对象自身的深度值与当前该像素点缓存的深度值进行比较,如果通过了,本对象在该像素点才会将颜色写入颜色缓冲区,否则否则不会写入颜色缓冲。
④从发展上理解:
画家算法:从远离屏幕的物体开始渲染,一层层叠加在上面。(造成很大的性能浪费,即Over Draw)。
Z-Buffer:通过深度缓冲区来控制渲染顺序。
2.深度缓冲区 Z-Buffer:
深度缓冲就像颜色缓冲(储存所有的片段颜色:视觉输出)一样,在每个片段中储存了信息,并且(通常)和颜色缓冲有着一样的宽度和高度。深度缓冲是由窗口系统自动创建的,它会以16、24或32位float的形式储存它的深度值。在大部分的系统中,深度缓冲的精度都是24位的。
z-buffer中存储的是当前的深度信息,对于每个像素存储一个深度值。
通过 Z Write 和 Z Test 来调用 Z-Buffer,实现想要的渲染结果。
3.深度写入 Z Write:
开启深度写入 并且通过深度测试,才会写入深度缓冲区。
4.深度测试 Z Test:
默认情况:Z Write On & Z Test LEqual,深度缓存一开始为无穷大
5.渲染队列:
①Unity内置的渲染队列:按照渲染顺序,从先到后进行排序, 队列数越小的,越先渲染,队列数越大的,越后渲染。
②Unity中设置渲染队列:Tags { “Queue” = “Transparent” },默认是Geometry。
③不透明物体的渲染顺序:从前往后
透明物体的渲染顺序:从后往前(会造成OverDraw)
6.Early Z技术:
7.深度值:
*为什么深度缓冲区中要存储一个非线性的深度值?
①给离镜头更近的物体更多的精度。
深度缓冲区的深度值是介于0~1的,从观察者看到其内容与场景中的所有对象的 z 值进行了比较。正确的投影特性的非线性深度方程是和1/z成正比的 。这样基本上做的是在z很近是的高精度和 z 很远的时候的低精度。
值为 0.5 在深度缓冲区并不意味着该对象的 z 值是投影平头截体的中间;顶点的 z 值是实际上相当接近近平面的。
②避免近处深度冲突(Z-fighting):
两个平面或三角形如此紧密相互平行深度缓冲区不具有足够的精度以至于无法得到哪一个靠前。结果是,这两个形状不断似乎切换顺序导致怪异出问题。深度冲突是深度缓冲区的普遍问题,当对象的距离越远一般越强(因为深度缓冲区在z值非常大的时候没有很高的精度)。
8.深度测试案例:
①案例一:
代码:
②案例二:
XRay:
代码:
Shader "XRay"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_XRayColor ("XRay Color", Color) = (1,1,1,1)
}
SubShader
{
Pass //XRay
{
Tags { "RenderType"="Opaque" "Queue"="Transparent"}
Blend SrcAlpha One
ZTest Greater
ZWrite Off
Cull Back
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 nDir : TEXCOORD1;
float3 vDir : TEXCOORD2;
float4 color : Color;
};
float4 _XRayColor;
v2f vert (appdata v) //需要在模型空间内
{
v2f o;
o.nDir = v.normal;
o.pos = UnityObjectToClipPos(v.vertex);
o.vDir = ObjSpaceViewDir(v.vertex);
float3 normal = normalize(v.normal);
float3 viewDir = normalize(o.vDir);
o.color = (1-dot(viewDir,normal))*_XRayColor;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
/*float3 normal = normalize(i.nDir);
float3 viewDir = normalize(i.vDir);
float4 color = (1-dot(viewDir,normal))*_XRayColor;*/
return i.color;
}
ENDCG
}
Pass //正常渲染
{
Tags { "RenderType"="Opaque" }
ZTest LEqual
ZWrite on
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
float4 posW : TEXCOORD0;
float3 nDir : TEXCOORD1;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.nDir = UnityObjectToWorldNormal(v.normal);
o.vertex = UnityObjectToClipPos(v.vertex);
o.posW = mul(unity_ObjectToWorld,v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 lDir = normalize(_WorldSpaceLightPos0.xyz);
float3 nDir = i.nDir;
float halfLambert = dot(lDir,nDir)*0.5+0.5;
float3 finalCol = tex2D(_MainTex,float2(halfLambert,1.0));
return float4(finalCol,1.0);
}
ENDCG
}
}
}
*深度测试总结:
使用深度缓冲区最重要的两个值:当前深度缓冲值(ZBufferValue)和深度参考值(referenceValue),并通过比较操作获取理想渲染效果。
Unity中的渲染顺序:先渲染不透明物体,顺序是从前到后;再渲染透明物体,顺序是从后到前。
通过Zwrite 和Z Test组合使用控制半透明物体的渲染。
引入early-z技术后的深度测试相关的渲染流程。
深度缓冲区中存储的深度值为0到1范围的浮点值,且为非线性。
9.深度测试拓展:
暂时不太熟练就不放代码。
教程链接: https://roystan.net/articles/toon-water/
https://zhuanlan.zhihu.com/p/157726737
二、混合模式和剔除
-
混合模式(Blend)
1.混合模式:就是把两种颜色混在一起。具体就是把某一像素位置原来的颜色和将要画上去的颜色,通过某种方式或者算法混在一起,从而实现新的效果。
最终颜色 = Shader计算后的颜色值 * 源因子(SrcFactor) +累积颜色 * 目标因子(DstFactor)
累计颜色可以理解为渲染当前物体后面的颜色即GBuffer中的像素,比如你的背景,还有身后的物体。 混合模式控制的就是源因子和目标因子,在脚本里会看到的就是 :Blend SrcFactor DstFactor
2.混合模式的类型:
①PS的混合模式:
②ShaderLab中的混合模式:
Ⅰ 如果颜色的某一分量超过了1.0,则它会被自动截取为1.0,不需要考虑越界的问题。
Ⅱ 在所有着色器执行完毕,所有纹理都被应用,所有像素准备被呈现到屏幕之后,使用Blend命令来操作这些像素进行混合。
Ⅲ 语法
Blend Off | 关闭blend混合(默认值)。 |
Blend SrcFactor DstFactor | 配置并启动混合。Shader产生的颜色被乘以SrcFactor,已存在于屏幕的颜色乘以DstFactor,并且两者将被加在一起。 |
Blend SrcFactor DstFactor, SrcFactorA DstFactorA | 同上,但是使用不同的要素来混合alpha通道。 |
BlendOp Value | 如果使用 BlendOp 命令,则混合操作将设置为该值。否则,混合操作默认为 Add。不是添加混合颜色在一起,而是对它们做不同的操作。 |
BlendOp OpColor, OpAlpha | 同上,但是使用不同的操作来处理alpha通道. |
AlphaToMaskOn | 常用在开启多重渲染(MSAA)的地表植被的渲染 |
Ⅳ 混合因子
Ⅴ操作符
总结:
3.混合模式的实现方法:
没有在SubShader的uniform内写入_Src/DstBlend值,是因为我们的混合操作不是在shader层内。
-
剔除
①法线剔除:也称为背面消隐,根据法线朝向判断哪个面被剔除掉。 可以用来控制是否双面渲染。
语法:Cull(Off Front Back)
②面裁切:clip函数会将参数小于某像素点直接在片元阶段丢弃掉,常用于制作溶解,裁剪等效果。
语法:Clip();默认会切掉0.5的部分。 或者使用if。
*总结:
1.开启双面渲染相当于绘制了两次。
2.Clip函数在某些PowerVR的机型上效率很低。
3.面裁切Clip使用AlphaTest队列。
-
作业
来源:https://www.yuque.com/wuyan-popj0/qo443s/toyn1l26c7iyoaob#DrM3P
Shader "BlendMode"
{
Properties
{
_FrontTex ("Front Tex", 2D) = "white" {}
_FrontTexAlpha("FrontTexAlpha",Range(0,1)) = 1
_GrabTexAlpha("GrabTexAlpha",Range(0,1)) = 1
[KeywordEnum(Normal,Alpha,Darken,Multiply,Screen,Lighten,LinearDodge,ColorBurn,LinearBurn,DarkenColor,ColorDodge,LighterColor,Overlay,SoftLight,HardLight,VividLight,LinearLight,PinLight,HardMix,Difference,Exclusion,Substract,Divide,Hue,Saturation,Color,Luminosity)] _BlendMode("混合模式", Float) = 1
}
SubShader
{
GrabPass{ "_GrabTex" }
Pass
{
Tags { "Queue"="Transparent" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma shader_feature _BLENDMODE_NORMAL _BLENDMODE_ALPHA _BLENDMODE_DARKEN _BLENDMODE_MULTIPLY _BLENDMODE_SCREEN _BLENDMODE_LIGHTEN _BLENDMODE_LINEARDODGE _BLENDMODE_COLORBURN _BLENDMODE_LINEARBURN _BLENDMODE_DARKENCOLOR _BLENDMODE_COLORDODGE _BLENDMODE_LIGHTERCOLOR _BLENDMODE_OVERLAY _BLENDMODE_SOFTLIGHT _BLENDMODE_HARDLIGHT _BLENDMODE_VIVIDLIGHT _BLENDMODE_LINEARLIGHT _BLENDMODE_PINLIGHT _BLENDMODE_HARDMIX _BLENDMODE_DIFFERENCE _BLENDMODE_EXCLUSION _BLENDMODE_SUBSTRACT _BLENDMODE_DIVIDE _BLENDMODE_HUE _BLENDMODE_SATURATION _BLENDMODE_COLOR _BLENDMODE_LUMINOSITY
#include "UnityCG.cginc"
sampler2D _FrontTex;float4 _FrontTex_ST;
sampler2D _GrabTex;float4 _GrabTex_ST;
float _FrontTexAlpha,_GrabTexAlpha;
//柔光
float SoftLight(float Src,float Dst)
{
return (Src < 0.5) ? Dst - (1.0 - 2.0 * Src) * Dst * (1.0 - Dst) : (Dst < 0.25) ? Dst + (2.0 * Src - 1.0) * Dst * ((16.0 * Dst - 12.0) * Dst + 3.0) : Dst + (2.0 * Src - 1.0) * (sqrt(Dst) - Dst);
}
float3 SoftLight(float4 Src,float4 Dst)
{
float3 c;
c.x = SoftLight(Src.x,Dst.x);
c.y = SoftLight(Src.y,Dst.z);
c.z = SoftLight(Src.z,Dst.z);
return c;
}
//强光
float HardLight(float Src,float Dst)
{
return (Src < 0.5) ? 2.0 * Src * Dst : 1.0 - 2.0 * (1.0 - Src) * (1.0 - Dst);
}
float3 HardLight(float4 Src,float4 Dst)
{
float3 c;
c.x = HardLight(Src.x,Dst.x);
c.y = HardLight(Src.y,Dst.z);
c.z = HardLight(Src.z,Dst.z);
return c;
}
//亮光
float VividLight(float Src,float Dst)
{
return (Src < 0.5) ? 1.0 - (1.0 - Dst) / (2.0 * Src) : Dst / (2.0 * (1.0 - Src));
}
float3 VividLight(float4 Src,float4 Dst)
{
float3 c;
c.x = VividLight(Src.x,Dst.x);
c.y = VividLight(Src.y,Dst.z);
c.z = VividLight(Src.z,Dst.z);
return c;
}
//点光
float PinLight(float Src,float Dst)
{
return (2.0 * Src - 1.0 > Dst) ? 2.0 * Src - 1.0 : (Src < 0.5 * Dst) ? 2.0 * Src : Dst;
}
float3 PinLight(float4 Src,float4 Dst)
{
float3 c;
c.x = PinLight(Src.x,Dst.x);
c.y = PinLight(Src.y,Dst.z);
c.z = PinLight(Src.z,Dst.z);
return c;
}
//色相
float3 RGB2HSV(float3 c)
{
float4 K = float4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
float4 p = lerp(float4(c.bg, K.wz), float4(c.gb, K.xy), step(c.b, c.g));
float4 q = lerp(float4(p.xyw, c.r), float4(c.r, p.yzx), step(p.x, c.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 HSV2RGB(float3 c)
{
float4 K = float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
float3 p = abs(frac(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * lerp(K.xxx, saturate(p - K.xxx), c.y);
}
float3 Hue(float3 Src,float3 Dst)
{
Dst = RGB2HSV(Dst);
Dst.x = RGB2HSV(Src).x;
return HSV2RGB(Dst);
}
//饱和度
float3 Saturation(float3 Src,float3 Dst)
{
Dst = RGB2HSV(Dst);
Dst.y = RGB2HSV(Src).y;
return HSV2RGB(Dst);
}
//颜色
float3 Color(float3 Src,float3 Dst)
{
Dst = RGB2HSV(Dst);
Dst.z = RGB2HSV(Src).z;
return HSV2RGB(Dst);
}
//明度
float3 Luminosity(float3 Src,float3 Dst)
{
float dLum = dot(Dst,float3(0.3,0.59,0.11));
float sLum = dot(Src,float3(0.3,0.59,0.11));
float lum = sLum - dLum;
float3 C = Dst + lum;
float minC = min(min(C.x,C.y),C.z);
float maxC = max(max(C.x,C.y),C.z);
if(minC < 0.0)
return sLum + ((C - sLum) * sLum) / (sLum - minC);
else if(maxC > 1.0)
return sLum + ((C - sLum)) * (1.0 - sLum) / (maxC - sLum);
else
return C;
}
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 scrPos : TEXCOORD1;
float4 pos : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.scrPos = ComputeGrabScreenPos(o.pos);
o.uv = TRANSFORM_TEX(v.uv, _FrontTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 Src = tex2D(_FrontTex,i.uv);
float2 screenPos = i.scrPos.xy / i.scrPos.w;
fixed4 Dst = tex2D(_GrabTex,screenPos);
fixed3 col = fixed3(0,0,0);
#if _BLENDMODE_NORMAL //正常
col = Src.rgb;
#elif _BLENDMODE_ALPHA //正常透明度混合
col = Src.rgb * _FrontTexAlpha + Dst.rgb * (1 - _FrontTexAlpha);
#elif _BLENDMODE_DARKEN //变暗
col = min(Src.rgb,Dst.rgb);
#elif _BLENDMODE_MULTIPLY //正片叠底
col = Src.rgb * Dst.rgb;
#elif _BLENDMODE_SCREEN //滤色
col = Src.rgb + Dst.rgb - Src.rgb * Dst.rgb;
#elif _BLENDMODE_LIGHTEN //变亮
col = max(Src.rgb,Dst.rgb);
#elif _BLENDMODE_LINEARDODGE //线性减淡
col = Src.rgb + Dst.rgb;
#elif _BLENDMODE_COLORBURN //颜色加深
col = 1.0 - (1.0 - Dst.rgb) / Src.rgb;
#elif _BLENDMODE_LINEARBURN //线性加深
col = Src.rgb + Dst.rgb - 1.0;
#elif _BLENDMODE_DARKENCOLOR //深色
col = (Src.x + Src.y + Src.z < Dst.x + Dst.y + Dst.z)? Src : Dst;
#elif _BLENDMODE_COLORDODGE //颜色减淡
col = Dst.rgb / (1.0 - Src.rgb);
#elif _BLENDMODE_LIGHTERCOLOR //浅色
col = (Src.x + Src.y + Src.z > Dst.x + Dst.y + Dst.z)? Src : Dst;
#elif _BLENDMODE_OVERLAY //叠加
col = (Dst.rgb < 0.5) ? 2.0 * Src.rgb * Dst.rgb : 1.0 - 2.0 * (1.0 - Src.rgb) * (1.0 - Dst.rgb);
#elif _BLENDMODE_SOFTLIGHT //柔光
col = SoftLight(Src,Dst);
#elif _BLENDMODE_HARDLIGHT //强光
col = HardLight(Src,Dst);
#elif _BLENDMODE_VIVIDLIGHT //亮光
col = VividLight(Src,Dst);
#elif _BLENDMODE_LINEARLIGHT //线性光
col = 2.0 * Src.rgb + Dst.rgb - 1.0;
#elif _BLENDMODE_PINLIGHT //点光
col = PinLight(Src,Dst);
#elif _BLENDMODE_HARDMIX //实色混合
col = floor(Src.rgb + Dst.rgb);
#elif _BLENDMODE_DIFFERENCE //差值
col = abs(Dst.rgb - Src.rgb);
#elif _BLENDMODE_EXCLUSION //排除
col = Src.rgb + Dst.rgb - 2.0 * Src.rgb * Dst.rgb;
#elif _BLENDMODE_SUBSTRACT //减去
col = Src.rgb - Dst.rgb;
#elif _BLENDMODE_DIVIDE //划分
col = Src / Dst;
#elif _BLENDMODE_HUE //色相
col = Hue(Src,Dst);
#elif _BLENDMODE_SATURATION //饱和度
col = Saturation(Src,Dst);
#elif _BLENDMODE_COLOR //颜色加深
col = Color(Src,Dst);
#elif _BLENDMODE_LUMINOSITY //明度
col = Luminosity(Src,Dst);
#endif
return fixed4(col,1.0);
}
ENDCG
}
}
}
三、曲面细分着色器和几何着色器
-
曲面细分着色器(Tessellation Shader)
1.曲面细分着色器的应用:
①海浪、雪地等
②与置换贴图的结合
可以用来制作海浪、雪地里出现的脚印;改变物体形状。可以根据距离或者自定的规则来动态调整模型的复杂度。
2.输入和输出:
①输入:Patch(可以看成多个顶点的几何,包含每个顶点的属性,可以指定一个Patch包含的顶点数以及自己的属性)
②功能:将图元进行细分(可以是三角形、矩形等)
③输出:细分后的顶点
3.曲面细分着色器的流程:
①Hull Shader:
Ⅰ决定细分的数量(设定Tessellation factor以及Inner Tessellation factor)
*Tessellation factor:
equal_Spacing(等分,并限制在[1, max], 如果有小数则向上取整)
fractional_even_spacing(限制在[2, max], 向上取最近的偶数)
fractional_odd_spacing(限制在[1, max - 1], 向上取最近的奇数,周长会被划分为n-2的等长部分,以及两个位于两端的部分(可能比中间部分更短)。具体长度与小数部分有关,为了获取更平滑的细分。)
*Inner Tessellation factor:
Ⅱ对输入的patch参数进行改变(如果需要)
②Tessellation Primitive Generation: 进行细分操作(不可编程)
③Domain Shader:对细分后的点进行处理,从重心空间转换到屏幕空间
-
作业
①quad
Shader "TessShader"
{
Properties
{
_TessellationUniform("TessellationUniform",Range(1,64)) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
//定义2个函数 hull domain
#pragma hull hullProgram
#pragma domain domainProgram
#pragma vertex tessvert
#pragma fragment frag
//曲面细分文件
#include "Tessellation.cginc"
#include "UnityCG.cginc"
#pragma target 5.0
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
//应用在domain函数中,用来空间转换的函数
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.normal = v.normal;
o.tangent = v.tangent;
return o;
}
//定义宏,能够在不支持曲面细分着色器的硬件上不报错
#ifdef UNITY_CAN_COMPILE_TESSELLATION
//顶点着色器结构
struct TessVertex
{
float4 vertex : INTERNALTESSPOS; //?
float3 normal : NORMAL;
float4 tangent : TANGENT;
float2 uv : TEXCOORD0;
};
struct OutputPatchConstant
{
//不同的图元,结构不同,用于Hull Shader里,定义了patch属性:
//Tessellation Factor和Inner Tessellation Factor
float edge[3] : SV_TESSFACTOR;
float inside : SV_INSIDETESSFACTOR;
};
//顶点着色器
TessVertex tessvert (appdata v)
{
TessVertex o;
o.vertex = v.vertex;
o.normal = v.normal;
o.tangent = v.tangent;
o.uv = v.uv;
return o;
}
float _TessellationUniform;
OutputPatchConstant hsconst (InputPatch<TessVertex,3> patch)
{
//定义曲面细分的参数
OutputPatchConstant o;
o.edge[0] = _TessellationUniform;
o.edge[1] = _TessellationUniform;
o.edge[2] = _TessellationUniform;
o.inside = _TessellationUniform;
return o;
}
[UNITY_domain("tri")]//确定图元,quad,triangle等
[UNITY_partitioning("fractional_odd")]//拆分edge的规则,equal_spacing,fractional_odd,fractional_even
[UNITY_outputtopology("triangle_cw")]//定义输出图元的顺序(按顺时针或逆时针)组装,影响到最后的显示
[UNITY_patchconstantfunc("hsconst")]//定义曲面细分的参数,hsconst规定patch的曲面细分的属性。一个patch(图元)一共有三个点,但是这三个点都共用这个函数
[UNITY_outputcontrolpoints(3)] //不同的图元会对应不同的控制点,三角形三个点
//根据图元的不同,InputPatch的参数不一样,三角形就是3个
TessVertex hullProgram (InputPatch<TessVertex,3> patch,uint id : SV_OutputControlPointID)
{
//定义hullshaderV函数
return patch[id];
}
[UNITY_domain("tri")]//同样需要定义图元
//bary相当于重心空间的重心系数,和真正的重心坐标相乘,并还原到模型空间
v2f domainProgram (OutputPatchConstant tessFactors, const OutputPatch<TessVertex,3>patch,float3 bary :SV_DOMAINLOCATION)//bary:重心坐标
{
appdata v;
v.vertex = patch[0].vertex*bary.x + patch[1].vertex*bary.y + patch[2].vertex*bary.z;
v.tangent = patch[0].tangent*bary.x + patch[1].tangent*bary.y + patch[2].tangent*bary.z;
v.normal = patch[0].normal*bary.x + patch[1].normal*bary.y + patch[2].normal*bary.z;
v.uv = patch[0].uv*bary.x + patch[1].uv*bary.y + patch[2].uv*bary.z;
v2f o = vert (v);
return o;
}
#endif
fixed4 frag (v2f i) : SV_Target
{
return float4(1.0,1.0,1.0,1.0);
}
ENDCG
}
}
}
②曲面细分和置换贴图
Shader "TessDisShader"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
_DisplacementMap ("DisplacementMap", 2D) = "gray"{}
_DisplacementStrength("DisplacementStrength",Range(0,1)) = 0
_Smoothness("Smoothness",Range(0,5))=0.5
_TessellationUniform("TessellationUniform",Range(1,64)) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" "LightMode"="ForwardBase"}
LOD 100
Pass
{
CGPROGRAM
#pragma hull hullProgram
#pragma domain domainProgram
#pragma vertex tessvert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "Tessellation.cginc"
#pragma target 5.0
sampler2D _MainTex; float4 _MainTex_ST;
sampler2D _DisplacementMap; float4 _DisplacementMap_ST;
float _DisplacementStrength;
float _Smoothness;
float _TessellationUniform;
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float4 posWS : TEXCOORD1;
half3 normalDirWS :TEXCOORD2;
half3 tangentDirWS :TEXCOORD3;
half3 bitangentDirWS :TEXCOORD4;
};
//应用在domain函数中,用来空间转换的函数
v2f vert (appdata v)
{
v2f o;
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
//Displacement
//由于并不是在Fragnent shader中读取图片,GPU无法获取mipmap信息,
//因此需要使用tex2Dlod来读取图片,使用第四坐标作为mipmap的level,这里取了0
float Displacement = _DisplacementStrength * tex2Dlod(_DisplacementMap, float4(o.uv.xy, 0.0, 0.0)).g;
//Displacement = (Displacement-0.5)*_DisplacementStrength;
v.vertex.xyz += v.normal * Displacement;
o.vertex = UnityObjectToClipPos(v.vertex);
o.posWS = mul(unity_ObjectToWorld,v.vertex);
//切线转世界空间
o.normalDirWS = normalize(UnityObjectToWorldNormal(v.normal));
o.tangentDirWS = normalize(mul(unity_ObjectToWorld, v.tangent).xyz);
o.bitangentDirWS = normalize(cross(o.normalDirWS,o.tangentDirWS) * v.tangent.w);
return o;
}
//定义宏,能够在不支持曲面细分着色器的硬件上不报错
#ifdef UNITY_CAN_COMPILE_TESSELLATION
//顶点着色器结构
struct TessVertex
{
float4 vertex : INTERNALTESSPOS; //?
float3 normal : NORMAL;
float4 tangent : TANGENT;
float2 uv : TEXCOORD0;
};
struct OutputPatchConstant
{
//不同的图元,结构不同,用于Hull Shader里,定义了patch属性:
//Tessellation Factor和Inner Tessellation Factor
float edge[3] : SV_TESSFACTOR;
float inside : SV_INSIDETESSFACTOR;
};
//顶点着色器
TessVertex tessvert (appdata v)
{
TessVertex o;
o.vertex = v.vertex;
o.normal = v.normal;
o.tangent = v.tangent;
o.uv = v.uv;
return o;
}
OutputPatchConstant hsconst (InputPatch<TessVertex,3> patch)
{
//定义曲面细分的参数
OutputPatchConstant o;
o.edge[0] = _TessellationUniform;
o.edge[1] = _TessellationUniform;
o.edge[2] = _TessellationUniform;
o.inside = _TessellationUniform;
return o;
}
[UNITY_domain("tri")]//确定图元,quad,triangle等
[UNITY_partitioning("fractional_odd")]//拆分edge的规则,equal_spacing,fractional_odd,fractional_even
[UNITY_outputtopology("triangle_cw")]//定义输出图元的顺序(按顺时针或逆时针)组装,影响到最后的显示
[UNITY_patchconstantfunc("hsconst")]//定义曲面细分的参数,hsconst规定patch的曲面细分的属性。一个patch(图元)一共有三个点,但是这三个点都共用这个函数
[UNITY_outputcontrolpoints(3)] //不同的图元会对应不同的控制点,三角形三个点
//根据图元的不同,InputPatch的参数不一样,三角形就是3个
TessVertex hullProgram (InputPatch<TessVertex,3> patch,uint id : SV_OutputControlPointID)
{
//定义hullshaderV函数
return patch[id];
}
[UNITY_domain("tri")]//同样需要定义图元
//bary相当于重心空间的重心系数,和真正的重心坐标相乘,并还原到模型空间
v2f domainProgram (OutputPatchConstant tessFactors, const OutputPatch<TessVertex,3>patch,float3 bary :SV_DOMAINLOCATION)//bary:重心坐标
{
appdata v;
v.vertex = patch[0].vertex*bary.x + patch[1].vertex*bary.y + patch[2].vertex*bary.z;
v.tangent = patch[0].tangent*bary.x + patch[1].tangent*bary.y + patch[2].tangent*bary.z;
v.normal = patch[0].normal*bary.x + patch[1].normal*bary.y + patch[2].normal*bary.z;
v.uv = patch[0].uv*bary.x + patch[1].uv*bary.y + patch[2].uv*bary.z;
v2f o = vert (v);
return o;
}
#endif
float4 frag (v2f i) : SV_Target
{
half3x3 TBN = half3x3(i.tangentDirWS, i.bitangentDirWS, i.normalDirWS);
float3 normalDirTS = UnpackNormal(tex2D(_DisplacementMap, i.uv));
float3 normalDirWS = normalize(mul(normalDirTS,TBN));
float3 lightDir = _WorldSpaceLightPos0.xyz;
float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.posWS.xyz);
float3 halfDir = normalize(lightDir + viewDir);
float halflambert = saturate(dot(normalDirWS,lightDir));
float blinnPhong = pow(saturate(dot(normalDirWS, halfDir)), _Smoothness * 100);
float3 albedo = tex2D(_MainTex, i.uv).rgb;
float3 lightColor = _LightColor0.rgb;
float3 diffuse = albedo * lightColor * halflambert;
float3 specular = albedo * blinnPhong;
return float4(diffuse + specular,1.0);
}
ENDCG
}
}
}
-
几何着色器(Geometry Shader)
1.几何着色器的应用:
①几何动画
②草地等(与曲面细分着色器结合)
2.作业
Shader "Geometry shader"
{
Properties
{
[Header(shader)]
_TopColor ("上部颜色", color) = (1.0,1.0,1.0,1.0)
_BottomColor ("下部颜色", color) = (1.0,1.0,1.0,1.0)
_Transparent ("透明度", range(0,1)) = 0.5
_Width ("宽度", float) = 0.05
_WidthRandom ("随机宽度", float) = 0.02
_Height ("高度", float) = 0.5
_HeightRandom ("随机高度", float) = 0.3
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Cull off
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//定义几何着色器
#pragma geometry geo
#include "UnityCG.cginc"
uniform float4 _TopColor;
uniform float4 _BottomColor;
uniform float _Transparent;
uniform float _Width;
uniform float _WidthRandom;
uniform float _Height;
uniform float _HeightRandom;
float rand(float3 co)
{
float f = frac(sin(dot(co.xyz, float3(12.9898, 78.233, 53.539))) * 43758.5453);
return f;
}
float3x3 AngleAxis3x3(float angle, float3 axis)
{
float c, s;
sincos(angle, s, c);
float t = 1 - c;
float x = axis.x;
float y = axis.y;
float z = axis.z;
return float3x3(
t * x * x + c, t * x * y - s *z, t * x * z + s * y,
t * x * x + s * z, t * y * y + c, t * y * z - s * x,
t * x * z - s * y, t * y * z + s * x, t * z * z + c);
}
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
struct v2f
{
float4 vertex : SV_POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = v.vertex; //不用转换空间
o.normal = v.normal;
o.tangent = v.tangent;
return o;
}
struct geometryOutput
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
geometryOutput CreateGeoOutput(float3 pos, float2 uv)
{
geometryOutput o;
o.pos = UnityObjectToClipPos(pos);
o.uv = uv;
return o;
}
[maxvertexcount(3)] //定义最多顶点数
void geo(triangle v2f IN[3] : SV_POSITION, inout TriangleStream<geometryOutput> triStream){
float3 pos = IN[0].vertex;
float3 vNormal = IN[0].normal;
float4 vTangent = IN[0].tangent;
float3 vBinormal = cross(vNormal, vTangent) * vTangent.w;
float height = (rand(pos.xyz) * 2 - 1) * _HeightRandom + _Height;
float width = (rand(pos.xyz) * 2 - 1) * _WidthRandom + _Width;
//构建矩阵 构建了TBN矩阵,并且与旋转矩阵相乘,获得转换矩阵
float3x3 facingRotationMatrix = AngleAxis3x3(rand(pos) * UNITY_TWO_PI, float3(0,0,1));
float3x3 TBN = float3x3(
vTangent.x, vBinormal.x, vNormal.x,
vTangent.y, vBinormal.y, vNormal.y,
vTangent.z, vBinormal.z, vNormal.z
);
float3x3 transformationMat = mul(TBN,facingRotationMatrix);
geometryOutput o;
//"TriangleStream"类似用来装配三角形的工具,用来输出图元
triStream.Append(CreateGeoOutput(pos+mul(transformationMat,float3(width,0,0)),float2(0,0)));
triStream.Append(CreateGeoOutput(pos+mul(transformationMat,float3(-width,0,0)),float2(1,0)));
triStream.Append(CreateGeoOutput(pos+mul(transformationMat,float3(0,0,height)),float2(0.5,1)));
}
fixed4 frag (geometryOutput i) : SV_Target
{
fixed4 color = lerp(_BottomColor, _TopColor, i.uv.y);
return color;
}
ENDCG
}
}
}
②草地1.0
https://roystan.net/articles/grass-shader/
GrassShader:
Shader "Grass"
{
Properties
{
_TopColor ("顶部颜色", Color) = (1.0, 1.0, 1.0, 1.0)
_BottomColor ("底部颜色", Color) = (1.0, 1.0, 1.0, 1.0)
_TranslucentGain("半阴影", float) = 1.0
_BendRotationRandom ("随机弯曲系数", Range(0,1)) = 0.2
_Width ("宽度", float) = 0.05
_WidthRandom ("随机宽度系数", float) = 0.02
_Height ("高度", float) = 0.5
_HeightRandom ("随机高度", float) = 0.3
_Forward ("前偏移量", float) = 0.38
_Curve ("曲率", range(1,4)) = 2
_TessellationUniform("细分系数", Range(1, 64)) = 1
_WindMap ("风场贴图", 2D) = "white"{}
_WindFrequency ("风场频率", Vector) = (0.05, 0.05, 0, 0)
_WindStrength("风力大小", Float) = 1
}
CGINCLUDE
#include "UnityCG.cginc"
#include "Autolight.cginc"
#include "Assets/TessShader/CustomTessellation.cginc"
#define BLADE_SEGMENTS 3 //定义这个语句为3,线段计数
uniform float4 _TopColor;
uniform float4 _BottomColor;
uniform float _TranslucentGain;
uniform float _BendRotationRandom;
uniform float _Width;
uniform float _WidthRandom;
uniform float _Height;
uniform float _HeightRandom;
uniform sampler2D _WindMap; uniform float4 _WindMap_ST;
uniform float2 _WindFrequency;
uniform float _WindStrength;
uniform float _Forward;
uniform float _Curve;
float rand(float3 co) //数学函数
{
float f = frac(sin(dot(co.xyz, float3(12.9898, 78.233, 53.539))) * 43758.5453);
return f;
}
float3x3 AngleAxis3x3(float angle, float3 axis)
{
float s, c;
sincos(angle, s, c);
float x = axis.x;
float y = axis.y;
float z = axis.z;
return float3x3(
x * x + (y * y + z * z) * c, x * y * (1 - c) - z * s, x * z * (1 - c) - y * s,
x * y * (1 - c) + z * s, y * y + (x * x + z * z) * c, y * z * (1 - c) - x * s,
x * z * (1 - c) - y * s, y * z * (1 - c) + x * s, z * z + (x * x + y * y) * c
);
}
struct geometryOutput
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
unityShadowCoord4 _ShadowCoord : TEXCOORD1;
};
geometryOutput geoOutput(float3 pos, float2 uv, float3 normal)
{
geometryOutput o;
o.pos = UnityObjectToClipPos(pos);
o.uv = uv;
// 阴影从屏幕空间阴影贴图纹理中采样
o._ShadowCoord = ComputeScreenPos(o.pos);
o.normal = UnityObjectToWorldNormal(normal);
//解决叶片表面的伪阴影(自身阴影投影导致),应用线性偏差或将顶点的剪辑空间位置稍微远离屏幕解决
//使用unity宏,确保操作仅在投影期间运行
#if UNITY_PASS_SHADOWCASTER
o.pos = UnityApplyLinearShadowBias(o.pos);
#endif
return o;
}
geometryOutput GrassVertex(float3 vertex, float width, float height, float forward, float2 uv, float3x3 transMatrix)
{
float3 posTS = float3(width, forward, height);
//先计算假设没有曲率的法线,float3(0, -1, 0)
//当曲率大于1,每个顶点的z分量将偏移forward量
float3 normalTS = normalize(float3(0, -1, forward));
float3 normalLS = mul(transMatrix, normalTS);
float3 posLS = vertex + mul(transMatrix, posTS);
//返回
return geoOutput(posLS, uv, normalLS);
}
//每个图元有几个点
[maxvertexcount(BLADE_SEGMENTS * 2 + 1)]
//设置每个顶点将使用<geometryOutput>结构输出三角形
void geo(triangle vertexOutput IN[3] : SV_POSITION, inout TriangleStream<geometryOutput> triStream)
{
float3 pos = IN[0].vertex;
float3 vNormal = IN[0].normal;
float4 vTangent = IN[0].tangent;
float3 vBinormal = cross(vNormal,vTangent) * vTangent.w;
float3x3 tangentToLocal = float3x3(
vTangent.x, vBinormal.x, vNormal.x,
vTangent.y, vBinormal.y, vNormal.y,
vTangent.z, vBinormal.z, vNormal.z
);
float3x3 facingRotationMatrix = AngleAxis3x3(rand(pos) * UNITY_TWO_PI, float3(0, 0, 1));
float3x3 bendRotationMatrix = AngleAxis3x3(rand(pos.zzx) * _BendRotationRandom * UNITY_PI * 0.5, float3(-1,0,0));
float2 uv = pos.xz * _WindMap_ST.xy + _WindMap_ST.zw + _WindFrequency * _Time.y;
//remap到-1到1
float2 windSample = (tex2Dlod(_WindMap, float4(uv, 0, 0)).xy * 2 - 1) * _WindStrength;
float3 wind = normalize(float3(windSample.x, windSample.y, 0));
float3x3 windRotation = AngleAxis3x3(UNITY_PI * windSample, wind);
//这个矩阵全部会把整个叶片都转向
float3x3 transMatrix = mul(mul(mul(tangentToLocal, windRotation), facingRotationMatrix),bendRotationMatrix);
//要新设置一个矩阵固定叶片底部的点
float3x3 transMatrixbottom = mul(tangentToLocal, facingRotationMatrix);
float height = (rand(pos.zyx) * 2 - 1) * _HeightRandom + _Height;
float width = (rand(pos.xzy) * 2 - 1) * _WidthRandom + _Width;
float forward = rand(pos.yyz) * _Forward;
for(int i=0; i<BLADE_SEGMENTS; i++)
{
float t = i/(float)BLADE_SEGMENTS;
float segmentHeight = height * t; //第几段就是三分之几的高度
float segmentWidth = width * (1-t);
float segmentForward = forward * pow(t, _Curve);
//如果i==0,那么就是底部的点 注意:只有在循环内新定义的矩阵才会生效
float3x3 transformMatrix = i == 0 ? transMatrixbottom : transMatrix;
triStream.Append(GrassVertex(pos, segmentWidth, segmentHeight, segmentForward, float2(0, t), transformMatrix));
triStream.Append(GrassVertex(pos, -segmentWidth, segmentHeight, segmentForward, float2(1, t), transformMatrix));
}
//triStream.Append(GrassVertex(pos, width, 0, float2(0, 0), transMatrixbottom));//geoOutput(pos + mul(transMatrixbottom,float3(width, 0, 0)), float2(0,0)));
//triStream.Append(GrassVertex(pos, -width, 0, float2(1, 0), transMatrixbottom));//geoOutput(pos + mul(transMatrixbottom,float3(-width, 0, 0)), float2(1,0)));
//顶点
triStream.Append(GrassVertex(pos, 0, height, forward,float2(0.5, 1), transMatrix));//geoOutput(pos + mul(transMatrix,float3(0, 0, height)), float2(0.5,1)));
}
ENDCG
SubShader
{
Tags { "RenderType"="Opaque" "LightMode" = "ForwardBase" }
LOD 100
Cull off
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma hull hull
#pragma domain domain
#pragma geometry geo
#pragma target 5.0
#pragma multi_compile_fwdbase
#include "Lighting.cginc"
float4 frag (geometryOutput i, fixed facing : VFACE) : SV_Target
{
float shadow = SHADOW_ATTENUATION(i);
float NdotL = saturate(saturate(dot(i.normal, _WorldSpaceLightPos0)) + _TranslucentGain) * shadow;
//环境光 球谐函数
float3 ambient = ShadeSH9(float4(i.normal, 1));
float4 lightIntensity = NdotL * _LightColor0 + float4(ambient,1);
float4 Color = lerp(_BottomColor, _TopColor * lightIntensity, i.uv.y);
//如果看正面 返回正数,如果看背面 返回负数
//float3 normal = facing > 0 ? i.normal : -i.normal;
return Color;
}
ENDCG
}
//阴影pass
Pass
{
Tags{ "LightMode" = "ShadowCaster" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma hull hull
#pragma domain domain
#pragma geometry geo
#pragma target 5.0
#pragma multi_compile_shadowcaster
float4 frag(geometryOutput i) : SV_TARGET
{
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
}
}
Tessellation:
// Tessellation programs based on this article by Catlike Coding:
// https://catlikecoding.com/unity/tutorials/advanced-rendering/tessellation/
struct vertexInput
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
struct vertexOutput
{
float4 vertex : SV_POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
struct TessellationFactors
{
float edge[3] : SV_TessFactor;
float inside : SV_InsideTessFactor;
};
vertexInput vert(vertexInput v)
{
return v;
}
vertexOutput tessVert(vertexInput v)
{
vertexOutput o;
// Note that the vertex is NOT transformed to clip
// space here; this is done in the grass geometry shader.
o.vertex = v.vertex;
o.normal = v.normal;
o.tangent = v.tangent;
return o;
}
float _TessellationUniform;
TessellationFactors patchConstantFunction (InputPatch<vertexInput, 3> patch)
{
TessellationFactors f;
f.edge[0] = _TessellationUniform;
f.edge[1] = _TessellationUniform;
f.edge[2] = _TessellationUniform;
f.inside = _TessellationUniform;
return f;
}
[UNITY_domain("tri")]
[UNITY_outputcontrolpoints(3)]
[UNITY_outputtopology("triangle_cw")]
[UNITY_partitioning("integer")]
[UNITY_patchconstantfunc("patchConstantFunction")]
vertexInput hull (InputPatch<vertexInput, 3> patch, uint id : SV_OutputControlPointID)
{
return patch[id];
}
[UNITY_domain("tri")]
vertexOutput domain(TessellationFactors factors, OutputPatch<vertexInput, 3> patch, float3 barycentricCoordinates : SV_DomainLocation)
{
vertexInput v;
#define MY_DOMAIN_PROGRAM_INTERPOLATE(fieldName) v.fieldName = \
patch[0].fieldName * barycentricCoordinates.x + \
patch[1].fieldName * barycentricCoordinates.y + \
patch[2].fieldName * barycentricCoordinates.z;
MY_DOMAIN_PROGRAM_INTERPOLATE(vertex)
MY_DOMAIN_PROGRAM_INTERPOLATE(normal)
MY_DOMAIN_PROGRAM_INTERPOLATE(tangent)
return tessVert(v);
}
四、延迟渲染(RenderPath)
-
渲染路径(Rendering Path)
渲染路径即决定光照的实现方式。简言之,就是当前渲染目标使用光照的流程。
-
渲染方式
1.前向渲染(Forward Rendering):
①具体流程:
在渲染每一帧时,每个顶点/片元都要执行一次片元着色器代码,这时需要将所有的光照信息都传递到片元着色器中。虽然大部分情况下的光源都趋向于小型化,而其照亮的区域也不大,但即便是光源离这个像素所对应的世界空间中的位置很远,但计算光照时,还是会把所有的光源都考虑进去。
*总结:计算光源时会把所有光源都计算进去,造成很大浪费。
2.延迟渲染(Deferred Rendering):
①原理介绍:主要解决大量光照渲染的方案。延迟渲染的实质,是先不要做迭代三角形做光照计算,而是先找出来你能看到的所有像素,再去迭代光照。直接迭代三角形的话,由于大量三角形你是看不到的,无疑是极大的浪费。
②流程:
Ⅰ将渲染过程拆分成两个渲染通路(pass)。
Ⅱ第一个pass称为几何处理通路。首先将场景渲染一次,获取到待渲染对象的各种几何信息存储到名为G-buffer的缓冲区中,这些缓冲区将会在之后用作更复杂的光照计算。由于有深度测试,所以最终写入G-buffer中的各个数据都是离摄像机最近的片元的几何属性,这意味着最后在G-buffer中的片元必定要进行光照计算的。
Ⅲ第二个pass称为光照处理通路。该pass会遍历所有G-buffer中的位置、颜色、法线等参数,执行一次光照计算。
RT(G-buffer)相当于把整个屏幕的信息绘制到一个图中,每个RT都可以写到一个G-buffer中。
G-buffer中的数据都是2D的,所以我们的光照计算就相当于一个2D的光照后处理。
③G-Buffer:指Geometry Buffer,亦即“几何缓冲”。区别于普通的仅将颜色渲染到纹理中,G-Buffer指包含颜色、法线、世界空间坐标的缓冲区,亦即指包含颜色、法线、世界空间坐标的纹理。由于G-Buffer需要的向量长度超出通常纹理能包含的向量的长度,通常在游戏开发中,使用多渲染目标技术来生成G-Buffer,即在一次绘制中将颜色、法线、世界空间坐标分别渲染到三张浮点纹理中。
④延迟渲染不支持不透明物体的渲染,延迟渲染中的透明物体渲染方式为前向渲染。(没有深度信息)
作业
3.不同渲染路径的优劣及特性:
①特性:
后处理方式不同。(如果需要深度信息进行后处理,前向渲染需要单独渲染出一张深度图,延迟渲染直接用G-buffer中的深度信息计算即可)
着色计算不同(shader)。(延迟渲染因为是最后统一计算光照的,所以只能算一个光照模型(如果需要其他光照模型,只能切换pass))
抗锯齿方式不同
②前向渲染:
优点:1.支持半透明渲染 2.支持使用多个光照pass 3.支持自定义光照计算方式
缺点:1.光源数量对计算复杂度影响巨大 2.访问深度等数据需要额外计算(额外渲染一张深度图)。
③延迟渲染:
优点:1.大量光照场景优势明显 2.只渲染可见像素,节省计算量 3.对后处理支持良好 4.用更少的shader
缺点:1.不支持MSAA 2.不能渲染透明物体 3.占用大量的显存带宽
-
渲染路径设置
-
移动端优化(作业)
两个TBDR
一个是SIGGRAPH 2010上提出的,通过分块来降低带宽内存用量
一个是PowerVR基于手机GPU的TBR架构提出的,通过HSR减少overdraw
-
其他渲染路径
https://zhuanlan.zhihu.com/p/54694743
-
关于MSAA
https://docs.microsoft.com/en-us/windows/win32/direct3d9/multiple-render-targets
https://catlikecoding.com/unity/tutorials/rendering/part-13/
http://docs.nvidia.com/gameworks/index.html#gameworkslibrary/graphicssamples/d3d_samples/antialiaseddeferredrendering.htm
五、early-Z 与 Z-Prepass
-
(前置知识)深度测试Depth Test
1.深度测试位于逐片元操作中的模板测试后,透明度混合前。
2.深度测试是用来解决物体遮挡的问题:
3.深度测试的流程和逻辑:
4.深度测试带来的问题:
性能浪费:深度测试前计算过的部分片元未通过深度测试会被抛弃掉,则深度测试前的计算就是性能浪费。
5.解决方法:early-z
-
提前深度测试 early-Z
1.early-z位于光栅化阶段后,片元着色器之前。
2.early-z流程:
①先渲染1的深度值,写入深度缓冲区中,当进行2和3的渲染时,会进行early-z的判断,不通过就不会进行片元着色器及以下的计算。
②在early-z的阶段可以添加模板测试。
③提前的深度测试叫做z-cull 后续的深度测试为了确定正确的遮挡关系,叫作Z-Check。
3.early-z失效:
①开启Alpha Test 或 clip/discard 等手动丢弃片元操作
②手动修改GPU插值得到的深度
③开启Alpha Blend (Zwrite Off)
④关闭深度测试Depth Test
4.如何高效利用early-z:
①在渲染前,cpu将不透明物体从近往远排序,再给gpu渲染。(但是复杂的场景,cpu性能消耗很大;严格按照由近到远的顺序渲染,将不能同时搭配批处理优化手段。)
②pre-z
-
使用Z-Prepass
1.方式1:
①双Pass法(在第一个Pass即Z-Prepass中仅仅只写入深度,不计算输出任何颜色,在第二个Pass中关闭深度写入,并且将深度比较函数设置为相等,进行正常的透明度混合。)
②效果:每个物体都会渲染两个pass,且所有物体的z-prepass的结果就自动形成了一个最小深度值的缓冲区Z-buffer,无需cpu进行排序。
Shader "Custom/EarlyZPresentation"{
Properties{ //.......}
SubShader
{
Tags { "RenderType"="Opaque" }
//Z-Prepass部分
Pass
{
ZWrite On //开启深度写入
ColorMask 0 //关闭颜色输出
CGPROGRAM
//...这里省略了单纯地顶点变换计算部分
ENDCG
}
//正常地计算输出颜色的Pass
Pass
{
Zwrite Off //关闭深度写入
ZTest Equal //深度相等为通过
CGPROGRAM
//...这里省略了顶点变换和颜色等计算部分
ENDCG
}
}
}
③问题:
Ⅰ动态批处理(多pass shader无法进行动态批处理 ---> Draw Call问题)
ⅡDraw Call(使用z-prepass shader 的物体,draw call会多一倍)
2.方式2:
①提前分离的PrePass:仍然使用两个Pass,将原先第一个Pass(即Z-Prepass)单独分离出来为单独一个Shader,并先使用这个Shader将整个场景的Opaque的物体渲染一遍。而原先材质只剩下原先的第二个Pass,仍然关闭深度写入,并且将深度比较函数设置为相等。
②效果:用于解决DrawCall问题
③例子:使用URP下的RendererFeature
勘误(评论区):URP的SRP batch做的合批是不会减少Draw Call的,他的最大的优化在于合并set pass call,减少set pass call的开销,因为CPU上的最大开销来自于准备工作(设置工作),而非DrawCall本身(这只是要放置GPU命令缓冲区的一些字节而已),draw call是不会减少的。可以看看这篇文章翻译https://blog.csdn.net/lsjsoft/article/details/90734932
3.Z-Prepass也是透明渲染的一种解决方案:
问题:无法看到透明物体的背面
→解决方法:透明物体双面渲染(pass1渲染背面 pass2渲染正面)
*由于Unity会顺序执行Subshader中的各个Pass,所以我们可以保证背面总是在正面被渲染之前渲染,来得到正确的深度渲染关系。
-
Z-Prepass所带来的问题
1.性能消耗是否能被忽视:
结论:当一个有非常多OverDraw的场景,且不能很好的将不透明物体从前往后进行排序时,可以考虑使用PreZ进行优化。PreZ会增加DrawCall,如果用错了可能是负优化。
-
Early-Z 和 Z-prepass的实例应用
1.面片叠加的头发渲染:对于半透明的面片来说,需要从后往前进行排序渲染才能得到正确的透明度混合结果。
问题:会带来很多OverDraw的问题。
2.性能改善:使用Early-Z剔除,透明度测试开启时Early-Z无法使用的解决方案:使用一个简单的shader进行透明度测试形成 Z-Buffer。(就是我们上边说的提前分离的z-prepass)
3.改善的渲染方案:
六、纹理压缩
-
纹理压缩概述:
1.纹理压缩:为了解决内存、带宽问题,专为在计算机图形渲染系统中存储纹理而使用的图像压缩技术。
2.图片与纹理:
①图片格式:图片格式是图片文件的存储格式,通常在磁盘、内存中储存和传输文件时使用;例如:JPG、PNG、GIF、BMP等;
②纹理格式:纹理格式是显卡能够直接进行采样的纹理数据格式,通常在向显卡中加载纹理时使用;
3.纹理管线:
①纹理压缩格式基于块压缩,能够更快读取像素所属字节块进行解压缩以支持随机访问;
②图片压缩格式基于整张图片进行压缩,无法直接实现单个像素的解析;
③图片压缩格式无法被GPU识别,还需要经CPU解压缩成非压缩纹理格式才能被识别;
4.常见纹理格式:
*黄色为最常用格式、
5.非压缩格式:
①RGBA8888(RGBA32):一个像素32位,包含A通道,所以一个像素消耗:4 * 8 = 32位(bit)= 4字节(byte),即一个像素消耗4字节;
②RGBA4444(RGBA16):一个像素16位,包含A通道,一个像素消耗:4 * 4 = 16位 = 2字节,即一个像素消耗2字节;
③RGB888(RGB24):一个像素24位,无A通道,即一个像素消耗3字节;
④RGB565(RGB16):一个像素16位,无A通道,即一个像素消耗2字节;
-
DXTC
①DCTC纹理压缩格式来源于S3公司提出的S3TC算法,基本思想是把4×4的像素块压缩成一个64或128位的数据块,优点为创建了一个固定大小且独立的编码片段,没有共享查找表或其他依赖关系,简化了解码过程;
②DXT1:每一个块具有2个16位RGB颜色值(RGB565),代表了此4×4像素块中颜色极端值,然后通过线性插值计算出两个中间颜色值,16个2位索引值则表示了每一个像素的颜色值索引;
③DXT2/3:DXT2/3与DXT1类似,表示颜色信息的64位数据块不变,另外附加了64位数据来表示每个像素的Alpha信息,整个数据块变为了128位;每个像素占用8位,0-3表示透明信息,4-7表示颜色信息;
④DXT4/5:DXT4/5与DXT2/3的差异在于其Alpha信息是通过线性插值所得的,表示颜色信息的64位数据块依然不变,而Alpha信息则由2个8位Alpha极端值和16个3位索引值组成;我们可以看到在Unity内贴图类型选为法线后会采用DXTnm压缩格式,该格式会把法线贴图R通道存入A通道,然后RB通道清除为1,这样可以将法线XY信息分别存入到RGB/A中分别压缩,以获得更高的精度,然后再根据XY构建出Z通道数据;
-
ATI1/2
①ATI1:ATI公司开发的纹理压缩格式,也被称为BC4,其每个数据块存储单个颜色的数据通道,以与DXT5中的Alpha数据相同的方式进行编码,常用于存储高度图、光滑度贴图,效果与原始图像基本无差异;
②ATI2:也被称为BC5,每一个块中存储两个颜色通道的数据,同上以与DXT5中Alpha数据相同的方式进行编码,相当于存储了两个BC4块;
如果是在将法线存储在XY双通道中采用BC5格式压缩,由于每个通道都有自己的索引,因此法线贴图XY信息可以比在BC1中保留更多的保真度,缺点是需要使用两倍内存,也需要更多的带宽才能将纹理传递到着色器中;
-
BC6/7
①BC6和BC7仅在D3D11级图形硬件中受支持,他们每个块占用16字节,BC7针对8位RGB或RGBA数据,而BC6针对RGB半精度浮点数据,因此BC6是唯一一个可以原生存储HDR的BC格式;
②BC6是专门针对HDR(高动态范围)图像设计的压缩算法,压缩比为6:1;
③BC7是专门针对LDR(低动态范围)图像设计的压缩算法,压缩比为3:1,该格式用于高质量的RGBA压缩,可以显著减少由于压缩法线带来的错误效果;
④BC6和BC7的官方原理说明:
https://docs.microsoft.com/zh-cn/windows/uwp/graphics-concepts/bc6h-format
https://docs.microsoft.com/zh-cn/windows/uwp/graphics-concepts/bc7-format
-
ETC
上述我们说的DirectX选择了DXTC作为标准压缩格式,那对于OpenGL则选择了爱立信研发的ETC格式,几乎所有的安卓设备都可以支持ETC压缩,所以其在移动平台上被广泛应用;
ETC方案与DXTC具有相同特点,将4×4的像素单元压缩成64位数据块,并将像素单元水平或竖直朝向分为两个区块,每个像素颜色等于基础颜色加上索引指向的亮度范围;
①ETC1:每一个数据块分区中的4位亮度索引信息会从16个内置亮度表中获取当前像素单元对应的亮度表,每个像素的2位像素索引值可以从亮度表的四个值中选取对应的亮度补充值;
②ETC2:ETC2是ETC1的扩展,支持了Alpha通道的压缩,硬件要求OpenGL ES 3.0和OpenGL 4.3以上;
-
ASTC
ASTC是由ARM和AMD联合开发的纹理压缩格式,ASTC在各项指标上都挺不错,优点是可根据不同图片选择不同压缩率的算法,图片不需要为2的幂次,同时支持LDR和HDR,缺点是兼容性不够完善且解码时间较长;
ASTC也是基于块的压缩算法,与BC7类似,其数据块大小固定为128位,不同的是块中的像素数量可变,从4×4到12×12像素都有;
每一个数据块中存储了两个插值端点,但不一定存储的是颜色信息,也可能是Layer信息,这样可以用来对Normal或Alpha进行更好的压缩;
对于块中每一个纹素,存储其对应插值端点的权重,存储的权重数量可以少于纹素数量,可通过插值得到每一个纹素的权重值,然后再进行颜色的计算;
-
PVRTC
①PVRTC由Imagination公司专为PowerVR显卡设计,仅支持Iphone、Ipad和部分安卓机;不同于DXTC和ETC这类基于块的算法,PVRTC将图像分为了低频信号和高频信号,低频信号由两张低分辨率图像AB组成,高频信号则是低精度的调制图像,记录了每个像素混合的权重,解码时AB图像经过双线性插值放大,然后根据调制图像的权重进行混合;
②PVRTC 4-bpp把一个4×4的像素单元压成一个64位数据块,每一个块中存储一个32位的调制数据,一个1位的调制标志,15位的颜色A,1位颜色A不透明标志,14位颜色B,1位颜色B不透明标志;而PVRTC 2-bpp则是把一个8×4的像素单元压成了64位数据块;
-
总结:
七、现代移动端的TBR和TBDR渲染管线
-
当前移动端GPU概况
1.移动端和桌面端功耗对比
①桌面级主流性能平台,功耗一般为300W(R7/I7+X60级别显卡),游戏主机150-200W
②入门和旗舰游戏本平台功耗为100W
③主流笔记本为50-60W,超极本为15-25W
④旗舰平板为8-15W
⑤旗舰手机为5-8W,主流手机为3-5W
2.移动端和桌面端带宽对比
-
名词解释
1.SOC(System on Chip):Soc是把CPU、GPU、内存、通信基带、GPS模块等等整合在一起的芯片的称呼。常见有A系Soc(苹果),骁龙Soc(高通),麒麟Soc(华为),联发科Soc,猎户座Soc(三星),苹果推出的M系Soc,暂用于Mac,但这说明手机、笔记本和PC的通用芯片已经出现了。
2.物理内存:Soc中GPU和CPU共用一块片内LPDDR物理内存,就是我们常说的手机内存,也叫System Memory,大概几个G。此外CPU和GPU还分别有自己的高速SRAM的Cache缓存,也叫On-chip Memory,一般几百K~几M。不同距离的内存访问存在不同的时间消耗,距离越近消耗越低,读取System Memory的时间消耗大概是On-chip Memory的几倍到几十倍。
3.On-Chip Buffer:在TB(D)R架构下会存储Tile的颜色、深度和模板缓冲,读写修改都非常快。如果Load/Store指令中缓冲需要被Preserve,将会被写入一份到System Memory中。
4.Stall:当一个GPU核心的两次计算结果之间有依赖关系而必须串行时,等待的过程便是Stall。
5.FillRate:像素填充率 = ROP运行的时钟频率 x ROP的个数 x 每个时钟ROP可以处理的像素个数。
6.TBR(Tile-Based (Deferred) Rendering):目前主流的移动GPU渲染架构,对应一般PC上的GPU渲染架构则是IMR(Immediate Mode Rendering )。
TB(D)R简单的意思:屏幕被分块(16*16像素或者32*32像素)渲染
TBR:VS - Defer - RS - PS
TBDR:VS - Defer - RS - Defer - PS
Defer:字面是延迟,但从渲染数据的角度来看,Defer就是“阻塞+批处理”GPU的“一帧”的多个数据,然后一起处理。
-
一般PC上的GPU渲染架构:立即渲染(IMR)
1.架构流程:
①用户应用程序提交的几个数据经过顶点Shader的处理
②经过一个类似于先进先出的管道顺序提交给片段shader
③把最终结果刷到FrameBuffer里
2.伪代码理解:
①每个renderPass里面有各种各样的DrawCall,每个DrawCall里面有很多图元。
②对每个图元里面的顶点做一个顶点Shader的处理,如果这个图元没有被剔除,则对每个图元里面的片段做一个片段Shader的处理。
3.Powervr公司官网详细示意图:
https://www.imgtec.com/blog/a-look-at-the-powervr-graphics-architecture-tile-based-rendering/
①虚框:代表系统内存区域
②整个过程直接和系统内存进行交互
-
移动端GPU架构:基于块元的渲染 TB(D)R
1.简述(见上名词解释):屏幕被分块(16*16像素或者32*32像素)渲染。
2.宏观上分为两个阶段:
①执行所有与几何相关的处理,并生成Primitive List(图元列表),并且确定每个tile上面有哪些primitive。(分图元)
②逐块执行光栅化及其后续处理,并在完成后将FrameBuffer从Tile Buffer写回到System Memory中。(不是直接写回到系统内存,而是写到片上内存里,即On-chip Memory)
3.架构流程:
①用户数据经过顶点shader的处理
②分图元处理(Tile)
③经过片元Shader处理
④数据存入On-chip Memory
4.伪代码:
①第一阶段:对每个renderPass的DrawCall里的图元进行顶点Shader处理(第一阶段关键是要分每个Tile上的Primitive)。
②第二阶段:对每个tile的primitive执行片元shader处理。
5.TB(D)R的硬件渲染顺序
①第一个虚线框:On-chip Memory,片上的内存。
②第二个虚线框:System Memory,系统内存。
6.举例动图:
在实际的GPU的渲染,并不是非常严格的按照从左到右,或者从右到左的顺序。
-
TB(D)R和IMR对比
IMR少了两个环节:①几何处理和片段处理单元中间没有中间数据 ②经过片段处理后的数据没有放到On-chip Memory中而是直接刷到系统内存上。
*总结:
1.核心目的:降低带宽,减少功耗,但渲染帧率上并不比IMR快。
2.优点:
①TBR给消除Overdraw提供了机会,PowerVR用了HSR技术,Mali用了Forward Pixel Killing技术,目标一样,就是要最大限度减少被遮挡pixel的texturing和shading。
②TBR主要是 cached friendly, 在cache里头的速度要比全局内存的速度快的多,以及有可能降低render rate的代价,降低带宽,省电。
3.缺点:
①这个操作需要在vertex阶段之后,将输出的几何数据写入到DDR,然后才被fragment shader读取。这之间也就是tile写入DDR的开销和fragment shader渲染读取DDR开销的平衡。另外还有一些操作(比如tessellation)也不适用于TBR;
②如果某些三角形叠加在数个图块(Overdraw),则需要绘制数次。这意味着总渲染时间将高于即时渲染模式。
-
Binning过程(类似四叉树)/第一个Defer
1.Binning过程:确定了由哪些块元渲染哪些图元。
2.用户数据 → 分配块元去渲染。
3.样例:
来源:https://www.youtube.com/embed/SeySx0TkluE?rel=0&width=560&height=315&wmode=transparent&iframe=true&autoplay=1
4.如果Binning阶段耗时过多:几何数据过多了。
-
不同GPU的Early-Depth-Test / 第二个Defer
1.Arm Mali的FPK:
采用Forward Pixel Kill技术,在early-z之后,队列中有4个带有屏幕位置和图元深度信息的quad(平面)。
2.PVR的HSR(Hide Surface Removal,隐形面剔除):
PowerVR以及后续自研GPU,采用内置模块HSR。修改原渲染管线结构,增强rasterizer硬件模块为HSR。
实现原理:虚拟出一个射线,遇到第一个不透明的渲染模型时停止,打断后续三角形的ps处理。
3.Android的LRZ模块:
Qualcomm Adreno:采用外置模块LRZ,在正常渲染管线之前,先多执行一次vs生成低精度depth texture,来提前剔除不可见的三角形(实现细节不知),就是用硬件实现occlusion culling(遮挡剔除),功能类似软光栅中的遮挡剔除。
-
移动端Tile Based Render的优化建议
1.记得不使用Framebuffer的时候clear或者discard。
主要是清空积存在tile buffer上的 frame Data,所以在unity里面对render texture的使用也特别说明了一下,当不再使用这个rt之前,调用一次Discard。在OpenGL ES上善用glClear,gllnvalidateFrameBuffer避免不必要的Resolve(刷system memory)行为。
2.不要在一帧里面频繁的切换framebuffer的绑定。
本质上就是减少tile buffer 和system memory之间的 的stall 操作。
3.对于移动平台,建议你使用 Alpha 混合,而非 Alpha 测试。
在实际使用中,你应该分析并比较 Alpha 测试和 Alpha 混合的表现,因为这取决于具体内容,因此需要测量,通常在移动平台上应避免使用 Alpha 混合来实现透明。需要进行 Alpha 混合时,尝试缩小混合区域的覆盖范围。
4.手机上必须要做Alpha Test,先做一遍Depth prepass,参考参考目录的[Alpha Test的双pass 优化思路]
5.图片尽量压缩 例如:ASTC ETC2。
6.图片尽量走 mipmap。
7.尽量使用从Vertex Shader传来的Varying变量UV值采样贴图(连续的),不要在FragmentShader里动态计算贴图的UV值(非连续的),否则CacheMiss。
8.在延迟渲染尽量利用Tile Buffer 存储数据。
9.如果你在Unity 里面调整 ProjectSetting/Quality/Rendering/Texture Quality 不同的设置,或者不同的分辨率下,帧率有很多的变化,那么十有八九是带宽出问题啦。
10.MSAA(增加对framebuffer读取的次数)其实在TBDR上反而是非常快速的。
11.少在FS 中使用 discard 函数,调用gl_FragDepth从而打断Early-DT( HLSL中为Clip,GLSL中为discard )。
12.尽可能的在Shader里使用Half Float,如果Shader中仅有少量FP16的运算,且FP16需和FP32混合计算,则统一使用Float。
好处:(1)带宽用量减少(2)GPU中使用的周期数减少,因为着色器编译器可以优化你的代码以提高并行化程度。(3)要求的统一变量寄存器数量减少,这反过来又降低了寄存器数量溢出风险。具体有哪些数据类型适合用half或者float 或者fix,请查看参考目录的[熊大的优化建议]
13.在移动端的TB(D)R架构中,顶点处理部分,容易成为瓶颈,避免使用曲面细分shader,置换贴图等负操作,提倡使用模型LOD,本质上减少FrameData的压力。
八、command buffer及URP概述
-
URP概述
Unity URP/SRP 渲染管线浅入深出【匠】_FoxGameY的博客-CSDN博客_urp srp
-
Command Buffer概述
Command Buffer:用来存储渲染命令的缓冲区。保存着渲染命令列表,如(set render target, draw mesh等等),可以设置为在摄像机渲染期间的不同点执行。
-
为什么有Command Buffer?用来干什么?
1.例子:
https://github.com/Doppelkeks/Unity-CommandBufferRefraction
https://github.com/Arihide/unity-selective-outline
①扭曲效果:使用Command Buffer获取在渲染这图中的这三个物体之前的color buffer(背景),将其作为一张RenderTexture保存下来,当渲染图中这三个物体的时候,在片元着色器中使用切线空间的法线 X.Y对其屏幕空间的坐标做一个偏移,再用这个偏移的uv采样保存的RenderTexture。
②使用多Pass让物体在被选择的时候渲染外扩描边
*总结:command buffer实际上是用来帮助我们告诉管线下一步应该如何绘制,绘制什么。
2.从OpenGL指令分析Command Buffer原理(?)
在Opengl中,渲染过程中,出于渲染流程的需要,会频繁绑定VAO,VBO,FBO等。这样重复性的操作,很是让人感觉比较麻烦,也不利于程序的扩展。因此,在我自己写Opengl的时候,也会根据自己的需要,会将一些重复性的操作”提取”出来:使用Shader类负责编译,在Model类添加一些读取模型,绑定VAO,VBO操作函数等等。在渲染流程中常常也会需要用到上一个渲染阶段的结果作为输入,最常见的操作就是后处理阶段,需要把上一阶段的frame buffer 绑定到当前输入。
*例子:
左边是我在对BRDF的镜面高光IBL项进行预积分渲染一张LUT,对每个粗糙度和入射角组合的响应结果存储在一张2D的LUT当中,计算出菲涅项的系数值(R通道)和偏移值(G通道)计算间接光的镜面高光项时:vec3 indirectSpecular = prefilteredColor * (F * envBRDF.x + envBRDF.y),然后再正式渲染的时候就是需要引用绑定刚刚预积分渲染的Texture。在整个渲染流程,如左图所示,给每一个小球设置好MVP矩阵,然后渲染skybox,如此循环。这里看不到整个LUTTextureGeneratePass是因为我把预积分项计算移动到Loop之外,这里的Debug是在Loop之内。所以整个渲染流程是分为一个一个Pass进行编写的话,更加容易辨识在每个渲染流程中应该做什么,计算出什么结果。(?)
而为了更好扩展unity的渲染管线,unity提供了CommandBuffer,让你根据自己的需求,在不同的渲染阶段插入绘制指令,例如插入DrawRenderer,DrawMesh,DrawProcedure,绘制的时候也可以根据需要设置绘制时材质(Material)的MaterialPropertyBlock更改当前绘制的材质的属性。
这里我用了RenderFeature在BeforeRenderingOpaques的时候用DrawMesh指令渲染了一个Box。PS:MaterialPropertyBlock比直接修改Material的优势是:不会创建出新的材质实例。
3.从自定义RenderPipeLine分析:
-
Command Buffer常用函数
-
综合分析CommandBuffer的使用方法
1.URP
2.RenderFeature与CommandBuffer原理分析
3.Volume组件
-
Command Buffer注意事项
1.Scene窗口CameraColorTexture丢失(MSAA导致的bug resolveAA)
2.Depth Buffer和Stencil Buffer(16bit和24/32bit)
3.RT精度格式问题
4.加速优化
-
Unity官方文档
SRP概念相关: https://www.cnblogs.com/leixinyue/p/11038565.html
https://en.wikipedia.org/wiki/Render_Target
https://zhuanlan.zhihu.com/p/363793724
LoadAction/StoreAction RendertexuteMemeoryLess
https://answer.uwa4d.com/question/5e856425acb49302349a1119
https://zhuanlan.zhihu.com/p/262537295
https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/LoadandStoreActions.html
Instances:
https://www.xuanyusong.com/archives/4504
https://www.xuanyusong.com/archives/4640
https://www.xuanyusong.com/archives/4683
https://www.cnblogs.com/OneStargazer/p/15170931.html
Draw相关指令:
https://github.com/keijiro/Swarm
https://github.com/keijiro/GraphPlotter
Compute Shader:
https://cheneyshen.com/directcompute-tutorial-for-unity-kernels-and-thread-groups/
https://catlikecoding.com/unity/tutorials/basics/compute-shaders/
MSAA: https://www.zhihu.com/question/479915187
https://github.com/wlgys8/SRPLearn/wiki/MSAA_Implement
RT精度:
https://en.wikipedia.org/wiki/Colour_banding