日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

如何快速成长为图形学工程师

發(fā)布時(shí)間:2023/12/14 编程问答 43 豆豆
生活随笔 收集整理的這篇文章主要介紹了 如何快速成长为图形学工程师 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.


本文來自作者?姜雪偉??GitChat?上分享 「如何快速成長為圖形學(xué)工程師」,閱讀原文查看交流實(shí)錄。

文末高能

編輯 | 哈比

目前 IT 市場出現(xiàn)了各路諸侯爭霸局面,從大的方向說分為三類:PC 端、移動(dòng)端、VR/AR,從細(xì)分領(lǐng)域來說有 MMO 端游、單機(jī)端游、MMO 移動(dòng)手游、單機(jī)手游、VR/AR、PC 端頁游、移動(dòng)端頁游等等。

隨著硬件的提升,玩家對產(chǎn)品的品質(zhì)要求越來越高,想提升品質(zhì)就需要 GPU 渲染,換句話說就是離不開圖形學(xué)渲染,涉及到的圖形渲染庫有 DX、OpenGL、OpenGLES、WebGL。

當(dāng)然實(shí)現(xiàn)渲染技術(shù)就需要對 GPU 編程,也就是我們通常說的 Shader 編程。

另外,IT 市場需求也越來越大,水漲船高,薪資也比普通的程序員高很多,通過招聘網(wǎng)站就可以看出,目前這方面的技術(shù)人員太少了,很多客戶端程序員或者獨(dú)立游戲開發(fā)者也想從事圖形學(xué)渲染,但是又感覺自己無處下手,不知道從何入手。

很多人都感覺圖形學(xué)高深莫測,有種恐懼感。

本篇課程就為了幫助你快速入手,快速掌握圖形學(xué)編程的一些知識點(diǎn)和一些編程技巧,這樣可以在比較短的時(shí)間內(nèi)成長為圖形學(xué)工程師,這也需要開發(fā)者自己的努力。

市場上成熟的引擎主要是 Unity、UE4,二者都提供了強(qiáng)大的渲染能力,作為新手如何才能快速的掌握圖形學(xué)編程呢,下面我們從幾個(gè)方面來分享:

一、圖像處理

Shader 編程其實(shí)就是對圖像進(jìn)行編程,常見的圖像格式有:jpg、png、tga、tga、bmp 等等。

作為圖形學(xué)開發(fā)者首要的事情是搞清楚它們的存儲(chǔ)格式,每種圖像格式它包括很多信息,當(dāng)然主要還是顏色的存儲(chǔ):rgb 或者說 rgba,另外圖像的存儲(chǔ)是按照矩陣的方式,如下圖所示:

如果有 A 通道就表明這個(gè)圖像可以有透明效果,R、G、B 每個(gè)分量一般是用一個(gè)字節(jié)(8 位)來表示,所以圖(1)中每個(gè)像素大小就是3*8=24位圖,而圖(2)中每個(gè)像素大小是4*8=32位。

圖像是二維數(shù)據(jù),數(shù)據(jù)在內(nèi)存中只能一維存儲(chǔ),二維轉(zhuǎn)一維有不同的對應(yīng)方式,比較常見的只有兩種方式:按像素 “行排列” 從上往下或者從下往上。

不同圖形庫中每個(gè)像素點(diǎn)中 RGBA 的排序順序可能不一樣,上面說過像素一般會(huì)有 RGB 或 RGBA 四個(gè)分量,那么在內(nèi)存中 RGB 的排列就多種情況,跟排列組合類似,不過一般只會(huì)有 RGB、BGR、RGBA、RGBA、BGRA 這幾種排列據(jù), 絕大多數(shù)圖形庫或環(huán)境是 BGR/BGRA 排列。

如果將圖像原始格式直接存儲(chǔ)到文件中將會(huì)非常大,比如一個(gè)5000*5000?24 位圖,所占文件大小為5000*5000*3字節(jié) =71.5MB, 其大小非常可觀。

如果用 zip 或 rar 之類的通用算法來壓縮像素?cái)?shù)據(jù),得到的壓縮比例通常不會(huì)太高,因?yàn)檫@些壓縮算法沒有針對圖像數(shù)據(jù)結(jié)構(gòu)進(jìn)行特殊處理。

于是就有了 jpeg、png 等格式,同樣是圖像壓縮算法 jpeg 和 png 也有不同的適用場景,給讀者看看圖像在內(nèi)存中的存儲(chǔ),如下圖所示:

jpeg、png 文件之于圖像,就相當(dāng)于 zip、rar 格式之于普通文件(用 zip、rar 格式對普通文件進(jìn)行壓縮)。

這個(gè)跟我們的 Unity 打包 Assetbundle 與 zip 類似,二者采用相同的壓縮方式。另外 bmp 是無壓縮的圖像格式,在這里以 Bmp 為例,介紹一下 Bmp 格式的圖片存儲(chǔ)格式。

bmp 格式?jīng)]有壓縮像素格式,存儲(chǔ)在文件中時(shí)先有文件頭、再圖像頭、后面就都是像素?cái)?shù)據(jù)了,上下顛倒存儲(chǔ)。

用 windows 自帶的 mspaint 工具保存 bmp 格式時(shí),可以發(fā)現(xiàn)有四種 bmp 可供選擇:

  • 單色: 一個(gè)像素只占一位,要么是 0,要么是 1,所以只能存儲(chǔ)黑白信息;

  • 16 色位圖: 一個(gè)像素 4 位,有 16 種顏色可選;

  • 256 色位圖: 一個(gè)像素 8 位,有 256 種顏色可選;

  • 24 位位圖: 就是圖 (1) 所示的位圖,顏色可有 2^24 種可選,對于人眼來說完全足夠了。

這里為了簡單起見,只詳細(xì)討論最常見的 24 位圖的 bmp 格式。

現(xiàn)在來看其文件頭和圖片格式頭的結(jié)構(gòu):

在這里為了能讓讀者更好的理解圖像的讀取方式,在此把圖像處理的文件頭和圖片頭代碼展示一下,網(wǎng)上資源很多:

? ?//bmp 文件頭typedef struct tagBITMAPFILEHEADER {unsigned short bfType; ? ? ?// 19778,必須是 BM 字符串,對應(yīng)的十六進(jìn)制為 0x4d42, 十進(jìn)制為 19778unsigned int bfSize; ? ? ? ?// 文件大小unsigned short bfReserved1; // 0unsigned short bfReserved2; // 0unsigned int bfOffBits; ? ? // 從文件頭到像素?cái)?shù)據(jù)的偏移,也就是這兩個(gè)結(jié)構(gòu)體的大小之和} BITMAPFILEHEADER;//bmp 圖像頭 typedef struct tagBITMAPINFOHEADER {unsigned int biSize; ? ? ? ?// 此結(jié)構(gòu)體的大小int biWidth; ? ? ? ? ? ? ? ?// 圖像的寬int biHeight; ? ? ? ? ? ? ? // 圖像的高unsigned short biPlanes; ? ?// 1unsigned short biBitCount; ?// 24unsigned int biCompression; // 0unsigned int biSizeImage; ? // 像素?cái)?shù)據(jù)所占大小 , 這個(gè)值應(yīng)該等于上面文件頭結(jié)構(gòu)中 bfSize-bfOffBitsint biXPelsPerMeter; ? ? ? ?// 0int biYPelsPerMeter; ? ? ? ?// 0unsigned int biClrUsed; ? ? // 0 unsigned int biClrImportant;// 0 } BITMAPINFOHEADER;

Bmp 結(jié)構(gòu)體,作為讀者了解一下即可,知道是咋回事?另外 png 是一種無損壓縮格式, 壓縮大概是用行程編碼算法,png 可以有透明效果。

png 比較適合適量圖,幾何圖。 比如本文中出現(xiàn)的這些圖都是用 png 保存。

通過對圖像格式的了解,可以幫助大家揭開一個(gè)謎團(tuán)就是說,無論哪種壓縮格式的圖片,加載到內(nèi)存中后,它們都會(huì)被解壓展開,這也是為什么我們的圖片大小幾十 K,加載到內(nèi)存后是幾 M 的原因。

總結(jié)

之所以給讀者介紹關(guān)于圖像的結(jié)構(gòu)和加載方式,是因?yàn)槲覀儗?GPU 編程核心就是對圖像的處理,只有掌握了它們,我們才可以根據(jù)策劃的需求或者是美術(shù)的需求做出各種渲染效果,比如在材質(zhì)中剔除黑色,進(jìn)行反射,折射,以及高光、法線等的渲染。

即使是后處理渲染又稱為濾鏡的渲染也是對圖片像素的處理,與材質(zhì)渲染不同的是它是對整個(gè)場景的渲染,因?yàn)橛螒蜻\(yùn)行也是通過一幀一幀渲染的圖片播放的,后處理就是對這些圖片進(jìn)行再渲染。

常用的后出比如:Bloom,Blur,HDR,PSSM 等等。所以關(guān)于圖片的存儲(chǔ)結(jié)構(gòu)大家一定要掌握,這樣你在學(xué)習(xí) Shader 編程時(shí)理解的就更深入了。

二、渲染流程

不論是 Unity 引擎還是 UE4 引擎,他們都有自己的渲染流程,它們都是從固定流水線的基礎(chǔ)上發(fā)展起來的可編程流水線。

我們就先從固定流水線講起,作為圖形學(xué)開發(fā)是必須要掌握的,因?yàn)楣潭魉€是圖形學(xué)渲染的基礎(chǔ),它們的核心是各個(gè)空間之間的矩陣變換。

這個(gè)我們在 Shader 編程時(shí)經(jīng)常遇到,比如我們經(jīng)常使用的UNITY_MATRIX_MVP。其實(shí),它就是將固定流水線中的矩陣運(yùn)算轉(zhuǎn)移到了 GPU 中進(jìn)行了。

游戲開發(fā)早期,在 3D 游戲開發(fā)的初級階段,顯卡功能還沒有現(xiàn)在這么強(qiáng)大,3D 游戲開發(fā)都是采用的固定流水線,也可以說固定流水線是 3D 游戲引擎開發(fā)的最基本的底層核心。

現(xiàn)在引擎開發(fā)采用的可編程流水線也是在固定流水線的基礎(chǔ)上發(fā)展起來的,有人可能會(huì)問,3D 固定流水線的作用是什么?

通俗的講,固定流水線的原理就是將 3D 圖形轉(zhuǎn)換成屏幕上的 2D 圖像顯示的過程,在此過程中都是通過 CPU 處理的,以前那些比較老的 3D 游戲都是按照這個(gè)原理制作的。

實(shí)現(xiàn)固定流水線有幾個(gè)步驟?

技術(shù)來源于生活,這句話是真理,我就用現(xiàn)實(shí)生活中的案例給大家介紹一下固定流水線。平時(shí)我們經(jīng)常用攝像機(jī)拍照片,比如我們要拍一個(gè)人物木偶,人物木偶首先要做出來,它是在工廠通過工人的機(jī)器制做出來的。

木偶在出廠前對于外界是看不到的,工廠制作完成后,將其拿到商場里面擺出來才能看到,然后我們用攝像機(jī)拍照木偶,因?yàn)閿z像機(jī)有視角和遠(yuǎn)近距離,視角外的物體拍不到的,距離遠(yuǎn)的會(huì)做模糊處理。

對準(zhǔn)人物木偶讓其在攝像機(jī)的正中位置,可以看到在攝像機(jī)的鏡頭上一個(gè)人物木偶就出現(xiàn)了,人物木偶的顏色也在鏡頭上顯示,這整個(gè)過程簡單一句話概括就是一個(gè) 3D 圖形在 2D 屏幕上的成像。

說這些的主要目的是為了讓大家能夠用自己的語言表述固定流水線。

以前在公司招聘 3D 程序員時(shí),固定流水線是必須要問的問題,面試的大部分人都回答不上或者回答的不完整,靠的是死記硬背,沒有真正領(lǐng)會(huì)其精髓。

流水線前加了兩個(gè)字 “固定” 就是告訴大家它是按照固定的流程實(shí)現(xiàn)的。將上面所說的流程轉(zhuǎn)化成程序語言就是固定流水線。固定流水線的流程如下圖所示:

物理空間就是我們說的模型自身的空間,比如美術(shù)制作的序列幀動(dòng)畫或者是 3D 模型,因?yàn)檫@些制作的序列幀動(dòng)畫或者模型也有自己的朝向,大小,這些都是它們自己擁有的與外界無關(guān),這就是物理空間也稱為局部空間。

將這些美術(shù)制作好的對象放置到游戲編輯器中比如 Unity 編輯器,編輯器所在的空間就是世界空間,比如,我們可以在編輯器中對模型進(jìn)行 Transfrom 組件中的 position、scale、Rotation 進(jìn)行設(shè)置,這些設(shè)置就是在世界空間中完成的。

它是相對于世界中的物體進(jìn)行設(shè)置的,比如我們經(jīng)常使用的 Transform.position 這個(gè)就是設(shè)置的世界空間的位置,而如果使用 Transform.localposition 就是設(shè)置的物體局部空間的位置,這種一般應(yīng)用到物體的子孩子進(jìn)行設(shè)置。

比如如果我們要在 Game 視圖中看到場景,我們就需要設(shè)置 Camera 相機(jī),這樣我們才能看到我們在場景中擺放的物體,這個(gè)就是可視空間,如下圖所示:

當(dāng)然并不是我們所有擺放的物體都能看到,有些是看不到的。

看不到的物體就被相機(jī)裁剪掉了,這會(huì)涉及到對物體進(jìn)行矩陣投影裁剪變換,因?yàn)槲覀円眉舻舨辉僖暱谥械奈矬w,我們需要將其投影變換,以及做遮擋剔除就是設(shè)置物體的前后關(guān)系。

如下圖所示:

最后將其相機(jī)中的物體在屏幕上顯示出來,效果如下圖所示:

各個(gè)空間之間的變換是通過矩陣變換也就是物體與矩陣相乘得到的,3D 中的物體是由很多點(diǎn)組成的。

這些點(diǎn)是三維的,在場景中做變換就是對 3D 物體中的點(diǎn)做矩陣運(yùn)算,但是開發(fā)者在操作時(shí)為什么沒有用到矩陣變換,這是因?yàn)橐娴讓右呀?jīng)為我們封裝好了,我們不需要再進(jìn)行矩陣變換而只需要對其進(jìn)行傳值操作。

為了方便讀者理解,現(xiàn)把 Unity 引擎底層關(guān)于矩陣計(jì)算的部分代碼給讀者展示如下,希望幫助讀者理解關(guān)于矩陣的換算:

? ?TransformType Transform::CalculateTransformMatrix (Matrix4x4f& transform) const {//@TODO: Does this give any performance gain??Prefetch(m_CachedTransformMatrix.GetPtr());if (m_HasCachedTransformMatrix){CopyMatrix(m_CachedTransformMatrix.GetPtr(), transform.GetPtr());return (TransformType)m_CachedTransformType;}const Transform* transforms[32];int transformCount = 1;TransformType type = (TransformType)0;Matrix4x4f temp;{// collect all transform that need CachedTransformMatrix updatetransforms[0] = this;Transform* parent = NULL;for (parent = GetParent(); parent != NULL && !parent->m_HasCachedTransformMatrix; parent = parent->GetParent()){transforms[transformCount++] = parent;// reached maximum of transforms that we can calculate - fallback to old methodif (transformCount == 31){parent = parent->GetParent();if (parent){type = parent->**CalculateTransformMatrixIterative**(temp);Assert(parent->m_HasCachedTransformMatrix);}break;}}// storing parent of last transform (can be null), the transform itself won't be updatedtransforms[transformCount] = parent;Assert(transformCount <= 31);} ? ? ? ?// iterate transforms from lowest parentfor (int i = transformCount - 1; i >= 0; --i){const Transform* t = transforms[i];const Transform* parent = transforms[i + 1];if (parent){Assert(parent->m_HasCachedTransformMatrix);// Build the local transform into temptype |= t->CalculateLocalTransformMatrix(temp); ? ? ? ? ? ? ? ?type |= (TransformType)parent->m_CachedTransformType;**MultiplyMatrices4x4**(&parent->m_CachedTransformMatrix, &temp, &t->m_CachedTransformMatrix);}else{// Build the local transform into m_CachedTransformMatrixtype |= t->CalculateLocalTransformMatrix(t->m_CachedTransformMatrix);}// store cached transformt->m_CachedTransformType = UpdateTransformType(type, t);t->m_HasCachedTransformMatrix = true;}Assert(m_HasCachedTransformMatrix);CopyMatrix(m_CachedTransformMatrix.GetPtr(), transform.GetPtr());return (TransformType)m_CachedTransformType; }

以上是引擎底層關(guān)于矩陣的運(yùn)算實(shí)現(xiàn),下面再給讀者介紹關(guān)于視口變換的案例。

我們在自己實(shí)現(xiàn)項(xiàng)目時(shí)遇到的問題就是使用下面的處理方式把問題解決了,我們的需求是將相機(jī)獲取到圖片在屏幕上某個(gè)位置顯示出來。

這就要實(shí)現(xiàn)從局部坐標(biāo)到世界坐標(biāo),再到屏幕坐標(biāo)的換算,實(shí)質(zhì)上就是對圖片的像素進(jìn)行具體轉(zhuǎn)換操作:

再解釋一下,上圖中 clipSpace 是相機(jī)裁剪完成后,讀者如果使用過 Unity3D 引擎知道,它的視口大小是 0,0,1,1,這個(gè)是被標(biāo)準(zhǔn)化處理過了,也就是圖中的 Normalized Device Space。

但是屏幕上的坐標(biāo)數(shù)值不是用 0,1 表示的,也就是圖中的 Window Space,我們知道屏幕的大小尺寸都是用像素表示的,比如: 640?480,720?640,1280 * 720 等,這些像素與 0,1 之間關(guān)系是一一對應(yīng)的,對應(yīng)公式如下所示:

公式中的參數(shù) (xw,yw) 是屏幕坐標(biāo),(x, y, width, height) 是傳入的參數(shù),(xnd, ynd) 是投影之后經(jīng)歸一化之后的點(diǎn),這樣我們就可以計(jì)算出屏幕上的點(diǎn)坐標(biāo)。

如果讀者對矩陣變換不理解可以查看《線性代數(shù)》和《3D 數(shù)學(xué)基礎(chǔ):圖形與游戲開發(fā)》這兩本書。

費(fèi)這么多筆墨給讀者介紹固定流水線以及矩陣變換,主要是為我們下面介紹的可編程流水線做鋪墊。

可編程流水線主要是針對 GPU 編程的,換句話說就是將固定流水線的矩陣變換放到 GPU 中進(jìn)行計(jì)算,這樣可以徹底解放 CPU 用于處理其他事情,提升效率。

這就涉及到 GPU 編程了,GPU 編程語言目前有 3 種主流語言:
–基于 OpenGL 的 GLSL(OpenGLShading Language,也稱為 GLslang)
–基于 Direct3D 的 HLSL(High Level ShadingLanguage)語言,
–NVIDIA 公司的 Cg (C for Graphic)語言。

跨平臺(tái)的 Shader 編程語言是 GLSL 和 CG,二者的語法跟 C 語言很類似,可編程流水線的執(zhí)行流程圖如下:

從上圖可以看出在 GPU 中——圖中黃色的部分,主要負(fù)責(zé)頂點(diǎn)坐標(biāo)變換、光照、裁剪、投影以及屏幕映射,該階段基于 GPU 進(jìn)行運(yùn)算。

在該階段的末端得到了經(jīng)過變換和投影之后的頂點(diǎn)坐標(biāo)、顏色、以及紋理坐標(biāo)。

當(dāng)然 GPU 并不是只是簡單的執(zhí)行這些,因?yàn)?GPU 是多線程的它可以做很多的工作,GPU 比較擅長做的就是關(guān)于矩陣的運(yùn)算。

說到 GPU 編程,不得不說頂點(diǎn)著色器和片段著色器,這個(gè)也是開發(fā)者要重點(diǎn)掌握的,再看看頂點(diǎn)著色器和片段著色器在 GPU 中的執(zhí)行流程,如下圖所示:

上圖主要是實(shí)現(xiàn)了頂點(diǎn)著色程序從 GPU 前端模塊(寄存器)中提取圖元信息(頂點(diǎn)位置、法向量、紋理坐標(biāo)等),并完成頂點(diǎn)坐標(biāo)空間轉(zhuǎn)換、法向量空間轉(zhuǎn)換、光照計(jì)算等操作,最后將計(jì)算好的數(shù)據(jù)傳送到指定寄存器中。

這就是說 GPU 也有自己的寄存器,我們在 Shader 中聲明的變量它是存儲(chǔ)在 GPU 的寄存器中。

片斷著色程序從寄存器中獲取需要的數(shù)據(jù),通常為 “紋理坐標(biāo)、光照信息等”,并根據(jù)這些信息以及從應(yīng)用程序傳遞的紋理信息(如果有的話)進(jìn)行每個(gè)片斷的顏色計(jì)算。

這個(gè)就涉及到圖片的像素計(jì)算了,也是開發(fā)者要掌握的。最后將處理后的數(shù)據(jù)送光柵操作模塊完成。

另外,我們自己所寫的 Shader,程序是如何使用的?換句話說,我們的 Shader 屬于一種特殊的腳本,程序加載它并對它進(jìn)行解釋,最后通過接口將其輸送到 GPU 中處理。

這個(gè)處理過程是引擎底層實(shí)現(xiàn)的,為了讓讀者清楚,在此通過一部分核心代碼給讀者展示引擎是通過加載讀取 Shader 并將其傳輸給 GPU 中處理。

現(xiàn)在比較流行的 H5 游戲,它使用的渲染庫是 WebGL,WebGL 提供了相應(yīng)的接口,用于加載已有的 Shader,當(dāng)然 OpenGL,DX 都提供了相應(yīng)的接口。

下面我們先定義頂點(diǎn)著色器和片段著色器腳本,簡單的舉個(gè)例子:

? ?attribute vec3 position; ? uniform ? mat4 mvpMatrix; ?void main(void){ ?gl_Position = mvpMatrix * vec4(position, 1.0); ? }

這里用到了一個(gè) attribute 變量和一個(gè) uniform 變量,用于在 Shader 中聲明變量,這個(gè)是原生態(tài)的 Shader 腳本與 Unity 的是不一樣的。

變量 position 的類型是 vec3,表示的是一個(gè) 3 維向量,里面是頂點(diǎn)的位置,向量的三個(gè)元素分別是 X,Y,Z 坐標(biāo),另一個(gè) uniform 聲明的變量 mvpMatrix,類型是 mat4,它表示的是一個(gè) 4x4 的方陣。

它是模型,視圖,投影的各個(gè)變換矩陣結(jié)合后的矩陣。這次的頂點(diǎn)著色器,只是利用坐標(biāo)變換矩陣來變換頂點(diǎn)的坐標(biāo)位置,使用乘法運(yùn)算,頂點(diǎn)著色器得到的結(jié)果將傳遞給片段著色器。

為了讓 position 和矩陣相乘,使用 vec4 先將其變成一個(gè) 4 維的向量,然后相乘,最后將計(jì)算結(jié)果代入到 gl_Position,頂點(diǎn)著色器的處理結(jié)束。

接著說片段著色器,這次,繪制的模型是一個(gè)簡單的三角形,先不進(jìn)行著色,只是使用白色來填充。

所以,片段著色器中的處理,就只是將白色信息傳給 gl_FragColor 中。下面是片段著色器的代碼。

? ?void main(void){ ?gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); ? }

關(guān)于顏色,基本上使用 vec3 或者 vec4 的情況比較多。因?yàn)橐话憔褪鞘褂?RGB 或者 RGBA,需要 3~4 個(gè)元素。

這一次使用的 vec4 是所有的參數(shù)都是 1.0 的向量,顏色是白色[紅,綠,藍(lán),不透明度的各元素都是最大=白色]。

接下來,我們看頂點(diǎn)著色器和片段著色器的運(yùn)行過程,也就是引擎內(nèi)部的實(shí)現(xiàn)過程,我們編寫 Shader 不僅要知道咋寫?還要清楚引擎內(nèi)部是咋調(diào)用的,做到知其然,知其所以然。

Shader 的編譯也不需要什么特別的編譯器,只需要調(diào)用 WebGL 內(nèi)部的接口函數(shù)就可以進(jìn)行編譯了。

從著色器的編譯,到實(shí)際著色器的生成這一連串的流程,都在一個(gè)函數(shù)中來完成。下面是這個(gè)函數(shù)的代碼:

? function create_shader(id){ ?// 用來保存著色器的變量 ?var shader; ?// 根據(jù) id 從 HTML 中獲取指定的 script 標(biāo)簽 ?var scriptElement = document.getElementById(id); ?// 如果指定的 script 標(biāo)簽不存在,則返回 ?if(!scriptElement){return;} ?// 判斷 script 標(biāo)簽的 type 屬性 ?switch(scriptElement.type){ ?// 頂點(diǎn)著色器 ?case 'x-vertex': ?shader = gl.createShader(gl.VERTEX_SHADER); ?break; ?// 片段著色器 ?case 'x-fragment': ?shader = gl.createShader(gl.FRAGMENT_SHADER); ?break; ?default : ?return; ?} ?// 將標(biāo)簽中的代碼分配給生成的著色器 ?gl.shaderSource(shader, scriptElement.text); ?// 編譯著色器 ?gl.compileShader(shader); ?// 判斷一下著色器是否編譯成功 ?if(gl.getShaderParameter(shader, gl.COMPILE_STATUS)){ ?// 編譯成功,則返回著色器 ?return shader; ?}else{ ?// 編譯失敗,彈出錯(cuò)誤消息 ?alert(gl.getShaderInfoLog(shader)); ?} ? }

簡單的介紹一下上述代碼,重點(diǎn)的函數(shù)都加了注釋,將代碼分配給生成的著色器的時(shí)候,使用的是 shaderSource 函數(shù),參數(shù)有兩個(gè),第一個(gè)參數(shù)是著色器對象,第二個(gè)參數(shù)是著色器的代碼。

這時(shí)候,只是把著色器的代碼分配給了著色器,Shader 編譯的時(shí)候,使用的是 compileShader 函數(shù),將著色器對象作為參數(shù)傳給這個(gè)函數(shù),這樣就可以將著色器在引擎中進(jìn)行編譯了。

這個(gè)自定義函數(shù),無論是頂點(diǎn)著色器還是片段著色器,都可以進(jìn)行編譯讀取。實(shí)際上,頂點(diǎn)著色器和片段著色器的處理不同的地方就是 createShader 函數(shù),其他地方是完全一樣的。

從頂點(diǎn)著色器向片段著色器中傳遞數(shù)據(jù),其實(shí),實(shí)現(xiàn)的就是從一個(gè)著色器向另一個(gè)著色器傳遞數(shù)據(jù)的,不是別的,就是程序?qū)ο蟆?/p>

程序?qū)ο笫枪芾眄旤c(diǎn)著色器和片段著色器,或者 WebGL 程序和各個(gè)著色器之間進(jìn)行數(shù)據(jù)的互相通信的重要的對象。

那么,生成程序?qū)ο?#xff0c;并把著色器傳給程序?qū)ο?#xff0c;然后連接著色器,將這些處理函數(shù)化,關(guān)于程序?qū)ο蟮膶?shí)現(xiàn)是通過調(diào)用函數(shù)接口實(shí)現(xiàn)的,代碼如下:

? ?function create_program(vs, fs){ ?// 程序?qū)ο蟮纳??var program = gl.createProgram(); ?// 向程序?qū)ο罄锓峙渲??gl.attachShader(program, vs); ?gl.attachShader(program, fs); ?// 將著色器連接 ?gl.linkProgram(program); ?// 判斷著色器的連接是否成功 ?if(gl.getProgramParameter(program, gl.LINK_STATUS)){ ?// 成功的話,將程序?qū)ο笤O(shè)置為有效 ?gl.useProgram(program); ?// 返回程序?qū)ο??return program; ?}else{ ?// 如果失敗,彈出錯(cuò)誤信息 ?alert(gl.getProgramInfoLog(program)); ?} ? }

這個(gè)函數(shù)接收頂點(diǎn)著色器和片段著色器兩個(gè)參數(shù),首先生成程序?qū)ο?#xff0c;分配著色器,生成著色器的時(shí)候,使用 WebGL 中的函 createProgram。

將著色器分配給程序?qū)ο蟮臅r(shí)候使用函數(shù)接口 attachShader,attachShader 函數(shù)的第一個(gè)參數(shù)是程序?qū)ο?#xff0c;第二個(gè)參數(shù)是著色器。

著色器分配結(jié)束后,根據(jù)程序?qū)ο?#xff0c;要連接兩個(gè)著色器,這時(shí)候使用 linkProgram 函數(shù),參數(shù)就是程序?qū)ο?。再后面就是賦值了,下面語句:

? ?var projMat = getPerspectiveProjection(30, 16 / 9, 1, 100);var viewMat = lookAt(6, 6, 14, 0, 0, 0, 0, 1, 0);var mvpMat = multiMatrix44(projMat, viewMat);var u_MvpMatrix = gl.getUniformLocation(sp, "mvpMatrix");gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMat);

該語句將世界視口投影矩陣傳遞給 GPU 使用,其他的跟這個(gè)類似,以上我們就實(shí)現(xiàn)了對 Shader 的加載編譯以及參數(shù)傳遞。

這些對 Unity?都是封閉的,開發(fā)者是不清楚的,給讀者介紹這些的目的是告訴讀者 Shader 在引擎中的一個(gè)執(zhí)行流程。

我們寫的代碼是通過以上類似的處理方式進(jìn)行的,掌握了以上兩點(diǎn)后,下面開始 Shader 實(shí)用技術(shù)編程講解。

三、Shader 編程技巧

在告訴大家編程技巧之前,我們首先要清楚 Shader 編程使用的接口函數(shù)的定義,換句話說我們要能看懂已有 Shader 的語句。

以 Unity 為例,Unity 為我們提供了很多現(xiàn)成的 Shader,還有一些 Shader 庫,為我們封裝了很多函數(shù),我們要做的事情就是要了解這些函數(shù)作用,在講解語句之前,先給讀者介紹幾個(gè)空間概念。

雖然我們已經(jīng)在上面提過,因?yàn)檫@幾個(gè)概念非常重要,所以有必要強(qiáng)調(diào)一下:

在 model space 中,坐標(biāo)是相對于模型網(wǎng)格的原點(diǎn)(0,0,0)定義的,這也是為什么我們要求美術(shù)在導(dǎo)出模型時(shí)將其設(shè)置成原點(diǎn)。

我們的 vertex function 需要把這些坐標(biāo)轉(zhuǎn)換到 clip space 中,為投影做準(zhǔn)備。

在 tangent space 中也稱為切線空間,法線紋理的計(jì)算就是在這個(gè)空間中進(jìn)行的,坐標(biāo)是相對于模型的正面定義的——在處理法線紋理時(shí)我們使用這個(gè) space,它是放在 UnityCG.cginc 庫中,定義如下:

? ?#define TANGENT_SPACE_ROTATION \ ?float3 binormal = cross( v.normal, v.tangent.xyz ) * v.tangent.w; \ ?float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )

可以看出 rotation 就是由三個(gè)向量構(gòu)建出了的 3X3 矩陣,而這三個(gè)向量分別對應(yīng)了 Object Space 中的 tangent、binormal 和 normal 的方向。

這三個(gè)方向?qū)?yīng)了 Tangent Space 中的三個(gè)坐標(biāo)軸的方向,效果如下所示:

其實(shí)就是一個(gè)坐標(biāo)變換,如果我們想得到從坐標(biāo)系 A 轉(zhuǎn)換到坐標(biāo)系 B 的一個(gè)變換矩陣,我們只需用 A 中 B 的三個(gè)坐標(biāo)軸的方向、按 X、Y、Z 軸的順序構(gòu)建一個(gè)矩陣即可。

在 world space 中,坐標(biāo)是相對于世界的原點(diǎn)(0,0,0)定義的。

在 view space 中,坐標(biāo)是相對于攝像機(jī)定義的,因此在這個(gè) space 中,攝像機(jī)的位置就是(0,0,0)。

在 clip space 中,通常圖元會(huì)被裁剪,然后再通過屏幕映射投影到屏幕空間中。

在 Shader 中我們通常會(huì)將其放到一個(gè)宏里面,比如 UNITY_MATRIX_MVP 這個(gè)就是(在 UnityShaderVariables.cginc 里定義)相乘,從而把頂點(diǎn)位置從 model space 轉(zhuǎn)換到 clip space。

我們使用了矩陣乘法操作 mul 來執(zhí)行這個(gè)步驟。

下面我們通過頂點(diǎn)著色器和片段著色器給讀者介紹關(guān)于使用的函數(shù)定義:

? ?v2f vert(appdata_base v) { ? ?v2f o; ?o.pos = mul (UNITY_MATRIX_MVP, v.vertex); ?o.srcPos = ComputeScreenPos(o.pos); ? ?o.w = o.pos.w; ?return o; ? ? ? }

頂點(diǎn)著色器中 ComputeScreenPos 是在 UnityCG.cginc 中定義的函數(shù),它就作用如名字一樣,計(jì)算該頂點(diǎn)轉(zhuǎn)換到屏幕上的位置。

還有如果我們需要把法線從模型空間變換到世界空間中,可以直接使用內(nèi)置函數(shù) UnityObjectToWorldNormal 函數(shù),常見的語句如下:

? ?fixed3 worldNormal = UnityObjectToWorldNormal(v.normal) 它的實(shí)現(xiàn)可以在它的實(shí)現(xiàn)可以在 UnityCG.cginc 里找到:inline float3 UnityObjectToWorldNormal( in float3 norm ) ? { ?// Multiply by transposed inverse matrix, actually using transpose() generates badly optimized code ?return normalize(_World2Object[0].xyz * norm.x + _World2Object[1].xyz * norm.y + _World2Object[2].xyz * norm.z); ? }

還有一種寫法:

? ?o.worldNormal = normalize(mul(v.normal, (float3x3)_World2Object));

總之,讀者一定要清楚重點(diǎn)函數(shù)的作用,這個(gè)可以查閱 Unity 自帶的庫。

我們在編寫 Shader 時(shí),通常會(huì)把引用的庫在 Shader 前面寫出來,比如 Shader 代碼中 Include”UnityCG.cginc” 等等。

它們都是可以在 Unity 提供的庫中找到的,讀者學(xué)習(xí) Shader 編程對于向量之間的點(diǎn)乘,叉乘,矩陣相乘都要搞清楚。

掌握了上面的知識點(diǎn)后,我們在項(xiàng)目開發(fā)時(shí),怎么去滿足需求呢?

首先先從 Unity 官方提供的 Shader 中匹配需求開發(fā),Unity 為我們提供了很多現(xiàn)成的 Shader,可以直接拿過來用。如果單個(gè)現(xiàn)成的滿足不了需求,看看能否將其中兩個(gè)合并成一個(gè)使用,或者在已有的基礎(chǔ)上修改,就不要自己重新寫了。

舉個(gè)例子,關(guān)于透明的材質(zhì)我們經(jīng)常會(huì)遇到渲染順序問題,比如我們渲染的透明材質(zhì)按照 Unity 提供的渲染順序,它是在不透明的物體后面渲染,如果我們遇到需求先渲染透明材質(zhì),這種問題解決起來比較簡單。

直接在 Tag 標(biāo)簽中的 Queue 中 減去 1000,這樣它的渲染數(shù)值就小于不透明物體了,代碼如下:

? ?Tags {"Queue"="Transparent-1000" "IgnoreProjector"="True" "RenderType"="Transparent"}

還有我們在做實(shí)時(shí)陰影渲染時(shí),為了防止陰影重疊,我們將原有的 Unity 自帶的 Shader 做了一個(gè)改動(dòng)就是將不需要的顏色才剪掉,只是加了一個(gè)判斷。

根據(jù)設(shè)置的參數(shù)去做裁剪,功能就實(shí)現(xiàn)出來了:

? half4 frag(v2f i) : SV_Target{half4 col = tex2D(_MainTex, i.texcoord);**if(col.a < _Cutoff)**{clip(col.a - _Cutoff);}else{col.rgb = col.rgb * float3(0, 0,0);col.rgb = col.rgb + _Color;col.a = _Color.a;}return col;}

其實(shí),我們只是加了一個(gè)判斷見加黑體部分,它就是根據(jù)材質(zhì)的 Alpha 值與設(shè)定的值進(jìn)行比較裁剪掉而已。這樣實(shí)現(xiàn)的效果如下:

原來的代碼是沒有這個(gè)判斷,這是我們根據(jù)自己的需求加上去的。另外,我們針對角色的渲染也會(huì)做一些處理,如下圖所示的效果:

它對應(yīng)的 Unity 中材質(zhì)的渲染如下圖所示:

其實(shí)我們也是在 Unity 原有 Shader 的基礎(chǔ)上增加了一些變量設(shè)置:

? ?Properties {_Color ("Main Color", Color) = (1,1,1,1)_ReflectColor ("Reflection Color", Color) = (1,1,1,0.5)_MainTex ("Base (RGB) RefStrength (A)", 2D) = "white" {}_Cube ("Reflection Cubemap", Cube) = "_Skybox" { TexGen CubeReflect }_BumpMap ("Normalmap", 2D) = "bump" {}_AddColor ("Add Color", Color) = (0,0,0,1)_Shininess ("Shininess", Range (0.05, 1)) = 0.9_ShininessPath ("ShininessPath", Range (0.1, 1)) = 0.5_IllColor ("Ill Color", Color) = (0.5, 0.5, 0.5, 1) }

當(dāng)然要定義這些變量:

? ?sampler2D _MainTex; sampler2D _BumpMap; samplerCUBE _Cube;fixed4 _Color; fixed4 _ReflectColor; fixed4 _AddColor; half _Shininess; half _ShininessPath; fixed4 _IllColor;

注意它們的名字跟 Properties 定義的是一一對應(yīng)的,最后在 Shader 中進(jìn)行處理這些變量,無非是對材質(zhì)進(jìn)行相乘或者相加運(yùn)算,最終還是對紋理的像素進(jìn)行操作:

? ?void surf (Input IN, inout SurfaceOutput o) {fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);fixed4 c = tex * _Color + _AddColor;o.Albedo = c.rgb;o.Gloss = tex.a;o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));float3 worldRefl = WorldReflectionVector (IN, o.Normal);fixed4 reflcol = texCUBE (_Cube, worldRefl);reflcol *= tex.a;o.Emission = reflcol.rgb * _ReflectColor.rgb ?+ tex.rgb * _IllColor.rgb;o.Alpha = reflcol.a * _ReflectColor.a;o.Specular *= o.Alpha * _Shininess + _ShininessPath; }

顏色的疊加或者對相應(yīng)的顏色進(jìn)行加強(qiáng)利用相乘得到,是不是很簡單?

另外,如果 Unity 現(xiàn)有的 Shader 滿足不了需求,我們可以借用 OpenGL 中的 Shader,它們也可以比較方便的修改成 Unity 的 Shader。

換句話說,如果我們用 Unity 自帶的 Shader 搞不定,如果你能找到 OpenGL 中的 Shader 一樣可以修改,代碼如下所示,下面是 OpenGL 中的代碼:

? ?varying vec2 v_texCoord; uniform sampler2D yTexture; uniform sampler2D uvTexture; const mat3 yuv2rgb = mat3(1, 0, 1.2802,1, -0.214821, -0.380589,1, 2.127982, 0);void main() { ? ?vec3 yuv = vec3(1.1643 * (texture2D(yTexture, v_texCoord).r - 0.0627),texture2D(uvTexture, v_texCoord).a - 0.5,texture2D(uvTexture, v_texCoord).r - 0.5);vec3 rgb = yuv * yuv2rgb;gl_FragColor = vec4(rgb, 1.0); }

我們要把以上的 Shader 代碼應(yīng)用到我們的 Unity 中,這就需要將上面代碼改成 Unity 能識別的 Shader 代碼。

下面就以修改這個(gè)為例給讀者分析一下:varying vec2 v_texCoord;可以定義成輸入結(jié)構(gòu)體,也就是我們說的 UV 紋理,定義的結(jié)構(gòu)體如下:

? ?struct Input{float2 uv_MainTex;}

再修改下面的代碼:

? ?uniform sampler2D yTexture; uniform sampler2D uvTexture;

可以修成成如下代碼:

? ?Properties {_XTex ("XTexture(RGB)", 2D) = "white" {}_YTex ("YTexture(RGB)", 2D) = "white" {} }

接下來再將定義的矩陣修成 Unity 中的 Shader,語句如下:

? ?float3x3 = matrix( ? 1, 0, 1.2802,1, -0.214821, -0.380589,1, 2.127982, 0);

接下來再改造:

vec3 rgb = yuv * yuv2rgb;float3= mul(yuv2rgb,yuv);

最后 獲取顏色:float4(rgb,1.0f);

按照這種方式就可以將 OpenGL 中的 Shader 改造成 Unity 自身的 Shader。當(dāng)然使用這種方式只是為了讓大家快的實(shí)現(xiàn)需求,實(shí)現(xiàn)不是目的。

我們的主要目的是在此基礎(chǔ)上掌握 Shader 的函數(shù)運(yùn)用以及實(shí)現(xiàn)它們的思路。

再此給讀者推薦一個(gè)網(wǎng)站 OpenGL 的,里面的 Shader 我們很多可以使用,比如用 Shader 實(shí)現(xiàn)的效果如下:

左邊是實(shí)時(shí)渲染效果,右邊是代碼和渲染通道。網(wǎng)址:https://www.shadertoy.com/。

四、材質(zhì)渲染

Shader 的核心用法就是材質(zhì)渲染,材質(zhì)渲染無非涉及到材質(zhì)高光,法線這些點(diǎn),還有反射,折射,卡通渲染,描邊等等,以及 Unity 高版本實(shí)現(xiàn)的 PBR 物理效果。

在此給讀者介紹一個(gè) Shader 編輯器工具 Shader Forge,如果讀者使用過 UE4 虛幻藍(lán)圖,它的操作方式跟 Shader Forge 非常類似。

為此我還專門寫過一篇博客介紹 Shader Forge,網(wǎng)上也有很多教程使用:教程一:http://blog.csdn.net/jxw167/article/details/69267236、教程二:http://blog.csdn.net/jxw167/article/details/69257559。

讀者可以先看看這兩篇博客在此就不重復(fù)了,只是說,在使用 Shader Forge 時(shí)要注意,我們使用時(shí)雖然實(shí)現(xiàn)了效果,但是要考慮到在手機(jī)端的效率問題,有時(shí)我們需要對其做一些效率優(yōu)化。

比如渲染順序問題,效率問題等等,我們用 Shader Forge 實(shí)現(xiàn)的效果如下:

對應(yīng)的 Unity 中的 Shader 控制面板 Shader 如下所示:

在優(yōu)化時(shí),比如我們不想讓其受光照,我們一般的做法是直接如下:

? ?// ? ? ? ? ? Tags { // ? ? ? ? ? ? ? ?"LightMode"="ForwardBase" // ? ? ? ? ? ?}

將光照模式注釋掉,同時(shí)可以加一句 Light off,關(guān)閉燈光。另外,下面這行代碼也要注意,將其注釋掉:

? //#pragma exclude_renderers gles3 metal d3d11_9x xbox360 xboxone ps3 ps4 psp2

因?yàn)槭謾C(jī)系統(tǒng)的適配,這句可以在 Shader Forge 中設(shè)置屏蔽掉,如果不設(shè)置可以直接把這行代碼注視掉,Shader Forge 操作起來非常方便,直接用線鏈接就可以,而且可以實(shí)時(shí)的查看效果。

下面再說說 Shader 優(yōu)化,這個(gè)是比較頭疼的問題,一方面要考慮到優(yōu)化,一方面要考慮到品質(zhì)。

五、Shader 優(yōu)化處理

Shader 的優(yōu)化處理,這個(gè)是一直困擾著程序的問題,想要好的品質(zhì),也要顧及到運(yùn)行效率,下面就給讀者分析一下。

在編寫 Shader 使如果遇到需求多時(shí),我們會(huì)在 Shader 中添加很多變量,在 Shader 如果使用變量比較多,我們通常的優(yōu)化方案是從其聲明的變量精度開始。

通常的定義如下:

  • float:表示的是最高精度,通常 32 位;

  • half:表示的是中等精度,通常 16 位;

  • fixed:表示的是最低精度,通常 11 位。

同樣還有我們常見的 float2,half2,fixed2 精度依次降低,當(dāng)然效率是逐步提升的,精度越低效率越高。

我們在使用時(shí)也是按照這種去考慮,當(dāng)然也要考慮到品質(zhì),fixed2 精度肯定不如 float2 的精度,當(dāng)然那品質(zhì)也就不如 float2 渲染的精度,這個(gè)要酌情處理。

另外,對于 Shader 代碼中使用的 if else,while,for,這些用于判斷的語句和循環(huán)語句盡量少用,因?yàn)?GPU 是多線程執(zhí)行的,這些容易打斷它的執(zhí)行。

盡量少用,不是說完全不用,因?yàn)橐恍┨厥庑枨筮€是要特殊處理的。

對于大批量的物體,比如國戰(zhàn)的游戲,需要大量的玩家,如果我們使用 CPU 去處理,這樣容易導(dǎo)致產(chǎn)生大量的批處理,嚴(yán)重影響效率,也有讀者會(huì)考慮到使用網(wǎng)格合成,但是這樣就不能一一操作單體了,所以這種方法需要排除。

如果我們使用 GPU Instancing 去處理就容易的多了,但是使用 GPU Instancing 處理的條件是必須是相同的角色動(dòng)作和材質(zhì),高版本的 Unity 的 Shader 都為我們提供了這個(gè)功能,在 Unity2017.2 中的 Shader 截圖如下:

紅線加粗的部分就是實(shí)例化,我們要勾選上,當(dāng)然,我們自己的 Shader 也可以這么處理,GPU Instancing 實(shí)例化角色的 Shader 代碼如下所示:

? ?Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag// 開啟 gpu instancing#pragma multi_compile_instancing#include "UnityCG.cginc"struct appdata{float2 uv : TEXCOORD0;UNITY_VERTEX_INPUT_INSTANCE_ID};struct v2f{float2 uv : TEXCOORD0;float4 vertex : SV_POSITION;UNITY_VERTEX_INPUT_INSTANCE_ID};sampler2D _MainTex;float4 _MainTex_ST;sampler2D _AnimMap;float4 _AnimMap_TexelSize;//x == 1/widthfloat _AnimLen;v2f vert (appdata v, uint vid : SV_VertexID){UNITY_SETUP_INSTANCE_ID(v);float f = _Time.y / _AnimLen;fmod(f, 1.0);float animMap_x = (vid + 0.5) * _AnimMap_TexelSize.x;float animMap_y = f;float4 pos = tex2Dlod(_AnimMap, float4(animMap_x, animMap_y, 0, 0));v2f o;o.uv = TRANSFORM_TEX(v.uv, _MainTex);o.vertex = UnityObjectToClipPos(pos);return o;}

看以上代碼帶有 INSTANCE_ID 的部分,這需要在 Shader 中事先聲明定義,這樣 Shader 才具有實(shí)例化功能。

只要將該 Shader 掛到需要實(shí)例化的角色身上即可,網(wǎng)上也有這樣的案例。

以上給讀者介紹了幾個(gè)常用的優(yōu)化方案,策劃會(huì)根據(jù)不同的項(xiàng)目提出不同的需求,方法掌握了,其他的修改就可以了。

六、Shader 后處理

GPU 不僅能用于渲染材質(zhì),而且還能渲染場景也就是我們說的后處理,又稱為濾鏡。

因?yàn)?Unity 使用的是單線程渲染方式,而且它是通過相機(jī)表現(xiàn)的,就是說后處理的腳本要掛接到相機(jī)上,這樣如果相機(jī)上的后處理腳本過多,嚴(yán)重影響運(yùn)行效率。

而 UE4 虛幻使用的是多線程渲染,這樣它的渲染效率大大提升。

所以 UE4 可以使用大量的后處理效果,當(dāng)然在 PC 端是完全可以的,手機(jī)端就要酌情考慮了。如果要掌握后處理渲染,首要的是要知道其工作原理。

下面以游戲中比較經(jīng)典 Bloom 的處理算法為例給讀者介紹:

Bloom 又稱 “全屏泛光”,在游戲場景的渲染中使用的非常多。

像 Bloom 這些后處理渲染效果,在游戲場景中是必備的渲染,它們的實(shí)現(xiàn)方式都是在 GPU 中實(shí)現(xiàn)的。要實(shí)現(xiàn) Bloom 算法,首先要做的事情是明白其實(shí)現(xiàn)原理,下面我們就來揭秘這個(gè) Bloom 特效的實(shí)現(xiàn)流程。

在流程上總共分為 4 步:

  • 提取原場景貼圖中的亮色;

  • 針對提取貼圖進(jìn)行橫向模糊;

  • 在橫向模糊基礎(chǔ)上進(jìn)行縱向模糊;

  • 所得貼圖與原場景貼圖疊加得最終效果圖。

首先展示流程的第 1 步代碼如下所示:

BloomExtract.fx // 提取原始場景貼圖中明亮的部分 // 這是應(yīng)用全屏泛光效果的第一步sampler TextureSampler : register(s0);float BloomThreshold;float4 ThePixelShader(float2 texCoord : TEXCOORD0) : COLOR0 {// 提取原有像素顏色float4 c = tex2D(TextureSampler, texCoord);// 在 BloomThreshold 參數(shù)基礎(chǔ)上篩選較明亮的像素return saturate((c - BloomThreshold) / (1 - BloomThreshold));}technique BloomExtract {pass Pass1{PixelShader = compile ps_2_0 ThePixelShader();} }

接下來是第 2,3 步的實(shí)現(xiàn)代碼如下所示:

GaussianBlur.fx // 高斯模糊過濾 // 這個(gè)特效要應(yīng)用兩次,一次為橫向模糊,另一次為橫向模糊基礎(chǔ)上的縱向模糊,以減少算法上的時(shí)間復(fù)雜度 // 這是應(yīng)用 Bloom 特效的中間步驟sampler TextureSampler : register(s0);#define SAMPLE_COUNT 15// 偏移數(shù)組 float2 SampleOffsets[SAMPLE_COUNT]; // 權(quán)重?cái)?shù)組 float SampleWeights[SAMPLE_COUNT];float4 ThePixelShader(float2 texCoord : TEXCOORD0) : COLOR0 {float4 c = 0;// 按偏移及權(quán)重?cái)?shù)組疊加周圍顏色值到該像素// 相對原理,即可理解為該像素顏色按特定權(quán)重發(fā)散到周圍偏移像素for (int i = 0; i < SAMPLE_COUNT; i++){c += tex2D(TextureSampler, texCoord + SampleOffsets[i]) * SampleWeights[i];}return c; }technique GaussianBlur {pass Pass1{PixelShader = compile ps_2_0 ThePixelShader();} }

第 4 步實(shí)現(xiàn)的代碼如下所示:

BloomCombine.fx // 按照特定比例混合原始場景貼圖及高斯模糊貼圖,產(chǎn)生泛光效果 // 這是全屏泛光特效的最后一步// 模糊場景紋理采樣器 sampler BloomSampler : register(s0);// 原始場景紋理及采樣器定義 texture BaseTexture; sampler BaseSampler = sampler_state {Texture ? = (BaseTexture);MinFilter = Linear;MagFilter = Linear;MipFilter = Point;AddressU ?= Clamp;AddressV ?= Clamp; };float BloomIntensity; float BaseIntensity;float BloomSaturation; float BaseSaturation;// 減緩顏色的飽和度 float4 AdjustSaturation(float4 color, float saturation) {// 人眼更喜歡綠光,因此選取 0.3, 0.59, 0.11 三個(gè)值float grey = dot(color, float3(0.3, 0.59, 0.11));return lerp(grey, color, saturation); }float4 ThePixelShader(float2 texCoord : TEXCOORD0) : COLOR0 {// 提取原始場景貼圖及模糊場景貼圖的像素顏色float4 bloom = tex2D(BloomSampler, texCoord);float4 base = tex2D(BaseSampler, texCoord);// 柔化原有像素顏色bloom = AdjustSaturation(bloom, BloomSaturation) * BloomIntensity;base = AdjustSaturation(base, BaseSaturation) * BaseIntensity;// 結(jié)合模糊像素值微調(diào)原始像素值base *= (1 - saturate(bloom));// 疊加原始場景貼圖及模糊場景貼圖,即在原有像素基礎(chǔ)上疊加模糊后的像素,實(shí)現(xiàn)發(fā)光效果return base + bloom; }technique BloomCombine {pass Pass1{PixelShader = compile ps_2_0 ThePixelShader();} }

具體實(shí)現(xiàn)見下圖所示:

下面,大家結(jié)合這個(gè)流程圖來分析各個(gè)步驟:

  • 第一步:應(yīng)用 BloomExtract 特效提取原始場景貼圖m_pResolveTarget中較明亮的顏色繪制到m_pTexture1貼圖中(m_pResolveTarget--->m_pTexture1)

  • 第二步:應(yīng)用 GaussianBlur 特效,在m_pTexture1貼圖基礎(chǔ)上進(jìn)行橫向高斯模糊到m_pTexture2貼圖(m_pTexture1--->m_pTexture2)

  • 第三步:再次應(yīng)用 GaussianBlur 特效,在橫向模糊之后的m_pTexture2貼圖基礎(chǔ)上進(jìn)行縱向高斯模糊,得到最終的模糊貼圖m_pTexture1(m_pTexture2--->m_pTexture1)
    注意:此時(shí),m_pTexture1貼圖即為最終的模糊效果貼圖。

  • 第四步:應(yīng)用 BloomCombine 特效,疊加原始場景貼圖 m_pResolveTarget 及兩次模糊之后的場景貼圖 m_pTexture1,從而實(shí)現(xiàn)發(fā)光效果 (m_pResolveTarget+m_pTexture1)。

實(shí)現(xiàn)的效果如下所示:

另外,其他的后處理方式比如 Blur,HDR,PSSM 等等,也是基于圖像的算法實(shí)現(xiàn)的。掌握了算法的原理,不論用什么引擎,它們的原理都是類似的,只是在一些細(xì)節(jié)方面做的不同罷了。

Unity 也為用戶提供了大量的后處理 Shader,拿過來使用就可以了,但是也要注意其效率,我們可以在此基礎(chǔ)上進(jìn)行優(yōu)化處理,比如適當(dāng)?shù)陌丫冉档鸵恍?/p>

有些函數(shù)語句在不影響效果的前提下可以簡化,比如一些 exp,exp2 等等,這種類似的函數(shù)都可以簡化處理,畢竟它們也是非常耗的。

七、Shader 調(diào)試

Shader 因?yàn)槭且环N腳本語言,面臨著非常尷尬的境地是無法調(diào)試,以前我們開發(fā)端游時(shí)使用的是 Render Monkey,它是可以調(diào)試的。

Unity 中的 Shader 可以看到其錯(cuò)誤的行數(shù),如果語句沒有錯(cuò)誤,我們要查找問題,通常的做法就是逐步的注釋掉語句進(jìn)行排查,雖然麻煩但是可以解決問題。

也可以使用特殊值的方式進(jìn)行測試語句。

八、總結(jié)

以上幾點(diǎn)也是作者自己關(guān)于 Shader 學(xué)習(xí)的一點(diǎn)總結(jié),希望對讀者有所幫助。

有一點(diǎn)大家要清楚,我們使用 Shader 渲染都是基于圖像的處理方式,不論是材質(zhì)渲染還是后處理渲染,其實(shí)如果想成為圖形學(xué)工程師,以上六點(diǎn)還是必須要掌握的。

在此也是拋磚引玉,里面用到了一些技巧,只是幫助你快速的入手,要想深入的學(xué)習(xí),我們還是要把基礎(chǔ)打好,其實(shí)圖形學(xué)沒有想象的那么復(fù)雜。

近期熱文

《談?wù)?Java 內(nèi)存模型》

《Jenkins 與 GitLab 的自動(dòng)化構(gòu)建之旅》

《通往高級 Java 開發(fā)的必經(jīng)之路》

《談?wù)勗创a泄露 · WEB 安全》

《用 LINQ 編寫 C# 都有哪些一招必殺的技巧?》

《機(jī)器學(xué)習(xí)面試干貨精講》

《深入淺出 JS 異步處理技術(shù)方案》


「閱讀原文」看交流實(shí)錄,你想知道的都在這里

總結(jié)

以上是生活随笔為你收集整理的如何快速成长为图形学工程师的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。