凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity でシェーダを使って 20,000 人が音楽に合わせてサイリウム振ってる様子を作ってみた

はじめに

以前、Unityシェーダのエントリ(Unity のシェーダの基礎を勉強してみたのでやる気出してまとめてみた - 凹みTips)を書いた際に @yuujii さんからこんなご提案を頂きました!


面白そうなのでやってみました。

デモ

低音域・中音域・高音域を青・緑・赤にマッピングして、ボリュームに合わせて振り幅が変わり、音が大きくなると横振りから縦振りに変わります。サイリウムはシェーダで動いているので 20,000 人出してもかなり軽いです。音からテンポを検出するのは難しそうなのでやっていません。。


音楽は H/MIX GALLERY さまから「砂塵の城塞」をお借りしました。壮大です。

f:id:hecomi:20140419220924j:plain

解説

簡単にまとめると、以下の様な感じです。

  1. 外部から与えられたパラメタを元に頂点シェーダでイイ感じに振ってるように見せてくれるシェーダを書く
  2. 観客席を Blender で作成してインポート
  3. Particle System の Shape でインポートしたメッシュを適用
  4. Renderer でマテリアルに先ほどのシェーダを適用したマテリアルを設定
  5. 音楽を AudioSource.GetSpectrumData で解析して音量・周波数を取得
  6. シェーダのパラメタに解析した値を入力
シェーダについて

シェーダのコードを貼るとこんな感じです。

Shader "Custom/Cyalume" {
    Properties {
        _BaseColor ("Base Color", Color) = (0.0, 1.0, 0.0)
        _WaveFactorX("Wave Factor X", Range(0.0, 2.0)) = 0.0
        _WaveFactorZ("Wave Factor Z", Range(0.0, 2.0)) = 0.0
        _WaveCorrection("Wave Correction", float) = 0.3
        _Pitch("Wave Pitch", float) = 1.0
        _Delay("Delay by Distance", float) = 0.02
        _Bend("Bend", float) = 0.3
        _MainTex ("Base (RGB)", 2D) = "white" {}
    }
    SubShader {
        Tags {
            "Queue"           = "Transparent"
            "RenderType"      = "Transparent"
        }
        Blend SrcAlpha One
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #define PI 3.14159

            #include "UnityCG.cginc"

            uniform float _WaveFactorX;
            uniform float _WaveFactorZ;
            uniform float _WaveCorrection;
            uniform float _Pitch;
            uniform float _Delay;
            uniform float _Bend;
            uniform float _WaveFactorY;
            uniform float4 _BaseColor;
            uniform sampler2D _MainTex;

            struct v2f {
                float4 position : SV_POSITION;
                fixed4 color    : COLOR;
                float2 uv       : TEXCOORD0;
            };

            v2f vert(appdata_full v) {
                float wave   = 2 * PI * _Time.x * 1000 / 60 / _Pitch;
                float delay  = _Delay * v.vertex.z;
                float bendX  = _Bend * v.texcoord.x;
                float angleX = wave + delay + bendX;
                float bendY  = _Bend * v.texcoord.y;
                float angleY = wave + delay + bendY;
                float bendZ  = _Bend * v.texcoord.z;
                float angleZ = wave + delay + bendZ;
                float lean   = sin((v.texcoord.y + 0.5) * PI) - 1.0;

                v.vertex.x += _WaveFactorX * sin(angleX) * lean;
                v.vertex.y += (_WaveFactorX + _WaveFactorZ) * _WaveCorrection * (1.0 - pow(cos(angleX), 2.0)) * lean;
                v.vertex.z += _WaveFactorZ * sin(angleZ) * lean;

                // Parameters given to fragment shader
                v2f o;
                o.position = mul(UNITY_MATRIX_MVP, v.vertex);
                o.uv       = v.texcoord;
                o.color    = v.color;
                return o;
            }

            fixed4 frag(v2f i) : COLOR {
                fixed4 tex = tex2D(_MainTex, i.uv);
                tex.rgb *= _BaseColor * i.color.rgb;
                tex.a   *= i.color.a;
                return tex;
            }
            ENDCG
        }
    }
    Fallback "VertexLit"
}

f:id:hecomi:20140419214021p:plain

ちょっと長いですがミソは3行です。

v.vertex.x += _WaveFactorX * sin(angleX) * lean;
v.vertex.y += (_WaveFactorX + _WaveFactorZ) * _WaveCorrection * (1.0 - pow(cos(angleX), 2.0)) * lean;
v.vertex.z += _WaveFactorZ * sin(angleZ) * lean;

本当は回転させるべきだと思うのですが、手抜きで「」な形状を「」になるように、X 方向、Z 方向それぞれに対して変形しています。一番下の頂点は固定して上の頂点に行くほど線形にずらせば正方形のポリゴンが平行四辺形に変形されます。この辺りの計算については以下の記事が詳しいです:

Particle System によるパーティクルのアニメーション

これを Particle System を使って大量生産します。アニメーションするシェーダを Particle System をと組み合わせて使うイケてるアイディアは以下を参考にしています(ありがとうございます)。

Particle System ではパーティクルの発生源の形状を Shape から Sphere や Cone などで指定できますが、Mesh を指定して好きな形から発生させることができます。

f:id:hecomi:20140419214817p:plain

そこで、観客席っぽいポリゴンを Blender で作成してこれを Mesh に指定しました。

f:id:hecomi:20140419214914p:plain

そして、Renderer の Material で先ほどのシェーダを設定したマテリアルを指定します。Render Mode では処理が軽いので Billboard(常にカメラの方を向く板)を指定しています。

f:id:hecomi:20140420124501p:plain

音の解析

低音や高音に応じて効果を加えたかったので音を周波数に分解(FFT)する必要があります。これは AudioSource.GetSpectrumData() を利用すると簡単に実現できます。

上記ドキュメントのサンプルの使い方は obesolete で警告が出るので、以下のようにバッファは予め確保した使い方をしましょう。

using UnityEngine;
using System.Collections;

public class Example : MonoBehaviour {
    void Update() {
    	var spectrum = new float[1024];
        audio.GetSpectrumData(spectrum, 0, FFTWindow.BlackmanHarris);
        ...
    }
}

手抜きですが音量もここから取ってしまいます。正確には AudioSource.GetOutputData() で取った波形から計算するのが良いと思います。Unity Answers の以下のスレッドが参考になります。

シェーダにパラメタを渡す

そしてこれらをシェーダの入力値として渡します。Material の GetFloat() / SetFloat() や GetColor() / SetColor() などの Getter / Setter を通じてやりとりが出来ます。

public class CyalumeController : MonoBehaviour
{
	public Color baseColor {
		get { return renderer.material.GetColor("_BaseColor"); }
		set { renderer.material.SetColor("_BaseColor", value); }
	}

	public float waveX {
		get { return renderer.material.GetFloat("_WaveFactorX"); }
		set { renderer.material.SetFloat("_WaveFactorX", value); }
	}
	...
}

これで冒頭のデモができます。

おわりに

パーティクルと組み合わせたシェーダによるアニメーションを書いてるとちょっと数式間違えた時もキレイな感じのエフェクトになったりして楽しいですし、もっと色々な表現ができると思います。今回のように、何かに特化した表現を作りこんであげたりすれば、軽い処理で結構面白いことが色々出来るのではないでしょうか。