circle-loader
by
67/ 0

一、最終効果びコストを最適化する

8人が同時に接続し、最高の画質で、1.1ミリ秒かかります(Qualcomm Snapdragon710)。 プロジェクトはまだオンラインになっていないため、テスト図を使って説明します。

二、オリジナルの反射スクリプトのソースコード

2.1 ソースコード

using UnityEngine;
using System.Collections;

[ExecuteInEditMode]
public class MirrorReflection : MonoBehaviour
{
    public Material m_matCopyDepth;
    public bool m_DisablePixelLights = true;
    public int m_TextureSize = 256;
    public float m_ClipPlaneOffset = 0.07f;

    public LayerMask m_ReflectLayers = -1;

    private Hashtable m_ReflectionCameras = new Hashtable(); // Camera -> Camera table

    public RenderTexture m_ReflectionTexture = null;
    public RenderTexture m_ReflectionDepthTexture = null;
    private int m_OldReflectionTextureSize = 0;

    private static bool s_InsideRendering = false;

    public void OnWillRenderObject()
    {
        if (!enabled || !GetComponent<Renderer>() || !GetComponent<Renderer>().sharedMaterial || !GetComponent<Renderer>().enabled)
            return;

        Camera cam = Camera.current;
        if (!cam)
            return;

        // Safeguard from recursive reflections.
        if (s_InsideRendering)
            return;
        s_InsideRendering = true;

        Camera reflectionCamera;
        CreateMirrorObjects(cam, out reflectionCamera);

        // find out the reflection plane: position and normal in world space
        Vector3 pos = transform.position;
        Vector3 normal = transform.up;

        // Optionally disable pixel lights for reflection
        int oldPixelLightCount = QualitySettings.pixelLightCount;
        if (m_DisablePixelLights)
            QualitySettings.pixelLightCount = 0;

        UpdateCameraModes(cam, reflectionCamera);

        // Render reflection
        // Reflect camera around reflection plane
        float d = -Vector3.Dot(normal, pos) - m_ClipPlaneOffset;
        Vector4 reflectionPlane = new Vector4(normal.x, normal.y, normal.z, d);

        Matrix4x4 reflection = Matrix4x4.zero;
        CalculateReflectionMatrix(ref reflection, reflectionPlane);
        Vector3 oldpos = cam.transform.position;
        Vector3 newpos = reflection.MultiplyPoint(oldpos);
        reflectionCamera.worldToCameraMatrix = cam.worldToCameraMatrix * reflection;

        // Setup oblique projection matrix so that near plane is our reflection
        // plane. This way we clip everything below/above it for free.
        Vector4 clipPlane = CameraSpacePlane(reflectionCamera, pos, normal, 1.0f);
        Matrix4x4 projection = cam.projectionMatrix;
        CalculateObliqueMatrix(ref projection, clipPlane);
        reflectionCamera.projectionMatrix = projection;

        reflectionCamera.cullingMask = ~(1 << 4) & m_ReflectLayers.value; // never render water layer
        reflectionCamera.targetTexture = m_ReflectionTexture;
        //GL.SetRevertBackfacing(true);
        GL.invertCulling = true;
        reflectionCamera.transform.position = newpos;
        Vector3 euler = cam.transform.eulerAngles;
        reflectionCamera.transform.eulerAngles = new Vector3(0, euler.y, euler.z);
        reflectionCamera.depthTextureMode = DepthTextureMode.Depth;
        reflectionCamera.Render();

        // copy depth
        Graphics.SetRenderTarget(m_ReflectionDepthTexture);
        m_matCopyDepth.SetPass(0);
        DrawFullscreenQuad();
        Graphics.SetRenderTarget(null);
        // Graphics.Blit(m_ReflectionTexture, m_ReflectionDepthTexture, m_matCopyDepth);


        reflectionCamera.transform.position = oldpos;
        //GL.SetRevertBackfacing(false);
        GL.invertCulling = false;
        Material[] materials = GetComponent<Renderer>().sharedMaterials;
        foreach (Material mat in materials)
        {
            mat.SetTexture("_ReflectionTex", m_ReflectionTexture);
            mat.SetTexture("_ReflectionDepthTex", m_ReflectionDepthTexture);
        }

        // // Set matrix on the shader that transforms UVs from object space into screen
        // // space. We want to just project reflection texture on screen.
        // Matrix4x4 scaleOffset = Matrix4x4.TRS(
        //  new Vector3(0.5f, 0.5f, 0.5f), Quaternion.identity, new Vector3(0.5f, 0.5f, 0.5f));
        // Vector3 scale = transform.lossyScale;
        // Matrix4x4 mtx = transform.localToWorldMatrix * Matrix4x4.Scale(new Vector3(1.0f / scale.x, 1.0f / scale.y, 1.0f / scale.z));
        // mtx = scaleOffset * cam.projectionMatrix * cam.worldToCameraMatrix * mtx;
        // foreach (Material mat in materials)
        // {
        //  mat.SetMatrix("_ProjMatrix", mtx);
        // }

        // Restore pixel light count
        if (m_DisablePixelLights)
            QualitySettings.pixelLightCount = oldPixelLightCount;

        s_InsideRendering = false;
    }


    // Cleanup all the objects we possibly have created
    void OnDisable()
    {
        if (m_ReflectionTexture)
        {
            DestroyImmediate(m_ReflectionTexture);
            m_ReflectionTexture = null;
        }
        if (m_ReflectionDepthTexture)
        {
            DestroyImmediate(m_ReflectionDepthTexture);
            m_ReflectionDepthTexture = null;
        }
        foreach (DictionaryEntry kvp in m_ReflectionCameras)
            DestroyImmediate(((Camera)kvp.Value).gameObject);
        m_ReflectionCameras.Clear();
    }


    private void UpdateCameraModes(Camera src, Camera dest)
    {
        if (dest == null)
            return;
        // set camera to clear the same way as current camera
        dest.clearFlags = src.clearFlags;
        dest.backgroundColor = src.backgroundColor;
        if (src.clearFlags == CameraClearFlags.Skybox)
        {
            Skybox sky = src.GetComponent(typeof(Skybox)) as Skybox;
            Skybox mysky = dest.GetComponent(typeof(Skybox)) as Skybox;
            if (!sky || !sky.material)
            {
                mysky.enabled = false;
            }
            else
            {
                mysky.enabled = true;
                mysky.material = sky.material;
            }
        }
        // update other values to match current camera.
        // even if we are supplying custom camera&projection matrices,
        // some of values are used elsewhere (e.g. skybox uses far plane)
        dest.farClipPlane = src.farClipPlane;
        dest.nearClipPlane = src.nearClipPlane;
        dest.orthographic = src.orthographic;
        dest.fieldOfView = src.fieldOfView;
        dest.aspect = src.aspect;
        dest.orthographicSize = src.orthographicSize;
    }

    // On-demand create any objects we need
    private void CreateMirrorObjects(Camera currentCamera, out Camera reflectionCamera)
    {
        reflectionCamera = null;

        // Reflection render texture
        if (!m_ReflectionTexture || m_OldReflectionTextureSize != m_TextureSize)
        {
            if (m_ReflectionTexture)
                DestroyImmediate(m_ReflectionTexture);
            m_ReflectionTexture = new RenderTexture(m_TextureSize, m_TextureSize, 16);
            m_ReflectionTexture.name = "__MirrorReflection" + GetInstanceID();
            m_ReflectionTexture.isPowerOfTwo = true;
            m_ReflectionTexture.hideFlags = HideFlags.DontSave;
            m_ReflectionTexture.filterMode = FilterMode.Bilinear;

            if (m_ReflectionDepthTexture)
                DestroyImmediate(m_ReflectionDepthTexture);
            m_ReflectionDepthTexture = new RenderTexture(m_TextureSize, m_TextureSize, 0, RenderTextureFormat.RHalf);
            // m_ReflectionDepthTexture = new RenderTexture(m_TextureSize, m_TextureSize, 0, RenderTextureFormat.R8);
            m_ReflectionDepthTexture.name = "__MirrorReflectionDepth" + GetInstanceID();
            m_ReflectionDepthTexture.isPowerOfTwo = true;
            m_ReflectionDepthTexture.hideFlags = HideFlags.DontSave;
            m_ReflectionDepthTexture.filterMode = FilterMode.Bilinear;

            m_OldReflectionTextureSize = m_TextureSize;
        }

        // Camera for reflection
        reflectionCamera = m_ReflectionCameras[currentCamera] as Camera;
        if (!reflectionCamera) // catch both not-in-dictionary and in-dictionary-but-deleted-GO
        {
            GameObject go = new GameObject("Mirror Refl Camera id" + GetInstanceID() + " for " + currentCamera.GetInstanceID(), typeof(Camera), typeof(Skybox));
            reflectionCamera = go.GetComponent<Camera>();
            reflectionCamera.enabled = false;
            reflectionCamera.transform.position = transform.position;
            reflectionCamera.transform.rotation = transform.rotation;
            reflectionCamera.gameObject.AddComponent<FlareLayer>();
            go.hideFlags = HideFlags.HideAndDontSave;
            m_ReflectionCameras[currentCamera] = reflectionCamera;
        }
    }

    // Extended sign: returns -1, 0 or 1 based on sign of a
    private static float sgn(float a)
    {
        if (a > 0.0f) return 1.0f;
        if (a < 0.0f) return -1.0f;
        return 0.0f;
    }

    // Given position/normal of the plane, calculates plane in camera space.
    private Vector4 CameraSpacePlane(Camera cam, Vector3 pos, Vector3 normal, float sideSign)
    {
        Vector3 offsetPos = pos + normal * m_ClipPlaneOffset;
        Matrix4x4 m = cam.worldToCameraMatrix;
        Vector3 cpos = m.MultiplyPoint(offsetPos);
        Vector3 cnormal = m.MultiplyVector(normal).normalized * sideSign;
        return new Vector4(cnormal.x, cnormal.y, cnormal.z, -Vector3.Dot(cpos, cnormal));
    }

    // Adjusts the given projection matrix so that near plane is the given clipPlane
    // clipPlane is given in camera space. See article in Game Programming Gems 5 and
    // http://aras-p.info/texts/obliqueortho.html
    private static void CalculateObliqueMatrix(ref Matrix4x4 projection, Vector4 clipPlane)
    {
        Vector4 q = projection.inverse * new Vector4(
            sgn(clipPlane.x),
            sgn(clipPlane.y),
            1.0f,
            1.0f
        );
        Vector4 c = clipPlane * (2.0F / (Vector4.Dot(clipPlane, q)));
        // third row = clip plane - fourth row
        projection[2] = c.x - projection[3];
        projection[6] = c.y - projection[7];
        projection[10] = c.z - projection[11];
        projection[14] = c.w - projection[15];
    }

    // Calculates reflection matrix around the given plane
    private static void CalculateReflectionMatrix(ref Matrix4x4 reflectionMat, Vector4 plane)
    {
        reflectionMat.m00 = (1F - 2F * plane[0] * plane[0]);
        reflectionMat.m01 = (-2F * plane[0] * plane[1]);
        reflectionMat.m02 = (-2F * plane[0] * plane[2]);
        reflectionMat.m03 = (-2F * plane[3] * plane[0]);

        reflectionMat.m10 = (-2F * plane[1] * plane[0]);
        reflectionMat.m11 = (1F - 2F * plane[1] * plane[1]);
        reflectionMat.m12 = (-2F * plane[1] * plane[2]);
        reflectionMat.m13 = (-2F * plane[3] * plane[1]);

        reflectionMat.m20 = (-2F * plane[2] * plane[0]);
        reflectionMat.m21 = (-2F * plane[2] * plane[1]);
        reflectionMat.m22 = (1F - 2F * plane[2] * plane[2]);
        reflectionMat.m23 = (-2F * plane[3] * plane[2]);

        reflectionMat.m30 = 0F;
        reflectionMat.m31 = 0F;
        reflectionMat.m32 = 0F;
        reflectionMat.m33 = 1F;
    }

    static public void DrawFullscreenQuad(float z = 1.0f)
    {
        GL.Begin(GL.QUADS);
        GL.Vertex3(-1.0f, -1.0f, z);
        GL.Vertex3(1.0f, -1.0f, z);
        GL.Vertex3(1.0f, 1.0f, z);
        GL.Vertex3(-1.0f, 1.0f, z);

        GL.Vertex3(-1.0f, 1.0f, z);
        GL.Vertex3(1.0f, 1.0f, z);
        GL.Vertex3(1.0f, -1.0f, z);
        GL.Vertex3(-1.0f, -1.0f, z);
        GL.End();
    }
}

関連リンク:

https://github.com/unity3d-jp/unitychan-crs/blob/master/Assets/UnityChanStage/Visualizer/MirrorReflection.cs

2.2 このスクリプトの主な機能

シーン内で現在有効なカメラを取得し、反射面に従ってミラーリングし、さらに設定されたレンダリングレイヤーをフレームごとに撮影し、それを反射面に送信して表示します。内蔵の半透明反射素材効果と組み合わせて、下にある素材に重ねて使用します。

 

2.3 主な問題

最適化する前は、この反射効果は約10ms以上かかりました。主な問題は次のように挙げられます。

 

①反射効果はすべてリアルタイムの反射であり、レイヤーによって制御されます。反射効果を確認するには、このレイヤーのオブジェクトを反射レイヤーに入れるしかできません。

②反射面は、実際の反射面から独立した半透明の素材であり、性能を浪費した上に、効果を保証することもできません。

③プロジェクトキャラクターのメッシュ数は非常に多く(1キャラクターあたり約10,000メッシュ)、極限同接は9キャラクターと追加ペットがあります。独自のキャラクターレンダリングを除い、アウトラインやスライスされたシャドウなどの効果もあります。カメラで写真を撮ることによって反射が直接生成される場合、追加のパフォーマンスコストは非常に高いです。

④プロジェクトキャラクター自体のレンダリングは、法線マップ、漫画のライトマップ、およびその他の複雑なレンダリング効果を備えた5ピクセルのライトをサポートします。カメラで写真を撮ることによって反射が直接生成される場合、追加のパフォーマンスは非常に高くつきます。

⑤スクリプト自体にもいくつかの問題があります。

  • 反射カメラは、シーン内のアクティブなカメラに応じて1つずつ生成されます。
  • フレームごとに反射素材を取得し、パラメータを渡します。
  • 反射層の設定は、アーティストが使用するたびに1回設定する必要があり、設定しすぎたり、設定を見逃したりしやすい。

 

三、最適化の方向

 

上記の問題に対し、二つの最適化のルートがあります。アート効果の最適化とパフォーマンスの最適化です。

 

3.1アート効果の最適化

3.1.1 反射効果と反射面の物理的特性とを関連付ける

反射面の法線マップのサポートを導入し、反射効果にリアルな法線歪み効果を追加します。

 

方法:デフォルトの半透明の反射面を破棄します。反射マップを反射面のレンダリング素材に直接割り当て、法線効果を増やし、法線マップのxyチャネルを使用して反射マップのUVをオフセットします(ここでは法線歪みの強さやマスクなどの調整も追加されています)。

3.1.2 反射表面粗さの設定を導入する

反射効果の明瞭度を調整できます(ぼかし戦略、詳しい内容は後のパフォーマンスの最適化セクションを参照してください)。

 

方法:パフォーマンスは一応考えずに、ここでスクリプトにパラメーターを渡し、ガウスぼかしを使って粗い表面の反射ぼかし効果を実行できます。

 

3.1.3 反射深度を導入して、距離に伴う反射の存在から非存在への柔軟な遷移を実現する

 

方法:反射マップをレンダリングするとき、減衰係数を渡し、頂点から地面の高さまでの距離に応じて減衰し、反射マップのAチャネルに減衰強度を書き込みます。

 

3.2 パフォーマンスの最適化

3.2.1 概要:

パフォーマンスの最適化は、主に次の面に焦点を当てています。

 

①データ量を減らし、CPUの負担を減らします。

②DrawCallを減らし、C/GPU転送速度を上げます。

③レンダリングの複雑さを軽減し、GPUレンダリングを高速化します。

④スクリプトを最適化して、GCと繰り返しの操作を減らします。

 

上記の考慮事項に基づいて、パフォーマンス最適化は、アート側の最適化とプログラム側の最適化という2つのルートに分けられます。

 

3.3.2 アートの最適化

 

(1)動的・静的分離

反射効果を、静的オブジェクトのベイク反射と動的オブジェクトのリアルタイム反射という2つの部分に分けます。これにより、リアルタイム計算の量を大幅に減らすことができます。

 

2プリベーク

シーンに必要な反射の明瞭度に基づいて、サイズが可能な限り小さい反射マップをプリベイクします。非常にラフで不明瞭な反射シーンの場合は、シーン反射マップを共有できます。

 

または、昼と夜、雨と晴れなど、シーンのさまざまな状態の反射マップを共有します。反射色、粗さ、反射強度などの素材パラメータを調整することで、さまざまな状態効果を実現できます。

 

BoxProjection方法でCubeMapベイクされたシーンとの揃え問題を修正します。

 

(3)リアルタイム

静的およびベイク処理と同様に、シーンの反射の明瞭度に応じて、可能な限り小さな反射マップをリアルタイムで生成するように使用できます。

 

プログラムサイズ調整を開放する

 

②コード記述はサイズ配置をサポートする

 

リアルタイム反射のDrawCallを減らします。たとえば、キャラクターの構成要素を可能な限りマージすることや、より小さい部品を無視して反射を実行しないことができます。

 

リアルタイム反射のデータ量と計算量を減らします。たとえば、メッシュ数が少ないLodを使って反射をレンダリングします。反射効果を減らすには、ライトや効果の数を減らす必要があります。

 

(4)反射面最適化戦略

同じシーンにおける反射面は1つしか存在できません。

反射面の素材をハイエンド素材として扱い、アーティストに特に注目を集めさせます。

できるだけ多くのパラメーターをアーティストに公開し、できるだけ高性能のパラメーターに適応させます。

3.3.3 ターミナルの最適化

 

同じく、概要で説明したいくつかの点から最適化を行います。

 

不要なレンダリングバッチと効果を削除する

反射効果では、透明度が限られているため、アウトライン効果をかリングでき、シャドウ効果をレンダリングしないこともできます。

 

②反射レンダリング計算複雑さを軽減する

反射効果では、基本的な明暗の関係を示すだけでよいので、1つだけの平行光効果を残します。ライティングの計算も、ピクセルライティングから頂点ライティングに変更します。

 

キャラクターのレンダリングには、ベースカラーテクスチャ、法線のテクスチャ、ラフメタリックテクスチャ、ボディ素材の区別テクスチャ、ライティングテクスチャ、およびバフエフェクトテクスチャが必要です。反射では、効果はベースカラーテクスチャとバフ効果テクスチだけが必要までに簡素化します。

 

上記の最適化案を実現するには、反射レンダリングを実行するときに、動的オブジェクトの素材を置き換えてレンダリングする必要があります。利用可能な戦略は次のとおりです。

 

(1)素材を変更する

フレームごとに反射素材で置き換えレンダリングする必要があります。後処理アウトラインや定型化されたモノクロ効果などの場合は、この方法の方が適しています。ただし、キャラクターの素材とパラメータが異なる場合、この方法を使用すると、動的オブジェクトの反射リストを自分で維持する必要があり、かなり面倒です。

 

(2)LodIntコントロール

SubShaderのLodパラメーターを使って制御することです。このようにして、各素材の反射シェーダーを1つのLodに集中させて計算します。最も簡単な方法ですが、テスト後、リアルタイムLodにスイッチングするコストが多すぎるため、実用的ではありません。

 

(3)マクロスイッチ

比較的シンプルで、優れた統合性と汎用性を備え、スイッチングコストも少ないです。最初は、この最適化スキームを使用しましたが、キャラクターのマルチ素材およびマルチパスレンダリングのため、後の段階では最適化の効率はまだ十分ではありません(アウトラインとシャドウを最適化することはできません)。

 

コアコード:

 

(4)CommandBuffを使用する

制御可能性と自由度が最も高いですが、問題は(1)と同じで、自分でキャラクターリストを維持する必要があります。維持コストは高いけれども実装できます。ロジックレイヤーでキャラクター変更が実行されるときに、キャラクターリストを反射プログラムに更新して処理すれば解決できます。

 

コアコードの一部:

 

(5)CameraReplaceShaderを使用する

私たちのプロジェクトで最後に使用された方法です。利点は、置換レンダリングを実現でき、自分でキャラクターのリストを維持する必要がないことです。欠点といえは、反射レンダリングのシェーダーを書き直すことです。

 

コアスクリプトコード:

 

SetReplacementShaderコマンドについて簡単に説明します。

 

それはカメラでレンダリングを行うすべてのシェーダーです、後で設定されたreplacementTagによって置き換えます。

 

たとえば、上記のコードでは「MirrorReflect」タグを使用しているため、シェーダーでは、「MirrorReflect」タグを含むすべてのサブシェーダーが、reflectShader内の同じ「MirrorReflect」タグのサブシェーダーに置き換えられます。

 

上記の例に示すように、カメラが反射レンダリングを実行すると、同じ「MirrorReflect」タグを含むサブシェーダーを置き換えて、それに応じてレンダリングできます。

 

3.3.4 プログラムの戦略的最適化

 

(1)静的反射とリアルタイム反射を組み合わせる

反射マップのAチャネルに保存されている反射減衰値に従って、動的および静的反射ミキシングを実行するだけで済みます。

 

(2)アート調整パラメータを公開する

前述のように、パフォーマンス関連の設定の一部は、アーティストが選択できるように公開することができます。

 

(3)反射の明瞭さ

粗さの概念を導入し、ぼかしを使用して、反射マップの解像度が不十分の問題をマスクします。反射分解能をさらに下げることができます。

 

ここでのぼかし戦略は、ガウスぼかしによって引き起こされるコストを回避するためにミップマップ法を採用しています。

 

3.3.5 プログラムロジックレイヤーのパフォーマンスの最適化

 

(1)反射カメラの数を減少する

反射カメラの数は、カメラごとに生成された多反射カメラからフレームごとの位置合わせた一眼レフカメラに最適化されています。

 

(2)GCを減らす

RTの適用、カメラの生成、反射素材の取得、素材パラメータの受け渡しなど、一度に実行できる操作を、OnWillRenderObject()から削除します。

 

(3)リフレッシュレートを最適化する

リフレッシュレートの設定を増やして、フレームごとのレンダリングを構成可能なリフレッシュレートのレンダリングに変更します。現在のデフォルトは1/2リフレッシュレートです。

 

(4)反射層プログラム端末を変更できないようにする

アートのエラーを避け、構成をスピードアップします。

 

(5)切り替え戦略

反射面が見えない場合は、反射機能をオフにして、すべてのアセットを解放してください(論理レイヤー制御)。

 

(6)プラットフォームマッチング

さまざまなパフォーマンスプラットフォームに適応します。現在、ハイエンドプラットフォームでのリアルタイムリフレクションエフェクトのみを有効にしています。中規模構成プラットフォームでは、静的反射効果のみが有効になります。


UWA公式サイト:https://jp.uwa4d.com

UWA公式ブログ:https://blog.jp.uwa4d.com

UWA公式Q&Aコミュニティ(中国語注意):https://answer.uwa4d.com