search
LoginSignup
12

More than 1 year has passed since last update.

posted at

updated at

Unityを用いたアニメーション撮影表現

アニメーション撮影とは?

 単語だけで言えばカメラワークなども含めかなり広義に捉えることができると思います。
 今回扱うのは編集の過程で用いられるエフェクトなどで、ゲーム的な見方で言えば、ポストプロセスエフェクトにあたるものです。

経緯

 先日までアニメーションRPG『カラクリショウジョの涙と終』を制作していました。
 その過程で利用した手法をこちらの動画(02:01:27頃)でプレゼンさせていただいたのですが、その内容をもう少し掘り下げてまとめたものになっております。
(動画ではワークフローも同時に説明させていただきましたが、そちらはまた別の記事にしたいと思います。)
 (2020/12/16追記 こちらにまとめました)

目次

1.パラ表現
2.カメラ表現
3.パーティクル

1. パラ表現

 パラ表現とは、画面に対しグラデーションをかけるシンプルな表現手法です。3Dで言う所のライティングに近いと思われます。
 今回は基本の直線グラデーション、もしくは円状のグラデーションを板ポリに描画し、それを重ねることで表現しました。


//乗算パラ

Shader "Custom/Para_Multiply"
{
    Properties
    {
        _Color("Tint",Color) = (1,1,1,1)
        [KeywordEnum(RADIUS,LINEAR)] _TYPE("Type",Float) = 0
    }
    SubShader
    {
        Tags
        {
            "RenderType"="Transparent"
            "Queue"="Transparent"
        }
        LOD 100

        Cull Off
        Lighting Off
        Blend Zero SrcColor //乗算ブレンド

        Pass
        {
            CGPROGRAM
            #pragma multi_compile  _TYPE_LINEAR _TYPE_RADIUS
            #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;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            fixed4 _Color;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = (0,0,0,0);

                #ifdef _TYPE_RADIUS //円形グラデーション描画
                    half distance = clamp(length(i.uv - 0.5) * 2,0,1)  ;
                    col = _Color * (1-distance);

                #elif _TYPE_LINEAR //線形グラデーション描画
                    col = _Color * i.uv.x;

                #else

                #endif

                return  col;
            }
            ENDCG
        }
    }
}

 『乗算ブレンド』と書かれている部分をBlend One Oneに書き換えることで加算ブレンドを行うように変更できます。
 今回は乗算用、加算用の二つのMaterialを用意し、スクリプトから二つの描画を切り替えることで発光、影両方でグラデーション表現を行いました。
(発光は恐らくパラ表現とは別に区分されますが、今回は実装方法の都合上同じ括りにまとめさせていただきます。)

使用例

<線形グラデーション + 乗算合成>
線形グラデーション+乗算

パラ表現 なし

スクリーンショット 2020-12-14 18.01.15.png

パラ表現 あり

スクリーンショット 2020-12-14 18.01.02.png|


<円形グラデーション + 加算合成>
円形グラデーション+加算

パラ表現なし

スクリーンショット 2020-12-14 17.57.38.png

パラ表現あり

スクリーンショット 2020-12-14 17.57.25.png


 手法だけで言えば、効果を適用したい形状のテクスチャやマップ画像を用意し、パーティクル用のShaderで板ポリに描画する方法が簡単だと思いますが、「全て一からShaderでやったらこうなった」という感じで見ていただけると幸いです。

2. カメラ表現

 今回は手書きアニメーションを用いたため、カットシーン表現において重要とされることが多い2つのエフェクトが使用できず擬似的に再現する必要がありました。

1. 被写界深度(2Dにはもちろん適用されない。奥行きを用いて配置してもいいが、程度とカメラワークによっては表現に破綻が起きる)
2. モーションブラー(キャラクターは定義された大きさの画像の中で動くため、オブジェクト自体が動かない)

被写界深度

 いわゆるピント表現です。ピントが合わないものはぼやけて表示されるため、まずは特定のオブジェクトのみぼかしをかけるといった形を取れるようにしました。
(ぼかし処理自体はこちらの記事を参考にさせていただいたため省きます。)
 今回はマップ画像を用いた発光処理を行っているため、元の絵とマップ画像両方にブラーをかけその結果を描画を行うShaderに流し込むといった形で実装しています。

スクリーンショット 2020-12-14 20.34.35.png

 また、特定のキャラクターと背景を一緒に、などまとめてぼかしをかけたい状況も発生しました。そのためGrabPassを用いてレンダリングされた結果に対してブラーをかける効果も実装しています。
ぼかし処理自体は参考にさせていただいた記事を見ていただければと思いますが、ブラーをかける形状を指定できるよう独自に処理を加えています。

//---中略---

            half4 _BlurCenterOffset; //中心位置
            half _MaskRadius; //円状にくり抜く際の半径

            half4 frag(v2f i) : SV_Target
            {

                //中略

                #ifdef Mapping_Default //全体にかかるブラー処理(省略)



                #elif Mapping_Radial //円形に繰り抜くようにかかるブラー処理

                //円の中心 + 半径からどれだけ離れているかを求める
                half distance = clamp(length(i.uv -_BlurCenterOffset) -MaskRadius,0,1) ;

                [loop]
                for (float x = -blur; x <= blur; x += 1)
                {

                /*
                  サンプリングする座標のオフセットに先ほどのdistanceをかける
                  =>より広い範囲をサンプリングするほどブラーはかかり方が大きくなる
                  =>かけるdistanceが大きいほどオフセットは大きくなる
                  =>円から距離があるほどブラーのかかり方も大きくなる
                /*

                }

                #elif Mapping_Linear //線形に強度が変わるブラー処理

                //基準とした点からどれだけ右にいるかを求める
                half distance = clamp(i.uv.x - _BlurCenterOffset.x,0,1) ;

                [loop]
                for (float x = -blur; x <= blur; x += 1)
                {

                /*
                  サンプリングする座標のオフセットに同じくdistanceをかける
                  =>以下の仕組みは円形ブラーと同じ
                /*

                }

                #endif

//---以下略--
標準ブラー

奥行きの表現などに用いることができます。
スクリーンショット 2020-12-14 21.40.46.png

円形、線形ブラー

視線誘導を行うことができます。

スクリーンショット 2020-12-14 21.42.04.png

スクリーンショット 2020-12-14 21.46.00.png

モーションブラー

 高速で動くものをカメラで捉えた際、シャッターが開いている間の動きがそのまま切り取られるためブレが生じる現象です。つまり移動の軌跡が残像として描画できれば良いということになります。
 今回は絵のオブジェクトごとに移動ブラーを用い、その強度と角度を動的に変更することで実装しました。
 ブラー実装自体はこちらの記事を参考にさせていただいております。

スクリーンショット 2020-12-14 23.37.39.png

3. パーティクル

 手軽に美麗なエフェクト表現を行うことができますが、今回手書きアニメーションと組み合わせるに当たって一つ問題がありました。
 手書きアニメーションは意図的に動きのフレームレートを落とすリミテッドアニメーションに分類されますが、これに対しパーティクルは各フレームで動きの更新が行われるフルアニメーションに属しています。
 このように動きの滑らかさの差があるため、どちらか片方の表現が画面に馴染まず浮いてしまう現象が発生しました。
 これを解決すべく、今回パーティクルを動的にリミテッド化する処理を実装しております。

ParticleController.cs

using UnityEngine;

[ExecuteAlways]//エディタでの確認用

public class ParticleController : MonoBehaviour {

    [SerializeField] ParticleSystem particle;

    [SerializeField] float currentTime;
    float beforeTime;

    [SerializeField]AnimationCurve limitedCurve;

    int interval;

    int frameCount;


    private void Update() {

        if (Mathf.Approximately(beforeTime, currentTime)) return;

        if(frameCount >= interval) {

            interval = (int)Mathf.Round(limitedCurve.Evaluate(currentTime);
            frameCount = 0;

            particle.Simulate(currentTime);

        }

        frameCount++;

        beforeTime = currentTime;

    }  


    public void SetCurrentTime(float currentTime) {

        this.currentTime = currentTime;

    }


}

 実際にゲームに使用したパーティクル周りのコードから必要な部分だけを抜粋しました。
 肝となるのはUpdate()内部のこの部分です。


        if(frameCount >= interval) {

            interval = (int)Mathf.Round(limitedCurve.Evaluate(currentTime);
            frameCount = 0;

            particle.Simulate(currentTime);

        }

        frameCount++;

 Simulate()を使い、パーティクル更新処理を全てプログラム内に移譲しています。そして更新ごとの間隔を開けてフレームレートを落としました。
 次に何フレーム間隔を開けるか(アニメ用語ではタメツメと呼ばれる部分のはずです)を指定するパラメータにはAnimationCurveを用いています。時間経過による値の変化がサポートされており、動きの緩急がつけやすいです。
(例:火花エフェクトの始まりは勢いを良くするために大胆にフレームレートを落とし、消え始めたらフレームを増やしてなめらかな動きにする)

比較は記事冒頭に載せたプレゼン動画の(02:06:24)頃で見ることができます。

※注意※

 先ほど載せたコードにはエディタでの確認ができるよう[ExecuteAlways]の記述がありますが、これを行うとUnityのSceneビューに用意されたパーティクルのプレビュー機能が使用できなくなります。
 エディタで再生したい場合はエディタ拡張を行うなど、対応をお願いいたします。

拡張例

(汚いコードですがご容赦を)

ParticleControllerEditor.cs
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(ParticleController))]
public class ParticleControllerEditor : Editor {

    bool isPlaying;

    float startedTime;


    bool isPaused = false;
    float pausedTime = 0;


    public override void OnInspectorGUI() {

        base.OnInspectorGUI();

        if (!isPlaying) {
            if (GUILayout.Button("Play")) {
                if (isPaused) {
                    startedTime = (float)EditorApplication.timeSinceStartup;
                    isPlaying = true;
                    EditorApplication.update += UpdateParticle;
                }
                else {
                    Play();
                }
            }
        }

        if (isPlaying) {
            if (GUILayout.Button("Pause")) {
                isPlaying = false;
                isPaused = true;
                pausedTime = pausedTime + (float)EditorApplication.timeSinceStartup - startedTime;
                EditorApplication.update -= UpdateParticle;
            }
        }

        if (GUILayout.Button("Restart")) {
            EditorApplication.update -= UpdateParticle;
            Play();
        }

        if (GUILayout.Button("Stop")) {
            Stop();

        }


    }


    private void OnDisable() {
        Stop();
    }


    private void Play() {
        pausedTime = 0;
        isPlaying = true;
        isPaused = false;
        startedTime = (float)EditorApplication.timeSinceStartup;
        EditorApplication.update += UpdateParticle;
    }


    private void Stop() {
        pausedTime = 0;
        isPaused = false;
        isPlaying = false;
        (target as ParticleController).SetCurrentTime(0);
        EditorApplication.update -= UpdateParticle;
    }


    private void UpdateParticle() {
        if (!isPlaying) return;

        if (isPaused) {
            (target as ParticleController).SetCurrentTime(pausedTime + (float)EditorApplication.timeSinceStartup - startedTime);
        }
        else {
            (target as ParticleController).SetCurrentTime((float)EditorApplication.timeSinceStartup - startedTime);
        }


    }


}

 パーティクルの再生、一時停止、頭出し、停止をInspectorから行うことができるようになる拡張です。

まとめ

 今回扱った内容は全て『手書きアニメーションをどこまでUnity上で豪華に魅せられるか』という点に重きを置いたものですので、多くの方のためになる内容ではないかもしれませんが、少しでも参考になる部分がありましたら幸いです。

参考サイト



Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
12