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
网友评论