作者:i_dovelemon
日期:2018-08-28
来源:优快云
主题:Projection Texture Mapping, Decal System
引言
游戏开发过程中有一个非常重要的功能:贴花(Decal)。这个功能指的是在多边形表面上绘制出其他图形,例如子弹击打到墙壁时的弹孔,英雄击打地面时产生的裂纹,车辆移动时的轨迹,游戏中玩家向墙上喷绘的logo等等。这样的功能,我们称之为:贴花(Decal)。
贴花实现的方法
经过一番调查,发现贴花的实现方法有以下四种可以使用:
- 投影网格,就是根据计算在物体的表面实际添加一层网格用来绘制贴花
- 投影贴图,根据投影贴图的功能实现,在绘制的过程中将贴图直接投影到物体表面上
- 超大贴图,如果你的系统使用的是超大贴图,即整个场景使用一张贴图(id soft提出的算法),那么就可以简单的更改这张贴图实现贴花功能
- 屏幕空间贴花,在屏幕空间实现贴花的功能
-
方法 缺点 投影网格 需要进行三角形级别的碰撞检测,由于是附着在物体表面之上的网格,存在z-fighting 投影贴图 对于不同角度贴花,需要将场景多次分批次进行绘制,drawcall压力过大 超大贴图 严重依赖系统基于mega texture的功能 屏幕空间 严重依赖系统基于延迟渲染的渲染路径 我的实现
从上面不同方法的分析,我们可以从中选择合适的实现方法。现如今,大多成熟渲染系统都是基于延迟渲染的,所以他们大多使用的是基于屏幕空间的贴花系统,易于实现且功能强大。
由于我的GLB Framework目前还是基于Forward的框架,所以此种方法暂时无法使用。
而超大贴图的方法,依赖于系统是否使用了mega texture的功能,我的系统也没有使用这种功能,所以抛弃了。
那么只剩下了投影网格和投影纹理这两种不同的方法。投影网格虽然有效,但是三角形级别的碰撞检测,会增加系统的复杂程度,同时z-fighting效果也不可避免。为了消除z-fighting,需要进行很多额外的消隐褪去的操作。所以我并不喜欢这种方法。
那么就只剩下了投影纹理的方法。投影纹理能够很好的解决投影网格z-fighting的问题。这个方法,是在纹理采样级别,将贴花的图采样到物体表面上,属于完全的贴合在物体表面上。唯一的问题,不同角度,不同样式的贴花需要将场景绘制多次。一旦场景中贴花数量过多,就会导致draw call压力过大,这也是个很麻烦的问题。
但是,由于我的游戏是一个俯视角的3D游戏。贴花主要集中在XZ平面之上,所以我使用了一个预处理,使得不需要增加额外的场景绘制就能够实现decal的功能。
在讲解整个简易贴花系统的实现之前,我们先来了解下投影贴图的实现方法。投影贴图
我们知道现在的3D光栅化流水线的基本操作如下:
- 局部坐标系到世界坐标系(世界变换)
- 世界坐标系到相机坐标系(相机变换)
- 相机坐标系到NDC坐标系(投影变换)
- NDC坐标系到屏幕坐标系(屏幕变换)
也就是说,我们定义了世界和一个观察空间,然后将观察空间里面的世界部分变成了一张图片显示在了屏幕上。那么,也就是说屏幕上显示的这张图片和观察到的空间是一个对应的关系。明白了这点,我们就可以假设如果我们提前给出一张图片(比如贴花系统里面的贴花图),那么观察空间里面必然每一个点都对应了这张图上的某一个像素。根据这个关系,我们就能够实现贴花的功能。
前面举例的时候,使用的观察空间是屏幕上玩家观察的观察空间。但是这个观察空间实际上是可以任意指定的,我们可以根据我们的需要来指定贴花投影的观察空间,这样这个观察空间里面对应的世界空间就能够通过一系列的计算得到我们指定的贴花图中的某一个像素,从而将贴图显示在世界空间里面。 -
投影计算
Nvidia有一篇paper详细的讲解了投影纹理以及投影计算的方法。我这里大概的讲解下这个流程:
- 计算贴花观察空间的相机矩阵(View Matrix)和投影矩阵(Projection Matrix)
- 正常绘制场景,计算顶点在贴花观察空间对应的贴花纹理坐标
- 采样贴花贴图,和正常场景贴图进行混合
如果你曾经做过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