Unity教程|手把手教你实现“美女与野兽”特效(下)
本文将继续分享波兰的游戏开发者Bogumił Mazurek的“美女与野兽”的视觉特效教程。
在上篇中,我们学习了项目设置、角色和动画设置、自定义着色器制作。本篇中,我们将学习编写MonoBehaviour脚本用于控制效果、使用Visual Effects Graph制作VFX、为粒子加入光线。
温馨提示:
本教程有大量截图和动画,并提供相应的注释。由于微信端自动压缩图片的问题,会导致部分图片设置模糊。我们将提供原始清晰版图片下载。
本教程提供带有附带演示场景的完整项目下载。运行时,请点击空格键让美女化身为野兽。
请发送“美女与野兽” 到微信后台,获得资源下载地址。
第四步:使用脚本控制效果
我们将编写一个简单的脚本来控制效果,该脚本会创建包含每个角色当前顶点位置的缓冲区。粒子系统会使用该缓冲区,实现特别的效果。
首先,我们创建一个新的C#脚本,命名为Summoner.cs。然后添加成员变量,最重要的变量是和缓冲区相关的变量。
我们将使用名称为pointCache的Texture2D对象,存储模型顶点的信息,其中的size参数决定角色边界框在比例锁定情况下的大小。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Summoner : MonoBehaviour
{
public GameObject Beauty;
public GameObject Beast;
public GameObject VFX;
private Texture2D pointCache;
private float size;
private bool summonning;
void Start()
{
}
void Update()
{
}
}
Summoner.cs脚本代码设置
为了确定边界框的大小,我们会使用下面的函数。
void UpdateSize(GameObject character)
{
SkinnedMeshRenderer[] renderers = character.GetComponentsInChildren<SkinnedMeshRenderer>();
Bounds bound = new Bounds();
foreach (SkinnedMeshRenderer renderer in renderers)
{
Mesh baked = new Mesh();
renderer.BakeMesh(baked);
bound.Encapsulate(baked.bounds);
}
this.size = Mathf.Max(bound.extents.x * 2, bound.extents.y * 2, bound.extents.z * 2);
}
Summoner脚本的UpdateSize方法
我们把归一化的顶点位置存储在pointCache Texure2D对象中,因此需要把世界空间顶点坐标转换为边界框空间的归一化坐标。
为此,我们可以使用下面的函数。
void UpdateCachePoint(GameObject character)
{
Mesh baked;
Vector3[] vertices;
Transform parent;
SkinnedMeshRenderer[] renderers = character.GetComponentsInChildren<SkinnedMeshRenderer>();
List<Color> normalizedVertices = new List<Color>();
foreach (SkinnedMeshRenderer renderer in renderers)
{
parent = renderer.gameObject.transform.parent;
baked = new Mesh();
renderer.BakeMesh(baked);
vertices = baked.vertices;
for (int i = 0; i < vertices.Length; i++)
{
vertices[i] = (character.gameObject.transform.InverseTransformPoint(renderer.gameObject.transform.TransformPoint(vertices[i])) + new Vector3(size * 0.5f, 0, size * 0.5f)) / size;
normalizedVertices.Add(new Color(vertices[i].x, vertices[i].y, vertices[i].z));
}
}
if (this.pointCache == null || this.pointCache.width != normalizedVertices.Count)
{
this.pointCache = new Texture2D(1, normalizedVertices.Count, TextureFormat.RGBA32, false, true);
this.pointCache.filterMode = FilterMode.Point;
}
this.pointCache.SetPixels(normalizedVertices.ToArray());
this.pointCache.Apply();
}
Summoner脚本的UpdateCachePoint方法
我们的着色器可以裁剪或遮罩部分模型的部分,从而实现简单的角色交换过程。
此外,每帧将更新pointCache缓冲区,让我们之后创建的粒子系统使用该缓冲区。在进行交换时,为了避免影响项目其它部分的执行过程,我们会使用C#协程。
private IEnumerator Summon()
{
this.summonning = true;
float minClippingLevel = 0;
float maxClippingLevel = 2;
float clippingLevel = maxClippingLevel;
this.Beauty.SetActive(true);
while (clippingLevel > minClippingLevel)
{
this.UpdateSize(this.Beauty);
this.UpdateCachePoint(this.Beauty);
clippingLevel -= Mathf.Abs(maxClippingLevel - minClippingLevel) / 2 * Time.deltaTime;
SkinnedMeshRenderer[] renderers = this.Beauty.GetComponentsInChildren<SkinnedMeshRenderer>();
foreach (SkinnedMeshRenderer renderer in renderers)
{
foreach (Material material in renderer.materials)
{
material.SetFloat("_ClippingLevel", clippingLevel);
}
}
yield return 0;
}
this.Beauty.SetActive(false);
yield return new WaitForSeconds(1);
minClippingLevel = 0;
maxClippingLevel = 3;
this.Beast.SetActive(true);
while (clippingLevel < maxClippingLevel)
{
this.UpdateSize(this.Beast);
this.UpdateCachePoint(this.Beast);
clippingLevel += Mathf.Abs(maxClippingLevel - minClippingLevel) / 2 * Time.deltaTime;
SkinnedMeshRenderer[] renderers = this.Beast.GetComponentsInChildren<SkinnedMeshRenderer>();
foreach (SkinnedMeshRenderer renderer in renderers)
{
foreach (Material material in renderer.materials)
{
material.SetFloat("_ClippingLevel", clippingLevel);
}
}
yield return 0;
}
yield return new WaitForSeconds(1);
this.summonning = false;
}
Summon协程
为了方便起见,我们使用空格键来运行协程并进行初始设置。
void Start()
{
this.summonning = false;
this.Beast.transform.position = this.Beauty.transform.position;
this.Beast.transform.rotation = this.Beauty.transform.rotation;
this.Beast.SetActive(false);
}
void Update()
{
if (!this.summonning && Input.GetKeyDown(KeyCode.Space) == true)
{
StartCoroutine(Summon());
}
}
Summoner.cs的Start和Update方法
这就是我们的脚本代码。然后创建一个空白游戏对象,把Summoner脚本附加到该对象。在检视窗口设置引用,这样就准备好了。
在运行模式中,我们可以按下空格键,运行交换协程。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Summoner : MonoBehaviour
{
public GameObject Beauty;
public GameObject Beast;
public GameObject VFX;
private Texture2D pointCache;
private float size;
private bool summonning;
void Start()
{
this.summonning = false;
this.Beast.transform.position = this.Beauty.transform.position;
this.Beast.transform.rotation = this.Beauty.transform.rotation;
this.Beast.SetActive(false);
}
void Update()
{
if(!this.summonning && Input.GetKeyDown(KeyCode.Space) == true)
{
StartCoroutine(Summon());
}
}
private IEnumerator Summon()
{
this.summonning = true;
float minClippingLevel = 0;
float maxClippingLevel = 2;
float clippingLevel = maxClippingLevel;
this.Beauty.SetActive(true);
while (clippingLevel > minClippingLevel)
{
this.UpdateSize(this.Beauty);
this.UpdateCachePoint(this.Beauty);
clippingLevel -= Mathf.Abs(maxClippingLevel - minClippingLevel) / 2 * Time.deltaTime;
SkinnedMeshRenderer[] renderers = this.Beauty.GetComponentsInChildren<SkinnedMeshRenderer>();
foreach (SkinnedMeshRenderer renderer in renderers)
{
foreach (Material material in renderer.materials)
{
material.SetFloat("_ClippingLevel", clippingLevel);
}
}
yield return 0;
}
this.Beauty.SetActive(false);
yield return new WaitForSeconds(1);
minClippingLevel = 0;
maxClippingLevel = 3;
this.Beast.SetActive(true);
while (clippingLevel < maxClippingLevel)
{
this.UpdateSize(this.Beast);
this.UpdateCachePoint(this.Beast);
clippingLevel += Mathf.Abs(maxClippingLevel - minClippingLevel) / 2 * Time.deltaTime;
SkinnedMeshRenderer[] renderers = this.Beast.GetComponentsInChildren<SkinnedMeshRenderer>();
foreach (SkinnedMeshRenderer renderer in renderers)
{
foreach (Material material in renderer.materials)
{
material.SetFloat("_ClippingLevel", clippingLevel);
}
}
yield return 0;
}
yield return new WaitForSeconds(1);
this.summonning = false;
}
void UpdateSize(GameObject character)
{
SkinnedMeshRenderer[] renderers = character.GetComponentsInChildren<SkinnedMeshRenderer>();
Bounds bound = new Bounds();
foreach(SkinnedMeshRenderer renderer in renderers)
{
Mesh baked = new Mesh();
renderer.BakeMesh(baked);
bound.Encapsulate(baked.bounds);
}
this.size = Mathf.Max(bound.extents.x * 2, bound.extents.y * 2, bound.extents.z * 2);
}
void UpdateCachePoint(GameObject character)
{
Mesh baked;
Vector3[] vertices;
Transform parent;
SkinnedMeshRenderer[] renderers = character.GetComponentsInChildren<SkinnedMeshRenderer>();
List<Color> normalizedVertices = new List<Color>();
foreach (SkinnedMeshRenderer renderer in renderers)
{
parent = renderer.gameObject.transform.parent;
baked = new Mesh();
renderer.BakeMesh(baked);
vertices = baked.vertices;
for (int i = 0; i < vertices.Length; i++)
{
vertices[i] = (character.gameObject.transform.InverseTransformPoint(renderer.gameObject.transform.TransformPoint(vertices[i])) + new Vector3(size * 0.5f, 0, size * 0.5f)) / size;
normalizedVertices.Add(new Color(vertices[i].x, vertices[i].y, vertices[i].z));
}
}
if(this.pointCache == null || this.pointCache.width != normalizedVertices.Count)
{
this.pointCache = new Texture2D(1, normalizedVertices.Count, TextureFormat.RGBA32, false, true);
this.pointCache.filterMode = FilterMode.Point;
}
this.pointCache.SetPixels(normalizedVertices.ToArray());
this.pointCache.Apply();
}
}
最终的Summoner脚本
预期效果如下图所示。
图 34-Summoner脚本的运行结果
第五步:特效制作
现在到了实现特效最重要的部分,我们将使用Visual Effect Graph。
在项目窗口,点击Create > Visual Effects > Visual Effect Graph创建Visual Effect Graph Asset文件,并把它拖拽到层级窗口,创建Visual Effect Graph对象“Sparkles”。
图 35-创建Visual Effect Graph对象
打开Sparkles,会随之弹出Visual Effect Graph编辑器窗口。它显示为VFX最基础的系统,含有四个主要部分:Spawn、Initialize、Update和Output。
在Spawn部分,我们可以告诉粒子系统想要生成的粒子数量,以及粒子的运动方式:是成群(Burst)出现,还是持续(Constant)出现。
Initialize部分可以设置每个发射粒子的初始数值。我们可以设置Capacity值,确定该系统可以处理的粒子数量,也可以设置Bounds部分,确定粒子的区域边界。
我们还可以在Update部分修改粒子的属性,包括:位置、旋转、大小等。
每个粒子的渲染方式在Output部分定义。我们可以选择点状、线性、四边形、网格或贴花的形状,来渲染我们的每个粒子。
图 36-Visual Effect Graph编辑器
Visual Effect Graph使用Quad四边形作为默认输出形状,我们的效果需要Line Output节点,因此要进行调整。
右键单击空白位置并选择Create Node,在弹出窗口选择Line Output。把Update部分连接到新建的输出节点(Line Output),删除原来的Quad Output。
为了让粒子有尾迹效果,把Target Offset设为(0, -1, 0)向量。在进行下一步前,我们应该添加在生命周期控制粒子颜色的属性块(Set Color over Life)。
在项目中,我们希望粒子开始时发绿光,然后变成红色,最后逐渐消失。
图 38-Target offset 和 Set Color over Life设置
如果把新建的Visual Effect Graph添加到场景中,我们会得到下图的效果。
图 39-运行效果
现在到了最棘手的部分。我们要获取模型上产生粒子的位置,就要用到之前创建的pointCache缓冲区。
为了在Sparkles中访问该缓冲区,我们要创建以下参数:
Texture2D PointCache:缓冲区纹理的引用。
Float Size:用于存储角色边界框的大小。
通过把Sparkles这个Visual Effect Graph对象从层级窗口拖到项目窗口,我们可以创建预制件,以便在Summoner脚本中引用。
图 41-Visual Effect Graph预制件
图 42-Summoner脚本和Visual Effect Graph设置
现在把pointCache缓冲区数据从脚本传输到Visual Effect Graph。为此,我们需要导入VFX命名空间。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Experimental.VFX;
导入VFX命名空间
然后修改Summon方法,使它传递数据。我们希望同步Visual Effect Graph和角色之间的位置和旋转信息。
private IEnumerator Summon()
{
this.summonning = true;
float minClippingLevel = 0;
float maxClippingLevel = 2;
float clippingLevel = maxClippingLevel;
this.Beauty.SetActive(true);
this.VFXInstance = Instantiate(this.VFX);
this.VFXInstance.transform.position = this.Beauty.transform.position;
this.VFXInstance.transform.rotation = this.Beauty.transform.rotation;
while (clippingLevel > minClippingLevel)
{
this.UpdateSize(this.Beauty);
this.UpdateCachePoint(this.Beauty);
clippingLevel -= Mathf.Abs(maxClippingLevel - minClippingLevel) / 2 * Time.deltaTime;
SkinnedMeshRenderer[] renderers = this.Beauty.GetComponentsInChildren<SkinnedMeshRenderer>();
foreach (SkinnedMeshRenderer renderer in renderers)
{
foreach (Material material in renderer.materials)
{
material.SetFloat("_ClippingLevel", clippingLevel);
}
}
this.VFXInstance.GetComponent<VisualEffect>().SetTexture("PointCache", this.pointCache);
this.VFXInstance.GetComponent<VisualEffect>().SetFloat("Size", this.size);
yield return 0;
}
this.Beauty.SetActive(false);
yield return new WaitForSeconds(1);
minClippingLevel = 0;
maxClippingLevel = 3;
this.Beast.SetActive(true);
while (clippingLevel < maxClippingLevel)
{
this.UpdateSize(this.Beast);
this.UpdateCachePoint(this.Beast);
clippingLevel += Mathf.Abs(maxClippingLevel - minClippingLevel) / 2 * Time.deltaTime;
SkinnedMeshRenderer[] renderers = this.Beast.GetComponentsInChildren<SkinnedMeshRenderer>();
foreach (SkinnedMeshRenderer renderer in renderers)
{
foreach (Material material in renderer.materials)
{
material.SetFloat("_ClippingLevel", clippingLevel);
}
}
this.VFXInstance.GetComponent<VisualEffect>().SetTexture("PointCache", this.pointCache);
this.VFXInstance.GetComponent<VisualEffect>().SetFloat("Size", this.size);
yield return 0;
}
yield return new WaitForSeconds(1);
this.summonning = false;
}
Summon方法的改动
现在,我们可以在Visual Effect Graph中访问和使用新得到的数据了。修改Visual Effect Graph和脚本,确保我们在对PointCache缓冲区进行读写。
图 43-Visual Effect Graph中Point Output的临时改动
private IEnumerator Summon()
{
this.summonning = true;
float minClippingLevel = 0;
float maxClippingLevel = 2;
float clippingLevel = maxClippingLevel;
this.Beauty.SetActive(true);
this.VFXInstance = Instantiate(this.VFX);
this.VFXInstance.transform.position = this.Beauty.transform.position;
this.VFXInstance.transform.rotation = this.Beauty.transform.rotation;
while (true/*clippingLevel > minClippingLevel*/)
{
this.UpdateSize(this.Beauty);
this.UpdateCachePoint(this.Beauty);
clippingLevel -= Mathf.Abs(maxClippingLevel - minClippingLevel) / 2 * Time.deltaTime;
SkinnedMeshRenderer[] renderers = this.Beauty.GetComponentsInChildren<SkinnedMeshRenderer>();
/*foreach (SkinnedMeshRenderer renderer in renderers)
{
foreach (Material material in renderer.materials)
{
material.SetFloat("_ClippingLevel", clippingLevel);
}
}*/
this.VFXInstance.GetComponent<VisualEffect>().SetTexture("PointCache", this.pointCache);
this.VFXInstance.GetComponent<VisualEffect>().SetFloat("Size", this.size);
yield return 0;
}
this.Beauty.SetActive(false);
yield return new WaitForSeconds(1);
minClippingLevel = 0;
maxClippingLevel = 3;
this.Beast.SetActive(true);
while (clippingLevel < maxClippingLevel)
{
this.UpdateSize(this.Beast);
this.UpdateCachePoint(this.Beast);
clippingLevel += Mathf.Abs(maxClippingLevel - minClippingLevel) / 2 * Time.deltaTime;
SkinnedMeshRenderer[] renderers = this.Beast.GetComponentsInChildren<SkinnedMeshRenderer>();
foreach (SkinnedMeshRenderer renderer in renderers)
{
foreach (Material material in renderer.materials)
{
material.SetFloat("_ClippingLevel", clippingLevel);
}
}
this.VFXInstance.GetComponent<VisualEffect>().SetTexture("PointCache", this.pointCache);
this.VFXInstance.GetComponent<VisualEffect>().SetFloat("Size", this.size);
yield return 0;
}
yield return new WaitForSeconds(1);
this.summonning = false;
}
Summoner脚本的临时改动
如下图所示,我们可以看到一个半透明的闪闪发光的幽灵在角色旁边跳舞。这表示我们的代码和Visual Effect Graph能够正常工作。我们实现了非常美观的效果,并且可以在以后变换各种使用方式。
图 44-PointCache测试结果
一切进展顺利,上述效果不错。我们现在可以撤销刚刚的改动,因为这些改动仅仅用于调试效果。
我们可以在脚本和视图之间传递更多数据。现在使用ClippingLevel和Emit参数,通过之前的方法传递它们。
Emit参数决定粒子系统是否应该生成粒子。当然我们必须在Visual Effect Graph中创建这些参数。
private IEnumerator Summon()
{
this.summonning = true;
float minClippingLevel = 0;
float maxClippingLevel = 2;
float clippingLevel = maxClippingLevel;
this.Beauty.SetActive(true);
this.VFXInstance = Instantiate(this.VFX);
this.VFXInstance.transform.position = this.Beauty.transform.position;
this.VFXInstance.transform.rotation = this.Beauty.transform.rotation;
while (clippingLevel > minClippingLevel)
{
this.UpdateSize(this.Beauty);
this.UpdateCachePoint(this.Beauty);
clippingLevel -= Mathf.Abs(maxClippingLevel - minClippingLevel) / 2 * Time.deltaTime;
SkinnedMeshRenderer[] renderers = this.Beauty.GetComponentsInChildren<SkinnedMeshRenderer>();
foreach (SkinnedMeshRenderer renderer in renderers)
{
foreach (Material material in renderer.materials)
{
material.SetFloat("_ClippingLevel", clippingLevel);
}
}
this.VFXInstance.GetComponent<VisualEffect>().SetTexture("PointCache", this.pointCache);
this.VFXInstance.GetComponent<VisualEffect>().SetFloat("Size", this.size);
this.VFXInstance.GetComponent<VisualEffect>().SetFloat("ClippingLevel", clippingLevel);
this.VFXInstance.GetComponent<VisualEffect>().SetBool("Emit", true);
yield return 0;
}
this.Beauty.SetActive(false);
yield return new WaitForSeconds(1);
minClippingLevel = 0;
maxClippingLevel = 3;
this.Beast.SetActive(true);
while (clippingLevel < maxClippingLevel)
{
this.UpdateSize(this.Beast);
this.UpdateCachePoint(this.Beast);
clippingLevel += Mathf.Abs(maxClippingLevel - minClippingLevel) / 2 * Time.deltaTime;
SkinnedMeshRenderer[] renderers = this.Beast.GetComponentsInChildren<SkinnedMeshRenderer>();
foreach (SkinnedMeshRenderer renderer in renderers)
{
foreach (Material material in renderer.materials)
{
material.SetFloat("_ClippingLevel", clippingLevel);
}
}
this.VFXInstance.GetComponent<VisualEffect>().SetTexture("PointCache", this.pointCache);
this.VFXInstance.GetComponent<VisualEffect>().SetFloat("Size", this.size);
this.VFXInstance.GetComponent<VisualEffect>().SetFloat("ClippingLevel", clippingLevel);
this.VFXInstance.GetComponent<VisualEffect>().SetBool("Emit", false);
yield return 0;
}
yield return new WaitForSeconds(1);
this.summonning = false;
}
Summoner脚本的ClippingLevel参数和Emit参数
通过使用ClippingLevel参数,我们可以隐藏起始位置大于该值的粒子,只需把对应粒子的生命周期设为0即可。
图 45-Visual Effect Graph的ClippingLevel参数
为了控制粒子生成过程,我们使用Emit参数和简单的Branch节点。
图 46-Visual Effect Graph的Emit参数
如果现在测试场景,我们会得到类似下图的效果,角色模型消散在空气中。
图 47-使用ClippingLevel和Emit参数的效果
粒子现在会向所有方向扩散,这不是我们想要的。我们希望让粒子形成漩涡,此时可以通过Visual Effect Graph的Update部分实现。
我给系统添加了三个作用力(Force):
第一个作用力会把粒子向上推动,让它们保持在特定高度。
第二个作用力会决定漩涡方向。
第三个作用力是让粒子维持在圆形轨道的向心力。
图 48-Visual Effect Graph新加的作用力
我们给系统的Output部分添加了新的属性块,它会根据速度设置粒子的大小(Set Scale.Y by Speed)。
图 49-Visual Effect Graph设置根据速度变化缩放大小
为了改善效果的总体外观,我提高了发射粒子的数量,实现了下图效果。
图 50-添加作用力的效果
这个效果看起来很不错,但是漩涡部分看来有点单调,我们要给它一点随机性。为此我使用了两个方法。
我们修改了添加给粒子的作用力,现在每个粒子都有随机频率的正弦轨道。然后添加了Turbulence属性块,它会朝着伪随机方向推动粒子,然后绕着X轴旋转湍流区域。
图 51-Visual Effect Graph加入随机效果
我们即将完成整个效果。
图 52-随机的效果
最后,我们需要的就是让粒子移动到另一个角色的位置。要实现这个效果不用修改Summoner.cs脚本。
在Emit参数设为false时,粒子不会生成,但是pointCache缓冲区仍会更新。通过使用ID属性,我们使用pointCache的数据,为每个粒子指定新的终点。
为了让粒子移动,我们会使用Conform to Sphere属性块。每个粒子会通过使用pointCache的信息,逐渐移动到网格上的不同小型球体。我把球体的半径值减小为0.01。
图 53-Visual Effect Graph的Conform to Sphere属性块
下图是最终效果。
图 54-使用Conform to Sphere的效果
第六步:为粒子加入光线
美女与野兽的特效到这里就基本完成了,但我最终又决定给效果添加额外的细节。
画面中有很多移动的粒子,发出很多种颜色的光,但是这些光不会对环境产生影响。我们可以改变这种情况。现在添加AreaLight对象到场景中,把颜色设为黑色,把Intensity设为400。
图 55-AreaLight
接下来,使用自己喜欢的软件创建渐变纹理。颜色梯度应该对应粒子在生命周期的颜色。再把纹理导入项目,在导入设置勾选Read/Write Enabled。
图 56-光线颜色梯度
图 57-光线颜色纹理设置
我们的脚本读取新纹理读取数据,就会改掉光线颜色。现在创建参数来引用Texture2D和Light对象。
public Light Light;
public Texture2D LightColor;
Summoner脚本的区域光Light和渐变纹理LightColor
图 58-Summoner脚本的区域光Light和渐变纹理LightColor
设置
最后一步就是编写更新光线颜色的代码,代码如下所示。
private IEnumerator Summon()
{
float lightStep = 0;
this.summonning = true;
float minClippingLevel = -1;
float maxClippingLevel = 2;
float clippingLevel = maxClippingLevel;
this.Beauty.SetActive(true);
this.VFXInstance = Instantiate(this.VFX);
this.VFXInstance.transform.position = this.Beauty.transform.position;
this.VFXInstance.transform.rotation = this.Beauty.transform.rotation;
this.Light.transform.position = this.Beauty.transform.position + new Vector3(0, 3, 0);
while (clippingLevel > minClippingLevel)
{
this.UpdateSize(this.Beauty);
this.UpdateCachePoint(this.Beauty);
clippingLevel -= Mathf.Abs(maxClippingLevel - minClippingLevel) / 5 * Time.deltaTime;
lightStep = Mathf.Abs(maxClippingLevel - clippingLevel) / Mathf.Abs(maxClippingLevel - minClippingLevel) * 0.5f;
SkinnedMeshRenderer[] renderers = this.Beauty.GetComponentsInChildren<SkinnedMeshRenderer>();
foreach (SkinnedMeshRenderer renderer in renderers)
{
foreach (Material material in renderer.materials)
{
material.SetFloat("_ClippingLevel", clippingLevel);
}
}
this.VFXInstance.GetComponent<VisualEffect>().SetTexture("PointCache", this.pointCache);
this.VFXInstance.GetComponent<VisualEffect>().SetFloat("Size", this.size);
this.VFXInstance.GetComponent<VisualEffect>().SetFloat("ClippingLevel", clippingLevel - 0.5f);
this.VFXInstance.GetComponent<VisualEffect>().SetBool("Emit", true);
this.Light.color = LightColor.GetPixel((int)(lightStep * LightColor.width), (int)(0.5f * LightColor.width));
yield return 0;
}
this.Beauty.SetActive(false);
yield return new WaitForSeconds(3);
minClippingLevel = -1;
maxClippingLevel = 3;
this.Beast.SetActive(true);
while (clippingLevel < maxClippingLevel)
{
this.UpdateSize(this.Beast);
this.UpdateCachePoint(this.Beast);
clippingLevel += Mathf.Abs(maxClippingLevel - minClippingLevel) / 10 * Time.deltaTime;
lightStep = (1 - Mathf.Abs(maxClippingLevel - clippingLevel) / Mathf.Abs(maxClippingLevel - minClippingLevel)) * 0.5f + 0.5f;
SkinnedMeshRenderer[] renderers = this.Beast.GetComponentsInChildren<SkinnedMeshRenderer>();
foreach (SkinnedMeshRenderer renderer in renderers)
{
foreach (Material material in renderer.materials)
{
material.SetFloat("_ClippingLevel", clippingLevel);
}
}
this.VFXInstance.GetComponent<VisualEffect>().SetTexture("PointCache", this.pointCache);
this.VFXInstance.GetComponent<VisualEffect>().SetFloat("Size", this.size);
this.VFXInstance.GetComponent<VisualEffect>().SetFloat("ClippingLevel", clippingLevel);
this.VFXInstance.GetComponent<VisualEffect>().SetBool("Emit", false);
this.Light.color = LightColor.GetPixel((int)(lightStep * LightColor.width), (int)(0.5f * LightColor.width));
yield return 0;
}
yield return new WaitForSeconds(1);
this.summonning = false;
}
如下图所示,给效果添加光线后,不得不说效果看起来更加逼真了。
图 59-最终效果
结语
本文篇幅虽然较长,但这样写是为了易于理解。文中所写的实现方法也并不尽善尽美,例如:我们还可以缓存缓冲区,但它至少是其中一种可行的方法。在使用Visual Effect Graph时,我们应该不断思考和尝试。
强烈建议大家亲自尝试制作,并深入研究Visual Effect Graph,因为它不像造火箭那样复杂,但像魔法一样神奇。
下载Unity Connect APP,请点击此处。 观看更多Unity官方精彩视频,请关注“Unity官方”B站账户。
你可以访问Unity答疑专区留下你的问题,Unity社区和官方团队帮你解答:
Connect.unity.com/g/discussion
推荐阅读
Unity UI Profiling:你怎么敢破坏我的批处理?
官方活动
「Unity X 创想家计划」是针对中国地区9-15岁青少年的编程教育计划,火热报名中~~[了解详情...]
喜欢本文,请点“在看”