Unity3D - Shader - 凹凸映射(Bump mapping)

凹凸映射的目的:是使用纹理来修改模型表面的法线,用于为模型提供更多细节。

凹凸映射的方式

  • 高度纹理(height map):用于模拟表面位移(displacement),也称作高度映射(height mapping)
  • 法线纹理(normal map):用于直接存储表面法线,又称为法线映射(normal mapping)

高度纹理

高度图中存储的是强度值(intensity),用于表示局部海拔高度。

颜色越浅表明该位置的表面越向外凸起,颜色越深表明该位置越向里凹。

有点是直观,缺点是计算复杂,无法实时得到表面法线。

法线纹理

法线纹理中存储的是表面法线的方向。

需要使用如下公式将,法线方向的分量[-1,1]映射到像素分量[0,1]:

pixel=normal+12

当在Shader中对法线纹理进行采样后,需要对结果使用公式进行一次反映射,用于得到原先的法线方向:

normal=pixel21

方向是相对于坐标空间来说的,因此法线纹理依据不同的坐标空间,还分为:

  • 模型空间的法线纹理(object-space normal map):直接将模型空间中的表面法线存储在一张纹理中。
  • 切线空间的法线纹理(tangent-space normal map):模型顶点的切线空间来存储法线。每个模型顶点都有自己的切线空间,其原点是顶点本身,z轴就是法线方向,x轴就是顶点的切线方向,y轴就是法线和切线叉积所得,也就是副切线

模型空间的法线纹理

将修改后的模型空间中表面法线存储在一张纹理中。

切线空间的法线纹理

在实际制作中使用,模型顶点的切线空间(tangent space)来存储发现。对于模型的每个顶点,它都有一个属于自己的切线空间,这个切线空间的原点就是该顶点本身,而Z轴是顶点的发现方向(n),X轴是顶点的切线方向(t),而Y轴是法线和切线的叉积,称为副切线(bitangent,b)或副法线。

这里写图片描述

这里写图片描述

切线空间的优点:

  • 自由度高,模型空间下的法线纹理记录的是绝对法线信息,仅可以用于创建它的那个模型。而切线空间下的法线纹理记录的是相对法线信息。
  • 可进行UV动画。可以移动一个纹理的UV坐标来实现凹凸移动的效果。UV动画在水或者火山熔岩这种类型的物体长经常用到。
  • 可重用法线纹理。
  • 可压缩。

实现

在计算光照模型时,需要统一各个方向矢量所在的坐标空间。由于法线纹理中存储的法线是切线空间下的方向,因此通常有两个选择:

  • 切线空间下进行光照计算,需要把光照方向、视角方向变换到切线空间下
  • 世界空间下进行光照计算,需要把采样得到的法线方向变换到世界空间下,再和世界空间下的光照方向和视角方向进行计算。

优缺点:

  • 性能上切线空间优于世界空间
  • 通用性上世界空间优于切线空间

切线空间

基本思路是:在片元着色器中通过纹理采样得到切线空间下的法线,然后再与切线空间下的视角方向、光照方向等进行计算,得到最终的计算结果。

需要先在顶点着色器中把视角方向和光照方向从模型空间变换到切线空间,即需要知道从模型空间到切线空间的变换矩阵。

从模型空间到切线空间的变换矩阵就是从切线空间到模型空间的变换矩阵的逆矩阵,把切线(x轴)、副切线(y轴)、法线(z轴)按顺序排列即可以得到。

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Unity Shaders Book/Chapter 7/Normal Map Tangent Space" {

    Properties {
        _Color ("Color Tint", Color) = (1, 1, 1, 1) 
        _MainTex("Main Tex", 2D ) = "white" {}
        // 法线纹理
        // bump是Unity内置的法线纹理
        _BumpMap("Normap Map", 2D) = "bump" {}
        // 用于控制凹凸程度,当它为0时,意味着该法线纹理不会对
        // 光照产生任何影响
        _BumpScale("Bump Scale", Float) = 1.0

        _Specular("Specular", Color) = (1, 1, 1, 1)
        _Gloss("Gloss", Range(8.0, 256)) = 20
    }

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

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            // 与Properties语义块中的属性建立联系
            fixed4 _Color;
            sampler2D _MainTex;
            // 得到MainTex纹理属性的平铺和偏移系数
            float4 _MainTex_ST;
            sampler2D _BumpMap;
            // 得到BumpMap纹理属性的平铺和偏移系数
            float4 _BumpMap_ST;
            float _BumpScale;
            fixed4 _Specular;
            float _Gloss;

            struct a2v {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                // 顶点的切线方向
                // 需要使用tangent.w分量来决定切线空间中的第三个坐标轴-副切线的方向性
                float4 tangent : TANGENT;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float4 uv : TEXCOORD0;
                // 光照方向
                float3 lightDir : TEXCOORD1;
                // 视角方向
                float3 viewDir : TEXCOORD2;
            };

            v2f vert(a2v v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);

                // xy分量存储了_MainTex的纹理坐标
                o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                // zw分量存储了_BumpMap的纹理坐标
                o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

                // 将模型空间下的切线方向、副切线方向和法线方向按行排列得到从模型空间到
                // 切线空间的变换矩阵rotation。

                // 之所以和w分量相乘,是为了决定使用哪一个方向
                // float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w; 
                // float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )
                TANGENT_SPACE_ROTATION;

                // Transform the light direction from object space to tangent space
                // ObjSpaceLightDir用于得到模型空间下的光照方向
                o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
                // ObjSpaceViewDir用于得到模型空间下的视角方向
                o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;

                return o;
            }

            // 采样得到切线空间下的法线方向,再在切线空间下进行光照计算即可
            fixed4 frag(v2f i) : SV_Target {
                fixed3 tangentLightDir = normalize(i.lightDir);
                fixed3 tangentViewDir = normalize(i.viewDir);

                // Get the texel in the normal map
                // 法线纹理中存储的是经过映射后得到的像素值
                fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
                fixed3 tangentNormal;

                // If the texture is not marked as "Normal map"
                // tangentNormal.xy = (packedNormal.xy * 2 - 1 ) * _BumpScale;
                // tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));

                //  Or mark the texture as "Normal map", and use the built-in funciton
                tangentNormal = UnpackNormal(packedNormal);
                tangentNormal.xy *= _BumpScale;
                tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));

                fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));

                fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);

                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal,halfDir)), _Gloss);

                return fixed4(ambient + diffuse + specular, 1.0);
            }

            ENDCG
        }
    }
    Fallback "Specular"
}

世界空间

基本思路:在顶点着色器中计算从切线空间到世界空间的变换矩阵,并传递给片元着色器。变换矩阵的计算可以由顶点的切线、副切线和法线在世界空间的表示来得到。

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

Shader "Unity Shaders Book/Chapter 7/Normal Map World Space" {
    Properties {
        _Color ("Color Tint", Color) = (1, 1, 1, 1) 
        _MainTex("Main Tex", 2D ) = "white" {}
        // 法线纹理
        // bump是Unity内置的法线纹理
        _BumpMap("Normap Map", 2D) = "bump" {}
        // 用于控制凹凸程度,当它为0时,意味着该法线纹理不会对
        // 光照产生任何影响
        _BumpScale("Bump Scale", Float) = 1.0

        _Specular("Specular", Color) = (1, 1, 1, 1)
        _Gloss("Gloss", Range(8.0, 256)) = 20
    }

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

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            // 与Properties语义块中的属性建立联系
            fixed4 _Color;
            sampler2D _MainTex;
            // 得到MainTex纹理属性的平铺和偏移系数
            float4 _MainTex_ST;
            sampler2D _BumpMap;
            // 得到BumpMap纹理属性的平铺和偏移系数
            float4 _BumpMap_ST;
            float _BumpScale;
            fixed4 _Specular;
            float _Gloss;

            struct a2v {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                // 顶点的切线方向
                // 需要使用tangent.w分量来决定切线空间中的第三个坐标轴-副切线的方向性
                float4 tangent : TANGENT;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float4 uv : TEXCOORD0;
                float4 TtoW0 : TEXCOORD1;
                float4 TtoW1 : TEXCOORD2;
                float4 TtoW2 : TEXCOORD3;
            };

            v2f vert(a2v v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);

                // xy分量存储了_MainTex的纹理坐标
                o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                // zw分量存储了_BumpMap的纹理坐标
                o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

                // 世界空间下位置                 
                float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                // 世界空间法线
                fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
                // 世界空间切线
                fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
                // 世界空间副切线
                fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;

                // Compute the matrix that transform directions from tangent space to world space
                // Put the world postion in w component for optimizeation
                o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x); 
                o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y); 
                o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z); 

                return o;
            }

            fixed4 frag(v2f i) : SV_Target {

                // Get the position in world space
                // 构建世界空间下的坐标
                float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);

                // Compute the light and view dir in world space
                // 世界空间的光照
                fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
                // 世界空间的视角
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));

                // Get the normal in tangent space
                // 使用UnpackNormal对法线纹理进行采样和解码(需要把法线纹理的格式识别为Normal map)
                // 并使用_BumpScale进行缩放。
                fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
                bump.xy *= _BumpScale;
                bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));

                // Transform the normal from tangent space to world space
                // 使用TtoW0/TtoW1/TtoW2存储的变换矩阵把法线变换到世界空间下。
                bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));

                fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));

                fixed3 halfDir = normalize(lightDir + viewDir);

                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss);

                return fixed4(ambient + diffuse + specular, 1.0);
            }

            ENDCG
        }
    }
    Fallback "Specular"
}
  • 2
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值