版本:unity 5.4.1  语言:C#

 

上一节中实现了折射和反射,一下子水的效果就出来了,看起来非常棒,不过有个BUG不知道大家有没有发现。

 

直接来看两张图感受一下吧:

unity海水插件 unity 海_ZBuffer

unity海水插件 unity 海_unity海水插件_02


第一张图是不是有点怪怪的?它是就是我第二节的代码,而第二张图已经将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),而镜子是可以任意角度的。

 

反射相机需要根据当前镜面的一个旋转之后再进行渲染,那反射相机和当前相机究竟是一个什么样的位置关系呢?通过下面的图片给大家说说原理:

unity海水插件 unity 海_unity海水插件_03


当前相机和反射相机的连线与镜面垂直,并且到镜面的距离两个相机相同,这样虽然当前相机看不到整棵树,但是通过反射相机的映射到镜面上,就能通过镜面的反射看到整棵树。

 

下面是镜面实现的代码,其中计算反射矩阵和剪切平面暂时不分析:

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坐标系的剪切平面,就也没什么好说的,这样就剩下一个投影矩阵,又是个大坑,下节再说,下节再说。