【Unity學習】 真實的水面效果開發
阿新 • • 發佈:2019-02-01
這個水面的特效真的是太難做了,調整了好久好久我才把它寫好
實現真實水面的相關要求
使用凹凸紋理
讓水面有起伏感,產生水面有細小波紋的效果使用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<MeshFilter>().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<MeshFilter>().mesh.vertices = vertices; // 使用更改後的頂點位置
uv_offset += (uv_direction * Time.deltaTime*0.1f); // 計算偏離以後的紋理座標
GetComponent<Renderer>().material.SetTextureOffset("_NormalTex",uv_offset); // 設定紋理偏移
GetComponent<MeshFilter>().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<Camera>(); // 新增攝影機元件
mirCam.enabled = false;
refTex = new RenderTexture(800, 600, 16); // 設定圖片大小
refTex.hideFlags = HideFlags.DontSave; // 設定圖片的屬性
mirCam.targetTexture = refTex;
GetComponent<Renderer>().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<Renderer>().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<Renderer>().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"
}
著色器主要是使用法線貼圖、使用漫反射貼圖、新增高光、半透明以及法線的擾動紋理