はじめに
自前のトゥーンシェーダを作りたくて、まずは Unity 付属のトゥーンシェーダについて調べてみました。Unity のシェーダについては以前の記事をサラッと読んでいただけると理解しやすいと思います。
環境
- Unity 5.0.1f1
追記:2021/02/11
新しいバージョンの Unity には Effects パッケージが標準では含まれておらず、アセットストアからダウンロードできる Standard Assets へと移動しました。
Toon/Basic
トゥーンシェーダは Assets > Import Package > Effects
アセットについてきます。まずは一番シンプルな Toon/Basic
を見てみます。
コードを見てみます。
Shader "Toon/Basic" { Properties { _Color ("Main Color", Color) = (.5,.5,.5,1) _MainTex ("Base (RGB)", 2D) = "white" {} _ToonShade ("ToonShader Cubemap(RGB)", CUBE) = "" { } } SubShader { Tags { "RenderType"="Opaque" } Pass { Name "BASE" Cull Off CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fog #include "UnityCG.cginc" sampler2D _MainTex; samplerCUBE _ToonShade; float4 _MainTex_ST; float4 _Color; struct appdata { float4 vertex : POSITION; float2 texcoord : TEXCOORD0; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; float2 texcoord : TEXCOORD0; float3 cubenormal : TEXCOORD1; UNITY_FOG_COORDS(2) }; v2f vert (appdata v) { v2f o; o.pos = mul (UNITY_MATRIX_MVP, v.vertex); o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex); o.cubenormal = mul (UNITY_MATRIX_MV, float4(v.normal,0)); UNITY_TRANSFER_FOG(o,o.pos); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col = _Color * tex2D(_MainTex, i.texcoord); fixed4 cube = texCUBE(_ToonShade, i.cubenormal); fixed4 c = fixed4(2.0f * cube.rgb * col.rgb, col.a); UNITY_APPLY_FOG(i.fogCoord, c); return c; } ENDCG } } Fallback "VertexLit" }
とてもシンプルなコードです。何をやっているかというと、シンプルな頂点 / フラグメントシェーダに以下の様なキューブマップテクスチャを入力として与えている形になります。
上記は標準の Effects
アセットをインポートした時についてくるテクスチャです。Unity 5 からはキューブマップテクスチャとして入力された画像のアスペクト比を見て、インポート時に自動的にキューブマップテクスチャにしてくれます。上記は 128x128 と 1:1 なテクスチャなのでスフィアマップなテクスチャと認識されます。
- Cubemaps - Unity マニュアル
- Importing cubemaps from single images · Aras' website
- What's new in Unity 5.0 - Unity (Improved and extended cubemap workflow)
スフィアマップとして扱われるので、中心の色が上側、周辺の色が下側として描画されて良い感じに影ができるわけです。Toon/Basic
は計算量的には軽量ですが、ライトの位置に関係なく下側に影が描画される形になります(試しにディレクショナルライトをグリグリしてみても影の位置は変わりません)。
ちなみに本エントリの主題からは外れますが、#pragma multi_compile_fog
や UNITY_FOG_COORDS
、 UNITY_TRANSFER_FOG
は Unity 5 から Vertex / Fragment シェーダへ Fog の効果を追加する便利機能のようです。
Toon/Lit
ライトの影響を受けるトゥーンシェーダとして Toon/Lit
が用意されています。
Toon Ramp
というプロパティに以下の様なテクスチャを与えています。
コードを見てみます。
Shader "Toon/Lit" { Properties { _Color ("Main Color", Color) = (0.5,0.5,0.5,1) _MainTex ("Base (RGB)", 2D) = "white" {} _Ramp ("Toon Ramp (RGB)", 2D) = "gray" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf ToonRamp sampler2D _Ramp; // custom lighting function that uses a texture ramp based // on angle between light direction and normal #pragma lighting ToonRamp exclude_path:prepass inline half4 LightingToonRamp (SurfaceOutput s, half3 lightDir, half atten) { #ifndef USING_DIRECTIONAL_LIGHT lightDir = normalize(lightDir); #endif half d = dot (s.Normal, lightDir)*0.5 + 0.5; half3 ramp = tex2D (_Ramp, float2(d,d)).rgb; half4 c; c.rgb = s.Albedo * _LightColor0.rgb * ramp * (atten * 2); c.a = 0; return c; } sampler2D _MainTex; float4 _Color; struct Input { float2 uv_MainTex : TEXCOORD0; }; void surf (Input IN, inout SurfaceOutput o) { half4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb; o.Alpha = c.a; } ENDCG } Fallback "Diffuse" }
Surface Shader でカスタムライティングモデルを利用した実装になっています。
- サーフェスシェーダーでのカスタムライティングモデル - Unity マニュアル
- サーフェスシェーダーの記述 - Unity マニュアル
- サーフェスシェーダーライティングの例 - Unity マニュアル
- [Unity] surface shaderでライティング - Qiita
Surface Shader は、表面の材質を記述する Surface Function と、ライティングとの相互作用を記述する Lighting Model からなるシェーダです。通常は Lighting Model は組み込みの Lambert(Diffuse)か Blingphone(Specular)を指定しますが、これらで表現できない今回のような場合に、カスタムライティングモデルを利用する形になります。カスタムライティングは Lighting* 接頭辞から始まる関数を作成し、#pragma surface
でその関数を使用するライティングモデルとして指定します。
それではコードを見てみます。ミソは以下の行で、ここで光のあたっている量を計算しています。
half d = dot (s.Normal, lightDir)*0.5 + 0.5;
簡単に説明すると、面の法線方向と入射光の方向を比較し、光源方向と表面の法線方向が一致しているほど 1.0 に近づき、一致していない(逆を向いている)ほど 0.0 に近づきます。図解した詳細は以下のページが詳しいです。
そして、この値 d
をテクスチャの UV 座標 (d, d)
として与え、影の強さとして取り出し、これと表面の色、ライトの色、減衰率を掛けて最終的な色として採用します。
half3 ramp = tex2D (_Ramp, float2(d,d)).rgb;
c.rgb = s.Albedo * _LightColor0.rgb * ramp * (atten * 2);
なお、Forward 用に #pragma lighting
で Diferred で効かないように記述されています。また、減衰率(attenuation)を示す atten
が 2 倍で与えられるのは、Unity 4 系までの負債で Unity 5 からは要らないのですが、おそらく修正し忘れではないかと思います(どっかで報告できないのかな)。
ちなみに影ありのディレクショナルライトと一緒にそのまま使うと結構汚い感じになってしまいます。
影を消せば綺麗にはなるのですが、影を表示したい場合の解決方法はちょっと思いつかないので保留です。。
Toon/Basic Outine
Toon/Basic Outline
と Toon/Lit Outline
がありますが、両方共 2-Pass なシェーダを利用して再現しています。
Shader "Toon/Basic Outline" { Properties { _Color ("Main Color", Color) = (.5,.5,.5,1) _OutlineColor ("Outline Color", Color) = (0,0,0,1) _Outline ("Outline width", Range (.002, 0.03)) = .005 _MainTex ("Base (RGB)", 2D) = "white" { } _ToonShade ("ToonShader Cubemap(RGB)", CUBE) = "" { } } CGINCLUDE #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; UNITY_FOG_COORDS(0) fixed4 color : COLOR; }; uniform float _Outline; uniform float4 _OutlineColor; v2f vert(appdata v) { v2f o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); float3 norm = mul ((float3x3)UNITY_MATRIX_IT_MV, v.normal); float2 offset = TransformViewToProjection(norm.xy); o.pos.xy += offset * o.pos.z * _Outline; o.color = _OutlineColor; UNITY_TRANSFER_FOG(o,o.pos); return o; } ENDCG SubShader { Tags { "RenderType"="Opaque" } UsePass "Toon/Basic/BASE" Pass { Name "OUTLINE" Tags { "LightMode" = "Always" } Cull Front ZWrite On ColorMask RGB Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fog fixed4 frag(v2f i) : SV_Target { UNITY_APPLY_FOG(i.fogCoord, i.color); return i.color; } ENDCG } } Fallback "Toon/Basic" }
UsePass "Toon/Basic/BASE"
で先ほどの Toon/Basic
シェーダを使って描画します。そして次に2つ目の Pass でアウトラインを描画します。やっていることとしては WebGL の解説ですが、アルゴリズムは同じなので wgld.org さんの図を見ると分かりやすいと思います。
ざっくり言うと、ちょっと拡大して裏側だけ描画するようにして指定されたアウトライン色で塗りつぶす、という感じです。拡大する部分のコードを見てみます。
float3 norm = mul ((float3x3)UNITY_MATRIX_IT_MV, v.normal); float2 offset = TransformViewToProjection(norm.xy);
ここの計算はちょっと複雑なのですが、要は法線の方向を求めています。v.normal
は元々のモデルの法線方向ですが、ライティングするためには回転・スケーリングされた後の法線が欲しいわけです。回転は単純に回転すればよいのですが、スケーリングに関しては面倒で、例えば球をペチャンコに潰すと法線は上面は (0, 1, 0)
、下面は (0, -1, 0)
となり、方向が変わるのが想像できると思います。また法線の大きさも正規化しないと行けません。このあたりを良い感じに計算しているのが上記2行なわけですね。ちなみに UNITY_MATRIX_IT_MV
はモデルビュー転置行列の逆行列(IT = Inverse Transposed)です。
z 方向は拡大しても描画に関係ないので、得られた xy 成分だけを offset
として取り出し o.pos
に加えます。
o.pos.xy += offset * o.pos.z * _Outline;
ここで、o.pos.z
をかけているのは、物体を遠ざけても同じサイズのアウトラインの太さを得るためです。o.pos.z
を取り除くと近づくほど太く、遠ざかるほど細くなるアウトライン(= 物体の縁をマーカで黒く塗ったような感じ)になります。
これで横と縦に膨らんだモデルが得られます。後は _OutlineColor
で塗りつぶして描画すれば、めでたくアウトラインが描画できるわけです。ただし、この方法には弱点があって、法線方向に膨らめているだけなので直角に交わる面付近などで線が途切れてしまいます。
Toon/Lit Outline
Toon/Lit Outline
では、ここで作った OUTLINE
を UsePass
してる実装になってます。再利用出来るの良いですね。
Shader "Toon/Lit Outline" { Properties { _Color ("Main Color", Color) = (0.5,0.5,0.5,1) _OutlineColor ("Outline Color", Color) = (0,0,0,1) _Outline ("Outline width", Range (.002, 0.03)) = .005 _MainTex ("Base (RGB)", 2D) = "white" {} _Ramp ("Toon Ramp (RGB)", 2D) = "gray" {} } SubShader { Tags { "RenderType"="Opaque" } UsePass "Toon/Lit/FORWARD" UsePass "Toon/Basic Outline/OUTLINE" } Fallback "Toon/Lit" }
改造してみる
色々分かったところで自前でトゥーンシェーダを書いてみます。以下のページで紹介されている方法で 1 Pass でアウトラインを描画してみます。
具体的には、Toon/Lit
では法線 - 光源方向の内積の値のみ使ってテクスチャから1次元で色を取り出していましたが、法線 - 視線方向の内積の値も使って2次元でテクスチャから色を取り出します。
Shader "Toon/LitWithOutline" { Properties { _Color ("Main Color", Color) = (0.5, 0.5, 0.5, 1) _MainTex ("Base (RGB)", 2D) = "white" {} _Ramp ("Toon Ramp (RGB)", 2D) = "gray" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf ToonRamp sampler2D _Ramp; sampler2D _MainTex; half4 _Color; inline half4 LightingToonRamp(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten) { #ifndef USING_DIRECTIONAL_LIGHT lightDir = normalize(lightDir); #endif half u = dot(s.Normal, lightDir) * 0.5 + 0.5; half v = dot(s.Normal, viewDir); half3 ramp = tex2D(_Ramp, half2(u, v)).rgb; half4 c; c.rgb = s.Albedo * _LightColor0.rgb * ramp; c.a = 0; return c; } struct Input { half2 uv_MainTex : TEXCOORD0; }; void surf (Input IN, inout SurfaceOutput o) { half4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb; o.Alpha = c.a; } ENDCG } Fallback "Diffuse" }
カスタムライティングモデルで指定する Lighting Function では、引数を追加すれば視線方向である half3 viewDir
を受け取ることができるので、これを利用しています。入力する画像は以下の様なものです。
結果がコレです。
...うーん、微妙ですね。
おまけ:Unity 4.x から Unity 5.x へのトゥーンシェーダの移行
これまで Unity 4.x 系でトゥーンシェーダを使っていたプロジェクトを Unity 5 に持ってくるとアウトラインが極太で変になると思います。これは以下の移行ガイドに書いてあるように、法線方向へのふくらめ度合いの計算時に normalize
するように修正してみてください。
// float3 norm = mul ((float3x3)UNITY_MATRIX_IT_MV, v.normal);
float3 norm = normalize(mul ((float3x3)UNITY_MATRIX_IT_MV, v.normal));
おわりに
仕組みがわかると色々と工夫しがいがあると思いますので、みなさんも色々と工夫したトゥーンシェーダ作ってみてください。