FPS游戏中的喷漆效果原理

最近导师给了个项目任务,要做一个类似投影仪的效果。就是用相机拍摄高铁站的一段视频,然后捕捉高铁站的点云数据,根据点云重建出粗略的高铁站模型网格,然后将拍摄的视频投影到网格上,实现较为真实的效果。在做投影效果的时候,突然想到,这和FPS游戏里喷漆的效果非常像,就是一个用图片作为贴图,一个把视频作为贴图,原理上都一样的。

在想到喷漆的时候,我脑子里第一个出来的就是深度图。然后又想到这和实现阴影映射不是一摸一样嘛。只不过在计算阴影的时候取深度大于深度图,而这里我们只要取小于等于深度图的就可以了。

阴影映射

这里我们就要提一下深度贴图实现阴影的原理,该方法称为阴影映射。阴影映射原理就是我们以光的位置为视角进行渲染,我们能看到的东西都将被点亮,看不见的就是在阴影之中了。

如图所示,蓝色的部分即为接收到光线的部分,而黑色的部分则为处于阴影的部分

算法原理也很简单如下图:

左侧的图片展示了一个定向光源(所有光线都是平行的)在立方体下的表面投射的阴影。通过储存到深度贴图中的深度值,我们就能找到最近点,用以决定片元是否在阴影中。我们使用一个来自光源的视图和投影矩阵来渲染场景就能创建一个深度贴图。这个投影和视图矩阵结合在一起成为一个T变换,它可以将任何三维位置转变到光源的可见坐标空间。

喷漆实现

我们的喷漆效果就是和上图一样的原理,将光源作为喷漆时的摄像机,将当前渲染的深度值保存在深度贴图中。然后在我们自己移动视口观察时,将观察到的信息,通过T变换转换到第一个喷漆的摄像机空间中,再对比深度值,将深度小于等于深度贴图的信息置换成喷漆的纹理。

生成深度贴图的代码如下:

GLuint genDepthMap(){const GLuint SHADOW_WIDTH = 2*winWidth, SHADOW_HEIGHT = 2*winHeight;glGenFramebuffers(1, &depthMapFBO);GLuint depthMap;glGenTextures(1, &depthMap);glBindTexture(GL_TEXTURE_2D, depthMap);glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT,SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);GLfloat borderColor[] = { 0.0, 0.0, 0.0, 0.0 };glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMap, 0);glDrawBuffer(GL_NONE);glReadBuffer(GL_NONE);glBindFramebuffer(GL_FRAMEBUFFER, 0);return depthMap;
}

我们根据喷漆的大小制定纹理的长宽,并用全局变量depthMapFBO创建桢缓冲去储存深度贴图。这里注意的是,当纹理坐标越界的时候,我们使用GL_CLAMP_TO_BORDER使越过边界的纹理自动用深度0.0去填充。保证我们看到的物体在经过T变换之后,其位置相对于喷漆摄像空间越界时(即坐标在(0,1)之外),始终不显示喷漆。如果这里使用REPEAT(重复)或者其他的纹理铺盖方式则会错。这里依旧用我的bunny来展示:

其中左图(使用REPEAT)出现深度错误的现象,而右图(使用CLAMP_TO_BORDER)则能正确的给上喷漆。其中每幅图右上角的小图则是我们保存的深度贴图。拍摄方向也就是我们在喷漆时摄像机的位置和方向。

接下来我们看一下depthMapFBO桢缓冲生成深度贴图的shader。

#version 410 corelayout(location=0) in vec3 position;uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;void main()
{gl_Position = projection*view*model*vec4(position,1.0);
}

在顶点着色器里,我们就简单的将传入的坐标通过喷漆空间变换,输出即可。其中projection矩阵选择透视变换矩阵,view矩阵为喷漆时摄像机对应方向的矩阵。

片元着色器则更为简单:

#version 410 coreout vec4 fragColor;void main(){fragColor=vec4(vec3(gl_FragCoord.z),1.0);
}

其中main函数中的fragColor=vec4(vec3(gl_FragCoord.z),1.0);这句可以删掉也没有任何影响,我这里只是用于显示调试。因为我们之前在设置桢缓冲的时候设置了glDrawBuffer(GL_NONE)和glReadBuffer(GL_NONE)。这两行代码表示我们对缓冲设为不需要绘制,也不需要读取。因此我们没有输出颜色值,深度值也已经自动保存了。

到这里,如果代码正确的话,我们可以将保存的深度贴图绘制出来就是右上角的图片中的样子了:

到这里,我们就可以写在主视图里的shader了。

首先我们看顶点着色器:

#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec2 texCoords;uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;out vec3 Normal;
out vec3 FragPos;
out vec2 TexCoords;void main()
{gl_Position = projection*view*model*vec4(position, 1.0f);Normal=mat3(transpose(inverse(model)))*normal;FragPos=vec3(model*vec4(position,1.0f));TexCoords=texCoords;
}

这里我们传入了位置,法线和纹理坐标。以及使用uniform传入了三个空间变换矩阵。但是这里的view,projection矩阵不同于喷漆相机空间的矩阵。这里是我们自己主空间观察的相机视口矩阵和投影矩阵。然后我们把法线,片元位置以及纹理坐标作为输出给片元着色器使用。

片元着色器代码如下所示:

#version 330 corestruct Material{vec3 ambient;vec3 diffuse;vec3 specular;float shininess;//反光度
};
struct Light{vec3 position;float ambient;float diffuse;float specular;
};in vec3 Normal;
in vec3 FragPos;
in vec2 TexCoords;uniform vec3 LightColor;
uniform Material material;
uniform Light light;
uniform vec3 eyePos;
out vec4 FragColor;uniform sampler2D depthMap;
uniform sampler2D tex1;
uniform mat4 LightSpaceMatrix;const float bias=0.05;const float near_plane=0.1;
const float far_plane=50.0;
float LinearizeDepth(float depth)
{float z = depth * 2.0 - 1.0; // Back to NDCreturn (2.0 * near_plane * far_plane) / (far_plane + near_plane - z * (far_plane - near_plane));
}void main()
{vec3 LightDirection=normalize(light.position-FragPos);vec3 Norm=normalize(Normal);vec3 ViewDirection=-normalize(FragPos-eyePos);vec3 ambient=light.ambient*LightColor*material.ambient;vec3 diffuse=light.diffuse*LightColor*max(dot(Norm,LightDirection),0.0f)*material.diffuse;vec3 specular=light.specular*LightColor*pow(max(dot(normalize(LightDirection+ViewDirection),Norm),0.0f),material.shininess)*material.specular;vec3 result=(ambient+diffuse+specular);//深度比较vec4 LightSpacePos=LightSpaceMatrix*vec4(FragPos,1.0);vec3 aftPos=LightSpacePos.xyz/LightSpacePos.w;aftPos=aftPos*0.5+0.5;float befdepth=LinearizeDepth(texture(depthMap,aftPos.xy).r);if (LinearizeDepth(aftPos.z<=befdepth+bias&&LinearizeDepth(aftPos.z)>0.1+bias){FragColor=texture(tex1,aftPos.xy);}else{FragColor=vec4(result,1.0f);}
}

其中Material和Light是场景中的灯光和材质熟悉,我们可以简单的设置其属性。如下是我上图效果的灯光和材质熟悉。

    lightShader.setVec3("material.ambient",  0.19225f, 0.19225f, 0.19225f);lightShader.setVec3("material.diffuse",  0.50754f, 0.50754f, 0.50754f);lightShader.setVec3("material.specular", 0.508273f, 0.508273f, 0.508273f);lightShader.setFloat("material.shininess", 128.0f*0.4f);lightShader.setFloat("light.ambient", 1.0f);lightShader.setFloat("light.diffuse", 1.0f);lightShader.setFloat("light.specular", 1.0f);lightShader.setVec3("LightColor", glm::vec3(1.0f));

然后我们还需要每一帧在uniform传入eyePos即摄像机的位置,以用来在光照模型中计算反射和高光,这里就不细讲了。然后我们还用uniform传入了刚才在桢缓冲中绘制好的深度贴图depthMap,以及网上找的一张岩石图tex1,用来表示喷漆图案。LightSpaceMatrix则为我们所说的T变换矩阵,就是喷漆相机的projection*view矩阵。

然后看我们的这几行代码

const float near_plane=0.1;
const float far_plane=50.0;
float LinearizeDepth(float depth)
{float z = depth * 2.0 - 1.0; // Back to NDCreturn (2.0 * near_plane * far_plane) / (far_plane + near_plane - z * (far_plane - near_plane));
}

这个线性化深度函数是我们将透视矩阵的深度值从标准化空间转换到摄像机近平面near_plane到远平面far_plane的线性插值。用于更好的进行深度比对。

然后在main函数中

    vec3 LightDirection=normalize(light.position-FragPos);vec3 Norm=normalize(Normal);vec3 ViewDirection=-normalize(FragPos-eyePos);vec3 ambient=light.ambient*LightColor*material.ambient;vec3 diffuse=light.diffuse*LightColor*max(dot(Norm,LightDirection),0.0f)*material.diffuse;vec3 specular=light.specular*LightColor*pow(max(dot(normalize(LightDirection+ViewDirection),Norm),0.0f),material.shininess)*material.specular;vec3 result=(ambient+diffuse+specular);

这里的代码,计算了光线方向,法线方向以及视口方向。然后使用它们计算一个包含环境光,漫反射以及镜面反射的基础光照模型。

在之后的深度比较代码里

    //深度比较vec4 LightSpacePos=LightSpaceMatrix*vec4(FragPos,1.0);vec3 aftPos=LightSpacePos.xyz/LightSpacePos.w;aftPos=aftPos*0.5+0.5;float befdepth=LinearizeDepth(texture(depthMap,aftPos.xy).r);if (LinearizeDepth(aftPos.z<=befdepth+bias&&LinearizeDepth(aftPos.z)>0.1+bias){FragColor=texture(tex1,aftPos.xy);}else{FragColor=vec4(result,1.0f);}

其中我们获取当前的坐标在喷漆空间下的位置,然后对其标准化(通过除以其次项w)。然而这时的坐标都为(-1,1),由于我们需要通过纹理坐标获得深度,纹理坐标范围为(0,1),并且深度缓冲的深度也是(0,1),因此我们需要对其变换到(0,1)的范围内。然后我们将之前喷漆空间内的深度和当前变换的深度进行线形变化后再比对。然而我们这里需要在之前的喷漆空间的深度加一个小小的偏移量,否则会出现下图效果。

这种条纹状的原理是因为:

如图所示,我们在喷漆空间的深度贴图是以像素保存的,每个像素在当前空间下观察会有一半在物体下面,一半在物体上面。因此,我们只要给一个小小的偏移值,使其位于当前观察的上方。

我们在进行比对便不会出现这种效果,我这里给的偏移值是bias=0.05;

之后我们还给出了一个限制条件是LinearizeDepth(aftPos.z)>0.1+bias。即变换后的深度大于0.1加上深度值,是因为我们的摄像机近平面是从0.1开始的,如果没有加入这个限制条件的话,摄像机的背面则也会进行深度比对,生成喷漆的图,这显然不是我们要的效果。

到这里我们的喷漆效果就很好的完成了。

FPS游戏中的喷漆效果原理相关推荐

  1. fps游戏枪口无后座的原理和实现

    fps游戏按住开火键后,枪口会上扬且子弹会散射,并且此时游戏人物的世界坐标.FOV.俯仰角和旋转角都没有任何变化,因此不能通过锁定其中某个因素来实现无后座, 枪械实现无后座的解决方法如下: (1)使每 ...

  2. FPS游戏中的同步算法

    FPS游戏中的同步算法 最近加班奋战2年多终于上线的游戏不到1个月因为种种原因也下线了, 随便写点东西缅怀一下. 在讲我们游戏的同步之前,我想先说下比较正统的做法,也就是守望先锋或者是unreal引擎 ...

  3. 归纳贴:武侠小说和游戏中的武功效果(转)

    归纳贴:武侠小说和游戏中的武功效果(转)[@more@] 最近归纳了下武侠小说和游戏中的武功效果,做了个总结如下.希望能对大家有所帮助,另外有不全之处还请大家补充.谢谢!~ 一般武功绝招都是有一种或多 ...

  4. 影子跟随算法:FPS游戏中游戏同步性的实现

    转自:https://www.gameres.com/454350.html 何为延迟补偿?如何进行坐标差值?B客户端屏幕上A已经跑到东边了,但是收到服务器说"A正在西边往北跑", ...

  5. 百万局对战教AI做人,技术解读FPS游戏中AI如何拟人化

    作者:johnxuan,腾讯 TEG 应用研究员 FPS 游戏 AI 是腾讯 AI Lab 的一大重要研究方向,其拟人化 AI 也在 FPS 手游<穿越火线-枪战王者(CFM)>春节期间上 ...

  6. 如何在FPS游戏中快速实现简单的人体定位算法

    概述 最近在很多B站的视频上看到大佬们分享的AI应用案例,其中有一个引起了我的兴趣:基于深度学习网络, 在CSGO中实现人体定位算法,并进行自动瞄准与射击.当然,这种明显有悖于游戏公平性的行为我是不会 ...

  7. RTS游戏中的战争迷雾原理

    原文:http://blog.csdn.net/xoyojank/article/details/12259161 说到战争迷雾(Fog of War, FOW), 其实还是非常普遍的一项技术, 在R ...

  8. 游戏中的脚本语言原理与发展

    作者:陈嘉栋(慕容小匹夫)     源地址:http://www.cnblogs.com/murongxiaopifu/p/4557365.html 从游戏脚本语言说起,剖析Mono所搭建的脚本基础 ...

  9. 【游戏逆向】FPS游戏自瞄追踪及原理算法

    FPS类游戏的自瞄和爆头效果看起来很奇妙,但实际情况下,算法却并不难,而且该功能也不算变态功能,只不过是通过内存数据计算出精准的准星朝向,我们知道计算器的计算速度,一个精确的角度而已,那么自瞄和爆头是 ...

  10. 【Unity】FPS游戏中的物理引擎——角色控制器(CharacterController)和刚体(Rigidbody)初解

    今天会谈到角色控制器和刚体主要是为了做一个游戏人物的控制器,角色控制器和刚体各有各的优点. 首先说一下刚体吧,刚体这个组件可以说是做一些真是物理游戏的开发者的福音,只要你给物体加上刚体基本可以算是给它 ...

最新文章

  1. a类不确定度计算器_统统帮您搞定:LIMS系统,换版、内审、期间核查、不确定度、数据分析…………...
  2. 客户区和非客户区指的什么?窗口客户区和视图客户区的区别
  3. 代码即财富之我学Java对象序列化与反序列化(2)
  4. OVS DPDK vhost-user详解(十三)
  5. k8s redis集群_基于K8S部署redis哨兵集群
  6. 快速实现dNet三层架构项目图解
  7. 【HDU - 1172】猜数字 (枚举暴力)
  8. 面试常备题(三)----顺时针打印矩阵
  9. 【数据结构笔记02】什么是算法
  10. 路漫漫其修远兮,吾要上下左右前后而求索
  11. 计算机桌面锁定了没设密码怎么解锁,电脑桌面锁屏怎么设置,电脑怎么给手机解锁...
  12. 深度学习(三):人脸关键点检测算法
  13. 在职研一英语课件难句整理
  14. 2022年全球与中国磁阻随机存储器(MRAM)市场现状及未来发展趋势
  15. 零基础CSS入门教程(7)——CSS外联写法
  16. java毕业生设计晨光文具店进销存系统设计与开发计算机源码+系统+mysql+调试部署+lw
  17. 【java】CGLIB动态代理原理
  18. StoneDT开源舆情系统大数据技术栈介绍
  19. [机缘参悟-82]:企业、HR、管理者激励员工的本质
  20. UE4 蓝图学习 FlipFlop

热门文章

  1. raw的服务器镜像是什么系统,如何将Ceph Raw格式镜像转换成Qcow2格式并上传云平台创建云主机...
  2. 喝咖啡的好处和坏处及注意事项
  3. iOS 给文字添加删除线
  4. 前台、中台、后台到底是什么?
  5. python怎么爬取电影海报_Python3 爬取时光网电影海报和电影数据
  6. 江苏省人力资源社会保障厅 省职称办 关于做好2021年度职称评审工作的通知
  7. plc是微型计算机,plc控制系统与微型计算机系统有什么区别
  8. Windows 注册表(Registry) 学习
  9. Ubuntu 视频 转 GIF
  10. 网络定位、A-GPS和GPS的关系