【unity shader】法线/凹凸贴图基础

当我们需要给材质增加凹凸细节的时候,需要使用到凹凸或者法线贴图。

1. 基于高度图的凹凸映射

以下是一张高度图。

一张高度图
在这里插入图片描述

1.1. 采样高度图的数值作为法线

显然直接把高度图作为basecolor输出,无法起到体现凹凸的效果。
我们需要把读取到的高度图的信息,作为法线数据。

fixed4 col = tex2D(_HeightMap, i.uv);
float3 worldNormal = normalize(float3(0.0 ,col.r , 0.0));

在这里插入图片描述
这是由于标准化会将所有大于0的值全部归一化为1,所以除了贴图上为纯黑的区域外(物理意义上说,即山谷中的山谷,地板中的地板,马里亚纳中的马里亚纳级的深沟),其他部分会全部被标准化为(0, 1,0),即被标准化后,留下来的只由地板级高度的部分。
所以接下来我们在计算法线的时候,不单单只考虑当前的采样点,而把下一个偏移后的采样点的数据也考虑进来。

1.2. 基于高度图趋势的法线采样

即考虑的是单位偏移内高度变化的趋势。
在这里插入图片描述

sampler2D _HeightMap;
//..._TexelSize:返回一个四位向量,前两位自动给出当前导入纹理的uv方向单位偏移量,后两位对应纹理的尺寸
// 256×128 纹理的返回值: (0.00390625, 0.0078125, 256, 128).
float4 _HeightMap_TexelSize;
fixed4 col = tex2D(_HeightMap, i.uv);
float2 delta = float2(_HeightMap_TexelSize.x, 0);
float h1 = tex2D(_HeightMap, i.uv);
float h2 = tex2D(_HeightMap, i.uv + delta);
float3 worldNormal = normalize(float3(1, (h2 - h1)/delta.x, 0));

在这里插入图片描述
当然,我们不喜欢除法,因为这往往会让精度很不可控,所以我们要微调标准化的一步。

float3 worldNormal = normalize(float3(delta.x, (h2 - h1), 0));

另我们可以通过设置一个缩放系数来控制法线的缩放程度(凹凸程度),让整体沟壑效果看起来没这么陡峭。

float3 worldNormal = normalize(float3(_sampleDelta, (h2 - h1), 0));

在这里插入图片描述
注意看这边出现了明显的光照偏移的情况,我们稍一排查就能发现,随着缩放系数的调整,标准化后的法线出现了明显的偏移。
在这里插入图片描述
所以我们要把法线方向沿y轴做一个顺时针90度的旋转。当然实际上单纯旋转并不能解决,我们使用切线空间下的高度图所带来的,法线方向会偏移的痛点,后续依然得通过切线空间转换来解决问题。

float3 worldNormal = normalize(float3((h1 - h2), _sampleDelta, 0));

在这里插入图片描述
接下来同时使用uv方向的采样偏移,并通过两个切线的叉乘来求解法线。

fixed4 col = tex2D(_HeightMap, i.uv);

float2 Hdelta = float2(_HeightMap_TexelSize.x * 0.5, 0);
float u1 = tex2D(_HeightMap, i.uv - Hdelta);
float u2 = tex2D(_HeightMap, i.uv + Hdelta);
float3 du = float3(_sampleDelta, (u2 - u1), 0);

float2 Vdelta = float2(0, _HeightMap_TexelSize.y * 0.5);
float v1 = tex2D(_HeightMap, i.uv - Vdelta);
float v2 = tex2D(_HeightMap, i.uv + Vdelta);
float3 dv = float3(0, (v2 - v1), _sampleDelta);
float3 worldNormal = cross(dv, du);

在这里插入图片描述

2.基于法线贴图的凹凸映射

2.1.灰度图转换为法线贴图

对于同样一张高度图,我们调整一下导入设置,就可以让其原地转生成法线贴图。
在这里插入图片描述
采样后直接缩放。

float3 worldNormal = tex2D(_HeightMap, i.uv) * 2 - 1;

我们会发现采样出来的法线结果不正确(右边是基于高度图采样的正确结果)。
在这里插入图片描述
这是由于我们由高度图转换来的法线贴图,都会自动使用DXT5nm格式进行压缩。DXT5格式仅保留了法线的x,y部分(即对应场景内的x, z部分),且保存在a,g通道中。
在这里插入图片描述
我们调整采样方式。

worldNormal.xz = tex2D(_NormalMap, i.uv).wy * 2 - 1;        
worldNormal.y = sqrt(1 - dot(worldNormal.xz, worldNormal.xz));

在这里插入图片描述
加上_BumpScale作为参数控制xz值的缩放。
在这里插入图片描述
unity本身还提供了方便的方法可以直接调用,UnpackScaleNormal/UnpackNormal方法可以适应不同法线贴图压缩格式的差异,采样到正确的法线数值。

worldNormal = UnpackScaleNormal(tex2D(_NormalMap, i.uv), _BumpScale).xzy;

在这里插入图片描述
我们注意看这个函数的源码其中的条件判断部分:
在这里插入图片描述
我们需要增加shader target的声明,来使对应的判断生效

#pragma target 3.0 

2.2.法线贴图混合

这里我们额外使用一张细节贴图(灰度图)作为额外的法线贴图,使其与原有的贴图混合,进一步增加凹凸细节。
同样的,使用导入法线贴图的配置使之转换。
在这里插入图片描述

worldNormal = UnpackScaleNormal(tex2D(_NormalMap, i.uv.xy), _BumpScale).xzy;
detailNormal = UnpackScaleNormal(tex2D(_NormalMap, i.uv.zw), _DetailBumpScale).xzy;
worldNormal = normalize((worldNormal + detailNormal)/2);

我们采样后直接进行混合,却发现效果并不明显。
在这里插入图片描述
这是因为直接相加的话,原本平坦的部分会对其他部分产生影响,从而导致整体表现变平。
所以我们这里采用一个偏导数相加的方法,去消除平面部分带来的影响。

mainNormal = UnpackScaleNormal(tex2D(_NormalMap, i.uv.xy), _BumpScale);
detailNormal = UnpackScaleNormal(tex2D(_NormalMap, i.uv.zw), _DetailBumpScale);
worldNormal = float3(mainNormal.xy/mainNormal.z + detailNormal.xy/detailNormal.z, 1);
worldNormal = normalize(worldNormal.xzy);

在bumpscale为1时,细节上的差别比较明显。
在这里插入图片描述
当然除法还是容易带来问题,我们直接通过乘法消除分母,并取消x,y值对应的缩放系数,即

worldNormal = float3(mainNormal.xy + detailNormal.xy, mainNormal.z * detailNormal.z);
worldNormal = normalize(worldNormal.xzy);

实际上这就是unity自带的BlendNormals方法的源码,我们也可以使用BlendNormals来实现。

3.切线空间

3.1. 切线空间可视化

很多人都知道,实际上模型除了法线信息外,还记录了每个顶点的切线信息。
接下来用一个脚本来让模型的顶点法线,切线,以及叉乘得到的副切线可视化。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TangentSpaceVisualizer : MonoBehaviour
{
    
    
    public float normalScale = 1;
    // Start is called before the first frame update
    void OnDrawGizmos()
    {
    
    
        MeshFilter filter = GetComponent<MeshFilter>();
        if (filter){
    
    
            Mesh mesh = filter.sharedMesh;
            if (mesh){
    
    
                showTangentSpace(mesh);
            }
        }
    }

    // Update is called once per frame
    void showTangentSpace(Mesh mesh)
    {
    
    
        Vector3[] vertices = mesh.vertices;
        Vector3[] normals = mesh.normals;
        Vector4[] tangents = mesh.tangents;
        for (int i =0; i<vertices.Length; i++){
    
    
            showTangentSpace(transform.TransformPoint(vertices[i]), transform.TransformDirection(normals[i]), transform.TransformDirection(tangents[i]));
        }
    }

    void showTangentSpace(Vector3 vertex, Vector3 normal, Vector3 tangent){
    
    
        Gizmos.color = Color.green;
        Gizmos.DrawLine(vertex, vertex + normal * normalScale);
        Gizmos.color = Color.red;
        Gizmos.DrawLine(vertex, vertex + tangent * normalScale);
        Gizmos.color = Color.blue;
        Vector3 bitangent = Vector3.Cross(tangent, normal);
        Gizmos.DrawLine(vertex, vertex + bitangent * normalScale);
    }
}

在这里插入图片描述

3.2. 使用切线空间的法线贴图

我们大可以直接使用原有的材质直接套给刚刚建立的默认球体,可以看到明显的效果错误。一方面是由于法线方向计算错误,导致diffuse光照错误,另一方面,我们可以看到球体顶部的纹理被压缩的很厉害,这是由于unity的默认球体本身的uv结构导致的。
在这里插入图片描述
为了正确使用法线贴图,我们需要做目前基本上任何一个初学者都知道的一步,要把根据模型自身的法线,切线来求解副切线,然后把这三个向量组成TBN矩阵,来对采样到的法线进行空间变换。

struct appdata
{
    
    
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
    float3 normal : NORMAL;
    float4 tangent : TANGENT;
};

struct v2f
{
    
    
    float4 uv : TEXCOORD0;
    float3 worldPos :TEXCOORD1;
    float3 Normal : TEXCOORD2;
    float4 tangent : TEXCOORD3;
    float4 vertex : SV_POSITION;
};

sampler2D _NormalMap;
float4 _NormalMap_ST;
float4 _NormalMap_TexelSize;
sampler2D _Albedo;
float4 _Albedo_ST;
sampler2D _DetailNormalMap;
float4 _DetailNormalMap_ST;
float _sampleDelta, _BumpScale, _DetailBumpScale;
fixed4 _BaseColor;

v2f vert (appdata v)
{
    
    
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv.xy = TRANSFORM_TEX(v.uv, _NormalMap);
    o.uv.zw = TRANSFORM_TEX(v.uv, _DetailNormalMap);
    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    o.Normal = UnityObjectToWorldNormal(v.normal);
    o.tangent = float4(UnityObjectToWorldDir(v.tangent), v.tangent.w);
    
    return o;
}

fixed4 frag (v2f i) : SV_Target
{
    
    
    float3 tanNormal, mainNormal, detailNormal;
    fixed3 col = tex2D(_Albedo, i.uv.xy) ;
    
    mainNormal = UnpackScaleNormal(tex2D(_NormalMap, i.uv.xy), _BumpScale);
    detailNormal = UnpackScaleNormal(tex2D(_NormalMap, i.uv.zw), _DetailBumpScale);
    
    tanNormal = BlendNormals(mainNormal, detailNormal);
    tanNormal = normalize(tanNormal);
    
    float3 bitangent = -cross(i.tangent.xyz, i.Normal) * i.tangent.w;

    float3x3 TBN = float3x3(i.tangent.xyz, bitangent, i.Normal);

    float3 worldNormal = normalize(mul(tanNormal, TBN));

    float3 LightDir = normalize(( _WorldSpaceLightPos0.xyz - i.worldPos));

    fixed3 diffuse = dot(LightDir, worldNormal) * _LightColor0.xyz;
    
    return fixed4( col * _BaseColor + diffuse, 1);
}
ENDCG

在这里插入图片描述

3.3. 关于mikktspace

mikktSpace是unity使用是一种生成切线空间和法线的标准,名称由来是其建立者Morten Mikkelsen。

对于使用mikktspace的着色器,它会在顶点着色器中获取标准化的法线和切线向量,并完成插值计算,而非在在每一个片元中进行重新标准化。

副切线主要是通过法线和切线的叉乘求解的,并且叉乘的结果会乘以切线向量的w分量。

在进行切线的计算时,unity默认使用的是mikktspace。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/misaka12807/article/details/131622700