查看原文
其他

Unity教程|手把手教你实现“美女与野兽”特效(下)

Bogumił Mazurek Unity官方平台 2022-05-07

本文将继续分享波兰的游戏开发者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。


图 37-Line 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:用于存储角色边界框的大小。

 

图 40-PointCache和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


推荐阅读

手把手教你实现“美女与野兽”特效(上)

制作蝴蝶烟火视觉特效

使用粒子实现Logo消融效果

使用DOTS制作一款第三人称僵尸射击游戏

Unity Reflect正式版发布

Unity UI Profiling:你怎么敢破坏我的批处理?


官方活动

Unity X 创想家计划

「Unity X 创想家计划」是针对中国地区9-15岁青少年的编程教育计划,火热报名中~~[了解详情...


AR应用创作大赛Unity与商汤科技强强联手举办AR应用创作大赛,帮助开发者了解使用商汤AR SDK进行开发的方法,高效的进行AR内容创作,推进AR应用创新与落地。[了解详情...



喜欢本文,请点“在看”

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存