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