平面镜反射是一种常用的实时渲染效果,且在许多游戏中都可以见到它的身影。
通常游戏中实现实时反射的方法有如下几种:
1.使用cubemap或预先烘焙反射探针
2.平面镜反射
3。屏幕空间反射(SSR)
三种方式进行比较,使用cubemap或反射探针可以用在反射物体不要求实时更新或变换的情况,且可以用在非平面物体上,如金属材质对天空的反射,或对一些静态场景物件的反射等。而屏幕空间反射虽然可以做实时更新反射物件,且也可以用在非平面物体上,但其性能要求也是最高的。相比之下,平面镜反射尽管在介质上仅局限于平面物体,但其可以做到反射物体的实时更新和变换,效果和性能都不错。
本文主要介绍平面镜反射的实现方式。
考虑到平面镜反射的实现的主要难点集中在相机矩阵的计算,因此分成两篇文章:
实现平面镜反射的主要原理如下:
1.根据已知平面,计算出该平面的镜像矩阵
2.对当前摄像机进行拷贝,得到一个新的相机,并对该相机使用镜像矩阵变换到镜像位置
3.根据平面向量计算镜像相机的斜裁剪矩阵,以裁剪平面负方向的不可视区域
4.渲染镜像相机得到RenderTexture并投影到平面的材质shader,在shader中计算投影坐标最终得到镜面反射效果
第一篇文章首先介绍镜像矩阵的计算:
首先考虑以下情况:
假设存在平面P=<n,d>,其法线为n,其中d=-n·p,p表示平面上的任意一点
在平面的正方向存在点Q,要求计算点Q相对于平面P镜像后的坐标Q’。
可知向量Q’Q垂直于平面,即与法线n同向,假设Q到平面P的垂直距离为k,则有Q=Q’+2*k*n/|n|。
即Q’=Q-2*k*n/|n|
接下来就需要求点Q到平面的距离k
假设平面上存在任意点S=(x0,y0,z0):
则有 (Q-S)·n =|Q-S||n|cosθ
其中cosθ可由k和Q到S的距离求得:
cosθ = k/|Q-S|
因此上述公式转换为(Q-S)·n=|n|*k
由于点S为平面上的任意一点,因此需要满足S·n=-d,所以上述公式还可以转换为Q·n+d=|n|*k,
最后得到点Q到平面的距离k = (Q·n+d)/|n|
结合以上结果,可以得到镜像点Q’的坐标:
Q’=Q-2*n/|n|*((Q·n+d)/|n|)
为了便于计算,我们只考虑法线n为单位向量的情况,则上述公式变为:
Q’=Q-2*n*(Q·n+d)
分解上述结果,最终得到点Q’坐标的各个分量如下:
Q’x = (1-2*nx*nx)*Qx-2*nx*ny*Qy-2*nx*nz*Qz-2*nx*d
Q’y = -2*nx*ny*Qx+(1-2*ny*ny)*Qy-2*ny*nz*Qz-2*ny*d
Q’z = -2*nx*nz*Qx-2*ny*nz*Qy+(1-2*nz*nz)*Qz-2*nz*d
根据以上信息即可求得镜面反射矩阵
在unity中的实现:
void OnWillRenderObject()
{
if (m_IsRenderering)
return;
m_IsRenderering = true;
m_MirrorCamera.CopyFrom(camera);
m_MirrorCamera.targetTexture = m_RenderTexture;
//计算平面向量:注意这里使用的平面模型为unity引擎自带的平面,其法线方向为模型的up轴方向,如果考虑使用其它平面模型,需要注意平面的法线朝向
m_Plane = new Vector4(plane.transform.up.x, plane.transform.up.y, plane.transform.up.z, -Vector3.Dot(plane.transform.up, plane.transform.position));
//将计算得到的镜像矩阵应用到镜像摄像机
m_MirrorCamera.worldToCameraMatrix = camera.worldToCameraMatrix * ReflectMatrix(m_Plane);
//注意当相机镜像后,其渲染的模型的顶点绕序也会镜像,需要将背面裁剪设置为正面裁剪,渲染结束后再修改回来
GL.invertCulling = true;
m_MirrorCamera.Render();
GL.invertCulling = false;
m_IsRenderering = false;
}
private Matrix4x4 ReflectMatrix(Vector4 plane)
{
Matrix4x4 m = default(Matrix4x4);
m.m00 = -2*plane.x*plane.x + 1;
m.m01 = -2 * plane.x * plane.y;
m.m02 = -2 * plane.x * plane.z;
m.m03 = -2 * plane.x * plane.w;
m.m10 = -2 * plane.x * plane.y;
m.m11 = -2 * plane.y * plane.y + 1;
m.m12 = -2 * plane.y * plane.z;
m.m13 = -2 * plane.y * plane.w;
m.m20 = -2 * plane.z * plane.x;
m.m21 = -2 * plane.z * plane.y;
m.m22 = -2 * plane.z * plane.z + 1;
m.m23 = -2 * plane.z * plane.w;
m.m30 = 0; m.m31 = 0;
m.m32 = 0; m.m33 = 1;
return m;
}
最终渲染效果如图:
此时对于位于平面以下的物体部分不会再被渲染到反射贴图中了。
此时的结果几乎看不出有什么问题,但仔细考虑如下情况:
当我们镜像相机C的到相机C’后,相机C’的视锥体裁剪区域为A+B,而实际上B区域位于平面以下,应该不属于被反射的部分,实际应该只能渲染A区域,因此如果上述效果中出现位于平面以下的部分,则渲染即会出现错误:
这种情况就需要使用平面P来计算斜裁剪矩阵了,详细内容请参考下一篇文章:
更多内容请浏览我的博客原文:http://www.lsngo.net/2018/01/07/graphics_mirrorcamera_1/