ディゾルブエフェクトで近づくと出現するマップ [ゲーム開発ログ 2020-10-26]

2020-10-26

ディゾルブシェーダ

  • 先週はシュワシュワ出現する感じのエフェクト(ディゾルブエフェクト) を作るためにシェーダをあれこれ書いて見た目の実験していた
  • 今週はシュワシュワ感の粗さや色、消す範囲などをパラメータ化してゲームで使えるようにシェーダを整えた

近づくと出現するディゾルブエフェクト

歩いていくとマップがシュワシュワと出現して道が拓けていく感じのデモを作った:

  • こういう夢の世界みたいなマップを歩くことを主としたゲームを開発中
  • 作りたいゲームは、マップの一部や特定のオブジェクトが何も無いところに出現するような演出を入れたかったので、 そのために必要だった
    • 思い描いていたものが形にできたので満足

ディゾルブの制御方法について

  • Unity URP においては MaterialPropertyBlock を使うと SRPBatcher が切れるので使いたくない
    • というか MaterialPropertyBlock だと、パラメータを更新する際に SetPropertyBlock し直さないといけないので GameObject の数だけ走査する必要があって効率が悪い
    • マテリアルのインスタンスを複製したものに差し替えてそのパラメータをいじる感じでやりたい
  • エフェクトを一部分にだけ適用するには、オブジェクトのツリー単位で特定のマテリアルのパラメータを書き換える必要がある
  • オブジェクトのツリー単位ごとにマテリアルのインスタンスを管理してパラメータを更新するスクリプトを書く
    • 同じ種類のマテリアルは共有して使い回す
    • マテリアルの種類数ぶんだけイテレートすれば OK な感じにできる
  • なんかとても説明しづらいので今回書いたコードを貼っておく
    • 以下をディゾルブしたい GameObject のルートにアタッチして使う:
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Kakera
{
    public class DissolveEffectGenerator : MonoBehaviour
    {
        [SerializeField] Vector3 dissolveOrigin = default;
        [SerializeField] Vector3 triggerPos = default;
        [SerializeField] Transform targetTransform = null;
        [SerializeField] float triggerDistance = 3.0f;
        [SerializeField] float dissolveMax = 20f;
        [SerializeField] float dissolveTime = 5f;

        static readonly int Prop_DissolveOrigin   = Shader.PropertyToID("_DissolveOrigin");
        static readonly int Prop_DissolveAreaSize = Shader.PropertyToID("_DissolveAreaSize");
        static readonly int Prop_DissolveDistance = Shader.PropertyToID("_DissolveDistance");

        List<Material> _cachedMaterials = new List<Material>();

        enum State {
            Hide, DissolveIn, Stable
        };
        State _state = State.Hide;

        float _passedTime = 0f;

        //----------------------------------------------------------------------
        // MonoBehaviour
        //----------------------------------------------------------------------

        void Awake()
        {
            CacheMaterials();
            InitMaterials();
        }

        void Update()
        {
            if (_state == State.Stable) { return; }

            UpdateState();
            if (_state == State.DissolveIn)
            {
                UpdateShaderProp();
            }
        }

        //----------------------------------------------------------------------
        // initialize
        //----------------------------------------------------------------------

        /// <summary>
        /// 子オブジェクト群の Renderer の Material を個別のインスタンスに置き換える。
        /// 同じ種類の Material には同じインスタンスを共有させる。
        /// 生成した Material は _cachedMaterials に保持し、子オブジェクト群の
        /// シェーダパラメータを一括で変更するのに使う。
        /// </summary>
        void CacheMaterials()
        {
            Dictionary<int, Material> materialMap = new Dictionary<int, Material>();
            Renderer[] renderers = GetComponentsInChildren<Renderer>();
            foreach (var renderer in renderers)
            {
                var sharedMaterials = renderer.sharedMaterials;
                Material[] newMaterials = new Material[sharedMaterials.Length];
                for (int i = 0; i < sharedMaterials.Length; ++i)
                {
                    Material sharedMat = sharedMaterials[i];
                    Material material = GetMaterialOrCreate(sharedMat, materialMap);
                    newMaterials[i] = material;
                }
                renderer.materials = newMaterials;
            }
            _cachedMaterials = materialMap.Values.ToList();
            Debug.Log($"{_cachedMaterials.Count} Materials found in {gameObject.name}");
        }

        Material GetMaterialOrCreate(Material sharedMaterial, Dictionary<int, Material> materialMap)
        {
            int materialId = sharedMaterial.GetInstanceID();
            Material material;
            if (!materialMap.TryGetValue(materialId, out material))
            {
                Material newMaterial = Instantiate<Material>(sharedMaterial);
                materialMap.Add(materialId, newMaterial);
                return newMaterial;
            }
            return material;
        }

        void InitMaterials()
        {
            foreach (var material in _cachedMaterials)
            {
                material.SetVector(Prop_DissolveOrigin, dissolveOrigin);
                material.SetFloat(Prop_DissolveAreaSize, 999f);
                material.SetFloat(Prop_DissolveDistance, 0f);
            }
        }

        //----------------------------------------------------------------------
        // update function
        //----------------------------------------------------------------------

        void UpdateState()
        {
            if (_state != State.Hide) { return; }
            if (Vector3.Distance(targetTransform.position, triggerPos) <= triggerDistance)
            {
                _state = State.DissolveIn;
            }
        }

        void UpdateShaderProp()
        {
            _passedTime += Time.deltaTime;
            if (_passedTime >= dissolveTime)
            {
                FinishDissolve();
                return;
            }

            float dissolveDistance = (_passedTime / dissolveTime) * dissolveMax;
            foreach (var material in _cachedMaterials)
            {
                material.SetFloat(Prop_DissolveDistance, dissolveDistance);
            }
        }

        void FinishDissolve()
        {
            foreach (var material in _cachedMaterials)
            {
                material.SetFloat(Prop_DissolveAreaSize, 0f);
            }
            _state = State.Stable;
        }

    }
}