Post

Unity Shader GUI 扩展二(翻译十)

Unity Shader GUI 扩展二(翻译十)

本篇摘要:

  • 把自身阴影烘焙进材质
  • 增加细节纹理部分
  • 支持更丰富的shader变体
  • 一次编辑多个材质球

遮挡区域的Self-Shading

美术能够创作非常复杂丰富的表面纹理,它只是一个视错觉。为了增强表面纹理视觉真实感,引入Self-Shading。

如何增强呢?通常我们使用了法线来增强模型表面的凹凸层次感,法线带来的视觉增强是第一步,但是法线只适用于采样直接光照下。现在开始第二步增强,给凹凸表面引入阴影:凸向凹投射阴影

Occlusion Map

使用遮挡纹理增加self-shadings,也是灰度图,凹趋近黑色0。在Properties声明和拓展gui:

1
2
3
4
[NoScaleOffset]_OcclusionMap("OcclusionMap", 2D) = "white"{}
_Occlusion("Occlusion", Range(0,1)) = 0

#pragma shader_feature _ _OCCLUSION_MAP

这两种是等效的

shader_feature _ _OCCLUSION_MAP _shader_feature _OCCLUSION_MAP

1
2
3
//但是这两种不等效!!!
#pragma shader_feature _SMOOTHNESS_ALBEDO _SMOOTHNESS_METALLIC
#pragma shader_feature _ _SMOOTHNESS_ALBEDO _SMOOTHNESS_METALLIC

总结一下shader_feature,只有一个关键字默认生成<no keywords defined>,若有多个关键字第一个关键字会替代<no keywords defined>,在使用手动收集ShaderVariantCollection要多加注意。而multi_compile有单个关键字时必须加_

Occlusion GUI_Extension

1
2
3
4
5
6
7
8
9
void OcclusionShow()
{
    EditorGUI.BeginChangeCheck();
    MaterialProperty mp = MakerMapWithScaleShow("_OcclusionMap", "_Occlusion", false, "遮挡纹理");
    if (EditorGUI.EndChangeCheck())
    {
        SetKeyword("_OCCLUSION_MAP", mp.textureValue);
    }
}

直接光阴影

创建采样函数

1
2
3
4
5
6
float GetOcclusion(Interpolators i) {
#ifdef _OCCLUSION_MAP
    return tex2D(_OcclusionMap, i.uv).g;
#endif
    return 1;
}

但是由于阴影的强度有可调需求,需要动态改变。结合_OcclusionStrength做线性插值

1
2
3
4
5
6
7
float GetOcclusion(Interpolators i) {
#ifdef _OCCLUSION_MAP
    float g = tex2D(_OcclusionMap, i.uv).g
    return lerp(1, g, _OcclusionStrength);
#endif
    return 1;
}

然后将采样得到的值作用于光照颜色内,包括直接光和间接光,这里是直接光

1
2
3
4
5
6
7
8
UnityLight CreateLight(Interpolators i) {
    //。。。
    UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
    attenuation *= GetOcclusion(i);
    light.color = _LightColor0.rgb * attenuation;
    light.ndotl = DotClamped(i.normal, light.dir);
    return light;
}
without and with occlusion map
without and with occlusion map
without and with occlusion map

上图带有遮挡纹理的凹陷阴影过渡,gif显示:

阴影过渡
阴影过渡

间接光阴影

直接光采样下凹陷越深阴影越重,但不是那么明显。这是因为除了直接光以外还有间接光,幸好OcclusionMap并不是针对特定光线的纹理,现在来给他增加间接光采样,然后看看效果如何。

1
2
3
4
5
6
7
8
9
UnityIndirect CreateIndirectLight(Interpolators i, float3 viewDir) {
#if defined(VERTEXLIGHT_ON)
    //...
    float occlusion = GetOcclusion(i);
    indirectLight.diffuse *= occlusion;
    indirectLight.specular *= occlusion;
#endif
    return indirectLight;
}
wihtout and with occlusion
wihtout and with occlusion
wihtout and with occlusion

把图1.2与图1.1对比,可以明显感觉到Occlusion纹理好似专门针对间接光而制作,它随着凹陷越深阴影越明显,甚至有点过头了。那么我们何不把直接光采样这步去掉,看看它的效果如何。

1
2
3
4
5
6
7
8
UnityLight CreateLight(Interpolators i) {
    //。。。
    UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
    ~attenuation *= GetOcclusion(i);~
    light.color = _LightColor0.rgb * attenuation;
    light.ndotl = DotClamped(i.normal, light.dir);
    return light;
}
without and with occlusion
without and with occlusion
without and with occlusion

把上图逐一对比,感觉最后一图的效果适中,看起来舒服。就Occlusion而言,它具有相当大真实感。虽然如此,我们通常会发现游戏里的遮挡图也被用在直接光上。Unity老旧的shader就是这样做的,虽然它不太真实,但是对灯光效果的控制提供了相当大的灵活性。

SSAO:screen-space-ambient-occlusion:它是屏幕后处理效果,使用深度缓冲来动态创建整个帧的遮挡映射。它被用来增强屏幕的深度感,因为它是后处理效果,它在所有的灯光渲染之后被使用。这意味着那个阴影即使用了间接光也使用了直接光。因此它也是不真实的。

合并纹理

我们只用了遮挡纹理的G通道,而metallic金属纹理是存储在R通道,SmoothNess纹理存储在alpha通道。这意味着我们可以把三个纹理合并为一个纹理。

合并后的纹理
合并后的纹理

优势

  • 单一纹理降低了内存和存储压力;

弊端

  • 这个Shader中它会采样两次;(可以手动优化为从同一纹理采样);
  • 使用DXT5压缩后,纹理大小变小了但是它的质量也降低了。所幸这些纹理不要求太高的细节和精度。

细节纹理

增加细节纹理和法线,把细节纹理和法线设置为fade-out mipmap。

细节纹理效果
细节纹理效果

细节遮罩纹理

根据图2.1效果,细节纹理覆盖整个表面后,看起来的效果不是太好。最好的效果是它不覆盖金属区域部分。所以,我们可以用细节遮罩纹理来控制这部分显示,这就好像蒙版测试。不同之处在于0表示没有细节,1表示完整的细节。

Unity的Standard Shader使用了遮罩纹理的alpha通道,这张纹理四个通道存储了同样的值。

Albedo Details

调整Albedo纹理采样,必须基于detal mask纹理的采样值,在未修改和修改后的albedo之间插值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
float3 GetAlbedo (Interpolators i) {
    float3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _Tint.rgb;
    float3 details = tex2D(_DetailTex, i.uv.zw) * unity_ColorSpaceDouble;
    albedo = lerp(albedo, albedo * details, GetDetailMask(i));
    return albedo;
}

float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
    // float3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _Tint.rgb;
    // albedo *= tex2D(_DetailTex, i.uv.zw) * unity_ColorSpaceDouble;
    float3 specularTint;
    float oneMinusReflectivity;
    float3 albedo = DiffuseAndSpecularFromMetallic(
        GetAlbedo(i), GetMetallic(i), specularTint, oneMinusReflectivity
    );
}

Normal Details

对于法线,同样需要相同的调整。但是,这里的细节不符合与未修改的切线空间法向量相对应。因为原作者给的这张法线纹理不是切线空间.需要手动匹配一次。

1
2
3
4
5
6
void InitializeFragmentNormal(inout Interpolators i) {
    float3 mainNormal = UnpackScaleNormal(tex2D(_NormalMap, i.uv.xy), _BumpScale);
    float3 detailNormal = UnpackScaleNormal(tex2D(_DetailNormalMap, i.uv.zw), _DetailBumpScale);
    detailNormal = lerp(float3(0, 0, 1), detailNormal, GetDetailMask(i));
    float3 tangentSpaceNormal = BlendNormals(mainNormal, detailNormal);
}
Mask Details
Mask Details
This post is licensed under CC BY 4.0 by the author.