【GPU Gems 学习笔记】Rendering Water Caustics

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/LeeXxs/article/details/87979215

一. 水的焦散

焦散(Caustic),是一种光学现象,是由于光线在曲面经过反射或折射后形成的聚光效果,类似于凸透镜效果。


对于不透明物体来说,当光线照射到具有强反射属性的曲面对象时,会形成反射焦散现象;而当光线照射到透明物体时,如流动的水或玻璃杯,通过折射后形成聚光的折射焦散现象。

                 


同理,对于水体来说,焦散效果不是在水面产生。一部分光线在水表面发生折射,穿透水面在水底形成折射焦散。另一部分光线被水面反射,可以在墙壁等平面形成反射焦散。

本章内容主要介绍了从美学的角度出发,来实时渲染水下的折射焦散的方法。
 

二. 原理与实现

光线入水后发生一次折射后继续前进,随着入水深度的增减,光强度逐渐减弱。最后,一部分光子经过不同路径,碰到了海底相同的区域并将其照亮,形成明亮的光斑,类似于凸透镜的聚光现象。无论是基于光线追踪还是逆向光线追踪的方法来实现,都非常的费时,因为只有极小部分的计算对最终结果有实际意义。

为了方便计算焦散,大胆假设:
       1. 我们是在赤道上计算正午的水面焦散,这意味着太阳处于头顶正上方;
       2. 对焦散效果做出贡献的光线与海底垂直。
          (光线从入水到碰到海底,传播距离最短的光线,才容易形成焦散,其余的被介质吸收化成热量发散)

具体实现步骤分为两步:
      1. 绘制海底底面:
            a. 假设有一道垂直于该顶点的折射光线;
            b. 找到该折射光线与水面的交点;
            c. 计算交点的法向量
            d. 逆向使用斯涅尔定律(Shell)计算入射光线;
               (书中代码并没有计算入射光线,而是使用了水面顶点法线方向和水面顶点与水底的距离做为采样坐标)
            d. 使用入射光线作为纹理坐标对"太阳"贴图采样,采样结果为焦散的强度;
      2. 渲染水表面。

代码如下,首先是申明两个Pass,分别绘制了海底和水面:

SubShader
{
    Tags {"Queue" = "Geometry" "RenderType" = "Geometry" } 
    pass{
        CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
        ENDCG
    } 
    
    Tags {"Queue" = "Transparent" "RenderType" = "Transparent" }
    pass{
        Blend SrcAlpha OneMinusSrcAlpha 
        CGPROGRAM
            #pragma vertex vertWater
            #pragma fragment fragWater
        ENDCG
    }    
}
v2f vertWater(a2v v){
    v2f o;
    float3 worldPos = mul(unity_ObjectToWorld, v.
    v.vertex.xyz = GetWaterWavePos(
    o.worldNormal = GetWaterWaveNormal(
    o.pos = UnityObjectToClipPos(v.
    o.uv = v.texcoord.
    return o;
}	    
	
fixed4 fragWater(v2f i) : SV_Target{ 
    fixed3 worldNormal = normalize(i.worldNormal);
    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
    fixed3 lambert = 0.5 * max(0, dot(worldNormal, worldLightDir)) + 0.5;
    fixed3 diffuse = lambert * _WaterColor.rgb * _LightColor0.xyz;
    return fixed4(diffuse * _WaterColor.rgb, _WaterColor.a);
}   
	
v2f vert(a2v v){
    v2f o;
    float3 worldPos = mul(unity_ObjectToWorld, v.vertex);  
    v.vertex.xyz = v.vertex.xyz - fixed3(0,0,_PlaneDistance);
    o.worldNormal = fixed3(0,1,0);
    o.worldPos = worldPos;
    o.pos = UnityObjectToClipPos(v.vertex);    
    o.uv = v.texcoord.xy;   
    return o;
}	    
	
fixed4 frag(v2f i) : SV_Target{ 
    fixed3 intercept = CaculateIntercept(i.worldPos);
    fixed3 caustic = tex2D(_SunLight, intercept.xy * _Caustic).r;
    fixed3 worldNormal = normalize(i.worldNormal);
    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
    fixed3 lambert = 0.5 * max(0, dot(worldNormal, worldLightDir)) + 0.5;
    fixed3 diffuse = lambert * _PlaneColor.rgb * _LightColor0.xyz + caustic * _CausticColor.rgb;
    return fixed4(diffuse, 1);
}   

计算水面波,这里我叠加了4个正弦波:

float3 GetWaterWavePos(float3 worldPos)
{
    float3 disPos = float3(0,0,0);
    disPos += CaculatePos(150, 0.002, 1, _Time.x * 10, fixed2(0.5,-1), worldPos,2); 
    disPos += CaculatePos(200,0.001, 1, _Time.x * 60, fixed2(4,-0.5), worldPos,4);  
    disPos += CaculatePos(300, 0.001, 1, _Time.x * 20, fixed2(1,-1), worldPos,5);
    disPos += CaculatePos(90, 0.001, 1, _Time.x * 100, fixed2(1.5,1), worldPos,1);
    return mul(unity_WorldToObject, float4(worldPos + disPos, 1));
}

float3 CaculatePos(fixed A, fixed omiga, fixed fai, fixed t, fixed2 dir, float3 p, int k)
{
    dir = normalize(dir);
    fixed height =  sin( omiga * (p.x * dir.x + p.z * dir.y) + t * fai);
    float3 pos = float3(0, A * pow((height+1)/2, k) * 2 ,0);
    return pos;
}

计算法线:

fixed3 GetWaterWaveNormal(float3 worldPos)
{
    fixed3 n = fixed3(0,0,0);
    n += CaculateNormal(150, 0.002, 1, _Time.x * 10, fixed2(0.5,-1), worldPos,2); 
    n += CaculateNormal(200, 0.001, 1, _Time.x * 60, fixed2(4,-0.5), worldPos,4);
    n += CaculateNormal(300, 0.001, 1, _Time.x * 20, fixed2(1,-1), worldPos,5);
    n += CaculateNormal(90, 0.001, 1, _Time.x * 100, fixed2(1.5,1), worldPos,1);
    return fixed3(n.x,1,n.z); 
}

fixed3 CaculateNormal(fixed A, fixed omiga, fixed fai, fixed t, fixed2 dir, float3 p, int k)
{
    dir = normalize(dir);
    fixed S = sin(omiga * (p.x*dir.x + p.z*dir.y) + t * fai);
    fixed C = cos(omiga * (p.x*dir.x + p.z*dir.y) + t * fai);
    fixed WA = k * omiga * A;
    fixed x = dir.x * WA * pow((S+1)/2, k - 1) * C;
    fixed y = 1;
    fixed z = dir.y * WA * pow((S+1)/2, k - 1) * C;
    fixed3 normal = fixed3(x, y, z);
    return normal;
}

计算"太阳贴图"的采样坐标:

fixed3 CaculateIntercept(float3 worldPos)
{
    fixed3 pos = GetWaterWavePos(worldPos);
    fixed3 worldNormal = GetWaterWaveNormal(worldPos);
    return pos + worldNormal * (pos.z + _PlaneDistance);
}

Unity实现效果:

 

三. 一些计算细节

a. 斯涅尔定律(Snell)

斯涅尔定律:当光波从一种介质传播到另一种具有不同折射率的介质时,会发生折射现象,入射角与折射角之间满足关系:

 \eta_1sin\theta _1 = \eta_2sin\theta _2

\eta_1 和 \eta_2 分别是两个介质的折射率,\theta _1 和 \theta _2 分别是入射角和折射角,是入射光和折射光与平面法线的夹角。水的折射率为1.33,空气的折射率近似等于1.00001。

不涉及角度计算的话,Foley等人在1996年提出,假如入射线,折射线,表面法线是共面的,那么有:

                                              T = N\left ( \frac{\eta _1}{\eta _2} (E\cdot N)\pm \sqrt{1+\left ( \frac{\eta _1}{\eta _2} \right )^{2}\left ( \left ( E\cdot N \right )^{2}-1 \right )}\right ) + \frac{\eta _1}{\eta _2}E

T : 折射线       N : 表面法线       E : 入射线         \eta _1,\eta _2 : 两个介质的折射率


b. 正弦波及法线计算

水面的波函数如下(书里的那个没看懂,所以这里替换为第一章中正弦波表现水波的方法):

                                              \dpi{100} \large H(x,z,t) =\sum( A_i\times sin(D_i\cdot (x,z)\times \omega_i + t\times \varphi_i ))
顶点法线:
                                                        N(x,z) = (-\frac{\partial }{\partial x }(H(x,z,t)),1,-\frac{\partial }{\partial z }(H(x,z,t)))


c. 纹理的采样坐标计算

使用水面顶点法线方向水面顶点与水底的距离做为采样坐标:

                                                                   intercept = p_1 + d \cdot n_1

猜你喜欢

转载自blog.csdn.net/LeeXxs/article/details/87979215