GraphicsLab Project之简易贴画系统(Decal System)
作者:i_dovelemon
日期:2018-08-28
来源:CSDN
主题: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 RenderTargetm_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 parametersglBindTexture(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 positionauto 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 Matrixm_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;// Sizefloat 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 spacemath::Vector pos = (maxPos + minPos) * 0.5f;// Target position in world spacemath::Vector look_at = math::Vector(0.0f, 1.0f, 0.01f);look_at.w = 0.0f;math::Vector target = pos + look_at;// Orthogonal projection matrixmath::Matrix proj;proj.MakeOrthogonalMatrix(-width / 2.0f, width / 2.0f, - height / 2.0f, height / 2.0f, - depth / 2.0f, depth / 2.0f);// View matrixmath::Matrix view;view.MakeViewMatrix(pos, target);// Shadow matrixmat.MakeIdentityMatrix();mat.Mul(proj);mat.Mul(view);return mat;}
构造一个在XZ平面上足够大的观察空间,可以将所有的贴花贴图都观察到。观察的方向总是向-Y轴看(注意上面的代码为了防止除0异常,给观察方向Z加上了一点偏移)。这里定义的观察空间是一个长方体而不是平截头体,这就意味着我们需要使用正交投影矩阵。这个观察空间最重要的是XZ平面的大小,Y平面上的深度无关紧要,只要不是0即可,所以我在处理depth的时候,以10为基础。
有了这些准备工作,接下来就将所有的贴花贴图绘制到前面我们创建的更大的render target之上:
void DrawDecal() {// Setup render targetrender::Device::SetRenderTarget(m_DecalRenderTarget);render::Device::SetDrawColorBuffer(render::COLORBUF_COLOR_ATTACHMENT0);// Setup viewportrender::Device::SetViewport(0, 0, 2048, 2048);// Clearrender::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 shaderrender::Device::SetShader(m_DecalProgram);render::Device::SetShaderLayout(m_DecalProgram->GetShaderLayout());// Setup texturerender::Device::ClearTexture();render::Device::SetTexture(0, render::texture::Mgr::GetTextureById(m_Decal->GetTexId(scene::Model::MT_ALBEDO)), 0);// Setup meshrender::Device::SetVertexLayout(render::mesh::Mgr::GetMeshById(m_Decal->GetMeshId())->GetVertexLayout());render::Device::SetVertexBuffer(render::mesh::Mgr::GetMeshById(m_Decal->GetMeshId())->GetVertexBuffer());// Setup render staterender::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 uniformrender::Device::SetUniformMatrix(m_DecalShaderWVPLoc, wvp);render::Device::SetUniformSampler2D(m_DecalShaderAlbedoLoc, 0);// Drawrender::Device::Draw(render::PT_TRIANGLES, 0, render::mesh::Mgr::GetMeshById(m_Decal->GetMeshId())->GetVertexNum());}// Reset viewportrender::Device::SetViewport(0, 0, app::Application::GetWindowWidth(), app::Application::GetWindowHeight());// Reset render targetrender::Device::SetRenderTarget(render::RenderTarget::DefaultRenderTarget());}
在得到了这张合成之后的贴花图之后,我们就可以绘制正常场景,并且在场景里面访问这张贴图,然后进行合成。
void DrawFloor() {// Setup shaderrender::Device::SetShader(m_ColorProgram);render::Device::SetShaderLayout(m_ColorProgram->GetShaderLayout());// Setup texturerender::Device::ClearTexture();render::Device::SetTexture(0, m_FloorAlbedoMap, 0);render::Device::SetTexture(1, m_DecalMap, 1);// Setup meshrender::Device::SetVertexLayout(render::mesh::Mgr::GetMeshById(m_Floor->GetMeshId())->GetVertexLayout());render::Device::SetVertexBuffer(render::mesh::Mgr::GetMeshById(m_Floor->GetMeshId())->GetVertexBuffer());// Setup render staterender::Device::SetDepthTestEnable(true);render::Device::SetCullFaceEnable(true);render::Device::SetCullFaceMode(render::CULL_BACK);// Setup uniformstatic 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);// Drawrender::Device::Draw(render::PT_TRIANGLES, 0, render::mesh::Mgr::GetMeshById(m_Floor->GetMeshId())->GetVertexNum());}
上面两个步骤分别使用了如下两个shader:
- 将所有贴花贴图绘制到更大的rendertarget上去的shader:decal.vs, decal.fs
这是一个很简单的shader如下:
#version 330in 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 330in vec2 vs_TexCoord;out vec4 oColor;uniform sampler2D glb_DecalTex;void main() {oColor = texture(glb_DecalTex, vs_TexCoord); }
- 绘制场景时,访问合成的贴花贴图的shader:color.vs, color.fs
#version 330in 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 330in 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
GraphicsLab Project之简易贴画系统(Decal System)相关推荐
- 基于JavaWeb的简易投票系统
基于JavaWeb的简易投票系统 项目文件 数据库文件 1.工具 IDEA JDK1.8 Tomcat8.5 MySQL 2.MySQL数据库 subjects表 /*Navicat Premium ...
- java简单小项目_Java简易抽奖系统小项目
本文实例为大家分享了Java简易抽奖系统的具体代码,供大家参考,具体内容如下 需求: 实现一个抽奖系统 1 注册 2 登录 3 抽奖 必须先注册 再登陆 再抽奖 随机产生4个随机数作为幸运卡号 用 ...
- GraphicsLab Project之基于物理的着色系统(Physical based shading) - 基于图像的光照(Image Based Lighting)(Diffuse篇)
作者:i_dovelemon 日期:2018-01-21 来源:CSDN 主题:PBR, Equrectangular Map, Cube Map, Irradiance Map, HDR Image ...
- DCC - Photoshop - Nvidia NormalMapFilter - 法线生成工具 - 顺便测试 Unity URP 12.1 中的 Decal System
文章目录 NVIDIA Texture Tools Exporter 下载.安装 法线生成素材图 扣干净无用像素 使用 NVIDIA Normal Map Filter 生成贴图 配置好 URP Re ...
- 简易考试系统(java、头歌实验)
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 目录 题目: 项目.包路径(文件位置) 代码: User.java文件(属性类) ExamManage.java文件(方法类) Men ...
- unity 最新输入系统Input system简介,并用其设置Xbox(plus:unity package导入详解)
unity推出了最新的输入系统Input system,不敢说一定比老版本Input manager好.键盘输入的话,个人觉得还是老的输入系统好,不过如果是Xbox手柄的话,个人认为新系统坐实舒服!就 ...
- 02_简易评分系统(DOS界面)(小钱版)[2011-08-01]
引言 (一)程序名称: 简易评分系统V1.0(DOS界面)(小钱版) (二)开发环境: 电脑型号: 惠普 HP Pavilion g4 Notebook PC 笔记本电脑 操作系统: ...
- Android平台上使用属性系统(property system)
在使用Android的属性系统(property system)时遇到了一些问题,结合此次经历,对属性系统的使用做以简单介绍. 一.Property系统简介 属性系统是android的一个重要特性.它 ...
- 搭建串口收发与存储双口RAM简易应用系统
搭建串口收发与存储双口RAM简易应用系统 为了实现通过串口发送数据到 FPGA 中, FPGA 接收到数据后将数据存储在双口 ram 的 一段连续空间中,当需要时,按下按键 S0,则 FPGA 将 R ...
最新文章
- uvc摄像头代码解析7
- 国外的开源的CMS汇总(转载)
- ゾーン10進数、パック10進数
- linux操作python
- springboot拦截请求路径_SpringBoot整合Ant Design Pro进行部署
- 分布式版本控制系统Git的安装和使用
- PADS2007中的层类型(plane type) 简介
- [zz]linux修改密码出现Authentication token manipulation error的解决办法
- 小程序日历插件的使用
- B2B跨境电子商务平台综合服务解决方案 1
- 通过YYtext实现文本点击(类似微博效果)
- WEB常用HTML颜色代码表
- 几款优秀的Windows密码抓取工具
- 反激式开关电源设计_变压器选型
- 2022年全球市场雷达目标模拟器总体规模、主要生产商、主要地区、产品和应用细分研究报告
- win10无线网卡启动服务器,win10系统无线网卡被禁用怎么办?win10开启无线网卡的方法...
- 新编计算机组装与维护教程,新编计算机组装与维护教程/21世纪高等学校计算机科学与技术规划教材...
- python使用Elasticsearch对wikipedia的数据进行检索(详细流程)
- 数据仓库ETL工具箱——实时ETL系统
- Java计算连续自交杂合概率代系变化
热门文章
- Joda-Time 实战
- 小标题 html,论文的小标题格式
- Python绘制3D立体花
- JavaWeb学习总结(五十一)——邮件的发送与接收原理
- xp怎么删除计算机管理员用户名和密码,Windows XP 的 Administrator 超级管理员密码忘记了,如何清除?...
- Excel怎么快速提取出网址
- 【干货】成功解决了无法进入系统的问题
- 金融信创虽风正时济,应对挑战该如何乘风破浪(一)
- \u开头的unicode中的\u被转义\\u的问题处理
- Adobe reader xi打开几秒后闪退问题