构建简单图形

我们知道可以使用Instantiate在场景中创建一个物品,那么我们可以通过for循环创建多个物品,通过改变不同物品的x和y轴,就可以呈现出不同的视觉效果。如下面的代码,会出现一条直线。

namespace DYFramework.Examples
{
    public class Graph : MonoBehaviour
    {
        // 假设这时一个立方体预制体
        [SerializeField] private Transform pointPrefab;
        private void Start()
        {
            for (int i = 0; i < 10; i++)
            {
                Transform go = Instantiate(pointPrefab);
                go.localPosition = new Vector3(i, 0, 0);
            }
        }
    }
}

从上面的代码可以看出立方体的坐标时从(0,0,0)~(10,0,0),因为y轴和z轴都为0,我们不妨将其省略掉,那么x的坐标范围为0~10

因为立方体的坐标按照中心点计算的,且立方体的大小为1×1×1,所以说视觉效果的范围其实为-0.5~10.5。在数学的角度来看,这就是函数的定义域,我们如何将函数的定义域限制在-1~1呢?

答案是缩小立方体的大小和改变立方体的x坐标。

我们来计算一下立方体的大小,要想限制在-1~1,它的长度为2。假设长度为2的线段中有count个立方体,那么一个立方体占用 2/count 的长度。那他们的坐标呢?因为坐标是按照中心点计算的,所以立方体的坐标为 (-1 + 2/count/2) ~ (1-2/count/2)

public class Graph : MonoBehaviour
{
    [SerializeField] private Transform pointPrefab;
    private void Start()
    {
        float count = 10;
        for (float i = -1+ 2/count/2; i <= 1 - 2/count/2; i+=2/count)
        {
             Transform go = Instantiate(pointPrefab, transform,false);
            go.localScale = Vector3.one * (2/count); 
            go.localPosition = new Vector3(i, 0, 0);
        }
    }
}

为此我们可以封装为一个方法,其中xLeft和xRight为x的范围,count表示会生成多少个立方体。

namespace DYFramework.Examples
{
    public class Graph : MonoBehaviour
    {
        [SerializeField] private Transform pointPrefab;
        private void Start()
        {
            DrawCube(-1, 1, 10);
        }

        public void DrawCube(float xLeft, float xRight, int count)
        {
            float length = xRight - xLeft;
            for (float i = xLeft+ length/count/2; i <= xRight - length/count/2; i+=length/count)
            {
                 Transform go = Instantiate(pointPrefab, transform,false);
                go.localScale = Vector3.one * (length/count); 
                go.localPosition = new Vector3(i, 0, 0);
            }
        }
    }
}

但是这种方法只会画一条直线,能不能画曲线。比如画一条 y = x^2。显然我们可以在代码中直接改变位置的y轴即可

public void DrawCube(float xLeft, float xRight, int count)
{
    float length = xRight - xLeft;
    var position = Vector3.zero;
    for (float i = xLeft+ length/count/2; i <= xRight - length/count/2; i+=length/count)
    {
         Transform go = Instantiate(pointPrefab, transform, false);
        go.localScale = Vector3.one * (length/count);
        position.x = i;
        position.y = i * i;
        go.localPosition = position;
    }
}

如果只是单一的颜色,未免有点单调,我们可以为其添加一个shader。

我们可以使用URP通用渲染管线,如果你没有用过URP的话或者你创建的2D项目不是URP的话,可以在包管理工具中下载,下载完成后,如下图这种效果

但这不会自动使 Unity 使用 URP。我们首先必须通过 Assets / Create / Rendering / Universal Render Pipeline / Pipeline Asset (Forward Renderer) 为它创建一个资产。

然后接下来,转到项目设置 Graphics 部分并将创建的 URP 资产分配给该 Scriptable Renderer Pipeline Settings 字段。

我们可以创建一个ShaderGraph,如果你没有Shader Graph,同样的道理,你可以在包管理器中的搜索栏中搜索Shader Graph,然后直接下载。

创建一个新的 Shader Graph via Assets / Create / Shader / Universal Render Pipeline / Lit Shader Graph 并将其命名为 Point URP 。

Lit和UnLit是什么意思?

Lit是有光照,UnLit是无光照效果

可以通过在项目窗口中双击其资源或按其检查器中的 Open Shader Editor 按钮来打开图形。

这将为其打开一个 Shader Graph 窗口,该窗口可能会被多个节点和面板弄乱。

这些面板是 Blackboard、Graph Inspector 和主预览面板,它们可以调整大小,也可以通过工具栏按钮隐藏。还有两个链接节点:一个 Vertex 节点和一个 Fragment 节点。这两个选项用于配置 shader graph 的输出。

Shader Graph 由表示数据和节点组成。目前, Smoothness Fragment 节点的值设置为 0.5。要使其成为可配置的着色器属性,请按 Point URP 面板上的加号按钮,选择 Float ,然后将新条目命名为 Smoothness。这会向Blackboard添加一个表示属性的圆角按钮。选择它,然后将图形检查器切换到其 Node Settings 选项卡以查看此属性的配置。

Reference 是内部已知属性的名称。然后将其下方的默认值设置为 0.5。确保启用其 Exposed 切换选项,因为这会控制材质是否为其获取着色器属性。最后,要使其显示为滑块,请将其 Mode 更改为 Slider 。

接下来,将圆角 Smoothness 按钮从黑板拖动到图表中的空白区域。这将向图表中添加一个 smoothness 节点。通过从其中一个点拖动到另一个点,将其连接到 PRB Master 节点的 Smoothness 输入。这将在它们之间建立联系。

现在,您可以通过 Save Asset 工具栏按钮保存图表,并创建一个使用它的材质 Point URP 。着色器的菜单项为 Shader Graphs / Point URP 。然后使 Point 预制件使用该材质。

要为点着色,我们必须从 position 节点开始。通过在图形的空白部分打开上下文菜单并从 New Node 中进行选择来创建一个。选择 Input / Geometry / Position 或仅搜索 Position

使用相同的方法创建 a Multiply 和 an Add 节点。使用这些选项可将位置的 XY 分量缩放 0.5,然后添加 0.5,同时将 Z 设置为零。这些节点根据它们所连接的内容调整其输入类型。因此,首先连接节点,然后填写它们的常量输入。然后将结果连接到 Fragment 的 Base Color 输入。

如果将鼠标悬停在 Multiply 和 Add 节点上,则可以通过按其右上角显示的箭头来压缩这些节点的视觉大小。这将隐藏所有未连接到花药节点的输入和输出。这消除了很多杂乱。您还可以通过 Vertex 和 Fragment 节点的上下文菜单删除其组件。这样,您可以隐藏保持其默认值的所有内容。

保存着色器资源后,我们现在在播放模式下获得与使用默认渲染管道时相同的彩色点。除此之外,调试更新程序还会显示在播放模式下的单独 DontDestroyOnLoad 场景中。这是用于调试 URP 的,可以忽略。

然后运行项目,就可以看到立方体的颜色会随着立方体世界位置不同而发生改变

但是图形无法动起来啊!要想动起来也比较简单,我们只需要拿到所创建的立方体,然后在Update改变其y轴的位置就可以让其动起来。

在此之前,我们先去设置一个正弦波的函数。

namespace DYFramework.Examples
{
    public class Graph : MonoBehaviour
    {
        [SerializeField] private Transform pointPrefab;

        private List<Transform> _transforms = new();
        private void Start()
        {
            DrawSinFun(-1, 1, 50);
        }

        public void DrawSinFun(float xLeft, float xRight, int count)
        {
            float length = xRight - xLeft;
            var position = Vector3.zero;
            for (float i = xLeft+ length/count/2; i <= xRight - length/count/2; i+=length/count)
            {
                Transform go = Instantiate(pointPrefab, transform,false);
                go.localScale = Vector3.one * (length/count);
                position.x = i;
                position.y = Mathf.Sin(i * Mathf.PI);
                go.localPosition = position;
                _transforms.Add(go);
            }
        }
    }
}

效果如下:

接下来我们可以让其动起来

要为该函数设置动画,请在计算 sin 函数之前将当前游戏时间添加到 X。它是通过 Time.time .如果我们将时间也缩放 π,该函数将每两秒重复一次。

整体代码如下,为了让视觉效果更好,我重新设置了输入的参数

using System;
using System.Collections.Generic;
using UnityEngine;

namespace DYFramework.Examples
{
    public class Graph : MonoBehaviour
    {
        [SerializeField] private Transform pointPrefab;

        private List<Transform> _transforms = new();
        private void Start()
        {
            DrawSinFun(-5, 5, 100);
        }

        public void DrawSinFun(float xLeft, float xRight, int count)
        {
            float length = xRight - xLeft;
            var position = Vector3.zero;
            for (float i = xLeft+ length/count/2; i <= xRight - length/count/2; i+=length/count)
            {
                Transform go = Instantiate(pointPrefab, transform,false);
                go.localScale = Vector3.one * (length/count);
                position.x = i;
                position.y = Mathf.Sin(i * Mathf.PI);
                go.localPosition = position;
                _transforms.Add(go);
            }
        }

        private void Update()
        {
            float time = Time.time;
            for (var i = 0; i < _transforms.Count; i++)
            {
                Transform point = _transforms[i];
                Vector3 pos = point.localPosition;
                pos.y = Mathf.Sin(Mathf.PI * (pos.x + time));
                point.localPosition = pos;
            }
        }
    }
}

数字表面