AmplifyImpostors原始碼閱讀

HONT發表於2024-11-28

首先看一下點選Bake按鈕後的執行流程:

1.AmplifyImpostorInspector部分

首先點選按鈕設定了bakeTexture = true

if( GUILayout.Button( TextureIcon, "buttonright", GUILayout.Height( 24 ) ) )
{
    // now recalculates texture and mesh every time because mesh might have changed
    //if( m_instance.m_alphaTex == null )
    //{
        m_outdatedTexture = true;
        m_recalculatePreviewTexture = true;
    //}

    bakeTextures = true;
}

如果展開了BillboardMesh選項或是bakeTextures為true,則都會執行下面部分:

if( ( ( m_billboardMesh || m_recalculatePreviewTexture ) && m_instance.m_alphaTex == null ) || ( bakeTextures && m_recalculatePreviewTexture ) )
{
    try
    {
        m_instance.RenderCombinedAlpha( m_currentData );
    }
    catch( Exception e )
    {
        Debug.LogWarning( "[AmplifyImpostors] Something went wrong with the mesh preview process, please contact support@amplify.pt with this log message.\n" + e.Message + e.StackTrace );
    }

    if( m_instance.m_cutMode == CutMode.Automatic )
        m_recalculateMesh = true;
    m_recalculatePreviewTexture = false;
}

1.1 RenderCombinedAlpha

該函式會遍歷一遍所有視角的模型,生成出覆蓋範圍最大的Bounds,並更新到這2個變數中:

m_xyFitSize = Mathf.Max(m_xyFitSize, frameBounds.size.x, frameBounds.size.y);
m_depthFitSize = Mathf.Max(m_depthFitSize, frameBounds.size.z);

透過RenderImpostor函式的combinedAlphas變數,將所有視角模型的alpha疊加在一張RT上,再透過這張疊加RT

修正原有Bounds:

m_xyFitSize *= maxBound;
m_depthFitSize *= maxBound;

接著得到哪張材質的索引對應傳入RT集合的alpha材質:

bool standardRendering = m_data.Preset.BakeShader == null;
int alphaIndex = m_data.Preset.AlphaIndex;
if (standardRendering && m_renderPipelineInUse == RenderPipelineInUse.HDRP)
    alphaIndex = 3;
else if (standardRendering)
    alphaIndex = 2;

用深度圖的邊緣生成alpha:

RenderTexture tempTex = RenderTextureEx.GetTemporary(m_alphaGBuffers[3]);
Graphics.Blit(m_alphaGBuffers[3], tempTex);
packerMat.SetTexture("_A", tempTex);
Graphics.Blit(m_trueDepth, m_alphaGBuffers[3], packerMat, 11);
RenderTexture.ReleaseTemporary(tempTex);

shader:

Pass // copy depth 11
{
    ZTest Always Cull Off ZWrite Off

    CGPROGRAM
    #pragma target 3.0
    #pragma vertex vert_img
    #pragma fragment frag
    #include "UnityCG.cginc"

    uniform sampler2D _MainTex;
    uniform sampler2D _A;

    float4 frag( v2f_img i ) : SV_Target
    {
        float depth = SAMPLE_RAW_DEPTH_TEXTURE( _MainTex, i.uv ).r;
        float3 color = tex2D( _A, i.uv ).rgb;
        float alpha = 1 - step( depth, 0 );

        return float4( color, alpha );
    }
    ENDCG
}

合併後的alpha會單獨存下來,也就是每一個sheet格子的alpha疊在一起,這樣做可以讓最終生成面片的頂點合理覆蓋:

1.2 GenerateAutomaticMesh

這個函式主要生成頂點,會存到AmplifyImpostorAsset的ShapePoints中。

這一步一定會設上triangulateMesh = true;

if (m_recalculateMesh && m_instance.m_alphaTex != null)
{
    m_recalculateMesh = false;
    m_instance.GenerateAutomaticMesh(m_currentData);
    triangulateMesh = true;
    EditorUtility.SetDirty(m_instance);
}

接著設定previewMesh:

if (triangulateMesh)
    m_previewMesh = GeneratePreviewMesh(m_currentData.ShapePoints, true);

然後會將CutMode改為手動,允許使用者二次修改:

if (autoChangeToManual /*&& Event.current.type == EventType.Layout*/ )
{
    autoChangeToManual = false;
    m_instance.m_cutMode = CutMode.Manual;
    Event.current.Use();
}

最後進入DelayedBake,呼叫AmplifyImpostor的RenderAllDeferredGroups函式。

2.AmplifyImpostor部分

進入函式RenderAllDeferredGroups,前面都和之前操作差不多,直到呼叫到RenderImpostor:

if (impostorMaps)
{
    commandBuffer.SetViewProjectionMatrices(V, P);
    commandBuffer.SetViewport(new Rect((m_data.TexSize.x / hframes) * x, (m_data.TexSize.y / (vframes + (impostorType == ImpostorType.Spherical ? 1 : 0))) * y, (m_data.TexSize.x / m_data.HorizontalFrames), (m_data.TexSize.y / m_data.VerticalFrames)));

繪製時每個sheet的格子都存放對應角度的模型圖片,透過SetViewport進行繪製目標區域的裁剪。

不同的ImpostorType對應繪製hframes、vframes的排布方式也不一樣。

繪製程式碼基本的邏輯結構如下:

for (int x = 0; x < hframes; x++)
{
    for (int y = 0; y <= vframes; y++)
    {
        if (impostorMaps)
        {
            commandBuffer.SetViewProjectionMatrices(V, P);
            commandBuffer.SetViewport(new Rect((m_data.TexSize.x / hframes) * x, (m_data.TexSize.y / (vframes + (impostorType == ImpostorType.Spherical ? 1 : 0))) * y, (m_data.TexSize.x / m_data.HorizontalFrames), (m_data.TexSize.y / m_data.VerticalFrames)));

            if (standardrendering && m_renderPipelineInUse == RenderPipelineInUse.HDRP)
            {
                commandBuffer.SetGlobalMatrix("_ViewMatrix", V);
                commandBuffer.SetGlobalMatrix("_InvViewMatrix", V.inverse);
                commandBuffer.SetGlobalMatrix("_ProjMatrix", P);
                commandBuffer.SetGlobalMatrix("_ViewProjMatrix", P * V);
                commandBuffer.SetGlobalVector("_WorldSpaceCameraPos", Vector4.zero);
            }
        }

        for (int j = 0; j < validMeshesCount; j++)
        {
            commandBuffer.DrawRenderer...
        }
    }
}
Graphics.ExecuteCommandBuffer(commandAlphaBuffer);

優先繪製Y軸,其次X軸,每次繪製寫入commandBuffer,最後統一在外部執行ExecuteCommandBuffer。

附一張測試例圖方便參考:

2.1 Remapping

這一步工作主要是將深度通道塞進去。

合併Alpha:

// Switch alpha with occlusion
RenderTexture tempTex = RenderTexture.GetTemporary(m_rtGBuffers[0].width, m_rtGBuffers[0].height, m_rtGBuffers[0].depth, m_rtGBuffers[0].format);
RenderTexture tempTex2 = RenderTexture.GetTemporary(m_rtGBuffers[3].width, m_rtGBuffers[3].height, m_rtGBuffers[3].depth, m_rtGBuffers[3].format);

packerMat.SetTexture("_A", m_rtGBuffers[2]);
Graphics.Blit(m_rtGBuffers[0], tempTex, packerMat, 4); //A.b
packerMat.SetTexture("_A", m_rtGBuffers[0]);
Graphics.Blit(m_rtGBuffers[3], tempTex2, packerMat, 4); //B.a
Graphics.Blit(tempTex, m_rtGBuffers[0]);
Graphics.Blit(tempTex2, m_rtGBuffers[3]);
RenderTexture.ReleaseTemporary(tempTex);
RenderTexture.ReleaseTemporary(tempTex2);

shader:

Pass // Copy Alpha 4
{
    CGPROGRAM
    #pragma target 3.0
    #pragma vertex vert_img
    #pragma fragment frag
    #include "UnityCG.cginc"

    uniform sampler2D _MainTex;
    uniform sampler2D _A;

    fixed4 frag (v2f_img i ) : SV_Target
    {
        float alpha = tex2D( _A, i.uv ).a;
        fixed4 finalColor = (float4(tex2D( _MainTex, i.uv ).rgb , alpha));
        return finalColor;
    }
    ENDCG
}

這一步會將RT[2]的alpha合併至RT[0],將RT[0]的alpha合併至RT[3]

接下來PackDepth,將深度資訊寫入RT[2]的A通道:

// Pack Depth
PackingRemapping(ref m_rtGBuffers[2], ref m_rtGBuffers[2], 0, packerMat, m_trueDepth);
m_trueDepth.Release();
m_trueDepth = null;

RT[2]存的是法線,a通道存深度後:

RT[0]的alpha:

FixAlbedo,m_rtGBuffers[1]對應extraTex引數,若傳參會被設定到_A取樣器。

// Fix Albedo
PackingRemapping(ref m_rtGBuffers[0], ref m_rtGBuffers[0], 5, packerMat, m_rtGBuffers[1]);

alb.rgb / (1-spec)不太清楚。

Pass // Fix albedo 5
{
    CGPROGRAM
    #pragma target 3.0
    #pragma vertex vert_img
    #pragma fragment frag
    #include "UnityCG.cginc"

    uniform sampler2D _MainTex;
    uniform sampler2D _A; //specular

    fixed4 frag (v2f_img i ) : SV_Target
    {
        float3 spec = tex2D( _A, i.uv ).rgb;
        float4 alb = tex2D( _MainTex, i.uv );
        alb.rgb = alb.rgb / (1-spec);
        return alb;
    }
    ENDCG
}

存TGA(如果預設裡勾選了TGA則呼叫該處,否則存PNG):

// TGA
for (int i = 0; i < outputList.Count; i++)
{
    if (outputList[i].ImageFormat == ImageFormat.TGA)
        PackingRemapping(ref m_rtGBuffers[i], ref m_rtGBuffers[i], 6, packerMat);
}

DilateShader邊緣膨脹處理:

Shader dilateShader = AssetDatabase.LoadAssetAtPath<Shader>(AssetDatabase.GUIDToAssetPath(DilateGUID));
Debug.Log(dilateShader, dilateShader);
Material dilateMat = new Material(dilateShader);

// Dilation
for (int i = 0; i < outputList.Count; i++)
{
    if (outputList[i].Active)
        DilateRenderTextureUsingMask(ref m_rtGBuffers[i], ref m_rtGBuffers[alphaIndex], m_data.PixelPadding, alphaIndex != i, dilateMat);
}

shader是沿著周圍8個方向外拓一圈:

float4 frag_dilate( v2f_img i, bool alpha )
{
    float2 offsets[ 8 ] =
    {
        float2( -1, -1 ),
        float2(  0, -1 ),
        float2( +1, -1 ),
        float2( -1,  0 ),
        float2( +1,  0 ),
        float2( -1, +1 ),
        float2(  0, +1 ),
        float2( +1, +1 )
    };

函式中會根據pixelBlend將這個shader呼叫N次:

for (int i = 0; i < pixelBleed; i++)
{
    dilateMat.SetTexture("_MaskTex", dilatedMask);

    Graphics.Blit(mainTex, tempTex, dilateMat, alpha ? 1 : 0);
    Graphics.Blit(tempTex, mainTex);

    Graphics.Blit(dilatedMask, tempMask, dilateMat, 1);
    Graphics.Blit(tempMask, dilatedMask);
}

預設值是呼叫32次:

[SerializeField]
[Range( 0, 64 )]
public int PixelPadding = 32;

3.Shader渲染部分

Octahedron八面體方案和球面分別使用2個對外Shader,

八面體方案會取樣3次做插值,球面則程式碼稍少,接下來只看球面部分。

3.1 SphereImpostorVertex

先看ForwardBase的pass:

頂點部分執行SphereImpostorVertex( v.vertex, v.normal, o.frameUVs, o.viewPos );

這個函式會處理Billboard的位置資訊,並返回常規頂點資訊和frameUVs資訊。

得到相對相機位置,並轉換至object空間,_Offset是實際模型中心偏移量,透過畫素轉頂點的方式離線計算得到

float3 objectCameraPosition = mul( ai_WorldToObject, float4( worldCameraPos, 1 ) ).xyz - _Offset.xyz; //ray origin
float3 objectCameraDirection = normalize( objectCameraPosition );

構建一組基向量:

float3 upVector = float3( 0,1,0 );
float3 objectHorizontalVector = normalize( cross( objectCameraDirection, upVector ) );
float3 objectVerticalVector = cross( objectHorizontalVector, objectCameraDirection );

橫向資訊用arctan2,變數名作者寫錯了

float verticalAngle = frac( atan2( -objectCameraDirection.z, -objectCameraDirection.x ) * AI_INV_TWO_PI ) * sizeX + 0.5;

縱向資訊用acos將點乘轉線性

float verticalDot = dot( objectCameraDirection, upVector );
float upAngle = ( acos( -verticalDot ) * AI_INV_PI ) + axisSizeFraction * 0.5f;

yRot構建的旋轉矩陣用作細節修正

float yRot = sizeFraction.x * AI_PI * verticalDot * ( 2 * frac( verticalAngle ) - 1 );

// Billboard rotation
float2 uvExpansion = vertex.xy;
float cosY = cos( yRot );
float sinY = sin( yRot );
float2 uvRotator = mul( uvExpansion, float2x2( cosY, -sinY, sinY, cosY ) );

最後sizeFraction用於將座標縮放為對應sheet內格子大小

float2 frameUV = ( ( uvExpansion * fractionsUVscale + 0.5 ) + relativeCoords ) * sizeFraction;

3.2 SphereImpostorFragment

frag一些邏輯都是常規操作,看下深度部分的處理,

離近了看會有真實深度的遮擋:

因為是正交相機拍攝,不存在DeviceDepth轉線性EyeDepth。

深度賦值取的clipPos.z:

fixed4 frag_surf (v2f_surf IN, out float outDepth : SV_Depth ) : SV_Target {
    ...
    IN.pos.zw = clipPos.zw;
    outDepth = IN.pos.z;

_DepthSize讀的是csharp變數m_depthFitSize,在烘焙時這個值是正交相機的遠截面:

Matrix4x4 P = Matrix4x4.Ortho(-fitSize + m_pixelOffset.x, fitSize + m_pixelOffset.x, -fitSize + m_pixelOffset.y, fitSize + m_pixelOffset.y, 0, zFar: -m_depthFitSize);

最後深度計算這裡,_DepthSize*0.5猜測是物體中心是z=0.5,是基於物體中心增加偏移深度,並且remapNormal.a之前已經隨著法線做了-1 - 1的對映操作:

float4 remapNormal = normalSample * 2 - 1; // object normal is remapNormal.rgb

最後乘以length( ai_ObjectToWorld[ 2 ].xyz )其實是乘以Z軸的縮放,如果沒有縮放改成1結果不變:

float depth = remapNormal.a * _DepthSize * 0.5 * length( ai_ObjectToWorld[ 2 ].xyz );

計算完後再將顏色和深度輸出:

fixed4 frag_surf (v2f_surf IN, out float outDepth : SV_Depth ) : SV_Target {
    UNITY_SETUP_INSTANCE_ID(IN);
    SurfaceOutputStandardSpecular o;
    UNITY_INITIALIZE_OUTPUT( SurfaceOutputStandardSpecular, o );

    float4 clipPos;
    float3 worldPos;
    SphereImpostorFragment( o, clipPos, worldPos, IN.frameUVs, IN.viewPos );
    IN.pos.zw = clipPos.zw;

    outDepth = IN.pos.z;

    UNITY_APPLY_DITHER_CROSSFADE(IN.pos.xy);
    return float4( _ObjectId, _PassValue, 1.0, 1.0 );
}

陰影部分ShadowCaster pass用了同樣的程式碼,因此impostor也有陰影。

相關文章