|
《Unity Shader入门精要》源代码
10.1 Unity 内置的时间变量
Image
11.2 纹理动画
纹理动画在游戏中的应用非常广泛,尤其是各种资源都斗劲局限的移动平台,我们往往会使用纹理动画来代替开销巨大的粒子系统模拟的各种动画效果。
11.2.1 序列帧动画
序列帧动画即播放一系列的关键帧图像,不需要进行任何物理计算就可以得到精美的效果,但是代价是美术工程量巨大。
序列帧图像凡是是透明纹理,所以需要设置它的队列。
由于天空盒的衬着在透明物体之后,而对于透明度混合的物体来说,需要封锁深度写入。所以在衬着天空盒时,如果透明物体后没有不透明物体,那该物体对应的 z-buffer 为空,Unity 就会将天空盒的颜色直接写入颜色缓冲,那么在 摄像机或是Game视图 透明物体就不会显示,所以不雅察看透明物体的效果时,需要封锁天空盒 或是在透明物体后加 plane 等不透明物体。
下面这张图就非常明显,左边是透明度混合的立方体,没有不透明物体的处所直接不显示,右边是 透明度测试的立方体就没问题(因为开启了深度写入)
Image
场景搭建:在场景中创建一个四边形(quad),调整它的正面朝向摄像机(背面会被裁剪掉),并为其创建对应的材质和 shader。
Shader:注意不要忘了 Pass 中属性的声明 以及 顶点着色器的输入输出布局体的定义(1)声明材质的各种属性- Properties
- {
- _Color (”Color Tint”, Color) = (1, 1, 1, 1)
- _MainTex (”Texture”, 2D) = ”white” {}
- _HorizontalAmount (”Horizontal Amount”, Float) = 8
- _VerticalAmount (”Vertical”, Float) = 8
- _Speed (”Speed”, Range(1, 100)) = 30
- }
复制代码 此中 _MainTex 用于存储序列帧纹理,_HorizontalAmount 和 _VerticalAmount 分袂对应水安然安祥竖直标的目的上序列帧个数,例如一个 5 * 6 的序列帧纹理,上述变量的值分袂为 5 和 6 。而 _Speed 用于控制播放速度。
(2)由于序列帧动画凡是是透明纹理,所以需要为其设置 Pass 的相关状态。- SubShader
- {
- Tags { ”Quene” = ”Transparent” ”IgnoreProjector” = ”True” ”RenderType” = ”Transparent”}
- Pass
- {
- Tags {”LightMode” = ”ForwardBase”}
- ZWrite Off
- Blend SrcAlpha OneMinusSrcAlpha
复制代码 (4)顶点着色器:- v2f vert(appdata v)
- {
- v2f o;
- o.vertex = UnityObjectToClipPos(v.vertex);
- o.uv = TRANSFORM_TEX(v.uv, _MainTex);
- return o;
- }
复制代码 (5)片元着色器- fixed4 frag(v2f i) : SV_Target
- {
- float time = floor(_Time.y * _Speed);
- float row = floor(time / _HorizontalAmount);
- float col = time - row * _HorizontalAmount;
- half2 uv = i.uv + half2(col, -row);
- uv.x /= _HorizontalAmount;
- uv.y /= _VerticalAmount;
- fixed4 c = tex2D(_MainTex, uv);
- c *= _Color;
- return c;
- }
复制代码 通过 _Time.y * _Speed 控制图像切换速度,并求出改时间下该当播放的图像地址的行和列。然后把某一帧图像所对应的纹理的 x,y映射的到 [0,1],就拿该代码中的 8*8 的序列帧纹理为例,我们计算出来的第一个图像的 uv 范围是 [0, 1] ,而在的实际 uv 的范围是 [0, 0.125],需要将[0, 1] 映射到 [0, 0.125], 需要处于 8也就是_VerticalAmount、_HorizontalAmount。 而需要注意 Unity 纹理,是从下到上的,而序列帧是从上到下的,所以进行偏移时 v 标的目的需要减去 row。(如果将图像的长和宽当作单元长度的话,那最右上角的坐标就是(_VerticalAmount,_HorizontalAmount)-本例中是(8,8),那只需要将改坐标系下的坐标除以整个纹理的长和宽就可以映射到 [0, 1],而对应的某个小纹理也会被映射到它实际的位置)。
(5) 设置 Fallback- Fallback ”Transparent/VertexLit”
复制代码 11.2.2 滚动布景
远近两层布景,仅距离布景滚动快,远距离布景滚动慢。
场景搭建:第一步封锁天空盒,然后将摄像机的模式设置为 正交投影,创建一个四边形(quad),并将其铺满正交区域,注意需正面朝向摄像机。
创建对应的材质和 Shader,并将其赋给 四边形。
Shader:
(1)声明属性:- Properties
- {
- _MainTex(”Base Layer (RGB)”, 2D) = ”white” {}
- _DetailTex(”2nd Layer (RGB)”, 2D) = ”white” {}
- //远平面布景滚动速度
- _ScrollX(”Base Tex Scroll Speed”, Float) = 1.0
- //近平面布景滚动速度
- _Scroll2X(”2nd Tex Scroll Speed”, Float) = 1.0
- //控制图像亮度
- _Multiplier(”Layer Multiplier”, Float) = 1.0
- }
复制代码 (2)顶点着色器:复制代码 想让图像在程度标的目的滚动,例如向左滚动,就要让同一个顶点在随着时间的递增,其对应的纹理坐标不竭向左移动,也就是 o.uv + (offset, 0)。此中 frac 函数的感化是 给一个向量,返回对应该向量每个分量的小数部门。frac 函数介绍。
(3)片元着色器:- fixed4 frag(v2f i) : SV_Target
- {
- fixed4 firstLayer = tex2D(_MainTex, i.uv.xy);
- fixed4 secondLayer = tex2D(_DetailTex, i.uv.zw);
- fixed4 c = lerp(firstLayer, secondLayer, secondLayer.a);
- c.rgb *= _Multiplier;
- return c;
- }
复制代码 用 secondLayer 也就是近平面的 alpha 分量来作为 lerp 的第三个参数,是因为想让近平面覆盖远平面。
(4)设置 Fallback,也可以封锁 Fallback11.3 顶点动画
11.3.1 流动的河流
用正弦函数对模型的顶点进行上下偏移,来模拟河流的波动。用时间程度标的目的上的纹理采样,来模拟河流的流动。
Image
场景搭建:
我们从首先打消天空盒,然后从 Prefabs 文件夹中拖出 Water 模型,默认情况下它的标的目的可能有些问题,可能会背面朝向我们,就不会显示,可以将其向上的轴取相反数。(我是吧 Y: -90 改成了 90)。然后再拖出两个,并按如图放置。然后创建 3 个材质 和 一个 Shader。
这是我们随机点一个 water,如下图,我们发现改模型 x 轴来到了竖直标的目的,z 来到了 程度标的目的(红绿蓝轴 依次对应 xyz 轴),所以写代码时,需要用对 x 分量进行偏移,模拟波动,z 标的目的用时间控制纹理偏移,来实现流动效果。
Image
Shader:
(1)声明一些属性- Properties
- {
- _Color(”Color Tint”, Color) = (1, 1, 1, 1)
- _MainTex(”Texture”, 2D) = ”white” {}
- //控制水流波动的幅度,0.1 似乎斗劲合适
- _Magnitude(”Distortion Magnitude”, Float) = 1
- //控制水流波动频率
- _Frequency(”Distortion Frequency”, Float) = 1
- //控制波长的倒数
- _InvWaveLength(”Distortion Inverse Wave Length”, Float) = 1
- _Speed(”Speed”, Float) = 1
- }
复制代码 (2)为透明效果设置合适的 SubShader 标签:- SubShader
- {
- Tags { ”RenderType” = ”Transparent” ”IngoreProjector” = ”True” ”Quene” = ”Transparent” ”DisableBatching”=”True”}
复制代码 在上面的设置中,我们除了为透明效果设置Qucue、IgnoreProjector 和 RendeaType外,还设量了一个新的标签——DisableBatching。我们在3.3.3 节中介绍过该标签的含义:一些 SubShader 在使用Unity 的批措置功能时会呈现问题,这时可以通过该标签来直接指明是否对该 SubShader 使用批措置。而这些需要特殊措置的 Shader 凡是就是指包含了模型空间的顶点动画的Shadr. 这是因为批措置会合并所有相关的模型,而这些模型各自的模型空间就会丢掉。而在本例中,我们需要在物体的模型空间下对顶点位置进行偏移。因此,在这里需要打消对该 Shader的批措置操作。
(3)设置 Pass 的衬着状态- Pass
- {
- Tags {”LightMode” = ”ForwardBase”}
- ZWrite Off
- Blend SrcAlpha OneMinusSrcAlpha
- Cull off
复制代码 (4)顶点着色器- v2f vert(appdata v)
- {
- v2f o;
- float4 offset;
- offset.yzw = float3(0.0, 0.0, 0.0);
- offset.x = sin(_Frequency * _Time.y + dot(v.vertex, float3(_InvWaveLength, _InvWaveLength, _InvWaveLength))) * _Magnitude;
- o.vertex = UnityObjectToClipPos(v.vertex + offset);
- o.uv = TRANSFORM_TEX(v.uv, _MainTex);
- o.uv += float2(0, _Time.y * _Speed);
- return o;
- }
复制代码 用 _Frequency 属性和内置的 _Time.y 来控制正弦函数的频率。同时为了让分歧位置具有分歧的位移,我们对上述成果加上了模型空间下的位置分量,并乘以 _InWaveLength 来控制波长。最后乘上 _Magnitude 控制振幅。最后用 _Time.y 和 _speed 来控制程度标的目的上的纹理动画。
(5)片元着色器:对纹理采样,并添加颜色控制即可:- fixed4 frag(v2f i) : SV_Target
- {
- fixed4 c = tex2D(_MainTex, i.uv);
- c.rgb *= _Color.rgb;
- return c;
- }
复制代码 (6)设置 Fallback- Fallback ”Transparent/VertexLit”
复制代码 11.3.2 广告牌
**广告牌技术(Billboarding)**会按照视角标的目的来衬着一个被纹理着色的多边形(凡是是四边形),这使得多边形仿佛总是面对着摄像机。广告牌技术用应用于衬着烟雾、云朵、闪光效果 的插片上。
该技术本质就是构建旋转矩阵,矩阵需要三个基向量:概况法线(normal)、指向上的标的目的(up),指向右的标的目的(right),除此之外还需指定一个锚点(anchor location) ,可以用模型的正中心(模型空间的坐标(0, 0, 0))。
难点也在于构建旋转矩阵,一般我们会通过初始计算得出概况法线(例如视角标的目的,因为我们但愿模型始终面朝我们)和指向上的标的目的,这两者往往不是垂直的。但是这两者此中之一是固定的,例如模拟草丛时,我们但愿其指向上的标的目的永远是 (0, 1, 0),而法线随视角变化;而模拟粒子效果时(例如烟雾的插片),我们但愿法线始终是视线标的目的,而指向上的标的目的是可以改变的。
我们假设法线固定,首先可以按照法线和想向上的标的目的的叉积求出向右的标的目的(可以将right、up、normal 当成 x,y,z 轴):$$right=up × normal$$归一化后,再有法线标的目的与向右标的目的计算出正交的向上的标的目的:$$up' = normal × right$$
Image
搭建场景:先去掉场景中的天空盒,然后创建 材质和 Shader,然后再场景中,创建多个四边形(并调整他们的位置和大小),并将创建的材质赋给它们。
Image
Shader:
(1)声明属性:- Properties
- {
- _Color(”Color Tint”, Color) = (1, 1, 1, 1)
- _MainTex(”Texture”, 2D) = ”white” {}
- //调整时固定法线还是固定指向上标的目的,即约束垂直标的目的的程度
- _VerticalBillboarding(”Vertical Restraints”, Range(0, 1)) = 1
- }
复制代码 此中通过将 _VerticalBillboarding 与 法线的 y 分量相乘,如果 _VerticalBillboarding 为 0, 那乘完后法线的 y 分量为0, 如果向上的标的目的要与其垂直,那么也需要指向上方,从而达到控制固定指向上标的目的的目的。
(2)为透明效果设置合适的标签:- SubShader
- {
- //由于是顶点动画所以需要解绑定
- Tags { ”RenderType” = ”Transparent” ”IngoreProjector” = ”True” ”Quene” = ”Transparent” ”DisableBatching” = ”True”}
复制代码 (3)设置 Pass 的衬着状态:- Pass
- {
- Tags{”LightMode” = ”ForwardBase”}
- ZWrite Off
- Blend SrcAlpha OneMinusSrcAlpha
- Cull Off
复制代码 封锁剔除功能是为了让每一面都能显示。
(4)顶点着色器是我们的核心,所有计算都是在模型空间下计算,我们选择模型空间的原点,即模型的中心做描点:- v2f vert(appdata v)
- {
- v2f o;
- float3 center = float3(0, 0, 0);
- float3 viewer = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1));
- fixed3 normalDir = viewer - center;
- normalDir.y *= _VerticalBillboarding;
- normalDir = normalize(normalDir);
- //为了防止法线与向上的标的目的平行,当法线的y分量接近 1 时,
- //我们将向上的标的目的设置为 (0,0,1), 否则叉积会得到错误的成果,
- float3 upDir = abs(normalDir.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);
- fixed3 rightDir = normalize(cross(upDir, normalDir));
- upDir = normalize(cross(normalDir, rightDir));
- float3 centerOffset = v.vertex.xyz - center;
- //这一步向相当于左乘旋转矩阵,矩阵的第 n 列乘向量的第 n 行,并相乘。
- float3 localPos = center + centerOffset.x * rightDir + centerOffset.y * upDir + centerOffset.z * normalDir;
- o.vertex = UnityObjectToClipPos(float4(localPos, 1));
- o.uv = TRANSFORM_TEX(v.uv, _MainTex);
- return o;
- }
复制代码 (5)片元着色器:- fixed4 frag(v2f i) : SV_Target
- {
- // sample the texture
- fixed4 c = tex2D(_MainTex, i.uv);
- c.rgb *= _Color.rgb;
- return c;
- }
复制代码 (6)设置 Fallback- Fallback ”Transparent/VertexLit”
复制代码 其他需要注意的是,我们使用的是 Unity 自带的四边形(Quad),而不是平面(plane),这是因为我们的代码是成立在一个竖直摆放的多边形的基础上的,也就是说这个多边形的顶点布局必需满足在模型空间下是竖直摆列的。只有这样才能计算出正确的相对于锚点的偏移量。
由于是透明的物体,所以 Fallback 用的是 Transparent/VertexLit ,但是它无法投射暗影,如果想要其投射暗影,需要将其换成 VertexLit,但是该中放发投射的暗影不会发生相应的动画效果:
Image
我们可以写一个专门用于它的 ShadowCaster Pass,来投射对应动画顶点暗影, 来代替 Fallback- Pass
- {
- Tags{ ”LightMode”=”ShadowCaster” }
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- #pragma multi_compile_shadowcaster
- #include ”UnityCG.cginc”
- float _Magnitude;
- float _Frequency;
- float _InvWaveLength;
- float _Speed;
- struct a2v
- {
- float4 vertex : POSITION;
- float4 normal : NORMAL;
- };
- struct v2f
- {
- V2F_SHADOW_CASTER;
- };
- v2f vert(a2v v)
- {
- v2f o;
- float4 offset;
- offset.yzw = float3(0, 0, 0);
- offset.x = sin(_Frequency * _Time.y + dot(v.vertex.xyz, float3(_InvWaveLength, _InvWaveLength, _InvWaveLength))) * _Magnitude;
- v.vertex = v.vertex + offset;
- TRANSFER_SHADOW_CASTER_NORMALOFFSET(o);
- return o;
- }
- fixed4 frag(v2f i) : SV_Target
- {
- SHADOW_CASTER_FRAGMENT(i)
- }
- ENDCG
- }
复制代码
Image
我们凡是使用 Unity 提供的内置宏 V2F_SHADOW_CASTER、TRANSFORM_SHADOW_CASTER_NORMALOFFSET、SHADOW_CASTER_FRAGMENT 来计算暗影投射所需各种变量,而我们可以只关心自定义的部门。在上述代码中我们首先在 v2f 布局体中操作 V2F_SHADOW_CASTER,来定义暗影投射所需的各种变量,然后计算顶点的偏移,并加到顶点位置变量中,然后使用 TRANSFORM_SHADOW_CASTER_NORMALOFFSET 让 Unity 帮我们完成剩下的事情。在片元着色器顶用 SHADOW_CASTER_FRAGMENT 让 Unity 帮我们完成残剩暗影投射的部门,把成果输出到深度图和暗影纹理中。
TRANSFORM_SHADOW_CASTER_NORMALOFFSET 会使用名称为 v 作为输入布局体,v 中需要包含顶点位置 v.vertex 和 顶点法线 v.normal 信息。本文使用 Zhihu On VSCode 创作并发布 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|