Unity3D手游项目的总结和思考(3) - 自动生成的水面

本文总结了Unity3D手游项目中水面效果的实现,包括自动生成水面网格、水面与岸边的Alpha过渡、动态海浪模拟、倒影与折射、法线贴图高光及互动水波纹等技术,探讨了效率与效果的平衡,提供了工具和shader的截图。
摘要由CSDN通过智能技术生成

     前面两篇提到场景和角色渲染的技术,这一篇讲一下水面.

     水面最开始是美术制作的,一个大的面片来实现,对于湖泊多的场景,这个面片甚至场景那么大,我觉得这种做法不是很好,虽然只有4个顶点,2个三角面,但是由于很大,像素填充率惊人,效率自然也不够好.后来要处理一些水面岸边渐变的效果,我觉得程序实现一个自动化的,不再让美术去操心这个事情.

    我想尽可能地让这个水面完美,所以尝试做了以下一些技术

1.自动生成水面网格

       美术把这个水面摆到某个地方,然后水面根据摆的位置的高度,就自动生成一个适配地形的水面网格.这个技术我以前提到过,就是种子填充算法,从水面一个点逐渐往8个方向的点扩散,直到遇到比他高的地形.

        // 八方向的边界填充算法
        void WaterBoundaryFill8(int x, int z, float boundaryHeight)
        {
            int index = x + z * (m_GridNumX + 1);
            if (m_VerticesFlag[index])
                return;

            float height = GetHeight(x, z);
            if (height <= boundaryHeight)
            {
                m_VerticesFlag[index] = true;
                float difference = Mathf.Clamp(boundaryHeight - height, 0, maxWaterDepth);
                m_VerticesAlpha[index] = Mathf.Clamp01(difference / maxWaterDepth);

                if (x + 1 < m_GridNumX + 1 && x - 1 >= 0 && z + 1 < m_GridNumZ + 1 && z - 1 >= 0)
                {
                    WaterBoundaryFill8(x + 1, z, boundaryHeight);
                    WaterBoundaryFill8(x - 1, z, boundaryHeight);
                    WaterBoundaryFill8(x, z + 1, boundaryHeight);
                    WaterBoundaryFill8(x, z - 1, boundaryHeight);

                    WaterBoundaryFill8(x - 1, z - 1, boundaryHeight);
                    WaterBoundaryFill8(x + 1, z - 1, boundaryHeight);
                    WaterBoundaryFill8(x - 1, z + 1, boundaryHeight);
                    WaterBoundaryFill8(x + 1, z + 1, boundaryHeight);
                }
            }
        }

        float GetHeight(int x, int z)
        {
            float height = float.MinValue;
            Vector3 centerOffset = new Vector3(-m_GridNumX * 0.5f, 0, -m_GridNumZ * 0.5f);
            Vector3 worldPos = GetVertexLocalPos(x, z, centerOffset) + transform.position;
            worldPos.y += 100.0f;
            RaycastHit hit;
            if (Physics.Raycast(worldPos, -Vector3.up, out hit, 200.0f))
            {
                height = hit.point.y;
            }
            else
            {
                //LogSystem.DebugLog("Physics.Raycast失败,请检查是否有Collider. x:{0} z:{0}", x, z);
            }
            
            return height;
        }


2.水面和岸边的alpha过渡

       如果不处理水面和岸边的alpha过渡.那么相交的地方会有一条很明显的线.

       水面的过渡处理,如果有场景的深度图,那么处理起来就特别方便,因为你就知道了场景中每个像素点的深度,和水平面一比较就知道了这个alpha过渡值.深度数据的获取,如果是延迟渲染,就从G-Buffer,前向渲染,就得自己渲染深度图(pre-z).但是对于手游,自己额外渲染深度图会有一定开销.如果抛弃这种处理方式,那么我们该怎么办呢?

        一般有两种办法,一种是用额外一张alpha贴图来逐像素地保存这个alpha值,缺点是,如果水面大,这个alpha图就得很大,而且得美术来制作,修改起来也很麻烦.另外一种就是用水面网格顶点颜色来保存这个alpha值,这个alpha值,程序自动计算,不需要美术去处理.缺点是过渡效果是逐顶点的,和网格的密度有关系.毫无疑问,我选择第二种.


3.岸边自动生成动态海浪

       我们首先需要一个Foam的纹理来模拟浪花.其次需要一个梯度图来实现一层一层的浪花的运动.再次我们需要知道浪花的运动方向,这个可以通过水的深浅决定,因为浪花总是从水深的地方移动到水浅的地方.


4.水面的倒影和折射

       水面的实时倒影和折射,也支持,后来还是没用到,因为开销大了点,我用环境贴图反射来代替水面实时倒影,效率高很多,效果也还可以,适合手游.

				// reflection
	#if _RUNTIME_REFLECTIVE
				float4 uv1 = i.uvProjector; uv1.xy += noise.xy * 0.25;
				fixed4 refl = tex2Dproj(_ReflectionTex, UNITY_PROJ_COORD(uv1)) * _ReflectionColor;
	#else		
				half3 worldReflectionVector = normalize(reflect(-viewDir, normalNoise));
				fixed4 refl = texCUBE(_ReflectionCubeMap, worldReflectionVector) * _ReflectionColor;			
	#endif
5.法线和高光

         水面法线贴图来实现扰动,在太阳或者月亮的方向产生高光.


6.互动的水波纹

         如果角色走进水面,应该产生波纹的.实现原理就是先渲染一张水波纹扰动图片,然后水面再根据这个图来扰动水面.

------------------------------------------------------------------------------------------------------------------

以上6大功能实现后,这个水面看起来相对完美了,基本满足了我这个强迫症患者的需求.其实还有些地方没处理的,比如水面的LOD,

水面的减面等等.

整个水面shader的编辑界面:


水面生成工具:

// 2016.5.22 luoyinan 自动生成水面
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace Luoyinan
{
    public enum MeshType
    {
        Gizmos_FullMesh,
        Gizmos_WaterMesh,
        WaterMesh,
    }

    public class WaterGenerateTool : MonoBehaviour
    {
        [Range(1, 200)]
        public int halfWidth = 20;
        [Range(1, 200)]
        public int halfHeight = 20;
        public float gridSize = 2.0f;
        public float maxWaterDepth = 4.0f;
        public Material material;

        string m_ShaderName = "Luoyinan/Scene/Water/WaterStandard";
        Mesh m_GizmosMesh;
        Vector3 m_LocalPos;
        int m_GridNumX;
        int m_GridNumZ;

        bool[] m_VerticesFlag;
        float[] m_VerticesAlpha;
        Vector3[] m_Vertices;
        int[] m_Triangles;
        List<Vector3> m_VerticesList = new List<Vector3>();
        List<Color> m_ColorsList = new List<Color>();
        List<Vector2> m_UvList = new List<Vector2>();
        List<int> m_TrianglesList = new List<int>();

        void OnDrawGizmosSelected()
        {
            // 水面指示器
            Gizmos.matrix = transform.localToWorldMatrix;
            if (m_GizmosMesh != null)
                UnityEngine.Object.DestroyImmediate(m_GizmosMesh);
            m_GizmosMesh = CreateMesh(MeshType.Gizmos_WaterMesh);
            Gizmos.DrawWireMesh(m_GizmosMesh);
        }

        public void GenerateWater()
        {
            Mesh mesh = CreateMesh(MeshType.WaterMesh);

            // 渲染水面
            foreach (Transform child in transform)
            {
                DestroyImmediate(child.gameObject);
            }
            string name = string.Format("{0}_{1}_{2}", SceneManager.GetActiveScene().name, transform.name, transform.position); 
            mesh.name = name;
            GameObject go = new GameObject(name);
            go.transform.parent = transform;
            go.transform.localPosition = m_LocalPos;
            go.layer = LayerMask.NameToLayer("Water");
            MeshFilter mf = go.AddComponent<MeshFilter>();
            MeshRenderer mr = go.AddComponent<MeshRenderer>();
            if (material == null)
                material = new Material(Shader.Find(m_ShaderName));
            mr.sharedMaterial = material;
            mf.sharedMesh = mesh; 

            // obj模型不支持顶点颜色,所以暂时不导出了.
            // 导出obj模型
            //MeshToFile(mf, "Assets/" + name + ".obj");
        }

        Mesh CreateMesh(MeshType type)
        {
            Mesh mesh = new Mesh();
            mesh.MarkDynamic();
            m_GridNumX = halfWidth * 2;
            m_GridNumZ = halfHeight * 2;
            Vector3 centerOffset = new Vector3(-halfWidth, 0, -halfHeight);

            if (type == MeshType.Gizmos_FullMesh)
            {
                int vectices_num = m_GridNumX * m_GridNumZ * 4;
                int triangles_num = m_GridNumX * m_GridNumZ * 6;
                m_Vertices = new Vector3[vectices_num];
                m_Triangles = new int[triangles_num];
            
                // 从左下角开始创建,三角形索引顺时针是正面.
                // 2 3
                // 0 1
                for (int z = 0; z < m_GridNumZ; ++z)
                {
                    for (int x = 0; x < m_GridNumX; ++x)
                    {
                        int index = x + z * m_GridNumX;
                        int i = index * 4;
                        int j = index * 6;

                        m_Vertices[i] = GetVertexLocalPos(x, z, centerOffset);
                        m_Vertices[i + 1] = GetVertexLocalPos(x + 1, z, centerOffset);
                        m_Vertices[i + 2] = GetVertexLocalPos(x, z + 1, centerOffset);
                        m_Vertices[i + 3] = GetVertexLocalPos(x + 1, z + 1, centerOffset);

                        m_Triangles[j] = i;
                        m_Triangles[j + 1] = i + 2;
                        m_Triangles[j + 2] = i + 3;
                        m_Triangles[j + 3] = i + 3;
                        m_Triangles[j + 4] = i + 1;
                        m_Triangles[j + 5] = i;
                    
  • 10
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值