概述
本篇是“练习项目”系列的第二篇,主要介绍一下利用消融实现的效果。在游戏开发的过程中,有很多看起来很神奇的效果,都是使用消融的原理实现的。
原理
主要的原理,就是使用噪声图和透明度测试,根据噪声图中采样的值,对某些像素进行剔除。
1、基本原理实现
这里会实现一个最基础的项目,来简单了解消融的基本原理。主要的代码如下:
float cutout = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, input.uv.zw).r;
AlphaDiscard(cutout, _Threshold);
注意,这里要使用AlphaDiscard方法的话,必须设置正确的KeyWord。
2、边缘颜色
在上面的动图中可以看到,只是简单实现了消融,但看起来效果不太好,比较单调。下面将使用几种方式来丰富效果。
2.1 纯颜色
第一种实现方式比较简单,只是在未消融的边界留下一段缓冲,显示边界的颜色。这里需要开放两个属性接口:_EdgeLength、_EdgeColor。即边缘长度和边缘颜色。先根据噪声图进行透明度剔除,然后根据透明度确定一段范围内显示边界的颜色。代码如下:
float cutout = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, input.uv.zw).r;
AlphaDiscard(cutout, _Threshold);
if (cutout - _Threshold < _EdgeLength)
return _EdgeColor;
2.2 两种颜色混合
一种颜色的效果看起来还是有点单调,使用两种颜色混合的效果可能会更好。根据_EdgeLength可以确定一个“边界”范围。“边界”与剔除区域的交界可以使用第一种颜色,“边界”与正常区域的交界可以使用第二种颜色,而在“边界”内部,则可以在第一种颜色和第二种颜色之间进行插值。代码如下:
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 边界颜色混合物体颜色
从上面的动图可以看到,在“边界”区域只是边界的颜色,看起来有点不自然。下一步,就是对边界颜色和物体颜色进行混合,从而看起来更加地自然。主要代码如下:
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对渐变纹理进行采样,来得到边界的颜色。得到边界颜色后,与物体本身的颜色混合的过程,就与上面相同了。主要代码如下:
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、从特定点开始消融
为了从特定点开始消融,必须将片元到特定点的距离考虑进来。
第一步,定义消融开始点的属性,该属性是在世界空间中定义的。在顶点着色器中将该点转换到物体的本地空间,然后将顶点的本地坐标和该点的本地空间坐标传递给片元着色器。在片元着色器中求出片元到该点的距离。代码如下:
//Properties
_StartPoint("Start Point",Vector) = (1,1,1,1) //需要找到该点的世界坐标
//Vert
output.objPos = input.positionOS.xyz;
output.objStartPos = TransformWorldToObject(_StartPoint.xyz);
//Frag
float distance = length(input.objPos - input.objStartPos);
第二步,求出网格内任意两点之间的最大距离,用来对上面求出的距离进行归一化处理。这一步需要在C#中实现,思路是遍历任意两点,然后求出最大距离。这里求出的是网格内任意两点之间的最大距离,所以上面定义的消融开始点最好在网格上面,这样效果才是对的。代码如下:
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属性来存放最大距离值。
_MaxDistance("Max Distance",Float) = 0
第三步就是归一化距离值。
float normalizedDistance = saturate(distance / _MaxDistance);
第四步是定义_DistanceEffect属性,来控制距离对整个消融效果的影响程度。
//Properties
_DistanceEffect("Distance Effect",Range(0,1)) = 0.5
//Frag
float cutout = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, input.uv.zw).r * (1.0 - _DistanceEffect) + normalizedDistance * _DistanceEffect;
AlphaDiscard(cutout, _Threshold);
4、应用:场景切换
利用上面的从特定点开始消融的原理,我们可以用来实现场景切换的效果。
如下图所示,就是我们要实现的效果。
因为我们在上面实现的是从特点点开始消融,而上图是从从外部向特定点开始消融,所以这里要做一些修改。
float normalizedDistance = 1.0 - saturate(distance / _MaxDistance);
这样,就会从四周向中心点开始消融了。
然后,我们的距离是在局部空间计算的。但是这里有很多物体,再使用局部空间的话,就不太方便,所以,这里转到世界空间计算。
//Vert
output.worldPos = TransformObjectToWorld(input.positionOS.xyz);
//Frag
float distance = length(input.worldPos - _StartPoint.xyz);
接下来,需要获得场景所有物体的顶点到消融点的最大距离,用来对上面的距离做归一化处理,这一步,需要在C#中处理,代码在这里。
这样,场景中建一个空物体Environment,然后给Environment添加上面的C#脚本,再把其它物体都放到Environment下面即可。
5、从特定方向开始消融
理解了上面的从特定点开始消融,那么这里的从特定方向开始消融就很好理解了。
这里实行的是从X方向消融。
第一步,求出X方向的边界,传递给Shader。
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方向上与边界的距离。再进行归一化处理。
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、灰烬飞散效果
第一步,灰烬向特定方向飞散。这一步可以在顶点着色器中通过顶点动画实现。
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;
第二步,从特定方向开始消融。上面已经介绍了。这里注意,因为要生成灰烬的效果,所以要延迟透明度剔除的时机。
float edgeCutout = cutout - _Threshold;
clip(edgeCutout + _AshWidth);
这样,可以在消融边缘留下大片的颜色。而我们需要的是细碎的灰烬,所以需要再次使用噪声图对这片颜色区域进行消融处理。
if(degree < 0.001)
{
clip(whiteNoise * _AshDensity + normalizedDistance * _DistanceEffect - _Threshold);
finalColor = _AshColor;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZSKmByXl-1614487076825)(https://cdn.jsdelivr.net/gh/bzyzhang/ImgHosting//img/2020-11-28/20201128210450.gif)]
7、镜头遮挡消融
这里要实现的,是当角色和镜头之间有障碍物时,对障碍物进行消融处理。
第一步,将角色的坐标传递给Shader,这一步是在C#中实现的。
public class SendPlayerPos : MonoBehaviour {
public Transform player;
public Material blockMat;
void Update () {
blockMat.SetVector ("_PlayerPos", player.position);
}
}
第二步,使用屏幕空间的遮罩纹理,对消融区域进行控制。对角色和镜头之间的片元,进行剔除处理。
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);
参考
- [1] Unity Shader - 消融效果原理与变体
- [2] 《Unity Shader入门精要》
- [3] Unity技术分享 |《Trifox》中的遮挡处理和溶解着色器(上)
- [4] A Burning Paper Shader
- [5] Tutorial - Burning Edges Dissolve Shader in Unity