unity, water cube

《纪念碑谷》里有一关开始是一个宝箱展开后里面有一个water cube,其中还有小鱼在游。如下截图:

技术分享

因为我们知道《纪念碑谷》是unity做的,而现在正开始学unity,所以也想做一个类似的。

unity5的standard assets里面有一个WaterProDaytime,折射和反射都有,开始以为用它一拼就完事儿了,可没想到这个东西只能平放效果才对,竖着放效果就不对了,如下图所示:

(下面水平放置的water水波纹是正常的,但上面竖立放置的water水纹变成竖直的长条,这种效果对于我们想实现的water cube来说是不可接受的)

技术分享

肯定是官方实现这个效果的人下意识假定我们使用它时一定是会水平放置。于是只好去看它的实现代码,看哪里使用了“水平放置假定”,找到了两处:

第一处:

FXWaterPro.shader中的顶点shader中有下面代码:

   // scroll bump waves
    float4 temp;
    float4 wpos = mul (_Object2World, v.vertex);
    temp.xyzw = wpos.xzxz * _WaveScale4 + _WaveOffset;
    o.bumpuv0 = temp.xy;
    o.bumpuv1 = temp.wz;

其中这一句:temp.xyzw = wpos.xzxz * _WaveScale4 + _WaveOffset;它用顶点世界坐标的xz分量来生成水面凹凸(bump)贴图的纹理坐标,这显然是假定水面是水平放置(或至少水面在XZ平面上投影不为零)。

最具通用性的改法是把水面的平面方程传入顶点shader,在其中计算wpos在此平面上的投影坐标projPos,然后再将projPos转化到局部空间得到projPosInLocalSpace,然后再temp.xyzw = projPosInLocalSpace.xzxz * _WaveScale4 + _WaveOffset;但是等一下,由于wpos是水面的顶点世界坐标,所以其在水面的投影projPos不就是wpos本身吗!然后将projPos转化成局部空间也就是将wpos转化到局部空间,但由于世界顶点坐标wpos就是由局部顶点坐标v.vertex转来的,所以wpos转到局部空间的结果不就是v.vertex吗!所以最后结论是起初直接用局部坐标v.vertex就ok了。即上面代码改为:

   // scroll bump waves
    float4 temp;
    float4 wpos = v.vertex;//mul (_Object2World, v.vertex);
    temp.xyzw = wpos.xzxz * _WaveScale4 + _WaveOffset;
    o.bumpuv0 = temp.xy;
    o.bumpuv1 = temp.wz;

这就是对任意角度水面都正确的代码了。

第二处:

在Water.cs中有下面代码:

     // find out the reflection plane: position and normal in world space

        Vector3 pos = transform.position;
        Vector3 normal = transform.up;

        // Optionally disable pixel lights for reflection/refraction
        int oldPixelLightCount = QualitySettings.pixelLightCount;
        if (disablePixelLights)
        {
            QualitySettings.pixelLightCount = 0;
        }

        UpdateCameraModes(cam, reflectionCamera);
        UpdateCameraModes(cam, refractionCamera);

        // Render reflection if needed
        if (mode >= WaterMode.Reflective)
        {
            // Reflect camera around reflection plane
            float d = -Vector3.Dot(normal, pos) - clipPlaneOffset;
            Vector4 reflectionPlane = new Vector4(normal.x, normal.y, normal.z, d);
            
            Matrix4x4 reflection = Matrix4x4.zero;
            CalculateReflectionMatrix(ref reflection, reflectionPlane);
            Vector3 oldpos = cam.transform.position;
            Vector3 newpos = reflection.MultiplyPoint(oldpos);
            reflectionCamera.worldToCameraMatrix = cam.worldToCameraMatrix * reflection;
            
            // Setup oblique projection matrix so that near plane is our reflection
            // plane. This way we clip everything below/above it for free.
            Vector4 clipPlane = CameraSpacePlane(reflectionCamera, pos, normal, 1.0f);
            reflectionCamera.projectionMatrix = cam.CalculateObliqueMatrix(clipPlane);
            
            reflectionCamera.cullingMask = ~(1 << 4) & reflectLayers.value; // never render water layer
            reflectionCamera.targetTexture = m_ReflectionTexture;
            GL.invertCulling = true;
            reflectionCamera.transform.position = newpos;
            Vector3 euler = cam.transform.eulerAngles;
            reflectionCamera.transform.eulerAngles = new Vector3(-euler.x, euler.y, euler.z);
            reflectionCamera.Render();
            reflectionCamera.transform.position = oldpos;
            GL.invertCulling = false;
            GetComponent<Renderer>().sharedMaterial.SetTexture("_ReflectionTex", m_ReflectionTexture);
        }

其中如下两句:

Vector3 euler = cam.transform.eulerAngles;

reflectionCamera.transform.eulerAngles = new Vector3(-euler.x, euler.y, euler.z);

意思是想把反射相机transform姿态设成与当前相机transform姿态关于水平面(XZ平面)对称,所以这句含有“水平放置假定”。

一般性的做法应该是让反射相机transform姿态与当前相机transform姿态关于反射面对称,可如下计算:

1,计算反射相机的位姿矩阵:

  由于上面代码中已经求出了镜像矩阵reflection,所以反射向机的位姿矩阵:

  reflectCamMatrix=reflection*cam.transform.localToWorldMatrix。

  (注意:镜像矩阵reflection与其逆矩阵是同一个矩阵,因为镜像的镜像就是本身。)

2,由位姿矩阵reflectCamMatrix提取姿态矩阵reflectCamRotationMatrix。

3,将姿态矩阵reflectCamRotationMatrix转成四元数,赋值给反射相机的transform.rotation。

关于由矩阵提取 位置、姿态 和 缩放,参考:http://forum.unity3d.com/threads/how-to-assign-matrix4x4-to-transform.121966/

不过真的需要做这些事儿吗?看上面代码中有这样一句:

reflectionCamera.worldToCameraMatrix = cam.worldToCameraMatrix * reflection;

查看Camera.worldToCameraMatrix的文档,写道:

If you change this matrix, the camera no longer updates its rendering based on its Transform. This lasts until you call ResetWorldToCameraMatrix.

是说,reflectionCamera.worldToCameraMatrix被设置以后,渲染就不再按reflectionCamera.transform走了(除非再调用reflectionCamera.ResetWorldToCameraMatrix)。因此代码中后面再对reflectionCamera.transform进行设置其实是多余、无效的,即上面代码中蓝字部分都是多余的。至于作者为何要写这些代码,我想可能是为了体现思维严谨吧。

为了简单验证我们的结论,可以把蓝色部分的代码全部删除,或者改成别的什么值,然后运行,可以看到渲染结果确实不受影响。

于是我们的最终结论是:虽然reflectionCamera.transform.eulerAngles = new Vector3(-euler.x, euler.y, euler.z);这句代码包含“水平放置假定”,但是由于其根本不起作用,所以整个Water.cs脚本仍然是具有通用性的。

另外要注意,由于上面代码中有下面两句:

  Vector3 normal = transform.up;

  Vector4 reflectionPlane = new Vector4(normal.x, normal.y, normal.z, d);

也就是说,它是将挂有Water.cs脚本的gameObject的transform.up用作反射面的法线,这就要求我们为此gameObject添加的mesh面片在未经任何变换之前必须是法线朝上的。如果我们想得到倾斜或者竖立的水面,我们应通过调整gameObject的rotation来实现。

所以,我们不能用unity里自带的Quad作为水面gameObject的mesh,因为它原始是竖立的。所以我们或者用建模软件建一个原始即为法线向上的面片模型导进unity中来,或者在unity中直接用脚本生成一个这样的面片,我采用的是后者,生成面片的脚本如下:

(生成一个法线为Y轴正方向的单位面片)

参考:http://www.cnblogs.com/wantnon/p/4522415.html

using UnityEngine;
using System.Collections;
[ExecuteInEditMode]
public class genQuadFacePY : MonoBehaviour {

    void Awake() {
        gameObject.GetComponent<MeshFilter> ().mesh = CreateMeshFacePY (1,1);
    }
    Mesh CreateMeshFacePY(float width, float height)
    {

        Mesh m = new Mesh();
        m.name = "quadFacePY";
        //note: unity is left-hand system
        m.vertices = new Vector3[] {
            new Vector3(-width/2, 0, -height/2),
            new Vector3(-width/2, 0, height/2),
            new Vector3(width/2, 0, height/2),
            new Vector3(width/2, 0, -height/2)
        };
        m.uv = new Vector2[] {
            new Vector2 (0, 0),
            new Vector2 (0, 1),
            new Vector2 (1, 1),
            new Vector2 (1, 0)
        };
        m.triangles = new int[] { 0, 1, 2, 0, 2, 3};
        m.RecalculateNormals();
        m.RecalculateBounds();
        return m;
    }
}

最后有两个注意事项:

1,组成water cube的六个水面一定要用六个不同的material,万万不可共用同一个material。

原因是:水面material的shader(FXWaterPro.shader)引用了reflectionTexture,而水面1的reflectionTexture是由水面1的reflectionCamera渲染出来的。水面2的reflectionTexture是由水面2的reflectionCamera渲染出来的。由于水面1的reflectionCamera与水面2的reflectionCamera的观察方向不同,所以必须用两个不同的camera,所以得到的reflectionTexture也是不同的,那么如果水面1和水面2的material用同一个,它们的shader引用的reflectionTexture就只能是同一个,这与水面1与水面2的reflectionTexture不同相矛盾,所以水面1和水面2必须用不同的matrial。(用refractionTexture来分析也是一样)。(如果好奇多个水面使用同一个matrial会发生什么,可以做一下试验,结果就是当摄像机旋转到某个角度时水面的reflection和refraction效果会发生奇怪的跳变)。

2,WaterProDaytime在Orthographic相机下有bug。

关于这个bug,以后将单独写一篇日志,这里只简单描述一下:

对于Orthographic相机,如果water在视截体内且water平面的法向量与相机视线垂直,unity将报出“Screen position out of view frustum”这个error。报错行是Water.cs脚本中的reflectionCamera.Render(),但经过分析可以确定是reflectionCamera.projectionMatrix = cam.CalculateObliqueMatrix(clipPlane)这一句导致的,更具体地说是unity的Camera.CalculateObliqueMatrix这个API的实现有bug/对Orthographic相机支持不完善。

技术分享

由于这是unity自己API的bug,完美的解决办法只能是等unity官方修正了。但work around应该是有的,等我试验好了再说。

截图:

技术分享

 

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。