【Unity Shader实例】 水体WaterEffect(三) 环境映射

Unity Shader实现水体的环境映射

平静的水面会像镜子一样映射出周围的景致(光的反射)。此外,水这种透明材质,应该可以透视,我们的眼睛可以透过水面看到水下的环境(光的折射)。

效果展示

这里写图片描述

实现

在图形学中,立方体纹理(CubeMap)是环境映射(Environment Mapping)的一种实现方法。环境映射可以模拟物体周围IDE环境,而使用了环境映射的物体可以看起来像镀了层膜一样反射出周围的环境。

我们接下来要解决的是,如何让水面反射周围环境?如何让水面折射水下环境?能量是守恒的,如何计算多少光发生了反射,多少光发生了折射?

1. 水面的环境反射

原理

环境反射的原理很简单,一个光滑的物体表面可以根据我们观察的不同角度反射出不同位置的环境。即物体表面一点反射的颜色和该点的法线,观察视线和反射视线有关系。
image

如果我们使用texCube函数对立方体纹理进行采样时,使用视线关于物体顶点法线的反射向量作为采样的方向向量就可以得到反射的效果。

具体实现

步骤:
  • 创建用于环境映射的立方体纹理(反射源)
  • 用该立方体纹理作为天空盒子
  • 使用worldRef(视线关于物体顶点法线的反射向量)作为采样的方向向量,对CubeMap采样作为物体的表面纹理

如果物体的纹理采集自与天空盒子同一个CubeMap,就会有一种环境映射的感觉。

shader代码:
Shader "Hidden/CMReflect"
{
    Properties
    {
        _CubeMap("CubeMap", CUBE) = ""{}
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;           
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldRef : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
                float3 worldViewDir : TEXCOORD3;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.worldNormal = mul(unity_ObjectToWorld, v.normal);
                o.worldViewDir =  normalize(_WorldSpaceCameraPos.xyz - o.worldPos.xyz);
                o.worldRef = reflect(-o.worldViewDir,normalize(o.worldNormal));
                return o;
            }

            samplerCUBE _CubeMap;
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = texCUBE(_CubeMap, i.worldRef);
                return col;
            }
            ENDCG
        }
    }
}
效果展示

这里写图片描述

2. 水面的环境折射

原理

透明材质,如水,玻璃,水晶,应该可以透视。这种环境的透视我们可以在对CubeMap采样时,使用视线关于物体法线的反射向量作为方向向量,就得到了环境透射的效果。

texCube(_CubeMap, refract(viewDir,normal,_RefractRatio));

_RefractRatio是折射相对系数,与物体的介质有关。
image

具体实现

shader代码
Shader "Hidden/CMRefract"
{
    Properties
    {
        _CubeMap("CubeMap", CUBE) = ""{}
        _RefractRatio("Refract Ratio", Float) = 0.5
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;               
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldRef : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
                float3 worldViewDir : TEXCOORD3;
                float4 vertexLocal : TEXCOORD4;
            };

            float _RefractRatio;
            v2f vert (appdata v)
            {
                v2f o;
                o.vertexLocal = v.vertex;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.worldNormal = mul(unity_ObjectToWorld, v.normal);
                o.worldViewDir =  normalize(_WorldSpaceCameraPos.xyz - o.worldPos.xyz);
                o.worldRef = refract(-o.worldViewDir,normalize(o.worldNormal),_RefractRatio);
                return o;
            }

            samplerCUBE _CubeMap;
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = texCUBE(_CubeMap, normalize(i.worldRef));
                return col;
            }
            ENDCG
        }
    }
}
效果展示

这里写图片描述

3.水面的菲涅尔反射

原理

image
水的近处较能看到水底的石头,远处渐变成对天空的反射,这就是菲涅尔反射效果的体现。

菲尼尔现象是指光到达材质交界处时,一部分被反射,一部分被折射,即视线垂直于表面时,反射较弱,而当视线非垂直表面时,夹角越小,反射越明显。所有物体都有菲尼尔反射,只是强大大小不同。因此,菲尼尔反射是为了模拟真实世界中的这种光学现象。多少光发生反射,多少光发生折射,可以用菲涅尔公式进行计算。

实际世界的菲涅尔公式非常复杂,我们同样用一些近似公式来计算,如下面提到的Schlick菲涅尔近似公式和Empricial菲涅尔近似公式:

Schlick菲涅尔近似等式

//resnelBias 菲尼尔偏移系数  
//fresnelScale 菲尼尔缩放系数  
//fresnelPower 菲尼尔指数  
reflectFact = fresnelBias + fresnelScale*pow(1-dot(viewDir,N)),fresnelPower);  //系数:多少光发生折射多少光发生反射

Empricial菲涅尔近似等式:

//F0是反射系数用于控制菲涅尔的强度
reflectFact = F0 + (1-F0)*pow(1-dot(viewDir,N),5);

image
有了反射系数,我们就可以根据能量守恒计算反射光线和折射光线的强度了。

c(reflect) = reflect(viewDir,N)
c(refract) = reflect(viewDir,N,eatRadio)
C(fresnelFinal) = reflectFact * C(reflect) + (1-reflectFact)*C(refract);  //能量守恒

具体实现

shader代码
Shader "CM/Fresnel_Schlick"
{
    Properties
    {
        _CubeMap("CubeMap", CUBE) = ""{}
        _RefractRatio("Refract Ratio", Float) = 0.5
        _FresnelScale("Fresnel Scale", Float) = 0.5
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;               
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldReflect : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
                float3 worldViewDir : TEXCOORD3;
                float4 vertexLocal : TEXCOORD4;
                float3 worldRefract : TEXCOORD5;
            };

            float _RefractRatio;
            v2f vert (appdata v)
            {
                v2f o;
                o.vertexLocal = v.vertex;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.worldNormal = mul(unity_ObjectToWorld, v.normal);
                o.worldViewDir =  normalize(_WorldSpaceCameraPos.xyz - o.worldPos.xyz);
                o.worldReflect = reflect(-o.worldViewDir,normalize(o.worldNormal));
                o.worldRefract = refract(-o.worldViewDir,normalize(o.worldNormal),_RefractRatio);
                return o;
            }

            float _FresnelScale;
            samplerCUBE _CubeMap;
            fixed4 frag (v2f i) : SV_Target
            {
                float4 fresnelReflectFactor = _FresnelScale + (1 - _FresnelScale)*pow(1-dot(i.worldViewDir,i.worldNormal), 5);
                fixed4 colReflect = texCUBE(_CubeMap, normalize(i.worldReflect));
                fixed4 colRefract = texCUBE(_CubeMap, normalize(i.worldRefract));
                fixed4 col = fresnelReflectFactor * colReflect + (1-fresnelReflectFactor) * colRefract;
                return col;
            }
            ENDCG
        }
    }
}
效果展示

菲涅尔效果
这里写图片描述

总结

我们实现了水体的环境映射和反射以及菲涅尔反射效应。至此,我们的水体有了一点点真实感,但这还远远不够。

首先,它虽然实现了对周围环境的反射和折射,但它是基于效率考虑的一种“假”反射,是基于天空盒的cubemap纹理采样模拟出来的,只是“模拟”环境反射,并不是“真”反射。如果环境中有其他物体,其他物体并不会被映射出来,只能反射天空盒子;且如果我们改变了环境的天空盒子,物体的反射的纹理并不会跟着改变。

关于环境映射的改进,我们会在后续的文章中继续讨论。

附:配套Unity3d工程下载地址

猜你喜欢

转载自blog.csdn.net/v_xchen_v/article/details/79692726