零基礎也能學會的舞動的曲線——Unity Shader基礎之Prefabs(二)下

語言: CN / TW / HK

theme: smartblue

持續創作,加速成長!這是我參與「掘金日新計劃 · 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 (兩種着色器語言)混合編寫的代碼部分。此代碼附上CGPROGRAMENDCG` 關鍵字中。

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

這個shader在vs2019中不被識別,沒有高亮等這些問題,大家可以安裝 Shaderlab, 或者採用github上官方的説明,根據需求 前往下載 此處踩了很多坑!!!

image.png

成功安裝後,你將看到着色器代碼不再是黑漆漆的一片了,歐耶

image.png

(之前我安裝了下面的這些仍舊不太好用,大家自行體會吧,可忽略這部分錯誤嘗試

image.png

image.png

設置行號 Toosl-->Options-->Text Editor-->All Languages-->General

image.png

保存後就會顯示行號了

image.png

安裝彩色括號插件,可以去官網選擇版本自行下載,但是shader中的括號仍然不可見,所以先忽略這個問題,讓我們繼續。

我們將基於他們的世界位置來給我們的點上色。為了使着色這件事在表面着色器中起作用,我們必須為配置函數定義輸入的結構體。它必須寫成 struct Input ,後面跟着一個代碼塊,然後是一個分號。在塊內部,我們聲明一個結構字段,確切地説是 float3 worldPos。 它將包含渲染內容的世界位置。float3 類型是 Vector3 結構的着色器等效物。

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

struct Input {
        float3 worldPos;
};
ENDCG

```