カスタムシェーダーでモバイルでも
最先端グラフィックスな格闘ゲームを!
D A Y 3 2 0 1 8 /5 /9 1 6 :3 0
冨澤 茂樹
株式会社バンダイナムコスタジオ
2000年旧ナムコに同業他社より中途入社
内製ミドルウェア初代NUライブラリの立ち上げ開発・保守を行う
最適化エンジニアとして複数プロジェクトにて60fps化を行う
現在 TEKKEN™ MOBILE プロジェクトにてグラフィックス担当
株式会社バンダイナムコスタジオ
冨澤 茂樹
TEKKEN™ MOBILE 開発体制
弊社バンクーバースタジオと本社東京スタジオとの共同開発
グラフィックス担当としての命題
TEKKEN™ MOBILE 制作にあたり
アーケード / PS4 / Xbox One / Steam
のプレイヤーさんが見劣りしないクォリティー
Unity 採用
Android の多種多様なデバイス
バージョン
チップセット
端末メーカー
キャリア
iOS も多種多様なデバイス
解像度
アスペクト比
経験者がいる
Unity 採用
Unity は賢い
Unity はなんでもやってくれる
ただし頼れば頼るほど CPU 負荷が増す
Unity 採用
そこで
Unity にすべて乗っかるのではなく
使う機能を取捨選択することにした
取捨選択の例
Unity のライト
Unity のリアルタイムライティングは行わない
静的なオブジェクト(ステージ)は事前ベイクライトマップ
動的なオブジェクト(キャラ)は独自ライティング
Unity の影
Unity のリアルタイムシャドウイングは行わない
静的なオブジェクト(ステージ)はライトマップにベイク
動的なオブジェクト(キャラ)は独自シャドウイング
本日のトピック
☆☆☆★ カスタムシェーダーを書く
☆☆★★ 自分でライトマップを貼る
☆★★★ 自分で影をレンダリングする
★★★★ モバイル用物理ベースレンダリング
カスタムシェーダーを書く
カスタムシェーダーを書く
Asset から Unlit Shader を作る
カスタムシェーダーを書く
• Surface Shader は扱いません
• Unity (OpenGL 系) ではフラグメントシェーダーと呼ぶところを
本セッションではピクセルシェーダーと呼ばせてください
• ShaderLab の記述スタイルは CGINCLUDE ~ ENDCG の間に
シェーダーコードを書きますがご了承ください(次ページ参照)
記述スタイル
Shader "BandaiNamc o/ Ho ge ho ge"
{
Propertie s
{
[NoScaleOffs et ] _MainTex("B a se Texture", 2D) = "white" {}
}
CGINCLUDE
#include "UnityCG.cginc"
ENDCG
SubShader
{
Tags { "Queue" = "Geometry" "IgnoreProj ec to r" = "True" "RenderType" = “Opaque” }
Pass
{
CGPROGRAM
#pragma vertex VSmain
#pragma fragment PSmain
#pragma target 2.5
ENDCG
}
}
CustomEditor "BandaiNa mco .Ho g eho g eSh ad erInsp ec to r"
}
ここにコードを書く
関数名は VSmain と PSmain
インスペクターは大事
適宜 Ztest や Cull を追加
シェーダー名は重複しないように
記述スタイル
sampler2D _MainTex;
struct VSinput
{
float4 position : POSITION;
float2 uv : TEXCOORD0;
};
struct PSinput
{
half2 uv : TEXCOORD0;
float4 position : SV_POSITION;
};
PSinput VSmain(V Sinp ut i)
{
PSinput o;
o.position = UnityObjectToClipP os (i.posit ion);
o.uv = i.uv;
return o;
}
fixed4 PSmain(PSinput i) : SV_Target
{
return fixed4(tex2D(_Ma inTe x, i.uv));
}
自分でライトマップを貼る
ライトマップ
Unity 内蔵の Enlighten でベイク
Platform が Android / iOS だと LDR になってしまう
PC, Mac & Linux Standalone なら HDR になる!
HDR のテクスチャーを Android / iOS に持ち込む話は後ほど
PC, Mac & Linux Standalone で描画
struct VSinput
{
float4 position : POSITION;
float2 uv : TEXCOORD0;
float2 lmuv : TEXCOORD1;
};
PSinput VSmain(VSinput i)
{
PSinput o;
o.position = UnityObjectToClipPos(i.posit ion);
o.uv.xy = i.uv;
o.uv.zw = i.lmuv * unity_LightmapST.xy + unity_LightmapST.zw;
return o;
}
PC, Mac & Linux Standalone でそのまま描画するなら簡単
UV値は TEXCOORD1 に入ってくる
スケールオフセットは unity_LightmapST に入ってくる
PC, Mac & Linux Standalone で描画
sampler2D _MainTex;
struct PSinput
{
half4 uv : TEXCOORD0;
float4 position : SV_POSITION;
};
fixed4 PSmain(PSinput i) : SV_Target
{
half4 result = tex2D(_MainTex, i.uv.xy);
result.rgb *= DecodeLightmap(tex2D(u nity_L ight map , i.uv.zw);
return fixed4(result);
}
PC, Mac & Linux Standalone でそのまま描画するなら簡単
テクスチャーは unity_Lightmap に入っている
フェッチした値を DecodeLightmap() 関数を通すことで値を得る
Android / iOS に HDR テクスチャーを持ち込む
トーンマップで使われる Reinhard 変換を利用[Reinhard et al. 2002]
Reinhard 変換をかけ LDR で保存
シェーダー内で Reinhard 逆変換をかけて HDR に戻す
𝑓 𝑥 =
𝑥
1 + 𝑥
𝑓 𝑥 =
𝑥
1 − 𝑥
, 𝑥 ≠ 1
Android / iOS に HDR テクスチャーを持ち込む
PSinput VSmain(VSinput i)
{
PSinput o;
o.position = mul(UNITY_MATRIX_P, i.position);
o.uv = i.uv;
return o;
}
fixed4 PSmain(PSinput i) : SV_Target
{
fixed4 lightmap = tex2D(_MainTex, i.uv);
half3 result = DecodeLightmap(lightmap);
result = result / (1.0h + result);
return fixed4(result, 1);
}
ライトマップに Reinhard 変換をかけるシェーダーを書く
Android / iOS に HDR テクスチャーを持ち込む
Material material = new Material(m_shader);
Texture2D inTex = LightmapSettings.lightmaps[ind ex].lightmapCo lor
RenderTexture tmpRT = RenderTexture.GetTempor ary(inTe x.w idth, inTex.height, 0,
RenderTextureFormat.ARG B32);
Graphics.Blit(inT ex, tmpRT, material);
outTex.ReadPixels(new Rect(0, 0, inTex.width, inTex.height), 0, 0, false);
outTex.Apply();
RenderTexture.Rele aseTe mpor ary(t mpRT );
先ほどのシェーダーでライトマップを保存
Android / iOS に HDR テクスチャーを持ち込む
sampler2D _MainTex;
sampler2D _Lightmap;
fixed4 PSmain(PSinput i) : SV_Target
{
half4 result = tex2D(_MainTex, i.uv.xy);
half3 lightmap = tex2D(_Lighttmap, i.uv.zw);
lightmap = lightmap / (1.0h - min(0.9h, lightmap));
result.rgb *= lightmap;
return fixed4(result);
}
描画シェーダー
テクスチャーは自分で変数を用意する
フェッチした値を Reinhard 逆変換をかける
自分で影をレンダリング
影のレンダリング
• まずはシャドウマップ
• 基本的には光源位置からライティング方向へデプスを
レンダリングする(シャドウカメラ≠視点カメラ)
• 近年ではきれいな影をレンダリングするためにデプス以外にも
いくつかのパラメーターを記録するようになってきた
• モバイルではまだしばらくデプスシャドウマップか
シャドウマップ
シャドウマップ技法の種類[Wimmer et al. 2004]
USM
PSM
LSPSM
[Wimmer et al. 2004]より引用
USM
Uniform Shadow Maps
平行投影でシャドウマップをレンダリングする
メリット
簡単
デメリット
視点近くの影の解像度が粗くなる
PSM
Perspective Shadow Maps
視点カメラの Perspective を適用し視点近くの解像度を上げる
メリット
視点近くの解像度が高い
デメリット
場合分けする必要がある
視点より後ろの影を落とすオブジェクトの処理ができない
LSPSM
Light Space Perspective Shadow Maps
視点近くほど解像度が高く
必要な全てのオブジェクトをレンダリングする
メリット
視点近くの解像度が高い
全てのオブジェクトに対応する
デメリット
視線ベクトルと光線ベクトルが平行に近いと使用できない
影をレンダリングする手順
• シャドウマップ用カメラ(シャドウカメラ)を作る
• シャドウマップ用 RenderTexture を作る
• シャドウマップレンダリング用シェーダーを作る
• シャドウマップレンダリングするシェーダーにキーワードをつける
• メインカメラ(視点カメラ)に C# コードをコンポーネントとして追加
• 影を受けるシェーダーに影をレンダリングするコードを追加
シャドウカメラを作る
GameObject の Camera を追加
disable にしておく
orthographic に設定
範囲を調整
シャドウマップ用 RenderTexture を作る
Assets で RenderTexture を作る
フォーマットによる違い
RenderTextureFormat.Depth
自分でデプスを比較して処理する必要がある
RenderTextureFormat.Shadowmap
ハードウェア比較機能を使うことができる
後者が圧倒的にラク
シャドウマップレンダリング用シェーダーを作る
struct VSinput
{
float4 position : POSITION;
};
struct PSinput
{
float4 position : SV_POSITION;
};
PSinput VSmain(VSinput i)
{
PSinput o;
o.position = UnityObjectToClipPos(i.posit ion);
return o;
}
fixed4 PSmain(PSinput i) : SV_Target
{
return fixed4(0,0,0,0);
}
シャドウマップレンダリングするシェーダーにキーワードをつける
SubShader
{
Tags { "RenderType" = “CastShadow” }
Pass
{
…
}
}
シャドウマップ用シェーダーと
影を落とすモデルのシェーダーに
“RenderType” のキーワードを同じものを指定する
視点カメラに C# コードをコンポーネントとして追加
public class CastShadow : MonoBehaviour {
public Camera m_shadowCamera;
public Shader m_shader;
public RenderTexture m_renderTexture;
public Renderer[] m_receivers;
void OnPreRender()
{
m_shadowCamera.tar getTexture = m_renderTexture;
m_shadowCamera.Re nderW ithShade r(m_shader , “RenderType”);
}
// 続く
CastShadow.cs を追加
OnPreRender メッセージで
Camera.RenderWithShader() を呼ぶ
視点カメラに C# コードをコンポーネントとして追加
void Update()
{
Matrix4x4 shadowView = m_shadowCamera.worldTo CameraM atrix;
Matrix4x4 shadowProjection = m_shadowCamera.proje ctionM atrix;
Matrix4x4 receiveMatrix = shadowProjection * shadowView;
MaterialPropertyBlo ck prop = new MaterialPropertyBlock();
prop.SetTexture("_ShadowTe x", m_renderTexture);
prop.SetMatrix("_ShadowM atrix", receiveMatrix);
foreach (Renderer r in m_receivers) {
r.SetPropertyBlock(p rop);
}
}
}
View マトリクスと Projection マトリクスを得て乗算する
シャドウマップとマトリクスを影を受けるレンダラーに渡す
影を受けるシェーダーに影をレンダリングするコードを追加
matrix4x4 _ReceiveMatrix;
…
half4 shadowpos : TEXCOORDn;
…
PSinput VSmain(VSinput i)
{
PSinput o;
…
float4 worldPos = mul(unity_ObjectToWorld, i.position);
o.shadowpos = mul(_ReceiveMatrix, worldPos);
…
return o;
}
頂点シェーダー
受け取ったマトリクスで position を変換
影を受けるシェーダーに影をレンダリングするコードを追加
UNITY_DECLARE_SHADOWMAP(_ShadowT ex);
fixed4 PSmain(PSinput i) : SV_Target
{
half4 shadowpos;
shadowpos.xyz = i.shadowpos.xyz * 0.5h + 0.5h;
shadowpos.w = i.shadowpos.w;
#if defined(UNITY_REVERSED_ Z)
shadowpos.z = 1.0h - shadowpos.z;
#endif
half shadow = UNITY_SAMPLE_SHADOW (_ShadowT ex, shadowpos);
//half shadow = UNITY_SAMPLE_SHADOW_ PROJ (_Shad owTex, shadowpos);
return fixed4(shadow.xxx, 1);
}
ピクセルシェーダー
UNITY_SAMPLE_SHADOW() マクロで比較結果が返ってくる
影の中 == 0, 影の外 == 1
自分でマトリクスを作る
void Update()
{
Matrix4x4 shadowView = CreateLookAt();
Matrix4x4 shadowProjection = CreateProjection();
Matrix4x4 receiveMatrix = shadowProjection * shadowView;
Matrix4x4 castMatrix = GL.GetGPUProjectionMatr ix(sh adowPr oje ction , true) * shadowView;
MaterialPropertyBlo ck prop = new MaterialPropertyBlock();
…
}
}
自分でマトリクスを作れば各技法を実装可能
ただし GL.GetGPUProjectionMatrix() 関数を呼ぶ必要あり
自分でマトリクスを作る
matrix4x4 _CastMatrix;
PSinput VSmain(VSinput i)
{
PSinput o;
float4 worldPos = mul(unity_ObjectToWorld, i.position);
o.position = mul(_CastMatrix, worldPos);
return o;
}
シャドウマップ用シェーダーも
受け取ったマトリクスで position を変換するようにする
おまけ
void OnPreRender()
{
m_commandBuffer .Clear();
m_commandBuffer .SetRende rTar get(m_renderT arget Id );
m_commandBuffer .ClearRe nderT arget(true , false, Color.clear, 1.0f);
foreach (MeshRenderer r in m_renderers) {
m_commandBuffer .Dr awRe nder er(r , m_material);
}
Graphics.Execut eCo mmand Buff er(m_ command Bu ffer );
}
さらに最適化した結果こうなりました
OnPreRender メッセージで
Graphics.ExecuteCommandBuffer() を呼ぶ
モバイル用物理ベースレンダリング
物理ベースレンダリング?
本アプリはフォトリアルを目指していない
光学物理シミュレーションを目指していない
物理ベースレンダリングで使われる関数 BRDF を利用
BRDF 法レンダリング
BRDF : Bidirectional Reflectance Distribution Function
BRDF 法レンダリングの導入
リニア空間で HDR で行う
ハイスペックハードウェアでは
ハードウェアガンマ補正あり
HDR レンダーターゲットあり
マルチプルレンダーターゲットあり
MRT 使えますが Forward Rendering で考えてみます
ハイスペックの BRDF 法レンダリング
HDR で計算して
HDR レンダーターゲットに
書き込む
ハードウェアリニア変換
BRDF 計算
HDR レンダーターゲット
sRGB テクスチャー
ハイスペックの BRDF 法レンダリング
全て描き終わったら
HDR レンダーターゲットの
輝度から目標の輝度を求め
トーンマッピングを行う
輝度判定
トーンマッピング
ハードウェアガンマ補正
LDR フレームバッファ
モバイルのスペック
どれくらいのスペックに合わせるか?
OpenGL ES バージョン?
Vulkan / Metal に対応するか?
GPU メーカー固有フォーマットに対応するか?
想定スペック
ハードウェアガンマ補正なし
テクスチャーは ETC / ETC2 / PVRTC
メーカー固有フォーマットや新しい ASTC は使わない
アルファチャンネルは本来の透過度で使用
ETC はアルファチャンネルなし
PVRTC はアルファチャンネル含めると画質が落ちる
マルチプルレンダーターゲットなし
レンダーターゲットは RGBA8
GPU のスペックを考慮
モバイル版 BRDF 法レンダリング
モバイル(想定スペック)では
入力(テクスチャー)はハードウェアガンマ補正なしで LDR
出力(レンダーターゲット)もハードウェアガンマ補正なしで LDR
入力出力ともアルファチャンネルは本来の透過度で使用
ならば一つのシェーダー内で先ほどの全てを賄おう
モバイル版 BRDF 法レンダリング
通常テクスチャー HDR 向けテクスチャー
リニア空間へ変換 Reinhard 逆変換
BRDF 計算
テクスチャーはリニア空間で HDR に変換し HDR で計算
モバイル版 BRDF 法レンダリング
あらかじめ設定しておいた
EV 補正値で露出補正
同時にトーンマッピング
同時にガンマ補正
露出補正
トーンマッピング
sRGB へ変換
LDR フレームバッファ
ガンマ補正
“UnityCG.cginc” のインライン関数を利用
sRGB からリニア空間への変換
リニア空間から sRGB への変換
half4 srgbColor = tex2D(_MainTex, i.uv);
half3 linearColor = GammaToLinearSpace(srgbColor.rgb);
half3 srgbColor = LinearToGammaSpace(linearColor);
露出補正 & トーンマッピング
EV ± 値はあらかじめアーティストが設定
露出補正トーンマッピングは
Filmic Tonemapping 近似式を使用[Hejl 2010]
half3 x = max(0, linearColor – 0.004h);
half3 result = (x * (6.2h * x + 0.5h)) / (x * (6.2h * x * 1.7h) + 0.06h);
モバイル版 BRDF 法レンダリング
これでひとつのシェーダー内で BRDF 法レンダリングができた
他のリニア空間ではないオブジェクトとも調和
アルファブレンディングとの相性も良し
まとめ
• Unity はかなりカスタマイズができる
• CPU が重ければ自分で書くこともできる
• 自分でシェーダーを書くと GPU 負荷コントロールにもなる
参考文献
E. Reinhard, M. Stark, P. Shirley, J. Ferwerda “Photographic Tone Reproduction for
Digital Images” SIGGRAPH 2002
http://www.cs.utah.edu/~reinhard/cdrom/
M. Wimmer, D. Scheizer, W. Purgathofer “Light Space Perspective Shadow Maps”
Eurographics Symposium on Rendering 2004
https://www.cg.tuwien.ac.at/research/vr/lispsm/
J. Hejl, R. Burgess-Dawson, J. Hable “Filmic Tonemapping for Real-time Rendering”
SIGGRAPH 2010 Color Course by H.P. Duiker
https://www.slideshare.net/hpduiker/filmic-tonemapping-for-realtime-rendering-
siggraph-2010-color-course
ご清聴ありがとうございました

【Unite Tokyo 2018】カスタムシェーダーでモバイルでも最先端グラフィックスな格闘ゲームを!