シェーダーを活用した3Dライブ演出のアップデート
~『ラブライブ!スクールアイドルフェスティバル ALL STARS』(スクスタ)の開発事例~
©2013 プロジェクトラブライブ! ©2017 プロジェクトラブライブ!サンシャイン!! ©2020 プロジェクトラブライブ!虹ヶ咲学園スクールアイドル同好会 ©KLabGames ©SUNRISE ©bushiroad
KLab株式会社 細田 翔
2
2015年にKLab株式会社へ入社。


『ラブライブ!スクールアイドルフェスティバル ALL STARS』の3Dグラフィックス開発を担当。


3Dライブの演出機能の実装、描画のパフォーマンス最適化を行う。


シェーダーやレイマーチングによる映像制作やデモシーン制作が趣味。


細田 翔

グラフィックスエンジニア / テクニカルアーティスト


技術統括部

KLab株式会社

画像

3
『ラブライブ!スクールアイドルフェスティバル ALL STARS』

(スクスタ)について

4
5
● Unity 2018.4 LTS

● Unity Timeline

○ プロジェクト専用のカスタムトラックが多数

● Post Processing Stack v1(軽量化して利用)

● Graphics API

○ Android: OpenGL ES 3.0

○ iOS: Metal

● Forward Rendering(built-in render pipeline)

開発情報

6
発表タイトルの「アップデート」とは?

シェーダーを活用した3Dライブ演出のアップデート

~『ラブライブ!スクールアイドルフェスティバル ALL STARS』(スクスタ)の開発事例~

発表タイトルに込めたアップデートの2つの意味



1. 2周年を迎えたスクスタのライブ演出のアップデート

2. 世の中のシェーダーの認識をアップデート
7
発表の目的:シェーダーの認識をアップデート🔄

よくある認識
「陰影処理」専用の
GPU用のプログラム
【画像出典】
Phong shading - Wikipedia
実際できること
「陰影処理」に留まらない可能性の塊
アニメーション計算・負荷削減
蝶の羽ばたき 扇子の軌跡
8
発表トピック

蝶の羽ばたき
 扇子の軌跡
 風で揺れ動く旗

ステージの

スモーク

潜れる雲

+α
関連セッション紹介



プロファイリング

9
発表トピック

蝶の羽ばたき
 扇子の軌跡
 風で揺れ動く旗

ステージの

スモーク

潜れる雲

+α
関連セッション紹介



プロファイリング

10
蝶の羽ばたきシェーダー利用楽曲



『未体験HORIZON』

11
12
蝶シェーダーの概要

蝶の移動の計算
ParticleSystem
設定項目が多い
制作だけで動きを細かく調整🥰
蝶の羽ばたきの計算
頂点シェーダー
スキニングをGPUの並列計算に
CPU負荷を軽減🥰
制作効率と負荷削減のバランスを両立🥰

13
1匹の蝶を羽ばたかせる

14
1匹の蝶を羽ばたかせる

Meshデータ

● 6頂点の板ポリ



動き

● 頂点シェーダーで

端の頂点●を回転

🦋
15
1匹の蝶を羽ばたかせる

Meshデータ

● 6頂点の板ポリ



動き

● 頂点シェーダーで

端の頂点●を回転

// localは頂点座標
float3 local = v.vertex;
// 羽ばたきの回転の角度を計算
float flap = _FlapIntensity * sin(_FlapSpeed * _TIME);
// Z軸を中心に回転
local.xy = mul(rotate(flap * sign(local.x)), local.xy);
🦋
16
1匹の蝶を羽ばたかせる

Meshデータ

● 6頂点の板ポリ



動き

● 頂点シェーダーで

端の頂点●を回転

// localは頂点座標
float3 local = v.vertex;
// 羽ばたきの回転の角度を計算
float flap = _FlapIntensity * sin(_FlapSpeed * _TIME);
// Z軸を中心に回転
local.xy = mul(rotate(flap * sign(local.x)), local.xy);
🦋
17
1匹の蝶を羽ばたかせる

Meshデータ

● 6頂点の板ポリ



動き

● 頂点シェーダーで

端の頂点●を回転

// localは頂点座標
float3 local = v.vertex;
// 羽ばたきの回転の角度を計算
float flap = _FlapIntensity * sin(_FlapSpeed * _TIME);
// Z軸を中心に回転
local.xy = mul(rotate(flap * sign(local.x)), local.xy);
🦋
18
1匹の蝶を羽ばたかせる

Meshデータ

● 6頂点の板ポリ



動き

● 頂点シェーダーで

端の頂点●を回転

🦋
// localは頂点座標
float3 local = v.vertex;
// 羽ばたきの回転の角度を計算
float flap = _FlapIntensity * sin(_FlapSpeed * _TIME);
// Z軸を中心に回転
local.xy = mul(rotate(flap * sign(local.x)), local.xy);
19
1匹の蝶を羽ばたかせる

Meshデータ

● 6頂点の板ポリ



動き

● 頂点シェーダーで

端の頂点●を回転

🦋
// localは頂点座標
float3 local = v.vertex;
// 羽ばたきの回転の角度を計算
float flap = _FlapIntensity * sin(_FlapSpeed * _TIME);
// Z軸を中心に回転
local.xy = mul(rotate(flap * sign(local.x)), local.xy);
角度から2Dの回転行列を生成
20
1匹の蝶を羽ばたかせる

Meshデータ

● 6頂点の板ポリ



動き

● 頂点シェーダーで

端の頂点●を回転

🦋
// localは頂点座標
float3 local = v.vertex;
// 羽ばたきの回転の角度を計算
float flap = _FlapIntensity * sin(_FlapSpeed * _TIME);
// Z軸を中心に回転
local.xy = mul(rotate(flap * sign(local.x)), local.xy);
左右で符号を反転
21
蝶を複数に拡張したい

蝶を複数に拡張したい
ParticleSystemと組み合わせよう!
22
ParticleSystemに蝶シェーダーを適用!

23
ParticleSystemに蝶シェーダーを適用!

何これ!?🤯
24
壊れた原因

● ドローコールのダイナミックバッチング

25
ParticleSystemのダイナミックバッチング

● ParticleSystemのドローコールを減らす最適化

● 複数のパーティクルを1つのMeshに結合

26
ParticleSystemのダイナミックバッチング

ParitcleSystemのダイナミックバッチング

頂点シェーダーによるMesh変形に影響あり🥺
ドローコール削減になるので嬉しいが…

27
ローカルな頂点座標が欲しい

蝶を独立して羽ばたかせるためには

Mesh結合前のローカルな頂点座標が必要

パーティクルの中心座標 = Custom Vertex Streamsから取得可能!
※Meshが回転しないなら

ローカルな頂点座標 = 結合後の頂点座標 - パーティクルの中心座標

28
Custom Vertex Streamsとは

● UnityのParticleSystemの機能

○ 頂点情報に埋め込むデータを自由にカスタマイズ

○ https://docs.unity3d.com/ja/2018.4/Manual/PartSysVertexStreams.html


シェーダーに自由なデータを送れる!
29
ParticleSystemの設定

● Custom Vertex Streams

を有効化

● Center(パーティクルの中心座標)を頂点
情報に埋め込み

30
ParticleSystemの設定

パーティクルのMeshの回転をさせない
Render Alignment: World

31
シェーダー修正:パーティクル対応

// localは頂点座標
float3 local = v.vertex;
// 羽ばたきの回転の角度を計算
float flap = _FlapIntensity * sin(_FlapSpeed * _TIME);
// Z軸を中心に回転
local.xy = mul(rotate(flap * sign(local.x)), local.xy);
// localは頂点座標
float3 local = v.vertex - center;
// 羽ばたきの回転の角度を計算
float flap = _FlapIntensity * sin(_FlapSpeed * _TIME);
// Z軸を中心に回転
local.xy = mul(rotate(flap * sign(local.x)), local.xy);
対応前
 対応後

32
パーティクルの中心座標を渡す

33
別の解決方法: GPUインスタンシング

● GPUインスタンシングを有効化

○ ダイナミックバッチング無効化 = Meshの結合がされない

34
別の解決方法: GPUインスタンシング

● GPUインスタンシング

○ 同じMeshを大量に描画する機能

○ Custom Vertex Streamsと共存可能

■ 任意の構造体をInstanceDataにできる

○ シェーダーの対応は必要(マニュアルが詳細)

■ https://docs.unity3d.com/ja/2018.4/Manual/PartSysInstancing.html

Unityマニュアル

35
別の解決方法: GPUインスタンシング

● GPUインスタンシングを有効化

○ パーティクルの中心のワールド座標をモデル行列から参照可能

float3 worldPos = unity_ObjectToWorld._14_24_34;
36
別の解決方法: GPUインスタンシング

● 2種類のParticleSystemのモード

ダイナミックバッチング GPUインスタンシング
グラフィックスAPI OpenGL ES 2.0 以上
ほぼすべての端末をサポート
OpenGL ES 3.0 以上
低スペック端末は切り捨て
パフォーマンス
※パーティクル数が多いケース
性能が低い傾向 性能が高い傾向
OpenGL ES 3.0以上をターゲット端末にするなら

GPUインスタンシングを推奨
37
別の解決方法: GPUインスタンシング

● スクスタの場合

○ 保守的にダイナミックバッチングを選択



○ 理由

■ パーティクル数はMAXで100程度

■ パフォーマンスの差は許容範囲

■ 海外圏にもリリースしている

■ OpenGL ES 3.0を前提とはしていたが、

OpenGL ES 2.0の低スペック端末も保守的にサポート

38
パーティクルの中心座標を渡す

正しく羽ばたいた!😄
動きが全部同じなので違和感🤨
39
乱数で動きをランダムにする

40
乱数で動きをランダムにする

Custom Vertex Streamsに乱数を追加
41
ParticleSystemの設定

StableRandomを

頂点情報に埋め込み



パーティクルの粒ごとの固定のランダム値

[余談] 毎フレーム変化するランダム値(VaryingRandom)もある
42
シェーダー修正:ランダム対応

// localは頂点座標
float3 local = v.vertex - center;
// 羽ばたきの回転の角度を計算
float flap = _FlapIntensity * sin(_FlapSpeed * _TIME);
// Z軸を中心に回転
local.xy = mul(rotate(flap * sign(local.x)), local.xy);
// localは頂点座標
float3 local = v.vertex - center;
// 羽ばたきの回転の角度を計算
float flap = _FlapIntensity *
lerp(1, random.x, _RandomFlapIntensity) *
sin(_FlapSpeed * (_TIME + _RandomFlapDelay * random.z));
// Z軸を中心に回転
local.xy = mul(rotate(flap * sign(local.x)), local.xy);
対応前
 対応後

43
シェーダー修正:ランダム対応

// localは頂点座標
float3 local = v.vertex - center;
// 羽ばたきの回転の角度を計算
float flap = _FlapIntensity * sin(_FlapSpeed * _TIME);
// Z軸を中心に回転
local.xy = mul(rotate(flap * sign(local.x)), local.xy);
// localは頂点座標
float3 local = v.vertex - center;
// 羽ばたきの回転の角度を計算
float flap = _FlapIntensity *
lerp(1, random.x, _RandomFlapIntensity) *
sin(_FlapSpeed * (_TIME + _RandomFlapDelay * random.z));
// Z軸を中心に回転
local.xy = mul(rotate(flap * sign(local.x)), local.xy);
対応前
 対応後

振幅のランダム
44
シェーダー修正:ランダム対応

// localは頂点座標
float3 local = v.vertex - center;
// 羽ばたきの回転の角度を計算
float flap = _FlapIntensity * sin(_FlapSpeed * _TIME);
// Z軸を中心に回転
local.xy = mul(rotate(flap * sign(local.x)), local.xy);
// localは頂点座標
float3 local = v.vertex - center;
// 羽ばたきの回転の角度を計算
float flap = _FlapIntensity *
lerp(1, random.x, _RandomFlapIntensity) *
sin(_FlapSpeed * (_TIME + _RandomFlapDelay * random.z));
// Z軸を中心に回転
local.xy = mul(rotate(flap * sign(local.x)), local.xy);
対応前
 対応後

振幅のランダム
位相(タイミング)ずら
しのランダム
45
シェーダー修正:ランダム対応

// 平行移動を計算します
float3 move = _RandomMove.xyz * fbm(87034.0f * random.yzw, _TIME * _RandomMove.w);
コード追加

ゆらゆらとした動きのカーブ
46
fBM(fractional Brownian motion)

● 日本語では「非整数ブラウン運動」

float fbm(float x, float t)
{
return sin(x + t) + 0.5 * sin(2.0 * x + t) + 0.25 * sin(4.0 * x + t);
}
fBM = 振幅と周波数を変えた波形の重ね合わせ(足し算)
波形1 波形2 波形3
47
fBM(fractional Brownian motion)

● sin と fBMの比較

sin(t) fbm(t)
48
乱数で動きをランダムにする

本物の蝶らしい動きになった!😄
でも蝶の向きが全部同じ🤨
49
50
Velocity(パーティクルの速度ベクトル)

をCustom Vertex Streamsに追加
蝶の向きをVelocityに回転
51
蝶の向きをVelocityに回転したい🤔

いい感じの回転行列の生成💡
52
回転行列とは何だったのか

回転行列 = 回転後の空間の基底ベクトルを並べたもの

right
up
forward
列優先の行列
53
回転行列とは何だったのか

回転行列 = 回転後の空間の基底ベクトルを並べたもの

right
up
forward
列優先の行列
54
回転行列とは何だったのか

回転行列 = 回転後の空間の基底ベクトルを並べたもの

right
up
forward
列優先の行列
55
回転行列とは何だったのか

回転行列 = 回転後の空間の基底ベクトルを並べたもの

r
i
g
h
t
u
p
f
o
r
w
a
r
d
r
i
g
h
t
u
p
f
o
r
w
a
r
d
right
up
forward
列優先の行列
どれだけ回転して
基底ベクトルの値が変化しても
回転行列の定義は変わらない
56
シェーダー修正:向き対応

// 回転の計算をします
float3 up = float3(0.0, 1.0, 0.0);
up.xz += fbm(87034.0f * random.xy, _TIME * _RandomUp.w) * _RandomUp.xz;
up = normalize(up);
up = mul((float3x3)unity_ObjectToWorld, up);
// 姿勢行列を生成します
float3 worldPos = mul(unity_ObjectToWorld, float4(center + move, 1.0));
float3 forward = normalize(velocity);
float3 right = normalize(cross(forward, up));
up = normalize(cross(right, forward));
float4x4 mat = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
mat._m00_m10_m20 = right;
mat._m01_m11_m21 = up;
mat._m02_m12_m22 = forward;
mat._m03_m13_m23 = worldPos;
57
シェーダー修正:向き対応

// 回転の計算をします
float3 up = float3(0.0, 1.0, 0.0);
up.xz += fbm(87034.0f * random.xy, _TIME * _RandomUp.w) * _RandomUp.xz;
up = normalize(up);
up = mul((float3x3)unity_ObjectToWorld, up);
// 姿勢行列を生成します
float3 worldPos = mul(unity_ObjectToWorld, float4(center + move, 1.0));
float3 forward = normalize(velocity);
float3 right = normalize(cross(forward, up));
up = normalize(cross(right, forward));
float4x4 mat = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
mat._m00_m10_m20 = right;
mat._m01_m11_m21 = up;
mat._m02_m12_m22 = forward;
mat._m03_m13_m23 = worldPos;
回転行列を
頂点シェーダーで生成
58
シェーダー修正:向き対応

// 回転の計算をします
float3 up = float3(0.0, 1.0, 0.0);
up.xz += fbm(87034.0f * random.xy, _TIME * _RandomUp.w) * _RandomUp.xz;
up = normalize(up);
up = mul((float3x3)unity_ObjectToWorld, up);
// 姿勢行列を生成します
float3 worldPos = mul(unity_ObjectToWorld, float4(center + move, 1.0));
float3 forward = normalize(velocity);
float3 right = normalize(cross(forward, up));
up = normalize(cross(right, forward));
float4x4 mat = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
mat._m00_m10_m20 = right;
mat._m01_m11_m21 = up;
mat._m02_m12_m22 = forward;
mat._m03_m13_m23 = worldPos;
回転行列を
頂点シェーダーで生成
forwardベクトルを
velocity に設定
59
シェーダー修正:向き対応

// 回転の計算をします
float3 up = float3(0.0, 1.0, 0.0);
up.xz += fbm(87034.0f * random.xy, _TIME * _RandomUp.w) * _RandomUp.xz;
up = normalize(up);
up = mul((float3x3)unity_ObjectToWorld, up);
// 姿勢行列を生成します
float3 worldPos = mul(unity_ObjectToWorld, float4(center + move, 1.0));
float3 forward = normalize(velocity);
float3 right = normalize(cross(forward, up));
up = normalize(cross(right, forward));
float4x4 mat = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
mat._m00_m10_m20 = right;
mat._m01_m11_m21 = up;
mat._m02_m12_m22 = forward;
mat._m03_m13_m23 = worldPos;
回転行列を
頂点シェーダーで生成
forwardベクトルを
velocity に設定
Y-upベクトルに乱数を加えて、
ゆらゆらとした回転
60
シェーダー修正:向き対応

// 回転の計算をします
float3 up = float3(0.0, 1.0, 0.0);
up.xz += fbm(87034.0f * random.xy, _TIME * _RandomUp.w) * _RandomUp.xz;
up = normalize(up);
up = mul((float3x3)unity_ObjectToWorld, up);
// 姿勢行列を生成します
float3 worldPos = mul(unity_ObjectToWorld, float4(center + move, 1.0));
float3 forward = normalize(velocity);
float3 right = normalize(cross(forward, up));
up = normalize(cross(right, forward));
float4x4 mat = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
mat._m00_m10_m20 = right;
mat._m01_m11_m21 = up;
mat._m02_m12_m22 = forward;
mat._m03_m13_m23 = worldPos;
回転行列を
頂点シェーダーで生成
forwardベクトルを
velocity に設定
Y-upベクトルに乱数を加えて、
ゆらゆらとした回転
rightベクトルは
upとforwardの両方に直交
するので、外積で求まる
upベクトルを再計算
61
シェーダー修正:向き対応

// 回転の計算をします
float3 up = float3(0.0, 1.0, 0.0);
up.xz += fbm(87034.0f * random.xy, _TIME * _RandomUp.w) * _RandomUp.xz;
up = normalize(up);
up = mul((float3x3)unity_ObjectToWorld, up);
// 姿勢行列を生成します
float3 worldPos = mul(unity_ObjectToWorld, float4(center + move, 1.0));
float3 forward = normalize(velocity);
float3 right = normalize(cross(forward, up));
up = normalize(cross(right, forward));
float4x4 mat = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
mat._m00_m10_m20 = right;
mat._m01_m11_m21 = up;
mat._m02_m12_m22 = forward;
mat._m03_m13_m23 = worldPos;
基底ベクトル
up, forward, right
を並べて行列を生成
62
シェーダー修正:カラフル対応

頂点カラーに対応

ParticleSystemによる色のランプ指定・複数指定が可能!

63
シェーダー修正:カラフル対応

● GPUインスタンシング対応の注意点

○ colorはuint(32bit整数)にpackingされてGPUに渡される

○ vertInstancingColor関数でunpackingが必要

■ UnityStandardParticleInstancing.cginc で定義

void vertInstancingColor(inout fixed4 color)
{
#ifndef UNITY_PARTICLE_INSTANCE_DATA_NO_COLOR
UNITY_PARTICLE_INSTANCE_DATA data = unity_ParticleInstanceData[unity_InstanceID];
color = lerp(fixed4(1.0f, 1.0f, 1.0f, 1.0f), color, unity_ParticleUseMeshColors);
color *= float4(data.color & 255, (data.color >> 8) & 255, (data.color >> 16) & 255, (data.color >> 24) & 255) * (1.0f / 255);
#endif
}
ビットシフト &
ビットマスクで
unpacking
64
マテリアルのプロパティ

● Flap Intensity: 羽ばたきの最大角度
● Flap Speed: 羽ばたきの速度
● Random Flap Intensity:
○ 羽ばたきの最大角度のバラバラ度
● Random Flap Delay:
○ 羽ばたきのタイミング(位相)のバラバラ度
● Random Move
○ 小刻みな平行移動
○ XYZ: 移動量, W: 移動の速度
● Random Up
○ 小刻みな揺れ(回転)
○ XZ: 回転の向き, W: 回転の速度

たくさんのプロパティにより
動きを細かく調整可能!
65
GPUの負荷計測

● iPhone 7 Plus
● 検証シーン
○ Skybox
○ 蝶シェーダー(パーティクル数 : 150)
○ Generalシェーダー(パーティクル数 : 150)
■ グレアのエフェクト用
■ エフェクトの汎用シェーダー
● Xcode上の結果
○ Device(GPU稼働率): 16%
○ Skybox: 1.22 ms
○ 蝶シェーダー: 0.06 ms
○ Generalシェーダー: 0.19 ms

とても軽い!
66
蝶シェーダーまとめ

● Custom Vertex Streams

○ 任意の情報を頂点情報に埋め込める

○ シェーダー芸(シェーダー活用)で大活躍!



● 利用したCustom Vertex Streams一覧

○ Center(Particleの中心位置)

■ パーティクル対応

○ StableRandom(乱数)

■ ランダム対応

○ Velocity(速度)

■ 回転対応

67
発表トピック

蝶の羽ばたき
 扇子の軌跡
 風で揺れ動く旗

ステージの

スモーク

潜れる雲

+α
関連セッション紹介



プロファイリング

68
扇子の軌跡のトレイルシェーダー利用楽曲



『Angelic Angel』

69
70
リアルタイムのトレイル生成

トレイル(扇子の軌跡)はリアルタイムに毎フレーム生成

ポリゴンの
カクカク感が目立つ😥
71
カクカク感を補間で解決

補間OFF

ポリゴンのカクカク感がある

補間ON

とても滑らか

72
テッセレーションとは?

Mesh分割し、頂点を補間 Mesh分割し、頂点を補間
【画像出典】History of hardware tessellation - RasterGrid
73
テッセレーションとは?

Mesh分割し、頂点を補間
【画像出典】History of hardware tessellation - RasterGrid
Mesh分割し、頂点を補間
74
テッセレーションとは?

【画像出典】History of hardware tessellation - RasterGrid
Mesh分割し、頂点を補間 Mesh分割し、頂点を補間
75
テッセレーション

OpenGL ES 3.2から

テッセレーションシェーダーが利用可能

GPUのパイプラインに
テッセレーションの
専用機能がある
【画像出典】History of hardware tessellation - RasterGrid
76
モバイルでもテッセレーションをしたい

頂点シェーダーで

テッセレーションの補間を実装して解決😊

OpenGL ES 3.2以上の分布は62%😰

2020年8月 Android端末

https://developer.android.com/about/dashboards?hl=ja

77
テッセレーションの詳細

Hull
Shader
Tessellator
Domain
Shader
Geometry
Shader
Vertex
Shader
Hull
Shader
Tessellator
Domain
Shader
Geometry
Shader
78
テッセレーションの詳細

テセレーションには3つのステージがある
Hull
Shader
Tessellator
Domain
Shader
Geometry
Shader
Vertex
Shader
Hull
Shader
Tessellator
Domain
Shader
Geometry
Shader
79
Vertex
Shader
Hull
Shader
Tessellator
Domain
Shader
Geometry
Shader
テッセレーションの詳細

ハルシェーダー
● どのようにポリゴンを分割するか を決定
○ ポリゴンを分割する密度
○ 曲面の制御点
80
テッセレーションの詳細

テッセレーター
ポリゴンを分割する固定処理
Vertex
Shader
Hull
Shader
Tessellator
Domain
Shader
Geometry
Shader
81
Vertex
Shader
Hull
Shader
Tessellator
Domain
Shader
Geometry
Shader
テッセレーションの詳細

ドメインシェーダー
● 分割されたポリゴンの頂点座標を計算
(制御点から座標を補間)
82
Vertex
Shader
Hull
Shader
Tessellator
Domain
Shader
Geometry
Shader
こうすればOpenGL ES 2.0でも実装可能

ポリゴンの分割は事前処理
ドメインシェーダー相当の
頂点座標の補間を
頂点シェーダーで実装
83
頂点シェーダーで補間

擬似的にテッセレーションを再現

● メッシュの分割は事前処理

● 頂点座標の補間を頂点シェーダー

モバイルのほぼ
全機種で使える😊
84
事前処理:板ポリを細かく分割

U方向に細かく分割

● この画像では20分割(見やすさ重視) 

● 実際は180分割

V方向は分割なし

85
頂点シェーダーで補間

頂点シェーダーで変形
板ポリ
 トレイル

86
補間の代表的なアルゴリズム

線形補間
 ベジェ曲線
 Catmull-Rom Spline 

【画像出典】 

ベジェ曲線で描く! 

かんたんイラスト制作の方法 

【画像出典】
Centripetal Catmull–Rom spline - Wikipedia
【画像出典】 

Linear interpolation - Wikipedia 



87
補間の代表的なアルゴリズム

線形補間
 ベジェ曲線
 Catmull-Rom Spline 

ポリゴンの頂点の場合は
まったく意味がない😕
補完後の曲線は
一部の制御点を通らない
😕
トレイルの用途に最適😍
88
補間 - Catmull-Rom Spline

● 4つの制御点

● 3次の多項式(t^3)

補間後の曲線が
すべての制御点の上を通過
トレイルの用途に最適😍

扇子の座標を制御点にできる 

89
頂点シェーダーで補間

1. 制御点の座標の配列を Material.SetVectorArray で送信



2. MeshのUV値のUから以下を計算

○ 対応する制御点のインデックス

○ Catmull-Rom Splineの補間率 t



3. Catmull-Rom Splineで補間された座標を計算

○ 補間された座標を頂点シェーダーの出力にする

○ ※変形前のMeshの座標は無視

90
補間 - Catmull-Rom Spline

float4 CatmullRom(float t, float4 p0, float4 p1, float4 p2, float4 p3) {
float4 a = -p0 + 3 * p1 - 3 * p2 + p3;
float4 b = 2 * p0 - 5 * p1 + 4 * p2 - p3;
float4 c = -p0 + p2;
float4 d = 2 * p1;
return 0.5 * ((t * t * t * a) + (t * t * b) + (t * c) + d);
}
t: 補間の率 (0~1) 4つの制御点
多項式なのでシェーダーで実装が可能
91
細かなブラッシュアップ

● 先端ほど色を薄く

● 先端ほどトレイルを細める

本来のシェーダーの使い方なので、簡単に実装できた😊

92
トレイルシェーダーまとめ

● OpenGL ES 2.0世代で動作するトレイルの頂点補間を実装

○ 頂点シェーダーでCatmull-Rom Splineを実装

○ 補完した座標を元にMeshを変形

ちょっと面白いシェー
ダーの活用ができた😊
93
発表トピック

蝶の羽ばたき
 扇子の軌跡
 風で揺れ動く旗

ステージの

スモーク

潜れる雲

+α
関連セッション紹介



プロファイリング

94
風で揺れ動く旗シェーダー利用楽曲



『未熟DREAMER』

95
96
頂点シェーダーによるMeshの変形

fBM(2つのsin波の重ね合わせ)で頂点を移動

// UVの斜め方向を t を定義
float t = v.uv.x + v.uv.y;
// 1つ目の波
float t1 = _WaveFreq1 * t + _WaveSpeed1 * _TIME;
float wave1 = _WaveAmplitude1 * sin(t1);
// 2つ目の波
float t2 = _WaveFreq2 * t + _WaveSpeed2 * _TIME;
float wave2 = _WaveAmplitude2 * sin(t2);
// 2つの波を合成して、頂点座標に反映します
float wave = fixTopScale * (wave1 + wave2);
v.vertex += wave;
UVの斜め方向を t と定義
t
97
頂点シェーダーによるMeshの変形

fBM(2つのsin波の重ね合わせ)で頂点を移動

// UVの斜め方向を t を定義
float t = v.uv.x + v.uv.y;
// 1つ目の波
float t1 = _WaveFreq1 * t + _WaveSpeed1 * _TIME;
float wave1 = _WaveAmplitude1 * sin(t1);
// 2つ目の波
float t2 = _WaveFreq2 * t + _WaveSpeed2 * _TIME;
float wave2 = _WaveAmplitude2 * sin(t2);
// 2つの波を合成して、頂点座標に反映します
float wave = fixTopScale * (wave1 + wave2);
v.vertex += wave;
UVの斜め方向を t と定義
t
98
頂点シェーダーによるMeshの変形

fBM(2つのsin波の重ね合わせ)で頂点を移動

// UVの斜め方向を t を定義
float t = v.uv.x + v.uv.y;
// 1つ目の波
float t1 = _WaveFreq1 * t + _WaveSpeed1 * _TIME;
float wave1 = _WaveAmplitude1 * sin(t1);
// 2つ目の波
float t2 = _WaveFreq2 * t + _WaveSpeed2 * _TIME;
float wave2 = _WaveAmplitude2 * sin(t2);
// 2つの波を合成して、頂点座標に反映します
float wave = fixTopScale * (wave1 + wave2);
v.vertex += wave;
UVの斜め方向を t と定義
t
99
頂点シェーダーによるMeshの変形

fBM(2つのsin波の重ね合わせ)で頂点を移動

// UVの斜め方向を t を定義
float t = v.uv.x + v.uv.y;
// 1つ目の波
float t1 = _WaveFreq1 * t + _WaveSpeed1 * _TIME;
float wave1 = _WaveAmplitude1 * sin(t1);
// 2つ目の波
float t2 = _WaveFreq2 * t + _WaveSpeed2 * _TIME;
float wave2 = _WaveAmplitude2 * sin(t2);
// 2つの波を合成して、頂点座標に反映します
float wave = fixTopScale * (wave1 + wave2);
v.vertex += wave;
UVの斜め方向を t と定義
t
100
法線の再計算

Meshを変形したら法線の再計算が必要

法線をどうやって求めるか🤔

101
法線を位置の微分から求める

f(x,y,z) = 0 で表現される陰関数の曲面において



法線 = 勾配

102
法線を位置の微分から求める

f(x,y,z) = 0 で表現される陰関数の曲面において



勾配 = fの各成分の偏微分

103
2つの微分の方法

数値的な微分 解析的な微分
方法 差分を計算 式を変形
微分可能な対象 あらゆる複雑な数式
例: テクスチャ
限られた数式のみ
計算コスト 高い
同じ数式を何回も評価
低い
同じ数式を1回だけ評価
104
2つの微分の方法

数値的な微分 解析的な微分
方法 差分を計算 式を変形
微分可能な対象 あらゆる複雑な数式
例: テクスチャ
限られた数式のみ
計算コスト 高い
同じ数式を何回も評価
低い
同じ数式を1回だけ評価
105
2つの微分の方法

数値的な微分 解析的な微分
方法 差分を計算 式を変形
微分可能な対象 あらゆる複雑な数式
例: テクスチャ
限られた数式のみ
計算コスト 高い
同じ数式を何回も評価
低い
同じ数式を1回だけ評価
採用!
106
sin関数のfBMは解析的に微分可能

● sinやcos関数は微分可能

○ sin’(x) = cos(x)

○ cos’(x) = -sin(x)



● sin関数のfBMは微分可能

○ sin関数のfBM = sin関数の足し算

○ 各項のsin関数の微分を足し合わせれば良い

107
解析的な法線の計算

wave1(位置)を t で偏微分した dWave1

// 波の高さ(位置)wave1 の計算
float wave1 = _WaveAmplitude1 * sin(t1);
// wave1 を t で偏微分した dWave1 の計算
float dWave1 = _WaveFreq1 * _WaveAmplitude1 * cos(t1);
108
解析的な法線の計算

wave1(位置)を t で偏微分した dWave1

// 波の高さ(位置)wave1 の計算
float wave1 = _WaveAmplitude1 * sin(t1);
// wave1 を t で偏微分した dWave1 の計算
float dWave1 = _WaveFreq1 * _WaveAmplitude1 * cos(t1);
109
解析的な法線の計算

wave2(位置)を t で偏微分した dWave2

// 波の高さ(位置)wave2 の計算
float wave2 = _WaveAmplitude2 * sin(t2);
// wave2 を t で偏微分した dWave2 の計算
float dWave2 = _WaveFreq2 * _WaveAmplitude2 * cos(t2);
110
解析的な法線の計算

2つのwaveの偏微分の結果を加算

// 波(位置)を偏微分した勾配から、法線を計算
float dFbm = fixTopScale * (dWave1 + dWave2);
float3 objNormal = normalize(float3(dFbm, dFbm, -1.0f));
o.normal = mul((float3x3)unity_ObjectToWorld, objNormal);
111
解析的な法線の計算

2つのwaveの偏微分の結果を加算

// 波(位置)を偏微分した勾配から、法線を計算
float dFbm = fixTopScale * (dWave1 + dWave2);
float3 objNormal = normalize(float3(dFbm, dFbm, -1.0f));
o.normal = mul((float3x3)unity_ObjectToWorld, objNormal);
112
解析的な法線の計算

2つのwaveの偏微分の結果を加算

// 波(位置)を偏微分した勾配から、法線を計算
float dFbm = fixTopScale * (dWave1 + dWave2);
float3 objNormal = normalize(float3(dFbm, dFbm, -1.0f));
o.normal = mul((float3x3)unity_ObjectToWorld, objNormal);
113
解析的な法線の計算

2つのwaveの偏微分の結果を加算

// 波(位置)を偏微分した勾配から、法線を計算
float dFbm = fixTopScale * (dWave1 + dWave2);
float3 objNormal = normalize(float3(dFbm, dFbm, -1.0f));
o.normal = mul((float3x3)unity_ObjectToWorld, objNormal);
114
ライティング

Half-Lambertによる簡単なライティング

// _ShadowIntensity = 0.5 のとき Half-Lambert
float3 light = normalize(_LightDirection.xyz);
o.shadow = lerp(1.0, saturate(dot(normal, light)), _ShadowIntensity);
Diffuse
 Half-Lambert

115
落下のアニメーション

落下のアニメーションは頂点シェーダーで実装

float a = 2.0f * _DropY - 1.0f;// -1~1 に変換
v.vertex.y = lerp(1.0f, v.vertex.y, pow(saturate(v.uv.y + a), 1.0f - a));
Y座標を数式で直接的に変化
もっともらしい

落下の動きができた!😄

116
旗シェーダーまとめ

● シェーダーによるGPU計算のみで3つを実装

○ Meshの変形

○ 法線の再計算

○ 落下のアニメーション



● CPUコスト0で高品質な旗の表現を実現

○ 高品質と低負荷を両立

117
発表トピック

蝶の羽ばたき
 扇子の軌跡
 風で揺れ動く旗

ステージの

スモーク

潜れる雲

+α
関連セッション紹介



プロファイリング

118
スモークシェーダー利用楽曲



『ジングルベルがとまらない』

119
120
スモーク表現の実現方法

正確なスモークの表現のためには

ボリュームレンダリングが必要

深度差の情報を利用した

擬似的なボリューム表現を採用
モバイル端末では負荷が高すぎる…

121
擬似的なボリューム表現

● 深度値の差の情報を用いてアルファ値を制御

○ 深度値の差が小さい(厚みが浅い) → アルファ値を小さく

○ 深度値の差が大きい(厚みが深い)→ アルファ値を大きく

ソフトパーティクルの仕組みを
通常のオブジェクトに利用
122
深度差の情報を利用したボリューム表現

ステージの床(不透明のMesh)
スモーク

(ソフトパーティクル = 半透明のMesh) 

カメラ

123
深度差の情報を利用したボリューム表現

ステージの床(不透明のMesh)
深度の差から
スモークの厚みを計算できる
124
深度差の情報を利用したボリューム表現

ステージの床(不透明のMesh)
深度の差から
スモークの厚みを計算できる
125
深度差の情報を利用したボリューム表現

ステージの床(不透明のMesh)
深度の差から
スモークの厚みを計算できる
126
ソフトパーティクルの実装

● 頂点シェーダー

○ スクリーン上のテクスチャ座標と深度をフラグメントシェーダーに渡す








 o.projPos = ComputeScreenPos(o.vertex);
COMPUTE_EYEDEPTH(o.projPos.z);
クリップ空間 → スクリーン空間
に座標変換をするUnityビルドイン関数
カメラからの距離を計算する
Unityビルドインマクロ
127
ソフトパーティクルの実装

● フラグメントシェーダー

○ 深度の差からアルファ値を制御

float sceneZ = LinearEyeDepth(
SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture,
UNITY_PROJ_COORD(i.projPos)));
float partZ = i.projPos.z;
col.a *= clamp(abs(sceneZ - partZ), 0.01, 1.0);
Depthテクスチャの深度
= 床などの不透明Meshの深度
現在のMesh頂点のZ値
= ソフトパーティクルの
半透明Meshの深度
2つの深度の差を計算
深度の差に比例してアルファ値を大きく制御
128
fBMによるMeshの変形

fBMによるMesh変形
 旗シェーダーと同じアプローチ
変形の可視化のために
fBMの振幅を変化
129
スモークの濃度のディテール

フラクタルノイズのテクスチャを利用して濃淡にディテールを追加

フラクタルノイズも広義のfBM
● 頂点シェーダー

○ ジオメトリレベルのfBM

● フラグメントシェーダー

○ テクスチャレベルのfBM

130
スモークの濃度のディテール

ドメインワーピングを利用してアニメーション

ドメインワーピングの概要



● fbm(p + A * fbm(B * p))

○ fBMのネスト

○ A, Bは係数



● fBMを使ってfBMの空間を歪める

131
スモークの濃度のディテール

ドメインワーピングの実装例

float distortionSpeed = _DistortionSpeed * _MusicTime;
float2 uv = v.uv;
uv.x += _DistortionAmplitude * sin(_DistortionFreq * uv.y + distortionSpeed);
uv.y += _DistortionAmplitude * sin(_DistortionFreq * uv.x + distortionSpeed);
o.uv.xy = TRANSFORM_TEX(uv, _MainTex);
o.uv.zw = _FlowFreq * o.uv.xy + _FlowSpeed * _MusicTime;
頂点シェーダーでUVを歪める 

half4 noise1 = tex2D(_MainTex, i.uv.zw);
half4 noise2 = tex2D(_MainTex, i.uv.xy + _FlowAmplitude * noise1.xy);
col = noise2;
フラグメントシェーダーで ドメインワーピング

なるべく頂点シェーダーに処
理を任せて軽量化
132
スモークの濃度のディテール

ドメインワーピングの実装例

float distortionSpeed = _DistortionSpeed * _MusicTime;
float2 uv = v.uv;
uv.x += _DistortionAmplitude * sin(_DistortionFreq * uv.y + distortionSpeed);
uv.y += _DistortionAmplitude * sin(_DistortionFreq * uv.x + distortionSpeed);
o.uv.xy = TRANSFORM_TEX(uv, _MainTex);
o.uv.zw = _FlowFreq * o.uv.xy + _FlowSpeed * _MusicTime;
頂点シェーダーでUVを歪める 

half4 noise1 = tex2D(_MainTex, i.uv.zw);
half4 noise2 = tex2D(_MainTex, i.uv.xy + _FlowAmplitude * noise1.xy);
col = noise2;
フラグメントシェーダーで ドメインワーピング

なるべく頂点シェーダーに処
理を任せて軽量化
133
スモークの濃度のディテール

ドメインワーピングの実装例

float distortionSpeed = _DistortionSpeed * _MusicTime;
float2 uv = v.uv;
uv.x += _DistortionAmplitude * sin(_DistortionFreq * uv.y + distortionSpeed);
uv.y += _DistortionAmplitude * sin(_DistortionFreq * uv.x + distortionSpeed);
o.uv.xy = TRANSFORM_TEX(uv, _MainTex);
o.uv.zw = _FlowFreq * o.uv.xy + _FlowSpeed * _MusicTime;
頂点シェーダーでUVを歪める 

half4 noise1 = tex2D(_MainTex, i.uv.zw);
half4 noise2 = tex2D(_MainTex, i.uv.xy + _FlowAmplitude * noise1.xy);
col = noise2;
フラグメントシェーダーで ドメインワーピング

なるべく頂点シェーダーに処
理を任せて軽量化
134
スモークシェーダーのまとめ

● ソフトパーティクルの仕組みを利用した

擬似的なボリューム表現



● fBMによるMeshの変形



● ドメインワーピングによる

テクスチャのアニメーション

135
発表トピック

蝶の羽ばたき
 扇子の軌跡
 風で揺れ動く旗

ステージの

スモーク

潜れる雲

+α
関連セッション紹介



プロファイリング

136
潜れる雲シェーダー利用楽曲



『WATER BLUE NEW WORLD』

137
138
「潜れる」雲シェーダー

スモーク表現シェーダーでは
実現できない内部を 潜るカメラワーク
139
「潜れる」雲シェーダーの仕組み

● 素直なパーティクル



● ソフトパーティクルを

本来の用途であるパーティクルとして利用

140
独自のパーティクルシステム

● UnityのParticleSystemを利用しない独自のパーティクル

○ Quadをランダム配置したMeshを事前生成

○ MeshRendererで描画

○ ParticleSystemのCPUコスト0に削減

141
パーティクル用のMesh作成方法

配置領域の指定用Mesh
専用のEditorツール
パーティクル用のMesh
142
パーティクル用のMesh作成方法

配置領域の指定用Mesh
専用のEditorツール
パーティクル用のMesh
143
パーティクル用のMesh作成方法

配置領域の指定用Mesh
専用のEditorツール
パーティクル用のMesh
144
独自パーティクルシステムの余談

CEDEC 2020で発表したペンライトの仕組みとほぼ同じ

CEDEC 2020 発表資料 118ページ
ペンライト
145
Meshの構成

● 雲やペンライト

○ パーティクルの数や位置は固定

○ その場で揺れるローカルな動きはシェーダーで制御

■ GPUの並列計算を活用してCPU負荷を軽減

ペンライト
雲
146
テクスチャ構成

● 雲の形状パターンを用意

● 1枚のテクスチャにパッキング

2x2のパターン数は
Editorツールで指定可能
● Mesh生成時に形状パターンを決定
○ パーティクルごとに形状パターンは固定
○ 形状パターンに対応する UVを
頂点情報に埋め込む
147
シェーダーによる雲のアニメーション制御

雲のサイズを大きく調整
アニメーションの可視化のため
雲のサイズを小さく調整
これらをシェーダーで実装
雲のアニメーション

1. 中心座標の揺れ

a. fBM

2. サイズの伸縮

a. 小さなサイズで出現、少しずつ拡大


3. アルファのアニメーション 

a. フェードイン、フェードアウト
ビルボード処理

● 常にカメラ方向にMeshが向く 

○ 回転行列を頂点シェーダーで生成 

○ 蝶シェーダーと同じアプローチ 

148
疑似的なボリューム表現シェーダーまとめ

● 2タイプのボリューム表現のシェーダーを実装


○ スモーク

○ 潜れる雲



● いずれもソフトパーティクルの仕組みを利用

○ ボリュームレンダリングより低負荷


○ モバイルでも動作する



● Mesh構成は表現のターゲットに合わせて調整


○ スモーク:平面MeshをfBMで変形


○ 潜れる雲:パーティクルを独自実装


149
発表トピック

蝶の羽ばたき
 扇子の軌跡
 風で揺れ動く旗

ステージの

スモーク

潜れる雲

+α
関連セッション紹介



プロファイリング

150
「スクスタ」3Dライブ関連セッションの紹介

今回 CEDEC+KYUSHU 2021
シェーダーを活用した
3Dライブ演出のアップデート
特定の楽曲と演出に特化した
専用シェーダーの紹介
前回 CEDEC 2020
高品質かつ低負荷な
3Dライブを実現するシェーダー開発
すべての楽曲に利用できる
汎用シェーダーの紹介
ペンライト
頂点シェーダーで
アニメーション
LEDパネル
カラーパレットシェーダー
蝶の羽ばたき 雲 扇子の軌跡
151
「スクスタ」3Dライブ関連セッションの紹介

今回 CEDEC+KYUSHU 2021
シェーダーを活用した
3Dライブ演出のアップデート
特定の楽曲と演出に特化した
専用シェーダーの紹介
前回 CEDEC 2020
高品質かつ低負荷な
3Dライブを実現するシェーダー開発
すべての楽曲に利用できる
汎用シェーダーの紹介
ペンライト
頂点シェーダーで
アニメーション
LEDパネル
カラーパレットシェーダー
蝶の羽ばたき 雲 扇子の軌跡
152
「スクスタ」3Dライブ関連セッションの紹介

今回 CEDEC+KYUSHU 2021
シェーダーを活用した
3Dライブ演出のアップデート
特定の楽曲と演出に特化した
専用シェーダーの紹介
前回 CEDEC 2020
高品質かつ低負荷な
3Dライブを実現するシェーダー開発
すべての楽曲に利用できる
汎用シェーダーの紹介
ペンライト
頂点シェーダーで
アニメーション
LEDパネル
カラーパレットシェーダー
蝶の羽ばたき 雲 扇子の軌跡
153
プロファイリング

154
「スクスタ」3Dライブ関連セッションの紹介

CI/CDと連携してプロファイリングを完全自動化😎

CEDEC 2019
Android向けUnity製ゲーム最適化のための
CI/CDと連携した自動プロファイリングシステム
https://www.slideshare.net/klab-tech/androidunitycicd
GDC 2019
Continuous Profiling for Android Game
Performance Optimization
https://www.slideshare.net/klab-tech/continuous-profiling-for-android-game-perfor
mance-optimization-216466184
155
自動プロファイリングのアップデート

2019年から2年が経過…

自動プロファイリングもアップデート





制作職だけで楽曲計測できる仕組みを構築🎉

156
完全自動化とは?

● Slackのメッセージ送信だけで完全自動で計測

○ アプリのビルド

○ アプリの実行

■ ライブの周回

■ プロファイリングのバックグラウンド実行

○ プロファイリング結果の集計

■ データ分析

■ 結果をクラウドにアップロード

ここまでは2年前と同じ
157
何がアップデートしたか?

計測シナリオの設定が楽になった👍

2年前
Unity上でScriptableObjectを編集
gitにcommit & push
面倒 & 開発の知識が必要🤨
現在
計測用のSlackメッセージ
の内容で設定
手間が少ない & 誰でもできる😄
158
自動プロファイリングのアップデート

● 計測用のSlackのメッセージ

○ Googleスプレッドシートから自動生成

入力①:計測楽曲の ID(複数指定可能)
入力②:3Dライブの品質設定
出力:計測用のSlackのメッセージ
159
なぜスプレッドシートを利用したか

課題

● 計測用のSlackメッセージの直接入力は非開発職には難しい 😖

○ 必要なパラメーターが多い + 技術的な知識が必要 

○ 接続先のサーバ情報やアプリのデバッグシンボルなど 

解決策

● 制作職が入力するパラメーターは最小限に 😃

○ 計測対象の楽曲ID

○ 3Dライブの品質設定

● その他のパラメーター 

○ 開発職がメンテナンス

○ スプレッドシート上でセルを非表示(非開発職に意識させない)

160
非開発職の計測で得られた効果

● 非開発職でも計測できるメリット

○ 制作チームから開発チームに

計測を依頼するフロー自体を削減

○ 製作途中の楽曲も計測できる

■ 負荷を意識して演出を制作できる

■ パフォーマンス上の問題を早期検出

思わぬメリットもあった👍

161
プロファイリング結果のダッシュボード

プロファイリング結果はダッシュボードに自動集計

Webブラウザ上でプロジェクトの全員が閲覧可能

計測サマリーの一覧
アプリバージョン×
端末×品質設定×楽曲で
検索結果を絞り込み
負荷の詳細
重たいメソッドTOP
負荷のカテゴリー分類
時系列の分析
フレーム単位の負荷
スパイクの検出に役立つ
162
GPUプロファイリングは手動

● 自動プロファイリングではUnityProfilerからデータを取得

○ Unity Profilerで取れないデータは未対応

○ GPUのプロファイリングは未対応



● GPUプロファイリングは手動

○ SoCメーカー提供のツールを利用

■ iPhone

■ Xcode

■ SnapdragonのAndroid端末

■ Snapdragon Profiler

163
発表のまとめ

● シェーダーの陰影処理に留まらない活用事例を紹介

○ アニメーションの実装:蝶🦋 旗🚩

○ 特殊表現の実装:トレイル🎗 雲💭

○ 負荷と品質の両立🤝

シェーダーの魅力と可能性が

みなさんに伝われば嬉しいです😀

164
今後も

ファンの皆様に喜んでいただけるようなゲーム

を目指して頑張ります!


シェーダーを活用した3Dライブ演出のアップデート ~『ラブライブ!スクールアイドルフェスティバル ALL STARS』(スクスタ)の開発事例~​