Unity Shader学习随笔
阴影:
光源看不到,但相机看得到的地方,就是阴影
变体:
一个普通的Shader可能会有很多种效果
例如一个火焰溶解效果,写在Shader里,但其实在未触发之前我们不需要去计算该效果
因此需要在未触发前,将火焰溶解的效果计算关闭
这就用到了变体,把火焰溶解的效果计算变成变体
无论如何都会被编译的变体multi_compile
#pragma multi_compile _ _NAME
变体名必须要全大写
通过材质使用情况来决定是否进行编译的变体shader_feature
一般用于特效,即运行过程中不会去开启该变体
属性中就是要暴露一个开关:
[Toggle]_MaskEnabled("启用遮罩", int) = 0
pass中要启用变体:
#pragma shader_feature _MASKENABLED_ON
然后就是和上面的判断一样了
#if
xxx;
#else
xxx;
#endif
这就可以做到:
同一个Shader可以给不同的材质球使用;
是否开启某个变体由材质球决定,并且在打包时不会将该材质球未开启的变体打进包里。
然后信息存在于这里
常用函数:
渲染队列Queue
2500以下会被认为是不透明物体,从前往后渲染,效率更高
2500以上就是透明物体,从后往前渲染,效率低,但这是为了保证显示效果正确
Queue=Background,1000,最先渲染
Queue=Geometry,2000,默认场景中的渲染对象
Queue=AlphaTest,2450,要么不透要么全透,常常用来实现美术部分的透贴
Queue=Transparent,3000,场景中的半透明对象
Queue=Overlay,4000,叠加效果,最后渲染的东西放这,如镜头光晕等
混合Blend
Blend后面一般跟随两至三个参数
如
Blend One Zero
Blend One One
第一个参数就是针对片段着色器pass计算出来的SrcFactor源颜色,要对该参数干什么
第二个参数就是针对存在于帧缓冲区的DstFactor目标颜色,要对该参数干什么
其实中间还有一个,不填就是默认加法
Blend SrcAlpha OneMinusSrcAlpha - 传统透明度
Blend One OneMinusSrcAlpha - 自左乘透明度
Blend One One
Blend OneMinusDstColor One - 软合并
Blend DstColor Zero - 乘法
Blend DstColor SrcColor - 双倍乘法
面剔除Cull
剔除不需要的面,优化渲染速度
Cull Front - 剔除正面
Cull Back - 剔除背面
正面背面是指模型的正面背面(顶点环绕方向,及法线),不是玩家看到的是正面
Cull Off - 关闭剔除
屏幕后处理
实现热扭曲的效果
1.做好场景
2.由代码抓取当前一帧的内容
3.获取受扭曲影响的部分屏幕坐标
4.利用屏幕坐标对抓取的图片采样
5.再采样扰动贴图做扭曲
使用unity参数组成屏幕坐标
查看代码
Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct v2f { float2 uv : TEXCOORD0; }; sampler2D _GrabTex; sampler2D _MainTex;float4 _MainTex_ST; fixed _SpeedX, _SpeedY, _Distort; v2f vert (float4 vertex : POSITION, float2 uv : TEXCOORD0, out float4 pos : SV_POSITION) { pos = UnityObjectToClipPos(vertex); v2f o; o.uv = TRANSFORM_TEX(uv, _MainTex) + float2(_SpeedX, _SpeedY) * _Time.x; return o; } //由于VPOS和SV_POSITION冲突,因此需要在vert里out fixed4 frag (v2f i, UNITY_VPOS_TYPE screenPos:VPOS) : SV_Target { //最初的版本,通过unity参数去计算,_ScreenParams.xy是屏幕的宽高 fixed2 uv = lerp(screenPos.xy / _ScreenParams.xy, tex2D(_MainTex, i.uv).xy, _Distort); fixed4 grabTex = tex2D(_GrabTex, uv); return grabTex; } ENDCG }
使用正交空间下的pos计算屏幕坐标
查看代码
Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct a2f { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 pos : SV_POSITION; float2 screenUV : TEXCOORD1; }; sampler2D _GrabTex; sampler2D _MainTex;float4 _MainTex_ST; fixed _SpeedX, _SpeedY, _Distort; v2f vert (a2f v) { v2f o; o.uv = TRANSFORM_TEX(v.uv, _MainTex) + float2(_SpeedX, _SpeedY) * _Time.x; o.pos = UnityObjectToClipPos(v.vertex); //1.此时pos是放在正交矩形中的顶点 //2.因此可以直接拿该值当做屏幕坐标 //3.但要注意pos的范围是-1到1 //4.因此需要把它规范到0到1,但OpenGL和DX11的屏幕坐标原点不同 //5.OpenGL的在左下角,仅需将xy * 0.5 + 0.5即可 //6.但DX11的在左上角,需要将y轴反转,即y = 1 - y // o.screenUV = o.pos.xy / o.pos.w * 0.5 + 0.5; // o.screenUV.y = 1 - o.screenUV.y; //7.上面就是根据正交中的顶点坐标转换成屏幕坐标的操作 //8.可以注意到稍微有点扭曲,原因是顶点不是片元,顶点和顶点之间的参数都是靠插值算出来的 // o.screenUV = ComputeScreenPos(o.pos) / o.pos.w; //9.当然,unitycg提供了方法,上面 //10.这里除以一个w是因为:正交矩形中是正交的,而相机是透视的,要将其还原 return o; } fixed4 frag (v2f i) : SV_Target { fixed2 uv = lerp(i.screenUV, tex2D(_MainTex, i.uv).xy, _Distort); fixed4 grabTex = tex2D(_GrabTex, uv); return grabTex; } ENDCG }
使用片元计算后的pos计算屏幕坐标
查看代码
Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct a2f { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 pos : SV_POSITION; float2 screenUV : TEXCOORD1; }; sampler2D _GrabTex; sampler2D _MainTex;float4 _MainTex_ST; fixed _SpeedX, _SpeedY, _Distort; v2f vert (a2f v) { v2f o; o.uv = TRANSFORM_TEX(v.uv, _MainTex) + float2(_SpeedX, _SpeedY) * _Time.x; o.pos = UnityObjectToClipPos(v.vertex); o.screenUV = o.pos; return o; } fixed4 frag (v2f i) : SV_Target { // return i.screenUV.x; // return i.pos.x; //可以看到,我明明对screenUV赋了pos的值,但上面两个居然不一样 //那是因为SV_POSITION在中间经过了改变 //在经过计算后,pos就是屏幕像素大小了,不再是-1到1了,那这样就更方便了; i.screenUV = i.pos.xy / _ScreenParams.xy; //OK,就上面这一行就解决战斗了 fixed2 uv = lerp(i.screenUV, tex2D(_MainTex, i.uv).xy, _Distort); fixed4 grabTex = tex2D(_GrabTex, uv); return grabTex; } ENDCG }
[PerRendererData]
属性的特性
Properties { _MainTex ("Texture", 2D) = "white" {} [PerRendererData]_Color ("Color", Color) = (0,0,0,0) }
该材质cs被调用时,会被打包进cs 的MaterialPropertyBlock
每帧修改一万个对象的颜色,
方法1,使用材质的方法去修改颜色,开销是16ms每帧
方法2,使用GetPropertyBlock(MaterialPropertyBlock prop)的方法修改颜色,开销是7ms每帧
Color
模板测试
模板缓冲区
自己的话
是屏幕大小的区域,例如1920*1080的大小
每个像素是8位
当某一个物体被渲染到屏幕上时,它看到带有材质,也就是肯定带有shader
而shader中可以定义,当前我被渲染出来的区域的模板值
因此,当另一个物体被渲染时,另一个shader上面的也会有模板值
两个模板值比较,新的模板值大会怎么样,小会怎么样,可以自己定义
因此就可以做出类似PS遮罩的效果
官方的话
模板缓冲区可以为屏幕上的每个像素点保存一个无符号的整数值
这个值的意义视程序的具体操作而定
在渲染过程中
可以用这个值与一个预先设定的参考值相比较
根据比较的结果来决定是否更新相应的像素点的颜色值,这个比较的过程被称为模板测
代码部分
公式
将StencilBuffer的值与ReadMask进行与运算,然后与Ref值进行Comp比较,结果为True时进行Pass操作,否则进行Fail操作
操作值写入StencilBuffer前先与WriteMask进行与运算
(Ref & ReadMask)Comp(StencilBuffer & ReadMask)? Pass : Fail
Comp
Less <
Greater >
Lequal <=
Gequal >=
Equal =
NotEqual !=
Always 总为true
Never 总为false
实践
UI组件加了Mask之后
会有这个,这是由UGUI自己去写的
Id就是公式中的StencilBuffer ,已经被写到深度缓冲区中了
我们写的新shader就是要和这个Id:1去比
Properties { [PerRendererData]_MainTex ("Texture", 2D) = "white" {} _Color ("Color", Color) = (0,0,0,0) _Ref("Stencil Ref", int) = 0 [Enum(UnityEngine.Rendering.CompareFunction)]_Comp("Stencil Comp", int) = 0 [Enum(UnityEngine.Rendering.StencilOp)]_OP("Stencil OP", int) = 0 }
Stencil//深度测试 { Ref [_Ref] //ReadMask [0~255] //WriteMask [0~255] Comp [_Comp] Pass [_OP] }
这就是Enum
此时_Ref = 1
这是Mask
这是效果
渲染方式
ForwardBase
可以实现一个逐像素的平行光
以及剩下所有的逐顶点和球谐SH光
ForwardAdd
用于剩下所有的逐像素光
Lambert光照模型
Diffuse = Ambient + Kd * LightColor * dot(N, L);
Diffuse:最终物体上的漫反射强度
Ambient:环境光强度,为简化计算,用常数表示
Kd:物体材质对光的反射系数
LightColor:光源颜色
N:normal
L:顶点指向光源的单位向量
dot = cos(N,L)
Phong光照模型
Specular = SpecularColor * Ks * pow( max(0, dot(R, V)), Shininess );
Specular:最终物体上的高光反射
SpecularColor:高光的颜色
Ks:反射系数
R:反射单位向量
V:顶点到观察点的单位距离
Shininess:高光指数,用于模拟高光范围
dot(R,V):
R就是反射光的方向
V就是眼睛观察改点的方向
因此两者方向越靠近,dot越大,就越亮
R的推演:
深度写入:
ZWrite On|Off
深度写入,默认为On
On:向深度缓冲中写入深度值
深度测试:
ZTest
深度测试,拿当前像素的深度值与深度缓冲中的深度值进行比较,默认为LEqual
可通过在属性中添加枚举:UnityEngine.Rendering.CompareFunction
Less:小于,表示如果当前像素的深度值小于深度缓冲中的深度值,则通过
Greater:大于
LEqual:小于等于
GEqual:大于等于
Equal:等于
NotEqual:不等于
Nerver:永不通过
Always:永远通过
当两个面重叠时,总会出现两者材质的交错现象
解决办法之一,就是移动其中某个面
当如果必须保持两个面重叠,但还需要某个面在上面时
可以用Offset
可以简单理解为,两个负数就是往相机移,正数就是远离相机
Unity雾效
就是Fog,pass的重点是#pragma multi_compile_fog、#if defined(FOG_LINEAR) || defined(FOG_LINEAR) || defined(FOG_LINEAR)、和雾效计算
查看代码
Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fog #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; float fogFactor : TEXCOORD1; }; sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); float z = length(_WorldSpaceCameraPos - mul(unity_ObjectToWorld, v.vertex)); #if defined(FOG_LINEAR) o.fogFactor = z * unity_FogParams.z + unity_FogParams.w; #elif defined(FOG_EXP) o.fogFactor = exp2(-unity_FogParams.y * z); #elif defined(FOG_EXP2) float value = unity_FogParams.x * z; o.fogFactor = exp2(-value * value); #endif return o; } fixed4 frag (v2f i) : SV_Target { fixed4 c = 1; #if defined(FOG_LINEAR) || defined(FOG_LINEAR) || defined(FOG_LINEAR) c = lerp(unity_FogColor,c,i.fogFactor); #endif return c; } ENDCG }
然后还有两种内置的:
平移、缩放、旋转矩阵
查看代码
//平移矩阵 float4x4 T = float4x4( 1, 0, 0, _Translate.x, 0, 1, 0, _Translate.y, 0, 0, 1, _Translate.z, 0, 0, 0, _Translate.w); v.positionOS = mul(T, v.positionOS); //缩放矩阵 float4x4 S = float4x4( _Scale.x*_Scale.w, 0, 0, 0, 0, _Scale.y*_Scale.w, 0, 0, 0, 0, _Scale.z*_Scale.w, 0, 0, 0, 0, 1); v.positionOS = mul(S, v.positionOS); //旋转矩阵 _Rotate.xyz = _Rotate.xyz * 3.1415926 / 180; float4x4 R1 = float4x4( 1,0,0,0, 0,cos(_Rotate.x),sin(_Rotate.x),0, 0,-sin(_Rotate.x),cos(_Rotate.x),0, 0,0,0,1 ); float4x4 R2 = float4x4( cos(_Rotate.y), 0, sin(_Rotate.y), 0, 0, 1, 0, 0, -sin(_Rotate.y), 0, cos(_Rotate.y), 0, 0, 0, 0, 1 ); float4x4 R3 = float4x4( cos(_Rotate.z), sin(_Rotate.z), 0, 0, -sin(_Rotate.z), cos(_Rotate.z), 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ); v.positionOS = mul(R1, v.positionOS); v.positionOS = mul(R3, v.positionOS); v.positionOS = mul(R2, v.positionOS);
世界空间->观察空间
我们需要先由于无法读取到摄像机和物体的全部坐标和旋转,故使用参数模拟
1.我们构建WorldSpace下的View坐标V_world的三个轴
ViewX.x, ViewY.x, ViewZ.x, 0,
ViewX.y, ViewY.y, ViewZ.y, 0,
ViewX.z, ViewY.z, ViewZ.z, 0,
0, 0, 0, 1
);
1, 0, 0, -_ViewPosition.x,
0, 1, 0, -_ViewPosition.y,
0, 0, 1, -_ViewPosition.z,
0, 0, 0, 1
);
float4x4 M_view = mul(M_viewRot, M_viewTran);
最后的计算
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了