笔者介绍:姜雪伟,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<b; L0D++) { //if(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; y<g_height; y+=den) { for (int x=0; x<g_width; x+=den) { int idx2 = g_width * y + x; verticesLOD[L0D] [idx] = vertices [idx2]; //lower the far lods to eliminate gaps in the horizon when having big waves if(L0D>0) { 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<tiles; y++) { for (int x=0; x<tiles; x++) { chDist = System.Math.Max (System.Math.Abs (tiles / 2 - y), System.Math.Abs (tiles / 2 - x)); chDist = chDist > 0 ? chDist - 1 : 0; if(nmaxLod<chDist) nmaxLod = chDist; } } max_LOD = nmaxLod+1; flodoffset = new float[max_LOD+1]; float ffact = farLodOffset/max_LOD; for(int i=0; i<max_LOD+1; i++) { flodoffset[i] = i*ffact; } btiles_LOD = new List<Mesh>(); tiles_LOD = new List<List<Mesh>>(); for (int L0D=0; L0D<max_LOD; L0D++) { btiles_LOD.Add(new Mesh()); tiles_LOD.Add (new List<Mesh>()); } GameObject tile; int ntl = LayerMask.NameToLayer ("Water"); for (int y=0; y<tiles; y++) { for (int x=0; x<tiles; x++) { chDist = System.Math.Max (System.Math.Abs (tiles / 2 - y), System.Math.Abs (tiles / 2 - x)); chDist = chDist > 0 ? chDist - 1 : 0; if(nmaxLod<chDist) nmaxLod = chDist; float cy = y - Mathf.Floor(tiles * 0.5f); float cx = x - Mathf.Floor(tiles * 0.5f); tile = new GameObject ("Lod_"+chDist.ToString()+":"+y.ToString()+"x"+x.ToString()); Vector3 pos=tile.transform.position; pos.x = cx * size.x; pos.y = transform.position.y; pos.z = cy * size.z; tile.transform.position=pos; tile.AddComponent <MeshFilter>(); tile.AddComponent <MeshRenderer>(); Renderer renderer = tile.GetComponent<Renderer>(); tile.GetComponent<MeshFilter>().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<MeshFilter>().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<Vector3> blobs; private List<float[]> prevBoya; //private bool engine = false; private List<float> 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<Renderer>(); if(!_renderer) { _renderer = GetComponentInChildren<Renderer>(); } } 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<Rigidbody>(); 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<BoxCollider> ().size; float length = bounds.z; float width = bounds.x; blobs = new List<Vector3> (); prevBoya = new List<float[]>(); int i = 0; float xstep = 1.0f / ((float)SlicesX - 1f); float ystep = 1.0f / ((float)SlicesZ - 1f); sinkForces = new List<float>(); float totalSink = 0; for (int x=0; x<SlicesX; x++) { for (int y=0; y<SlicesX; y++) { blobs.Add (new Vector3 ((-0.5f + x * xstep) * width, 0.0f, (-0.5f + y * ystep) * length) + Vector3.up * ypos); if(interpolate) { prevBoya.Add(new float[interpolation]); } float force = Random.Range(0f,1f); force = force * force; totalSink += force; sinkForces.Add(force); i++; } } // normalize the sink forces for (int j=0; j< sinkForces.Count; j++) { sinkForces[j] = sinkForces[j] / totalSink * sinkForce; } } void Update() { if(!useFixedUpdate) update(); } void FixedUpdate() { if(useFixedUpdate) update(); } bool visible, lastvisible; int lastFrame=-15; void update() { if (ocean != null) { visible = _renderer.isVisible; //put object on the correct height of the sea surface when it has visibilty checks on and it became visible again. if(visible != lastvisible) { if(visible && !lastvisible) { if(Time.frameCount-lastFrame>15) { 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; j<blobs.Count; j++) { wpos = transform.TransformPoint (blobs[j]); //get a random blob to apply a force with the choppy waves if(ChoppynessAffectsPosition) { if(j == ran) cpos = wpos; } if(!cvisible || visible) { float buyancy = magnitude * (wpos.y); if (ocean.enabled) { if(ocean.canCheckBuoyancyNow[0]==1) { float off = 0; if(ocean.choppy_scale>0) 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; i<intplt; i++) { bbuyancy += prevBoya[k][i]; } bbuyancy *= iF; } } rrigidbody.AddForceAtPosition (-Vector3.up * (bbuyancy + coef * damp), wpos); k++; } } if(interpolate) { tick++; if(tick==intplt) tick=0; } tack++; if (tack == (int)Random.Range(2, 9) ) tack=0; if(tack>9) 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去查看。
代码链接地址: http://pan.baidu.com/s/1pK91RYV 密码:xdx6