前一篇:【進度】3D背景—實裝影子2、cube shadow map 1
巴哈姆特
官網
像解數學題目一樣,先把問題簡化看會變成怎麼樣,寫個較簡單的程式,只做建視窗、初始化Direct3D和cubemap,其他一概省略。
同時發在巴哈姆特
實驗一,cubemap內容來自圖檔,圖檔坐標與cubemap坐標的關係。
艾莉兒:照以前的測試程式嗎?好。
用CreateWindow()建視窗、建立ID3D11Device、IDXGISwapChain、ID3D11DeviceContext物件……
(初始化程式碼很長,在此省略)
艾莉兒:然後建一個ID3D11Texture2D物件,照前一篇說的設成ArraySize=6,MiscFlags=D3D11_RESOURCE_MISC_TEXTURECUBE。
以下是完整的建立貼圖程式碼,比較特別的地方是,貼圖用程式自動產生,再傳給顯卡。
//貼圖大小 const int TEX_W=64; const int TEX_H=64; const int TEX_PIXEL_NUM=TEX_W*TEX_H; //要建立的物件 ID3D11ShaderResourceView* cubeTexture; //create cube texture HRESULT hr; D3D11_TEXTURE2D_DESC td; td.Width=TEX_W; td.Height=TEX_H; td.MipLevels=1; td.ArraySize=6; //single image or cubemap td.Format=DXGI_FORMAT_B8G8R8A8_UNORM; td.SampleDesc.Count=1; //multisample sample number td.SampleDesc.Quality=0; td.Usage=D3D11_USAGE_DEFAULT; td.BindFlags=D3D11_BIND_SHADER_RESOURCE; td.CPUAccessFlags = 0; td.MiscFlags=D3D11_RESOURCE_MISC_TEXTURECUBE; ID3D11Texture2D* texture; hr=device->CreateTexture2D(&td, NULL, &texture); const uint32_t CUBE_FACES[]={0xffff0000, 0xff800000, 0xff00ff00, 0xff008000, 0xff0000ff,0xff000080}; //產生64×64 pixel的buffer uint32_t colorBuffer[TEX_PIXEL_NUM]; for(int i=0;i<6;i++){ for(int j=0;j<TEX_PIXEL_NUM;j++){ colorBuffer[j]=CUBE_FACES[i]; } colorBuffer[0]=0xffffffff; colorBuffer[1]=0xffffffff; colorBuffer[TEX_W]=0xffffffff; colorBuffer[TEX_W+1]=0xffffffff; colorBuffer[TEX_W-2]=0xff808080; colorBuffer[TEX_W-1]=0xff808080; colorBuffer[TEX_W*2-2]=0xff808080; colorBuffer[TEX_W*2-1]=0xff808080; colorBuffer[TEX_PIXEL_NUM-TEX_W-2]=0xff000000; colorBuffer[TEX_PIXEL_NUM-TEX_W-1]=0xff000000; colorBuffer[TEX_PIXEL_NUM-2]=0xff000000; colorBuffer[TEX_PIXEL_NUM-1]=0xff000000; context->UpdateSubresource(texture,i,NULL,colorBuffer, TEX_W*4, 0); } hr=device->CreateShaderResourceView(texture, NULL, &cubeTexture); texture->Release(); //扣掉CreateTexture2D()的reference count |
+X:紅
-X:暗紅
+Y:綠
-Y:暗綠
+Z:藍
-Z:暗藍
然後把colorBuffer裡左上角的點設成白色,右上角設成灰色,右下角設成黑色,藉此看出如果貼圖來自圖檔時,圖檔和cubemap的坐標怎麼對應。
shader就儘量寫得簡單,vertex shader只把坐標傳給pixel shader,pixel shader讀取cubemap並顯示在螢幕。
TextureCube<float4> cubeTexture1:register(t1); SamplerState defaultSampler:register(s0); struct DrawCubemapVsIn{ float2 pos:P; float3 cubeTexCoord:T; }; struct DrawCubemapVsOut{ float4 pos:SV_Position; float3 cubeTexCoord:T; }; void drawCubemapVS(in DrawCubeMapVsIn IN,out DrawCubeMapVsOut OUT){ OUT.pos=float4(IN.pos,0,1); OUT.cubeTexCoord=IN.cubeTexCoord; } float4 drawCubemapPS(in DrawCubeMapVsOut IN): SV_Target{ return cubeTexture1.Sample(defaultSampler, IN.cubeTexCoord); } |
鈷寶:好……。
然後,畫六個正方形把cube各面顯示出來,頂點坐標用寫在程式裡的方式。
struct DrawCubeVertex{ float pos[2]; //螢幕坐標 float cubeTexCoord[3]; //cubemap貼圖坐標 }; const DrawCubeVertex DRAW_CUBEMAP_VERTEX[]={ {-1, 0.25, 1, 1,-1}, //+X左邊 {-1, -0.25, 1,-1,-1}, {-0.5, 0.25, 1, 1, 1}, //+X與+Z的邊 {-0.5,-0.25, 1,-1, 1}, {0, 0.25, -1, 1,1}, //+Z與-X的邊 {0,-0.25, -1,-1,1}, {0.5, 0.25, -1, 1,-1}, //-X與-Z的邊 {0.5,-0.25, -1,-1,-1}, {1, 0.25, 1, 1,-1}, //-Z右邊 {1,-0.25, 1,-1,-1}, {1,-0.25, 0,0,0}, //dummy {-1,0.75, 0,0,0}, {-1, 0.75, -1, 1,-1}, //+Y face {-1, 0.25, 1, 1,-1}, {-0.5, 0.75,-1, 1, 1}, {-0.5, 0.25, 1, 1,1}, {-0.5, 0.25, 0,0,0}, //dummy {-1, -0.25, 0,0,0}, {-1, -0.25, 1, -1,-1}, //-Y face {-1, -0.75, -1, -1,-1}, {-0.5,-0.25, 1, -1, 1}, {-0.5,-0.75,-1, -1, 1}, }; const int DRAW_CUBEMAP_VERTEX_NUM = sizeof(DRAW_CUBEMAP_VERTEX)/sizeof(DrawCubeVertex); //幾何圖形設成triangle strip |
結果
Direct3D,左手坐標系1
字不是測試程式畫的,是之後標上去的。
放大後可看到正方形邊緣隱約有雜色,例如-X面上邊緣混了一點綠色。
這是顯卡做反鋸齒時,把相鄰像素也拿來內插造成的,這一面跟+Y面相鄰就混入+Y面的綠色。
實驗二,cubemap內容是用framebuffer object繪製,framebuffer坐標與cubemap坐標的關係。
建立貼圖的程式改成這樣
//貼圖大小 const int TEX_W=64; const int TEX_H=64; const int TEX_PIXEL_NUM=TEX_W*TEX_H; //要建立的物件 ID3D11ShaderResourceView* cubeTexture; ID3D11RenderTargetView* cubeRenderTarget[6]; //create cube texture HRESULT hr; D3D11_TEXTURE2D_DESC td; td.Width=TEX_W; td.Height=TEX_H; td.MipLevels=1; td.ArraySize=6; //single image or cubemap td.Format=DXGI_FORMAT_B8G8R8A8_UNORM; td.SampleDesc.Count=1; //multisample sample number td.SampleDesc.Quality=0; td.Usage=D3D11_USAGE_DEFAULT; //BindFlags跟上面不一樣 td.BindFlags=D3D11_BIND_SHADER_RESOURCE|D3D11_BIND_RENDER_TARGET; td.CPUAccessFlags = 0; td.MiscFlags=D3D11_RESOURCE_MISC_TEXTURECUBE; ID3D11Texture2D* texture; hr=device->CreateTexture2D(&td, NULL, &texture); D3D11_RENDER_TARGET_VIEW_DESC rtDesc; rtDesc.Format=DXGI_FORMAT_B8G8R8A8_UNORM; rtDesc.ViewDimension=D3D11_RTV_DIMENSION_TEXTURE2DARRAY; rtDesc.Texture2DArray.MipSlice=0; rtDesc.Texture2DArray.ArraySize=1; //六面各建立一個render target view for(int i=0;i<6;i++){ rtDesc.Texture2DArray.FirstArraySlice=i; hr=device->CreateRenderTargetView(texture, &rtDesc, cubeRenderTarget+i); } hr=device->CreateShaderResourceView(texture, NULL, &cubeTexture); texture->Release(); //扣掉CreateTexture2D()的reference count |
更新cubemap的shader這樣寫
struct UpdateCubemapVsOut{ float4 pos:SV_Position; float2 screenCoord:P; }; void updateCubemapVS(in float2 pos:P, out UpdateCubemapVsOut OUT){ //將-1~1變為0~1 OUT.screenCoord=pos*0.5+0.5; OUT.pos=float4(pos,0,1); } float4 updateCubemapPS(in UpdateCubemapVsOut IN): SV_Target{ return float4(IN.screenCoord.x, 0, IN.screenCoord.y, 1); } |
(-1,-1) : 黑
(1,-1) : 紅
(-1,1) : 藍
(1,1) : 紫
頂點這樣設,一個正方形佔滿整個面。
const float UPDATE_CUBEMAP_VERTEX[]={ -1,1, -1,-1, 1,1, 1,-1, }; |
最後再套用實驗一的頂點坐標和shader,把cubemap顯示在畫面。
Direct3D,左手坐標系2
艾莉兒:OpenGL要不要也試一下?OpenGL的貼圖坐標定義是上下顛倒的,不知道對cubemap有沒有影響。
說得也是,兩位,到Linux開發機工作吧。
實驗一,建貼圖的程式碼是這樣
//貼圖大小 const int TEX_W=64; const int TEX_H=64; const int TEX_PIXEL_NUM=TEX_W*TEX_H; //要建立的物件 uint32_t texture; //create cube texture glGenTextures(1,&texture); glBindTexture(GL_TEXTURE_CUBE_MAP,texture); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAX_LEVEL, 0); const int CUBE_FACES[]={0xffff0000, 0xff800000, 0xff00ff00, 0xff008000, 0xff0000ff,0xff000080}; //產生64×64 pixel的buffer uint32_t colorBuffer[TEX_PIXEL_NUM]; for(int i=0;i<6;i++){ for(int j=0;j<TEX_PIXEL_NUM;j++){ colorBuffer[j]=CUBE_FACES[i]; } colorBuffer[0]=0xffffffff; colorBuffer[1]=0xffffffff; colorBuffer[TEX_W]=0xffffffff; colorBuffer[TEX_W+1]=0xffffffff; colorBuffer[TEX_W-2]=0xff808080; colorBuffer[TEX_W-1]=0xff808080; colorBuffer[TEX_W*2-2]=0xff808080; colorBuffer[TEX_W*2-1]=0xff808080; colorBuffer[TEX_PIXEL_NUM-TEX_W-2]=0xff000000; colorBuffer[TEX_PIXEL_NUM-TEX_W-1]=0xff000000; colorBuffer[TEX_PIXEL_NUM-2]=0xff000000; colorBuffer[TEX_PIXEL_NUM-1]=0xff000000; glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X+i, 0, GL_RGBA8, TEX_W, TEX_H, 0, GL_BGRA, GL_UNSIGNED_BYTE, colorBuffer); } |
GL_TEXTURE_CUBE_MAP_POSITIVE_X
GL_TEXTURE_CUBE_MAP_NEGATIVE_X
GL_TEXTURE_CUBE_MAP_POSITIVE_Y
GL_TEXTURE_CUBE_MAP_NEGATIVE_Y
GL_TEXTURE_CUBE_MAP_POSITIVE_Z
GL_TEXTURE_CUBE_MAP_NEGATIVE_Z
但因為這六個剛好是連續整數,可以用「GL_TEXTURE_CUBE_MAP_POSITIVE_X+i」代替。
前一篇受過教訓,這次我沒忘了設定GL_TEXTURE_MAX_LEVEL了。
shader
//====common //要用OpenGL 3.3的規格 //如果不加這行,會視為最早的1.0版編譯 #version 330 core //====drawCubemapVS layout(location=0) in vec2 pos; layout(location=1) in vec3 cubeTexCoord; varying vec3 varCubeTexCoord; void main(){ varCubeTexCoord=cubeTexCoord; gl_Position=vec4(pos,0,1); } //====drawCubemapPS varying vec3 varCubeTexCoord; uniform samplerCube cubeTexture1; void main(){ gl_FragColor=texture(cubeTexture1,varCubeTexCoord); } |
我的做法是用「//====」當作分隔記號,然後叫鈷寶弄(寫個Python程式讀這個shader檔)。
鈷寶:嗯,搜尋「//====」,分隔字串……,然後各個字串分開打包……。
包進遊戲裡後,叫艾莉兒一個一個丟給驅動程式編譯。
頂點坐標跟上面Direct3D相同。
初始化時還要呼叫這個,才能有跟D3D11相同的結果。
glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS); |
與D3D的相同。
實驗二,這樣建貼圖
//貼圖大小 const int TEX_W=64; const int TEX_H=64; const int TEX_PIXEL_NUM=TEX_W*TEX_H; //要建立的物件 uint32_t texture; uint32_t framebuffer[6]; //create cube texture glGenTextures(1,&texture); glBindTexture(GL_TEXTURE_CUBE_MAP,texture); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAX_LEVEL, 0); glGenFramebuffers(6,cubeFramebuffer); //六面各建立一個framebuffer物件 for(int i=0;i<6;i++){ glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X+i, 0, GL_RGBA8, TEX_W, TEX_H, 0,GL_BGRA,GL_UNSIGNED_BYTE, 0); glBindFramebuffer(GL_FRAMEBUFFER,cubeFramebuffer[i]); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X+i, texture, 0); } |
更新cubemap的shader這樣寫
//====updateCubemapVS layout(location=0) in vec2 pos; varying vec2 varScreenCoord; void main(){ //將-1~1變為0~1 varScreenCoord=pos*0.5+0.5; gl_Position=vec4(pos,0,1); } //====updateCubemapPS varying vec2 varScreenCoord; void main(){ gl_FragColor=vec4(varScreenCoord.x,0,varScreenCoord.y,1); } |
與D3D上下相反。
鈷寶:主人,那個,右手坐標系呢?
嗯,雖然我的引擎使用左手坐標系,順便把各種情況都試試看好了。
與左手坐標系的差別是,+Z與-Z面要對調,畫cubemap的頂點陣列要把Z坐標正負相反。
const DrawCubeVertex DRAW_CUBEMAP_VERTEX[]={ {-1, 0.25, 1, 1, 1}, //+X左邊 {-1, -0.25, 1,-1, 1}, {-0.5, 0.25, 1, 1,-1}, //+X與-Z的邊 {-0.5,-0.25, 1,-1,-1}, {0, 0.25, -1, 1,-1}, //-Z與-X的邊 {0,-0.25, -1,-1,-1}, {0.5, 0.25, -1, 1, 1}, //-X與+Z的邊 {0.5,-0.25, -1,-1, 1}, {1, 0.25, 1, 1, 1}, //+Z右邊 {1,-0.25, 1,-1, 1}, {1,-0.25, 0,0,0}, //dummy {-1,0.75, 0,0,0}, {-1, 0.75, -1, 1, 1}, //+Y face {-1, 0.25, 1, 1, 1}, {-0.5, 0.75,-1, 1,-1}, {-0.5, 0.25, 1, 1,-1}, {-0.5, 0.25, 0,0,0}, //dummy {-1, -0.25, 0,0,0}, {-1, -0.25, 1, -1, 1}, //-Y face {-1, -0.75, -1, -1, 1}, {-0.5,-0.25, 1, -1,-1}, {-0.5,-0.75,-1, -1,-1}, }; |
結果
Direct3D,右手坐標系
OpenGL,右手坐標系
一切都明白了,全部就是下面的圖。
把展開方式改變一下比較好記,改成看cube內部的面,cubemap的應用大部分是假設從cube內部往外看。
OpenGL的framebuffer坐標上下顛倒,查了一下原因,找到這篇解釋。
https://stackoverflow.com/questions/11685608/convention-of-faces-in-opengl-cubemapping
他說因為Cubemap遵循RenderMan的規則,貼圖左上角是原點,但OpenGL定義貼圖原點=左下角=framebuffer的(-1,-1),必須上下顛倒才對得起來。
我在OpenGL原本就把矩陣設成上下顛倒,剛好抵消Y坐標顛倒的效果,詳細看這篇:
【程式】倒立的OpenGL貼圖坐標
如果貼圖來自圖檔,,一般圖檔以左上角為原點,但OpenGL定義傳給glTexImage2D()的圖以左下角為原點,也會負負得正,所以坐標跟D3D相同。
至於上面這張圖怎麼用?如果在Direct3D左手坐標系,想用framebuffer畫+X面,首先要把坐標轉換到cube本身的坐標系。
從上圖找Direct3D framebuffer坐標的+X面,可看出Fx軸指向-Z,Fy軸指向+Y,畫出下圖A。
但是3D繪圖要把坐標轉換成上圖B,+Z軸指向正面,所以要乘上「繞Y軸轉+90度」的矩陣。(以Y為軸的話,從Z轉到X是正)
如果引擎有lookAt的函式,可以乘上lookAt(1,0,0),上方是(0,1,0)的矩陣。
右手坐標系的話是這樣,要轉成-Z軸向正面。
除了旋轉-90度以外還要把X軸鏡射,culling方向也要隨之變更。
如果cubemap是來自圖檔,也要跟繪製的人講清楚是左手還右手、六個面怎麼排在一張圖裡,否則會變成鏡中世界。
弄清楚基礎知識後可以正式實裝了,艾莉兒,六個面的軸如下,照這樣旋轉鏡頭畫出六個面。
艾莉兒:OK!
//要把鏡頭面對這些方向 const float LOOKAT_DIR[6][3]={ {1,0,0}, //+X {-1,0,0}, //-X {0,1,0}, //+Y {0,-1,0}, //-Y {0,0,1}, //+Z {0,0,-1}, //-Z }; //上方,即旋轉後的Y軸 const float LOOKAT_TOP_DIR[6][3]={ {0,1,0}, {0,1,0}, {0,0,-1}, {0,0,1}, {0,1,0}, {0,1,0}, }; //第三個軸用外積求出 |
利用一個特性可簡單求出旋轉矩陣:如果知道旋轉後的坐標軸,把三個軸的向量填入3×3矩陣就是旋轉矩陣,要填入行還是列因矩陣運算方式而異,看向量乘以矩陣是向量放左邊還是右邊。
shader有個地方要改,前一篇在vertex shader裡有這一段。
//float3 worldPos為頂點在世界坐標系的位置 OUT.shadowMapPos.xyz=mul(float4(worldPos,1), shadowMap.viewMatrix); float3 absValue=abs(OUT.shadowMapPos.xyz); float maxComponent = max(absValue.x, max(absValue.y, absValue.z)); OUT.shadowMapPos.w = maxComponent*shadowMap.projCoef.z+ shadowMap.projCoef.w; //shadowMapPos.xyz=鏡頭坐標系,w=要跟shadow map比較的值 |
——cube shadow map要跟shadow map比較的值是XYZ裡絕對值最大的。
這兩句是對的,但如果有一個polygon橫跨cube的兩個面,兩個頂點的「絕對值最大的」不是同一個。
內插之後polygon內部的像素就會算錯,變成像下圖有多餘的影子。
所以找出最大值的工作要在pixel shader裡做。
//vertex shader只求出頂點在shadow map鏡頭坐標系的位置 OUT.shadowMapPos.xyz=mul(float4(worldPos,1), shadowMap.viewMatrix); OUT.shadowMapPos.w=0; //pixel shader float4 shadowMapPos=IN.shadowMapPos; float3 absValue=abs(shadowMapPos.xyz); float maxComponent = max(absValue.x, max(absValue.y, absValue.z)); //要與shadow map比較的值 shadowMapPos.w = maxComponent*shadowMap.projCoef.z + shadowMap.projCoef.w; shadowMapPos.w/=maxComponent; //仿照vertex shader算完後除以w的步驟 //用shadow sampler讀取貼圖 float notInShadow=cubeShadowMap1.SampleCmpLevelZero(shadowMapSampler, shadowMapPos.xyz, shadowMapPos.w); |
鈷寶:嗯。
…………
艾莉兒:經過三篇進度文的努力,總算完成實裝影子的任務啦!
無論光源在前後左右上下都可以產生影子了。
製作途中有個感想,用shadow map實作影子有shadow acne、要考慮解析度、以及一個shadow map可能不夠涵蓋所有物體的問題,另一種方法:shadow volume或許例外情況比較少,做一次就可用在各種狀況。
不過筆者研究過後,目前不用shadow volume的理由如下。
1.模型裡要有相鄰三角形的資訊,不然就要讀取模型時即時產生,PMD、PMX沒有這些資訊。
2.要有geometry shader才比較容易做,考慮支援的硬體和作業系統,決定目前不使用geometry shader。
關於第2點,研究過一些電腦、手機的硬體和GPU的資料後,我開的顯示晶片需求如下。
Windows Linux Windows App Android | :Direct3D 11 feature level 10_0 :OpenGL 3.3 :Direct3D 11 feature level 9_3 :OpenGL ES 3.0 |
沒有留言 :
張貼留言