Unity3D教程:实现水面渲染(四)
水面渲染在很多游戏项目中都会碰到,下面就分四篇文章去给大家介绍在Unity3D中水面渲染的实现方法,现在介绍的水面渲染(四)。
一、前言
本文旨在与大家一起探讨学习新知识,如有疏漏或者谬误,请大家不吝指出。
二、概述
先来看看最后的实现效果:
目前为止,我们已经实现了水波的模拟,反射以及折射。上一张没有散射的效果图:
我们常看到的海水是呈现一种通透的蓝色,而我们现在还没有实现这种水的颜色,这章我们就来讨论下如何实现。
上面我们说到的水的颜色,其实是指的光线在水中散射后被我们眼睛所接受到的颜色。这个跟大气散射的原理其实是类似的。由于水中有微小的颗粒,光线射到这些颗粒上面就会产生散射,不同大小的颗粒产生散射的颜色是不同的,当然颗粒本身的颜色也会影响我们观察到的水的颜色。这种现象最明显的要数黄龙的五彩池了。
三、散射的计算
如上图,e点是我们视点所在位置,w是我们观察到的水底的某个点。现在我们已知水面的高度water_height,e点的位置_WorldSpaceCameraPos,以及w点的世界坐标worldPos,我们第一步需要求出wp线段的长度:
根据相似三角的定律,我们得出:
ow : hw = we :wp;
所以有:
wp = we * hw /ow;
根据上述已知条件有:
1 2 3 4 5 | we =length(_WorldSpaceCameraPos.xyz - worldPos); wh= water_height- worldPos.y; ow =_WorldSpaceCameraPos.y; |
带入后就可以求出wp的长度。
在得到wp的长度后,光线从w点穿过水,到达p点射出水面进入我们的眼中。在这里我们使用了一个假设条件:光线从p点射出后并未发生折射,这会大大简化我们的计算。在这个光线的传播过程中,不断有光线从wp线段上散射出去,我们可以使用指数函数exp来模拟这个光的衰减过程:
1 | outScattering =exp(-attenuation*wp); |
其中attenuation系数表示光的衰减系数。
除了上面光的out,在wp这个光路中,还会有不同的光从四面八方汇聚到wp线段上,进入我们的视点,我们称这个过程为in,这里给出一个简化的公式:
1 | inScattering=diff_radiance*(1-outScattering*exp(-wh*kd)); |
上述的公式中,diff_radiance指水的辐射度。
四、实现散射
在实现上其实有两种大体的思路来实现散射,一种是专门为海底的物体写一个shader,这个shader里包含散射的计算;另外一种是专门用一个相机来渲染海底的场景并计算散射。在这里我选择第二种思路,第一种方法可能效率更高,不过第二种方法适用性更广更方便。
由于之前有了折射的相机来渲染海底的场景,所以我在这里就直接使用折射相机来渲染散射,然后直接把散射的计算结果叠加在折射图上。散射的参数则通过Shader.SetGlobal来传入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | sampler2D_MainTex; float4_MainTex_ST; sampler2D_UnScatteringRefrTex; float3_WaterScatteringKd; float3_WaterScatteringAttenuation; float3_WaterScatteringDiffuseRadiance; float_WaterAltitude; voidSimpleWaterScattering(half viewDist, half3 worldPos, half depth, half3diffuseRadiance, half3attenuation_c, half3 kd, out half3 outScattering, out half3 inScattering) { floatt = depth / (_WorldSpaceCameraPos.y - worldPos.y); //Water scattering floatd = viewDist*t; // one way! outScattering= exp(-attenuation_c*d); inScattering= diffuseRadiance* (1 - outScattering*exp(-depth*kd)); } v2f vert(appdata v) { v2fo; o.vertex= mul(UNITY_MATRIX_MVP, v.vertex); o.uv= TRANSFORM_TEX(v.uv, _MainTex); o.screenPos= ComputeScreenPos(o.vertex).xyw; float3worldPos = mul(_Object2World, v.vertex); float3outScattering = float3(1,0,0); float3inScattering = float3(1,0,0); float3viewVector = worldPos - _WorldSpaceCameraPos.xyz; floatdepth = max(0, _WaterAltitude - worldPos.y); SimpleWaterScattering(length(viewVector),worldPos.xyz,depth,_WaterScatteringDiffuseRadiance,_WaterScatteringAttenuation, _WaterScatteringKd, outScattering, inScattering); o.outScattering= outScattering; o.inScattering= inScattering; returno; } half4 frag (v2fi) : SV_Target { //sample the texture half4result = half4(0,0,0,1); float2srcUV = i.screenPos.xy/i.screenPos.z; half4refractionColor = tex2D(_UnScatteringRefrTex, srcUV); result.xyz= refractionColor.xyz*i.outScattering+i.inScattering; result.w= refractionColor.w; returnresult; } |
五、实现高光
高光的实现其实很简单,使用Blinn-Phone公式就行了:
1 2 3 | float3 halfVector= normalize(-viewVector+normalize(_WorldSpaceLightPos0.xyz)); float3specularColor = pow(max(0, dot(halfVector, worldNormal)), _Shiness); |
半角向量halfVector是指点到相机的向量和光照方向向量的和。在这里我直接是用的平行光,并没有考虑光源是点光源的情况。需要注意的是点到相机的向量的计算不要搞错方向了,正确的是(_WorldSpaceCameraPos - worldPos)。
完整的源码下载: