「UnityShader笔记」12.Unity中的前向渲染(Forward Base)
Posted 睦月兔
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了「UnityShader笔记」12.Unity中的前向渲染(Forward Base)相关的知识,希望对你有一定的参考价值。
Part1. Unity前向渲染的介绍
1.1 前向渲染的基本原理
前向渲染的主要特点是针对每个物体,对于每个光源都会分别进行一次光照计算,最后的颜色值是由所有光源的光照结果混合而成的,比如场景中有M个物体,N个光源,则渲染整个场景需要N × M个Pass,可以看到如果光源数目多,前向渲染的开销是非常巨大的
为了解决这个开销问题,选让引擎常常会限制在每个物体上进行逐像素光照的数目,Unity引擎也是这样做的
1.2 Unity中前向渲染的实现原理
Unity的前向渲染中,实现光照有三种方式:逐像素处理、逐顶点处理、球谐函数(SH),它们的开销是依次递减的
Unity中,我们可以手动设置光照的重要度模式,有三种可选:Important, Not Important, Auto
设置为Important的光源,总是按照逐像素方式进行处理
设置为Not Important的光源,会按照逐顶点或者SH的方式处理
设置为Auto的光源,将由Unity根据光源对于物体的影响程度,对光源进行重要度排序,然后分配逐顶点、逐像素、SH方式的使用
1.3 Base Pass
前向渲染的Pass被分为两类:Base Pass 和 Additional Pass
Base Pass只会执行一次(如果定义了多个Bass Pass,也可能执行多次)
Bass Pass需要处理一个逐像素的平行光和其他所有逐顶点光源和SH光源,如果存在多个平行光,引擎会选择将最重要的平行光传递给Base Pass处理
环境光和自发光的实现也是在Base Pass中实现的,因为如果在Additional Pass中实现这些效果,由于Additional Pass是多次执行的,对于每个光源都会执行一次Additional Pass,这就会造成多个环境光和自发光的叠加,这是我们不希望看到的,而Base Pass只会执行一次,适用于处理环境光和自发光
Bass Pass是默认开启阴影的
1.4 Additional Pass
Additional Pass是多次执行的,对于每个光源都会执行一次Additional Pass
Additional Pass需要对其余所有的逐像素光源进行处理,即每个逐像素光源调用一次Additional Pass
Additional Pass默认不开启阴影,我们也可以使用#pragma multi_compile_fwdadd_fullshadows代替#pragma multi_compile_fwdadd编译指令来开启阴影
Additional Pass需要开启混合模式,因为每个逐顶点光源是依次计算的,我们不能直接覆盖上一次的计算颜色,而需要进行混合
Part2.代码实现一个典型的前向渲染Shader
Pass
// Pass for ambient light & first pixel light (directional light)
Tags "LightMode"="ForwardBase"
CGPROGRAM
// 需要添加base pass的编译指令
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
在第一个Pass:Base Pass中,我们需要手动添加Base Pass的编译指令
v2f vert(a2v v)
v2f o;
//将顶点位置从模型空间变换到裁剪空间
o.pos = UnityObjectToClipPos(v.vertex);
//将法线从模型空间变换到世界空间
o.worldNormal = UnityObjectToWorldNormal(v.normal);
//将顶点位置从模型空间变换到世界空间
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
在Base Pass的顶点着色器,我们只需要做一些简单的工作,对顶点位置和法线进行空间变换
fixed4 frag(v2f i) : SV_Target
//法向量的归一化
fixed3 worldNormal = normalize(i.worldNormal);
//调用内置变量_WorldSpaceLightPos0
//对于点光源,它可以获取当前正在处理的逐像素光源的位置
//对于平行光的_WorldSpaceLightPos0代表了平行光方向
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
//调用内置变量UNITY_LIGHTMODEL_AMBIENT,获取环境光的颜色
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//基于兰伯特模型计算漫反射项
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
//通过世界空间下相机位置和顶点位置坐差,获得观察方向
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
//计算半程向量,为Blinn-Phong模型计算高光项做准备
fixed3 halfDir = normalize(worldLightDir + viewDir);
//基于Blinn-Phong模型计算高光项
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
//手动设置衰减因子为1.0,代表不衰减
fixed atten = 1.0;
//叠加后返回颜色值
return fixed4(ambient + (diffuse + specular) * atten, 1.0);
片元着色器实现了简单的兰伯特和Blinn-Phong模型光照,需要注意的点是Unity内置变量_WorldSpaceLightPos0的使用,对于点光源,该变量获取的是光源的世界空间位置,对于平行光,该变量获取的是世界空间下的光照方向,我们假设该shader的Base Pass只需要处理平行光,于是直接将_WorldSpaceLightPos0归一化即可获得光照方向
Pass
// 设置光照模式为ForwardAdd,代表这是一个Additional Pass
Tags "LightMode"="ForwardAdd"
//设置混合模式
Blend One One
CGPROGRAM
// 需要添加Additional Pass的编译指令
#pragma multi_compile_fwdadd
在下一个Pass:Additional Pass中,我们需要手动添加Additional Pass的编译指令,并手动设置混合模式,这里设置为Blend One One,代表不考虑透明度,进行1:1混合
v2f vert(a2v v)
v2f o;
//将顶点位置从模型空间变换到裁剪空间
o.pos = UnityObjectToClipPos(v.vertex);
//将法线从模型空间变换到世界空间
o.worldNormal = UnityObjectToWorldNormal(v.normal);
//将顶点位置从模型空间变换到世界空间
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
顶点着色器中的工作和Base Pass完全一致
fixed4 frag(v2f i) : SV_Target
fixed3 worldNormal = normalize(i.worldNormal);
#ifdef USING_DIRECTIONAL_LIGHT //判断是否正在处理平行光
//平行光的_WorldSpaceLightPos0代表了平行光方向
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
//其他光源(点光源、聚光灯)的_WorldSpaceLightPos0代表了光源所处的位置在世界空间下的坐标
//光源世界坐标减去像素点世界坐标得到光照方向
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
#ifdef USING_DIRECTIONAL_LIGHT
fixed atten = 1.0;
#else
//将坐标变换到光源坐标系
float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
//使用坐标在光照材质上进行采样,得到衰减值
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#endif
return fixed4((diffuse + specular) * atten, 1.0);
在片元着色器中,我们需要处理其余所有的逐像素光照,它可能是平行光,也可能是点光源,我们希望能够对它们分别处理,所以需要用到条件判断语句#ifdef USING_DIRECTIONAL_LIGHT,它可以判断当前是否正在处理平行光
根据之前对于内置变量_WorldSpaceLightPos0的描述我们知道,对于点光源,_WorldSpaceLightPos0的含义是光源位置,所以为了获取光源方向,我们需要将光源位置和顶点位置坐差,而在平行光中则可直接通过_WorldSpaceLightPos0获取光源方向
我们还希望在光照衰减上对点光源和平行光进行分别处理,这同样需要依靠条件判断语句#ifdef USING_DIRECTIONAL_LIGHT
对于平行光,我们可以假设它不衰减,所以直接定义衰减因子为1.0
对于点光源,为了节省计算,我们不采用数学计算的方式来计算衰减因子,而是直接在一张光照材质上进行采样得到衰减因子
Part3.完整代码
Shader "Chapter 9/Forward Rendering"
Properties
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
SubShader
Tags "RenderType"="Opaque"
Pass
Tags "LightMode"="ForwardBase"
CGPROGRAM
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v
float4 vertex : POSITION;
float3 normal : NORMAL;
;
struct v2f
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
;
v2f vert(a2v v)
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
fixed4 frag(v2f i) : SV_Target
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
fixed atten = 1.0;
return fixed4(ambient + (diffuse + specular) * atten, 1.0);
ENDCG
Pass
Tags "LightMode"="ForwardAdd"
Blend One One
CGPROGRAM
#pragma multi_compile_fwdadd
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "AutoLight.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v
float4 vertex : POSITION;
float3 normal : NORMAL;
;
struct v2f
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
;
v2f vert(a2v v)
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
fixed4 frag(v2f i) : SV_Target
fixed3 worldNormal = normalize(i.worldNormal);
#ifdef USING_DIRECTIONAL_LIGHT
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
#ifdef USING_DIRECTIONAL_LIGHT
fixed atten = 1.0;
#else
float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#endif
return fixed4((diffuse + specular) * atten, 1.0);
ENDCG
FallBack "Specular"
Unity Shader入门精要学习笔记 - 第11章 让画面动起来
转自 冯乐乐的 《Unity Shader入门精要》
Unity Shader 中的内置变量
动画效果往往都是把时间添加到一些变量的计算中,以便在时间变化时画面也可以随之变化。Unity Shader 提供了一系列关于时间的内置变量来允许我们方便地在Shader中访问允许时间,实现各种动画效果。下表给出了这些内置的时间变量。
纹理动画
纹理动画在游戏中的应用非常广泛。尤其在各种资源都比较局限的移动平台上,我们往往会使用纹理动画来代替复杂的例子系统等模拟各种动画效果。
最常用的纹理动画之一就是序列帧动画。序列帧动画的原理非常简单,它像放电影一样,依次播放一系列关键帧图像,当播放速度达到一定数值时,看起来就是一个连续的动画。它的有点在于灵活性很强,我们不需要进行任何物理计算就可以得到非常细腻的动画效果。而它的缺点也很明显,由于序列帧中每张关键帧图像都不一样,因此,要制作一张出色的序列帧纹理所需要的美术工程量比较大。
想要实现序列帧动画,我们先要提供一张包含了关键帧图像的图像。如下图所示。
上图包含了8×8张关键帧图像,它们的大小相同,而且播放顺序为从左到右、从上到下、下图给出了不同时刻播放的不同动画效果。
为了再Unity实现序列帧动画,我们做如下准备工作。
1)新建一个场景,去掉天空盒子。
2)新建一个材质,新建一个Shader,并赋给材质
3)新建一个Quad,调整它的位置使其正面朝向摄像机,并把上步材质赋给它
上述序列帧动画的精髓在于,我们需要在每个时刻计算该时刻下应该播放的关键帧的位置,并对该关键帧进行纹理采样。我们修改Shader 代码。
- Shader "Unity Shaders Book/Chapter 11/Image Sequence Animation" {
- Properties {
- _Color ("Color Tint", Color) = (1, 1, 1, 1)
- //关键帧纹理
- _MainTex ("Image Sequence", 2D) = "white" {}
- //图像在水平方向上的关键帧个数
- _HorizontalAmount ("Horizontal Amount", Float) = 4
- //图像在垂直方向上的关键帧个数
- _VerticalAmount ("Vertical Amount", Float) = 4
- //控制序列帧的播放速度
- _Speed ("Speed", Range(1, 100)) = 30
- }
- SubShader {
- //由于序列帧图像通常都是透明纹理,我们需要设置Pass的相关状态,以渲染透明效果
- Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
- Pass {
- Tags { "LightMode"="ForwardBase" }
- ZWrite Off
- Blend SrcAlpha OneMinusSrcAlpha
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- #include "UnityCG.cginc"
- fixed4 _Color;
- sampler2D _MainTex;
- float4 _MainTex_ST;
- float _HorizontalAmount;
- float _VerticalAmount;
- float _Speed;
- struct a2v {
- float4 vertex : POSITION;
- float2 texcoord : TEXCOORD0;
- };
- struct v2f {
- float4 pos : SV_POSITION;
- float2 uv : TEXCOORD0;
- };
- v2f vert (a2v v) {
- v2f o;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
- o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
- return o;
- }
- fixed4 frag (v2f i) : SV_Target {
- //_Time.y 是自该场景后所经过的时间,与速度相乘来得到模拟的时间,再用floor函数取整
- float time = floor(_Time.y * _Speed);
- //获得行索引
- float row = floor(time / _HorizontalAmount);
- //获得列索引
- float column = time - row * _HorizontalAmount;
- // half2 uv = float2(i.uv.x /_HorizontalAmount, i.uv.y / _VerticalAmount);
- // uv.x += column / _HorizontalAmount;
- // uv.y -= row / _VerticalAmount;
- //进行位置偏移
- half2 uv = i.uv + half2(column, -row);
- //进行大小锁定
- uv.x /= _HorizontalAmount;
- uv.y /= _VerticalAmount;
- //进行采样
- fixed4 c = tex2D(_MainTex, uv);
- c.rgb *= _Color;
- return c;
- }
- ENDCG
- }
- }
- FallBack "Transparent/VertexLit"
- }
很多2D游戏都使用了不断滚动的背景来模拟游戏角色在场景中的穿梭,这些背景往往包含了多个层来模拟一种视觉效果。而这些背景的实现往往就是利用了纹理动画。我们将实现一个包含了两层的无限滚动的2D游戏背景。我们可以得到类似下图的效果。单击允许后,我们就可以得到一个无限滚动的背景效果。
为此,我们需要进行如下准备工作。
1)新建一个场景,去掉天空盒子,摄像机投影模式设置为正交投影。
2)新建一个材质,新建一个Shader,赋给材质
3)新建一个Quad,调整大小位置,使它充满摄像机的视野范围,然后把第2步的材质拖拽给它。
修改shader 代码
- Shader "Unity Shaders Book/Chapter 11/Scrolling Background" {
- Properties {
- //第一层(较远)背景纹理
- _MainTex ("Base Layer (RGB)", 2D) = "white" {}
- //第二层(较近)背景纹理
- _DetailTex ("2nd Layer (RGB)", 2D) = "white" {}
- //第一层滚动速度
- _ScrollX ("Base layer Scroll Speed", Float) = 1.0
- //第二层滚动速度
- _Scroll2X ("2nd layer Scroll Speed", Float) = 1.0
- //控制纹理的整体亮度
- _Multiplier ("Layer Multiplier", Float) = 1
- }
- SubShader {
- Tags { "RenderType"="Opaque" "Queue"="Geometry"}
- Pass {
- Tags { "LightMode"="ForwardBase" }
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- #include "UnityCG.cginc"
- sampler2D _MainTex;
- sampler2D _DetailTex;
- float4 _MainTex_ST;
- float4 _DetailTex_ST;
- float _ScrollX;
- float _Scroll2X;
- float _Multiplier;
- struct a2v {
- float4 vertex : POSITION;
- float4 texcoord : TEXCOORD0;
- };
- struct v2f {
- float4 pos : SV_POSITION;
- float4 uv : TEXCOORD0;
- };
- v2f vert (a2v v) {
- v2f o;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
- //计算两层背景纹理的纹理坐标
- //首先利用了TRANSFORM_TEX 来得到初始的纹理坐标
- //再利用_Time.y变量在水平方向上对纹理坐标进行偏移
- o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex) + frac(float2(_ScrollX, 0.0) * _Time.y);
- o.uv.zw = TRANSFORM_TEX(v.texcoord, _DetailTex) + frac(float2(_Scroll2X, 0.0) * _Time.y);
- return o;
- }
- 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;
- }
- ENDCG
- }
- }
- FallBack "VertexLit"
- }
顶点动画
河流的模拟是顶点动画最常见的应用之一。它的原理通常就是使用正弦函数等来模拟水流波动效果。我们将学习如何模拟一个2D的河流效果,我们可以得到类似下图的效果。
为此,我们需要进行如下准备工作。
1)新建一个场景,去掉天空盒子,摄像机投影模式设置为正交投影。
2)新建一个材质,新建一个Shader,赋给材质
3)在场景中创建多个Water模型,调整它们的位置、大小和方向,把上步的材质赋给它
修改shader代码:
- Shader "Unity Shaders Book/Chapter 11/Water" {
- Properties {
- //河流纹理
- _MainTex ("Main Tex", 2D) = "white" {}
- //控制整体颜色
- _Color ("Color Tint", Color) = (1, 1, 1, 1)
- //控制水流波动的幅度
- _Magnitude ("Distortion Magnitude", Float) = 1
- //控制波动频率
- _Frequency ("Distortion Frequency", Float) = 1
- //用于控制波长的倒数
- _InvWaveLength ("Distortion Inverse Wave Length", Float) = 10
- //河流纹理的移动速度
- _Speed ("Speed", Float) = 0.5
- }
- SubShader {
- // 禁用批处理,因为批处理会合并所有相关的模型,而这些模型各自的模型空间就会丢失
- Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}
- Pass {
- Tags { "LightMode"="ForwardBase" }
- //关闭深度写入,开启并设置了混合模式,并关闭了剔除功能。这是为了让水流的每个面都能显示
- ZWrite Off
- Blend SrcAlpha OneMinusSrcAlpha
- Cull Off
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- #include "UnityCG.cginc"
- sampler2D _MainTex;
- float4 _MainTex_ST;
- fixed4 _Color;
- float _Magnitude;
- float _Frequency;
- float _InvWaveLength;
- float _Speed;
- struct a2v {
- float4 vertex : POSITION;
- float4 texcoord : TEXCOORD0;
- };
- struct v2f {
- float4 pos : SV_POSITION;
- float2 uv : TEXCOORD0;
- };
- v2f vert(a2v v) {
- v2f o;
- float4 offset;
- offset.yzw = float3(0.0, 0.0, 0.0);
- //只在水平方向上偏移,利用_Frequency 和 内置的_Time.y 来控制正弦函数的频率
- //为了让不同的位置具有不同的位移,我们对上述结果加上了模型空间下的位置分量,并乘以_InvWaveLength 来控制波长
- //最后乘以_Magnitude 来控制波动幅度,得到最终的位移。
- offset.x = sin(_Frequency * _Time.y + v.vertex.x * _InvWaveLength + v.vertex.y * _InvWaveLength + v.vertex.z * _InvWaveLength) * _Magnitude;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex + offset);
- o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
- o.uv += float2(0.0, _Time.y * _Speed);
- return o;
- }
- fixed4 frag(v2f i) : SV_Target {
- //这边只进行纹理采样再添加颜色控制即可
- fixed4 c = tex2D(_MainTex, i.uv);
- c.rgb *= _Color.rgb;
- return c;
- }
- ENDCG
- }
- }
- FallBack "Transparent/VertexLit"
- }
另一种常见的顶点动画就是广告牌技术。广告牌技术会根据视角方向来旋转一个被纹理着色的多边形(通常就是简单的四边形,这个多边形就是广告牌),是的多边形看起来好像总是面对着摄像机。广告牌技术被用于很多应该,比如渲染烟雾、运毒、闪光效果等。
广告牌技术的本质就是构建旋转矩阵,而我们知道一个变换矩阵需要3个基向量。广告牌技术使用的基向量通常就是表面法线(normal)、指向上的方向(up)以及指向右的方向(right)。除此之外,我们还需要指定一个锚点。这个锚点在旋转的过程中是固定不变的,以此来确定多边形在空间中的位置。
广告牌技术的难点在于,如何根据需要来构建3个相互正交的基向量。计算过程通常是,我们首先会通过初始计算得到目标的表面法线(例如就是视角方向)和指向上的方向,而两者往往是不垂直的。但是,两者其中之一是固定的,例如当模拟草丛时,我们希望广告牌的法线方向是固定的,即总是指向视角方向,指向上的方向则可以发生变换。我们假设法线方向是固定的,首先,我们根据初始的表面法线和指向上的方向来计算出目标方向的指向右的方向(通过叉积操作):
right = up × normal
对其归一化后,再由法线方向和指向右的方向计算出正交的指向上的方向即可:
up‘ = normal × right
至此,我们就可以得到用于旋转的3个正交基了。下图给出了上述计算过程的图示。如果指向上的方向是固定的,计算过程也是类似的。
下面,我们将在Unity中实现上面提到的广告牌技术。我们可以得到类似下图中的效果。
为此,我们需要做如下准备工作。
1)新建一个场景,去掉天空盒子
2)新建一个材质,新建一个Shader,赋给材质
3)在场景中创建多个Quad,调整位置和大小,把上步材质赋给它们。
更改Shader代码。
- Shader "Unity Shaders Book/Chapter 11/Billboard" {
- Properties {
- //广告牌显示的透明纹理
- _MainTex ("Main Tex", 2D) = "white" {}
- //控制整体颜色
- _Color ("Color Tint", Color) = (1, 1, 1, 1)
- //调整是固定法线还是固定指向上的方向
- _VerticalBillboarding ("Vertical Restraints", Range(0, 1)) = 1
- }
- SubShader {
- // Need to disable batching because of the vertex animation
- Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}
- Pass {
- Tags { "LightMode"="ForwardBase" }
- //这里关闭了深度写入,开启并设置了混合模式,并关闭了剔除功能。这是为了让广告牌的每个面都能显示
- ZWrite Off
- Blend SrcAlpha OneMinusSrcAlpha
- Cull Off
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- #include "Lighting.cginc"
- sampler2D _MainTex;
- float4 _MainTex_ST;
- fixed4 _Color;
- fixed _VerticalBillboarding;
- struct a2v {
- float4 vertex : POSITION;
- float4 texcoord : TEXCOORD0;
- };
- struct v2f {
- float4 pos : SV_POSITION;
- float2 uv : TEXCOORD0;
- };
- v2f vert (a2v v) {
- v2f o;
- // 选择模型空间的原点作为广告牌的锚点,并利用内置变量获取模型空间下的视角位置
- float3 center = float3(0, 0, 0);
- float3 viewer = mul(_World2Object,float4(_WorldSpaceCameraPos, 1));
- //计算3个正交矢量。首先,我们根据观察位置和锚点计算目标法线方向,
- //并根据_VerticalBillboarding 属性来控制垂直方向上的约束度。
- float3 normalDir = viewer - center;
- // 如果 _VerticalBillboarding 等于 1, 意味着法线方向固定为视角方向
- // 如果 _VerticalBillboarding 等于 0, 意味着向上方向固定为(0,1,0)
- normalDir.y =normalDir.y * _VerticalBillboarding;
- //归一化操作
- normalDir = normalize(normalDir);
- //我们得到了粗略的向上方向。为了防止法线方向和向上方向平行
- //我们对法线方向的y分量进行判断,以得到合适的向上方向。然后,根据法线方向
- //和粗略的向上方向得到向右方向,并对结果进行归一化。但由于此时向上的方向还是不
- //准确的,我们又根据准确的法线方向和向右方向得到最后的向上方向
- float3 upDir = abs(normalDir.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);
- float3 rightDir = normalize(cross(upDir, normalDir));
- upDir = normalize(cross(normalDir, rightDir));
- //我们根据原始的位置相对于锚点的偏移量以及3个正交基矢量,以计算得到新的顶点位置。
- float3 centerOffs = v.vertex.xyz - center;
- float3 localPos = center + rightDir * centerOffs.x + upDir * centerOffs.y + normalDir * centerOffs.z;
- //最后,把模型空间的顶点位置变换到裁剪空间中
- o.pos = mul(UNITY_MATRIX_MVP, float4(localPos, 1));
- o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
- return o;
- }
- fixed4 frag (v2f i) : SV_Target {
- fixed4 c = tex2D (_MainTex, i.uv);
- c.rgb *= _Color.rgb;
- return c;
- }
- ENDCG
- }
- }
- FallBack "Transparent/VertexLit"
- }
需要说明的是,在上面的例子中,我们使用的是Unity自带的Quad来作为广告牌,而不能使用自带的Plane。这是因为,我们的代码是建立在一个竖直摆放的多边形的基础上的,也就是说,这个多边形的顶点结构需要满足在模型空间下是竖直排列的。只有这样,我们才能使用v.vertex来计算到正确的相对于中心的位置偏移量。
顶点动画虽然非常灵活有效,但有些注意事项需要注意。
首先,在之前看到的那样,如果我们在模型空间下进行了一些顶点动画,那么批处理往往就会破坏这种动画效果。这时,我们可以通过SubShader的DisableBatching标签来强制取消对该Unity Shader的批处理。然而,取消批处理会带来一定的性能下降,增加了Draw Call,因此我们应该尽量避免使用模型空间下的一些绝对位置和方向来进行计算。在广告牌的例子中,为了避免显示使用模型空间的中心来作为锚点,我们可以利用顶点颜色来存储每个顶点到锚点的距离值,这种做法在商业游戏中很常见。
其次,如果我们想要对包含了顶点动画的物体添加阴影,那么如果像之前那样使用内置的Diffuse等包含的阴影Pass来渲染,就得不到正确的阴影效果(这里指的是无法向其他物体正确地投射阴影)。这是因为,我们讲过Unity 的阴影绘制需要调用一个ShadowCaster Pass,而如果直接使用这些内置的ShadowCasterPass,这个Pass中并没有进行相关的顶点动画,因此Unity 自定义的ShadowCaster Pass,而这个Pass中,我们将进行统一的顶点变换过程。需要注意的是,在前面的实现中,如果涉及半透明物体我们都把Fallback设置成了Transparent/VertexLit ,而Transparent/VertexLit没有定义ShadowCaster Pass,因此也就不会产生阴影。
在之前的场景中,我们给出了计算顶点动画的阴影的一个例子。在这个例子中,我们使用了之前的大部分代码,模拟一个波动的水流。同时,我们开启了场景中平行光的阴影效果,并添加了一个平面来接收来自“水流”的阴影。我们还把这个Unity Shader 的Fallback 设置为内置的VertexLit,这样Unity将根据Fallback最终找到VertexLit 中的ShadowCaster Pass 来渲染阴影。下图给出了这样的结果。
可以看出,此时虽然Water模型发生了形变,但它的阴影并没有产生相应的动画效果。为了正确绘制变形对象的阴影,我们就需要提供自定义的ShadowCaster Pass。我们新建一个Shader来实现,效果如下图。
在这个Shader中,我们提供了一个ShadowCaster Pass,相关代码如下:
- Shader "Unity Shaders Book/Chapter 11/Billboard" {
- Properties {
- //广告牌显示的透明纹理
- _MainTex ("Main Tex", 2D) = "white" {}
- //控制整体颜色
- _Color ("Color Tint", Color) = (1, 1, 1, 1)
- //调整是固定法线还是固定指向上的方向
- _VerticalBillboarding ("Vertical Restraints", Range(0, 1)) = 1
- }
- SubShader {
- // Need to disable batching because of the vertex animation
- Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}
- Pass {
- Tags { "LightMode"="ForwardBase" }
- //这里关闭了深度写入,开启并设置了混合模式,并关闭了剔除功能。这是为了让广告牌的每个面都能显示
- ZWrite Off
- Blend SrcAlpha OneMinusSrcAlpha
- Cull Off
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- #include "Lighting.cginc"
- sampler2D _MainTex;
- float4 _MainTex_ST;
- fixed4 _Color;
- fixed _VerticalBillboarding;
- struct a2v {
- float4 vertex : POSITION;
- float4 texcoord : TEXCOORD0;
- };
- struct v2f {
- float4 pos : SV_POSITION;
- float2 uv : TEXCOORD0;
- };
- v2f vert (a2v v) {
- v2f o;
- // 选择模型空间的原点作为广告牌的锚点,并利用内置变量获取模型空间下的视角位置
- float3 center = float3(0, 0, 0);
- float3 viewer = mul(_World2Object,float4(_WorldSpaceCameraPos, 1));
- //计算3个正交矢量。首先,我们根据观察位置和锚点计算目标法线方向,
- //并根据_VerticalBillboarding 属性来控制垂直方向上的约束度。
- float3 normalDir = viewer - center;
- // 如果 _VerticalBillboarding 等于 1, 意味着法线方向固定为视角方向
- // 如果 _VerticalBillboarding 等于 0, 意味着向上方向固定为(0,1,0)
- normalDir.y =normalDir.y * _VerticalBillboarding;
- //归一化操作
- normalDir = normalize(normalDir);
- //我们得到了粗略的向上方向。为了防止法线方向和向上方向平行
- //我们对法线方向的y分量进行判断,以得到合适的向上方向。然后,根据法线方向
- //和粗略的向上方向得到向右方向,并对结果进行归一化。但由于此时向上的方向还是不
- //准确的,我们又根据准确的法线方向和向右方向得到最后的向上方向
- float3 upDir = abs(normalDir.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);
- float3 rightDir = normalize(cross(upDir, normalDir));
- upDir = normalize(cross(normalDir, rightDir));
- //我们根据原始的位置相对于锚点的偏移量以及3个正交基矢量,以计算得到新的顶点位置。
- float3 centerOffs = v.vertex.xyz - center;
- float3 localPos = center + rightDir * centerOffs.x + upDir * centerOffs.y + normalDir * centerOffs.z;
- //最后,把模型空间的顶点位置变换到裁剪空间中
- o.pos = mul(UNITY_MATRIX_MVP, float4(localPos, 1));
- o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
- return o;
- }
- fixed4 frag (v2f i) : SV_Target {
- fixed4 c = tex2D (_MainTex, i.uv);
- c.rgb *= _Color.rgb;
- return c;
- }
- ENDCG
- }
- }
- FallBack "Transparent/VertexLit"
- }
- Pass{
- Tags{"LightMode"="ShadowCaster"}
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- #pragma multi_compile_shadowcaster
- #include "UnityCG.cginc"
- float _Magnitude;
- float _Frequency;
- float _InvaWaveLength;
- float _Speed;
- struct a2v{
- float4 vertex : POSITION;
- float4 texcoord : TEXCOORD0;
- };
- struct v2f{
- V2F_SHADOW_CASTER;
- };
- v2f vert(a2v i){
- v2f o;
- float4 offset;
- offset.yzw = float3(0.0,0.0,0.0);
- //计算偏移
- offset.x = sin(_Frequency*_Time.y+v.vertex.x*_InvaWaveLength+
- v.vertex.y*_InvaWaveLength+v.vertex.z*_InvaWaveLength)*_Magnitude;
- //加上偏移
- v.vertex = v.vertex + offset;
- TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
- return o;
- }
- fixed4 frag(v2f i) : SV_Target{
- //使用SHADOW_CASTER_FRAGMENT来让Unity自动完成阴影投射的部分,把结果输出到深度图和阴影映射纹理中
- SHADOW_CASTER_FRAGMENT(i)
- }
- ENDCG
- }
以上是关于「UnityShader笔记」12.Unity中的前向渲染(Forward Base)的主要内容,如果未能解决你的问题,请参考以下文章
Unity Shader入门精要学习笔记 - 第11章 让画面动起来
Unity Shader入门精要学习笔记 - 第9章 更复杂的光照