零基础也能学会的舞动的曲线——Unity Shader基础之Surface Shader(二)下

1,898 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第9天,点击查看活动详情。你可以简单浏览一下目录,有需要的阅读,写文章不易,阅读之前请给我点个赞吧~

上节我们讲到

  • 创建一个预制(prefab)
  • 实例化多个立方体(cubes)
  • C# 代码的一些精简
  • 用立方体展现一个数学函数,绘制笑脸
  • 还没看的同学请自行前往,因为两节课是有关联的哦~

这节主要内容

  • 创建一个 表面着色器(surface shader)着色器图(shader graph)
  • 让曲线动起来

一、创建更多的立方体

上节的笑脸很简单,同时也很粗糙。如果我们用更多更小的立方体,看起来会更好。

1.1 变化的分辨率(Resolution)

我们可以使其可配置,而不是使用固定数量的立方体。为了使这成为可能,为‘Graph’的分辨率添加一个可序列化的整数字段。设置它的默认值为10,代码如下。

[SerializeField]
Transform pointPrefab;

[SerializeField]
int resolution = 10;

image.png

现在我们可以通过检查器(inspector)来调整图形的分辨率。然而,并不是所有的整数都是有效的分辨率。至少他们必须是的。我们可以指示 inspector 为我们的分辨率强制执行一个范围。这是通过添加 Range 属性来实现的。我们可以将 resolution 的两个属性放在各自的方括号中,也可以将它们合并到一个逗号分隔的属性列表中。也就是下面的这种:

[SerializeField, Range]
int resolution = 10;

inspector 检查字段是否带有 Range 属性。如果有,它将约束该值并显示一个滑块。然而,要做到这一点,它需要知道允许的范围。因此 Range 需要两个参数 —— 像方法一样 —— 来获取最小值和最大值。我们用10和100。

[SerializeField, Range(10, 100)]
int resolution = 10;

huagui.gif

这里先设置为50

1.2 变量的实例化(Variable Instantiation)

为了使用配置的分辨率,我们必须改变实例化的立方体的数量。在 Awake 中,迭代次数不再是固定次数,而是受到分辨率的限制,而不是总是10次。所以如果分辨率设置为50,我们将在进入播放模式后获得50个立方体。

for (int i = 0; i < resolution; i++) {
        …
}

我们还必须调整立方体的缩放比例(scale)和位置(positions),以保持它们在−1-1域内。我们每次迭代的每一步的大小现在是2除以分辨率。将这个值存储在一个变量中,并使用它来计算立方体的比例及其X坐标。

using UnityEngine;

public class Graph : MonoBehaviour
{
    [SerializeField]
    Transform pointPrefab;

    [SerializeField, Range(10, 100)]
    int resolution = 10;

    void Awake()
    {
        var position = Vector3.zero;
        var position2 = Vector3.zero;
        var position3 = Vector3.zero;

        float step = 2f / resolution;
        var scale = Vector3.one * step;

        for (int i = 0; i < resolution; i++)
        {
            Transform point = Instantiate(pointPrefab);
            position.x = (i + 0.5f) * step - 1f;
            position.y = position.x * position.x;
            point.localPosition = position;
            point.localScale = scale;

            Transform point2 = Instantiate(pointPrefab);
            position2.x = (i + 0.5f) * step - 2f;
            position2.y = -1 * position2.x * position2.x - 2 * position2.x + 1;
            point2.localPosition = position2;
            point2.localScale = scale;

            Transform point3 = Instantiate(pointPrefab);
            position3.x = (i + 0.5f) * step;
            position3.y = -1 * position3.x * position3.x + 2 * position3.x + 1;
            point3.localPosition = position3;
            point3.localScale = scale;
        }  
    }
}

image.pngimage.png

1.3 设置父节点

在进入分辨率为 50 的播放模式后,许多实例化的立方体会出现在场景中,因此也会出现在项目窗口中。

image.png

这些点目前是根对象,但将它们作为图形(graph)对象的子对象是有意义的。我们可以在实例化一个点之后建立这种关系,方法是调用 Transform 组件的 SetParent 方法,并将所需的父 Transform 传递给它。我们可以通过 Graphtransform 属性获得图形对象的  Transform 组件,该属性继承自  Component。在循环块的末尾执行此操作。

for (int i = 0; i < resolution; i++) {
        …
        point.SetParent(transform);
}

image.png

当设置了一个新的父对象时,Unity 将尝试保持对象在其原始的世界位置、旋转和缩放。我们的案子不需要这个。我们可以通过将' false '作为第二个参数传递给' SetParent '来发出这个信号。
point.SetParent(transform, false);

二、给图着色

白色的图看起来并不漂亮。我们可以用另一种纯色(solid color),但那也不是很有趣。用一个点的位置来确定它的颜色更有趣。

调整每个立方体颜色的简单方法是设置其材质的颜色属性。我们可以在循环中进行。因为每个立方体将得到不同的颜色,这意味着我们将以每个对象一个唯一的材质实例结束。当我们之后给图形做动画的时候我们也需要一直调整这些材料。虽然这种方法有效,但效率并不高。如果我们可以使用单一的材质直接使用位置作为颜色,那就更好了。不幸的是,Unity没有这样的材料。所以让我们自己做吧。

2.1 创建一个表面着色器(Surface Shader)

GPU 运行着色程序来渲染 3D 对象。Unity 的材质资产(material assets)决定使用哪个着色器,并允许配置它的属性。我们需要创建一个自定义着色器来获得我们想要的功能。通过 Assets --> Create --> Shader --> Standard Surface Shader 创建一个着色器,命名为 Point Surface

image.png

image.png

归类到同一个文件夹

image.png

我们现在有一个着色器资产,你可以像一个脚本那样打开它。我们的着色器文件包含定义表面着色器的代码,它使用与 c# 不同的语法。它包含一个表面着色器模板,但我们将删除所有内容,从头开始创建一个最小的着色器。

表面着色器是如何工作的?

Unity 提供了一个框架来快速生成执行默认照明计算的着色器,你可以通过调整某些值来影响这些计算。这样的着色器称为表面着色器。不幸的是,它们只适用于默认的渲染管线。我们将在后面讨论(cover)通用渲染管线(Universal render pipeline)。

Unity 有自己的着色器资产语法,总体上大致类似于c#,但它是不同语言的混合。它以 Shader 关键字开始,后面跟着一个为 Shader 定义菜单项的字符串。字符串写在双引号内。我们将使用 Graph/Point Surface。之后是着色器内容的代码块

Shader "Graph/Point Surface" {}

着色器可以有多个子着色器,每个子着色器由' SubShader '关键字定义,后面跟着一个代码块。我们只需要一个。

Shader "Graph/Point Surface" {
	SubShader {}
}

在子着色器下面,我们还想添加一个回退到标准漫反射着色器,通过写入FallBack "Diffuse"

Shader "Graph/Point Surface" {
	
	SubShader {}
	
	FallBack "Diffuse"
}

表面着色器的子着色器需要用 CG 和 HLSL (两种着色器语言)混合编写的代码部分。此代码附上 CGPROGRAM  和 ENDCG 关键字中。

	SubShader {
		CGPROGRAM
		ENDCG
	}

第一个需要的语句是一个编译程序指令,称为 pragm a。它被写成 #pragma ,后面跟着一个指令。在这种情况下,我们需要 #pragma surface ConfigureSurface Standard fullforwardshadows,它指示着色器编译器生成一个具有标准光照和完全支持阴影的表面着色器。 ConfigureSurface 指的是一个用于配置着色器的方法,我们将不得不创建它。

    CGPROGRAM
    #pragma surface ConfigureSurface Standard fullforwardshadows
    ENDCG

接下来我们使用#pragma target 3.0指令,它为着色器的目标级别和质量设置了最小值。

    CGPROGRAM
    #pragma surface ConfigureSurface Standard fullforwardshadows
    #pragma target 3.0
    ENDCG

2.1.1 解决 visual studio 中 shader 不识别的问题

这个shader在vs2019中不被识别,没有高亮等这些问题,大家可以安装 Shaderlab, 或者采用github上官方的说明,根据需求 前往下载 此处踩了很多坑!!!

image.png

成功安装后,你将看到着色器代码不再是黑漆漆的一片了,欧耶

image.png

(之前我安装了下面的这些仍旧不太好用,大家自行体会吧,可忽略这部分错误尝试

image.png

image.png

设置行号 Toosl-->Options-->Text Editor-->All Languages-->General

image.png

保存后就会显示行号了

image.png

安装彩色括号插件,可以去官网选择版本自行下载, 但是shader 中仍有一些代码不可见,这个问题困扰了我很久,现在补上(一个很蠢的问题!!!就是字体的颜色与背景色相近所以看不清!!!)。 我们只需在 Tools-->Options中,修改如下字体颜色即可,我这里选择了 Olive

image.png

设置后的效果如下所示,终于能看见了!可以愉快的写代码了~

image.png

我们将基于他们的世界位置来给我们的点上色。为了使着色这件事在表面着色器中起作用,我们必须为配置函数定义输入的结构体。它必须写成 struct Input ,后面跟着一个代码块,然后是一个分号。在块内部,我们声明一个结构字段,确切地说是 float3 worldPos。 它将包含渲染内容的世界位置。float3 类型是 Vector3 结构的着色器等效物。

    CGPROGRAM
    #pragma surface ConfigureSurface Standard fullforwardshadows
    #pragma target 3.0

    struct Input {
            float3 worldPos;
    };
    ENDCG

【思考】这是否意味着移动图形会影响它的颜色?

是的。使用这种方法,只有当我们将 Graph 对象留在它所在的位置时,也就是说在世界原点,没有旋转,缩放为1,着色才会是正确的。
还要注意,这个位置是由每个顶点决定的。在我们的例子中,这是一个立方体的每个角。 颜色将被插值到立方体的各个面。立方体越大,这种颜色转换就越明显。

下面我们定义了 ConfigureSurface 方法,尽管在着色器的例子中,它总是被称为函数,而不是方法。它是一个带有两个参数的 void 函数。首先是一个输入参数,它具有我们刚刚定义的 Input 类型。第二个参数是表面配置数据,类型为 SurfaceOutputStandard

    struct Input {
            float3 worldPos;
    };

    void ConfigureSurface (Input input, SurfaceOutputStandard surface) {}

第二个形参的类型前面必须写有 inout 关键字,这表明它既传递给函数,又用于函数的结果。

    void ConfigureSurface (Input input, inout SurfaceOutputStandard surface) {}

现在我们有了一个功能着色器( functioning shader ),为它创建一个材质,命名为 Point Surface

image.png

设置它使用我们的着色器,通过它的 inspector 顶部的 * shader *下拉列表选择 Graph --> Point Surface

image.png

该材料目前是固体哑光黑色。我们可以在配置函数中将 surface.Smoothness 设置到0.5,来使它看起来更像默认材质。在编写着色器代码时,我们不必在 float 值上添加 f 后缀。

    void ConfigureSurface (Input input, inout SurfaceOutputStandard surface) {
            surface.Smoothness = 0.5;
    }

现在这种材料不再是完全哑光的了。你可以在检查器顶部的小材质预览中看到,或者在底部的可调整大小的预览中看到。

image.png

我们还可以使平滑性可配置,就像为它添加一个字段并在函数中使用它一样。默认的风格是在着色器配置选项前加上下划线并大写下一个字母,所以我们将使用 _Smoothness

    float _Smoothness;

    void ConfigureSurface (Input input, inout SurfaceOutputStandard surface) {
            surface.Smoothness = _Smoothness;
    }

为了使这个配置选项出现在编辑器中,我们必须在着色器的顶部,子着色器的上方添加一个Properties 块。在这里写入 _Smoothness,后面跟着("Smoothness", Range(0,1)) = 0.5。这赋予它Smoothness标签,将其作为一个范围为 0-1 的滑块,并将其默认值设置为0.5。

Shader "Graph/Point Surface" {

    Properties {
        _Smoothness ("Smoothness", Range(0,1)) = 0.5
    }

    SubShader {
            …
    }
}