如何編寫Shadersmod光影包(附錄)

本節為附錄部分,包含了一些參考信息和經驗之談.

鏈接:上篇 下篇

附錄 - 所有可用的頂點屬性

變量名 格式 描述
mc_Entity vec4 僅在繪製磚塊和天空時有意義,在繪製磚塊時,X值代表當前磚塊的磚塊ID,Y值代表其渲染類型(RenderType)(似乎1.8中不存在了),Z值代表Metadata.繪製天空時,在gbuffers_textured中,X值為-3代表繪製雲;在gbuffers_skybasic中,X值為-2時代表繪製特殊的天空背景,比如黃昏時西邊的晚霞,想要看它具體是什麼樣的話,在日落時面朝北方或南方,那個明顯亮出一塊的就是由它繪製的特殊天空背景.
mc_midTexCoord vec4 st值代表紋理中當前待繪製區域的中點位置
at_tangent vec4 頂點的切線

附錄 - 所有可用的一致變量


注意,以下內容均不包括gbuffers_spidereyes,它太過於不常用了...

GBuffer繪製階段和ShadowMap繪製階段特有:

變量名 格式 描述 取值
texture sampler2D 物體紋理 這還用說嗎:) 不過說起來我還真不知道無紋理的物體的紋理取值是啥呢...純黑的圖像?
tex sampler2D ShadowMap繪製階段獨有,等同於texture
lightmap sampler2D 光照貼圖 當前時間的光照貼圖
normals sampler2D 法線貼圖,只有當前材質包擁有法線圖並開啟了Shadersmod的法線貼圖功能時才有效
specular sampler2D 反射貼圖,只有當前材質包擁有反射圖並開啟了Shadersmod的反射貼圖功能時才有效
entityHurt int 繪製實體時特有,表明實體是否處於受攻擊時的閃紅 0為未受傷害;按作者的說法102代表受傷,反正肯定是大於0...
entityFlash int 繪製實體時特有,用於表示苦力帕爆炸前的閃光 0為無閃光;1~255為即將爆炸的苦力帕

後處理與最終合成階段特有:

變量名 格式 描述 取值
gcolor / colortex0 sampler2D 相當於顏色緩衝gcolor
gdepth / colortex1 sampler2D 相當於顏色緩衝gdepth
gnormal / colortex2 sampler2D 相當於顏色緩衝gnormal
composite / colortex3 sampler2D 相當於顏色緩衝composite
gaux1 / colortex4 sampler2D 相當於顏色緩衝gaux1
gaux2 / colortex5 sampler2D 相當於顏色緩衝gaux2
gaux3 / colortex6 sampler2D 相當於顏色緩衝gaux3
gaux4 / colortex7 sampler2D 相當於顏色緩衝gaux4
depthtex0 sampler2D或sampler2DShadow 相當於深度緩衝depthtex0
depthtex1 sampler2D或sampler2DShadow 相當於深度緩衝depthtex1
depthtex2 sampler2D或sampler2DShadow 相當於深度緩衝depthtex2

所有階段都共有:

變量名 格式 描述 取值
shadow sampler2D或sampler2DShadow 見幀緩衝知識點中對shadow的解釋.在Shadersmod的代碼中你確實可以在ShadowMap繪製階段訪問陰影貼圖...但誰知道這會發生什麼呢?
watershadow sampler2D或sampler2DShadow 見幀緩衝知識點中對watershadow的解釋
shadowtex0 sampler2D或sampler2DShadow 包含水面的陰影貼圖
shadowtex1 sampler2D或sampler2DShadow 不包含水面的陰影貼圖
shadowcolor / shadowcolor0 sampler2D ShadowMap的顏色 默認值為(1.0, 1.0, 1.0, 1.0)
shadowcolor1 sampler2D ShadowMap的顏色
noisetex sampler2D 噪聲圖 格式為RGB,每一個分量均是一個範圍在[0.0, 1.0]之間的隨機數.
heldItemId int 玩家所持物品的物品id -1代表什麼也沒拿
heldBlockLightValue int 玩家所持磚塊的光照強度 範圍在0~15,0為不發光,15為最強光(比如火炬)
fogMode int 霧模式 0代表當前渲染的物體不受霧效果影響,9729為線性狀漸變,2048為指數狀漸變,2049為指數平方狀漸變,具體公式可以百度...
fogColor vec3 霧顏色
skyColor vec3 天空顏色
worldTime int 當前遊戲世界的時間,範圍在0(破曉)~23999(黎明前)
moonPhase int 月相
frameTimeCounter float 以秒為單位計算遊戲正式啟動(即載入了一個存檔/加入了一個服務器)後的時間,這個變量的範圍是[0.0, 100000.0).遊戲暫停時仍會計時
sunAngle float 太陽角度
shadowAngle float 陰影角度
rainStrength float 雨強度 範圍在0.0~1.0,平時為0.0,開始下雨/下雪/雷暴時逐漸遞增到1.0,停止後再逐漸遞減回0.0
aspectRatio float 屏幕橫縱比 等於屏幕寬度除以高度
viewWidth float 屏幕寬度
viewHeight float 屏幕高度
near float 投影矩陣的近裁面 為常數0.05
far float 投影矩陣的遠裁面 等於遊戲設置內的最大視距乘16
sunPosition vec3 眼坐標系中太陽的位置
moonPosition vec3 眼坐標系中月亮的位置
upPosition vec3 一個"近似"可以表示鏡頭的正上方朝向的向量 說它是"近似"是因為它只考慮鏡頭的俯仰角(Pitch),而沒考慮鏡頭的偏轉角(Yaw).當玩家水平直視某一方向時,它的值是(0.0, 100.0, 0.0),顯然當鏡頭水平時它的上方向自然是指向Y軸正方向;當玩家的鏡頭朝向正上方時,它的值是(0.0, 0.0, -100.0);朝向正下方時,它的值為(0.0, 0.0, 100.0).
cameraPosition vec3 玩家實體在世界空間的位置
previousCameraPosition vec3 上一幀的cameraPosition...
gbufferModelView mat4 理論上是當前的模型視圖矩陣,但實際上並不是...教學着色器Tutorial_BadMatrixes演示了它不等於gl_ModelViewMatrix.具體解釋見另一篇附錄吧.
gbufferModelViewInverse mat4 gbufferModelView的逆矩陣 根據另一篇附錄的結論,顯然用gbufferModelViewInverse乘以一個眼坐標系下的坐標,得到的是一個處於世界坐標系中,但被偏移了-cameraPosition個單位距離的點.
gbufferPreviousModelView mat4 上一幀的gbufferModelView
gbufferProjection mat4 等同於當前的投影矩陣(gl_ProjectionMatrix)
gbufferProjectionInverse mat4 gbufferProjection的逆矩陣
gbufferPreviousProjection mat4 上一幀的gbufferProjection
shadowModelView mat4 ShadowMap的模型視圖矩陣
shadowModelViewInverse mat4 shadowModelView的逆矩陣
shadowProjection mat4 ShadowMap的投影矩陣
shadowProjectionInverse mat4 shadowProjection的逆矩陣
wetness float 濕度 一個範圍在0.0~1.0之間的數,當下雨時會緩慢增加,雨停後會緩慢下降.
eyeAltitude float 玩家眼部高度 即F3菜單中的eyes pos
eyeBrightness ivec2 玩家所在位置的亮度 X值代表來自磚塊的光照;Y值代表白天來自天空的光照,這意味着這個變量忽視了晝夜變化...取值範圍均為0~240之間的整數,對應0~15這16個光照等級,注意這個值是突變的,即從無光區走到1亮度區時取值會從0突變到15,如果想要漸變版本,請看下面的.
eyeBrightnessSmooth ivec2 玩家所在位置的亮度的漸變版本 eyeBrightness的漸變版本,會在兩個不同亮度之間進行平滑.
terrainTextureSize ivec2 紋理分辨率(尺寸) 唔...也許是MC在後台拼合成的紋理貼圖的尺寸? (腦補舊時代MC那256x256的紋理集...)
terrainIconSize int 單個磚塊的貼圖尺寸 例如默認MC的貼圖尺寸是16(像素)
isEyeInWater bool 玩家是否在水下
hideGUI bool 是否隱藏GUI界面
centerDepthSmooth float 屏幕中央的深度緩衝數值,這個是經過插值平滑的,可以用來實現DoF(景深)等特效.
atlasSize ivec2 TODO

附錄 - 所有可用的控制參數


控制參數是Shadersmod供光影包調整一些內部參數的方式,比如通過控制參數來控制ShadowMap的分辨率等. 注意:以下所有內容均要求一條控制參數獨佔一行,且開頭不能有縮進,結尾不能有多餘的內容.

內容 描述 控制方式
啟用緩衝區 Shadersmod會掃描着色器腳本,自動找出需要啟用的顏色緩衝或深度緩衝,不過在使用緩衝時盡量避免跳着用,比如避免放着gaux1和gaux2不用,直接用gaux3去了...系統依然會為gaux1和gaux2分配顯存,於是便造成性能浪費 (當然如果你不在乎的話那就當我沒說) 在任意一個片元着色器中加入:
uniform sampler2D [緩衝名];
例如:uniform sampler2D gaux1;
效果為啟用gaux1
對於深度緩衝可以使用sampler2DShadow代替sampler2D,如果你知道怎麼使用sampler2DShadow的話...
設定顏色緩衝格式 所有緩衝區都有默認的格式,有些你不能控制(比如深度緩衝的格式和ShadowMap的格式),但你可以控制G-Buffer的8個顏色緩衝的格式. 在任意一個片元着色器中加入:
const int [格式] = [隨便一個整數,隨便,真的];
const int [名稱或別名]Format = [格式];
例如:
const int RGBA32F = 12450;
const int gaux1Format = RGBA32F;
讓gaux1的格式為RGBA32F
啟用顏色緩衝的Mipmap 默認的顏色緩衝都不啟用Mipmap(不知道是什麼東西自行百度...),如果你希望使用Mipmap的話可以啟用它,需要注意的顏色緩衝的Mipmap是分階段的,比如一個顏色緩衝可以在composite、composite2和final中啟用Mipmap,而在composite1中不啟用Mipmap. 在需要使用Mipmap的後處理或最終合成階段的片元着色器中加入:
const bool [名稱或別名]MipmapEnabled = [是否啟用Mipmap];
例如:
在composite1.fsh中加入:
const bool gaux1MipmapEnabled = true;
在composite1階段中啟用gaux1的Mipmap
片元着色器輸出 有時你可能不想向所有的顏色緩衝都輸出顏色,這時你可以聲明只向某幾個顏色緩衝輸出顏色. 在該片元着色器中新起一行加入:
/* DRAWBUFFERS:[各個輸出目標的ID] */
具體方式見上篇的"知識點 - Shadersmod可用的緩衝區"
ShadowMap基準分辨率 默認的ShadowMap基準分辨率為1024,還要乘以遊戲中Shadersmod配置中的那個ShadowMap分辨率係數才是實際分辨率.如果你認為默認的分辨率太低的話,可以手動提高它.不過要小心的是太大的分辨率可能會在低端顯卡上無法運行,Shadersmod最大的分辨率係數是4.0,乘以默認基準分辨率後是4096*4096,剛好是低端顯卡所最大支持的紋理面積. 方法1:在任意一個片元着色器中新起一行加入:
/* SHADOWRES:[基準分辨率] */
方式2:在任意一個片元着色器中加入:
const int shadowMapResolution = [基準分辨率];
ShadowMap覆蓋面積 修改ShadowMap覆蓋面積其實就是修改其投影矩陣.默認投影矩陣為正交投影,在ShadowMapping技術中正好符合平行光源(比如太陽光),它的默認參數是左右上下裁面均位於±160,近裁面位於0.05,遠截面位於256.
如果你有特殊需要的話,也可以將繪製ShadowMap的投影矩陣換為透視投影,按照ShadowMapping技術定義只有點光源才會使用透視投影,換成透視投影后的默認參數為Fov 90°,橫縱比為1(準確說是ShadowMap的寬度除以高度,但我想不出來能讓兩者不相等的辦法),近裁面0.05,遠裁面256.
聲明為正交投影:
方法1:在任意一個片元着色器中新起一行加入:
/* SHADOWHPL:[覆蓋面積的半長,默認值為160] */
方式2:在任意一個片元着色器中加入:
const float shadowDistance = [覆蓋面積的半長,默認值為160.0];
聲明為透視投影:
在任意一個片元着色器中新起一行加入:
/* SHADOWFOV:[投影的Fov,默認值為90] */
ShadowMap位置矯正頻率 ShadowMap的焦點並非始終緊隨玩家,它是當玩家每移動一段距離時才會矯正位置,這個值默認是2.0. 注意這個特性僅當ShadowMap使用正交投影時才生效,在透視投影下ShadowMap始終會緊隨玩家. 在任意一個片元着色器中加入:
const float shadowIntervalSize = [每次矯正時最小移動距離];
ShadowMap的Mipmap 默認情況下ShadowMap不會生成Mipmap,如果你需要它的話可以將其開啟. 在任意一個片元着色器中加入:
const bool generateShadowMipmap = [是否生成所有深度緩衝的Mipmap];
const bool generateShadowColorMipmap = [是否生成所有ShadowMap顏色的Mipmap];
如果你只想讓某一個生成的話:
const bool shadowtexMipmap = [是否生成shadowtex0的Mipmap];
const bool shadowtex0Mipmap = [是否生成shadowtex0的Mipmap];
const bool shadowtex1Mipmap = [是否生成shadowtex1的Mipmap];
const bool shadowcolor0Mipmap = [是否生成shadowcolor0的Mipmap];
const bool shadowColor0Mipmap = [是否生成shadowcolor0的Mipmap];
const bool shadowcolor1Mipmap = [是否生成shadowcolor1的Mipmap];
const bool shadowColor1Mipmap = [是否生成shadowcolor1的Mipmap];
(注:這種多個參數效果相同的意為一種參數有多個別名,不是說你必須全寫上才能生效...)
ShadowMap的硬件支持的深度緩衝比較 默認情況下ShadowMap不會啟用ARB_shadow這個擴展引入的硬件支持的深度緩衝比較功能,說白了就是不能使用sampler2DShadow和shadow2D,顯然如果你想用那倆個的話需要手動開啟硬件支持. 在任意一個片元着色器中加入:
const bool shadowHardwareFiltering = [是否開啟ShadowMap的所有深度緩衝的ARB_shadow支持];
如果你只想啟用某一個的話:
const bool shadowHardwareFiltering0 = [是否開啟shadowtex0的ARB_shadow支持]
const bool shadowHardwareFiltering1 = [是否開啟shadowtex1的ARB_shadow支持]
ShadowMap的最鄰近過濾 默認情況下ShadowMap使用的是雙線性過濾(GL_LINEAR),但如果你有特殊需要的話,可以換成最鄰近過濾(GL_NEAREST).相應的,如果你還開啟了Mipmap的話,系統會自動使用帶Mipmap的最鄰近過濾(GL_NEAREST_MIPMAP_NEAREST) 在任意一個片元着色器中加入:
const bool shadowtexNearest = [是否將shadowtex0設為最鄰近過濾];
const bool shadowtex0Nearest = [是否將shadowtex0設為最鄰近過濾];
const bool shadow0MinMagNearest = [是否將shadowtex0設為最鄰近過濾];
const bool shadowtex1Nearest = [是否將shadowtex1設為最鄰近過濾];
const bool shadow1MinMagNearest = [是否將shadowtex1設為最鄰近過濾];
const bool shadowcolor0Nearest = [是否將shadowcolor0設為最鄰近過濾];
const bool shadowColor0Nearest= [是否將shadowcolor0設為最鄰近過濾];
const bool shadowColor0MinMagNearest= [是否將shadowcolor0設為最鄰近過濾];
const bool shadowcolor1Nearest = [是否將shadowcolor1設為最鄰近過濾];
const bool shadowColor1Nearest= [是否將shadowcolor1設為最鄰近過濾];
const bool shadowColor1MinMagNearest= [是否將shadowcolor1設為最鄰近過濾];
乾濕度漸變速率 Shadersmod提供了wetness來作為一個"平滑插值的rainStrength",其漸變速率可以通過濕度變化率(默認值200)和乾燥變化率(默認值600)來控制,其中變化率越小,則變化越快,為1時基本上wetness就等同於rainStrength了.其中濕度變化率控制下雨時wetness增加的速率,乾燥變化率控制雨停時wetness的遞減速率. 方法1:在任意一個片元着色器中新起一行加入:
/* DRYNESSHL:[濕度變化率] */
/* WETNESSHL:[乾燥變化率] */
方式2:在任意一個片元着色器中加入:
const float drynessHalflife= [濕度變化率];
const float wetnessHalflife = [乾燥變化率];
亮度漸變速率 Shadersmod提供了eyeBrightnessSmooth作為eyeBrightness的漸變版本,其漸變速率(默認為10)可以由eyeBrightnessHalflife來控制,越小則變化越快. 在任意一個片元着色器中加入:
const float eyeBrightnessHalflife = [變化率];
中央深度漸變速率 Shadersmod提供了centerDepthSmooth作為經過平滑插值的屏幕中央深度,其漸變速率(默認為1)可以由centerDepthHalflife來控制,越小則變化越快. 在任意一個片元着色器中加入:
const float centerDepthHalflife = [變化率];
太陽傾斜角 Minecraft默認的太陽直挺挺地東升西落雖然效果不錯,但對於陰影繪製來說是個噩夢,因此適當地修改太陽傾斜角可以抖個機靈迴避陰影繪製時的問題. 在任意一個片元着色器中加入:
const float sunPathRotation = [太陽傾斜角];
環境光遮蔽強度 環境光遮蔽是近些年遊戲引入的一項真實光照技術,早期的光照模型都會忽略光的二次反射問題,如果你看過一些早期OpenGL光照系統的演示的話,肯定會見過類似一個完全漆黑的房間里有一盞燈和它突兀的亮光的場景,現實中這種情景很少見,因為物體除了會被光源直接照亮外,還會被其他亮處的物體的漫反射光照亮.對此一個常見的解決方案是引入環境光,即最低亮度,然而這又帶來一個問題就是一些本該不亮的地方也被照亮了...環境光遮蔽技術就是讓那些不該亮的地方不亮.其實遊戲中已經自帶了一定程度上的環境光遮蔽效果,而這個參數就是控制該效果的.默認值為0.8,當改為0.0時場景會很亮,而1.0時則會有些暗.具體效果看下圖(點擊放大):
0.0:AO0
0.8:AO8
1.0:AO10
在任意一個片元着色器中加入:
const float ambientOcclusionLevel = [強度];
超採樣抗鋸齒 好吧,這個其實沒效果...僅僅是Shadersmod的保留功能. 在任意一個片元着色器中加入:
const int superSamplingLevel = [超採樣等級];
噪聲圖分辨率 Shadersmod默認的噪聲圖分辨率為256x256,通常來說是夠用了,但如果你想獲得更多的多樣性的話,可以調高噪聲圖的分辨率來減少重複. 在任意一個片元着色器中加入:
const int noiseTextureResolution = [噪聲圖分辨率];

附錄 - Shadersmod的所有階段


見上篇的"知識點 - 光影包的組成"

附錄 - DRAWBUFFERS導致的水面異常

這個標題我覺得挺蛋疼的,作為一個自己能被自己的想法嚇到的人,在深更半夜寫下這種詭異的標題我反正是覺得挺毛骨悚然的,管它呢,我們來切入正題.
我想你已經知道"/* DRAWBUFFERS:xxx */"的前面和後面不能加包括tab和空格在內的任何東西這件事情(如果你確實不知道的話,現在也知道了),之前我一直認為如果不聲明DRAWBUFFERS的話,會默認按照01234567的順序來輸出,但現在發現不盡然,對於後處理(即composite系著色器)來說,如果不聲明DRAWBUFFERS的話,會導致水面出現異常.

2015-10-09_01.50.32
圖:沒有聲明DRAWBUFFERS的composite,輸出內容為直接輸出gcolor

2015-10-09_01.50.45
圖:聲明DRAWBUFFERS:0的composite,輸出內容為直接輸出gcolor

此外,就連最備受推崇的SEUS都中招了...SEUS10.1的水面會有閃爍效果,如果手動加上DRAWBUFFERS:012的話就會解決這個問題.

2015-10-09_01.49.52
圖:在GTX970下,未經修改的SEUS10.1,注意圖左側的裂紋

2015-10-09_01.50.13
圖:手動在composite中加上DRAWBUFFERS:012的SEUS10.1

如果你在網上搜這個問題的話,你也會注意到很多人都發現了SEUS10.1的水面閃爍或馬賽克問題,其中大部分人都指出問題僅發生在使用Maxwell架構的N卡上,刨去那些什麼"SEUS是專門為Kepler架構開發的"之類的裝(sha)逼言論不說,這個問題確實可能是由於顯卡在OpenGL底層處理的方式不同而造成的bug,但我更覺得是Shadersmod沒有正確調用OpenGL,導致在舊架構下能勉強運行的dirty code在新架構中掛掉...同樣的代碼沒法保證在不同的顯卡下取得相同的效果,這也是OpenGL飽受人詬病的,我之前以為這種差異僅存在於不同品牌的顯卡間,現在才發現同一品牌下的不同系列顯卡也會有這種問題(而且還都是近些年的高端產品,而不是新品與古董),確實讓人大開眼界,也不得不讓人想說"你OGL要完啊"..."我愛大GL!我怕她要完啊!" (笑)

附錄 - ST坐標系,朝上還是朝下?


ST坐標系即OpenGL的紋理採樣器所使用的坐標系,弄清其T軸到底朝下還是朝上是很重要的,因為朝上意味着紋理可以是左下角置於原點,而朝下意味着左上角可以置於原點.當我真的想確認這一點時被網上眾說紛紜的說法嚇尿了,大部分書籍和網上的資料都說是T軸朝上,而一部分網站和在MC中實際測試結果是朝下,最後我的結論是:確實是T軸朝上的,但MC對紋理做了一次垂直翻轉...

在大部分繪圖軟件中,圖像的原點都被標記在左上角,Y坐標向下為正;如果你有過在Windows上的GUI編程經驗的話,也會知道屏幕坐標系是左上角為原點,Y軸朝下為正;DirectX的紋理取樣器便順應大眾的習慣,它使用的坐標是T軸朝下為正.然而OpenGL卻截然相反,它將原點定為圖像的左下角,並規定T軸向上為正.

20100531_DX_OpenGL
圖:DX與OGL在ST坐標繫上的區別,圖片摘自TheDev.log (可能需翻牆)

應付這種反人類設計的方法有許多,一種方式是手動封裝一遍操作紋理坐標的函數,對y軸的操作全改成1-y;而另一種更取巧的方法是傳入紋理時直接傳進去一個垂直翻轉的,這樣就可以按照T軸朝下的坐標系來操作了.

stc1
stc2
圖:圖片在經過垂直翻轉後,便可以按照T軸朝下的方式來操作了.

那麼那些說OpenGL的ST坐標系朝下的是怎麼回事?我猜這和他們使用的圖像庫有關...一副圖像在讀入成位圖格式時,其數據是按照從下往上還是從上往下的順序和文件本身以及圖像庫本身都有關係,那些人可能碰巧使用了一個自動翻轉圖像的庫,因此得出了OpenGL ST坐標朝下的結論.
更新:其實還有一個原因,glTexImage2D這個函數在上傳紋理時是從左下角開始,按照從左到右,從下到上的方式傳入數據,這意味着如果圖像載入是按照從上到下,從左往右的方式讀取的話,未經任何處理的圖像在被載入成紋理後剛好是上下顛倒的,不留意的人會誤以為坐標系是朝下為正.

此外,你可以通過教學着色器Tutorial_STCoordinate來親自驗證MC中的紋理坐標,在Tutorial_STCoordinate中,黑色代表紋理坐標靠近(0,0),紅色代表靠近(1,0),綠色代表靠近(0,1),黃色代表靠近(1,1).

2015-09-13_19.29.07
2015-09-13_19.29.13

附錄 - gbufferModelView與shadowModelView ~ 追尋特大型矩陣之謎

刨去逗悶子的標題,gbufferModelView與shadowModelView的特性確實挺讓人困惑,官方wiki上對gbufferModelView的描述是"在設置完相機變換後的4x4模型視圖矩陣,和之前的用途稍有不同,所以名字有一些歧義",而對"shadowModelView"的描述則乾脆是"生成ShadowMap時用的模型視圖矩陣".但實際上使用時它們絕對沒法取代gl_ModelViewMatrix,教學着色器Tutorial_BadMatrixes演示了這一點,那麼gbufferModelView和shadowModelView到底是什麼?

gbufferModelView的描述提到了"設置完相機變換後",那麼它和cameraPosition會不會有什麼關係?經過測試確實有...
通過gbufferModelViewInverse * gl_ModelViewMatrix * gl_Vertex可以得到的是一個奇怪的坐標,但如果將這個坐標加上cameraPosition,得到的就是世界坐標.因此我們可以得出一個公式:
Mc*Mgmv-1*Mmv*V = Mm*V
其中Mc是將坐標平移cameraPosition個單位的變換矩陣,Mgmv是gbufferModelView,那麼:
Mc*Mgm-1*Mgv-1*Mv*Mm*V = Mm*V
Mc*Mgm-1*Mgv-1*Mv = I
Mc = Mv-1*Mgv*Mgm
Mv*Mc = Mgv*Mgm = Mgmv
於是結論是gbufferModelView等於Mv*Mc,Mv為視圖矩陣,Mc是一個將物體平移cameraPosition個單位距離的4x4矩陣.

附錄 - ⑨評gl_NormalMatrix


好吧,居然還真有人對這個東西的來歷和實質感興趣...gl_NormalMatrix是對法線向量進行模型視圖變換的矩陣,簡單地說,gl_NormalMatrix在大部分情況下就是gl_ModelViewMatrix的左上3x3部分,即大部分情況下gl_NormalMatrix * gl_Normal等於(gl_ModelViewMatrix * vec4(gl_Normal, 0.0)).xyz,那麼什麼才算是少部分情況?當模型視圖矩陣涉及到縮放時.
縮放分為統一縮放和非統一縮放,統一縮放是指在xyz軸上進行相同倍數的縮放,顯然這種情況下法線向量在經過模型視圖矩陣變換後方向依然不變,只是長度不為1了,經過歸一化修正後依然能保持結果正確;然而當不同軸上的縮放倍數不同,即進行非統一縮放時問題就來了,法線向量在經過變換後會指向錯誤的方向,比如一個緊貼xy軸的等腰直角三角形的斜邊的法線N是(1,1)(未歸一化到單位長度,下同),切線T是(1,-1),顯然N·T=0,符合法線垂直於切線的定義,然而如果將三角形沿y軸伸長到2倍,那麼變形後的切線顯然依然是(1,-2),而法線卻不可能是(1,2),因為顯然(1,-2)·(1,2)≠0.

normalmat1 normalmat2
圖片源自lighthouse3d

因此我們需要一個特殊的矩陣能夠讓法線向量進行正確的模型視圖變換,這個矩陣就是gl_NormalMatrix,它的來源我倒覺得不像是人為擬定的,而更像是從現有的公式下反推出來的,還記得我們剛才提到的法線與切線的公式,N·T=0不?現在我們就用它來反推出gl_NormalMatrix,不過這裡需要說明的是,由於接下來我們要用的是矩陣運算,因此必須把向量點乘轉化為矩陣乘法,方法就是將兩個向量都寫為矩陣,然後將第一個矩陣進行轉置,即A·B=MAT * MB,A和B為2個向量,MA和MB是它們的矩陣形式.
首先我們設n和t為變換前的法線和切線向量,n`和t`為變換後的法線和切線向量,我們得到了兩個約束條件:
n·t=0
n`·t`=0
如果將兩個向量計算公式轉化為矩陣計算公式的話,那麼就是:
NT * T = 0
N`T * T` = 0
其中N、N`、T和T`均為其小寫對應的向量的齊次坐標矩陣,即xyz直接對應原向量的xyz,w為0.
然後我們來研究如何表示T`,按照切線的定義,t等於其所在平面上任意兩點p0、p1之差,即
t=p0-p1
即使轉換成矩陣形式,只要兩者的齊次項係數w相等,等式也是成立的
T=P0-P1
而t`則等於經過變換後的p0`和p1`兩點之差,即
t`=p0`-p1`
T` = Mmv * P0 - Mmv * P1
其中Mmv為模型視圖矩陣,按照矩陣乘法的結合律,有
T` = Mmv * (P0 - P1) = Mmv * T
將T`代回原式,有
N`T * Mmv * T = 0
接下來我們研究的是如何用一個矩陣表示N`的變換,我們設那個矩陣為Mn,於是便有
N` = Mn * N
(Mn * N)T * Mmv * T = 0
NT * MnT * Mmv * T = 0
於是我們最後一步就是解上式,如果要直接硬上的話確實有點難算,但如果用到數學中鬼畜的"不妨設",那麼這個問題就迎刃而解了,所以不妨設
MnT * Mmv = I
那麼原式就變成
NT * I * T = NT * T = 0 成立
因此,我們只需要解MnT * Mmv = I就行了,結果顯然是:
Mn = (Mmv-1)T
因此,Mn在數學上等於gl_ModelViewMatrix的逆矩陣的轉置,由於gl_Normal是三維向量,而非是四維齊次坐標,因此gl_NormalMatrix只有Mn的左上3x3部分.
如果你還沒看暈的話,你大概會想起開頭我提到在大部分情況下gl_NormalMatrix就是gl_ModelViewMatrix的左上3x3,也就是說在大部分情況下
Mn = Mmv = (Mmv-1)T
如果你對數學足夠敏感,或者在線性代數課上睡的覺(jue)比較少的話,那麼你大概會發現在這種情況下Mmv是個正交矩陣,只有正交矩陣才會滿足Mmv-1 = MmvT的特性,也才會有Mmv = (Mmv-1)T.變換矩陣在幾何意義上只涉及到旋轉時,它始終是滿足正交性的.

附錄 - 淺談控制流與並行運算


想在着色器中通過條件分支來實現優化異常困難,不只因為執行條件分支本身對GPU來說就是一件困難的事,還因為一些GPU實現在底層就根本不支持分支.
眾所周知,GPU的並行運算是通過許多個核心同時處理數據來實現,Nvidia在2006年發布的Tesla架構中引入了SIMT(Single instruction, multiple thread)的概念,複數個核心作為線程被編為一組(GLSL規範要求4個一組) 運行時組內所有的線程都會執行相同的指令,但可以有不同的結果,比如如果一個線程負責處理一個像素的話,一條紋理取樣指令可以讓組內所有的線程都根據當前像素對應的UV從紋理中取得不同的顏色.
然而當引入條件分支時事情就有些棘手了,在一個IF-ELSE中很可能組內某幾個線程進入一條控制流路徑,其它線程進入另一條,這時SIMT不但幫不上忙,反而會阻礙分支的執行,因此大部分GPU使用的策略是執行全部的路徑,但對不符合分支條件的線程採取關閉或結果不寫回的手段,這也是為什麼條件分支在着色器中優化效果有限的原因,當然這並不代表就沒有效果,對於組內所有線程都不滿足條件的分支GPU是不會執行的,因此在着色器中一些大型的早期分支依然會有一定的優化效果.
然而仍然存在一種會對分枝產生影響的情況 - 並行性依賴指令(不用搜了,這個名詞是我隨口造的?),我們都知道texture紋理取樣指令對Mipmap紋理可以自行判斷最佳的LOD,那麼它是怎麼在只知道一個紋理坐標的情況下實現的?先聊個題外話,在GLSL中還有幾個很有意思的指令:dFdx、dFdy以及一系列它們的衍生指令,它們的作用是根據輸入值返回它在X/Y方向上的導數...這個鬼畜指令的原理就是依賴了並行性,或者說依賴了SIMT的特性,由於SIMT要求組內所有的線程在運行時都是在執行同一條指令,因此當一個線程執行並發性依賴指令時,它周圍所有的線程也都是在執行這個指令,這些線程只要相互交換數據,就可以根據已有的信息來執行一些特殊的操作,比如結合相鄰線程所執行的dFdx中的參數以及自己的參數來計算導數,紋理取樣時判斷LOD的原理也是根據相鄰線程的UV與自己的UV的差值來判斷最佳的LOD.在存在此類指令時,OpenGL可能會強制一些本來已經通過IF-RETURN來提前結束的分支繼續執行下去,以便獲取這些信息.
此外,還有一個關於片元着色階段的一個錯覺是"所有的像素真的是同時並行處理的"...想想看,一個1920x1080的屏幕會有二百萬個像素,老黃再瘋狂也造不出單卡二百萬核心的卡...所以在概念上雖然所有的像素都可以並行處理,但實際上還是要分個先後順序的,這個又涉及到了一個問題:紋理讀寫的競態條件,具體見下一節.

附錄 - 紋理讀寫的競態條件


上一個附錄提到了GPU其實做不到所有的像素都並行處理,這在使用幀緩衝和RenderToTexture時會有個問題,如果你向一個正在讀的紋理寫入數據的話,可能會引發各種喜聞樂見的事情.
這個問題具體還分為兩種,一種是在多次DrawCall時,着色器向正在讀取的紋理寫入數據,這個問題其實類似於CPU的多線程時內存可見性,OpenGL在引入Texture barrier前不要求實現保證第二次DrawCall一定能從紋理中讀取到第一次DrawCall寫入的數據,在沒有Texture barrier時想要破解這個問題也很簡單,重新綁定一下幀緩衝和紋理就行了,Shadersmod在進行多遍後處理時也這麼做了,於是幫我們迴避了一個問題.我們面臨的是第二種問題,在一次DrawCall時對同一張紋理即讀即寫,在正篇中我已經提到"如果對一個緩衝區既涉及到讀又涉及到寫的話,一個像素只能由負責向它寫入的那個片元着色器讀",顯然當一個GPU在分批處理屏幕上的像素時,如果一個對紋理即讀即寫的片元着色器需要從其他位置讀取像素的話,就會面臨一個問題,它讀到的可能是尚未被着色器處理過的像素,也可能讀到已經被處理過的像素.在編程時程序員往往期望讀到尚未被改寫過的紋理,如果它恰巧讀到已經被寫入過的紋理的話,程序的運行結果肯定會與預期不符.

2015-10-25_19.32.51

事實上,OpenGL並不鼓勵對紋理即讀即寫 - 即使是之前所說的一個像素只能由負責向它寫入的那個片元着色器讀,這裡它能運行只能說是顯卡允許這樣做,而不是規範允許這樣做,正確的處理姿勢是使用PingPong技術,準備兩塊幀緩衝區然後每次DrawCall讀一塊寫一塊,完成後兩塊交換.Shadersmod只留了一塊緩衝區,看上去是省了一點顯存(其實能省多少啊...MC本來就不吃顯存,一個RGBA8格式的1920x1080的顏色緩衝有8MB大小,8塊顏色緩衝外加它們的交換緩衝大約128MB,能開光影的顯卡哪個不是1G起底),但實際上卻多了不少麻煩,所以這時我們要麼手動在多塊幀緩衝間來回寫入,要麼冒險利用顯卡特性在一個紋理上小心地同時讀寫.

附錄 - 那些年,那些對蘋果的黑槍


儘管理論上OpenGL在各個平台中都擁有相同的特性,但事實上它就和早期使用AWT的Java GUI程序一樣,屬於"一次編寫,到處測試".
先來欽點一番巨軟,Windows對OpenGL雖然一直不用正眼看它,但諷刺的是Windows是對OpenGL支持最好的,除了那個需要double-boot的啟動方式(想要安全地創建OpenGL環境必須得傳入有效參數,而想要獲得所有可用的有效參數必須得先有個OpenGL環境...所以主流採用的方案是先用最低的參數配置啟動一個OpenGL環境,查到可用參數後立刻銷毀掉舊環境然後再去正式啟動)有些蹩腳,以及手動查詢函數指針的方式有些奇怪(其實也不是很奇怪,OpenGL確實允許這樣做)以外,別處還真沒有什麼大問題;在Linux上近些年隨着顯卡驅動的改善,對OpenGL的支持也不錯;那麼說到蘋果...好吧,《Windows遊戲編程大師技巧》的作者André LaMothe直言不諱地說遊戲程序員都不喜歡OSX,蘋果對OpenGL的態度近些年一直處於一個微妙的狀態,或許這和當年OpenGL推出核心模式時有關,在Khronos接管OpenGL主持權時,面臨的是一個腐朽的,充滿各種舊時代的各種廠家的奇技淫巧的API,為此Khronos花了三年的時間策划了OpenGL3.2時的"分裂",在OpenGL3.2時整個API集被分為兩部分,兼容模式(Compatibility Profile)擁有全部的API,你能從中找到1992年推出OpenGL1.0時的API,也能看到2009年OpenGL3.2中的DirectX10風格API;而核心模式(Core Profile)則是它的子集,只保留一套精簡的高性能API,舊的API只保留必要的部分,凡是擁有更新的替代品的全部剔除掉.在謀劃兩個模式時廠商們自然是根據自身的技術實力和利益在支持派和反對派中站隊,Nvidia和蘋果都是站在了支持派的隊伍中,其中Nvidia是喊得聲最大的,而蘋果是做的最積極的,在蘋果的最新驅動中就壓根沒提供兼容模式,然而當塵埃落定時Nvidia卻態度急轉,聲稱兼容模式依然有必要存在,被所有人擺了一遭的蘋果似乎就此黑化了,對驅動的改進從此也不再上心,時至今日OSX還具有三大操作系統上最獵奇的OpenGL模式,它有兩個模式:支持3.2以及更高的核心模式以及只支持2.1的遺留模式(Legacy)(事實上OSX的GUI系統乃至整個圖形系統就是個獵奇,你知道它有四套OpenGL接口嗎?),這意味着開發者要麼使用最新的API並拋棄掉所有舊的,要麼就只能停留在OpenGL2.1時代,通過擴展來調用新功能.不但如此,蘋果的顯驅對OpenGL的更新支持也明顯不如當初那麼勤快了,明眼人都能一眼看出拋棄了"業界標準"的蘋果心裡打着什麼算盤,果然這兩年蘋果發布了Metal這套自己做的圖形API,還號稱效率比OpenGL提高了多少多少,法克,就憑你那態度在OSX上OpenGL能快的了嗎...最近,原來的OpenGLNG,現在的Vulkan在被宣布後蘋果對它的態度一直不冷不熱,小夥伴們都猜得出被Khronos賣了一次的蘋果在好不容易造出自家的圖形API卻又被Khronos和ATI的Vulkan(ATI的Mantle是Vulkan的原型)拆台時的心情,也都做好了最壞的準備.

附錄 - 已知的Shadersmod Bug


聲明gdepth時如果使用的是colortex1而不是gdepth,並且沒有聲明默認格式的話,其格式會被初始化為RGBA8而不是RGBA32F.
composite中如果沒有添加DRAWBUFFERS的話在繪製水面時會有問題,似乎由於OpenGL API沒有被正確調用有關,具體見上文的"附錄 - DRAWBUFFERS導致的水面異常".
一旦啟用ShadowMap繪製的話,水下的霧會有問題,估計作者在繪製ShadowMap時改了霧參數然後忘了將它重置了...
某些關於ShadowMap的變量在光影包之間沒有被正確地隔離,在多個光影包之間切換可能導致某些舊的光影包中的控制參數被錯誤地保留到新的光影包中,具體是哪些參數尚不清楚...已確認的有shadowDistance(如果新光影包未聲明的話,會沿用舊光影包的設定,而不是重置回默認的160)
ShadowMap繪製階段沒有關閉Alpha測試,因為它被設計為能自適應樹葉等自帶透明部分的紋理(這也是為什麼你在ShadowMap繪製時必須輸出一個紋理顏色的原因),如果你希望在ShadowMap繪製時向顏色紋理輸出其他數據,比如打包成RGBA8的高精度深度數據的話,就得使用昂貴的MRT,在shadowcolor1中輸出你需要的數據,然後在shadowcolor/shadowcolor0中輸出紋理顏色. (其實透明剔除這種事情可以由開發者在着色器中完成,不是嗎?)
稱不上Bug的Bug,準確說是一個設計疏忽,就是即使只用到ShadowMap繪製階段輸出的顏色紋理(shadowcolor/shadowcolor0和shadowcolor1),而沒用到深度緩衝(shadow/shadowtex0和shadowtex1)的話,也必須聲明深度緩衝,否則系統不會啟動ShadowMap繪製.
depthtex1在玩家位於雲層以上高度時不包括雲我覺得與其說是特性倒不如說是Bug,因為遊戲的雲渲染分兩種情況,玩家在雲層下(y<=128)和玩家在雲層上(y>128),估計作者忘了處理第二種情況了...
迄今為止見過的最無語的Bug,在末地時Shadersmod會錯誤地開啟所有顏色緩衝的Alpha混合,而且混合方程好像還弄錯了,這導致如果你想讓你的光影包能在末地正常工作的話所有顏色緩衝的Alpha位就都不能用了,必須得輸出1.0.不過不用虛,我還沒見過幾個光影包能在末地正常工作,最好的也不過是把末地當正常世界的沙灘來渲染,居然還上了個炫光和體積雲...牛逼啊,其餘的光影包在末地就是一團黑.
在末地和下界sunPosition和moonPosition會變得無法使用,因為它們的數值將始終保持在你切換世界前那一刻的值(說白了就是在沒有天空的世界系統不更新sunPosition和moonPosition的值),而眼坐標系下的坐標又不可能在不實時更新的情況下使用...

附錄 - 主流光影包的技術特性


這裡記述了主流的光影包的高配版本的技術特性,供有心研(piao)究(qie)的人參考.
唔...我把它們寫在一個Excel表格里了,但還需要整理一番.

附錄 - 所有下載內容


這裡包含了本文中所有可供下載的內容:
[SkyDrive] [百度網盤]

附錄 - 教學着色器


教學着色器用於演示一些遊戲內的特性,可以從這裡下載: [SkyDrive] [百度網盤]

Tutorial_DefaultShadowMapping
演示了默認的Shadow Map
2015-09-13_23.33.31

Tutorial_StereographicShadowMapping
演示了係數為0.85的球面投影Shadow Map
2015-09-13_23.33.48

Tutorial_LinearDepth
演示了概念上的線性深度緩衝,注意這並非是真實的深度緩衝,而是根據3張深度貼圖重建來的線性深度緩衝,黑色代表距離近,白色代表距離遠,青色代表只有depthtex0有而depthtex1和2沒有的點,藍色代表depthtex0和1有而depthtex2沒有的點.
2015-09-13_23.28.42

Tutorial_LightSpacePosition
演示了光照坐標系下的像素位置.XYZ坐標被直接映射到RGB上.
2015-09-13_23.42.49

Tutorial_WorldSpacePosition
演示了世界坐標系下的像素位置.XYZ坐標被直接映射到RGB上.
2015-09-13_23.42.41

Tutorial_STCoordinate
演示了頂點的ST坐標,黑色為靠近(0,0)的一點,紅色為靠近(1,0)的一點,綠色為靠近(0,1)的一點,黃色為靠近(1,1)的一點.
2015-09-13_19.00.28

Tutorial_BadMatrixes
演示了gbufferModelView並不能取代gl_ModelViewMatrix,或者說是前者並不等價於後者.
2015-09-13_23.49.05