美文网首页
Unity自定义SRP(八):复杂纹理

Unity自定义SRP(八):复杂纹理

作者: Dragon_boy | 来源:发表于2021-01-17 18:42 被阅读0次

https://catlikecoding.com/unity/tutorials/custom-srp/complex-maps/

这里使用一个示例材质来引入多种不同的贴图。

1 电路材质

Albedo:

Emission:

2 遮罩贴图

​ 遮罩贴图是一张灰度图,用于控制作用区域。

2.1 MODS

​ 对于PBR材质,主要是控制金属度和粗糙度,我们可以对这两个属性设置单独的遮罩贴图,但由于只是用单通道,我们其实可以将这些混合在一张贴图中。Unity的HDRP使用的是MODS贴图,对应金属度、遮蔽、细节和光滑度,每个通道可用于控制对应的属性。

​ 比如下面这张:

​ 在shader中添加对应属性:

        [NoScaleOffset] _MaskMap("Mask (MODS)", 2D) = "white" {}
        _Metallic ("Metallic", Range(0, 1)) = 0
        _Smoothness ("Smoothness", Range(0, 1)) = 0.5

2.2 遮罩输入

​ 在LitInput中定义获取方法:

TEXTURE2D(_BaseMap);
TEXTURE2D(_MaskMap);
…

float4 GetMask (float2 baseUV) 
{
    return SAMPLE_TEXTURE2D(_MaskMap, sampler_BaseMap, baseUV);
}

​ 为简化,我们定义一个宏:

#define INPUT_PROP(name) UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, name)

​ 替换对应代码即可。

2.3 金属度

​ 在GetMetallic中我们应用遮罩图:

float GetMetallic (float2 baseUV) 
{
    float metallic = INPUT_PROP(_Metallic);
    metallic *= GetMask(baseUV).r;
    return metallic;
}

2.4 光滑度

​ 光滑度同理:

float GetSmoothness (float2 baseUV) 
{
    float smoothness = INPUT_PROP(_Smoothness);
    smoothness *= GetMask(baseUV).a;
    return smoothness;
}

2.5 遮蔽

​ 遮罩贴图的G通道包含遮蔽数据,即物体自身的片段会相互遮挡:

float GetOcclusion (float2 baseUV) 
{
    return GetMask(baseUV).g;
}

​ 在Surface中添加对应属性:

struct Surface 
{
    …
    float occlusion;
    float smoothness;
    float fresnelStrength;
    float dither;
};

LitPassFragment中赋值:

    surface.occlusion = GetOcclusion(input.baseUV);

​ 遮蔽只应用于间接环境光照:

float3 IndirectBRDF (
    Surface surface, BRDF brdf, float3 diffuse, float3 specular
) 
{
    …
    
    return (diffuse * brdf.diffuse + reflection) * surface.occlusion;
}

​ 我们还可以控制遮蔽强度。添加属性:

        _Occlusion ("Occlusion", Range(0, 1)) = 1

​ 在UnityPerMaterial缓冲中添加。然后修改GetOcclusion方法:

float GetOcclusion (float2 baseUV) 
{
    float strength = INPUT_PROP(_Occlusion);
    float occlusion = GetMask(baseUV).g;
    occlusion = lerp(occlusion, 1.0, strength);
    return occlusion;
}

3 细节贴图

​ 细节贴图往往会使用较高的拼贴数,用于在拉近观察物体时给出更多的细节。细节贴图应该只负责修改表面属性,我们可以将数据整合在一张非颜色贴图中。HDRP使用ANySNx格式,albedo修改值在R,光滑度修改值在B,细节法线的XY组件在AG。目前我们先不支持细节法线。

3.1 细节UV坐标

​ 细节贴图默认使用linearGrey,这样就不会一开始就造成修改:

        _DetailMap("Details", 2D) = "linearGrey" {}

​ 添加对应的纹理声明和方法:

TEXTURE2D(_DetailMap);
SAMPLER(sampler_DetailMap);

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
    UNITY_DEFINE_INSTANCED_PROP(float4, _BaseMap_ST)
    UNITY_DEFINE_INSTANCED_PROP(float4, _DetailMap_ST)
    …
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

…

float2 TransformDetailUV (float2 detailUV) 
{
    float4 detailST = INPUT_PROP(_DetailMap_ST);
    return detailUV * detailST.xy + detailST.zw;
}

float4 GetDetail (float2 detailUV) 
{
    float4 map = SAMPLE_TEXTURE2D(_DetailMap, sampler_DetailMap, detailUV);
    return map;
}

LitPass中添加对应的结构体属性:

struct Varyings 
{
    …
    float2 baseUV : VAR_BASE_UV;
    float2 detailUV : VAR_DETAIL_UV;
    …
};

Varyings LitPassVertex (Attributes input) 
{
    …
    output.baseUV = TransformBaseUV(input.baseUV);
    output.detailUV = TransformDetailUV(input.baseUV);
    return output;
}

3.2 细节albedo

​ 向GetBase添加一个默认参数,细节贴图的uv,默认为0:

float4 GetBase (float2 baseUV, float2 detailUV = 0.0) 
{
    float4 map = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, baseUV);
    float4 color = INPUT_PROP(_BaseColor);
    
    float4 detail = GetDetail(detailUV);
    map += detail;
    
    return map * color;
}

​ 不过这样子不太正确,我们首先需要确保低细节值会降低亮度,高细节值会提升亮度,因此我们需要在GetDetail中将值映射到-1-1:

float4 GetDetail (float2 detailUV) 
{
    float4 map = SAMPLE_TEXTURE2D(_DetailMap, sampler_DetailMap, detailUV);
    return map * 2.0 - 1.0;
}

​ 同时,只有R通道影响albedo,如果该值小于0,则在原albedo与黑色间插值,大于0,则在原albedo和白色间插值,用于降低或提升亮度:

    float detail = GetDetail(detailUV).r;
    //map += detail;
    map.rgb = lerp(map.rgb, detail < 0.0 ? 0.0 : 1.0, abs(detail));

​ 不过由于我们是在先行颜色空间进行的操作,所以说感知到的亮度提升要比亮度降低明显,我们可以在gamma空间中进行,然后返回线性:

    map.rgb = lerp(sqrt(map.rgb), detail < 0.0 ? 0.0 : 1.0, abs(detail));
    map.rgb *= map.rgb;

​ 遮罩也加上来:

    float mask = GetMask(baseUV).b;
    map.rgb = lerp(sqrt(map.rgb), detail < 0.0 ? 0.0 : 1.0, abs(detail) * mask);

​ 在shader中添加修改细节albedo强度的属性:

        _DetailAlbedo("Detail Albedo", Range(0, 1)) = 1

​ 应用:

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
    …
    UNITY_DEFINE_INSTANCED_PROP(float, _DetailAlbedo)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

…

float4 GetBase (float2 baseUV, float2 detailUV = 0.0) 
{
    …
    float detail = GetDetail(detailUV).r * INPUT_PROP(_DetailAlbedo);
    …
}

3.3 细节光滑度

​ 完全同理:

        _DetailSmoothness("Detail Smoothness", Range(0, 1)) = 1
UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
    …
    UNITY_DEFINE_INSTANCED_PROP(float, _DetailSmoothness)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

…

float GetSmoothness (float2 baseUV, float2 detailUV = 0.0) 
{
    float smoothness = INPUT_PROP(_Smoothness);
    smoothness *= GetMask(baseUV).a;

    float detail = GetDetail(detailUV).b * INPUT_PROP(_DetailSmoothness);
    float mask = GetMask(baseUV).b;
    smoothness = lerp(smoothness, detail < 0.0 ? 0.0 : 1.0, abs(detail) * mask);
    
    return smoothness;
}

3.4 渐变细节

​ 距离过远的话,我们就不该使用细节贴图了。如果我们针对细节纹理开启Fadeout Mip Map选项,Unity会自动进行渐变,滤波模式调整为Trilinear

4 法线贴图

4.1 采样法线

​ 添加Shader的属性,法线贴图和凹凸值:

        [NoScaleOffset] _NormalMap("Normals", 2D) = "bump" {}
        _NormalScale("Normal Scale", Range(0, 1)) = 1

​ 有两种存储法线信息的方式,最直观的是将法线信息存储在RGB通道。不过我们若假设法线永远都在表面上朝上,我们可以只存储法线的XY组件,第三个值可计算得到。这是一种压缩纹理格式,XY存储在RG或AG通道,DXT5nm就是如此。Unity的Core RP提供两种法线数据转换方式:

float3 DecodeNormal (float4 sample, float scale) 
{
    #if defined(UNITY_NO_DXT5nm)
        return UnpackNormalRGB(sample, scale);
    #else
        return UnpackNormalmapRGorAG(sample, scale);
    #endif
}

​ 两种方法在Packing.hlsl中定义:

real3 UnpackNormalRGB(real4 packedNormal, real scale = 1.0)
{
    real3 normal;
    normal.xyz = packedNormal.rgb * 2.0 - 1.0;
    normal.xy *= scale;
    return normal;
}

real3 UnpackNormalAG(real4 packedNormal, real scale = 1.0)
{
    real3 normal;
    normal.xy = packedNormal.ag * 2.0 - 1.0;
    normal.xy *= scale;
    normal.z = sqrt(1.0 - saturate(dot(normal.xy, normal.xy)));
    return normal;
}

// Unpack normal as DXT5nm (1, y, 0, x) or BC5 (x, y, 0, 1)
real3 UnpackNormalmapRGorAG(real4 packedNormal, real scale = 1.0)
{
    // Convert to (?, y, 0, x)
    packedNormal.a *= packedNormal.r;
    return UnpackNormalAG(packedNormal, scale);
}

​ 由此,我们就可以获得切线空间的法线:

TEXTURE2D(_NormalMap);
…

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
    …
    UNITY_DEFINE_INSTANCED_PROP(float, _NormalScale)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

…

float3 GetNormalTS (float2 baseUV) 
{
    float4 map = SAMPLE_TEXTURE2D(_NormalMap, sampler_BaseMap, baseUV);
    float scale = INPUT_PROP(_NormalScale);
    float3 normal = DecodeNormal(map, scale);
    return normal;
}

4.2 切线空间

​ 我们可以在切线或世界空间计算光照,这里在世界空间计算,为此,我们需要将法线从切线空间转换到世界空间。定义转换方法:

float3 NormalTangentToWorld (float3 normalTS, float3 normalWS, float4 tangentWS) {
    float3x3 tangentToWorld =
        CreateTangentToWorld(normalWS, tangentWS.xyz, tangentWS.w);
    return TransformTangentToWorld(normalTS, tangentToWorld);
}

CreateTangentToWorld:

real3x3 CreateTangentToWorld(real3 normal, real3 tangent, real flipSign)
{
    // For odd-negative scale transforms we need to flip the sign
    real sgn = flipSign * GetOddNegativeScale();
    real3 bitangent = cross(normal, tangent) * sgn;

    return real3x3(tangent, bitangent, normal);
}

TransformTangentToWorld:

real3 TransformTangentToWorld(real3 dirTS, real3x3 tangentToWorld)
{
    // Note matrix is in row major convention with left multiplication as it is build on the fly
    return mul(dirTS, tangentToWorld);
}

LitPass中,我们使用TANGENT语义来标识切线:

struct Attributes 
{
    float3 positionOS : POSITION;
    float3 normalOS : NORMAL;
    float4 tangentOS : TANGENT;
    …
};

struct Varyings 
{
    float4 positionCS : SV_POSITION;
    float3 positionWS : VAR_POSITION;
    float3 normalWS : VAR_NORMAL;
    float4 tangentWS : VAR_TANGENT;
    …
};

​ 在顶点着色器中获取世界空间切线 :

    output.tangentWS =
        float4(TransformObjectToWorldDir(input.tangentOS.xyz), input.tangentOS.w);

​ 最后我们可获得世界空间的法线:

    surface.normal = NormalTangentToWorld(
        GetNormalTS(input.baseUV), input.normalWS, input.tangentWS
    );

4.3 为阴影偏移插值法线

​ 我们也需要使用从顶点法线插值得来的片元法线来偏移阴影采样,因此向Surface添加插值法线属性,毕竟现在的法线来自于法线贴图:

struct Surface 
{
    float3 position;
    float3 normal;
    float3 interpolatedNormal;
    …
};

​ 替换对应的代码即可。

4.4 细节法线

image

​ shader中添加对应的属性:

        _DetailMap("Details", 2D) = "linearGrey" {}
        [NoScaleOffset] _DetailNormalMap("Detail Normals", 2D) = "bump" {}
        _DetailAlbedo("Detail Albedo", Range(0, 1)) = 1
        _DetailSmoothness("Detail Smoothness", Range(0, 1)) = 1
        _DetailNormalScale("Detail Normal Scale", Range(0, 1)) = 1

​ 修改GetNormalTS方法,最后调用BlendNormalRNM来混合法线:

float3 GetNormalTS (float2 baseUV, float2 detailUV = 0.0) 
{
    float4 map = SAMPLE_TEXTURE2D(_NormalMap, sampler_BaseMap, baseUV);
    float scale = INPUT_PROP(_NormalScale);
    float3 normal = DecodeNormal(map, scale);

    map = SAMPLE_TEXTURE2D(_DetailNormalMap, sampler_DetailMap, detailUV);
    scale = INPUT_PROP(_DetailNormalScale) * GetMask(baseUV).b;
    float3 detail = DecodeNormal(map, scale);
    normal = BlendNormalRNM(normal, detail);

    return normal;
}

BlendNormalRNM,将细节法线沿基础法线旋转:

real3 BlendNormalRNM(real3 n1, real3 n2)
{
    real3 t = n1.xyz + real3(0.0, 0.0, 1.0);
    real3 u = n2.xyz * real3(-1.0, -1.0, 1.0);
    real3 r = (t / t.z) * dot(t, u) - u;
    return r;
}

5 贴图选项

​ 我们可以添加一些选项属性来有选择的使用贴图。

5.1 法线贴图

​ 拿法线贴图举例。

[Toggle(_NORMAL_MAP)] _NormalMapToggle ("Normal Map", Float) = 0

​ 添加对应的shader feature:

            #pragma shader_feature _NORMAL_MAP
    #if defined(_NORMAL_MAP)
        surface.normal = NormalTangentToWorld(
            GetNormalTS(input.baseUV, input.detailUV),
            input.normalWS, input.tangentWS
        );
    #else
        surface.normal = normalize(input.normalWS);
    #endif
    surface.interpolatedNormal = surface.normal;

结构体属性也可以控制:

struct Varyings 
{
    …
    #if defined(_NORMAL_MAP)
        float4 tangentWS : VAR_TANGENT;
    #endif
    …
};

Varyings LitPassVertex (Attributes input) 
{
    …
    #if defined(_NORMAL_MAP)
        output.tangentWS = float4(
            TransformObjectToWorldDir(input.tangentOS.xyz), input.tangentOS.w
        );
    #endif
    …
}

5.2 输入配置

​ 我们可以将UV等输入数据整合起来,在LitInput中优化代码:

struct InputConfig 
{
    float2 baseUV;
    float2 detailUV;
};

InputConfig GetInputConfig (float2 baseUV, float2 detailUV = 0.0) 
{
    InputConfig c;
    c.baseUV = baseUV;
    c.detailUV = detailUV;
    return c;
}

5.3 可选遮罩贴图

​ 为InputConfig加入是否使用遮罩的属性:

struct InputConfig 
{
    float2 baseUV;
    float2 detailUV;
    bool useMask;
};

InputConfig GetInputConfig (float2 baseUV, float2 detailUV = 0.0) 
{
    InputConfig c;
    c.baseUV = baseUV;
    c.detailUV = detailUV;
    c.useMask = false;
    return c;
}

​ 在GetMask,若使用遮罩则采样,否则返回0:

float4 GetMask (InputConfig c) 
{
    if (c.useMask) {
        return SAMPLE_TEXTURE2D(_MaskMap, sampler_BaseMap, c.baseUV);
    }
    return 1.0;
}

​ 添加对应的选项属性和shader feature:

        [Toggle(_MASK_MAP)] _MaskMapToggle ("Mask Map", Float) = 0
        [NoScaleOffset] _MaskMap("Mask (MODS)", 2D) = "white" {}
            #pragma shader_feature _MASK_MAP

LitPassFragment中:

    InputConfig config = GetInputConfig(input.baseUV, input.detailUV);
    #if defined(_MASK_MAP)
        config.useMask = true;
    #endif

5.4 可选细节

​ 同理,为细节添加选项:

struct InputConfig {
    …
    bool useDetail;
};

InputConfig GetInputConfig (float2 baseUV, float2 detailUV = 0.0) 
{
    …
    c.useDetail = false;
    return c;
}
float4 GetDetail (InputConfig c) 
{
    if (c.useDetail) {
        float4 map = SAMPLE_TEXTURE2D(_DetailMap, sampler_DetailMap, c.detailUV);
        return map * 2.0 - 1.0;
    }
    return 0.0;
}

GetBase中:

    if (c.useDetail) 
    {
        float detail = GetDetail(c).r * INPUT_PROP(_DetailAlbedo);
        float mask = GetMask(c).b;
        map.rgb =
            lerp(sqrt(map.rgb), detail < 0.0 ? 0.0 : 1.0, abs(detail) * mask);
        map.rgb *= map.rgb;
    }

​ 光滑度和法线也加上。

​ shader中加上对应的控制选项属性和shader feature:

        [Toggle(_DETAIL_MAP)] _DetailMapToggle ("Detail Maps", Float) = 0
        _DetailMap("Details", 2D) = "linearGrey" {}
            #pragma shader_feature _DETAIL_MAP

​ 只有在需要是配置细节UV:

struct Varyings 
{
    …
    #if defined(_DETAIL_MAP)
        float2 detailUV : VAR_DETAIL_UV;
    #endif
    …
};

Varyings LitPassVertex (Attributes input) 
{
    …
    #if defined(_DETAIL_MAP)
        output.detailUV = TransformDetailUV(input.baseUV);
    #endif
    return output;
}

LitPassFragment中进行输入配置:

    InputConfig config = GetInputConfig(input.baseUV);
    #if defined(_MASK_MAP)
        config.useMask = true;
    #endif
    #if defined(_DETAIL_MAP)
        config.detailUV = input.detailUV;
        config.useDetail = true;
    #endif

相关文章

网友评论

      本文标题:Unity自定义SRP(八):复杂纹理

      本文链接:https://www.haomeiwen.com/subject/uvczaktx.html