
マイケル
みなさんこんにちは!
マイケルです!
マイケルです!

エレキベア
こんにちクマ〜〜〜

マイケル
新年一発目の記事は、みんな大好きトゥーンシェーダーの実装だ!

エレキベア
ついに来たクマか・・・

マイケル
トゥーン調陰影から、輪郭線描画まで行って、
更にその改善まで行うので本格的な内容になっています!
更にその改善まで行うので本格的な内容になっています!

エレキベア
それは楽しみクマ〜〜〜
トゥーン調の陰影の実装

マイケル
まずはトゥーン調の陰影の実装から!
今回は
・コード内で設定する方法
・ランプテクスチャで設定する方法
の2つを解説します!
今回は
・コード内で設定する方法
・ランプテクスチャで設定する方法
の2つを解説します!
コード内で設定する方法

マイケル
コード内で陰影を設定する方法として、法線と光の方向の内積の値からしきい値を設ける方法があります。
実装内容は下記になります。
実装内容は下記になります。
Shader "Unlit/ToonSimpleShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.normal = UnityObjectToWorldNormal(v.normal);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// しきい値を設けて陰影を計算
half nl = max(0, dot(i.normal, _WorldSpaceLightPos0.xyz));
if (nl <= 0.01f) nl = 0.3f;
else if (nl <= 0.3f) nl = 0.5f;
else nl = 1.0f;
// テクスチャカラーに乗算
fixed4 col = tex2D(_MainTex, i.uv);
col.rgb *= nl;
return col;
}
ENDCG
}
}
}

マイケル
こちらはオモチャラボさんの下記の記事を参考にさせていただいています!
輪郭線の実装もこちらの記事を見させていただいています!
輪郭線の実装もこちらの記事を見させていただいています!
【Unityシェーダ入門】アウトラインシェーダを作る | おもちゃラボ

マイケル
陰影の実装部分は下記になっています。
fixed4 frag (v2f i) : SV_Target
{
// しきい値を設けて陰影を計算
half nl = max(0, dot(i.normal, _WorldSpaceLightPos0.xyz));
if (nl <= 0.01f) nl = 0.3f;
else if (nl <= 0.3f) nl = 0.5f;
else nl = 1.0f;
// テクスチャカラーに乗算
fixed4 col = tex2D(_MainTex, i.uv);
col.rgb *= nl;
return col;
}
ENDCG
}
↑内積の値からしきい値を設ける
エレキベア
シンプルな実装クマね

マイケル
こちらの実行結果は下記になります。
問題なく陰影が付き、光の方向によって変わるのも確認できます。
問題なく陰影が付き、光の方向によって変わるのも確認できます。



エレキベア
おお〜〜〜トゥーンっぽいクマ〜〜
ランプテクスチャで設定する方法

マイケル
前述の内容でもコードを書き換えれば陰影も変えることができますが、
ランプテクスチャとして用意して参照することで、テクスチャを変えるだけで陰影を変えることができるようになります。
ランプテクスチャとして用意して参照することで、テクスチャを変えるだけで陰影を変えることができるようになります。


エレキベア
このテクスチャの色を陰影とするクマね

マイケル
具体的な実装は下記になります。
Shader "Unlit/ToonRampShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_RampTex ("Ramp", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv_MainTex : TEXCOORD0;
float2 uv_RampTex : TEXCOORD1;
};
struct v2f
{
float4 vertex : SV_POSITION;
float3 normal : NORMAL;
float2 uv_MainTex : TEXCOORD0;
float2 uv_RampTex : TEXCOORD1;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _RampTex;
float4 _RampTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.normal = mul(unity_ObjectToWorld, v.normal);
o.uv_MainTex = TRANSFORM_TEX(v.uv_MainTex, _MainTex);
o.uv_RampTex = TRANSFORM_TEX(v.uv_RampTex, _RampTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// ライト向き、法線の内積を0.0〜1.0にする
const half nl = dot(i.normal, _WorldSpaceLightPos0.xyz) * 0.5 + 0.5;
// RampMapから取り出して乗算
const fixed3 ramp = tex2D(_RampTex, fixed2(nl, 0.5)).rgb;
fixed4 col = tex2D(_MainTex, i.uv_MainTex);
col.rgb *= ramp;
return col;
}
ENDCG
}
}
}

マイケル
陰影の描画部分は下記!
ランプテクスチャの縦方向の中心と、内積の結果を座標として参照するようにしています。
ランプテクスチャの縦方向の中心と、内積の結果を座標として参照するようにしています。
fixed4 frag (v2f i) : SV_Target
{
// ライト向き、法線の内積を0.0〜1.0にする
const half nl = dot(i.normal, _WorldSpaceLightPos0.xyz) * 0.5 + 0.5;
// RampMapから取り出して乗算
const fixed3 ramp = tex2D(_RampTex, fixed2(nl, 0.5)).rgb;
fixed4 col = tex2D(_MainTex, i.uv_MainTex);
col.rgb *= ramp;
return col;
}
ENDCG
}

エレキベア
さっきのコードで書いてた部分をテクスチャ参照にしたわけクマね

マイケル
こちらの実行結果は下記!


エレキベア
こっちもいい感じクマね

マイケル
試しに下記のようなテクスチャに変えてみると、陰影結果も変わることが確認できます!



エレキベア
緑っぽくなったクマ〜〜〜
輪郭線の実装
背面法を使用する方法

マイケル
陰影ができたところで、次は輪郭線を実装します。
こちらは下記の記事でも少し紹介した、「背面法」という手法を使用した実装になります。
こちらは下記の記事でも少し紹介した、「背面法」という手法を使用した実装になります。

エレキベア
確か、モデルを膨脹して裏面を通常モデルの後ろに描画することで輪郭線っぽく見せる手法だったクマね


マイケル
その通り!
2回描画する必要があるから、パスも2つ分必要になってくるよ!
実装は下記になります!
2回描画する必要があるから、パスも2つ分必要になってくるよ!
実装は下記になります!
Shader "Unlit/ToonOutlineShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_RampTex ("Ramp", 2D) = "white" {}
_OutlineWidth("Outline Width", Float) = 0.04
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
// 背面
Pass
{
Cull Front
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
};
float _OutlineWidth;
v2f vert (appdata v)
{
v2f o;
v.vertex += float4(v.normal * _OutlineWidth, 0); // 法線方向に膨張
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return fixed4(0, 0, 0, 1); // 黒色で固定
}
ENDCG
}
// 前面
Pass
{
Cull Back
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv_MainTex : TEXCOORD0;
float2 uv_RampTex : TEXCOORD1;
};
struct v2f
{
float4 vertex : SV_POSITION;
float3 normal : NORMAL;
float2 uv_MainTex : TEXCOORD0;
float2 uv_RampTex : TEXCOORD1;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _RampTex;
float4 _RampTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.normal = UnityObjectToWorldNormal(v.normal);
o.uv_MainTex = TRANSFORM_TEX(v.uv_MainTex, _MainTex);
o.uv_RampTex = TRANSFORM_TEX(v.uv_RampTex, _RampTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// RampMapから取り出して乗算
const half nl = dot(i.normal, _WorldSpaceLightPos0.xyz) * 0.5 + 0.5;
const fixed3 ramp = tex2D(_RampTex, fixed2(nl, 0.5)).rgb;
fixed4 col = tex2D(_MainTex, i.uv_MainTex);
col.rgb *= ramp;
return col;
}
ENDCG
}
}
}

マイケル
アウトライン描画部分は下記のパスで、「Cull Front」指定することで裏面を描画するようにしています。
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
// 背面
Pass
{
Cull Front
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
};
float _OutlineWidth;
v2f vert (appdata v)
{
v2f o;
v.vertex += float4(v.normal * _OutlineWidth, 0); // 法線方向に膨張
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return fixed4(0, 0, 0, 1); // 黒色で固定
}
ENDCG
}

エレキベア
頂点座標は法縁方向に膨脹させているクマね

マイケル
こちらの描画結果は下記になります!


エレキベア
ちゃんと輪郭線クマ〜〜〜
でも、キューブの方が何か変になってるクマね
でも、キューブの方が何か変になってるクマね

マイケル
そう、背面法の欠点として、ハードエッジモデルの輪郭描画がおかしくなってしまうという問題があるんだ。
こちらの対処法はこの後解説します!
こちらの対処法はこの後解説します!
ハードエッジモデルへの対応

マイケル
ハードエッジモデルの輪郭線を綺麗に描画する方法として、
輪郭線描画の時だけソフトエッジの法線情報を参照するという方法があります。
輪郭線描画の時だけソフトエッジの法線情報を参照するという方法があります。

マイケル
そのため、モデルの情報としてソフトエッジの法線情報を別で持たせる方法がありますが、
今回は下記の記事で紹介されている「Unityスクリプトで法線情報をソフトエッジ情報に変換して頂点カラーに埋め込む」方法で実装してみます!
今回は下記の記事で紹介されている「Unityスクリプトで法線情報をソフトエッジ情報に変換して頂点カラーに埋め込む」方法で実装してみます!
輪郭線を少しだけ綺麗に描画する | シンソフィアではたらく人のブログ

マイケル
実装は下記になります!
同じ位置の頂点の法線座標を平均したものをそれぞれ設定する、といった内容になっています。
同じ位置の頂点の法線座標を平均したものをそれぞれ設定する、といった内容になっています。
using UnityEngine;
public class HardEdgeModel : MonoBehaviour
{
private void Awake()
{
EmbedSoftEdgeToVertexColor(gameObject);
}
/// <summary>
/// ソフトエッジ情報を頂点カラーに埋め込む
/// </summary>
/// <param name="obj"></param>
private static void EmbedSoftEdgeToVertexColor(GameObject obj)
{
var meshFilters= obj.GetComponentsInChildren<MeshFilter>();
foreach (var meshFilter in meshFilters)
{
var mesh = meshFilter.sharedMesh;
var normals = mesh.normals;
var vertices = mesh.vertices;
var vertexCount = mesh.vertexCount;
// ソフトエッジ法線情報の生成
var softEdges = new Color[normals.Length];
for (var i = 0; i < vertexCount; i++)
{
// 同じ位置の頂点の法線座標の平均を設定する
var softEdge = Vector3.zero;
for (var j = 0; j < vertexCount; j++)
{
var v = vertices[i] - vertices[j];
if (v.sqrMagnitude < 1e-8f)
{
softEdge += normals[j];
}
}
softEdge.Normalize();
softEdges[i] = new Color(softEdge.x, softEdge.y, softEdge.z, 0);
}
// 頂点カラーに埋め込む
mesh.colors = softEdges;
}
}
}

エレキベア
なるほどクマ〜〜〜
ソフトエッジのモデルはそういう意味だったのクマね
ソフトエッジのモデルはそういう意味だったのクマね

マイケル
あとは下記のように、頂点を膨脹する際に埋め込んだ頂点カラーを参照するようにすれば完成です!
Shader "Unlit/ToonOutlineHardShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_RampTex ("Ramp", 2D) = "white" {}
_OutlineWidth("Outline Width", Float) = 0.04
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
// 背面
Pass
{
Cull Front
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 color : COLOR;
};
struct v2f
{
float4 vertex : SV_POSITION;
};
float _OutlineWidth;
v2f vert (appdata v)
{
v2f o;
v.vertex += float4(v.color.rgb * _OutlineWidth, 0); // ソフトエッジ法線情報を使用する
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return fixed4(0, 0, 0, 1); // 黒色で固定
}
ENDCG
}
// 前面
・・・同様のため省略・・・
}
}

マイケル
こちらの描画結果は下記になります!


エレキベア
おお〜〜〜〜
ハードエッジのモデルも綺麗に描画されてるクマ〜〜〜
ハードエッジのモデルも綺麗に描画されてるクマ〜〜〜
アウトラインのマスク対応

マイケル
これで大体綺麗に描画できるようになったのですが、
それでも下記のようにどうしても汚く描画されてしまうことがあります。
それでも下記のようにどうしても汚く描画されてしまうことがあります。


エレキベア
うええぇ・・・・

マイケル
特にキャラクターの目元が顕著に出てきますね・・・
こちらの対処法として、輪郭線のアルファ値を記録したテクスチャを用意して参照することで、不要な部分は輪郭線描画しないといった手法があります。
こちらの対処法として、輪郭線のアルファ値を記録したテクスチャを用意して参照することで、不要な部分は輪郭線描画しないといった手法があります。



エレキベア
少々無理やりクマが、仕方ないクマね・・・

マイケル
こちらの実装は下記の通り!
法線膨脹時にテクスチャをalpha値として参照して掛け合わせるだけですね
法線膨脹時にテクスチャをalpha値として参照して掛け合わせるだけですね
Shader "Unlit/ToonOutlineAlphaShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_RampTex ("Ramp", 2D) = "white" {}
_AlphaTex ("Alpha", 2D) = "white" {}
_OutlineWidth("Outline Width", Float) = 0.04
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
// 背面
Pass
{
Cull Front
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 color : COLOR;
float2 uv_AlphaTex : TEXCOORD2;
};
struct v2f
{
float4 vertex : SV_POSITION;
};
sampler2D _AlphaTex;
float4 _AlphaTex_ST;
float _OutlineWidth;
v2f vert (appdata v)
{
v2f o;
float alpha = tex2Dlod(_AlphaTex, float4(v.uv_AlphaTex.xy, 0, 0)).r; // テクスチャからアルファ値を取得
v.vertex += float4(v.color.rgb * (_OutlineWidth * alpha.x), 0); // ソフトエッジ法線情報を使用する
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return fixed4(0, 0, 0, 1); // 黒色で固定
}
ENDCG
}
// 前面
・・・同様のため省略・・・
}
}

エレキベア
ちょいと味付けしたくらいクマね

マイケル
アルファテクスチャを適当に設定して描画した結果は下記になります!


エレキベア
おお〜〜〜綺麗になったクマ〜〜〜

マイケル
まだ少し汚い部分はあるけど、微調整すればよさそうだね!
おわりに

マイケル
というわけで今回はトゥーンシェーダーの実装でした!
どうだったかな??
どうだったかな??

エレキベア
案外簡単だったクマが、輪郭線を綺麗に描画するのが中々面倒クマね

マイケル
輪郭線描画は、万能な方法が見つかってないと言われるから一工夫必要だね・・・
ポストエフェクトで輪郭線描画する方法もあるみたいだから、その内実装してみたいね!
ポストエフェクトで輪郭線描画する方法もあるみたいだから、その内実装してみたいね!

マイケル
それでは今日はこの辺で!
アデューー!!
アデューー!!

エレキベア
クマ〜〜〜〜
【Unity】トゥーンシェーダーを一から自作する(陰影+輪郭線)【シェーダー】〜完〜
コメント