修正半透明头发的渲染异常

Posted by 恶毒的狗 on December 24, 2019

修正半透明头发的渲染异常

这两天在整理 小甜甜 的捏脸系统,顺带把前项目 半透明头发渲染异常的bug 修正了,效果如下:

img

其实,前项目的头发工作良好,只是有一点 半透明渲染顺序 的小瑕疵,但正如前文 一个自阴影的Bug 所提到的:我在迁移shader的时候因为偷懒,把头发改成了 Surface Shader,从而带来了一些新的问题。


问题一:关于Surface Shader的alpha指令

首先,我犯了第一个错误,改 Surface Shader 的过程中,我漏掉了 alpha 指令的设置,shader变成了 实体渲染,表现如下:

img

关于 Surface Shader的alpha指令,可以参考Unity帮助文档:

alpha or alpha:auto - Will pick fade-transparency (same as alpha:fade) for simple lighting functions, and premultiplied transparency (same as alpha:premul) for physically based lighting functions.

alpha:blend - Enable alpha blending.

alpha:fade - Enable traditional fade-transparency.

alpha:premul - Enable premultiplied alpha transparency.

虽然我指定了 Queue = TransparentBlend SrcAlpha OneMinusSrcAlpha,但因为没设置 alpha,Unity最终生成的代码给我加了这么一句:

1
UNITY_OPAQUE_ALPHA(c.a);

UNITY_OPAQUE_ALPHA定义如下:

1
#define UNITY_OPAQUE_ALPHA(outputAlpha) outputAlpha = 1.0

这里认为是 实体渲染,强制的把alpha设置为 1 了。

修正这个问题很简单,加上 alpha 指令的设置即可:

1
#pragma surface surf BGHair fullforwardshadows addshadow vertex:vert alpha

问题二:半透明的渲染顺序

加上 alpha 后,头发通透了好多,但是新的问题来了,当 头发的半透明部分相互交错 时,渲染表现错误:

img

因为是半透明渲染,Surface Shader生成的代码开启了 深度测试,关闭了 深度缓冲写入,当 半透部分相互交错 时,前后关系难以保证。

这是半透材质经常遇到的问题,我们可以把头发模型拆成 实体半透 两部分,先渲染 实体部分,再渲染 半透部分,在实体的基础上做Alpha混合,从而尽可能避免半透交错的机会。

不过当初做这个头发的时候,并没有拆分模型,做法也很简单:开启 深度缓冲写入,让头发的半透明部分仅局限于 发尾,由于半透区域很小,一般情况下很难穿帮,不过某些角度下细看还是会有问题。

比如下图红圈标注的部位,后面的头发因为 深度测试 没通过,导致前面的头发直接和背景混合,引发了表现错误。

img

此外,因为迁移到了 Surface Shader,我发现如果打开了 alpha 指令,无论我是否指定 ZWrite On,Unity给我生成的代码始终是 ZWrite Off 的。


解决方式一:用AlphaTest代替AlphaBlend

如果这里我们用 AlphaTest 来取代 AlphaBlend ,效果会如何呢?

作为测试,简单的加一句 clip,并且关闭 alpha 指令:

1
clip(mainColor.a - 0.6);

效果如下:

img

渲染正确了,只是发尾太硬,用美术的话说:不够透气


解决方式二:加一个Pass

方式一 不够完美,不过我们可以用它做一个 额外的Pass,类似拆分模型后的 实体部分渲染,渲染完毕再做之前的 半透明渲染

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
Pass
{
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #include "UnityCG.cginc"

    sampler2D _MainTex;
    float4 _MainTex_ST;

    half4 _Color;
    half4 _Color2;

    half _Color2Offset;

    struct appdata            
    {
        float3 pos : POSITION;    
        float3 uv0 : TEXCOORD0;   
        UNITY_VERTEX_INPUT_INSTANCE_ID
    };
 
    struct v2f 
    {
        UNITY_POSITION(pos);
        float4 uv0 : TEXCOORD0;
        UNITY_VERTEX_OUTPUT_STEREO
    };
 
    v2f vert (appdata IN)
    {
        v2f o;
        UNITY_SETUP_INSTANCE_ID(IN);
        UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);

        o.pos = UnityObjectToClipPos(IN.pos);
        o.uv0.xy = TRANSFORM_TEX(IN.uv0, _MainTex);

        return o;
    }
 
    half4 frag(v2f IN) : SV_Target
    {
        half4 mainColor = tex2D( _MainTex, IN.uv0.xy );

        clip(mainColor.a - 0.999);

        half3 hairColor = _Color.rgb * mainColor.r;
        half mixStrength = saturate(mainColor.g + _Color2Offset);
        hairColor = lerp(hairColor, _Color2.rgb, mixStrength);

        return half4(hairColor, 1);
    }
    ENDCG          
}

代码很简单,最主要的就是下面这句:

1
clip(mainColor.a - 0.999);

这里把原来的半透部分尽可能的裁剪掉,只保留实体的轮廓,如下:

img

在这个基础之上,再做一次半透明渲染即可。


局限

采用 方式二 后,原先 发尾 部分因为 半透重叠 引发的 渲染瑕疵 也可以更大程度的缓解了,如下图:

img

此外,需要注意的是,这里能良好工作,头发的半透部分仅局限于 发尾 也是一个重要因素。

最后换个脸妆再来一张:

img

拜拜!