【Unity】LEDっぽい画面を作る
こんにちは、オオブチです。
普段はロジマジでUnityエンジニアっぽい事をしています。
弊社はモーションキャプチャを使った映像制作が得意な会社なので、VTuberなどCGキャラクターのライブ映像を作ることが度々あります。ライブといえば音楽に合わせて動く煌びやかなライト、紙吹雪や煙などのパーティクルの類、ステージの移動や変形など様々な演出があって一見するだけでもいろんな細かな要素が詰めあわされていることが判りますが、今回の記事では、その中でも使いまわしのいい背面のサービススクリーンを描くシェーダーをUnityのBuilt-inレンダーパイプライン上で試作しようと思います。せっかくなのでこの記事を読んだ機会にシェーダー入門しましょう。
作りたいもののイメージとしてはLEDのディスプレイみたいな感じです。遠目で見たら絵がぼんや~り光っていて、近くに寄ったらドット(画素)が見えるような感じ。ぼんやり光る部分はポストプロセスでやればいいので、近寄ったらLEDの網目が見えるようなマテリアルを作ってみましょう。
1. 画素の形を出す方法
どうやって画素の形を出そうかと考えれば手段はいくつか考えられますが、今回はこちらの画像を使おうと思います。
それではProjectウィンドウから右クリック→Createからシェーダーとマテリアルをを新規作成、シェーダーをマテリアルにD&Dして適応し、Sceneビュー中の適当なQuadとかに作ったマテリアルを割り当てておきます。下にコードを貼って置くので作ったシェーダーにコードを書き込むかコピペしてください。
Shader "Unlit/test"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_PixShape("Pixel Shape Texture", 2D) = "white" {}
_UV_X("Pixel num x", Range(10,1600)) = 960
_UV_Y("Pixel num y", Range(10,1600)) = 360
_Intensity("intensity", float) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex, _PixShape;
float4 _MainTex_ST, _PixShape_ST;
float _UV_X, _UV_Y, _Intensity;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//縦横何個並べるか
float2 uv_res = float2(_UV_X, _UV_Y);
fixed4 col = tex2D(_MainTex,i.uv);
//画素
float2 uv = i.uv * uv_res;
float4 pix = tex2D(_PixShape, uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col * pix * _Intensity;
}
ENDCG
}
}
}
出力の結果がこちら↓
ここで綺麗に絵が出ない人は画素用の画像のImport SettingsでWrap Modeとかを確認してみてください。
2. もう少しLEDっぽくする
とりあえずLEDのスクリーンっぽい絵は出ました。でもよく近寄って見てください、1つの画素に複数の色が跨っています。まあ別に見ている側は誰一人として気にしないでしょうが、実際に使われるLEDは一つの色しか出さないはずです。細部を詰めることでリアリティが出ると信じてここを解消してみましょう。
現在のコードだと沢山敷き詰めたLEDの枠に元画像を乗算している状態です。単一のLEDで一色だけを出すためには乗算する前の元画像を、並んでいるLEDの数と同じだけ低解像度にしたモザイク柄にしてしまえばいいはずです。それを考慮してフラグメントシェーダ部分を書き換えます。
fixed4 frag (v2f i) : SV_Target
{
float2 uv_res = float2(_UV_X, _UV_Y);
// ここを書き換える↓
fixed4 col = tex2D(_MainTex, (floor(i.uv * uv_res) / uv_res + (1 / (uv_res * 2))));
float2 uv = i.uv * uv_res;
float4 pix = tex2D(_PixShape, uv);
UNITY_APPLY_FOG(i.fogCoord, col);
return col * pix * _Intensity;
}
改良後の出力結果がこちら↓
良くなった気がします。実際使う時は、映す内容によっては「文字を映したいからボケない方がいい」とかいろんな事情があるので好きな方を選んだり、画素のマスクに使う画像を差し換えたりしてみてください。
意見として「折角シェーダー書いてるんだから画素の模様も同様にシェーダーのコードで書いてしまえば画像の解像度とか気にせんでいいし、見栄えいいのでは??」って話ももちろんわかるんですが、細密な模様を細密に書きすぎるとアーティファクトが出ます(記事下部に補足あり)。
そういうのを回避する処理を自前で書くのも面倒です。画像を使えば遠くから画素を見てもMipmapで勝手にうまい具合に輪郭をぼかしてくれるのでそれっぽく手ごろに出来ますし、いろんな形にしたりマスクしたりもできますし一石二鳥です。
今回はシェーダー入門者向けでかつ、ライブ演出っぽい話題という事でLED風のスクリーンを作りました。それにしても何でシェーダーってこんなに勉強しにくいんでしょうね。初級者向け記事はそれなりにあるのに少し慣れたような気になると急にどこで勉強すればいいのかわからなくなる。気が向いたら今度はまた別のエフェクト製作とかもう少し踏み込んだシェーダーの記事とかを書こうと思います。
補足
① アーティファクトって何やって人向け
Weblio大先生によれば
とのことです。今回の例で言えば記事の上の方で触れた画像みたいなことを言ってます。例えば遠くから見たとき、つまりCG上の一画素の大きさが実物のディスプレイの1ピクセル以下の小ささになってしまった場合、実物の画面のピクセル一個一個には画素の枠と画素の中間みたいな色が出てくれれば画面上は違和感ないはずです。が、ただ模様を描くだけでなんの対処もしないと色がきっぱり分かれて全然ほしくない干渉縞みたいな出力になっちゃいます。最終的に出る絵を書くためのフラグメントシェーダは物理的な画面のピクセル一個ごとに行われてほしい計算を書く場所なので、もとの丸の画像の中のどこをサンプルして表示すればいいか探ると飛び飛びの場所しかサンプルできずこんな模様が出ちゃう事もあるんですね。不思議だね。
ピクセルに使う画像のImport SettingsからGenerate Mip Mapsのチェックを外すと同じ現象を体験することができます。
気になる人は標本化定理とかで調べてみると似たような説明が出るかもしれません。
② 意図的に画素をぼかしたい人向け
今回は画素のMipmapレベルを使って遠くから見た時のみぼかしましたが、実はMipmapレベルをシェーダー側からも触れます。
//コードの例
float4 pix = tex2Dbias(_PixShape, float4(uv, 0, _MipmapBias));
float4 pix = tex2Dlod(_PixShape, float4(uv, 0, _LOD));
参考:https://learn.microsoft.com/ja-jp/windows/win32/direct3dhlsl/dx-graphics-hlsl-tex2dbias
おまけ
画素に使う画像を差し換えてみた場合