コンテンツへスキップ

QualiArtsengineer blog

Unity SRPとVolumeを用いた雲の表現

Unity SRPとVolumeを用いた雲の表現

4 min read

はじめに

株式会社QualiArtsのテクニカルアーティスト室に所属している我彦です。 SRP(ScriptableRenderPipeline)で雲の描画を実装し、Volumeを用いてデザイナーが作業しやすい環境を目指しました。 本記事ではその実装方法を紹介します。

GATSBY_EMPTY_ALT

概要

Unityで雲を作る際には様々な手法があります。シーン上に雲のモデルを直接配置をする手法や、Skyboxのマテリアルに設定するテクスチャに雲を直接書く手法などです。 しかし、今回の要件として、同じテクスチャを用いて夕方などの時間帯に雲の色変化をもたらしたり雲の動きの速さを部分的に変化させたりしたいという要望があり、上記の手法では、マテリアルの数やテクスチャの数が増えてしまう問題がありました。

そこでSRPを用いて雲の描画をするパイプラインを作成し、雲の色変化や速度変化に対応できるようにしました。 Volumeを用いることでデザイナーが表現を作りやすい環境を整え、高いイテレーションで雲のアセットを制作できる環境の構築を目指しました。

雲のモデルデータの作成

まずは雲の描画を行うためにモデルデータを作っていきます。 雲のモデルデータはメッシュを筒状にしたものを作成しました。 Unity上ではメッシュと雲の速度を設定できるモデルデータをScriptableObjectで作成しました。

ScriptableObjectの構造
雲のモデルデータを設定するScriptableObject

雲の制御用Volumeの作成

空の背景に合わせて雲の色を変えたり、天候状況の変化によって全体的に雲の移動速度を変更したい、という要望があり、これらはVolumeを利用して制御できます。

    [Serializable]
    public class CloudDataParameter : VolumeParameter<CloudData>
    {
        public CloudDataParameter(CloudData value)
            : base(value, false)
        {
        }
    }

    [Serializable, VolumeComponentMenu("Cloud/Volume")]
    public class CloudVolume : VolumeComponent
    {
        public CloudDataParameter cloudData = new CloudDataParameter(null);
        public FloatParameter multiplySpeed = new FloatParameter(1f);
        public FloatParameter cloudOffsetY = new FloatParameter(0f);
        public ColorParameter cloudOverrideColor = new ColorParameter(Color.white, true, true, true);
    }
Volumeの構造

雲のモデルデータを設定したり、全体の速度変更、色の乗算などの設定をVolumeから行うことができます。

雲描画パイプラインの作成

SRPではScriptableRenderPassを継承したクラスを作成し、ScriptableRendererFeatureを継承したクラスでそのパスを追加する処理を記述することで独自の描画パスを追加することができます。

Passに描画処理を記述

Passは描画処理の記述を行うところです。ScriptableRenderPassクラスのExecuteメソッドをOverrideすることでPassの描画処理の記述をします。

  public class CloudPass : ScriptableRenderPass
   {
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            var volumeStack = VolumeManager.instance.stack;
            var cloudVolume = volumeStack.GetComponent<CloudVolume>();
            DrawClouds(cmd, cloudVolume);
        }

        private void DrawClouds(CommandBuffer cmd, CloudVolume cloudVolume)
        {
            var cloudData = cloudVolume.cloudData.value;
            var currentTime = ShaderTimeManager.CurrentTime;

            foreach (var cloudLayer in cloudData.cloudLayers)
            {
                var cloudMesh = cloudLayer.mesh;

                var position = new Vector3(0f, cloudVolume.cloudOffsetY.value, 0f);

                var rotate = ((double)cloudLayer.rotateSpeed * currentTime * cloudVolume.multiplySpeed.value) % 360.0;

                var cloudMatrix = Matrix4x4.TRS(position,
                    Quaternion.AngleAxis((float)rotate, Vector3.up),
                    Vector3.one);
                
                _cloudMaterialPropertyBlock.SetColor(_CloudColor, cloudVolume.cloudOverrideColor.value);
                
                cmd.DrawMesh(cloudMesh, cloudMatrix, cloudData.cloudMaterial, 0, 0, _cloudMaterialPropertyBlock);
            }
        }
   }

メッシュの描画はCommandBuffer.DrawMesh()を用いて行い、MaterialPropertyBlockで色の乗算を行っています。メッシュの位置と角度の指定はMatrix4x4.TRS()を用いています。

ScriptableRenderFeatureにPassを追加

PassをScriptableRendererFeatureに渡すためにコンストラクタを追加します。引数としてRenderPassEventを追加します。RenderPassEventはPassの実行タイミングを設定するものです。

    public class CloudPass : ScriptableRenderPass
    {
        public CloudPass(RenderPassEvent evt)
        {
            renderPassEvent = evt;
        }
        // 略
    }

次にScriptableRenderFeatureを継承したクラスでPassを追加する処理を作成します。 今回はスカイボックスの描画計算後にCloudPassを実行したいのでRenderPassEventAfterRenderingSkyboxを指定しています

    [Serializable]
    public class DrawCloudRendererFeature : ScriptableRendererFeature
    {
        private RenderPassEvent evt;
        private CloudPass pass;
        
        public override void Create()
        {
            DisposePass();
            evt = RenderPassEvent.AfterRenderingSkybox;
            pass = new CloudPass(evt);
        }

        public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
        {
            if (pass == null) return;
            renderer.EnqueuePass(pass);
        }

        protected override void Dispose(bool disposing)
        {
            DisposePass();
            base.Dispose(disposing);
        }

        private void DisposePass()
        {
            pass?.Dispose();
            pass = null;
        }
    }

制作結果

本実装での描画結果はこちらです。

GATSBY_EMPTY_ALT

Volumeを使った雲の色乗算表現

最初に挙げたシーン上に雲のモデルを直接配置する手法や、Skyboxのマテリアルに設定するテクスチャに雲を直接書く手法だと、雲の表現の変更はコストが高い状況でした。しかし、このようにVolumeとして管理する手法に変更したことで空の表現と雲の制御を分離することができました。また、雲のモデルや乗算カラーは差し替えが可能になっているため、従来より効率的かつ少ないアセット量で多くのバリエーションを実現できるようになりました。

最後に

今回は、SRPを用いた雲の描画Passの実装や、Volumeを用いた表現の拡張をデザイナーが取り組みやすくする仕組みを紹介しました。今回は雲の描画のみでしたが、今後は空の色や太陽光の向きに合わせて表現を変えるなど、より現実的な表現ができるように研究を進めていきたいと考えています。

2023年にサイバーエージェントに新卒入社。テクニカルアーティストとして、Unityのツール開発を担当