TAが勉強を兼ねてモバイル向け電光掲示板シェーダー書いてみた

はじめまして、亀川と申します。


セガゲームス開発統括部アート&デザイン部TAセクションというところに所属しており普段はモバイル向けのゲーム開発に従事しております。


所属はTAセクションですが、業務内容はデザイン業務とTA業務が半々ぐらいです。
内容はモーション制作・管理、Mayaをはじめ各種DCCツールのツールやライブラリの開発、保守など日々TAセクションに寄せられる要望に応える形で作業を行っております。


弊部のゲーム開発は全てUnityで行われているのですが、ご存じの通りUnityはかなりオープンなゲームエンジンなので各種情報が公式ドキュメントやWEB上でも入手しやすく学習しやすいものとなっており、ゲームの実装に関わる部分でもTAが関われる部分が増えてきたというのが実感としてあります。


そういった中でもシェーダー開発は飛躍的に敷居が低くなったのではないかと思います。
少しプログラムをかじったことがあればレベルにもよりますが、すぐにでも画面に出るものが作って試せます。これは非常に楽しいです。


そういった事もあり、ここ1、2年は空前のシェーダープログラミングブームが来ていると思います。
私もご多分に漏れず、これまであまり触れる事のなかったゲーム中のビジュアル周りに関わるシェーダーの勉強を始めてみました。


前置きが長くなってしまいましたが、今回の記事は以前に部内のあるプロジェクト用に作った「電光広告看板」、「電光掲示板」用シェーダーについて作成のプロセスと内容について触れていきたいと思います。

電光広告看板とは?

電光広告看板という言葉は普段生活している中ではほぼ聞く事はないのですが、街中や施設の中など目にする機会は非常に多いと思います。


LED電球を並べて広告を映像として表示させることができるものです。
その中でも特に今回はサッカー場の横に置かれている電光広告看板を想定して作成しました。

制作物

結果から先に見せてしまうと、こういう物を制作しました。

f:id:sgtech:20171123211804j:plain:w120
f:id:sgtech:20171123211800j:plain:w120
f:id:sgtech:20171123211753j:plain:w120
f:id:sgtech:20171123211749j:plain:w120
f:id:sgtech:20171123211745j:plain:w120

電光広告看板に見えますでしょうか?実際はアニメーションしていますので本当は動画を置きたかったのですが”はてなblog”に動画を直接アップロードできなかったので静止画になっています。

それではこの看板シェーダーを作るプロセスと実際のコード内容を説明していきたいと思います。

制作前の仕様確認

制作前には仕様の確認が必要となりますので発注者であるアートリーダーと最終イメージと使い方などを含めた仕様のすり合わせを行いました。


具体的には広告の動かし方?テクスチャは何パターン用意するのか?メッシュのUVの持たせ方は?背景のライティングとの兼ね合いは?プログラムからの操作、アーティストがどこまで調整できるかなどなどをまずはざっくりで良いのでヒヤリングを行いシェーダーに持たせるパラメータや仕様を決めて行きます。


最初のヒヤリングで全ての要求仕様が出揃うことはないので、まずは最低限作成できるレベルの仕様はつめます。


ここでざっくり決まった仕様はこんな感じです。


・最近の電光掲示板のような広告
・サッカー場の回りにポリゴン板を立てて置くイメージ。何パターンか置きたい。
・ライティングは反映する必要はない。
・テクスチャスロットは2枚。プログラムで切り替える。
・2枚とも非表示にする事がある。その時は指定色が出るようにしたい。
・ブレンドで切り替えたい。
・広告テクスチャの移動はUVアニメーションで制御したい。

電光掲示板らしさの要素抽出

機能的な仕様が出そろったのでいよいよ今度はビジュアル面の方向性を固めていきます。


今回のゲームでは中継カメラを意識したような視点が想定されるので素直にTVを通して見た電光掲示板らしい表現を目指すことにします。


そのためは電光掲示板がそれらしく見えている特徴的な部分の抽出が必要です。この要素分解をするのが楽しいところですが、私はこう考えました。


・LEDならではのドット感。
・カメラのレンズを通す事による色収差、にじみ感。
・広告が切り替わる動き。スクロールする時のアニメーション。
・LED表示を描画しているスキャンラインの雰囲気。
・光のにじみのグロー表現


以上の要素があればだいたいそれらしく見えるのではないかと思いました。
今回は、広告の動きはUVアニメーション、光のにじみはポストエフェクトで適宜行うとして看板シェーダー自体の描画ではその前段階までを表現することにしました。

シェーダー実制作

先に完成したコードがこちらになります。

全体のシェーダーコードは以下のようになります。


インスペクターに表示されるシェーダープロパティはこんな風になっています。
f:id:sgtech:20171123211818p:plain:w120


それでは上で書いた要素がソース中でどのように実装されているのか中身を追って見ていきたいと思います。

LED球のドット感の表現

ドット感の表現はシンプルにLED球1つを表現したテクスチャ1枚をタイリングでしきつめて上から乗算で被せます。加えて、広告画像の方にも少し手を加えますがこちらは実装に少し迷ったので2つの表現を用意しました。

LED枠のイメージ

まずはLED枠のイメージですがこんな感じシンプルにタイリングするだけです。
この枠の下に広告画像が来る事になります。
f:id:sgtech:20171123211722p:plain

広告画像テクスチャの加工1 ”1ドット1色”

一つ目は広告画像をタイリング枠に合わせて1ドット1色という色の取り方をするものです。
こちらの方がLED球1つに対して1色なので原理的にはより本物に近いのですが、一方で相当細かくLED球をタイリングしないと絵が潰れてしまい判別できなくなる問題もあります。

f:id:sgtech:20171123211718p:plain

ソースはこうなっています。

                #ifdef _IS_REAL_REAL_LED
                    //こちらが1ドット1色
                    //Pixelate for MainTex
                    half2 steppedUV = i.uv.xy + _CellSizeXY*0.5;
                    steppedUV /= _CellSizeXY.xy;
                    steppedUV = round(steppedUV);
                    steppedUV *= _CellSizeXY.xy;
                    col = tex2D(_MainTex, steppedUV);

                    //Pixelate for MainTex2
                    steppedUV = i.uv2.xy + _CellSizeXY*0.5;
                    steppedUV /= _CellSizeXY.xy;
                    steppedUV = round(steppedUV);
                    steppedUV *= _CellSizeXY.xy;
                    col_mt2 = tex2D(_MainTex2, steppedUV);

この部分でポイントになってくるのは"_LedTex"で取得したLED枠テクスチャのタイリング単位でテクスチャの色を1色だけサンプリングしてくる部分です。

テクスチャのタイリング数は"_LedTex_ST.xy"でxyそれぞれのタイリング回数を得る事が出来ます。xを3回、yを3回繰り返すなら(3,3)という形で格納されています。


またテクスチャの色はUV空間で取得するのでこのタイリングのxy回数の1回分のサイズを0-1のUV空間の値で知る必要があります。それをコード中で事前に行っている部分がこちらです。

static const half2 _CellSizeXY = 1.0 / _LedTex_ST.xy;


UV値単位で1つのセルサイズを知ることが出来たら後はテクスチャのUVの値に対してセルサイズの半分を足しその値をセルサイズで割り、roundで端数を切り捨てる事で段階的なUV値を得ます。そのUV値を使ってテクスチャの色を取得することで段階的に取得することが出来ます。

広告画像テクスチャの加工2 ”画素ずらしによる疑似色収差”

1ドット1色は本物に近いのですがやはりLED枠タイリングを細かくしないとそもそも画像の認識が難しいです。
そのため、ドット化なしに通常通りテクスチャを張り付けるパターンも用意しました。これが2つ目です。

こちらはそのままテクスチャを表示するだけなのですが、そのままでは少し物足りなかったのでカメラのレンズによる色収差を表現する事にしました。
非常にお手軽な手法ですが色を取得する際にrgbの各チャンネルを1pxずつずらして取得することにしました。

それがこの画像になります。
f:id:sgtech:20171123211714p:plain


よりアップで見た画像がこちらです。LEDの枠はまたいで色はあるものの離れて見るときは1ドット1色とそう差異は感じられない印象です。

f:id:sgtech:20171123211710p:plain

こちらのソースはこうなっています。

                #elif _IS_REAL_FAKE_LED
                    //枠は関係なくテクスチャを読み込む
                    //Shift Texture color by 1px
                    col = tex2D(_MainTex, i.uv);
                    col.g = tex2D(_MainTex, i.uv.xy + _Texby1pt.x).g;
                    col.b = tex2D(_MainTex, i.uv.xy + _Texby1pt.x).b;

                    //Shift Texture color by 1px
                    col_mt2 = tex2D(_MainTex2, i.uv2);
                    col_mt2.g = tex2D(_MainTex2, i.uv2.xy + _Texby1pt.x).g;
                    col_mt2.b = tex2D(_MainTex2, i.uv2.xy + _Texby1pt.x).b;
                    
                #endif

テクスチャからrgbチャンネルごとに1pxずつずらして取得していることがわかります。
テクスチャの解像度の1px単位がUV値に換算するとどれぐらいになるのかは下に書いた行で事前に取得していました。

static const half2 _Texby1pt = half2(_MainTex_TexelSize.x , _MainTex_TexelSize.y );
マルチコンパイル

この2つの表現方法をUnityのマルチコンパイルという機能を使い1つのシェーダー内で2つのバリエーションのシェーダーを持てるように実装しています。


この機能の実装部分はCGPROGRAM~の少し下の行のスニペットと呼ばれる部分に記述されている以下の行となります。

#pragma multi_compile _IS_REAL_LED _IS_FAKE_LED

これはプロパティ部分を記述している部分に

[KeywordEnum(REAL_LED, FAKE_LED )]
_IS_REAL("IS Real LED", Float) = 0

このように記載することでアーティストがシェーダープロパティからどちらのモードを使用するか使い分ける事が出来ます。

f:id:sgtech:20171123211706p:plain


さらにフラグメントシェーダーの中もこれに対応している部分があります。上述のLED部分の中で#ifdefから始まり#endifまでの部分でプロパティのキーワードで条件として処理を分けています。

個人的にはピクセル化を行うREAL_LEDモードの方の方が力を入れて用意したのですが実際の現場では疑似モードの方でLEDの枠のわかりやすさ、テクスチャのわかりやすさから選択されているようです。

広告画像の切り替え

Lerp関数

1枚目と2枚目、そしてベースカラーの3つの色の切り替えは主にlerp関数を使って行っています。
動画が貼れないのでまた静止画ですみません。左から時系列でベースカラー、広告1、広告2そしてまたベースカラーへと変遷しています。

f:id:sgtech:20171123212418j:plain

                //Blend Texture Main and Main2farPower
                col.rgb = lerp(col.rgb, col_mt2.rgb, _MainTex2_BlendPower);
                //Blend Base color and Textures.
                col.rgb = lerp(_BaseColor.rgb, col.rgb, _BlendPower);


lerp関数を2回使う事で3つの色の切り替えを行っています。
最初の行で2枚の広告テクスチャの切り替えを、次のlerpで広告と指定カラーの切り替えを行います。


lerp関数はテクスチャのブレンドを行う際はよく使う関数でlerp(x, y, s)という形で使用しx,yの2枚のテクスチャをs(0~1)の値でブレンド度合を決めます。勿論用途はテクスチャに限らず2つの要素を0~1で割合でブレンド調整したい時には使いやすい関数だと思います。


広告の切り替えなので2つのテクスチャで十分ではないかと思われるかもしれませんがどちらの広告もオフにして何も表示しない状態にする時に第三の色が必要になってくるのでこの形式を取っています。また完全にどちらの広告が表示されないという状態の時にプログラムから広告テクスチャを入れかえる事も可能です。

LED描画更新による輝度ムラの表現

ここまでやってきましたがまだ何か物足りない気がしましたので輝度を時間軸で少しいじってみることにしました。

実際のLED電光掲示板は人間の目では意識できないほど高速に1ラインずつ描画を更新しています。それをカメラで写すとシャッタースピードのズレから輝度のムラのようなものを感じる事があります。そういうニュアンスを入れる事でさらに電光掲示板らしさが増します。

f:id:sgtech:20171123212531j:plain


左が何もしない時の映像で右が輝度の変化を加えたものになります。
どうでしょうか。上は静止画ですが実際は輝度ムラが上下左右でアニメーションします。
少し右側の方がそれらしく見えるのではないでしょうか?


その部分のソースがこちらになります。

                // 走査線1 uv基準でスキャンラインを描画
                half scanLineColor = sin(_Time.y * _LineSpeed + i.uv.y * _LineSpacing);// / 2 + 0.5;

                // 走査線2 uv基準でスキャンラインを描画
                half scanLineColor2 = sin(_Time.y * _LineSpeed*0.5 + i.uv.y * _LineSpacing*0.1);/// 2 + 0.5;
                
                // 走査線3 uv基準で横スキャンラインを描画
                half scanLineColor3 = sin(_Time.y * _LineSpeed*0.25 + i.uv.x * _LineSpacing2); //; / 2 + 0.5;

                col += (saturate(scanLineColor) + saturate(scanLineColor2) + saturate(scanLineColor3)) * lerp(_LineBrightness, 0.0, farPower) * _BlendPower;


やっていることは簡単です。sin関数を用いて現在の表示色に白を加算する事で輝度ムラを加えています。
それを波形のリズムを変えて3つ重ねています。こうする事で1つの時より自然な感じが出ます。

カメラからの距離に応じた対策

上述の輝度の変化ですがこれは看板に近い時は効果的ですが離れたところから見た時はやや不自然に感じました。


そこでカメラの距離に応じてその影響を加減することにしました。実は上で書いたコードにはそれはすでに含まれています。計算式の中のこの部分になります。

 (_LineBrightness * (1 - farPower)) * _BlendPower;

事前にカメラからの距離をfarPowerという変数に格納してあり距離に応じて光の強度が乗算する割合を変えています。

シェーダー高速化について

シェーダーを書く上で常に意識しないといけないのはやはり処理負荷の部分です。

この高速化について少し触れてみたいと思います。これに関しては月並みですがUnityドキュメントの”シェーダーを書く場合のパフォーマンスのヒント”という項目を忠実に行うにつきるかと思います。
https://docs.unity3d.com/jp/560/Manual/SL-ShaderPerformance.html


できるだけ頂点シェーダー内で済ませる

頂点シェーダー部分で取得した情報はフラグメントシェーダー内では”補間された値”で受け取ることができるので、ライティング情報など頂点で受け取っても問題ない場合は頂点シェーダー内で受け取ります。例えば3頂点で囲まれたポリゴンの場合、頂点シェーダーでの計算は3回ですがフラグメントはピクセル数に応じて行われますのでその計算量の差は圧倒的です。

できるだけ計算の精度を下げる。

floatよりhalf、halfよりfixedの順で計算が低精度になっていくので処理負荷が低くなります。
fixedで問題のないケースは積極的にfixedを使うとよいでしょう。

ただし

現代の多くの GPU(OpenGL ES 3 や Metal を実行することができるもの)は固定小数点数と
半精度浮動小数点数を、内部的にはまったく同じものとして取り扱います。

とUnityのドキュメントにもありますのでhalfで書いていても実質は問題ないというケースは多いのではないかと思います。

テクスチャへのフェッチ(読み込み)回数を減らす

テクスチャは通常フラグメント関数の部分でフェッチしますがピクセル数に応じて行われるのでパフォーマンスに影響します。例えば上述のシェーダーソース内の”画素ずらし”の部分でテクスチャのフェッチを4回やってますがこれはなかなかの負荷だと思われますので画像がこの用途以外に使い道がないのであれば事前にPhotoshop等で画素ずらしを行っておいてこの処理を省いてもよいでしょう。


まとめ


以上、電光掲示板シェーダーの制作事例の説明となります。今回はライティング、影や半透明ソートなどのややこしい部分は考えなくてよいのでシェーダーとしては比較的簡単な部類には入るのではないかと思いますが、それでも実機で表示されて実際のゲーム中で動作するとなかなか達成感があります。



今回は右も左もわからない、それこそセマンティクスって何ですか?というようなところからシェーダー学習を始めたのですが学習法は完全に我流でいろんな情報を並行して学んでいきました。


Unity公式ドキュメントやネット上の制作事例を参考にした他、Unityシェーダーの書籍、nVidiaのCgマニュアルなども参考にしました。

nVidiaのドキュメントはこのあたりから読む事が出来ます。


Cg Toolkit | NVIDIA Developer


ただ上記URL内のドキュメントは全て英語なので正直言ってざっと斜め読みするのも大変です。私を含めてそういった方には日本語に訳された物もあります。

http://developer.download.nvidia.com/cg/Cg_3.0/Cg_Users_Manual_JP.pdf


こちらはリリース日がかなり古いのが気になりますが、Cgの基本や数学関数の説明などは今でも使えるものですので一読されることをお薦め致します。


またゲーム会社ということもありシェーダーを始め描画周りの知識が豊富な者がたくさんおりますからアドバイスをもらったり、コードレビューしてもらったりしたことも大きな助けになりました。


ただ何にでも言えることだとは思うのですが、一番学習効果が高い勉強法は”何か作りたいものがあって実際に作ってみる”。これにつきると思います。


これが一番学習意欲が上がりますし、質問がより具体的になり、望む答えも得やすくなりますので是非皆さんも何か題材を見つけて取り組んでみてください。


次回はリグのネタでも載せられればと考えています。それでは次回またお会いしましょう。


 TAセクションではこのようにアーティストの制作に役立つ環境を提供できるよう力を注いでおります。やりたいことを遠回りせずに行える環境でお仕事したい方はぜひ下記の弊社グループ採用サイトをご確認ください。いっしょに働きましょう!現在いろいろな職種でアーティストを多数募集しております!

採用情報 | セガ企業情報サイト

(C)SEGA

Powered by はてなブログ