笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等。
CSDN视频网址:http://edu.csdn.net/lecturer/144
运动模糊是3D格斗游戏中非常受欢迎的技术,其目的是为移动物体增加模糊效果,这增强了玩家体验到的现实感,运动模糊可以以各种方式实现。 有一个基于相机的运动模糊,它专注于相机运动,并且有一个基于对象的运动模糊。 在本篇博客中,我们将研究一个以后完成的选项。
运动模糊的原理是我们可以计算两帧之间每个渲染像素的运动矢量(a.k.a运动矢量), 通过从当前颜色缓冲区沿着该向量进行采样并对结果进行平均,我们得到代表底层对象运动的像素, 让我们来看看下一个细节, 以下是所需步骤,之后我们将审查实际代码。
1、该技术分为两个通道 - 一个渲染通过,然后是一个运动模糊传递。
2、在渲染通道中,我们渲染为两个缓冲区:常规颜色缓冲区和运动矢量缓冲区, 颜色缓冲区包含原始图像,就像它没有运动模糊一样被渲染。 运动矢量模糊包含每个像素的向量,表示其在先前帧和当前帧之间沿屏幕的移动。
3、通过将前一帧的WVP矩阵提供给VS来计算运动矢量,我们使用当前WVP矩阵和前一个顶点将每个顶点的本地空间位置转换为裁剪空间,并将两个结果传递给FS。 我们得到FS中的插值剪辑空间位置,并将它们除以它们各自的W坐标转换成NDC。 这完成了他们对屏幕的投影,所以现在我们可以从当前的位置减去前一个位置并获得运动矢量, 将运动矢量写入纹理。
4、运动模糊通道通过渲染全屏四边形来实现, 我们对FS中每个像素的运动矢量进行采样,然后从该矢量(从当前像素开始)从色彩缓冲器采样。
5、我们总结每个取样操作的结果,同时给当前像素赋予最高的权重,而对于运动矢量中最遥远的像素赋予最小权重(这是我们在本博客中所做的,但还有很多其他选择这里)。
本篇博客是基于骨骼动画技术的讲解, 我们将在这里查看添加运动模糊到该博客代码的更改。
virtual void RenderSceneCB()
{
CalcFPS();
m_pGameCamera->OnRender();
RenderPass();
MotionBlurPass();
RenderFPS();
glutSwapBuffers();
}
这是主要的渲染功能,它非常简单。 我们对场景中的所有对象都有一个渲染过程,然后对运动模糊进行后处理。
void RenderPass()
{
m_intermediateBuffer.BindForWriting();
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
m_pSkinningTech->Enable();
vector Transforms;
float RunningTime = (float)((double)GetCurrentTimeMillis() - (double)m_startTime) / 1000.0f;
m_mesh.BoneTransform(RunningTime, Transforms);
for (uint i = 0 ; i < Transforms.size() ; i++) {
m_pSkinningTech->SetBoneTransform(i, Transforms[i]);
m_pSkinningTech->SetPrevBoneTransform(i, m_prevTransforms[i]);
}
m_pSkinningTech->SetEyeWorldPos(m_pGameCamera->GetPos());
m_pipeline.SetCamera(m_pGameCamera->GetPos(), m_pGameCamera->GetTarget(), m_pGameCamera->GetUp());
m_pipeline.SetPerspectiveProj(m_persProjInfo);
m_pipeline.Scale(0.1f, 0.1f, 0.1f);
Vector3f Pos(m_position);
m_pipeline.WorldPos(Pos);
m_pipeline.Rotate(270.0f, 180.0f, 0.0f);
m_pSkinningTech->SetWVP(m_pipeline.GetWVPTrans());
m_pSkinningTech->SetWorldMatrix(m_pipeline.GetWorldTrans());
m_mesh.Render();
m_prevTransforms = Transforms;
}
函数实现如下所示:
void MotionBlurPass()
{
m_intermediateBuffer.BindForReading();
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
m_pMotionBlurTech->Enable();
m_quad.Render();
}
在运动模糊通道
中,我们绑定用于读取的中间缓冲区(这意味着渲染输出到屏幕)并呈现全屏四边形,
每个屏幕像素将被处理一次,并且将计算运动模糊的效果。
(skinning.vs)
#version 330
layout (location = 0) in vec3 Position;
layout (location = 1) in vec2 TexCoord;
layout (location = 2) in vec3 Normal;
layout (location = 3) in ivec4 BoneIDs;
layout (location = 4) in vec4 Weights;
out vec2 TexCoord0;
out vec3 Normal0;
out vec3 WorldPos0;
out vec4 ClipSpacePos0;
out vec4 PrevClipSpacePos0;
const int MAX_BONES = 100;
uniform mat4 gWVP;
uniform mat4 gWorld;
uniform mat4 gBones[MAX_BONES];
uniform mat4 gPrevBones[MAX_BONES];
void main()
{
mat4 BoneTransform = gBones[BoneIDs[0]] * Weights[0];
BoneTransform += gBones[BoneIDs[1]] * Weights[1];
BoneTransform += gBones[BoneIDs[2]] * Weights[2];
BoneTransform += gBones[BoneIDs[3]] * Weights[3];
vec4 PosL = BoneTransform * vec4(Position, 1.0);
vec4 ClipSpacePos = gWVP * PosL;
gl_Position = ClipSpacePos;
TexCoord0 = TexCoord;
vec4 NormalL = BoneTransform * vec4(Normal, 0.0);
Normal0 = (gWorld * NormalL).xyz;
WorldPos0 = (gWorld * PosL).xyz;
mat4 PrevBoneTransform = gPrevBones[BoneIDs[0]] * Weights[0];
PrevBoneTransform += gPrevBones[BoneIDs[1]] * Weights[1];
PrevBoneTransform += gPrevBones[BoneIDs[2]] * Weights[2];
PrevBoneTransform += gPrevBones[BoneIDs[3]] * Weights[3];
ClipSpacePos0 = ClipSpacePos;
vec4 PrevPosL = PrevBoneTransform * vec4(Position, 1.0);
PrevClipSpacePos0 = gWVP * PrevPosL;
}
以上我们看到蒙皮
技术VS的变化,
我们添加了一个统一的数组,其中包含了前一帧的骨变换,我们用它来计算前一帧中当前顶点的裁剪
空间位置,
该位置以及当前帧中的当前顶点的裁剪
空间位置被转发到FS。
(skinning.fs:123)
layout (location = 0) out vec3 FragColor;
layout (location = 1) out vec2 MotionVector;
void main()
{
VSOutput In;
In.TexCoord = TexCoord0;
In.Normal = normalize(Normal0);
In.WorldPos = WorldPos0;
vec4 TotalLight = CalcDirectionalLight(In);
for (int i = 0 ; i < gNumPointLights ; i++) {
TotalLight += CalcPointLight(gPointLights[i], In);
}
for (int i = 0 ; i < gNumSpotLights ; i++) {
TotalLight += CalcSpotLight(gSpotLights[i], In);
}
vec4 Color = texture(gColorMap, TexCoord0) * TotalLight;
FragColor = Color.xyz;
vec3 NDCPos = (ClipSpacePos0 / ClipSpacePos0.w).xyz;
vec3 PrevNDCPos = (PrevClipSpacePos0 / PrevClipSpacePos0.w).xyz;
MotionVector = (NDCPos - PrevNDCPos).xy;
}
蒙皮技术的FS已经被更新,以将两个向量输出到两个独立的缓冲器(颜色和运动矢量缓冲器)中, 颜色按照惯例计算, 为了计算运动矢量,我们通过对两者进行透视分割来投影当前帧和前一帧的裁剪空间位置,并从另一个减去一个。
注意,运动矢量只是2D矢量, 这是因为它只在屏幕上“生活”, 使用类型GL_RG创建相应的运动缓冲区以进行匹配。
(motion_blur.vs)
#version 330
layout (location = 0) in vec3 Position;
layout (location = 1) in vec2 TexCoord;
out vec2 TexCoord0;
void main()
{
gl_Position = vec4(Position, 1.0);
TexCoord0 = TexCoord;
}
这是运动模糊技术的VS,
我们简单地传递全屏四边形的每个顶点的位置和纹理坐标。
(motion_blur.fs)
#version 330
in vec2 TexCoord0;
uniform sampler2D gColorTexture;
uniform sampler2D gMotionTexture;
out vec4 FragColor;
void main()
{
vec2 MotionVector = texture(gMotionTexture, TexCoord0).xy / 2.0;
vec4 Color = vec4(0.0);
vec2 TexCoord = TexCoord0;
Color += texture(gColorTexture, TexCoord) * 0.4;
TexCoord -= MotionVector;
Color += texture(gColorTexture, TexCoord) * 0.3;
TexCoord -= MotionVector;
Color += texture(gColorTexture, TexCoord) * 0.2;
TexCoord -= MotionVector;
Color += texture(gColorTexture, TexCoord) * 0.1;
FragColor = Color;
}
这就是所有运动模糊的乐趣,
我们对当前像素的运动矢量进行采样,并使用它从色彩缓冲器中取样四个像素
。 使用原始纹理坐标对当前像素的颜色进行采样,并给出最高重量(0.4),
然后我们沿着运动矢量向后移动纹理坐标,并再次采样三个颜色纹素。 我们将它们结合在一起,同时在我们移动的同时给予更小更小的权重。你可以看到我们将原始运动矢量分成两部分: 你可能需要在这里进行一些微调,以及重量以获得最佳效果。
以下是可能输出的案例图片: