概述
本篇是“练习项目”系列的第二篇,主要介绍一下利用消融实现的效果。在游戏开发的过程中,有很多看起来很神奇的效果,都是使用消融的原理实现的。
原理
主要的原理,就是使用噪声图和透明度测试,根据噪声图中采样的值,对某些像素进行剔除。
1、基本原理实现
这里会实现一个最基础的项目,来简单了解消融的基本原理。主要的代码如下:
1 2 float cutout = SAMPLE_TEXTURE2D(_NoiseTex , sampler_NoiseTex , input .uv .zw ) .r;AlphaDiscard(cutout , _Threshold ) ;
注意,这里要使用AlphaDiscard方法的话,必须设置正确的KeyWord。
代码
2、边缘颜色
在上面的动图中可以看到,只是简单实现了消融,但看起来效果不太好,比较单调。下面将使用几种方式来丰富效果。
2.1 纯颜色
第一种实现方式比较简单,只是在未消融的边界留下一段缓冲,显示边界的颜色。这里需要开放两个属性接口:_EdgeLength、_EdgeColor。即边缘长度和边缘颜色。先根据噪声图进行透明度剔除,然后根据透明度确定一段范围内显示边界的颜色。代码如下:
1 2 3 4 5 float cutout = SAMPLE_TEXTURE2D(_NoiseTex , sampler_NoiseTex , input .uv .zw ) .r;AlphaDiscard(cutout , _Threshold ) ; if (cutout - _Threshold < _EdgeLength) return _EdgeColor;
代码
2.2 两种颜色混合
一种颜色的效果看起来还是有点单调,使用两种颜色混合的效果可能会更好。根据_EdgeLength可以确定一个“边界”范围。“边界”与剔除区域的交界可以使用第一种颜色,“边界”与正常区域的交界可以使用第二种颜色,而在“边界”内部,则可以在第一种颜色和第二种颜色之间进行插值。代码如下:
1 2 3 4 5 6 7 8 float cutout = SAMPLE_TEXTURE2D(_NoiseTex , sampler_NoiseTex , input .uv .zw ) .r;AlphaDiscard(cutout , _Threshold ) ; if (cutout - _Threshold < _EdgeLength){ float degree = (cutout - _Threshold) / _EdgeLength; return lerp(_EdgeFirstColor, _EdgeSecondColor, degree); }
代码
2.3 边界颜色混合物体颜色
从上面的动图可以看到,在“边界”区域只是边界的颜色,看起来有点不自然。下一步,就是对边界颜色和物体颜色进行混合,从而看起来更加地自然。主要代码如下:
1 2 3 4 5 6 7 8 9 float cutout = SAMPLE_TEXTURE2D(_NoiseTex , sampler_NoiseTex , input .uv .zw ) .r;AlphaDiscard(cutout , _Threshold ) ; float degree = saturate((cutout - _Threshold) / _EdgeLength);half4 edgeColor = lerp(_EdgeFirstColor, _EdgeSecondColor, degree); half4 col = SAMPLE_TEXTURE2D(_MainTex , sampler_MainTex , input .uv .xy ) ; half4 finalColor = lerp(edgeColor, col, degree);
在上面的代码中,degree的范围是[0,1],剔除区域是0,正常区域是1,而在“边界”区域,则在[0,1]之间。使用degree进行第一次插值,得到边界颜色,第二次插值,则混合了边界颜色和物体本身的颜色。
代码
2.4 使用渐变纹理
为了让“边界”颜色更加丰富,可以使用渐变纹理。
然后就可以使用degree对渐变纹理进行采样,来得到边界的颜色。得到边界颜色后,与物体本身的颜色混合的过程,就与上面相同了。主要代码如下:
1 2 3 4 5 6 7 8 9 float cutout = SAMPLE_TEXTURE2D(_NoiseTex , sampler_NoiseTex , input .uv .zw ) .r;AlphaDiscard(cutout , _Threshold ) ; float degree = saturate((cutout - _Threshold) / _EdgeLength);half4 edgeColor = SAMPLE_TEXTURE2D(_RampTex , sampler_RampTex , float2 (degree , degree ) ); half4 col = SAMPLE_TEXTURE2D(_MainTex , sampler_MainTex , input .uv .xy ) ; half4 finalColor = lerp(edgeColor, col, degree);
代码
3、从特定点开始消融
为了从特定点开始消融,必须将片元到特定点的距离考虑进来。
第一步,定义消融开始点的属性,该属性是在世界空间中定义的。在顶点着色器中将该点转换到物体的本地空间,然后将顶点的本地坐标和该点的本地空间坐标传递给片元着色器。在片元着色器中求出片元到该点的距离。代码如下:
1 2 3 4 5 6 7 8 9 _StartPoint("Start Point" ,Vector) = (1 ,1 ,1 ,1 ) output.objPos = input.positionOS.xyz; output.objStartPos = TransformWorldToObject(_StartPoint .xyz ) ; float distance = length(input.objPos - input.objStartPos);
第二步,求出网格内任意两点之间的最大距离,用来对上面求出的距离进行归一化处理。这一步需要在C#中实现,思路是遍历任意两点,然后求出最大距离。这里求出的是网格内任意两点之间的最大距离,所以上面定义的消融开始点最好在网格上面,这样效果才是对的。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Dissolve : MonoBehaviour { void Start () { Material mat = GetComponent<MeshRenderer> ().material; mat.SetFloat ("_MaxDistance" , CalculateMaxDistance ()); } float CalculateMaxDistance () { float maxDistance = 0 ; Vector3[] vertices = GetComponent<MeshFilter> ().mesh.vertices; for (int i = 0 ; i < vertices.Length; i++) { Vector3 v1 = vertices[i]; for (int k = 0 ; k < vertices.Length; k++) { if (i == k) continue ; Vector3 v2 = vertices[k]; float mag = (v1 - v2).magnitude; if (maxDistance < mag) maxDistance = mag; } } return maxDistance; } }
同时,也要定义_MaxDistance属性来存放最大距离值。
1 _MaxDistance ("Max Distance" ,Float) = 0
第三步就是归一化距离值。
1 float normalizedDistance = saturate(distance / _MaxDistance);
第四步是定义_DistanceEffect属性,来控制距离对整个消融效果的影响程度。
1 2 3 4 5 6 _DistanceEffect("Distance Effect" ,Range(0,1) ) = 0.5 float cutout = SAMPLE_TEXTURE2D(_NoiseTex , sampler_NoiseTex , input .uv .zw ) .r * (1.0 - _DistanceEffect) + normalizedDistance * _DistanceEffect;AlphaDiscard(cutout , _Threshold ) ;
代码
4、应用:场景切换
利用上面的从特定点开始消融的原理,我们可以用来实现场景切换的效果。
如下图所示,就是我们要实现的效果。
因为我们在上面实现的是从特点点开始消融,而上图是从从外部向特定点开始消融,所以这里要做一些修改。
1 float normalizedDistance = 1 .0 - saturate(distance / _MaxDistance);
这样,就会从四周向中心点开始消融了。
然后,我们的距离是在局部空间计算的。但是这里有很多物体,再使用局部空间的话,就不太方便,所以,这里转到世界空间计算。
1 2 3 4 output.worldPos = TransformObjectToWorld(input .positionOS .xyz ) ; float distance = length(input.worldPos - _StartPoint . xyz);
接下来,需要获得场景所有物体的顶点到消融点的最大距离,用来对上面的距离做归一化处理,这一步,需要在C#中处理,代码在这里 。
这样,场景中建一个空物体Environment,然后给Environment添加上面的C#脚本,再把其它物体都放到Environment下面即可。
代码
5、从特定方向开始消融
理解了上面的从特定点开始消融,那么这里的从特定方向开始消融就很好理解了。
这里实行的是从X方向消融。
第一步,求出X方向的边界,传递给Shader。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class DissolveDirection : MonoBehaviour { void Start () { Material mat = GetComponent<Renderer>() .material; float minX, maxX; CalculateMinMaxX(out minX , out maxX ) ; mat.SetFloat("_MinBorderX" , minX ) ; mat.SetFloat("_MaxBorderX" , maxX ) ; } void CalculateMinMaxX(out float minX , out float maxX ) { Vector3[] vertices = GetComponent<MeshFilter>() .mesh.vertices; minX = maxX = vertices[0 ] .x; for (int i = 1 ; i < vertices.Length; i++) { float x = vertices[i ] .x; if (x < minX) minX = x; if (x > maxX) maxX = x; } } }
第二步,定义从X的正方向还是负方向开始消融,确定边界,然后求出各个片元在X方向上与边界的距离。再进行归一化处理。
1 2 3 4 5 6 7 float range = _MaxBorderX - _MinBorderX;float border = _MinBorderX;if (_Direction == 1 ) //1 表示从X正方向开始,其他值则从负方向 border = _MaxBorderX; float distance = abs (input.objPosX - border );float normalizedDistance = saturate(distance / range );
代码
6、灰烬飞散效果
第一步,灰烬向特定方向飞散。这一步可以在顶点着色器中通过顶点动画实现。
1 2 3 4 5 float cutout = GetNormalizeDistance (output.positionWS.y);float3 localFlyDirection = TransformWorldToObjectDir (_FlyDirection.xyz); float flyDegree = (_Threshold- cutout) / _EdgeLength;float val = saturate(flyDegree * _FlyIntensity);input.positionOS.xyz += localFlyDirection * val ;
第二步,从特定方向开始消融。上面已经介绍了。这里注意,因为要生成灰烬的效果,所以要延迟透明度剔除的时机。
1 2 float edgeCutout = cutout - _Threshold;clip (edgeCutout + _AshWidth);
这样,可以在消融边缘留下大片的颜色。而我们需要的是细碎的灰烬,所以需要再次使用噪声图对这片颜色区域进行消融处理。
1 2 3 4 5 if (degree < 0.001 ){ clip(whiteNoise * _AshDensity + normalizedDistance * _DistanceEffect - _Threshold ); finalColor = _AshColor ; }
代码
7、镜头遮挡消融
这里要实现的,是当角色和镜头之间有障碍物时,对障碍物进行消融处理。
第一步,将角色的坐标传递给Shader,这一步是在C#中实现的。
1 2 3 4 5 6 7 8 public class SendPlayerPos : MonoBehaviour { public Transform player; public Material blockMat; void Update () { blockMat.SetVector ("_PlayerPos" , player.position); } }
第二步,使用屏幕空间的遮罩纹理,对消融区域进行控制。对角色和镜头之间的片元,进行剔除处理。
1 2 3 4 5 6 7 8 9 10 float toCamera = distance(input.positionWS, _WorldSpaceCameraPos);float playerToCamera = distance(_PlayerPos . xyz, _WorldSpaceCameraPos); float2 wcoord = input.positionNDC.xy / input.positionNDC.w; float mask = SAMPLE_TEXTURE2D(_ScreenSpaceMaskTex , sampler_ScreenSpaceMaskTex , wcoord ) .r;half4 col = SAMPLE_TEXTURE2D(_MainTex , sampler_MainTex , input .uv ) ; float gradient = SAMPLE_TEXTURE2D(_NoiseTex , sampler_NoiseTex , input .uv ) .r; if (toCamera < playerToCamera) clip(gradient - mask + (toCamera - _WorkDistance) / _WorkDistance);
代码
参考