GraphicsLab Project之簡易貼畫系統(Decal System)
作者:i_dovelemon
日期:2018-08-28
來源:CSDN
主題:Projection Texture Mapping, Decal System
引言
遊戲開發過程中有一個非常重要的功能:貼花(Decal)。這個功能指的是在多邊形表面上繪製出其他圖形,例如子彈擊打到牆壁時的彈孔,英雄擊打地面時產生的裂紋,車輛移動時的軌跡,遊戲中玩家向牆上噴繪的logo等等。這樣的功能,我們稱之為:貼花(Decal)。
貼花實現的方法
經過一番調查,發現貼花的實現方法有以下四種可以使用:
方法 | 缺點 |
---|---|
投影網格 | 需要進行三角形級別的碰撞檢測,由於是附著在物體表面之上的網格,存在z-fighting |
投影貼圖 | 對於不同角度貼花,需要將場景多次分批次進行繪製,drawcall壓力過大 |
超大貼圖 | 嚴重依賴系統基於mega texture的功能 |
螢幕空間 | 嚴重依賴系統基於延遲渲染的渲染路徑 |
我的實現
從上面不同方法的分析,我們可以從中選擇合適的實現方法。現如今,大多成熟渲染系統都是基於延遲渲染的,所以他們大多使用的是基於螢幕空間的貼花系統,易於實現且功能強大。
由於我的GLB Framework目前還是基於Forward的框架,所以此種方法暫時無法使用。
而超大貼圖的方法,依賴於系統是否使用了mega texture的功能,我的系統也沒有使用這種功能,所以拋棄了。
那麼只剩下了投影網格和投影紋理這兩種不同的方法。投影網格雖然有效,但是三角形級別的碰撞檢測,會增加系統的複雜程度,同時z-fighting效果也不可避免。為了消除z-fighting,需要進行很多額外的消隱褪去的操作。所以我並不喜歡這種方法。
那麼就只剩下了投影紋理的方法。投影紋理能夠很好的解決投影網格z-fighting的問題。這個方法,是在紋理取樣級別,將貼花的圖取樣到物體表面上,屬於完全的貼合在物體表面上。唯一的問題,不同角度,不同樣式的貼花需要將場景繪製多次。一旦場景中貼花數量過多,就會導致draw call壓力過大,這也是個很麻煩的問題。
但是,由於我的遊戲是一個俯視角的3D遊戲。貼花主要集中在XZ平面之上,所以我使用了一個預處理,使得不需要增加額外的場景繪製就能夠實現decal的功能。
在講解整個簡易貼花系統的實現之前,我們先來了解下投影貼圖的實現方法。
投影貼圖
我們知道現在的3D光柵化流水線的基本操作如下:
也就是說,我們定義了世界和一個觀察空間,然後將觀察空間裡面的世界部分變成了一張圖片顯示在了螢幕上。那麼,也就是說螢幕上顯示的這張圖片和觀察到的空間是一個對應的關係。明白了這點,我們就可以假設如果我們提前給出一張圖片(比如貼花系統裡面的貼花圖),那麼觀察空間裡面必然每一個點都對應了這張圖上的某一個畫素。根據這個關係,我們就能夠實現貼花的功能。
前面舉例的時候,使用的觀察空間是螢幕上玩家觀察的觀察空間。但是這個觀察空間實際上是可以任意指定的,我們可以根據我們的需要來指定貼花投影的觀察空間,這樣這個觀察空間裡面對應的世界空間就能夠通過一系列的計算得到我們指定的貼花圖中的某一個畫素,從而將貼圖顯示在世界空間裡面。
投影計算
Nvidia有一篇paper詳細的講解了投影紋理以及投影計算的方法。我這裡大概的講解下這個流程:
如果你曾經做過Shadow Map相關的功能,那麼你就會發現前面的操作流程和訪問shadow map的方法一模一樣。的確,shadow map也使用了投影紋理的相關技術。
實現
前面我們已經講解過了如何將一張貼花投影到世界中去。從中你可以看出,不同的貼花觀察空間需要計算不同的View Matrix和Projection Matrix,需要將場景繪製多次(或者傳遞一堆貼花貼圖和矩陣到shader中去),嚴重影響效率。根據前面我們的描述,由於我的遊戲採用的是俯視角,而貼花也主要集中在XZ平面之上,所以就有了這樣的一個優化方案:
我們提前將所有的貼花繪製到一張大圖上去,然後將這張大圖作為貼花使用上面的流程投影到世界空間中去。
能這麼做的基礎就是前面提到的貼花都在XZ平面上。如果你的需求和我相似,那麼你也可以使用這樣的方法來實現。
下面來看看程式碼的實現:
// Create RenderTarget
m_DecalRenderTarget = render::RenderTarget::Create(2048, 2048);
m_DecalMap = render::texture::Texture::CreateFloat32Texture(2048, 2048);
m_DecalRenderTarget->AttachColorTexture(render::COLORBUF_COLOR_ATTACHMENT0, m_DecalMap);
首先建立了一個2048x2048的RenderTarget。這裡的尺寸你可以根據實際需要選擇你需要的尺寸,不過建議使用正方形的貼圖可以方便後面計算投影矩陣。
// Change Texture parameters
glBindTexture(GL_TEXTURE_2D, reinterpret_cast<GLuint>(m_DecalMap->GetNativeTex()));
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
設定貼花貼圖的取樣方式。這裡使用的是CLAMP_TO_BORDER的形式。也就是說,一旦取樣的紋理座標不再[0,1]之間,就會返回一個(0, 0, 0, 0)的顏色,方便後面進行貼花貼圖與場景貼圖的混合操作。
void UpdateDecalPos() {
static int32_t sFrame = 0;
if (sFrame == 0) {
// Create Decal position
auto RandRange = [](int min, int max) {
return min + rand() % (max - min);
};
for (math::Vector& pos : m_DecalPos) {
pos = math::Vector(1.0f * RandRange(-20.0f, 20.0f), 0.0f, 1.0f * RandRange(-20.0f, 20.0f));
}
// Create Decal View Projection Matrix
m_DecalViewProjM = CalculateDecalViewProjMatrix();
}
sFrame = sFrame + 1;
if (sFrame > 100) sFrame = 0;
}
每隔100FPS,隨機的改變下所有貼花的位置,然後根據這些位置計算最終的View Matrix和Projection Matrix。
math::Matrix CalculateDecalViewProjMatrix() {
math::Vector minPos(FLT_MAX, FLT_MAX, FLT_MAX);
math::Vector maxPos(-FLT_MAX, -FLT_MAX, -FLT_MAX);
for (math::Vector& pos : m_DecalPos) {
if (pos.x < minPos.x) minPos.x = pos.x;
if (pos.y < minPos.y) minPos.y = pos.y;
if (pos.z < minPos.z) minPos.z = pos.z;
if (pos.x > maxPos.x) maxPos.x = pos.x;
if (pos.y > maxPos.y) maxPos.y = pos.y;
if (pos.z > maxPos.z) maxPos.z = pos.z;
}
maxPos = maxPos + m_Decal->GetBoundBoxMax();
minPos = minPos + m_Decal->GetBoundBoxMin();
math::Matrix mat;
// Size
float width = (maxPos.x - minPos.x);
float height = (maxPos.z - minPos.z);
width = height = max(width, height);
float depth = 10.0f + (maxPos.y - minPos.y);
// Camera position in world space
math::Vector pos = (maxPos + minPos) * 0.5f;
// Target position in world space
math::Vector look_at = math::Vector(0.0f, 1.0f, 0.01f);
look_at.w = 0.0f;
math::Vector target = pos + look_at;
// Orthogonal projection matrix
math::Matrix proj;
proj.MakeOrthogonalMatrix(-width / 2.0f, width / 2.0f, - height / 2.0f, height / 2.0f, - depth / 2.0f, depth / 2.0f);
// View matrix
math::Matrix view;
view.MakeViewMatrix(pos, target);
// Shadow matrix
mat.MakeIdentityMatrix();
mat.Mul(proj);
mat.Mul(view);
return mat;
}
構造一個在XZ平面上足夠大的觀察空間,可以將所有的貼花貼圖都觀察到。觀察的方向總是向-Y軸看(注意上面的程式碼為了防止除0異常,給觀察方向Z加上了一點偏移)。這裡定義的觀察空間是一個長方體而不是平截頭體,這就意味著我們需要使用正交投影矩陣。這個觀察空間最重要的是XZ平面的大小,Y平面上的深度無關緊要,只要不是0即可,所以我在處理depth的時候,以10為基礎。
有了這些準備工作,接下來就將所有的貼花貼圖繪製到前面我們建立的更大的render target之上:
void DrawDecal() {
// Setup render target
render::Device::SetRenderTarget(m_DecalRenderTarget);
render::Device::SetDrawColorBuffer(render::COLORBUF_COLOR_ATTACHMENT0);
// Setup viewport
render::Device::SetViewport(0, 0, 2048, 2048);
// Clear
render::Device::SetClearColor(0.0f, 0.0f, 0.0f, 0.0f);
render::Device::SetClearDepth(1.0f);
render::Device::Clear(render::CLEAR_COLOR | render::CLEAR_DEPTH);
// Setup shader
render::Device::SetShader(m_DecalProgram);
render::Device::SetShaderLayout(m_DecalProgram->GetShaderLayout());
// Setup texture
render::Device::ClearTexture();
render::Device::SetTexture(0, render::texture::Mgr::GetTextureById(m_Decal->GetTexId(scene::Model::MT_ALBEDO)), 0);
// Setup mesh
render::Device::SetVertexLayout(render::mesh::Mgr::GetMeshById(m_Decal->GetMeshId())->GetVertexLayout());
render::Device::SetVertexBuffer(render::mesh::Mgr::GetMeshById(m_Decal->GetMeshId())->GetVertexBuffer());
// Setup render state
render::Device::SetDepthTestEnable(true);
render::Device::SetCullFaceEnable(false);
render::Device::SetCullFaceMode(render::CULL_BACK);
for (math::Vector& pos : m_DecalPos) {
math::Matrix wvp = m_DecalViewProjM * math::Matrix::CreateTranslateMatrix(pos.x, pos.y, pos.z);
// Setup uniform
render::Device::SetUniformMatrix(m_DecalShaderWVPLoc, wvp);
render::Device::SetUniformSampler2D(m_DecalShaderAlbedoLoc, 0);
// Draw
render::Device::Draw(render::PT_TRIANGLES, 0, render::mesh::Mgr::GetMeshById(m_Decal->GetMeshId())->GetVertexNum());
}
// Reset viewport
render::Device::SetViewport(0, 0, app::Application::GetWindowWidth(), app::Application::GetWindowHeight());
// Reset render target
render::Device::SetRenderTarget(render::RenderTarget::DefaultRenderTarget());
}
在得到了這張合成之後的貼花圖之後,我們就可以繪製正常場景,並且在場景裡面訪問這張貼圖,然後進行合成。
void DrawFloor() {
// Setup shader
render::Device::SetShader(m_ColorProgram);
render::Device::SetShaderLayout(m_ColorProgram->GetShaderLayout());
// Setup texture
render::Device::ClearTexture();
render::Device::SetTexture(0, m_FloorAlbedoMap, 0);
render::Device::SetTexture(1, m_DecalMap, 1);
// Setup mesh
render::Device::SetVertexLayout(render::mesh::Mgr::GetMeshById(m_Floor->GetMeshId())->GetVertexLayout());
render::Device::SetVertexBuffer(render::mesh::Mgr::GetMeshById(m_Floor->GetMeshId())->GetVertexBuffer());
// Setup render state
render::Device::SetDepthTestEnable(true);
render::Device::SetCullFaceEnable(true);
render::Device::SetCullFaceMode(render::CULL_BACK);
// Setup uniform
static float sRotX = 0.0f, sRotY = 0.0f;
static float sPosX = 0.0f, sPosY = 0.0f, sPosZ = 0.0f;
math::Matrix world;
world.MakeIdentityMatrix();
float mouseMoveX = Input::GetMouseMoveX();
float mouseMoveY = Input::GetMouseMoveY();
sRotX = sRotX + mouseMoveX * 0.1f;
sRotY = sRotY + mouseMoveY * 0.1f;
world.RotateY(sRotX);
world.RotateX(sRotY);
if (Input::IsKeyboardButtonPressed(BK_A)) {
sPosX = sPosX + 0.1f;
} else if (Input::IsKeyboardButtonPressed(BK_D)) {
sPosX = sPosX - 0.1f;
}
if (Input::IsKeyboardButtonPressed(BK_Q)) {
sPosY = sPosY + 0.1f;
} else if (Input::IsKeyboardButtonPressed(BK_E)) {
sPosY = sPosY - 0.1f;
}
if (Input::IsKeyboardButtonPressed(BK_W)) {
sPosZ = sPosZ + 0.1f;
} else if (Input::IsKeyboardButtonPressed(BK_S)) {
sPosZ = sPosZ - 0.1f;
}
world.Translate(sPosX, sPosY, sPosZ);
math::Matrix wvp;
wvp.MakeIdentityMatrix();
wvp = m_Proj * m_View * world;
math::Matrix inv_trans_world = world;
inv_trans_world.Inverse();
inv_trans_world.Transpose();
render::Device::SetUniformMatrix(m_ColorShaderWVPLoc, wvp);
render::Device::SetUniformMatrix(m_ColorShaderDecalWVPLoc, m_DecalViewProjM);
render::Device::SetUniformSampler2D(m_ColorShaderAlbedoLoc, 0);
render::Device::SetUniformSampler2D(m_ColorShaderDecalMapLoc, 1);
// Draw
render::Device::Draw(render::PT_TRIANGLES, 0, render::mesh::Mgr::GetMeshById(m_Floor->GetMeshId())->GetVertexNum());
}
上面兩個步驟分別使用瞭如下兩個shader:
這是一個很簡單的shader如下:
#version 330
in vec3 glb_attr_Pos;
in vec2 glb_attr_TexCoord;
uniform mat4 glb_WVP;
out vec2 vs_TexCoord;
void main() {
gl_Position = (glb_WVP * vec4(glb_attr_Pos, 1.0));
vs_TexCoord = glb_attr_TexCoord;
}
#version 330
in vec2 vs_TexCoord;
out vec4 oColor;
uniform sampler2D glb_DecalTex;
void main() {
oColor = texture(glb_DecalTex, vs_TexCoord);
}
#version 330
in vec3 glb_attr_Pos;
in vec2 glb_attr_TexCoord;
uniform mat4 glb_WVP;
out vec2 vs_TexCoord;
out vec4 vs_Vertex;
void main() {
gl_Position = (glb_WVP * vec4(glb_attr_Pos, 1.0));
vs_TexCoord = glb_attr_TexCoord;
vs_Vertex = vec4(glb_attr_Pos, 1.0);
}
#version 330
in vec2 vs_TexCoord;
in vec4 vs_Vertex;
out vec3 oColor;
uniform sampler2D glb_AlbedoTex;
uniform sampler2D glb_DecalTex;
uniform mat4 glb_DecalWVP;
void main() {
vec3 albedo = texture(glb_AlbedoTex, vs_TexCoord).xyz;
vec4 decalTexcoord = glb_DecalWVP * vs_Vertex;
decalTexcoord.xyz /= 2.0;
decalTexcoord.xyz += 0.5;
decalTexcoord.xyz /= decalTexcoord.w;
vec4 decal = texture(glb_DecalTex, decalTexcoord.xy);
oColor = albedo * (1.0 - decal.w) + decal.xyz * decal.w;
}
總結
自此,一個簡易的用於俯視角的貼花系統就成功了。當然,這裡旨在給出基本的概念,可以在此基礎之上進行更加複雜的擴充套件,實現你們想要的功能。完整的程式碼可以在這裡找到。
以下是本次demo的截圖: