Unity3D Shader入門指南(一)

動(dòng)機(jī)
自己使用Unity3D也有一段時(shí)間了,但是很多時(shí)候是流于表面,更多地是把這個(gè)引擎簡單地用作腳本控制,而對更深入一些的層次幾乎沒有了解。雖然說Unity引擎設(shè)計(jì)的初衷就是創(chuàng)建簡單的不需要開發(fā)者操心的誰都能用的3D引擎,但是只是膚淺的使用,可能是無法達(dá)到隨心所欲的境地的,因此,這種狀況必須改變!從哪里開始呢,貌似有句話叫做會(huì)寫Shader的都是高手,于是,想大概看看從Shader開始能不能使自己到達(dá)的層次能再深入一些吧,再于是,有了這個(gè)系列(希望我能堅(jiān)持寫完它,雖然應(yīng)該會(huì)拖個(gè)半年左右)。
Unity3D的所有渲染工作都離不開著色器(Shader),如果你和我一樣最近開始對Shader編程比較感興趣的話,可能你和我有著同樣的困惑:如何開始?Unity3D提供了一些Shader的手冊和文檔(比如這里這里這里),但是一來內(nèi)容比較分散,二來學(xué)習(xí)階梯稍微陡峭了些。這對于像我這樣之前完全沒有接觸過有關(guān)內(nèi)容的新人來說是相當(dāng)不友好的。國內(nèi)外雖然也有一些Shader的介紹和心得,但是也同樣存在內(nèi)容分散的問題,很多教程前一章就只介紹了基本概念,接下來馬上就搬出一個(gè)超復(fù)雜的例子,對于很多基本的用法并沒有解釋。也許對于Shader熟練使用的開發(fā)者來說是沒有問題,但是我相信像我這樣的入門者也并不在少數(shù)。在多方尋覓無果后,我覺得有必要寫一份教程,來以一個(gè)入門者的角度介紹一些Shader開發(fā)的基本步驟。其實(shí)與其說是教程,倒不如說是一份自我總結(jié),希望能夠幫到有需要的人。
所以,本“教程”的對象是
總的來說是新接觸Shader開發(fā)的人:也許你知道什么是Shader,也會(huì)使用別人的Shader,但是僅限于知道一些基本的內(nèi)建Shader名字,從來沒有打開它們查看其源碼。
想要更多了解Shader和有需求要進(jìn)行Shader開發(fā)的開發(fā)者,但是之前并沒有Shader開發(fā)的經(jīng)驗(yàn)。

當(dāng)然,因?yàn)槲冶旧碓赟hader開發(fā)方面也是一個(gè)不折不扣的大菜鳥,本文很多內(nèi)容也只是在自己的理解加上一些可能不太靠譜的求證和總結(jié)。本文中的示例應(yīng)該會(huì)有更好的方式來實(shí)現(xiàn),因此您是高手并且恰巧路過的話,如果有好的方式來實(shí)現(xiàn)某些內(nèi)容,懇請您不吝留下評論,我會(huì)對本文進(jìn)行不斷更新和維護(hù)。
一些基本概念
Shader和Material
如果是進(jìn)行3D游戲開發(fā)的話,想必您對著兩個(gè)詞不會(huì)陌生。Shader(著色器)實(shí)際上就是一小段程序,它負(fù)責(zé)將輸入的Mesh(網(wǎng)格)以指定的方式和輸入的貼圖或者顏色等組合作用,然后輸出。繪圖單元可以依據(jù)這個(gè)輸出來將圖像繪制到屏幕上。輸入的貼圖或者顏色等,加上對應(yīng)的Shader,以及對Shader的特定的參數(shù)設(shè)置,將這些內(nèi)容(Shader及輸入?yún)?shù))打包存儲(chǔ)在一起,得到的就是一個(gè)Material(材質(zhì))。之后,我們便可以將材質(zhì)賦予合適的renderer(渲染器)來進(jìn)行渲染(輸出)了。
所以說Shader并沒有什么特別神奇的,它只是一段規(guī)定好輸入(顏色,貼圖等)和輸出(渲染器能夠讀懂的點(diǎn)和顏色的對應(yīng)關(guān)系)的程序。而Shader開發(fā)者要做的就是根據(jù)輸入,進(jìn)行計(jì)算變換,產(chǎn)生輸出而已。
Shader大體上可以分為兩類,簡單來說
表面著色器(Surface Shader) - 為你做了大部分的工作,只需要簡單的技巧即可實(shí)現(xiàn)很多不錯(cuò)的效果。類比卡片機(jī),上手以后不太需要很多努力就能拍出不錯(cuò)的效果。
片段著色器(Fragment Shader) - 可以做的事情更多,但是也比較難寫。使用片段著色器的主要目的是可以在比較低的層級上進(jìn)行更復(fù)雜(或者針對目標(biāo)設(shè)備更高效)的開發(fā)。

因?yàn)槭侨腴T文章,所以之后的介紹將主要集中在表面著色器上。
Shader程序的基本結(jié)構(gòu)

因?yàn)橹鞔a可以說專用性非常強(qiáng),因此人為地規(guī)定了它的基本結(jié)構(gòu)。一個(gè)普通的著色器的結(jié)構(gòu)應(yīng)該是這樣的:
一段Shader程序的結(jié)構(gòu)
一段Shader程序的結(jié)構(gòu)

首先是一些屬性定義,用來指定這段代碼將有哪些輸入。接下來是一個(gè)或者多個(gè)的子著色器,在實(shí)際運(yùn)行中,哪一個(gè)子著色器被使用是由運(yùn)行的平臺(tái)所決定的。子著色器是代碼的主體,每一個(gè)子著色器中包含一個(gè)或者多個(gè)的Pass。在計(jì)算著色時(shí),平臺(tái)先選擇最優(yōu)先可以使用的著色器,然后依次運(yùn)行其中的Pass,然后得到輸出的結(jié)果。最后指定一個(gè)回滾,用來處理所有Subshader都不能運(yùn)行的情況(比如目標(biāo)設(shè)備實(shí)在太老,所有Subshader中都有其不支持的特性)。
需要提前說明的是,在實(shí)際進(jìn)行表面著色器的開發(fā)時(shí),我們將直接在Subshader這個(gè)層次上寫代碼,系統(tǒng)將把我們的代碼編譯成若干個(gè)合適的Pass。廢話到此為止,下面讓我們真正實(shí)際進(jìn)入Shader的世界吧。

Hello Shader
百行文檔不如一個(gè)實(shí)例,下面給出一段簡單的Shader代碼,然后根據(jù)代碼來驗(yàn)證下上面說到的結(jié)構(gòu)和闡述一些基本的Shader語法。因?yàn)楸疚氖轻槍nity3D來寫Shader的,所以也使用Unity3D來演示吧。首先,新建一個(gè)Shader,可以在Project面板中找到,Create,選擇Shader,然后將其命名為Diffuse Texture


在Unity3D中新建一個(gè)Shader
在Unity3D中新建一個(gè)Shader

隨便用個(gè)文本編輯器打開剛才新建的Shader:
Shader "Custom/Diffuse Texture" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Lambert sampler2D _MainTex; struct Input { float2 uv_MainTex; }; void surf (Input IN, inout SurfaceOutput o) { half4 c = tex2D (_MainTex, IN.uv_MainTex); o.Albedo = c.rgb; o.Alpha = c.a; } ENDCG } FallBack "Diffuse"}

如果您之前沒怎么看過Shader代碼的話,估計(jì)細(xì)節(jié)上會(huì)看不太懂。但是有了上面基本結(jié)構(gòu)的介紹,您應(yīng)該可以識(shí)別出這個(gè)Shader的構(gòu)成,比如一個(gè)Properties部分,一個(gè)SubShader,以及一個(gè)FallBack。另外,第一行只是這個(gè)Shader的聲明并為其指定了一個(gè)名字,比如我們的實(shí)例Shader,你可以在材質(zhì)面板選擇Shader時(shí)在對應(yīng)的位置找到這個(gè)Shader。

在Unity3D中找到剛才新建的Shader
在Unity3D中找到剛才新建的Shader

接下來我們講逐句講解這個(gè)Shader,以期明了每一個(gè)語句的意義。
屬性
在Properties{}
中定義著色器屬性,在這里定義的屬性將被作為輸入提供給所有的子著色器。每一條屬性的定義的語法是這樣的:
_Name("Display Name", type) = defaultValue[{options}]

_Name - 屬性的名字,簡單說就是變量名,在之后整個(gè)Shader代碼中將使用這個(gè)名字來獲取該屬性的內(nèi)容
Display Name - 這個(gè)字符串將顯示在Unity的材質(zhì)編輯器中作為Shader的使用者可讀的內(nèi)容
type - 這個(gè)屬性的類型,可能的type所表示的內(nèi)容有以下幾種:Color - 一種顏色,由RGBA(紅綠藍(lán)和透明度)四個(gè)量來定義;
2D - 一張2的階數(shù)大小(256,512之類)的貼圖。這張貼圖將在采樣后被轉(zhuǎn)為對應(yīng)基于模型UV的每個(gè)像素的顏色,最終被顯示出來;
Rect - 一個(gè)非2階數(shù)大小的貼圖;
Cube - 即Cube map texture(立方體紋理),簡單說就是6張有聯(lián)系的2D貼圖的組合,主要用來做反射效果(比如天空盒和動(dòng)態(tài)反射),也會(huì)被轉(zhuǎn)換為對應(yīng)點(diǎn)的采樣;
Range(min, max) - 一個(gè)介于最小值和最大值之間的浮點(diǎn)數(shù),一般用來當(dāng)作調(diào)整Shader某些特性的參數(shù)(比如透明度渲染的截止值可以是從0至1的值等);
Float - 任意一個(gè)浮點(diǎn)數(shù);
Vector - 一個(gè)四維數(shù);

defaultValue 定義了這個(gè)屬性的默認(rèn)值,通過輸入一個(gè)符合格式的默認(rèn)值來指定對應(yīng)屬性的初始值(某些效果可能需要某些特定的參數(shù)值來達(dá)到需要的效果,雖然這些值可以在之后在進(jìn)行調(diào)整,但是如果默認(rèn)就指定為想要的值的話就省去了一個(gè)個(gè)調(diào)整的時(shí)間,方便很多)。Color - 以0~1定義的rgba顏色,比如(1,1,1,1);
2D/Rect/Cube - 對于貼圖來說,默認(rèn)值可以為一個(gè)代表默認(rèn)tint顏色的字符串,可以是空字符串或者"white","black","gray","bump"中的一個(gè)
Float,Range - 某個(gè)指定的浮點(diǎn)數(shù)
Vector - 一個(gè)4維數(shù),寫為 (x,y,z,w)

另外還有一個(gè){option},它只對2D,Rect或者Cube貼圖有關(guān),在寫輸入時(shí)我們最少要在貼圖之后寫一對什么都不含的空白的{},當(dāng)我們需要打開特定選項(xiàng)時(shí)可以把其寫在這對花括號內(nèi)。如果需要同時(shí)打開多個(gè)選項(xiàng),可以使用空白分隔。可能的選擇有ObjectLinear, EyeLinear, SphereMap, CubeReflect, CubeNormal中的一個(gè),這些都是OpenGL中TexGen的模式,具體的留到后面有機(jī)會(huì)再說。

所以,一組屬性的申明看起來也許會(huì)是這個(gè)樣子的
//Define a color with a default value of semi-transparent blue_MainColor ("Main Color", Color) = (0,0,1,0.5)//Define a texture with a default of white_Texture ("Texture", 2D) = "white" {}

現(xiàn)在看懂上面那段Shader(以及其他所有Shader)的Properties部分應(yīng)該不會(huì)有任何問題了。接下來就是SubShader部分了。
Tags
表面著色器可以被若干的標(biāo)簽(tags)所修飾,而硬件將通過判定這些標(biāo)簽來決定什么時(shí)候調(diào)用該著色器。比如我們的例子中SubShader的第一句
Tags { "RenderType"="Opaque" }

告訴了系統(tǒng)應(yīng)該在渲染非透明物體時(shí)調(diào)用我們。Unity定義了一些列這樣的渲染過程,與RenderType是Opaque相對應(yīng)的顯而易見的是"RenderType" = "Transparent"
,表示渲染含有透明效果的物體時(shí)調(diào)用。在這里Tags其實(shí)暗示了你的Shader輸出的是什么,如果輸出中都是非透明物體,那寫在Opaque里;如果想渲染透明或者半透明的像素,那應(yīng)該寫在Transparent中。
另外比較有用的標(biāo)簽還有"IgnoreProjector"="True"
(不被Projectors影響),"ForceNoShadowCasting"="True"
(從不產(chǎn)生陰影)以及"Queue"="xxx"
(指定渲染順序隊(duì)列)。這里想要著重說一下的是Queue這個(gè)標(biāo)簽,如果你使用Unity做過一些透明和不透明物體的混合的話,很可能已經(jīng)遇到過不透明物體無法呈現(xiàn)在透明物體之后的情況。這種情況很可能是由于Shader的渲染順序不正確導(dǎo)致的。Queue指定了物體的渲染順序,預(yù)定義的Queue有:
Background - 最早被調(diào)用的渲染,用來渲染天空盒或者背景
Geometry - 這是默認(rèn)值,用來渲染非透明物體(普通情況下,場景中的絕大多數(shù)物體應(yīng)該是非透明的)
AlphaTest - 用來渲染經(jīng)過Alpha Test的像素,單獨(dú)為AlphaTest設(shè)定一個(gè)Queue是出于對效率的考慮
Transparent - 以從后往前的順序渲染透明物體
Overlay - 用來渲染疊加的效果,是渲染的最后階段(比如鏡頭光暈等特效)

這些預(yù)定義的值本質(zhì)上是一組定義整數(shù),Background = 1000, Geometry = 2000, AlphaTest = 2450, Transparent = 3000,最后Overlay = 4000。在我們實(shí)際設(shè)置Queue值時(shí),不僅能使用上面的幾個(gè)預(yù)定義值,我們也可以指定自己的Queue值,寫成類似這樣:"Queue"="Transparent+100"
,表示一個(gè)在Transparent之后100的Queue上進(jìn)行調(diào)用。通過調(diào)整Queue值,我們可以確保某些物體一定在另一些物體之前或者之后渲染,這個(gè)技巧有時(shí)候很有用處。
LOD
LOD很簡單,它是Level of Detail的縮寫,在這里例子里我們指定了其為200(其實(shí)這是Unity的內(nèi)建Diffuse著色器的設(shè)定值)。這個(gè)數(shù)值決定了我們能用什么樣的Shader。在Unity的Quality Settings中我們可以設(shè)定允許的最大LOD,當(dāng)設(shè)定的LOD小于SubShader所指定的LOD時(shí),這個(gè)SubShader將不可用。Unity內(nèi)建Shader定義了一組LOD的數(shù)值,我們在實(shí)現(xiàn)自己的Shader的時(shí)候可以將其作為參考來設(shè)定自己的LOD數(shù)值,這樣在之后調(diào)整根據(jù)設(shè)備圖形性能來調(diào)整畫質(zhì)時(shí)可以進(jìn)行比較精確的控制。
VertexLit及其系列 = 100
Decal, Reflective VertexLit = 150
Diffuse = 200
Diffuse Detail, Reflective Bumped Unlit, Reflective Bumped VertexLit = 250
Bumped, Specular = 300
Bumped Specular = 400
Parallax = 500
Parallax Specular = 600

Shader本體
前面雜項(xiàng)說完了,終于可以開始看看最主要的部分了,也就是將輸入轉(zhuǎn)變?yōu)檩敵龅拇a部分。為了方便看,請容許我把上面的SubShader的主題部分抄寫一遍

CGPROGRAM
#pragma surface surf Lambert 
sampler2D _MainTex;
struct Input {
     float2 uv_MainTex;
};
void surf (Input IN, inout SurfaceOutput o) { 
        half4 c = tex2D (_MainTex, IN.uv_MainTex); 
        o.Albedo = c.rgb; 
        o.Alpha = c.a;
}
ENDCG

還是逐行來看,首先是CGPROGRAM。這是一個(gè)開始標(biāo)記,表明從這里開始是一段CG程序(我們在寫Unity的Shader時(shí)用的是Cg/HLSL語言)。最后一行的ENDCG與CGPROGRAM是對應(yīng)的,表明CG程序到此結(jié)束。
接下來是是一個(gè)編譯指令:#pragma surface surf Lambert
,它聲明了我們要寫一個(gè)表面Shader,并指定了光照模型。它的寫法是這樣的

pragma surface surfaceFunction lightModel [optionalparams]

surface - 聲明的是一個(gè)表面著色器
surfaceFunction - 著色器代碼的方法的名字
lightModel - 使用的光照模型。

所以在我們的例子中,我們聲明了一個(gè)表面著色器,實(shí)際的代碼在surf函數(shù)中(在下面能找到該函數(shù)),使用Lambert(也就是普通的diffuse)作為光照模型。
接下來一句sampler2D _MainTex;
,sampler2D是個(gè)啥?其實(shí)在CG中,sampler2D就是和texture所綁定的一個(gè)數(shù)據(jù)容器接口。等等..這個(gè)說法還是太復(fù)雜了,簡單理解的話,所謂加載以后的texture(貼圖)說白了不過是一塊內(nèi)存存儲(chǔ)的,使用了RGB(也許還有A)通道,且每個(gè)通道8bits的數(shù)據(jù)。而具體地想知道像素與坐標(biāo)的對應(yīng)關(guān)系,以及獲取這些數(shù)據(jù),我們總不能一次一次去自己計(jì)算內(nèi)存地址或者偏移,因此可以通過sampler2D來對貼圖進(jìn)行操作。更簡單地理解,sampler2D就是GLSL中的2D貼圖的類型,相應(yīng)的,還有sampler1D,sampler3D,samplerCube等等格式。
解釋通了sampler2D是什么之后,還需要解釋下為什么在這里需要一句對_MainTex
的聲明,之前我們不是已經(jīng)在Properties
里聲明過它是貼圖了么。答案是我們用來實(shí)例的這個(gè)shader其實(shí)是由兩個(gè)相對獨(dú)立的塊組成的,外層的屬性聲明,回滾等等是Unity可以直接使用和編譯的ShaderLab;而現(xiàn)在我們是在CGPROGRAM...ENDCG
這樣一個(gè)代碼塊中,這是一段CG程序。對于這段CG程序,要想訪問在Properties
中所定義的變量的話,必須使用和之前變量相同的名字進(jìn)行聲明。于是其實(shí)sampler2D _MainTex;
做的事情就是再次聲明并鏈接了_MainTex,使得接下來的CG程序能夠使用這個(gè)變量。
終于可以繼續(xù)了。接下來是一個(gè)struct結(jié)構(gòu)體。相信大家對于結(jié)構(gòu)體已經(jīng)很熟悉了,我們先跳過之,直接看下面的的surf函數(shù)。上面的#pragma段已經(jīng)指出了我們的著色器代碼的方法的名字叫做surf,那沒跑兒了,就是這段代碼是我們的著色器的工作核心。我們已經(jīng)說過不止一次,著色器就是給定了輸入,然后給出輸出進(jìn)行著色的代碼。CG規(guī)定了聲明為表面著色器的方法(就是我們這里的surf)的參數(shù)類型和名字,因此我們沒有權(quán)利決定surf的輸入輸出參數(shù)的類型,只能按照規(guī)定寫。這個(gè)規(guī)定就是第一個(gè)參數(shù)是一個(gè)Input結(jié)構(gòu),第二個(gè)參數(shù)是一個(gè)inout的SurfaceOutput結(jié)構(gòu)。
它們分別是什么呢?Input其實(shí)是需要我們?nèi)ザx的結(jié)構(gòu),這給我們提供了一個(gè)機(jī)會(huì),可以把所需要參與計(jì)算的數(shù)據(jù)都放到這個(gè)Input結(jié)構(gòu)中,傳入surf函數(shù)使用;SurfaceOutput是已經(jīng)定義好了里面類型輸出結(jié)構(gòu),但是一開始的時(shí)候內(nèi)容暫時(shí)是空白的,我們需要向里面填寫輸出,這樣就可以完成著色了。先仔細(xì)看看INPUT吧,現(xiàn)在可以跳回來看上面定義的INPUT結(jié)構(gòu)體了:
struct Input { float2 uv_MainTex;};

作為輸入的結(jié)構(gòu)體必須命名為Input,這個(gè)結(jié)構(gòu)體中定義了一個(gè)float2的變量…你沒看錯(cuò)我也沒打錯(cuò),就是float2,表示浮點(diǎn)數(shù)的float后面緊跟一個(gè)數(shù)字2,這又是什么意思呢?其實(shí)沒什么魔法,float和vec都可以在之后加入一個(gè)2到4的數(shù)字,來表示被打包在一起的2到4個(gè)同類型數(shù)。比如下面的這些定義:
//Define a 2d vector variablevec2 coordinate;//Define a color variablefloat4 color;//Multiply out a colorfloat3 multipliedColor = color.rgb * coordinate.x;

在訪問這些值時(shí),我們即可以只使用名稱來獲得整組值,也可以使用下標(biāo)的方式(比如.xyzw,.rgba或它們的部分比如.x等等)來獲得某個(gè)值。在這個(gè)例子里,我們聲明了一個(gè)叫做uv_MainTex
的包含兩個(gè)浮點(diǎn)數(shù)的變量。
如果你對3D開發(fā)稍有耳聞的話,一定不會(huì)對uv這兩個(gè)字母感到陌生。UV mapping的作用是將一個(gè)2D貼圖上的點(diǎn)按照一定規(guī)則映射到3D模型上,是3D渲染中最常見的一種頂點(diǎn)處理手段。在CG程序中,我們有這樣的約定,在一個(gè)貼圖變量(在我們例子中是_MainTex
)之前加上uv兩個(gè)字母,就代表提取它的uv值(其實(shí)就是兩個(gè)代表貼圖上點(diǎn)的二維坐標(biāo) )。我們之后就可以在surf程序中直接通過訪問uv_MainTex來取得這張貼圖當(dāng)前需要計(jì)算的點(diǎn)的坐標(biāo)值了。
如果你堅(jiān)持看到這里了,那要恭喜你,因?yàn)殡x最后成功讀完一個(gè)Shader只有一步之遙。我們回到surf函數(shù),它的兩有參數(shù),第一個(gè)是Input,我們已經(jīng)明白了:在計(jì)算輸出時(shí)Shader會(huì)多次調(diào)用surf函數(shù),每次給入一個(gè)貼圖上的點(diǎn)坐標(biāo),來計(jì)算輸出。第二個(gè)參數(shù)是一個(gè)可寫的SurfaceOutput,SurfaceOutput是預(yù)定義的輸出結(jié)構(gòu),我們的surf函數(shù)的目標(biāo)就是根據(jù)輸入把這個(gè)輸出結(jié)構(gòu)填上。SurfaceOutput結(jié)構(gòu)體的定義如下
struct SurfaceOutput { half3 Albedo; //像素的顏色 half3 Normal; //像素的法向值 half3 Emission; //像素的發(fā)散顏色 half Specular; //像素的鏡面高光 half Gloss; //像素的發(fā)光強(qiáng)度 half Alpha; //像素的透明度};

這里的half和我們常見float與double類似,都表示浮點(diǎn)數(shù),只不過精度不一樣。也許你很熟悉單精度浮點(diǎn)數(shù)(float或者single)和雙精度浮點(diǎn)數(shù)(double),這里的half指的是半精度浮點(diǎn)數(shù),精度最低,運(yùn)算性能相對比高精度浮點(diǎn)數(shù)高一些,因此被大量使用。
在例子中,我們做的事情非常簡單:
half4 c = tex2D (_MainTex, IN.uv_MainTex);o.Albedo = c.rgb;o.Alpha = c.a;

這里用到了一個(gè)tex2d
函數(shù),這是CG程序中用來在一張貼圖中對一個(gè)點(diǎn)進(jìn)行采樣的方法,返回一個(gè)float4。這里對_MainTex在輸入點(diǎn)上進(jìn)行了采樣,并將其顏色的rbg值賦予了輸出的像素顏色,將a值賦予透明度。于是,著色器就明白了應(yīng)當(dāng)怎樣工作:即找到貼圖上對應(yīng)的uv點(diǎn),直接使用顏色信息來進(jìn)行著色,over。
接下來...
我想現(xiàn)在你已經(jīng)能讀懂一些最簡單的Shader了,接下來我推薦的是參考Unity的Surface Shader Examples多接觸一些各種各樣的基本Shader。在這篇教程的基礎(chǔ)上,配合一些google的工作,完全看懂這個(gè)shader示例頁面應(yīng)該不成問題。如果能做到無壓力看懂,那說明你已經(jīng)有良好的基礎(chǔ)可以前進(jìn)到Shader的更深的層次了(也許等不到我的下一篇教程就可以自己開始動(dòng)手寫些效果了);如果暫時(shí)還是有困難,那也沒有關(guān)系,Shader學(xué)習(xí)絕對是一個(gè)漸進(jìn)的過程,因?yàn)橛泻芏嗉s定和常用技巧,多積累和實(shí)踐自然會(huì)進(jìn)步并掌握。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,737評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內(nèi)容