笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等。
CSDN视频网址:http://edu.csdn.net/lecturer/144
海水仿真渲染一直是比较困难的事情,虽然市面上有了各种海水的渲染算法,但是真正做到仿真渲染的少之又少。大多停留在试验阶段,达到了仿真的要求,但是硬件配置要求高,不利于推广。本篇博客给读者介绍的是关于海水的实施渲染以及通过算法实现船只航行轨迹效果,真实的达到了海水的渲染,海水的网格采用了多个面片网格拼接的方式,网格采用的是LOD处理的,这样就优化了效率。同时将海水的绘制以及算法实现放到C++中进行,这样对于复杂算法的实现效率明显提升。先给读者看几幅效果图:
船在海水中航行的轨迹效果图,轨迹是实时绘制的。再来一副海水到岸边产生的泡沫效果图:
除了岸边的效果图外,船在水中周围也会产生泡沫效果。最后一副效果图如下:
海水的反射折射效果。下面开始给读者介绍实现该海水的原理以及核心代码,最后把整个工程奉献给读者。
第一步:海水网格的实现,海水网格采用的是面片拼接的方式,并且面片采用了LOD运算,其在Unity中的效果如下所示:
从里向外,面片的数量逐步减少,它是根据摄像机的远近处理的,对应的核心代码如下所示:
//update the meshes with the final calculated mesh data
void updateTiles(int a, int b) {
if(skipLods) {
lodSkip++;
if(lodSkip >= lodSkipFrames+1) lodSkip=0;
}
for (int L0D=a; L0D
//this will skip one update of the tiles higher then Lod0
if(L0D>0 && lodSkip==0 && !ticked && skipLods) { break; }
//this will skip one update of the LOD0 tiles because they got updated earlier when they should.
if(ticked2 && L0D==0) { ticked2=false; continue; }
#if !NATIVE
int den = MyIntPow (2, L0D);
int idx = 0;
for (int y=0; y0) {
if(farLodOffset!=0) {
verticesLOD[L0D] [idx].y += flodoffset[L0D] * flodFact;
}
}
tangentsLOD[L0D] [idx] = tangents [idx2];
normalsLOD[L0D] [idx++] = normals [idx2];
}
}
#else
uocean._updateTilesA(verticesLOD[L0D], vertices, tangentsLOD[L0D], tangents, normalsLOD[L0D], normals, L0D, farLodOffset, flodoffset, flodFact);
#endif
btiles_LOD[L0D].vertices = verticesLOD[L0D];
btiles_LOD[L0D].normals = normalsLOD[L0D];
btiles_LOD[L0D].tangents = tangentsLOD[L0D];
}
if(ticked) ticked = false;
}
void GenerateTiles() {
int chDist, nmaxLod=0; // Chebychev distance
for (int y=0; y 0 ? chDist - 1 : 0;
if(nmaxLod();
tiles_LOD = new List>();
for (int L0D=0; L0D());
}
GameObject tile;
int ntl = LayerMask.NameToLayer ("Water");
for (int y=0; y 0 ? chDist - 1 : 0;
if(nmaxLod();
tile.AddComponent ();
Renderer renderer = tile.GetComponent();
tile.GetComponent().mesh = btiles_LOD[chDist];
//tile.isStatic = true;
//shader/material lod (needs improvement)
if(useShaderLods && numberLods>1) {
if(numberLods==2) {
if(chDist <= sTilesLod) { if(material) renderer.material = material; }
if(chDist > sTilesLod) { if(material1) renderer.material = material1; }
}else if(numberLods==3){
if(chDist <= sTilesLod ) { if(material) renderer.material = material; }
if(chDist == sTilesLod+1) { if(material1) renderer.material = material1; }
if(chDist > sTilesLod+1) { if(material2) renderer.material = material2; }
}
} else {
renderer.material = material;
}
//Make child of this object, so we don't clutter up the
//scene hierarchy more than necessary.
tile.transform.parent = transform;
//Also we don't want these to be drawn while doing refraction/reflection passes,
//so we'll add the to the water layer for easy filtering.
tile.layer = ntl;
tiles_LOD[chDist].Add( tile.GetComponent().mesh);
}
}
//enable/disable the fixed disc
initDisc();
}
在Unity中生成的效果如下所示:
第二步,海水的渲染Shader,根据不同的LOD等级实行不同的Shader渲染,举个例子,LOD等级2的Shader代码如下所示:
Shader "Mobile/OceanL2" {
Properties {
_SurfaceColor ("SurfaceColor", Color) = (1,1,1,1)
_WaterColor ("WaterColor", Color) = (1,1,1,1)
_Specularity ("Specularity", Range(0.01,1)) = 0.3
_SpecPower("Specularity Power", Range(0,1)) = 1
[HideInInspector] _SunColor ("SunColor", Color) = (1,1,0.901,1)
_Bump ("Bump (RGB)", 2D) = "bump" {}
_Size ("UVSize", Float) = 0.015625//this is the best value (1/64) to have the same uv scales of normal and foam maps on all ocean sizes
[HideInInspector] _SunDir ("SunDir", Vector) = (0.3, -0.6, -1, 0)
_FakeUnderwaterColor ("Water Color LOD1", Color) = (0.196, 0.262, 0.196, 1)
_DistanceCancellation ("Distance Cancellation", Float) = 2000
}
//water bump
SubShader {
Tags { "RenderType" = "Opaque" "Queue"="Geometry"}
LOD 2
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//#pragma multi_compile_fog
#pragma multi_compile FOGON FOGOFF
#pragma multi_compile DCON DCOFF
#pragma target 2.0
#include "UnityCG.cginc"
struct v2f {
float4 pos : SV_POSITION;
half3 floatVec : TEXCOORD0;
float2 bumpTexCoord : TEXCOORD1;
//half3 viewDir : TEXCOORD2;
half3 lightDir : TEXCOORD2;
half2 buv : TEXCOORD3;
half3 normViewDir : TEXCOORD4;
//UNITY_FOG_COORDS(7)
#ifdef FOGON
half dist : TEXCOORD5;
#ifdef DCON
half distCancellation : TEXCOORD6;
#endif
#endif
};
half _Size;
half4 _SunDir;
half4 _FakeUnderwaterColor;
#ifdef FOGON
uniform half4 unity_FogStart;
uniform half4 unity_FogEnd;
uniform half4 unity_FogDensity;
#ifdef DCON
half _DistanceCancellation;
#endif
#endif
v2f vert (appdata_tan v) {
v2f o;
UNITY_INITIALIZE_OUTPUT(v2f, o);
o.bumpTexCoord.xy = v.vertex.xz*_Size;///float2(_Size.x, _Size.z)*5;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
half3 objSpaceViewDir = ObjSpaceViewDir(v.vertex);
half3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) );
half3x3 rotation = half3x3( v.tangent.xyz, binormal, v.normal );
half3 viewDir = mul(rotation, objSpaceViewDir);
o.lightDir = mul(rotation, half3(_SunDir.xyz));
o.buv = float2(o.bumpTexCoord.x + _CosTime.x * 0.2, o.bumpTexCoord.y + _SinTime.x * 0.3);
o.normViewDir = normalize(viewDir);
o.floatVec = normalize(o.normViewDir - normalize(o.lightDir));
#ifdef FOGON
//manual fog
half fogDif = 1.0/(unity_FogEnd.x - unity_FogStart.x);
o.dist = (unity_FogEnd.x - length(o.pos.xyz)) * fogDif;
#ifdef DCON
o.distCancellation = (unity_FogEnd.x - _DistanceCancellation) * fogDif;
#endif
#endif
//autofog
//UNITY_TRANSFER_FOG(o, o.pos);
return o;
}
sampler2D _Bump;
half4 _WaterColor;
half4 _SurfaceColor;
half _Specularity;
half _SpecPower;
half4 _SunColor;
half4 frag (v2f i) : COLOR {
#ifdef FOGON
#ifdef DCON
if(i.dist>i.distCancellation){
#endif
#endif
half3 tangentNormal0 = (tex2D(_Bump, i.buv.xy) * 2.0) -1;
half3 tangentNormal = normalize(tangentNormal0);
half4 result = half4(0, 0, 0, 1);
//half fresnelLookup = dot(tangentNormal,i. normViewDir);
//float bias = 0.06;
//float power = 4.0;
//half fresnelTerm = 0.06 + (1.0-0.06)*pow(1.0 - fresnelLookup, 4.0);
half fresnelTerm = 1.0 - saturate(dot (i.normViewDir, tangentNormal0));
half specular = pow(max(dot(i.floatVec, tangentNormal) , 0.0), 250.0 * _Specularity ) * _SpecPower;
result.rgb = lerp(_WaterColor*_FakeUnderwaterColor, _SunColor.rgb*_SurfaceColor*0.85, fresnelTerm*0.6) + specular*_SunColor.rgb;
//fog
//UNITY_APPLY_FOG(i.fogCoord, result);
#ifdef FOGON
//manual fog (linear) (reduces instructions on d3d9)
float ff = saturate(i.dist);
result.rgb = lerp(unity_FogColor.rgb, result.rgb, ff);
#endif
return result;
#ifdef FOGON
#ifdef DCON
}else{
return unity_FogColor;
}
#endif
#endif
}
ENDCG
}
}
}
渲染的效果图如下所示:
第三步:船只在海水中航行的轨迹效果以及随着海水起伏效果实现代码
using UnityEngine;
using System.Collections.Generic;
public class Boyancy : MonoBehaviour {
private Ocean ocean;
public int renderQueue;
public bool useFixedUpdate = false;
public bool moreAccurate = false;
public float magnitude = 2f;
public float ypos = 0.0f;
private List blobs;
private List prevBoya;
//private bool engine = false;
private List sinkForces;
public float CenterOfMassOffset = -1f;
public float dampCoeff = .1f;
//buoyancy slices. (Cannot be smaller then 2)
//Raise these numbers if you want more accurate simulation. However it will add overhead. So keep it as small as possible.
public int SlicesX = 2;
public int SlicesZ = 2;
public int interpolation = 3;
private int intplt;
public bool ChoppynessAffectsPosition = false;
public float ChoppynessFactor = 0.2f;
public bool WindAffectsPosition = false;
public float WindFactor = 0.1f;
public bool xAngleAddsSliding = false;
public float slideFactor = 0.1f;
public bool cvisible, wvisible, svisible;
public Renderer _renderer ;
public bool sink = false;
public float sinkForce = 3;
private float iF;
private bool interpolate = false;
private Rigidbody rrigidbody;
private int tick, tack;
private Vector3 wpos, cpos;
private bool useGravity;
private float accel;
private int prevAngleX, currAngleX;
private float bbboyancy;
private float prevBuoyancy;
void Start () {
if(!_renderer) {
_renderer = GetComponent();
if(!_renderer) {
_renderer = GetComponentInChildren();
}
}
if(_renderer && renderQueue>0) _renderer.material.renderQueue = renderQueue;
if(!_renderer) {
if(cvisible) { Debug.Log("Renderer to check visibility not assigned."); cvisible = false; }
if(wvisible) { Debug.Log("Renderer to check visibility not assigned."); wvisible = false; }
if(svisible) { Debug.Log("Renderer to check visibility not assigned."); svisible = false; }
}
if(dampCoeff<0) dampCoeff = Mathf.Abs(dampCoeff);
rrigidbody = GetComponent();
useGravity = rrigidbody.useGravity;
if(interpolation>0) {
interpolate = true;
iF = 1/(float)interpolation;
intplt = interpolation;
}
if(SlicesX<2) SlicesX=2;
if(SlicesZ<2) SlicesZ=2;
ocean = Ocean.Singleton;
rrigidbody.centerOfMass = new Vector3 (0.0f, CenterOfMassOffset, 0.0f);
Vector3 bounds = GetComponent ().size;
float length = bounds.z;
float width = bounds.x;
blobs = new List ();
prevBoya = new List();
int i = 0;
float xstep = 1.0f / ((float)SlicesX - 1f);
float ystep = 1.0f / ((float)SlicesZ - 1f);
sinkForces = new List();
float totalSink = 0;
for (int x=0; x15) {
float off = ocean.GetChoppyAtLocation(transform.position.x, transform.position.z);
float y = ocean.GetWaterHeightAtLocation2 (transform.position.x-off, transform.position.z);
transform.position = new Vector3(transform.position.x, y, transform.position.z);
lastFrame = Time.frameCount;
}
}
lastvisible = visible;
}
//prevent use of gravity when buoyancy is disabled
if(cvisible) {
if(useGravity) {
if(!visible) {
rrigidbody.useGravity=false;
if(wvisible && svisible) return;
} else {
rrigidbody.useGravity = true;
}
}else {
if(!visible) { if(wvisible && svisible) return;}
}
}
float coef = dampCoeff;
int index = 0, k=0;
int ran = (int)Random.Range(0, blobs.Count-1);
for(int j = 0; j0) off = ocean.GetChoppyAtLocation(wpos.x, wpos.z);
if(moreAccurate) {
buyancy = magnitude * (wpos.y - ocean.GetWaterHeightAtLocation2 (wpos.x-off, wpos.z));
}else {
buyancy = magnitude * (wpos.y - ocean.GetWaterHeightAtLocation (wpos.x-off, wpos.z));
buyancy = Lerp(prevBuoyancy, buyancy, 0.5f);
prevBuoyancy = buyancy;
}
bbboyancy = buyancy;
} else {
buyancy = bbboyancy;
}
}
if (sink) { buyancy = System.Math.Max(buyancy, -3) + sinkForces[index++]; }
float damp = rrigidbody.GetPointVelocity (wpos).y;
float bbuyancy = buyancy;
//interpolate last (int interpolation) frames to smooth out the jerkiness
//interpolation will be used only if the renderer is visible
if(interpolate) {
if(visible) {
prevBoya[k][tick] = buyancy;
bbuyancy=0;
for(int i=0; i9) tack =1;
//if the boat has high speed do not influence it (choppyness and wind)
//if it has lower then fact then influence it depending on the speed .
float fact = rrigidbody.velocity.magnitude * 0.02f;
//this code is quick and dirty
if(fact<1) {
float fact2 = 1-fact;
//if the object gets its position affected by the force of the choppy waves. Useful for smaller objects).
if(ChoppynessAffectsPosition) {
if(!cvisible || visible) {
if(ocean.choppy_scale>0) {
if(moreAccurate) {
if(tack==0) rrigidbody.AddForceAtPosition (-Vector3.left * (ocean.GetChoppyAtLocation2Fast() * ChoppynessFactor*Random.Range(0.5f,1.3f))*fact2, cpos);
else rrigidbody.AddForceAtPosition (-Vector3.left * (ocean.GetChoppyAtLocation2Fast() * ChoppynessFactor*Random.Range(0.5f,1.3f))*fact2, transform.position);
} else {
if(tack==0) rrigidbody.AddForceAtPosition (-Vector3.left * (ocean.GetChoppyAtLocationFast() * ChoppynessFactor*Random.Range(0.5f,1.3f))*fact2, cpos);
else rrigidbody.AddForceAtPosition (-Vector3.left * (ocean.GetChoppyAtLocationFast() * ChoppynessFactor*Random.Range(0.5f,1.3f))*fact2, transform.position);
}
}
}
}
//if the object gets its position affected by the wind. Useful for smaller objects).
if(WindAffectsPosition) {
if(!wvisible || visible) {
if(tack==1) rrigidbody.AddForceAtPosition(new Vector3(ocean.pWindx, 0 , ocean.pWindy) * WindFactor*fact2, cpos);
else rrigidbody.AddForceAtPosition(new Vector3(ocean.pWindx, 0 , ocean.pWindy) * WindFactor*fact2, transform.position);
}
}
}
//the object will slide down a steep wave
//modify it to your own needs since it is a quick and dirty method.
if(xAngleAddsSliding) {
if(!svisible || visible) {
float xangle = transform.localRotation.eulerAngles.x;
currAngleX = (int)xangle;
if(prevAngleX != currAngleX) {
float fangle=0f;
if(xangle>270 && xangle<355) {
fangle = (360-xangle)*0.1f;
accel -= fangle* slideFactor; if(accel<-20) accel=-20;
}
if(xangle>5 && xangle<90) {
fangle = xangle*0.1f;
accel += fangle* slideFactor; if(accel>20) accel=20;
}
prevAngleX = currAngleX;
}
if((int)accel!=0) rrigidbody.AddRelativeForce (Vector3.forward * accel, ForceMode.Acceleration);
if(accel>0) { accel-= 0.05f; if(accel<0) accel=0; }
if(accel<0) { accel+= 0.05f; if(accel>0) accel=0; }
}
}
}
}
public void Sink(bool isActive) { sink = isActive; }
static float Lerp (float from, float to, float value) {
if (value < 0.0f) return from;
else if (value > 1.0f) return to;
return (to - from) * value + from;
}
}
在Unity编辑器中的表现如下所示:
效果非常绚丽。。。。。。。。
第四步,除了LOD海水网格渲染外,还提供了整个海水平面的渲染,以及可视化的界面操作,效果如下所示:
在该操作界面中有OceanMaterial材质对应的Shader脚本,在这里就不一一列举了。
第五步,多线程渲染主要是用于初始化海水网格以及算法的计算:
uocean.setThreads(2);
if(SystemInfo.processorCount == 1) uocean.setThreads(1);
//--------------------------------------------------------------------------------------------------------------------------------------------
uocean.UoceanInit(width, height, pWindx, pWindy, speed, waveScale, choppy_scale, size.x, size.y, size.z, waveDistanceFactor);
uocean._calcComplex(data, t_x, Time.time, 0, height);
uocean._fft1(data);
uocean._fft2(t_x);
uocean._calcPhase3(data, t_x, vertices, baseHeight, normals, tangents, reflectionRefractionEnabled, canCheckBuoyancyNow, waveScale);
其他的读者可以通过Demo去查看。
代码链接地址: https://pan.baidu.com/s/1WeECo8kksWG8PDMdKrrLXQ 提取码:biyc