🕑

『あんさんぶるスターズ!!Music』MV軽量化の取り組み(レーダー風扇形シェーダー)・新卒入社して一年の感想

トゥウィンクル空中戦

「Happy Elements Advent Calendar 2023」 12月11日の記事です。

はじめに

はじめまして、Happy Elements株式会社『あんさんぶるスターズ!!』グループ所属でゲームエンジニアとして働いているI.S.です。
普段は主に3D部分の開発を担当させていただいております。
今回は『あんさんぶるスターズ!!』グループに異動して初めて実装したシェーダーのご紹介をさせていただきます。

扇形内の色を変えてレーダー風の表現をするシェーダー

実装経緯

トゥウィンクル空中戦スクリーン.mp4

つい先日公開された『トゥウィンクル空中戦』という曲のMV内でこのような映像を中央スクリーンに表示する表現があります。
これを愚直に映像データを表示する方式にしてしまうと少し容量が大きくなってしまいます。
理由としては、

  • 黄色い部分が360度回る
  • レーダーの色が途中でピンク→水色に変化する、黄色部分は不変
  • ハートが拡大縮小、同じハートでもレーダー内外で色が変わる

↑これらを映像のみで実現するには、短い動画をループにする、色のみマスクする等の工夫も出来ません。
そのためこの映像パターンを入れ込んだ長尺になってしまい、必然的に映像用のデータが大きくなってしまいます。
ここでシェーダーを実装出来れば、テクスチャ数枚と簡単な計算のみで実現出来るため、軽量化が可能そうです。

実装

マテリアルで設定する値

マテリアルで設定出来る値は以下とします。

  • ベースとなる色(MainColor)、レーダーの扇形内の色(SubColor)
    • テクスチャは白単色・透過有で形だけ指定します
  • レーダーの回るスピード
  • レーダーの初期位置(時間で指定)
  • レーダーの扇型の角度(三等分された領域の合計)
  • 時計回りに1番目、2番目、3番目のアルファ値(ブレンド率)

実装方針

  • それぞれの扇形の角度は三等分で決め打ち(本MV使い切り予定のため)
  • レーダーの背景、レーダーの同心円、ハート は透過の別オブジェクトにする
    • サブカメラで撮影した内容をモニターにセットして描画します
    • 最終的に円形メッシュ状に切り取られるため、扇形と言いつつ今回は円形にマスクはしません
  • 扇形の内外判定をしてブレンド率を出し、その値でMainColorとSubColorをブレンド
  • 扇形の内外判定
    • 扇形の始点となる角度を_Timeから計算
    • 始点の角度とuv座標の中心からの角度を比較し、角度が扇形に収まっていれば対応したブレンド率を返す

以上が出来れば可能そうです。

実装

以下のように実装しました。

扇形の計算部分

#include "UnityCG.cginc"

half _Speed;
half _TimeOffset;
half _FanAngle;
half _FirstAlpha;
half _SecondAlpha;
half _ThirdAlpha;
float _SceneTime; // global

float fmodPositive(float x, float y)
{
    return x - y * floor(x / y);
}

half separatedFan(float2 uv) // uvは中心からの相対座標
{
    half angle = radians(_FanAngle) / 3;
    half baseRad = radians((_SceneTime + _TimeOffset) * _Speed);
    // 0時方向から時計回りの角度(ラジアン)
    half uvRad = -atan2(uv.y, uv.x) + UNITY_PI / 2;
    half diff = fmodPositive(uvRad - baseRad, 2 * UNITY_PI);
    int index = floor(diff / angle);
    half4 alphas = half4(_FirstAlpha, _SecondAlpha, _ThirdAlpha, 0);
    half alpha = alphas[clamp(index, 0, 3)];
    return alpha;
}

レーダーイメージ

図は実装のイメージです。 扇形の始点(baseRad)を時間経過で動かし、uv座標の0時方向からの角度と比較し、3等分の扇形の何番目に含まれているのかを判定しています。
また、cgでのfmod()負の値で割った場合に結果が負になってしまい、今回の 0 ~ 2π の範囲への変換には適さないため、fmodPositive()として常に正の剰余を返す関数を用意しました。

色のブレンド処理(頂点シェーダとフラグメントシェーダ)

コード(抜粋)
sampler2D _MainTex;
float4 _MainTex_ST;
half4 _MainColor;
half4 _SubColor;
float _CenterPosX;
float _CenterPosY;

struct appdata
{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
};

struct v2f
{
    float2 uv : TEXCOORD0;
    float4 vertex : SV_POSITION;
    float2 posToCenter : TEXCOORD1;
};

v2f vert(appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    float3 worldPos = mul(unity_ObjectToWorld, float4(v.vertex.xyz, 1.0)).xyz;
    // ステージセットアップの都合上xの相対座標はX軸反転にさせる
    o.posToCenter = float2(_CenterPosX - worldPos.x, worldPos.y - _CenterPosY);
    return o;
}

fixed4 frag(v2f i) : SV_Target
{
    fixed4 mainCol = tex2D(_MainTex, i.uv);
    return mainCol * lerp(_MainColor, _SubColor, separatedFan(i.posToCenter));
}

レーダーの中心となるグローバル座標を設定し、そこを中心とした相対座標で色を判定することで、今回の小さいハートのような複数オブジェクト同士でレーダーの色分けが同期されるようにしました。
全てのオブジェクトが、オブジェクト自身の中心点からの角度でレーダーの判定をすればいい場合は、separatedFan()内での計算をuvの中心からの相対座標(x - 0.5, y - 0.5)で行い、フラグメントシェーダではuvを渡せば大丈夫です。

アンチエイリアス処理

以上の実装で目的の表現は出来るようになったのですが、MVの見栄えを良くするためにひと手間加えます。

昨年のH.K.さんの記事では、fwidthを用いた方法でアンチエイリアス処理を行っていましたが、
(参考: 『あんさんぶるスターズ!!Music』における3D演出開発の工夫 〜U.S.A.のプロジェクションマッピング〜)
今回は簡単な図形なのもあり、解析的な方法でアンチエイリアス処理を追加してみたいと思います。

実装

アンチエイリアス処理を追加したコードは以下になります。

float fmodPositive(float x, float y)
{
    return x - y * floor(x / y);
}

half smoothFanEdge(half rad, half len)
{
    half edgeThreshold = 0.01;
    half d = len * sin(rad);
    return smoothstep(0, edgeThreshold, abs(d));
}

half separatedFan(float2 uv)
{
    half angle = radians(_FanAngle) / 3;
    half baseRad = radians((_SceneTime + _TimeOffset) * _Speed);
    half uvRad = -atan2(uv.y, uv.x) + UNITY_PI / 2;
    half diff = fmodPositive(uvRad - baseRad, 2 * UNITY_PI);
    int index = floor(diff / angle);
    half len = length(uv);
    half alphaBlendRatio = smoothFanEdge(diff - angle * index, len);
    half4 alphas = half4(_FirstAlpha, _SecondAlpha, _ThirdAlpha, 0);
    half leftAlpha = index < 1 ? 0 : alphas[clamp(index - 1, 0, 3)];
    half rightAlpha = alphas[clamp(index, 0, 3)];
    return lerp(leftAlpha, rightAlpha, alphaBlendRatio);
}

扇形の端をぼかしていくことで、端のジャギーを抑えることが出来ます。
smoothFanEdgeではそのuvが属しているindexの扇形の始点との距離を計算し、その距離に応じてsmoothstepでブレンド値を返しています。
このとき、単にdiffの値でグラデーションさせてしまうと円の中心ではボカシが小さく、円の外周ではボカシが大きくなってしまうため、扇形の始点となる直線とuvとの距離を計算してボカシを掛けています。

アンチエイリアスイメージ

図のように考えると、直角三角形の関係から、始点からuvへの距離は

uvの長さ * sin(始点とuvの角度)

で求めることが出来ます。
また、始点からのみボカシを掛ける関係で、4つめの透明な扇形を新たに描画しています。

結果

before

before_アンチエイリアス

after

after_アンチエイリアス

ジャギーが軽減され、綺麗になりました!

おわりに

私が初めて業務で書くシェーダーだったので、記念も兼ねて記事にさせていただきました。
今回のレーダーは少しクセのあるデザインでしたが、ゲームではレーダー風の表現が使われることがままあると思います。
良ければ参考に実装してみてください!

おまけ: 新卒で入社して一年の感想

私は大学在学中からHappy Elements株式会社でアルバイトをしており、卒業後に新卒として入社しました。
社員として働き始めてからちょうど一年と少し経ったので軽く感想を記しておきたいと思います。
弊社はアルバイトや新入社員にもユーザー様の手元に届く仕事を任せて頂けることが多く、色々貴重な経験をさせていただきました。
希望があれば普段の業務以外のプロジェクトに参加することができ、私はこの一年間で、Super Lite, あんさんぶるトレーニング などにも参加させていただきました。
弊社は新人研修というものはほとんどなく、業務の中での成長が求められる環境は人を選ぶかもしれませんが、最初からモノ作りに関わることが出来る というのはゲームを作りたい人間には合うのかもしれません。

ユーザーの皆様にもっと楽しんで頂けるゲームを作れるよう、これからも精進していきたいと思います!

GitHubで編集を提案
Happy Elements

Discussion