Unity 3D 海水的实现2 折射与反射 离屏相机的渲染

版本:unity 5.4.1  语言:C#

在上节博文实现后,我添加了一些场景的元素,海水的效果大概是这个样子的:

接下来的目标是实现海水的折射和反射,书中使用的Unity应该是4.x时代的,Unity自带基础包是5.x的,然后我再在网上看了一个例子,看了下来基本原理都差不多。

还记得移动端简单阴影的实现吧,对,就是添加一个相机把照到的玩家传给Shader后,显示在地上,原理是一样的。

首先获取到玩家的相机,新建相机到玩家当前相机,经过一个反射矩阵的旋转后,截取海平面以上的渲染,然后再将渲染出来的Texture传递给Shader处理;折射更加简单,不用矩阵旋转,当前位置的海平面以下渲染出Texture,再传递给Shader。

下面是代码,生成Mesh的代码,我就去掉了:

public class Ocean : MonoBehaviour
{

    // 一片区域网格横纵数量
    public int width = 32;
    public int height = 32;

    int g_height;   // 组成网格横纵的线条数量
    int g_width;
    Vector2 sizeInv;

    // 区域的数量和大小
    public int tiles = 2;
    public Vector3 size = new Vector3(150f, 1f, 150f);

    // 材质
    public Material material;
    public Shader oceanShader;

    public Transform player;
    public Transform sun;
    public Vector4 SunDir;

    // 网格相关
    Vector3[] vertices; //顶点
    Vector3[] normals;  //法线
    Vector4[] tangents; //三角
    Mesh baseMesh;

    // LOD,越在靠后List的Mesh,网格越少
    int maxLOD = 4;
    List<List<Mesh>> tiles_LOD;

    // 折射反射相关
    public bool renderReflection = true; //是否启用反射折射
    public int renderTexWidth = 128;
    public int renderTexHeight = 128;

    RenderTexture reflectionTexture = null;
    RenderTexture refractionTexture = null;
    Camera offscreenCam = null;
bool reflectionRefractionEnabled = false;   //是否初始化完成

// Use this for initialization
    void Start()
    {
        // 折射反射
        sizeInv = new Vector2(1f / size.x, 1f / size.z);
        SetupOffscreenRendering();  // 添加离屏相机

        if (!renderReflection)
            EnableReflection(false);
        else
            EnableReflection(true);

        // 计算线条数量
        g_height = height + 1;
        g_width = width + 1;

        // LOD,Mesh所在的List的LOD List编号越小,Mesh的网格越多
        tiles_LOD = new List<List<Mesh>>();
        for (int LOD = 0; LOD < maxLOD; LOD++)
        {
            tiles_LOD.Add(new List<Mesh>());
        }

        for (int y = 0; y < tiles; ++y)
        {
            for (int x = 0; x < tiles; ++x)
            {
                Debug.Log("创建了一片水");
                float cy = y - Mathf.Floor(tiles * 0.5f);
                float cx = x - Mathf.Floor(tiles * 0.5f);

                // 创建一片水
                GameObject tile = new GameObject("WaterTile");

                // 坐标以当前节点为中心
                tile.transform.parent = transform;
                tile.transform.localPosition = new Vector3(cx * size.x, 0f, cy * size.z);

                // 添加Mesh渲染组件
                tile.AddComponent<MeshFilter>();
                tile.AddComponent<MeshRenderer>().material = material;

                tile.layer = LayerMask.NameToLayer("Water");
                tiles_LOD[0].Add(tile.GetComponent<MeshFilter>().mesh);
            }
        }
        GenerateHeightmap();

    }

    // 设置折射反射
    void SetupOffscreenRendering()
    {
        // 创建折射反射图
        RecalculateRenderTextures();

        // 创建Camera实现离屏渲染
        GameObject cam = new GameObject();
        cam.name = "DeepWaterOffscreenCam";
        cam.transform.parent = transform;

        offscreenCam = cam.AddComponent<Camera>();
        offscreenCam.clearFlags = CameraClearFlags.Color;
        offscreenCam.depth = -1;
        offscreenCam.enabled = false;
}

    // 当设置reflection和refraction被禁用的时候,设置lod为1
    void EnableReflection(bool isActive)
    {
        renderReflection = isActive;
        if (!isActive)  // 关闭反射折射,使用波光粼粼的图片替代
        {
            material.SetTexture("_Reflection", null);
            material.SetTexture("_Refraction", null);
            oceanShader.maximumLOD = 1;
        }
        else    // 启用反射折射
        {
            OnDisable();
            oceanShader.maximumLOD = 2;
            RecalculateRenderTextures();
        }
    }

    // 重新生成反射折射的缓存图片
    void RecalculateRenderTextures()
    {
        if (renderReflection)
        {
            reflectionTexture = new RenderTexture(renderTexWidth, renderTexHeight, 0);
            refractionTexture = new RenderTexture(renderTexWidth, renderTexHeight, 0);

            reflectionTexture.wrapMode = TextureWrapMode.Clamp;
            refractionTexture.wrapMode = TextureWrapMode.Clamp;

            reflectionTexture.isPowerOfTwo = true;
            refractionTexture.isPowerOfTwo = true;

            material.SetTexture("_Reflection", reflectionTexture);
            material.SetTexture("_Refraction", refractionTexture);
            material.SetVector("_Size", new Vector4(size.x, size.y, size.z, 0f));
        }
    }

    // 删除反射折射使用的缓存图片
    void OnDisable()
    {
        if (reflectionTexture != null)
        {
            DestroyImmediate(reflectionTexture);
        }
        if (refractionTexture != null)
        {
            DestroyImmediate(refractionTexture);
        }
        reflectionTexture = null;
        refractionTexture = null;
    }

    // 折射反射渲染物体
    void RenderObject()
    {
        if (Camera.current == offscreenCam)
            return;

        if (reflectionTexture == null || refractionTexture == null)
            return;

        if (renderReflection)
            RenderReflectionAndRefraction();
    }

    public LayerMask renderLayers = -1;

    // 具体的渲染,使用第二个相机拷贝当前相机的设置
    void RenderReflectionAndRefraction()
    {
        // 获取当前角色身上的主相机
        Camera renderCamera = Camera.main;

        Matrix4x4 originalWorldToCam = renderCamera.worldToCameraMatrix;    // 获取世界到相机的矩阵,如果改变了相机的矩阵就不会再根据原Transform渲染,除非调用ResetWorldToCameraMatrix
        int cullingMask = ~(1 << 4) & renderLayers.value;   //剪裁Mask,忽略水本身

        // 计算反射矩阵
        float d = -transform.position.y;
        Matrix4x4 reflection = Matrix4x4.zero;
        CameraHelper.CalculateReflectionMatrix(ref reflection, new Vector4(0f, 1f, 0f, d)); //这里不明白,总是弄出了反射矩阵

        // 根据反射矩阵计算离屏相机位置和矩阵
        offscreenCam.backgroundColor = RenderSettings.fogColor;
        offscreenCam.transform.position = reflection.MultiplyPoint(renderCamera.transform.position);    //当前相机置换到反射矩阵中
        offscreenCam.transform.rotation = renderCamera.transform.rotation;
        offscreenCam.worldToCameraMatrix = originalWorldToCam * reflection;

        offscreenCam.cullingMask = cullingMask; //设置剪裁mask
        offscreenCam.targetTexture = reflectionTexture; //将反射缓存图片添加到离屏相机里,跟之前阴影是一个原理

        // 因为反射截取到的图片是翻转的,所以需要设置翻转
        GL.SetRevertBackfacing(true);

        // 获取剪裁平面,transform.position.y是当前海水的高度,最后两个值的正负表示剪裁的方向
        Vector4 cameraSpaceClipPlane = CameraHelper.CameraSpacePlane(offscreenCam, new Vector3(0.0f, transform.position.y, 0.0f), Vector3.up, 1.0f);

        Matrix4x4 projection = renderCamera.projectionMatrix;   //获得渲染相机的投影矩阵
        Matrix4x4 obliqueProjection = projection;

        offscreenCam.fieldOfView = renderCamera.fieldOfView;    //设置FOV
        offscreenCam.aspect = renderCamera.aspect;  //设置宽高比

        CameraHelper.CalculateObliqueMatrix(ref obliqueProjection, cameraSpaceClipPlane);

        // 开始真正的渲染
        offscreenCam.projectionMatrix = obliqueProjection;

        if (!renderReflection)
            offscreenCam.cullingMask = 0;

        offscreenCam.Render();

        GL.SetRevertBackfacing(false);

        // 折射渲染
        offscreenCam.cullingMask = cullingMask;
        offscreenCam.targetTexture = refractionTexture;
        obliqueProjection = projection;

        // 将渲染相机的各个参数设置给离屏相机
        offscreenCam.transform.position = renderCamera.transform.position;
        offscreenCam.transform.rotation = renderCamera.transform.rotation;
        offscreenCam.worldToCameraMatrix = originalWorldToCam;

        // 获取剪裁平面,计算投影矩阵
        cameraSpaceClipPlane = CameraHelper.CameraSpacePlane(offscreenCam, new Vector3(0.0f, transform.position.y, 0.0f), Vector3.up, -1.0f);
        CameraHelper.CalculateObliqueMatrix(ref obliqueProjection, cameraSpaceClipPlane);
        offscreenCam.projectionMatrix = obliqueProjection;

        offscreenCam.Render();
        offscreenCam.projectionMatrix = projection;
        offscreenCam.targetTexture = null;
    }
// 初始化Mesh信息,请参考上一节
    void GenerateHeightmap(){}

    // 这边应该是Update的,但写在Update中会报GUI Window tries to begin rendering while something else has not finished rendering的错误
    void OnGUI()
    {
        // 设置玩家、太阳角度,并更新反射折射,折射反射是根据玩家视角来计算的
        if (player == null)
            player = GameObject.FindGameObjectWithTag("Player").GetComponent<Transform>();

        if (sun != null)
        {
            SunDir = sun.transform.forward;
            material.SetVector("_SunDir", SunDir);
        }

        if (renderReflection)
            RenderObject();
    }
}

然后是CameraHelper的脚本:

public class CameraHelper
{
    private static float sgn(float a)
    {
        if (a > 0.0f)
            return 1.0f;
        if (a < 0.0f)
            return -1.0f;
        return 0.0f;
    }

    public static void CalculateObliqueMatrix(ref Matrix4x4 projection, Vector4 clipPlane)
    {
        Vector4 q = projection.inverse * new Vector4(sgn(clipPlane.x), sgn(clipPlane.y), 1.0f, 1.0f);
        Vector4 c = clipPlane * (2.0F / (Vector4.Dot(clipPlane, q)));
        projection[2] = c.x - projection[3];
        projection[6] = c.y - projection[7];
        projection[10] = c.z - projection[11];
        projection[14] = c.w - projection[15];
    }

    public static Vector4 CameraSpacePlane(Camera cam, Vector3 pos, Vector3 normal, float sideSign)
    {
        Vector3 offsetPos = pos + normal * 0.02f;
        Matrix4x4 m = cam.worldToCameraMatrix;
        Vector3 cpos = m.MultiplyPoint(offsetPos);
        Vector3 cnormal = m.MultiplyVector(normal).normalized * sideSign;
        return new Vector4(cnormal.x, cnormal.y, cnormal.z, -Vector3.Dot(cpos, cnormal));
    }

    public static void CalculateReflectionMatrix(ref Matrix4x4 reflectionMat, Vector4 plane)
    {
        reflectionMat.m00 = (1F - 2F * plane[0] * plane[0]);
        reflectionMat.m01 = (-2F * plane[0] * plane[1]);
        reflectionMat.m02 = (-2F * plane[0] * plane[2]);
        reflectionMat.m03 = (-2F * plane[0] * plane[3]);

        reflectionMat.m10 = (-2F * plane[1] * plane[0]);
        reflectionMat.m11 = (1F - 2F * plane[1] * plane[1]);
        reflectionMat.m12 = (-2F * plane[1] * plane[2]);
        reflectionMat.m13 = (-2F * plane[1] * plane[3]);

        reflectionMat.m20 = (-2F * plane[2] * plane[0]);
        reflectionMat.m21 = (-2F * plane[2] * plane[1]);
        reflectionMat.m22 = (1F - 2F * plane[2] * plane[2]);
        reflectionMat.m23 = (-2F * plane[2] * plane[3]);

        reflectionMat.m30 = 0F;
        reflectionMat.m31 = 0F;
        reflectionMat.m32 = 0F;
        reflectionMat.m33 = 1F;
    }
}

花了三天时间终于整理出来了比较精简的代码,不过渲染部分的矩阵,我还是没有太理解。

这边提一下作者的Shader,因为是老版本创建的Shader,所以如果颜色空间不使用Gamma的话,反射的倒影会用问题。还有作者的代码是有点问题的,如果想直接用他的代码,最好对比一下我这边的代码,把一些错误排掉。

完成之后的效果是这样的:

有折射和反射效果,但总感觉这个颜色很不对劲,没错,这是上一节留下的一个BUG,Mesh中没有添加法线,你可以自己尝试在Mesh初始化的时候添加,或者像我这样马后炮:

normals = new Vector3[baseMesh.vertices.Length];
for (int i = 0; i < baseMesh.vertices.Length; ++i)
    normals[i] = Vector3.Normalize(new Vector3(0, 1f, 0));

for (int k = 0; k < tiles_LOD[0].Count; ++k)
{
    Mesh meshLOD = tiles_LOD[0][k];
    meshLOD.vertices = baseMesh.vertices;
    meshLOD.normals = normals;
    meshLOD.tangents = baseMesh.tangents;
}

这段代码放到OnGUI中,不过只要运行一次就好,不然电脑会很卡。。

最终效果:

功夫不负有心人啊,花了我很长的时间,不过对比第一张图和最后一张图的效果,我感觉一切都是值得的,也了解了Unity4.x和5.x的一些区别,总的来说大概明白了折射反射的原理。

下一节的文章,我想挑战一下自己深入了解一下反射矩阵,波浪的东西留到后面再做。

时间: 2024-10-03 14:45:24

Unity 3D 海水的实现2 折射与反射 离屏相机的渲染的相关文章

Unity shader实现水效果(折射,反射,波浪,1.菲尼尔,深度颜色)

整个实现过程,包括水面的UV流动,折射,反射,根据深度进行透明值处理等等 原文地址:https://www.cnblogs.com/ubanck/p/9606626.html

【Unity 3D】学习笔记二十八:unity工具类

unity为开发者提供了很多方便开发的工具,他们都是由系统封装的一些功能和方法.比如说:实现时间的time类,获取随机数的Random.Range( )方法等等. 时间类 time类,主要用来获取当前的系统时间. using UnityEngine; using System.Collections; public class Script_04_13 : MonoBehaviour { void OnGUI() { GUILayout.Label("当前游戏时间:" + Time.t

Unity 3D 实战核心技术详解 书籍出版

经过半年的努力,处女作<Unity 3D 实战核心技术详解>终于问世了,购买网址: http://www.broadview.com.cn/article/70 在12月5日到12日期间,在打折的基础上优惠,书籍内容全是干货,购买方式:可以查看网页中的"如何购买".

unity 3d yield 用法总结

最近,需要需要用unity 3d做点东西,但是了碰到了延迟加载问题,我总结余下: Coroutines & Yield是unity3d编程中重要的概念,它可以实现将一段程序延迟执行或者将其各个部分分布在一个时间段内连续执行,但是在Javascript与C#中实现Coroutines & Yield,在语法上却有一些区别: javascript中yield用法很简单,直接yield就行了,或者yield WaitForSeconds (2); c#中的用法如下: yield不可单独使用 需要

Unity 3D实现帧同步技术

笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:<手把手教你架构3D游戏引擎>电子工业出版社和<Unity3D实战核心技术详解>电子工业出版社等. CSDN视频网址:http://edu.csdn.net/lecturer/144 现在竞技类网络游戏比较火,市面上也出现了很多这种类型的游戏竞赛,提到网络游戏就回避不了一个问题:同步技术,多个人在一个游戏场景围攻一个怪物或者说多人组队战斗等等.现在在移动端的游戏由

Unity 3d中Shader是什么,可以吃吗?

众所周知,Unity3d是一款跨平台非常广的游戏引擎,上手容易,界面友好,集成功能众多,是目前开发手游的主流引擎.本人有幸使用Unity 3d进行开发已一年多时间,已领略了这歀引擎的强大之处. 编写shader也是我工作内容的一部分,先来说说shader是什么吧,我以自己的理解说明一下: 首先 shader是一种语言,一种在GPU,也就是显卡上执行的高级语言.shader的本意是着色器,可以自定义GPU的渲染管线中的两个环节(即顶点和片段).由此,我们可以控制对象在屏幕上的渲染效果,甚至实现一些

【Unity 3D】学习笔记四十二:粒子特效

粒子特效 粒子特效的原理是将若干粒子无规则的组合在一起,来模拟火焰,爆炸,水滴,雾气等效果.要使用粒子特效首先要创建,在hierarchy视图中点击create--particle system即可 粒子发射器 粒子发射器是用于设定粒子的发射属性,比如说粒子的大小,数量和速度等.在创建完粒子对象后,在右侧inspector视图中便可以看到所有的粒子属性: emit:是否是使用粒子发射器. min size:粒子最小尺寸. max size:粒子最大尺寸. min energy:粒子的最小生命周期

【图说】Eclipse与Unity 3D协同工作

原地址:http://blog.csdn.net/h570768995/article/details/9355313 Eclipse开发过程中总会碰到很多的难题,如何利用好工具帮助我们更快捷的开发也是一件重要事. 下面介绍Eclipse与Unity 3D协同工作: 1,首先在Unity3D中简单建立一个工程文件 2,好的,接下来点击[File]->[Build setting] 3,然后点击[Build],记好你Build出来的.apk的名字.这时候在工程目录下会出现“temp”文件夹(没有B

【Unity 3D】学习笔记三十四:游戏元素——常用编辑器组件

常用编辑器组件 unity的特色之一就是编辑器可视化,很多常用的功能都可以在编辑器中完成.常用的编辑器可分为两种:原有组件和拓展组件.原有组件是编辑器原生的一些功能,拓展组件是编辑器智商通过脚本拓展的新功能. 摄像机 摄像机是unity最为核心组件之一,游戏界面中显示的一切内容都得需要摄像机来照射才能显示.摄像机组件的参数如下: clear flags:背景显示内容,默认的是skybox.前提是必须在render settings 中设置天空盒子材质. background:背景显示颜色,如果没