作者: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详细的讲解了投影纹理以及投影计算的方法。我这里大概的讲解下这个流程:
前面我们已经讲解过了如何将一张贴花投影到世界中去。从中你可以看出,不同的贴花观察空间需要计算不同的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(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:
#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的截图:
[1] Projective Texture Mapping
[2] How to project decals
[3] Drawing Stuff On Other Stuff With Deferred Screenspace Decals