5

【Unity3D】反射和折射 - little_fat_sheep

 1 year ago
source link: https://www.cnblogs.com/zhyan8/p/17615592.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

【Unity3D】反射和折射

立方体纹理(Cubemap)和天空盒子(Skybox)中介绍了生成立方体纹理和制作天空盒子的方法,本文将使用立方体纹理进行采样,实现反射、菲涅耳反射和折射效果。另外,本文还使用了 GrabPass 抓取屏幕图像,替代立方体纹理,作为折射的采样纹理。

​ 立方体纹理采样原理:从世界坐标系的坐标原点出发,发射一条射线,与边长为 1 的立方体相交(其中心在坐标原点,并且每个面与对应坐标轴垂直),交点位置的像素即为采样的像素。立方体纹理采样函数如下,cubemap 为立方体纹理,worldVec 为世界坐标系中采样方向向量,color 为采样的颜色。

// 立方体纹理采样
fixed4 color = texCUBE(cubemap, worldVec)

​ 本文完整资源见→Unity3D反射和折射

​ 对于模型上的任意一点,其反射颜色计算方法为:首先计算相机指向该点的向量,再根据该点的法线信息计算反射向量,接着使用反射向量在 cubemap 中进行纹理采样,最后使用采样后的颜色和漫反射颜色进行混合。

​ Reflect.shader

Shader "MyShader/Reflection" { // 反射
    Properties {
        _Color("Color Tint", Color) = (1, 1, 1, 1) // 物体颜色
        _ReflectColor("Reflection Color", Color) = (1, 1, 1, 1) // 反射光的颜色
        _ReflectAmount("Reflect Amount", Range(0, 1)) = 1 // 反射比例(用于漫反射和反射之间插值)
        _Cubemap("Reflection Cubemap", Cube) = "_Skybox" {} // 立方体纹理
    }

    SubShader{
        Tags { "RenderType" = "Opaque" "Queue" = "Geometry"}

        Pass {
            Tags { "LightMode" = "ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            fixed4 _Color; // 物体颜色
            fixed4 _ReflectColor; // 反射光的颜色
            fixed _ReflectAmount; // 反射比例(用于漫反射和反射之间插值)
            samplerCUBE _Cubemap; // 立方体纹理

            struct a2v {
                float4 vertex : POSITION; // 模型空间顶点坐标
                float3 normal : NORMAL; // 模型空间法相向量
            };

            struct v2f {
                float4 pos : SV_POSITION; // 裁剪空间顶点坐标
                float3 worldPos : TEXCOORD0; // 世界空间顶点坐标
                fixed3 worldNormal : TEXCOORD1; // 顶点法线向量
                fixed3 worldViewDir : TEXCOORD2; // 观察向量(顶点指向相机)
                fixed3 worldRefl : TEXCOORD3; // 反射向量
            };

            v2f vert(a2v v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex); // 模型空间顶点坐标变换到裁剪空间, 等价于: mul(UNITY_MATRIX_MVP, v.vertex)
                o.worldNormal = UnityObjectToWorldNormal(v.normal); // 计算世界空间中顶点法线向量(已归一化)
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 计算世界空间中顶点坐标
                o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos); // 计算世界空间中观察向量(顶点指向相机)
                o.worldRefl = reflect(-o.worldViewDir, o.worldNormal); // 计算观察向量的反射向量
                return o;
            }

            fixed4 frag(v2f i) : SV_Target {
                fixed3 worldNormal = normalize(i.worldNormal); // 法线向量
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); // 灯光向量(顶点指向光源)
                fixed3 worldViewDir = normalize(i.worldViewDir); // 观察向量(顶点指向相机)
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; // 环境光颜色
                fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir)); // 漫反射光颜色
                fixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb * _ReflectColor.rgb; // 反射光颜色
                fixed3 color = ambient + lerp(diffuse, reflection, _ReflectAmount); // 漫反射光与反射光颜色进行插值
                return fixed4(color, 1.0);
            }

            ENDCG
        }
    }

    FallBack "Reflective/VertexLit"
}

​ _ReflectAmount 值为 1 反射效果如下:

img

​ _ReflectAmount 值由 0 至 1 渐变反射效果如下:

img

​ 说明:即使没有房间模型,中间的 5 个物体也会反射房间环境,这是因为它们使用的纹理源于 Cubemap 采样,而 Cubemap 一旦生成,就与环境无关。

3 菲涅耳反射

​ 菲涅耳反射描述了一种光学现象,即当光线射到物体表面时,一部分发生反射,一部分进入物体内部,发生折射或散射。被反射的光和入射角度存在一定的比率关系(入射角越小,反射的光越少;入射角越大,反射的光越多;当入射角大到某个值时,会发生全反射,即没有折射现象),这个比值可以通过菲涅耳等式计算得到。当前应用比较广泛的菲涅耳近似等式主要有:Schlick 菲涅耳近似等式、Empricial 菲涅耳近似等式。

1)Schlick 菲涅耳近似等式

img

​ 说明:F0 为反射系数,用于控制菲涅耳反射的强度,用户可以根据物体材质特性进行设定,v、n 分别为入射向量和法线向量。

2)Empricial 菲涅耳近似等式

img

​ 说明:bias、scale、power 都是待定参数,用户可以根据物体材质特性进行设定,v、n 分别为入射向量和法线向量。

​ FresnelReflect.shader

Shader "MyShader/FresnelReflect" { // 菲涅耳反射
    Properties {
        _Color("Color Tint", Color) = (1, 1, 1, 1) // 物体颜色
        _FresnelScale("Fresnel Scale", Range(0, 1)) = 0.5 // 菲涅耳反射系数缩放值
        _Cubemap("Reflection Cubemap", Cube) = "_Skybox" {} // 立方体纹理
    }

    SubShader{
        Tags { "RenderType" = "Opaque" "Queue" = "Geometry"}

        Pass {
            Tags { "LightMode" = "ForwardBase" }

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            fixed4 _Color; // 物体颜色
            fixed _FresnelScale; // 菲涅耳反射系数缩放值
            samplerCUBE _Cubemap; // 立方体纹理

            struct a2v {
                float4 vertex : POSITION; // 模型空间顶点坐标
                float3 normal : NORMAL; // 模型空间法相向量
            };

            struct v2f {
                float4 pos : SV_POSITION; // 裁剪空间顶点坐标
                float3 worldPos : TEXCOORD0; // 世界空间顶点坐标
                fixed3 worldNormal : TEXCOORD1; // 顶点法线向量
                fixed3 worldViewDir : TEXCOORD2; // 观察向量(顶点指向相机)
                fixed3 worldRefl : TEXCOORD3; // 反射向量
            };

            v2f vert(a2v v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex); // 模型空间顶点坐标变换到裁剪空间, 等价于: mul(UNITY_MATRIX_MVP, v.vertex)
                o.worldNormal = UnityObjectToWorldNormal(v.normal); // 计算世界空间中顶点法线向量(已归一化)
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 计算世界空间中顶点坐标
                o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos); // 计算世界空间中观察向量(顶点指向相机)
                o.worldRefl = reflect(-o.worldViewDir, o.worldNormal); // 计算观察向量的反射向量
                return o;
            }

            fixed4 frag(v2f i) : SV_Target {
                fixed3 worldNormal = normalize(i.worldNormal); // 法线向量
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); // 灯光向量(顶点指向光源)
                fixed3 worldViewDir = normalize(i.worldViewDir); // 观察向量(顶点指向相机)
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; // 环境光颜色
                fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir)); // 漫反射光颜色
                fixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb; // 反射光颜色
                fixed fresnel = _FresnelScale + (1 - _FresnelScale) * pow(1 - dot(worldViewDir, worldNormal), 5); // 菲涅耳反射系数
                fixed3 color = ambient + lerp(diffuse, reflection, saturate(fresnel)); // 漫反射光与反射光颜色进行插值
                return fixed4(color, 1.0);
            }

            ENDCG
        }
    }

    FallBack "Reflective/VertexLit"
}

​ 菲涅耳反射效果如下:

img

​ 说明:即使没有房间模型,中间的 5 个物体也会反射房间环境,这是因为它们使用的纹理源于 Cubemap 采样,而 Cubemap 一旦生成,就与环境无关。

​ 对于模型上的任意一点,其折射颜色计算方法为(仅考虑 1 次折射,现实世界会发生 2 次折射):首先计算相机指向该点的向量,再根据该点的法线信息和折射率比值计算折射向量,接着使用折射向量在 cubemap 中进行纹理采样,最后使用采样后的颜色和漫反射颜色进行混合。

​ Refract.shader

Shader "MyShader/Refraction" { // 折射
    Properties {
        _Color("Color Tint", Color) = (1, 1, 1, 1) // 物体颜色
        _RefractColor("Refraction Color", Color) = (1, 1, 1, 1) // 折射光的颜色
        _RefractAmount("Refraction Amount", Range(0, 1)) = 1 // 折射比例(用于漫反射和折射之间插值)
        _RefractRatio("Refraction Ratio", Range(0.1, 1)) = 0.5 // 折射比(入射介质折射率/折射介质折射率)
        _Cubemap("Refraction Cubemap", Cube) = "_Skybox" {} // 立方体纹理
    }

    SubShader{
        Tags { "RenderType" = "Opaque" "Queue" = "Geometry"}

        Pass {
            Tags { "LightMode" = "ForwardBase" }

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            fixed4 _Color; // 物体颜色
            fixed4 _RefractColor; // 折射光的颜色
            float _RefractAmount; // 折射比例(用于漫反射和折射之间插值)
            fixed _RefractRatio; // 折射比(入射介质折射率/折射介质折射率)
            samplerCUBE _Cubemap; // 立方体纹理

            struct a2v {
                float4 vertex : POSITION; // 模型空间顶点坐标
                float3 normal : NORMAL; // 模型空间法相向量
            };

            struct v2f {
                float4 pos : SV_POSITION; // 裁剪空间顶点坐标
                float3 worldPos : TEXCOORD0; // 世界空间顶点坐标
                fixed3 worldNormal : TEXCOORD1; // 顶点法线向量
                fixed3 worldViewDir : TEXCOORD2; // 观察向量(顶点指向相机)
                fixed3 worldRefr : TEXCOORD3; // 折射向量
            };

            v2f vert(a2v v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex); // 模型空间顶点坐标变换到裁剪空间, 等价于: mul(UNITY_MATRIX_MVP, v.vertex)
                o.worldNormal = UnityObjectToWorldNormal(v.normal); // 计算世界空间中顶点法线向量(已归一化)
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 计算世界空间中顶点坐标
                o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos); // 计算世界空间中观察向量(顶点指向相机)
                o.worldRefr = refract(-normalize(o.worldViewDir), o.worldNormal, _RefractRatio); // 计算观察向量的折射向量
                return o;
            }

            fixed4 frag(v2f i) : SV_Target {
                fixed3 worldNormal = normalize(i.worldNormal); // 法线向量
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); // 灯光向量(顶点指向光源)
                fixed3 worldViewDir = normalize(i.worldViewDir); // 观察向量(顶点指向相机)
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; // 环境光颜色
                fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir)); // 漫反射光颜色
                fixed3 refraction = texCUBE(_Cubemap, i.worldRefr).rgb * _RefractColor.rgb; // 折射光颜色
                fixed3 color = ambient + lerp(diffuse, refraction, _RefractAmount); // 漫反射光与折射光颜色进行插值
                return fixed4(color, 1.0);
            }

            ENDCG
        }
    }

    FallBack "Reflective/VertexLit"
}

​ _RefractAmount 值为 1 折射效果如下:

img

​ _RefractAmount 值由 0 至 1 渐变折射效果如下:

img

​ 说明:现实世界中,光线从空气射入半透明物体,再射出到空气中,会发生 2 次折射,但是 Unity Shader 是逐像素渲染,每个像素都是独立渲染的(便于 GPU 并行计算,提高渲染效率),光线在物体出射点发生折射时,无法获取到入射点的位置及法线信息,因此无法模拟二次折射效果。

5 基于 GrabPass 的折射

​ 第 4 节中基于 Cubemap 采样实现折射特效,本节基于 GrabPass 屏幕采样实现折射特效。

​ GrabPass 用于获取屏幕纹理,有以下两种形式:

// 1. 后续的Pass中通过_GrabTexture访问屏幕图像, 该方式较耗性能, Unity为每个使用了GrabPass的物体进行一次抓取屏幕图像操作
GrabPass {}
// 2. 后续的Pass中通过TextureName访问屏幕图像, 该方式性能较好, Unity只会在每一帧为第一个使用TextureName纹理的物体执行一次抓取屏幕图像操作, 这个纹理也可以在其他Pass中被访问
GrabPass { "TextureName" }

​ GrabPass 通常用于渲染透明物体,尽管代码里并不包含混合指令,但我们仍需要把物体的渲染队列设置为透明队列(即 "Queue"="Transparent")。这样才可以保证当渲染该物体时,所有不透明物体都已经被绘制在屏幕上,从而获取正确的屏幕图像。

​ GrabRefract.shader

Shader "MyShader/GrabRefract" { // 基于GrabPass纹理采样的折射
    Properties {
        _Color("Color Tint", Color) = (1, 1, 1, 1) // 物体颜色
        _RefractColor("Refraction Color", Color) = (1, 1, 1, 1) // 折射光的颜色
        _RefractAmount("Refraction Amount", Range(0, 1)) = 1 // 折射比例(用于漫反射和折射之间插值)
        _RefractRatio("Refraction Ratio", Range(0.1, 1)) = 0.5 // 折射比(入射介质折射率/折射介质折射率)
    }

    SubShader{
        // 渲染队列必须设置为 transparent, 确保所有不透明物体都在该对象之前已经渲染在屏幕上
        Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
        // 抓取屏幕图像并存储在_RefractionTex中, 作为折射采样的纹理
        GrabPass { "_RefractionTex" }

        Pass {
            Tags { "LightMode" = "ForwardBase" }

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            fixed4 _Color; // 物体颜色
            fixed4 _RefractColor; // 折射光的颜色
            float _RefractAmount; // 折射比例(用于漫反射和折射之间插值)
            fixed _RefractRatio; // 折射比(入射介质折射率/折射介质折射率)
            sampler2D _RefractionTex; // GrabPass抓取的屏幕图像, 作为折射采样的纹理

            struct a2v {
                float4 vertex : POSITION; // 模型空间顶点坐标
                float3 normal : NORMAL; // 模型空间法相向量
            };

            struct v2f {
                float4 pos : SV_POSITION; // 裁剪空间顶点坐标
                float4 scrPos : TEXCOORD0; // 屏幕空间顶点坐标(_RefractionTex采样的uv坐标)
                float3 worldPos : TEXCOORD1; // 世界空间顶点坐标
                fixed3 worldNormal : TEXCOORD2; // 顶点法线向量
                fixed3 worldViewDir : TEXCOORD3; // 观察向量(顶点指向相机)
                fixed3 worldRefr : TEXCOORD4; // 折射向量
            };

            v2f vert(a2v v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex); // 模型空间顶点坐标变换到裁剪空间, 等价于: mul(UNITY_MATRIX_MVP, v.vertex)
                o.scrPos = ComputeGrabScreenPos(o.pos); // 屏幕空间顶点坐标(_RefractionTex采样的uv坐标)
                o.worldNormal = UnityObjectToWorldNormal(v.normal); // 计算世界空间中顶点法线向量(已归一化)
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 计算世界空间中顶点坐标
                o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos); // 计算世界空间中观察向量(顶点指向相机)
                o.worldRefr = refract(-normalize(o.worldViewDir), o.worldNormal, _RefractRatio); // 计算观察向量的折射向量
                return o;
            }

            fixed4 frag(v2f i) : SV_Target {
                fixed3 worldNormal = normalize(i.worldNormal); // 法线向量
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); // 灯光向量(顶点指向光源)
                fixed3 worldViewDir = normalize(i.worldViewDir); // 观察向量(顶点指向相机)
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; // 环境光颜色 
                fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir)); // 漫反射光颜色
                float offset = 1 - dot(-worldViewDir, normalize(i.worldRefr)); // 折射偏移
                float2 cameraViewDir = normalize(mul(unity_MatrixV, float4(worldViewDir, 0)).xy); // 观察坐标系下观察向量坐标
                i.scrPos.xy = i.scrPos.xy + cameraViewDir * offset; // 顶点对应的偏移后的屏幕坐标(屏幕纹理采样坐标)
                fixed3 refraction = tex2D(_RefractionTex, i.scrPos.xy / i.scrPos.w).rgb; // 折射光颜色
                fixed3 color = ambient + lerp(diffuse, refraction, _RefractAmount); // 漫反射光与折射光颜色进行插值
                return fixed4(color, 1.0);
            }

            ENDCG
        }
    }

    FallBack "Reflective/VertexLit"
}

​ _RefractAmount 值为 1 折射效果如下:

img

​ _RefractAmount 值由 0 至 1 渐变折射效果如下:

img

​ 声明:本文转自【Unity3D】反射和折射


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK