Cyber Sprite 2實裝3D背景第二階段。
之前的3D研究進度在此
【進度】3D背景實裝過程同時發(fā)在官網(wǎng)上次做到這樣,有些部位顏色錯了。
下一步是解決反鋸齒計算時,被隔壁像素影響的問題。
由於目前的貼圖擠得滿滿的,沒有地方增加邊緣。
改良的第一步是叫3D美術(shù)把貼圖改小一點。
然後我把合併的貼圖重新排列,每塊之間留一些空隙。
叫鈷寶修改模型檔裡的貼圖檔名和貼圖坐標(biāo)。
鈷寶:……好。(開始工作,修改PMD檔的內(nèi)容)這裡的鈷寶現(xiàn)實中是個Inkscape外掛,上次就寫好的。
順便做個小實驗,把底色改成粉紅色。
輸出貼圖,然後用PmxEditor看模型。
到處都有粉紅色的線,這就是因為在每塊的邊緣,反鋸齒計算把外面的粉紅色也拿來內(nèi)插造成的。
再來真正對貼圖做處理了,做法如下,把邊緣的像素複製,往外擴張數(shù)個pixel。
至於用什麼方法?可以用GIMP手動複製貼上,也可以用Inkscape的pattern功能截取圖的一部分,但是數(shù)量很多人工做很費事,有必要自動化。有找了一下GIMP裡有沒有現(xiàn)成的濾鏡可以做這件事……,發(fā)現(xiàn)沒有。
還是要靠我的電子妖精了。
檢查並修改像素的值是很底層的操作,雖然是要做輔助工具,但鈷寶不擅長做這個(難以用GIMP或Inkscape的外掛,或是用腳本語言做到),要叫艾莉兒弄(用C++寫個程式)。
艾莉兒,第一步是用WIC讀取PNG檔。
艾莉兒:好,開始!(做法像這篇寫的:
【程式】讀取圖檔的方法-Windows篇)
再來用迴圈檢查每個像素。
艾莉兒:我看看……,先檢查周圍四個像素是不是透明。艾莉兒:兩個不透明代表角落,三個不透明代表邊,要修改周圍像素,其他情況可以不用管。(不知道有沒有更快的演算法,不過輔助工具不要求速度,作者自己的電腦跑得動就行了,不調(diào)效能也沒關(guān)係)
弄完了,用WIC存回PNG檔。
艾莉兒:(工作中)……,大功告成!主人請過目。用PmxEditor看看。
這樣沒有奇怪的顏色了。
3D美術(shù)跟我說希望加上影子,也就是阻擋陰影,目前為止都只有做背光陰影。
有畫圖的人應(yīng)該都知道,陰影有背光產(chǎn)生的和阻擋產(chǎn)生的兩種。
3D繪圖裡,背光陰影可以用頂點法線和光源位置算出來,但是阻擋陰影就沒那麼簡單了,現(xiàn)實中看似平常的現(xiàn)像,在電腦裡模擬要費一番工夫。
我手上一本書有介紹影子的做法,可以照它的方法做而不用自己發(fā)明,一般有兩種方法:shadow map和shadow volume。其中shadow volume需要模型裡有記錄相鄰三角形的資訊,PMD和PMX沒有此資訊,而且用geometry shader才比較容易計算,考慮不想讓硬體要求太高,以及以後可能會寫手機軟體,目前不想用geometry shader。
(雖然畫「
【進度】Cyber Sprite外語版 (3)」的插圖時開發(fā)出用shadow volume畫影子的方法,但此法不能用在3D繪圖)
shadow map的做法大致如下:
要修改很多地方。
- 需要建立另一種framebuffer物件:有Z buffer、Z buffer可被其他shader讀取、無color buffer。
艾莉兒:好。
- 3D繪圖流程要改,畫3D scene之前多一個步驟:對特定光源畫出shadow map。
Direct3D的設(shè)定如sampler、viewport、rasterizer state也要準(zhǔn)備一份畫shadow map專用的。
艾莉兒:好,再加一些東西。
- 各種3D物件除了畫出本體的draw()函式,還要多一個只更新Z buffer的drawZPass()函式。
艾莉兒:慢一點,等我弄好前面的。
現(xiàn)在只有一種類型的3D物件:靜態(tài)模型,以後如果要做3D遊戲可能還有其他的,每種要各別處理。
- 增加用來畫shadow map的shader,只要計算頂點位置,不需要貼圖、法線、打光。
//CPU傳來的頂點資料 struct modelZPassVsIn{ float3 pos:P; };
//vertex shader傳給pixel shader的資料 struct modelZPassVsOut{ float4 pos:SV_POSITION; };
void modelZPassVS(in modelZPassVsIn IN, out modelZPassVsOut OUT){ float4 viewPos = transform3D(IN.pos, shadowMapMatrix); OUT.pos = projection3D(viewPos, shadowMapProjectionCoef); //transform3D和projection3D是我自己寫的函式 //這次的光源是點光源,所以跟透視投影一樣要乘上投影系數(shù)。 }
float4 modelZPassPS(modelZPassVsOut IN):SV_Target { return float4(0,0,0,0); } |
這部分是鈷寶的工,寫好後請她編譯和打包shader。
鈷寶:……嗯。
- shader與input layout是成對的,新增shader也要新增對應(yīng)的input layout。
先拿畫物件本體的D3D11_INPUT_ELEMENT_DESC試試看,如果可以用就不用寫新的。
艾莉兒:我試試看……,可以用,沒問題。
艾莉兒:主人,一次加那麼多東西跑起來沒問題嗎?總算可以做個初步測試,以上過程像在安裝零件,只能靠想像完成時的樣子來寫程式,全部裝好了才能起動機器跑跑看。
————————
艾莉兒:報告主人,不能建shadow map的貼圖。D3D11建立framebuffer或貼圖時要先建立一個Texture2D物件,再用它建立render target view、depth stencil view或shader resource view物件。由於shadow map會被用在depth buffer也會被其他shader讀取,BindFlags要設(shè)成「D3D11_BIND_DEPTH_STENCIL|D3D11_BIND_SHADER_RESOURCE」,但這樣寫建不出貼圖。
D3D11_TEXTURE2D_DESC td; //填入其他屬性 …… td.BindFlags=D3D11_BIND_DEPTH_STENCIL|D3D11_BIND_SHADER_RESOURCE; td.Format=DXGI_FORMAT_D32_FLOAT;
ID3D11Texture2D* texture; device->CreateTexture2D(&td, NULL, &texture); |
研究一下,發(fā)現(xiàn)Format必須設(shè)成TYPELESS,之後建depth stencil view和shader resource view時分別指定格式。
通常情況下只要建立Texture2D時指定格式即可,建立view物件時不用再設(shè)定,但建立shadow map時不一樣。
D3D11_TEXTURE2D_DESC td; //填入其他屬性 …… td.BindFlags=D3D11_BIND_DEPTH_STENCIL|D3D11_BIND_SHADER_RESOURCE; td.Format = DXGI_FORMAT_R32_TYPELESS;
ID3D11Texture2D* texture; device->CreateTexture2D(&td, NULL, &texture);
D3D11_DEPTH_STENCIL_VIEW_DESC dsDesc; ZeroMemory(&dsDesc, sizeof(D3D11_DEPTH_STENCIL_VIEW_DESC)); dsDesc.Format = DXGI_FORMAT_D32_FLOAT; dsDesc.ViewDimension=D3D11_DSV_DIMENSION_TEXTURE2D; ID3D11DepthStencilView* dsView; device->CreateDepthStencilView(texture, &dsDesc, &dsView);
D3D11_SHADER_RESOURCE_VIEW_DESC srDesc; ZeroMemory(&srDesc, sizeof(D3D11_SHADER_RESOURCE_VIEW_DESC)); srDesc.Format = DXGI_FORMAT_R32_FLOAT; srDesc.ViewDimension=D3D11_SRV_DIMENSION_TEXTURE2D; srDesc.Texture2D.MipLevels=1; ID3D11ShaderResourceView* srView; device->CreateShaderResourceView(texture, &srDesc, &srView); |
————————
相關(guān)物件的建立都沒問題了,先做個小實驗,試試看在shader讀取Z buffer,然後顯示出來。
寫一個讀取Z buffer的shader叫鈷寶打包,整合進引擎。
然後叫艾莉兒套用貼圖和shader畫出Z buffer。
場景
Z buffer是這樣
顏色越淺代表距離越遠。
shader裡讀取Z buffer後有用一行「pow(zBuffer, 8);」調(diào)整數(shù)值,因為依照3D繪圖裡Z buffer的設(shè)計,Z buffer大部分區(qū)域都是接近1,如果不用pow()縮小數(shù)值看起來會一片白。
————————
再來要真正實裝了,畫模型的shader裡要讀取shadow map做計算。
shadow map基本觀念不難,但製作時還有一些細節(jié)要注意,有查了一些資料,主要參考這篇。
https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping如這篇所說直接畫會產(chǎn)生奇怪的紋路(shadow acne),解法是修改cull設(shè)定,Z pass時改成畫back face,畫本體時再改成畫front face。
vertex shader要做兩種坐標(biāo)轉(zhuǎn)換並輸出兩個位置,一個是轉(zhuǎn)換到真正的鏡頭,一個是把光源當(dāng)作鏡頭來轉(zhuǎn)換,後者用來從shadow map讀取像素。
大概像這樣
//IN.pos是頂點坐標(biāo)
//實際的位置 float3 viewPos = transform3D(IN.pos, modelViewMatrix); OUT.pos = projection3D(viewPos, projectionCoef); //在shadow map裡的位置 float3 shadowMapPos = transform3D(IN.pos, shadowMapMatrix); OUT.shadowMapPos = projection3D(shadowMapPos, shadowMapProjectionCoef); |
shadowMapMatrix和shadowMapProjectionCoef和上面Z pass shader裡的相同。
把OUT.pos和OUT.shadowMapPos傳給pixel shader做之後的計算。
pixel shader裡還要做點處理才能得到真正的坐標(biāo)。
//要先除以w float3 shadowMapPos = IN.shadowMapPos.xyz/IN.shadowMapPos.w; //此時xy=-1~1,轉(zhuǎn)換成0~1且y要反向 shadowMapPos.xy = shadowMapPos.xy*float2(0.5,-0.5) + 0.5; //讀取shadow map float outOfShadow = shadowMap1.SampleCmpLevelZero( shadowMapSampler, shadowMapPos.xy, shadowMapPos.z); |
點光源可以朝四面八方照射但shadow map的範(fàn)圍有限,3D空間裡shadow map配置在哪裡也是要考慮的,要看情況調(diào)整光源的矩陣,儘量讓shadow map涵蓋到可視範(fàn)圍。
本遊戲還算比較好調(diào),模型只用在背景,只會從前方看過去而不會進到場景裡亂跑。
覺得有個地方也要改,之前打光是在鏡頭坐標(biāo)系計算,改成世界坐標(biāo)系似乎比較好,本來shader裡用一個modelView矩陣計算,改成model矩陣和view矩陣分開。
發(fā)現(xiàn)計算鏡頭位置的算式有錯,順便修正。
為了實裝shadow map,畫一個物體要寫三個版本的shader。
1.畫出物體,不考慮影子。
2.Z pass,只更新Z buffer的shader。
3.畫出物體,會計算影子。
引擎裡render物體的部分也要分成這三種情況。
畫影子也是計算量較大的操作,所以預(yù)定做成可以在option開關(guān),玩家的配備太弱的話可以設(shè)成不畫影子。
————————
艾莉兒:framebuffer、sampler、cull設(shè)定、矩陣計算……,還有新增的render流程……,好多啊。鈷寶:(編譯及打包shader,包括新增的和修改做法的)……。好了,主人。艾莉兒:主人,我們準(zhǔn)備好了,動手吧!弄了上面一大串,總算可以看到成果了。
把以前做實驗用過的Patchouli也拿來試試看。
人、椅子、隔板這些物體可以擋住光線,在後面產(chǎn)生影子了。
人的大小我沒有仔細調(diào)整,可能跟場景不太能配合。
Patchouli的模型來自這裡。
https://mikumikudance.fandom.com/wiki/Patchouli_Knowledge_(Zakoneko)由於還沒做3D骨骼動畫,目前還沒辦法讓她動。
shadow map解析度會有影響,這張圖開1024×1024還是會有shadow acne,要開到2048×2048才行。
相對地另一種畫影子的方法:shadow volume有不受解析度限制的好處。
附帶一提,上圖是用一個自製工具顯示3D畫面。
做個工具比較方便調(diào)參數(shù),可以調(diào)打光、鏡頭位置等等,調(diào)好再寫進程式裡。
還有一個地方有問題,二樓地板沒有產(chǎn)生影子。
這是因為畫shadow map的時候修改cull設(shè)定,改成畫back face,而此處的背後因為遊戲中看不到,沒有做背面。
做遊戲把玩家看不到的地方省略是常用做法,但有畫影子的時候就不能省略背面了,要叫美術(shù)改一下。
shadow map總算做到可以用的程度,有夠難做。
以上是用Direct3D 11寫,Linux版要再寫一份OpenGL的,演算法照抄就行了不是難事。
過程中又忍不住想抱怨,Blender匯入OBJ的功能有問題,匯入PMD的plugin試了好幾個也找不到能用的,所以無法用Blender修改模型檔,想編輯模型只能靠PmxEditor,不然就是派我的電子妖精上場。
別人的輪子有問題的時候也只好自己發(fā)明一個,哪天我會想自己寫個Blender外掛,甚至一個模型編輯器也說不定。