前面两篇提到场景和角色渲染的技术,这一篇讲一下水面.
水面最开始是美术制作的,一个大的面片来实现,对于湖泊多的场景,这个面片甚至场景那么大,我觉得这种做法不是很好,虽然只有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;