1、简介
当站在湖畔岸边或者镜子面前的时候,都可以看到倒着的自己,那种效果就叫倒影,或者叫镜面反射。在SPE中也支持这种效果,实现的基本思路是把物体根据反射平面镜像之后再绘制一遍,绘制镜像时有几个问题需要注意:1、把深度测试关掉;2、如果打开了背面剔除,背面要反转,即如果设置逆时针为正面,则绘制倒影时逆时针应为背面。3、记得打开混合,并设置好倒影的透明度;4、先绘制反射平面,接着绘制倒影,最后绘制正常物体。
2、reflection matrix的推导
给定一个顶点V(x,y,z,1)以及一个平面P(n,d),其中n为平面单位法向量,d为原点到该平面的有向距离;怎么求得顶点V相对于平面P的镜像顶点V‘呢?如图1所示:如果求得V到平面P的距离D,那V‘ = V - 2*Dn,把顶点V跟平面P的方程进行点乘即可求得D,即D = V dot P = nx*x+ny*y+nz*z+d,把D代入到前面的式子中,可以把式子变成V' = MV的形式,这个M是个4x4的矩阵,就叫反射矩阵(reflection matrix),它的各元素如下所示:
图1:求顶点的镜像
3、绘制倒影
有了反射矩阵,在绘制物体时,只需要在物体世界变换之后再加入镜像变换,即乘以世界变换矩阵之后,再乘以反射矩阵,就可以绘制倒影了。主要的代码如下:
void cReflectDemo::OnFrameRender() { cCamera *pCam = cScene::GetSingletonRef().GetActiveCamera(); pCam->LoadAllMatrix(); glClearColor(0.5f,0.6f,0.7f,1.0f); glClear(GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT|GL_STENCIL_BUFFER_BIT); // render plane planeMat_.ApplyMaterial(); Utils::DrawPlane(3.5f,Utils::PLANE_Y,VECTOR3D(0,-1.0f,0)); // render mirrored cube MATRIX4X4 reflectMat; MakeReflectMatrix(reflectMat,VECTOR3D(0,1,0),1.0f); glFrontFace(GL_CW); // attention!! glDisable(GL_DEPTH_TEST); glEnable(GL_BLEND); glBlendColor(0,0,0,0.4f); glBlendFunc(GL_CONSTANT_ALPHA,GL_ONE_MINUS_CONSTANT_ALPHA); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glMultMatrixf(reflectMat); cubeMat_.ApplyMaterial(); cube.Render(); glPopMatrix(); glEnable(GL_DEPTH_TEST); glDisable(GL_BLEND); glFrontFace(GL_CCW); // render cube noramlly cubeMat_.ApplyMaterial(); cube.Render(); }
该代码绘制了一个大小为2的正方体,以及其相对于平面<(0,1.0f,0),1.0f>的镜像,如下图所示:
注意到倒影在非镜面所占的区域也有显示(右下角),在实际中是不可能的,解决这个问题的方法是:将镜面所占的屏幕区域进行标记,然后绘制倒影时,只在标记了的屏幕区域进行显示,这在OpenGL中可以用模板测试做到,更改后的代码如下,其中在//...和//...之间的代码为新加的,新的倒影效果如图3所示:
void cReflectDemo::OnFrameRender() { cCamera *pCam = cScene::GetSingletonRef().GetActiveCamera(); pCam->LoadAllMatrix(); glClearColor(0.5f,0.6f,0.7f,1.0f); glClear(GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT|GL_STENCIL_BUFFER_BIT); //////////////////// // open stencil test glEnable(GL_STENCIL_TEST); // render plane // flag the plane area with 1's glStencilFunc(GL_ALWAYS,0x1,0x1); glStencilOp(GL_KEEP,GL_KEEP,GL_REPLACE); //////////////////// planeMat_.ApplyMaterial(); Utils::DrawPlane(3.5f,Utils::PLANE_Y,VECTOR3D(0,-1.0f,0)); // render mirror cube //////////////////// // only draw the mirror cube to the plane area glStencilFunc(GL_EQUAL,0x1,0x1); glStencilOp(GL_KEEP,GL_KEEP,GL_KEEP); //////////////////// MATRIX4X4 reflectMat; MakeReflectMatrix(reflectMat,VECTOR3D(0,1,0),1.0f); glFrontFace(GL_CW); // attention!! glDisable(GL_DEPTH_TEST); glEnable(GL_BLEND); glBlendColor(0,0,0,0.4f); glBlendFunc(GL_CONSTANT_ALPHA,GL_ONE_MINUS_CONSTANT_ALPHA); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glMultMatrixf(reflectMat); cubeMat_.ApplyMaterial(); cube.Render(); glPopMatrix(); glEnable(GL_DEPTH_TEST); glDisable(GL_BLEND); glFrontFace(GL_CCW); //////////////////// // close stencil test to draw other things glDisable(GL_STENCIL_TEST); //////////////////// // render cube cubeMat_.ApplyMaterial(); cube.Render(); }
图3:倒影1
4、物体与镜面相交时的倒影
图4:倒影artifacts
如图4所示,此时镜面跟立方体相交,如果按照上面的代码绘制倒影,就会出现不正常现象:镜面下方的物体部分被反射到上面来了;解决的办法是:绘制倒影时使用设置镜面为裁剪面,把镜面上方的倒影部分裁掉,不予以显示;OpenGL中设置裁剪面的函数是glClipPlane,它的原型是void glClipPlane( GLenum plane, const GLdouble *equation),关于该函数,官方文档中有一句话是这么说的:
When glClipPlane is called, equation is transformed by the inverse of the modelview matrix and stored in the resulting eye coordinates. Subsequent changes to the modelview matrix have no effect on the stored plane-equation components.
意思是说当函数被调用时,裁剪平面会被当前模型视图矩阵的逆矩阵变换到视图空间,然后被保存起来,之后模型视图矩阵的改变不会再影响它。这说明平面在物体的模型空间给定,然后被变换到视图空间,但是变换到视图空间应该乘以模型视图矩阵的逆的转置才对,我已经试验过,OpenGL确实是乘以模视矩阵的逆的转置,但文档写错了,误导了不少人。
下面贴上加了裁剪面的实现代码:
void cReflectDemo::OnFrameRender() { cCamera *pCam = cScene::GetSingletonRef().GetActiveCamera(); pCam->LoadAllMatrix(); glClearColor(0.5f,0.6f,0.7f,1.0f); glClear(GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT|GL_STENCIL_BUFFER_BIT); // open stencil test glEnable(GL_STENCIL_TEST); // render plane // flag the plane area with 1's glStencilFunc(GL_ALWAYS,0x1,0x1); glStencilOp(GL_KEEP,GL_KEEP,GL_REPLACE); planeMat_.ApplyMaterial(); Utils::DrawPlane(3.5f,Utils::PLANE_Y,VECTOR3D(0,0.3f,0)); // render mirror cube // only draw the mirror cube to the plane area glStencilFunc(GL_EQUAL,0x1,0x1); glStencilOp(GL_KEEP,GL_KEEP,GL_KEEP); MATRIX4X4 reflectMat; MakeReflectMatrix(reflectMat,VECTOR3D(0,1,0),-0.3f); glFrontFace(GL_CW); // attention!! glDisable(GL_DEPTH_TEST); glEnable(GL_BLEND); glBlendColor(0,0,0,0.4f); glBlendFunc(GL_CONSTANT_ALPHA,GL_ONE_MINUS_CONSTANT_ALPHA); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glMultMatrixf(reflectMat); //////////////////////// // setup clipping plane GLdouble planeEqu[] = {0,1.0,0,-0.3}; glEnable(GL_CLIP_PLANE0); glClipPlane(GL_CLIP_PLANE0,planeEqu); //////////////////////// cubeMat_.ApplyMaterial(); cube.Render(); //////////////////////// // disable clipping plane0 glDisable(GL_CLIP_PLANE0); //////////////////////// glPopMatrix(); glEnable(GL_DEPTH_TEST); glDisable(GL_BLEND); glFrontFace(GL_CCW); // close stencil test to draw other things glDisable(GL_STENCIL_TEST); // render cube cubeMat_.ApplyMaterial(); cube.Render(); }
纠正后的倒影效果如下图所示:
5、倒影绘制shader
为了更方便,也可以使用专门设计的shader来绘制倒影,顶点shader和像素shader看起来像下面这样:
// reflection.vs // for phong shading varying vec3 viewPos; varying vec3 viewNorm; uniform mat4 reflectionMatrix; uniform mat4 worldMatrix; uniform mat4 viewMatrix; uniform mat4 projectionMatrix; void main() { viewPos = viewMatrix*reflectionMatrix*worldMatrix * gl_Vertex; viewNorm = normalize(gl_NormalMatrix * gl_Normal); gl_Position = projectionMatrix*viewMatrix*reflectionMatrix*worldMatrix*gl_Vertex; gl_ClipVertex = gl_ModelViewMatrix * pos; }
// reflection.fs varying vec3 viewPos; varying vec3 viewNorm; uniform float alpha; void main() { gl_FragColor = Lighting(viewPos,viewNorm); gl_FragColor.a = alpha; }