CapsuleAO實現的學習

HONT發表於2021-07-03

正是一個炎夏,又到了整活的好時候。最近抽些時間研究下CapsuleAO,記述實踐體會。

 

1.簡介

這是一個通過在角色骨骼上繫結虛擬膠囊體並以數學方法實現膠囊近似的AO環境光遮蔽效果的方法,

當角色處於陰影中時,CapsuleAO的效果比較明顯。當角色在露天環境中,效果較弱。

下圖是我自己遊戲裡截圖的效果,以做參考:

不同專案有不同的實現,UE4中也有類似實現,叫做Capsule Shadow,這裡不多做介紹:

 

2.CapsuleAO實現嘗試

首先用自己的思路實現一下,首先參考了IQ大神的SphereAO:

https://www.shadertoy.com/view/4djSDy

拋開公式的話,其實就是一個點光源的做法,然後把顏色改成黑色加上函式係數進行調節,使其更接近AO的感覺。

這是嘗試實現的效果:

 

實現時要將球體變為膠囊,要在膠囊的長度軸上計算投影。投影完後對投影長度進行Clamp約束,約束後的兩個端點

和周圍畫素進行Distance計算,這樣直接就是膠囊的效果了,而不是圓柱。做了個簡單的圖:

具體見程式碼。

Shader:

Shader "Unlit/CapsuleAOShader"
{
    Properties
    {
        _Adjust("Adjust", float) = 2
        _CapsuleRadius("Capsule Radius", float) = 0.3
        
        _DistanceFix("Distance Fix", float) = 0.3
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;

                float3 wPos : TEXCOORD2;
            };

            float4 _PlanePos;
            float4 _PlaneNormal;
            float4 _CapsuleP0;
            float4 _CapsuleP1;
            float _Adjust;
            float _CapsuleRadius;
            
            float _DistanceFix;
            
            float3 Formula(float3 sphP0, float3 sphP1, float length, float radius, float3 comparePoint)
            {
                float3 norm1 = normalize(sphP1 - sphP0);
                float3 relativeComparePoint = comparePoint-sphP0;
                float3 projValue = dot(relativeComparePoint, norm1);
                
                float x = clamp(projValue, -length, length);
                float3 projPoint = sphP0 + x * norm1;
                float3 norm2 = normalize(comparePoint - projPoint);
                
                return projPoint + norm2 * radius;
            }
            
            float3 DistanceFix(float3 distancePoint, float wPos, float3 norm, float distanceFix)
            {
                return distancePoint + norm * distanceFix;
            }
            
            float Occlusion(float3 pos, float3 nor, float3 sphP0, float3 sphP1)
            {
                float3 finalPoint = Formula(sphP0, sphP1, distance(sphP1,sphP0), _CapsuleRadius, pos);/*Add*/
                finalPoint = DistanceFix(finalPoint, pos, nor, _DistanceFix);/*Add*/
                float3  di = finalPoint - pos;
                float l = length(di);
                float nl = dot(nor, di / l);
                float h = l / 0.5;
                float h2 = h * h;
                float k2 = 1.0 - h2 * nl*nl;

                float res = pow(clamp(0.5*(nl*h + 1.0) / h2, 0.0, 1.0), 1.5);

                return res;
            }

            v2f vert (appdata v)
            {
                v2f o = (v2f)0;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;

                o.wPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                float occ0 = Occlusion(i.wPos, _PlaneNormal.xyz, _CapsuleP0.xyz, _CapsuleP1.xyz);
                return 1.0 - occ0;
            }
            ENDCG
        }
    }
}

c#部分,控制膠囊的傳入,與球體不同;膠囊需要一個長度向量資訊表示長度軸的朝向:

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

public class CapsuleAOParamUpdate : MonoBehaviour
{
    public Transform capsuleP0;
    public Transform capsuleP1;
    Material mMaterial;


    private void Update()
    {
        mMaterial = GetComponent<MeshRenderer>().sharedMaterial;

        mMaterial.SetVector("_PlanePos", transform.position);
        mMaterial.SetVector("_PlaneNormal", transform.up);

        mMaterial.SetVector("_CapsuleP0", new Vector4(capsuleP0.position.x, capsuleP0.position.y, capsuleP0.position.z, 1f));
        mMaterial.SetVector("_CapsuleP1", new Vector4(capsuleP1.position.x, capsuleP1.position.y, capsuleP1.position.z, 1f));
    }
}

 

 

3.Capsule Shadow實現嘗試

看了下UE裡的實現程式碼,比較複雜。這部分自己處理比較簡單。

先看最終效果(支援非平面表面,光照方向改變,但某些光照角度有一定穿幫感):

 

首先,以主光平行光的方向作為投影平面,在shader的frag裡得到地面每個畫素世界座標時,

將畫素位置,膠囊位置,膠囊方向向量等都投影到平面上進行計算。

 

大致如下圖:

膠囊體需要兩個引數,以確定膠囊方向。根據投影后的膠囊方向和地面法線位置,得到叉乘位置,然後可以作為x,y座標取樣貼圖。

取樣貼圖雖然效果好些,但開銷較高,所以也可以自己去擬合。

接著混合3個權重資訊:

  1. 對平行光的相反方向做一個點乘處理,防止反方向上也被對映上陰影(FadeDirectionWeight)。
  2. 對主要投影區域做一個權重係數,讓只有被投影地面上有陰影圖案(MaskWeight)。
  3. 將投影前的地面世界空間座標和投影前的膠囊位置做一個權重,讓陰影有一個深到淺的漸變效果(FadeWeight)。

最後將所有權重混合:

return shadowCol * maskWeight * fadeWeight * faceDirectionWeight;

 

 

最後上程式碼,程式碼為取樣貼圖版本,開銷比較高。

shader部分:

Shader "Unlit/CapsuleShadowShader"
{
    Properties
    {
        _MainShapeTex ("Main Shape Tex (RGB)", 2D) = "white" {}
        _UvOffset("Uv Offset", vector) = (0.0, 0.0, 0.0, 0.0)
        _ShadowScaleFactor("Shadow Scale Factor", float) = 1.0
        _FadeWeightFactor("Fade Weight Factor", float) = 10.0
        _FadeDirectionFactor("Fade Direction Factor", float) = 1.0
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;

                float3 wPos : TEXCOORD2;
            };

            uniform float4 _LightDirectionVector;
            uniform float4 _CapsulePos1;
            uniform float4 _CapsulePos2;
            uniform float4 _CapsulePos3;
            uniform float4 _CapsulePos4;
            uniform float4 _CapsulePos5;
            uniform float4 _CapsulePos6;
            uniform float4 _CapsulePos7;
            uniform float4 _CapsulePos8;
            uniform float4 _CapsulePos9;
            uniform float4 _CapsulePos10;
            uniform float4 _CapsulePos11;
            uniform float4 _CapsulePos12;
            uniform float4 _CapsulePos13;
            uniform float4 _CapsulePos14;


            sampler2D _MainShapeTex;
            float4 _UvOffset;
            float _ShadowScaleFactor;
            float _FadeWeightFactor;
            
            float _FadeDirectionFactor;
            
            v2f vert (appdata v)
            {
                v2f o = (v2f)0;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;

                o.wPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                return o;
            }
            
            half3 ProjectOnPlane(half3 vec, half3 planeNormal)
            {
                float num = dot(planeNormal, planeNormal);
                float num2 = dot(vec, planeNormal);
                return half3(vec.x - planeNormal.x * num2 / num, vec.y - planeNormal.y * num2 / num, vec.z - planeNormal.z * num2 / num);
            }
            
            half CapsuleShadow(float3 capsulePos, float3 capsulePosDirectVec, float3 wPos)
            {
                half faceDirectionWeight = max(_FadeDirectionFactor, dot(normalize(capsulePos - wPos), _WorldSpaceLightPos0.xyz));

                half fadeDistance = distance(wPos, capsulePos);
                half fadeWeight = (1.0/distance(wPos, capsulePos)) * _FadeWeightFactor;

                half3 proj = ProjectOnPlane(wPos, _WorldSpaceLightPos0.xyz);
                half3 centerProj = ProjectOnPlane(capsulePos, _WorldSpaceLightPos0.xyz);
                half3 centerVectorProj = ProjectOnPlane(capsulePosDirectVec, _WorldSpaceLightPos0.xyz);
                half3 dir1 = normalize(centerVectorProj - centerProj);
                half3 dir2 = normalize(cross(dir1, _WorldSpaceLightPos0.xyz));
                
                half x = dot(proj - centerProj, dir1) * _UvOffset.z + _UvOffset.x;
                half y = dot(proj - centerProj, dir2) * _UvOffset.w + _UvOffset.y;
                half shadowCol = tex2D(_MainShapeTex, half2(x, y)).r;
                
                half maskWeight = saturate(distance(proj, centerProj) / _ShadowScaleFactor);
                maskWeight = max(0.4, maskWeight);
                
                return shadowCol * maskWeight * fadeWeight * faceDirectionWeight;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                half col = CapsuleShadow(_CapsulePos1, _LightDirectionVector, i.wPos);
                col = max(col, CapsuleShadow(_CapsulePos2, _LightDirectionVector, i.wPos));
                col = max(col, CapsuleShadow(_CapsulePos3, _LightDirectionVector, i.wPos));
                col = max(col, CapsuleShadow(_CapsulePos4, _LightDirectionVector, i.wPos));
                col = max(col, CapsuleShadow(_CapsulePos5, _LightDirectionVector, i.wPos));
                col = max(col, CapsuleShadow(_CapsulePos6, _LightDirectionVector, i.wPos));
                col = max(col, CapsuleShadow(_CapsulePos7, _LightDirectionVector, i.wPos));
                col = max(col, CapsuleShadow(_CapsulePos8, _LightDirectionVector, i.wPos));
                col = max(col, CapsuleShadow(_CapsulePos9, _LightDirectionVector, i.wPos));
                col = max(col, CapsuleShadow(_CapsulePos10, _LightDirectionVector, i.wPos));
                col = max(col, CapsuleShadow(_CapsulePos11, _LightDirectionVector, i.wPos));
                col = max(col, CapsuleShadow(_CapsulePos12, _LightDirectionVector, i.wPos));
                col = max(col, CapsuleShadow(_CapsulePos13, _LightDirectionVector, i.wPos));
                col = max(col, CapsuleShadow(_CapsulePos14, _LightDirectionVector, i.wPos));

                return lerp(0.5, 0.0, col);
            }
            ENDCG
        }
    }
}

c#部分:

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

public class CapsuleAOParamUpdate : MonoBehaviour
{
    public Transform capsuleHead;
    public Transform capsuleBody1;
    public Transform capsuleBody2;
    public Transform capsuleBody3;

    public Transform capsuleLeftArm1;
    public Transform capsuleLeftArm2;
    public Transform capsuleLeftArm3;
    public Transform capsuleRightArm1;
    public Transform capsuleRightArm2;
    public Transform capsuleRightArm3;
    public Transform capsuleLeftLeg1;
    public Transform capsuleLeftLeg2;
    public Transform capsuleLeftLeg3;
    public Transform capsuleRightLeg1;
    public Transform capsuleRightLeg2;
    public Transform capsuleRightLeg3;

    public Transform lightDirectionVector;

    private Material mMaterial;


    private void Update()
    {
        mMaterial = GetComponent<MeshRenderer>().sharedMaterial;

        if (capsuleHead)
            mMaterial.SetVector("_CapsulePos1", capsuleHead.position);
        
        if (capsuleBody1)
            mMaterial.SetVector("_CapsulePos2", capsuleBody1.position);
        if (capsuleBody2)
            mMaterial.SetVector("_CapsulePos2", capsuleBody2.position);
        if (capsuleBody3)
            mMaterial.SetVector("_CapsulePos2", capsuleBody3.position);
        
        if (capsuleLeftArm1)
            mMaterial.SetVector("_CapsulePos3", capsuleLeftArm1.position);
        if (capsuleLeftArm2)
            mMaterial.SetVector("_CapsulePos4", capsuleLeftArm2.position);
        if (capsuleLeftArm3)
            mMaterial.SetVector("_CapsulePos5", capsuleLeftArm3.position);
        
        if (capsuleRightArm1)
            mMaterial.SetVector("_CapsulePos6", capsuleRightArm1.position);
        if (capsuleRightArm2)
            mMaterial.SetVector("_CapsulePos7", capsuleRightArm2.position);
        if (capsuleRightArm3)
            mMaterial.SetVector("_CapsulePos8", capsuleRightArm3.position);
        
        if (capsuleLeftLeg1)
            mMaterial.SetVector("_CapsulePos9", capsuleLeftLeg1.position);
        if (capsuleLeftLeg2)
            mMaterial.SetVector("_CapsulePos10", capsuleLeftLeg2.position);
        if (capsuleLeftLeg3)
            mMaterial.SetVector("_CapsulePos11", capsuleLeftLeg3.position);
        
        if (capsuleRightLeg1)
            mMaterial.SetVector("_CapsulePos12", capsuleRightLeg1.position);
        if (capsuleRightLeg2)
            mMaterial.SetVector("_CapsulePos13", capsuleRightLeg2.position);
        if (capsuleRightLeg3)
            mMaterial.SetVector("_CapsulePos14", capsuleRightLeg3.position);

        mMaterial.SetVector("_LightDirectionVector", lightDirectionVector.position);
    }
}

 

4.總結

這篇文章以學習為主,就不提供下載工程了。具體使用還需自行開發。

 

若該方案需要在專案中的落地,我做如下建議:

1.不要一次性在shader裡傳入所有膠囊,可以分成多個pass來做,也可以放到螢幕blit裡去做,或者先畫到一張臨時RT裡。

2.當角色暴露在強光下,基本看不出CapsuleAO效果,當角色在陰影中或處於柔和光照環境下,才會有明顯的CapsuleAO表現。

3.也可以只有主角有CapsuleAO效果。

4.也可以CapsuleAO+CapsuleShadow,但是要分成多個pass來做。

相關文章