版本:unity 5.4.1 语言:C#
上一节中实现了折射和反射,一下子水的效果就出来了,看起来非常棒,不过有个BUG不知道大家有没有发现。
直接来看两张图感受一下吧:
第一张图是不是有点怪怪的?它是就是我第二节的代码,而第二张图已经将BUG排除。其中的BUG就是相机在渲染的时候忽略了深度测试。
当然没错,在这里说一下就是那本书中代码的错误,我后来是研究官方的水和网上写的反射找到了排除BUG的方法。
那究竟怎样排除BUG呢?其实排除就是几个字符的事情,但想要了解他的原理必须从头开始说。
首先我们在新建折射反射图片的时候使用了RenderTexture:
reflectionTexture = new RenderTexture(renderTexWidth, renderTexHeight, 0);
refractionTexture = new RenderTexture(renderTexWidth, renderTexHeight, 0);
构造函数中的长宽我就不说了,关键是第三个参数depth(这边参考了csxiaoshui的关于OpenGL模板缓冲区),设定深度缓冲区的大小,或者说Z buffer。
0代表不使用Z buffer,16表示使用16bit的Z buffer,而24或32则至少使用24位的Z buffer,还拥有一个stencil buffer 模板缓冲区,模板缓冲区主要是做颜色过滤就不做过多的研究了。
关键是深度缓冲,一个相机照射过去,没有深度缓冲会怎么样?各个模型之间在渲染时是不是都不知道显示的先后顺序?仔细看看第一张图,它并不是没有显示小山包,而是山包后面的树显示在山包前面了,这就是会造成的结果。所以BUG排除很简单:
reflectionTexture = new RenderTexture(renderTexWidth, renderTexHeight, 16);
refractionTexture = new RenderTexture(renderTexWidth, renderTexHeight, 16);
或者设置为32,保险起见功能都给加上,哈哈。
接下来讲讲反射矩阵相关的内容,比如说我们想要做一面镜子,这边指出一下,第二节实现的海水反射仅仅适用于海面平放的情况,即法线为Vector3(0, 1f, 0),而镜子是可以任意角度的。
反射相机需要根据当前镜面的一个旋转之后再进行渲染,那反射相机和当前相机究竟是一个什么样的位置关系呢?通过下面的图片给大家说说原理:
当前相机和反射相机的连线与镜面垂直,并且到镜面的距离两个相机相同,这样虽然当前相机看不到整棵树,但是通过反射相机的映射到镜面上,就能通过镜面的反射看到整棵树。
下面是镜面实现的代码,其中计算反射矩阵和剪切平面暂时不分析:
public class Mirror : MonoBehaviour {
Camera renderCamera; //当前玩家视角的相机
Camera reflectionCamera; //反射渲染的相机
public LayerMask layerValue = -1;
RenderTexture rTex;
// Use this for initialization
void Start ()
{
// 获得玩家相机
if (renderCamera == null)
renderCamera = Camera.main;
// 添加反射渲染相机
GameObject go = new GameObject("reflectionCamera");
go.transform.parent = transform;
reflectionCamera = go.AddComponent<Camera>();
go.AddComponent<FlareLayer>();
go.AddComponent<Skybox>();
reflectionCamera.enabled = false;
// 新建反射缓存图片
rTex = new RenderTexture(256, 256, 16); //注意这边的深度不要设置为0
rTex.isPowerOfTwo = true;
reflectionCamera.targetTexture = rTex;
GetComponent<Renderer>().material.SetTexture("_Ref", rTex);
}
// 官方和网上都使用该函数更新
// 物体渲染之前调用,Camera.current当前要渲染的相机
void OnWillRenderObject()
{
// 设一个平面的公式为Ax + By + Cz + D = 0,其中normal代表该平面的法线,pos表示经过该法线的一个点
// 则A、B、C分别为normal的x、y、z值,然后带入pos这个点
// 即normal.x * pos.x + normal.y * pos.y + normal.z * pos.z + D = 0
// 所以D为 -Vector3.Dot(normal, pos)
Vector3 pos = transform.position;
Vector3 normal = -transform.forward.normalized; //单位化,方便算矩阵
float D = -Vector3.Dot(normal, pos);
Vector4 plane = new Vector4(normal.x, normal.y, normal.z, D - 0.07f); //求出平面
// 保存玩家相机的矩阵
Matrix4x4 originMatrix = renderCamera.worldToCameraMatrix;
// 计算出反射矩阵
Matrix4x4 reflectionMatrix = Matrix4x4.zero;
CameraHelper.CalculateReflectionMatrix(ref reflectionMatrix, plane);
// 计算反射相机的坐标与世界到相机矩阵
reflectionCamera.clearFlags = CameraClearFlags.Skybox;
reflectionCamera.transform.position = reflectionMatrix.MultiplyPoint(renderCamera.transform.position); //通过矩阵计算出相机的位置
reflectionCamera.transform.rotation = renderCamera.transform.rotation;
reflectionCamera.worldToCameraMatrix = originMatrix * reflectionMatrix; //相机的世界到相机矩阵同时改变
reflectionCamera.cullingMask = ~(1 << 4) & layerValue; //设置可以反射的物体
// 计算剪切面
Vector4 clipPlane = CameraHelper.CameraSpacePlane(reflectionCamera, pos, normal, 1.0f);
Matrix4x4 projection = renderCamera.projectionMatrix;
CameraHelper.CalculateObliqueMatrix(ref projection, clipPlane); //计算出剪切面相关的投影矩阵,剪切面以下内容不显示
reflectionCamera.projectionMatrix = projection;
// 反射相机渲染
GL.invertCulling = true; //渲染的顶点做了翻转操作,但法线没有,所以背面会消隐,该操作翻转剔除操作
reflectionCamera.Render();
GL.invertCulling = false;
//4.6的代码
//GL.SetRevertBackfacing(true);
//reflectionCamera.Render();
//GL.SetRevertBackfacing(false);
}
}
好了,BUG排除,反射也精简到最核心的代码。折射就不说了,就是玩家的相机渲染一下水平面的物体。
网上看一遍反射矩阵的推导后,我按照我的代码来推导一遍,我们传入的plane包含了面的法线normal(以n(nx, ny, nz)表示)和面距离原点的距离D,设当前相机的坐标Q(x, y, z),反射相机的坐标为Q’(x’,y’,z’)。
首先我们需要Q到平面的长度,因为n是单位向量,所以Q到n的投影为Q*n,再考虑到平面与原点的距离,则Q到平面的长度为Q*n - D。
因为Q - Q’= 2 * Q到平面长度 * n,所以Q’= Q - 2 * (Q*n + D) * n。
之后我们来分别看看个分量的公式:
x’= x - 2 * ( x*nx + y*ny + z*nz + D ) * nx
y’= y - 2 * ( x*nx + y*ny + z*nz + D ) * ny
z’= z - 2 * ( x*nx + y*ny + z*nz + D ) * nz
整理得到:
x’ = (1 - 2*nx*nx) * x - 2*nx*ny*y - 2*nx*nz*z - 2*D*nx
y’ = -2*ny*nx*x + (1 - 2*ny*ny)*y - 2*ny*nz*z - 2*D*ny
z’ = -2*nz*nx*x - 2*nz*nx*y + (1 - 2*nz*nz)*z - 2*D*nz
所以最终的矩阵为:
(1 - 2*nx*nx) -2*ny*nx -2*nz*nx 0
-2*nx*ny (1 - 2*ny*ny) -2*nz*nx 0
-2*nx*nz -2*ny*nz (1 - 2*nz*nz) 0
-2*D*nx -2*D*ny -2*D*nz 1
即上一节CameraHelper中的代码。
CameraSpacePlane其实就是计算Camera坐标系的剪切平面,就也没什么好说的,这样就剩下一个投影矩阵,又是个大坑,下节再说,下节再说。