白纸一张

三十而立,四十而不惑

0%

练习项目(二):消融效果

概述

本篇是“练习项目”系列的第二篇,主要介绍一下利用消融实现的效果。在游戏开发的过程中,有很多看起来很神奇的效果,都是使用消融的原理实现的。

原理

主要的原理,就是使用噪声图和透明度测试,根据噪声图中采样的值,对某些像素进行剔除。

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
//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#中实现,思路是遍历任意两点,然后求出最大距离。这里求出的是网格内任意两点之间的最大距离,所以上面定义的消融开始点最好在网格上面,这样效果才是对的。代码如下:

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
//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、应用:场景切换

利用上面的从特定点开始消融的原理,我们可以用来实现场景切换的效果。

如下图所示,就是我们要实现的效果。

因为我们在上面实现的是从特点点开始消融,而上图是从从外部向特定点开始消融,所以这里要做一些修改。

1
float normalizedDistance = 1.0 - saturate(distance / _MaxDistance);

这样,就会从四周向中心点开始消融了。

然后,我们的距离是在局部空间计算的。但是这里有很多物体,再使用局部空间的话,就不太方便,所以,这里转到世界空间计算。

1
2
3
4
//Vert
output.worldPos = TransformObjectToWorld(input.positionOS.xyz);
//Frag
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);

代码

参考

-------------本文结束感谢您的阅读-------------