2018-12-06

【進度】3D背景—解決顏色問題、實裝影子

Cyber Sprite 2實裝3D背景第二階段。
之前的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。
    鈷寶:……嗯。
  • 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);
shadowMapMatrix和shadowMapProjectionCoef和上面Z pass shader裡的相同。
把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外掛,甚至一個模型編輯器也說不定。

沒有留言 :

張貼留言