Unity サウンドと同期してぷるぷるするエフェクト


MagicaVoxel で作ったボクセルユニティちゃんに、曲と同期してぷるぷるするポストエフェクトをかけてみました。
ボクセルユニティちゃんは、MagicaVoxel で適当に作成して、Blender(Rigify)でリギングしたものを使用しています。


(公開時のキャプチャ動画はエンコードが変で音と絵が全く同期していなかったため再アップしました)

今回の制作手順は大きく分けて三段階です。

1. ポストエフェクト作成
2. サウンドと連携させてポストエフェクトを動かす
3. エフェクトをかけるオブジェクトとかけないオブジェクトに分ける

では、手順を見ていきましょう。

1. ポストエフェクト作成

今回作成したポストエフェクトは、_MainTex からの tex2D の RGB成分を、上下左右にずらしてぷるぷる震えるように見せるだけの単純なエフェクトです。
エフェクトのソースはこんな感じ。

・スクリプト

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ColorOfset : MonoBehaviour {

	private Material _material;
	protected string _shaderName = "ImageEffect/ColorOfset";

	[SerializeField]
	[Range(1, 1000)]
	public float speed;
	[SerializeField]
	[Range(0, 1)]
	public float powerA;
	[SerializeField]
	[Range(0, 1)]
	public float powerB;
	[SerializeField]
	[Range(0, 1)]
	public float powerC;


	protected virtual void Awake()
	{
		Shader shader = Shader.Find(_shaderName);
		_material = new Material(shader);
	}

	void OnRenderImage(RenderTexture source, RenderTexture destination)
	{
		UpdateMaterial();

		Graphics.Blit(source, destination, _material);
	}

	void UpdateMaterial()
	{
		_material.SetFloat("_Speed", speed);
		_material.SetFloat("_PowerA", powerA);
		_material.SetFloat("_PowerB", powerB);
		_material.SetFloat("_PowerC", powerC);
	}
}

・シェーダ

Shader "ImageEffect/ColorOfset"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
		_Speed ("Speed", Range(1, 1000)) = 500
		_PowerA ("PowerA", Range(0, 1)) = 0
		_PowerB ("PowerB", Range(0, 1)) = 0
		_PowerC ("PowerC", Range(0, 1)) = 0
	}
	SubShader
	{
		// No culling or depth
		Cull Off ZWrite Off ZTest Always

		Pass
		{
			CGPROGRAM
			#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;
			};

			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
				o.uv = v.uv;
				return o;
			}


			//
			float rand(float2 co) {
				return frac(sin(dot(co.xy, float2(12.9898, 78.233))) * 43758.5453);
			}

			//
			sampler2D _MainTex;
			float _Speed;
			float _PowerA;
			float _PowerB;
			float _PowerC;

			fixed4 frag (v2f i) : SV_Target
			{
				fixed4 tex = tex2D(_MainTex, i.uv);

				float3 col;
				float time = _Time * _Speed;

				float ran = rand(float2(1, 10));

				col.r = tex2D(_MainTex, i.uv).r;
				col.g = tex2D(_MainTex, i.uv - float2(cos(time) * .05 * _PowerA * rand(float2(-50, 50)), sin(time) * .035) * _PowerC * rand(float2(-25, 25))).g;
				col.b = tex2D(_MainTex, i.uv - float2(sin(time) * .06 * _PowerB * rand(float2(-50, 50)), cos(time) * .025) * _PowerC * rand(float2(-25, 25))).b;

				return float4(col, 1);
			}
			ENDCG
		}
	}
}

2. サウンドと連携させてポストエフェクトを動かす

ポストエフェクトができたら、次は音声に合わせてエフェクトを動かします。
まず、AudioSource(この例ではユニティちゃんが歌う曲)からフーリエ変換によって周波数成分(スペクトラム)を取り出します。

今回は音の周波数成分を三つに分けてエフェクトのパラメータをして使用したかったので、ここ http://tips.hecomi.com/entry/2014/11/11/021147 を参考にして、取り出したスペクトラムを周波数帯で3つ(high, mid, low)に分けました。

で、スペクトラムから得られた3つの値を、先ほど作成したポストエフェクトで動く値に計算しなおして、エフェクトコンポーネントの public 変数に渡したら、サウンドから取り出したスペクトラムに応じて動くエフェクトの完成です。

// スペクトラムを取得
var spectrum = audioSource.GetSpectrumData(resolution, 0, FFTWindow.BlackmanHarris);
var deltaFreq = AudioSettings.outputSampleRate / resolution;

// スペクトラムから取り出した値を格納する変数
float low = 0f;
float mid = 0f;
float high = 0f;

// 周波数帯に応じて値を取り出す
for (var i = 0; i < resolution; ++i) {
	var freq = deltaFreq * i;
	if      (freq <= lowFreqThreshold)  low  += spectrum[i];
	else if (freq <= midFreqThreshold)  mid  += spectrum[i];
	else if (freq <= highFreqThreshold) high += spectrum[i];
}

// 値を調整
low  *= lowEnhance;
mid  *= midEnhance;
high *= highEnhance;
low  *= .15f;
mid  *= .15f;
high *= .15f;

// ポストエフェクト側の変数に値を渡す
imageEffest.powerA = high;
imageEffest.powerB = mid;
imageEffest.powerC = low;

3. エフェクトをかけるオブジェクトとかけないオブジェクトに分ける

これで、サウンドと連動したエフェクトはできましたが、これをメインカメラに適用させると画面全体にエフェクトがかかります。
全体にエフェクトをかける目的ならこれで終了なのですが、今回はキャラクターのみにエフェクトをかけたかったので、あと一手間必要になりました。

まず、エフェクトをかけたいオブジェクトだけを別レイヤーに分けます。
今回は Charactors というレイヤーにしました。

次に、カメラをもうひとつ追加します。名前は Charactor Camera にしました。このキャラ用カメラに先ほど作成したポストエフェクトを適用します。
その次に、Charactor Camera のカリングマスク設定を Charactors のみに変更して Depth を 0 に、メインカメラの Depth を 1 にして、それぞれのカメラのクリアフラグを Don’t Clear にします。

このままでは2つあるカメラのクリアフラグがどちらも Don’t Clear なのに背景(SkyBox)がないため、とてつもなく変な表示になってしまうため、Depth -1 に背景のみ(カリングマスク Nothing)のカメラを追加します。

これでやっとまともに表示できるようになったのですが、メインカメラとキャラ用カメラ双方で Charactors レイヤーを表示するようにしているため、キャラクターがダブって表示されてしまいます。
なので、メインカメラのカリングマスクから Charactors を外すと…
キャラクターの影はメインカメラで表示している床オブジェクトに投影されるため、メインカメラにキャラクターが映っていないと影も同時に消えてしまいます。
これではさすがにかっこ悪すぎるので、下記のスクリプトをキャラ用カメラにアタッチして、エフェクト対象のキャラクターをメインカメラでレンダリングするときだけ ShadowCastingMode を ShadowsOnly(影のみレンダリング)となるようにします。

using UnityEngine;

public class ShadowOnly : MonoBehaviour {

	public GameObject renderTarget;

	protected Renderer[] _rendererComponents;


	// Use this for initialization
	void Start () {

		_rendererComponents = renderTarget.GetComponentsInChildren<Renderer>();
	
	}


	// 自分(このスクリプトがアタッチされたカメラ)用のレンダラー設定
	void OnPreRender()
	{
		foreach (var renderer in _rendererComponents) {
			renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.On;
		}
	}

	// 自分よりあと(このスクリプトがアタッチされたカメラ以外)用のレンダラー設定
	void OnPostRender()
	{
		foreach (var renderer in _rendererComponents) {
			renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.ShadowsOnly;
		}
	}
}

これでやっと、キャラクターにだけエフェクトがかかった状態になりました。
ってこれ、地味にめんどくさすぎる… なんかもっと簡単なやり方があると思うんですが…

 
今回参考にさせていただいた記事 : http://tips.hecomi.com/entry/2014/11/11/021147

—- 2017.03.10 追記 —-

ぷるぷるエフェクトを応用した例。
惑星に衝突した時に鳴るサウンドのスペクトラムを利用して画面を揺らすエフェクトを発生させてみました。

  

この作品はユニティちゃんライセンス条項の元に提供されています。