这个水面的特效真的是太难做了,调整了好久好久我才把它写好
使用凹凸纹理
让水面有起伏感,产生水面有细小波纹的效果
使用FTT制作水面的波动
对于水面上的某一个点,其当前的水波可以由若个正舷波叠加得到
添加高光
添加高光主要是水面对阳光和灯光的反射
添加反射
反射主要是在水面位置使用一个摄像机,然后将这个摄像机拍下的画面取反贴在水平面上。
纹理扰动
不能光将反射的图像贴在水平面上,它还需要产生凹凸不平的效果,就像真的水面一样。
我主要使用了三个文件来实现水面效果,两个cs文件一个shader文件
文件一:挂载在水面模型上的Water_wave.cs脚本
作用是计算水面的波纹,定义了是个水波的产生点,它们互相叠加,产生波纹
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Water_wave : MonoBehaviour {
private Vector3[] vertices; //顶点数组
private float mytime; //计时器
public float waveFrequency1 = 0.3f; // 4种波频
public float waveFrequency2 = 0.5f;
public float waveFrequency3 = 0.9f;
public float waveFrequency4 = 1.5f;
private Vector3 v_zero = Vector3.zero; // 零点位置
public float Speed = 1; // 波纹速度
private int index1 = 760; // 一号波纹起始点索引
private int index2 = 900; // 二号波纹起始点索引
private int index3 = 120000; // 三号波纹起始点索引
private Vector2 uv_offset = Vector2.zero; // 纹理偏移量
private Vector2 uv_direction = new Vector2(0.5f, 0.5f); // 纹理偏移方向
// Use this for initialization
void Start () {
vertices = GetComponent().mesh.vertices; // 获取网格顶点坐标数组值
}
// Update is called once per frame
void Update () {
mytime += Time.deltaTime * Speed; // 计时器
for(int i=0; i < vertices.Length; i++)
{
vertices[i] = new Vector3(vertices[i].x, FindHeight(i), vertices[i].z); // 计算定点的y值
}
GetComponent().mesh.vertices = vertices; // 使用更改后的顶点位置
uv_offset += (uv_direction * Time.deltaTime*0.1f); // 计算偏离以后的纹理坐标
GetComponent().material.SetTextureOffset("_NormalTex",uv_offset); // 设置纹理偏移
GetComponent().mesh.RecalculateNormals(); // 重新计算法线
}
float FindHeight(int i)
{
float H = 0;
float distance1 = Vector2.Distance(new Vector2(vertices[i].x, vertices[i].z), v_zero); // 获取点到中心的距离
float distance2 = Vector2.Distance(new Vector2(vertices[i].x, vertices[i].z),
new Vector2(vertices[index1].x, vertices[index1].z)); // 获取点到一号点的距离
float distance3 = Vector2.Distance(new Vector2(vertices[i].x, vertices[i].z),
new Vector2(vertices[index1].x, vertices[index1].z)); // 获取点到二号点的距离
float distance4 = Vector2.Distance(new Vector2(vertices[i].x, vertices[i].z),
new Vector2(vertices[index1].x, vertices[index1].z)); // 获取点到三号点的距离
// 最后的高度就是是个波纹的加权累加 h=距离x波频xPI+时间变化
H = Mathf.Sin((distance1) * waveFrequency1 * Mathf.PI + mytime) / 30;
H += Mathf.Sin((distance2) * waveFrequency2 * Mathf.PI + mytime) / 25;
H += Mathf.Sin((distance3) * waveFrequency3 * Mathf.PI + mytime) / 35;
H += Mathf.Sin((distance4) * waveFrequency4 * Mathf.PI + mytime) / 40;
return i;
}
}
文件二:接下来依然是挂载在水面模型上的Mirror.cs脚本
它的主要功能是提供一个渲染摄像机,将拍摄到的场景存放到一个贴图里
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Mirror : MonoBehaviour {
public RenderTexture refTex; // 声明一张图片
public Matrix4x4 correction; // 修正矩阵
public Matrix4x4 projM; // 摄像机的投影矩阵
Matrix4x4 world2ProjView; // 摄像机自身矩阵
public Matrix4x4 cm; // 摄像机内的投影矩阵
private Camera mirCam; // 镜像摄像机
private bool busy = false; // 忙碌标志位
void Start () {
if (mirCam) return; // 如果有摄影机了,就不再产生
GameObject g = new GameObject("Mirror Camera"); // 创建一个镜像摄像机对象
mirCam = g.AddComponent(); // 添加摄影机组件
mirCam.enabled = false;
refTex = new RenderTexture(800, 600, 16); // 设置图片大小
refTex.hideFlags = HideFlags.DontSave; // 设置图片的属性
mirCam.targetTexture = refTex;
GetComponent().material.SetTexture("_MainTex",refTex); // 将反射图传递给着色器
correction = Matrix4x4.identity; // 初始化修正矩阵
correction.SetColumn(3, new Vector4(0.5f, 0.5f, 0.5f, 1f)); // 修正矩阵第四列
correction.m00 = 0.5f; // 设置矩阵特定位置参数
correction.m11 = 0.5f;
correction.m22 = 0.5f;
}
void Update () {
GetComponent().material.SetTexture("_MainTex", refTex); // 将反射图传递给着色器
}
private void OnWillRenderObject() // 如果对象可见,这相机都会调用这个函数
{
if (busy) return; // 忙吗?
busy = true; // 不忙,忙起来
Camera cam = Camera.main; // 获取主摄像机
mirCam.CopyFrom(cam); // 将主摄像机的设置拷贝给镜像摄像机
mirCam.transform.parent = transform; // 设置镜像相机的父对象为水平面
Camera.main.transform.parent = transform; // 设置主摄像机的对象为水平面
Vector3 mPos = mirCam.transform.localPosition; // 记录镜像相机的位置
mPos.y *= -1f; // 对位置做镜像
mirCam.transform.localPosition = mPos; // 将设置好的位置赋予镜像相机
Vector3 rt = Camera.main.transform.localEulerAngles; // 记录主摄像机的朝向参数
Camera.main.transform.parent = null; // 将主摄像机的父对象设置为空
mirCam.transform.localEulerAngles = new Vector3(-rt.x, rt.y, -rt.z); // 根据之前的主摄像机角度做镜像
// 计算镜像相机到水平面的距离
float d = Vector3.Dot(transform.up, Camera.main.transform.position - transform.position) + 0.05f;
mirCam.nearClipPlane = d; // 设置镜像相机的裁剪近平面
Vector3 pos = transform.position; // 记录水平面的位置
Vector3 normal = transform.up; // 记录法线方向
Vector4 clipPlane = CameraSpacePlane(mirCam, pos, normal, 1.0f); // 计算裁剪平面
Matrix4x4 proj = cam.projectionMatrix; // 获取摄像机的投影矩阵
proj = cam.CalculateObliqueMatrix(clipPlane); // 计算倾斜矩阵
mirCam.projectionMatrix = proj; // 指定镜面相机的投影矩阵
mirCam.targetTexture = refTex; // 指定渲染图片
mirCam.Render(); // 渲染
Proj(); // 计算摄像机内投影矩阵
GetComponent().material.SetMatrix("_Projmat",cm); // 传递摄像机内部投影矩阵到着色器
busy = false;
}
// 计算摄像机内投影矩阵 方法:想计算出摄像机的自身矩阵,再计算出摄像机的投影矩阵
void Proj()
{
world2ProjView = mirCam.transform.worldToLocalMatrix; // 将世界矩阵化为自身矩阵
projM = mirCam.projectionMatrix; // 得到摄像机的投影矩阵
projM.m32 = 1f; // 修改第三排第二个数字
cm = correction * projM * world2ProjView; // 设置摄像机内投影矩阵
}
// 计算裁剪平面,即水平面。 方法:先添加一个扰动量,附加在水平面的位置上,使裁剪面的位置略低于水平面,然后得到摄像机矩阵变换后的信息
private Vector4 CameraSpacePlane(Camera cam, Vector3 pos, Vector3 normal, float sideSign)
{
Vector3 offsetPos = pos + normal * -0.1f; // 偏移后位置
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)); // 返回裁剪平面信息
}
}
其实这段代码写得有点复杂,我还在优化
基本思路是设置一个以睡眠为镜面中心对主摄像机镜像出一个镜像摄像机,并且计算出该摄像机的裁剪平面
文件三:水面的Shader脚本 WaterShader
Shader "Custom/WaterShader" {
Properties{
_MainTint("Diffuse Tint", Color) = (1,1,1,0) // 反射纹理色调
_MainTex("Base (RGB)", 2D) = "white" {} // 反射纹理
_BackTint("Back Tint", Color) = (1,1,1,0) // 背面纹理色调
_BackTex("Background", 2D) = "white" {} // 背景纹理
_SpecColor("Specular Color", Color) = (1,1,1,1) // 高光颜色
_SpecPower("Specular Power", Range(0.5, 100)) = 3 // 高光强度
_NormalTex("Normal Map", 2D) = "bump"{} // 法线贴图
_TransVal("Transparecy Value", Range(0, 1)) = 0.5 // 透明度
_PerturbationAmt("Perturbation Amt", Range(0, 1)) = 1 // 扰动参数
}
// 13
SubShader{
Tags { "Queue" = "Transparent-20" "RenderType" = "Opaque" } // 用来保证渲染顺序在透明之前
CGPROGRAM
#pragma surface surf CustomBlinnPhong vertex:vert alpha
#pragma target 3.0
#include "UnityCG.cginc"
float4 _MainTint;
sampler2D _MainTex;
float4 _BackTint;
sampler2D _BackTex;
//float4 _SpecColor;
float _SpecPower;
sampler2D _NormalTex;
float _TransVal;
float _PerturbationAmt;
float4x4 _ProjMat; // 摄像机投影矩阵
// 30
struct Input {
float2 uv_MainTex; // 反射纹理
float2 uv_NormalTex; // 法线纹理
float4 pos; // 定点位置
float4 texc; // 扰动后的纹理坐标
INTERNAL_DATA
};
inline fixed4 LightingCustomBlinnPhong(SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten){
float3 halfVector = normalize(lightDir+viewDir); // 半角向量h
float diff = max(0,dot(s.Normal,lightDir)); // 对漫反射的计算 法线*关照方向
float nh = max(0,dot(s.Normal,halfVector)); // 高光 法线*半角向量h
float spec = pow(nh,_SpecPower)*_SpecColor; // 计算高光强度
float4 c; // 声明一个颜色
c.rgb=(s.Albedo*_LightColor0.rgb*diff)+(_LightColor0.rgb*_SpecColor.rgb*spec)*(atten*2); // 高光颜色
c.a = s.Alpha; // 设置透明度
return c; // 返回颜色
}
// 50
void vert (inout appdata_full v, out Input o) {
UNITY_INITIALIZE_OUTPUT(Input, o); // 声明结构体o
o.pos=v.vertex; // 设置pos参数为该结构体位置
}
void surf (Input IN, inout SurfaceOutput o){
float4x4 proj=mul(_ProjMat, _Object2World); // 摄像机投影矩阵转世界矩阵
IN.texc=mul(proj, IN.pos); // 使用proj来转换顶点坐标
float4 c_Back = tex2D(_BackTex, IN.uv_MainTex); // 背面贴图采样
float3 normalMap = UnpackNormal(tex2D(_NormalTex, IN.uv_NormalTex)); // 采样法线图
half2 offset=IN.texc.rg/IN.texc.w; // 原纹理坐标
offset.x = offset.x+_PerturbationAmt*offset.x*normalMap.x; // 根据法线扰动之后的纹理坐标x
offset.y = offset.y+_PerturbationAmt*offset.y*normalMap.y; // 根据法线扰动之后的纹理坐标y
float4 c_Main = tex2D(_MainTex, offset) * _MainTint; // 反射纹理采样
float3 finalColor = lerp(c_Back, c_Main, 0.7).rgb*_BackTint; // 最终颜色
o.Normal = normalize(normalMap.rgb+o.Normal.rgb); // 设置片元法线
o.Specular = _SpecPower; // 设置高光强度
o.Gloss = 1.0; // 设置自发光强度
o.Albedo = finalColor; // 设置反射颜色
o.Alpha = (c_Main.a*0.5+0.5)*_TransVal; // 设置透明度
}
ENDCG
}
FallBack "Diffuse"
}
着色器主要是使用法线贴图、使用漫反射贴图、添加高光、半透明以及法线的扰动纹理