Unity-頂點(diǎn)和片元著色器示例
本頁(yè)面包含頂點(diǎn)和片元程序示例。 有關(guān)著色器的基本介紹,請(qǐng)參閱著色器教程:?第 1 部分和第 2 部分。有關(guān)編寫(xiě)常規(guī)材質(zhì)著色器的簡(jiǎn)便方法,請(qǐng)參閱表面著色器。
可通過(guò)?Unity 項(xiàng)目壓縮包的形式下載以下顯示的示例。
設(shè)置場(chǎng)景
如果您不熟悉 Unity 的?Scene 視圖、Hierarchy 視圖、?Project 視圖__和?Inspector__,現(xiàn)在最好是閱讀 手冊(cè)的前幾部分,從?Unity 的界面開(kāi)始。
第一步是創(chuàng)建一些用于測(cè)試著色器的對(duì)象。在主菜單中選擇?Game Object?>?3D Object?>?Capsule。然后,調(diào)整攝像機(jī)位置,使其顯示膠囊體。在 Hierarchy 視圖中雙擊膠囊體 (Capsule) 以將 Scene 視圖聚焦在該膠囊體上,然后選擇主攝像機(jī) (Main Camera) 對(duì)象,并單擊主菜單中的?Game object?>?Align with View。

在 Project 視圖中,從菜單中選擇?Create?> _Material__,創(chuàng)建新的材質(zhì)。Project 視圖中將顯示一個(gè)名為?New Material_ 的新材質(zhì)。

創(chuàng)建著色器
現(xiàn)在以類(lèi)似方式創(chuàng)建一個(gè)新的著色器資源。在 Project 視圖中,從菜單中選擇?Create?>?Shader?>?Unlit Shader。 隨后將創(chuàng)建一個(gè)基本著色器,該著色器僅顯示一個(gè)紋理,無(wú)任何光照。

Create?>?Shader?菜單中的其他條目將創(chuàng)建基本要素著色器 或其他類(lèi)型,例如,基本表面著色器。
鏈接網(wǎng)格、材質(zhì)和著色器
通過(guò)材質(zhì)的檢視面板使材質(zhì)使用著色器,或者在 Project 視圖中將著色器資源拖動(dòng)到材質(zhì)資源上。材質(zhì)檢視面板在使用此著色器時(shí)將顯示白色球體。

現(xiàn)在將材質(zhì)拖動(dòng)到 Scene 或 Hierarchy 視圖中的網(wǎng)格對(duì)象上。或者,選擇對(duì)象,并在檢視面板中使其使用網(wǎng)格渲染器 (Mesh Renderer)?組件的 Materials 字段中的材質(zhì)。

完整這些設(shè)置后,現(xiàn)在可以開(kāi)始查看著色器代碼,并會(huì)在 Scene 視圖中看到對(duì)膠囊體上著色器所做的更改的結(jié)果。
著色器的主要部分
要開(kāi)始檢查著色器的代碼,請(qǐng)?jiān)?Project 視圖中雙擊著色器資源。著色器代碼將在腳本編輯器(MonoDevelop 或 Visual Studio)中打開(kāi)。
著色器最開(kāi)始的代碼如下:
這個(gè)初始著色器看上去不是那么簡(jiǎn)單!但不要擔(dān)心, 我們將逐步介紹每個(gè)部分。
讓我們看看這個(gè)簡(jiǎn)單著色器的主要部分。
Shader?命令包含一個(gè)帶有 著色器名稱(chēng)的字符串。在材質(zhì)檢視面板中選擇著色器時(shí),可以使用正斜杠字符“/”將著色器放置在子菜單中。
Properties?代碼塊包含著色器變量 (紋理、顏色等),這些變量將保存為材質(zhì)的一部分, 并顯示在材質(zhì)檢視面板中。在我們的無(wú)光照著色器模板中, 聲明了單個(gè)紋理屬性。
一個(gè)著色器 (Shader) 可以包含一個(gè)或多個(gè)子著色器 (SubShader),主要 用于實(shí)現(xiàn)不同 GPU 功能的著色器。 在本教程中,我們并不太關(guān)心這一點(diǎn),所以我們 所有的著色器都只包含一個(gè)子著色器。
每個(gè)子著色器由多個(gè)通道組成,每個(gè)通道代表 為使用著色器材質(zhì)渲染的同一對(duì)象執(zhí)行 頂點(diǎn)和片元代碼。 許多簡(jiǎn)單的著色器只使用一個(gè)通道,但與光照交互的 著色器可能需要更多通道(有關(guān)詳細(xì)信息,請(qǐng)參閱?光照管線)。通道內(nèi)部的 命令通常設(shè)置固定函數(shù)狀態(tài),例如 混合模式。
這些關(guān)鍵字包裹著頂點(diǎn)和片元著色器中的 HLSL 代碼 部分。通常,大多數(shù)相關(guān)代碼都包含在這里。有關(guān)詳細(xì)信息,請(qǐng)參閱?頂點(diǎn)和片元著色器。
簡(jiǎn)單的無(wú)光照著色器
無(wú)光照著色器模板比顯示帶紋理的對(duì)象 絕對(duì)需要更多的代碼。例如, 它支持霧效,以及材質(zhì)中的紋理平鋪/偏移字段。 讓我們最大程度簡(jiǎn)化著色器,并添加更多注釋?zhuān)?/p>
__頂點(diǎn)著色器__是對(duì) 3D 模型的每個(gè)頂點(diǎn)運(yùn)行的程序。通常情況下,它并不做任何特別有趣的事情。這里我們只是將頂點(diǎn)位置從對(duì)象空間轉(zhuǎn)換為所謂的“裁剪空間”(由 GPU 用于柵格化屏幕上的對(duì)象)。我們還傳遞未修改的輸入紋理坐標(biāo);我們需要該坐標(biāo)來(lái)對(duì)片元著色器中的紋理進(jìn)行采樣。
__片元著色器__是對(duì)屏幕上對(duì)象占據(jù)的每個(gè)像素運(yùn)行的程序,通常用于計(jì)算和輸出每個(gè)像素的顏色。通常,屏幕上有數(shù)百萬(wàn)個(gè)像素,并且片元著色器將 針對(duì)所有像素執(zhí)行!優(yōu)化片元著色器是整體游戲性能優(yōu)化工作的重要組成部分。
一些變量或函數(shù)定義后跟一個(gè)語(yǔ)義指示符,例如?: POSITION?或?: SV_Target。這些語(yǔ)義指示符將這些變量的“含義”傳達(dá)給 GPU。有關(guān)詳細(xì)信息,請(qǐng)參閱著色器語(yǔ)義頁(yè)面。
用于具有良好紋理的良好模型時(shí),我們的簡(jiǎn)單著色器看起來(lái)非常好!

更簡(jiǎn)單的單色著色器
讓我們進(jìn)一步簡(jiǎn)化著色器:我們將制作一個(gè)用單一顏色繪制整個(gè)對(duì)象的 著色器。這并不是很有用,但別忘了,我們是在學(xué)習(xí)。

這次,著色器函數(shù)只是手動(dòng)拼出輸入,而不是使用輸入結(jié)構(gòu) (appdata) 和輸出結(jié)構(gòu) (v2f)。兩種方式都有效,選擇使用哪種方式取決于您編寫(xiě)代碼的風(fēng)格和偏好。
使用網(wǎng)格法線,輕松獲利
讓我們繼續(xù)了解在世界空間中顯示網(wǎng)格法線的著色器。言歸正傳:

除了產(chǎn)生漂亮的顏色外,法線還用于各種圖形效果:光照、反射、輪廓等。
在上面的著色器中,我們開(kāi)始使用 Unity 的內(nèi)置著色器 include 文件之一。 這里使用的?UnityCG.cginc?包含一個(gè)方便的函數(shù)?UnityObjectToWorldNormal。我們還使用了實(shí)用函數(shù) __UnityObjectToClipPos__,該函數(shù)將頂點(diǎn)從對(duì)象空間轉(zhuǎn)換為屏幕。這使得代碼更易于閱讀,并且在某些情況下更有效。
在所謂的“插值器”(有時(shí)稱(chēng)為“變化”)中,我們已經(jīng)看到數(shù)據(jù)可從頂點(diǎn)傳入片元著色器。在 HLSL 著色語(yǔ)言中,它們通常用?TEXCOORDn?語(yǔ)義進(jìn)行標(biāo)記,其中每一個(gè)最多可以是一個(gè) 4 分量矢量(有關(guān)詳細(xì)信息,請(qǐng)參閱語(yǔ)義頁(yè)面)。
此外,我們學(xué)習(xí)了如何使用一種簡(jiǎn)單的技術(shù)將標(biāo)準(zhǔn)化矢量(在 –1.0 到 +1.0 范圍內(nèi))可視化為顏色:只需將它們乘以二分之一并加二分之一。請(qǐng)?jiān)陧旤c(diǎn)程序輸入頁(yè)面中查看更多頂點(diǎn)數(shù)據(jù)可視化示例。
使用世界空間法線的環(huán)境反射
如果在場(chǎng)景中使用天空盒作為反射源(請(qǐng)參閱?Lighting 窗口), 基本上會(huì)創(chuàng)建一個(gè)包含天空盒數(shù)據(jù)的“默認(rèn)”反射探針。 反射探針內(nèi)部是立方體貼圖紋理;我們將擴(kuò)展上面的世界空間法線著色器來(lái)對(duì)其進(jìn)行深入了解。
代碼現(xiàn)在開(kāi)始變得有點(diǎn)復(fù)雜。當(dāng)然,如果您希望著色器能夠自動(dòng)處理光源、陰影、反射以及光照系統(tǒng)的其余部分,使用表面著色器會(huì)容易得多。此示例旨在向您展示如何以“手動(dòng)”方式使用光照系統(tǒng)的各個(gè)部分。

以上示例使用了內(nèi)置著色器 include 文件中的部分內(nèi)容:
來(lái)自?xún)?nèi)置著色器變量的?unity_SpecCube0、unity_SpecCube0_HDR、Object2World?和?UNITY_MATRIX_MVP。unity_SpecCube0?包含激活的反射探針的數(shù)據(jù)。
UNITY_SAMPLE_TEXCUBE?是一個(gè)用于采樣立方體貼圖的內(nèi)置宏。通常使用標(biāo)準(zhǔn) HLSL 語(yǔ)法聲明和 使用大多數(shù)常規(guī)立方體貼圖(__samplerCUBE__ 和?texCUBE__),但 Unity 中的反射探針立方體貼圖以特殊方式聲明以節(jié)省采樣器字段。如果您不知道這是什么,請(qǐng)不要擔(dān)心,只需要知道:要使用?unity_SpecCube0__ 立方體貼圖,必須使用?UNITY_SAMPLE_TEXCUBE?宏。
UnityCG.cginc?中的?UnityWorldSpaceViewDir?函數(shù)以及來(lái)自同一文件的?DecodeHDR?函數(shù)。后者用于從反射探針數(shù)據(jù)中獲取實(shí)際顏色(因?yàn)?Unity 以特殊編碼方式存儲(chǔ)反射探針立方體貼圖)。
reflect?只是一個(gè)內(nèi)置的 HLSL 函數(shù),用于計(jì)算給定法線周?chē)氖噶糠瓷洹?/p>
使用法線貼圖的環(huán)境反射
通常,__法線貼圖__用于在對(duì)象上創(chuàng)建其他細(xì)節(jié),而無(wú)需創(chuàng)建其他幾何體。讓我們看看如何使用法線貼圖紋理制作發(fā)射環(huán)境的著色器。
現(xiàn)在開(kāi)始真正涉及到了數(shù)學(xué)知識(shí),所以我們將分幾步完成。在上面的著色器中,反射 方向是按每個(gè)頂點(diǎn)計(jì)算的(在頂點(diǎn)著色器中),片元著色器僅執(zhí)行反射探針 立方體貼圖查找。然而,一旦我們開(kāi)始使用法線貼圖,表面法線本身需要按每個(gè)像素計(jì)算,這意味著我們還必須按每個(gè)像素計(jì)算環(huán)境的反射方式!
首先,讓我們重寫(xiě)上面的著色器來(lái)實(shí)現(xiàn)相同效果,只是我們將一些計(jì)算移到片元著色器,從而按每個(gè)像素進(jìn)行計(jì)算:
這里并沒(méi)有帶給我們太多不同:著色器看起來(lái)完全相同,只是它現(xiàn)在運(yùn)行得更慢,因?yàn)樗鼘?duì)屏幕上的每個(gè)像素進(jìn)行更多的計(jì)算,而不是僅對(duì)每個(gè)模型的頂點(diǎn)進(jìn)行計(jì)算。但是,我們很快就會(huì)真正需要這些計(jì)算。更高的圖形保真度通常需要更復(fù)雜的著色器。
我們現(xiàn)在也要學(xué)習(xí)新東西,即所謂的“切線空間”。法線貼圖紋理通常在坐標(biāo)空間中表示,可將其視為“跟隨模型表面”。在我們的著色器中,我們需要知道切線空間基矢量,從紋理中讀取法線矢量,將其轉(zhuǎn)換為世界空間,然后通過(guò)上面的著色器 進(jìn)行所有數(shù)學(xué)運(yùn)算。我們行動(dòng)吧!
哎呦,這個(gè)非常復(fù)雜。但是,看看法線貼圖反射!
添加更多紋理
讓我們?yōu)樯厦娴姆ň€貼圖、天空反射著色器添加更多紋理。我們將添加基礎(chǔ)顏色紋理(如第一個(gè)無(wú)光照的示例中所示)和遮擋貼圖來(lái)使洞穴變暗。

氣球貓看起來(lái)很不錯(cuò)!
紋理化著色器示例
程序化棋盤(pán)圖案
下面的著色器根據(jù)網(wǎng)格的紋理坐標(biāo)輸出棋盤(pán)圖案:
Properties?代碼塊中的密度滑動(dòng)條控制棋盤(pán)的密集程度。在頂點(diǎn)著色器中,網(wǎng)格 UV 與密度值相乘,使它們從 0 到 1 的范圍變?yōu)?0 到密度的范圍。假設(shè)密度設(shè)置為 30,這將使片元著色器中的?i.uv?輸入包含 0 到 30 的浮點(diǎn)值,對(duì)應(yīng)于正在渲染的網(wǎng)格的各個(gè)位置。
然后,片元著色器代碼僅使用 HLSL 的內(nèi)置?floor?函數(shù)獲取輸入坐標(biāo)的整數(shù)部分,并將其除以 2?;叵胍幌?,輸入坐標(biāo)是從 0 到 30 的數(shù)字;這使得它們都被“量化”為 0、0.5、1、1.5、2、2.5 等等的值。輸入坐標(biāo)的 x 和 y 分量都完成了此操作。
接下來(lái),我們將這些 x 和 y 坐標(biāo)相加(每個(gè)坐標(biāo)的可能值只有 0、0.5、1、1.5 等等),并且只使用另一個(gè)內(nèi)置的 HLSL 函數(shù)?frac?來(lái)獲取小數(shù)部分。結(jié)果只能是 0.0 或 0.5。然后,我們將它乘以 2 使其為 0.0 或 1.0,并輸出為顏色(這分別產(chǎn)生黑色或白色)。

三面紋理
對(duì)于復(fù)雜網(wǎng)格或程序化網(wǎng)格,不使用常規(guī) UV 坐標(biāo)對(duì)它們進(jìn)行紋理化,有時(shí)只需在三個(gè)主方向?qū)⒓y理“投影”到對(duì)象上即可。這稱(chēng)為“三面”紋理。這個(gè)想法是使用表面法線來(lái)加權(quán)三個(gè)紋理方向。下面是著色器:

光照計(jì)算
通常,當(dāng)您需要適用于 Unity 光照管線的著色器時(shí), 可以編寫(xiě)表面著色器。這樣會(huì)為您完成大部分“繁重的工作”, 您的著色器代碼只需要定義表面屬性。
但在某些情況下,您希望繞過(guò)標(biāo)準(zhǔn)表面著色器路徑; 或者是出于性能原因,您只想支持整個(gè)光照管線的某個(gè)有限子集, 或者您想要進(jìn)行“標(biāo)準(zhǔn)光照”以外的自定義。以下示例 將說(shuō)明如何從手動(dòng)編寫(xiě)的頂點(diǎn)和片元著色器獲取光照數(shù)據(jù)。 查看表面著色器生成的代碼(通過(guò)著色器檢視面板)也是一種 很好的學(xué)習(xí)資源。
簡(jiǎn)單的漫射光照
我們需要做的第一件事就是指出我們的著色器確實(shí)需要傳遞給它的光照信息。Unity 的渲染管線支持各種渲染方式;這里我們將使用默認(rèn)的前向渲染。
我們首先只支持一個(gè)方向光。Unity 中的前向渲染的工作方式是在一個(gè)名為?ForwardBase?的單個(gè)通道中渲染主方向光、環(huán)境光、光照貼圖和反射。在著色器中,通過(guò)添加以下通道標(biāo)簽來(lái)指示這一情況:__Tags {“LightMode”=“ForwardBase”}__。這將使方向光數(shù)據(jù)通過(guò)一些內(nèi)置變量傳入著色器。
以下著色器為每個(gè)頂點(diǎn)計(jì)算簡(jiǎn)單漫射光照,并使用單個(gè)主紋理:

帶環(huán)境光的漫射光照
上面的示例不考慮任何環(huán)境光照或光照探針。我們來(lái)解決這個(gè)問(wèn)題! 事實(shí)證明,我們可以通過(guò)添加一行代碼來(lái)實(shí)現(xiàn)這一目標(biāo)。環(huán)境光和光照探針數(shù)據(jù)都以球諧函數(shù)形式傳遞給著色器,__UnityCG.cginc__?include 文件?中的?ShadeSH9?函數(shù)在給定世界空間法線的情況下完成所有估算工作。

事實(shí)上,這個(gè)著色器開(kāi)始看起來(lái)非常類(lèi)似于內(nèi)置的舊版漫射著色器!
實(shí)現(xiàn)陰影投射
我們的著色器目前既不能接受也不能投射陰影。讓我們先實(shí)現(xiàn)陰影投射。
為了投射陰影,著色器必須在其任何子著色器或任何回退中具有?ShadowCaster?通道類(lèi)型。ShadowCaster 通道用于將對(duì)象渲染到陰影貼圖中,通常它非常簡(jiǎn)單:頂點(diǎn)著色器只需要估算頂點(diǎn)位置,片元著色器幾乎不執(zhí)行任何操作。陰影貼圖只是深度緩沖區(qū),因此即使是片元著色器輸出的顏色也無(wú)關(guān)緊要。
這意味著對(duì)于許多著色器,陰影投射物通道幾乎完全相同(除非對(duì)象具有基于自定義頂點(diǎn)著色器的變形,或者具有 Alpha 鏤空/半透明部分)。最簡(jiǎn)單的捕捉方法是使用著色器命令?UsePass:
但我們?cè)谶@里是為了學(xué)習(xí),所以讓我們通過(guò)“手動(dòng)”方式實(shí)現(xiàn)相同效果。為了縮短代碼長(zhǎng)度, 我們已經(jīng)將光照通道 (“ForwardBase”) 替換為僅執(zhí)行無(wú)紋理環(huán)境光的代碼。在它下面,有一個(gè)“ShadowCaster”通道,讓對(duì)象能夠支持陰影投射。
現(xiàn)在下方有一個(gè)平面,使用常規(guī)的內(nèi)置漫射著色器,因此我們可以看到 我們的陰影生效(請(qǐng)記住,我們當(dāng)前的著色器還不支持_接受_陰影!)。

我們使用了?#pragma multi_compile_shadowcaster?指令。這會(huì)導(dǎo)致著色器被編譯為多個(gè)變體,并為每個(gè)變體定義了不同的預(yù)處理器宏(有關(guān)詳細(xì)信息,請(qǐng)參閱?多個(gè)著色器變體頁(yè)面)。渲染到陰影貼圖時(shí),點(diǎn)光源與其他光源類(lèi)型需要著色器代碼略有不同,這就是需要此指令的原因。
接受陰影
要實(shí)現(xiàn)對(duì)接受陰影的支持,必須將基礎(chǔ)光照通道編譯成 若干變體,以正確處理“沒(méi)有陰影的方向光”和“有陰影的方向光”這兩種情況。#pragma multi_compile_fwdbase?指令便可執(zhí)行此操作(有關(guān)詳細(xì)信息,請(qǐng)參閱?多個(gè)著色器變體)。實(shí)際上它的功能還不止于此: 它還為不同的光照貼圖類(lèi)型編譯變體、實(shí)時(shí) GI 打開(kāi)或關(guān)閉等。目前我們不需要這些,所以我們將明確跳過(guò)這些變體。
然后,為了獲得實(shí)際的陰影計(jì)算,我們將?#include “AutoLight.cginc”?著色器的?include 文件?并使用該文件中的 SHADOW_COORDS、TRANSFER_SHADOW 和 SHADOW_ATTENUATION 宏。
下面是著色器:

看看,現(xiàn)在我們實(shí)現(xiàn)了陰影!
其他著色器示例
Fog
可在此處下載上文中顯示的示例(以?Unity 項(xiàng)目壓縮包的形式提供)。
示例文件:https://docs.unity.cn/cn/2019.4/uploads/Examples/UnityShaderDocExamples.zip