作者:i_dovelemon
日期:2017 - 03 - 27
来源:CSDN
主题:Environment Mapping, Cube Map
引言
GLB库在此之前只支持了Diffuse,,Alpha和Normal贴图,最近希望能够支持Reflect 贴图,所以就添加了对Reflect贴图的支持。一般来说,反射贴图是使用Cube Map(Environment Map)来实现的。这个Cube Map编码了需要反射的环境,将它附加给物体,就能够通过shader进行Cube Mapping,实现简单的反射效果。如果想要实时的反射游戏中物体周围的环境,就需要在物体周围环境发生变化的时候,动态的产生这张Cube Map,以此来实现动态的实时反射效果。
立方体映射(Cube Mapping)
那么,首先就来介绍下今天的主人公,立方体映射,即Cube Mapping。立方体映射的技术很早之前就提出了,它的主要思想是将环境编码到6个不同的贴图中去,这6个贴图构成了一个立方体的6个面,将他们合在一起的时候,刚好是一个封闭的立方体,如下图所示:
由于要编码周围的环境需要6张贴图才行,但是以前的硬件没有办法对连续的6张纹理进行采样。直到Nvidia Gefore 256显卡出来之后,才有了硬件上对这种贴图进行采样的支持,也就是现在我们知道的Cube Map。现在的图形硬件基本都提供了对Cube Map的采样操作,所以我们就能够依靠图形硬件来实现我们需要的动态环境反射效果。
当我们准备好了Cube Map之后,我们如何来为物体着色,从而实现反射效果了?这个很简单,由于Cube Map编码了周围的环境,当我们计算一个物体上某个点A的环境反射颜色时,我们只要根据该点的法线,以及该点A到相机的方向向量(即反射到我们人眼中的向量)来逆推出那个将要反射到我们眼中的纹素即可,如下图所示:
产生环境贴图
由于我希望实现的是动态的实时反射,所以就需要动态产生环境贴图。主流的方案是以需要进行反射的物体为中心,然后分别对着世界坐标轴的+X,-X,+Y,-Y,+Z,-Z渲染场景6次,产生一张纹理贴图。除此之外在Real time rendering还提到了通过Geometry shader和Layer rendering技术来渲染场景一次产生环境贴图。不过后面一个方案目前还不是十分的稳定成熟,感兴趣的同学可以自行尝试下。
由于渲染的场景需要是立方体的6个面,所以我们必须要重新设置投影矩阵,以90度视野和宽高比为1的画幅来进行渲染,这样才能够保证渲染出来的结果是正方形的。
集成到框架中
最初设计的时候,我希望环境贴图的产生完全有框架来控制,可是最终发现很难决定到底以哪种方式实现。如果对每一个需要反射的物体都渲染一张贴图,明显开销太大;如果以区块划分,然后不同区块产生不同的贴图,这种方法又会有浪费,所以最终我决定,只在框架中集成烘培环境贴图的功能,具体要在什么位置烘焙,哪些物体需要用到哪种贴图,由上层应用来决定。所以,如下就是基本的框架。
首先,我定义了数据结构,用来保存用户请求的环境贴图烘焙数据,如下所示:
struct EnvBaker {
RenderTarget* target;
Object* obj;
int32_t tex_id;
int32_t width;
int32_t height;
EnvBaker()
: target(NULL)
, obj(NULL)
, tex_id(-1)
, width(0)
, height(0) {
}
};
并且提供了接口,用来请求烘焙和取消烘焙,如下所示:
int32_t RenderImp::RequestBakeEnvMap(int32_t width, int32_t height, Object* obj) {
int32_t result = -1;
RenderTarget* rt = RenderTarget::Create(width, height);
texture::Texture* env_map = texture::Texture::CreateFloat16CubeTexture(width, height);
if (rt != NULL && env_map != NULL && obj != NULL) {
DrawColorBuffer attach_pos[6] = {
COLORBUF_COLOR_ATTACHMENT0,
COLORBUF_COLOR_ATTACHMENT1,
COLORBUF_COLOR_ATTACHMENT2,
COLORBUF_COLOR_ATTACHMENT3,
COLORBUF_COLOR_ATTACHMENT4,
COLORBUF_COLOR_ATTACHMENT5
};
rt->AttachCubeTexture(attach_pos, env_map);
result = texture::Mgr::AddTexture(env_map);
EnvBaker baker;
baker.target = rt;
baker.obj = obj;
baker.tex_id = result;
baker.width = width;
baker.height = height;
m_EnvBakers.push_back(baker);
} else {
GLB_SAFE_ASSERT(false);
}
return result;
}
void RenderImp::CancleBakeEnvMap(Object* obj) {
if (obj != NULL) {
if (!m_EnvBakers.empty()) {
std::vector<EnvBaker>::iterator it = m_EnvBakers.begin();
for (; it != m_EnvBakers.end(); ++it) {
if (it->obj == obj) {
GLB_SAFE_DELETE(it->target);
it->tex_id = -1;
it->obj = NULL;
m_EnvBakers.erase(it);
break;
}
}
}
}
}
这样当用户想要为某个模型实现反射效果的时候,只要请求烘焙一张环境贴图,然后将请求的贴图ID保存起来,就能够在后面当实际环境贴图渲染完毕之后拿来使用了,如下是一个使用该接口的典型场景:
m_Cube = glb::scene::Scene::AddObject("ball.obj");
obj = glb::scene::Scene::GetObjectById(m_Cube);
obj->SetCullFaceEnable(true);
obj->SetCullFaceMode(glb::render::CULL_BACK);
obj->SetDepthTestEnable(true);
obj->SetPos(Vector(0.0f, 0.0f, 0.0f));
int32_t ref_tex = glb::render::Render::RequestBakeEnvMap(1024, 1024, obj);
obj->SetTexWithId(glb::Model::MT_REFLECT, ref_tex);
从上面的代码可以看出,我没有将模型使用反射贴图的操作绑定为必须使用烘焙的方式,如果你从某个文件中读取了静态的环境贴图,也能够通过SetTexWithId来为模型设置对应的反射贴图。
实现
实现的代码很简单,如下所示:
void RenderImp::DrawEnvMapCore() {
// Save original proj-view matrix information
PerspectiveProj old_proj = m_Perspective[Render::PRIMARY_PERS];
camera::CameraBase* old_camera = NULL;
scene::Scene::GetCurCamera()->Clone(&old_camera);
Vector old_sky_object_pos = scene::Scene::GetSkyObject()->GetPos();
// New perspective matrix
m_Perspective[Render::PRIMARY_PERS].znear = 0.1f;
m_Perspective[Render::PRIMARY_PERS].zfar = 1000.0f;
m_Perspective[Render::PRIMARY_PERS].aspect = 1.0f;
m_Perspective[Render::PRIMARY_PERS].fov = 90.0f;
m_Perspective[Render::PRIMARY_PERS].m.MakeProjectionMatrix(1.0f, 90.0f, 0.1f, 1000.0f);
int32_t size = m_EnvBakers.size();
for (int32_t i = 0; i < size; i++) {
Object* ref_obj = m_EnvBakers[i].obj;
Vector ref_pos = ref_obj->GetPos();
scene::Scene::GetSkyObject()->SetPos(ref_pos);
scene::Scene::GetSkyObject()->Update();
// 6 View matrix(+X,-X,+Y,-Y,+Z,-Z)
Matrix views[6];
views[0].MakeViewMatrix(ref_pos, ref_pos + Vector(1.0f, 0.0f, 0.0f));
views[1].MakeViewMatrix(ref_pos, ref_pos + Vector(-1.0f, 0.0f, 0.0f));
views[2].MakeViewMatrix(ref_pos, Vector(1.0f, 0.0f, 0.0f), Vector(0.0f, 0.0f, 1.0f), Vector(0.0f, -1.0f, 0.0f));
views[3].MakeViewMatrix(ref_pos, Vector(1.0f, 0.0f, 0.0f), Vector(0.0f, 0.0f, -1.0f), Vector(0.0f, 1.0f, 0.0f));
views[4].MakeViewMatrix(ref_pos, ref_pos + Vector(0.0f, 0.0f, 1.0f));
views[5].MakeViewMatrix(ref_pos, ref_pos + Vector(0.0f, 0.0f, -1.0f));
// Render Target
render::Device::SetRenderTarget(m_EnvBakers[i].target);
// View port
render::Device::SetViewport(0, 0, m_EnvBakers[i].width, m_EnvBakers[i].height);
for (int32_t j = 0; j < 6; j++) {
scene::Scene::GetCurCamera()->SetViewMatrix(views[j]);
// Draw Buffer
render::Device::SetDrawColorBuffer(static_cast<render::DrawColorBuffer>(render::COLORBUF_COLOR_ATTACHMENT0 + j));
// Clear
render::Device::SetClearColor(1.0f, 1.0f, 1.0f);
render::Device::SetClearDepth(1.0f);
render::Device::Clear(CLEAR_COLOR | CLEAR_DEPTH);
for (int32_t k = 0; k < static_cast<int32_t>(m_ShaderGroups.size()); k++) {
std::vector<Object*> objs = m_ShaderGroups[k].GetObjects();
shader::Descriptor desc = m_ShaderGroups[k].GetShaderDesc();
shader::Program* program = m_ShaderGroups[k].GetShaderProgram();
std::vector<uniform::UniformEntry>& uniforms = program->GetUniforms();
// Shader
render::Device::SetShader(program);
render::Device::SetShaderLayout(program->GetShaderLayout());
// Scene uniforms
for (int32_t l = 0; l < static_cast<int32_t>(uniforms.size()); l++) {
uniform::UniformEntry entry = uniforms[l];
if (entry.flag) {
// TODO: for now, id is the index of the uniform picker table
uniform::Wrapper uniform_wrapper = uniform::kUniformPickers[entry.id].picker(NULL);
SetUniform(entry.location, uniform_wrapper);
}
}
// Objects
for (int32_t l = 0; l < static_cast<int32_t>(objs.size()); l++) {
Object* obj = objs[l];
if (obj == ref_obj) {
continue;
}
// Textures
if (obj->GetModel()->HasDiffuseTexture()) {
render::Device::SetTexture(render::TS_DIFFUSE, texture::Mgr::GetTextureById(obj->GetModel()->GetTexId(Model::MT_DIFFUSE)), 0);
}
if (obj->GetModel()->HasAlphaTexture()) {
render::Device::SetTexture(render::TS_ALPHA, texture::Mgr::GetTextureById(obj->GetModel()->GetTexId(Model::MT_ALPHA)), 1);
}
if (obj->GetModel()->HasNormalTexture()) {
render::Device::SetTexture(render::TS_NORMAL, texture::Mgr::GetTextureById(obj->GetModel()->GetTexId(Model::MT_NORMAL)), 2);
}
if (obj->GetModel()->IsAcceptShadow()) {
render::Device::SetTexture(render::TS_SHADOW, texture::Mgr::GetTextureById(m_ShadowMap), 3);
}
if (obj->GetModel()->IsUseAO()) {
render::Device::SetTexture(render::TS_AO_MAP, texture::Mgr::GetTextureById(m_AOMap), 4);
}
// Object Uniform
for (int32_t m = 0; m < static_cast<int32_t>(uniforms.size()); m++) {
uniform::UniformEntry entry = uniforms[m];
if (!entry.flag) {
// TODO: for now, id is the index of the uniform picker table
uniform::Wrapper uniform_wrapper = uniform::kUniformPickers[entry.id].picker(obj);
SetUniform(entry.location, uniform_wrapper);
}
}
// Vertex Buffer
int32_t mesh_id = obj->GetModel()->GetMeshId();
VertexLayout layout = mesh::Mgr::GetMeshById(mesh_id)->GetVertexLayout();
int32_t num = mesh::Mgr::GetMeshById(mesh_id)->GetVertexNum();
render::Device::SetVertexBuffer(mesh::Mgr::GetMeshById(mesh_id)->GetVertexBuffer());
render::Device::SetVertexLayout(layout);
if (obj->IsCullFaceEnable()) {
render::Device::SetCullFaceEnable(true);
render::Device::SetCullFaceMode(obj->GetCullFaceMode());
} else {
render::Device::SetCullFaceEnable(false);
}
if (obj->IsDepthTestEnable()) {
render::Device::SetDepthTestEnable(true);
} else {
render::Device::SetDepthTestEnable(false);
}
if (obj->IsAlphaBlendEnable()) {
render::Device::SetAlphaBlendEnable(true);
render::Device::SetAlphaBlendFunc(render::FACTOR_SRC, obj->GetAlphaBlendFunc(render::FACTOR_SRC));
render::Device::SetAlphaBlendFunc(render::FACTOR_DST, obj->GetAlphaBlendFunc(render::FACTOR_DST));
} else {
render::Device::SetAlphaBlendEnable(false);
}
// Draw
render::Device::Draw(render::PT_TRIANGLES, 0, num);
}
}
}
// Reset render target
render::Device::SetRenderTarget(0);
}
// Restore original proj-view matrix information
m_Perspective[Render::PRIMARY_PERS] = old_proj;
scene::Scene::GetCurCamera()->Restore(&old_camera);
scene::Scene::GetSkyObject()->SetPos(old_sky_object_pos);
scene::Scene::GetSkyObject()->Update();
render::Device::SetViewport(0, 0, m_Width, m_Height);
}
为保存的baker数据中,每一个需要渲染的环境贴图渲染6次场景,最终得到一个环境贴图。
当环境贴图渲染完毕之后,我们就可以在最终的渲染中,开启反射贴图选项,使用对应的shader,来进行反射计算,如下代码表示了反射计算的过程:
#ifdef GLB_ENABLE_REFLECT_TEX
vec3 refl = reflect(-view_vec, normal);
refl = normalize(refl);
oColor.xyz = textureCube(glb_ReflectTex, refl).xyz; // Just for environment mapping now, please fix this
#endif
下面是通过此种技术渲染的简单场景:
上图即是渲染的结果。读者可能发现上图中反射的环境图并不是连续成一个整体的,具体原因我们接下来仔细分析。
渲染结果不是整体的原因?
首先,我们将球体四周的6个立方体,分别用带有上,下,左,右,前和后的贴图来表示,以便于我们区分反射的具体情况。如下图即是结果:
仔细分析上图可以看到,左右前后的贴图上下颠倒了,并且前和后的位置颠倒了。通过调查发现这是OpenGL保存Cube Map的方式,具体可以看
OpenGL Specific中有关的介绍。
那么,怎么解决这个问题了?最先想到的方法就是对每一个贴图产生的时候,对相机矩阵做一些处理。但是这样就绑定到了OpenGL这个API,在DX中实际上是不需要这样处理的。所以这里使用一个比较讨巧的方法。
我们在绘制+X,-X,+Y,-Y,+Z,-Z场景的时候,他们和对应的Cube Map面的对应关系如下所示:
+X <==> GL_CUBE_MAP_POSITIVE_X
-X <==> GL_CUBE_MAP_NEGATIVE_X
+Y <==> GL_CUBE_MAP_POSITIVE_Y
-Y <==> GL_CUBE_MAP_NEGATIVE_Y
+Z <==> GL_CUBE_MAP_POSITIVE_Z
-Z <==> GL_CUBE_MAP_NEGATIVE_Z
我们将这个对应关系,改为如下的方式:
+X <==> GL_CUBE_MAP_POSITIVE_X
-X <==> GL_CUBE_MAP_NEGATIVE_X
+Y <==> GL_CUBE_MAP_NEGATIVE_Y
-Y <==> GL_CUBE_MAP_POSITIVE_Y
+Z <==> GL_CUBE_MAP_NEGATIVE_Z
-Z <==> GL_CUBE_MAP_POSITIVE_Z
通过这样的改变,我们就能够得到如下所示的图:
可以看到,更改对应关系之后,整个反射的画面已经是一个连续的状态,但是还是存在一个问题,反射的图像上下颠倒了。这是因为我们向前面那样改变,本质上就是将反射的环境贴图上下颠倒,想要获得正常的结果,就需要在采样的时候,将纹理坐标绕X轴旋转180度,即:
#ifdef GLB_ENABLE_REFLECT_TEX
vec3 refl = reflect(-view_vec, normal);
refl = normalize(refl);
refl.yz = -refl.yz;
oColor.xyz = textureCube(glb_ReflectTex, refl).xyz; // Just for environment mapping now, please fix this
#endif
总结
将环境映射技术集成到框架中去之后,慢慢的就发现原来设计中有很多不足的地方,除此之外速度也变得很慢,过段时间可能集中精力优化整个框架,让程序能够更流畅的运行。
参考文献
[1]
https://en.wikipedia.org/wiki/Cube_mapping Cube Mapping wiki
[2]
https://www.khronos.org/opengl/wiki/Cubemap_Texture Cube Map Texture Opengl Wiki
[3]
http://www.3dcpptutorials.sk/index.php?id=24
3D C/C++ tutorials - OpenGL 2.1 - GLSL cube mapping