鉛筆画っぽい表現ができたらいいなーと思ってそれっぽいものを作ってみました.アウトラインを表示して、六段階に階調化してデッサンの線みたいなやつを表示してます。
これは3DSの脱出アドベンチャーシリーズのクロノテクトです. eショップサ終する前に全人類やってね
最終的に書いたものがこれ
Shader "Custom/PencilShader"
{
Properties
{
_MainTex ("MainTexture", 2D) = "white" {}
_PaperTex ("PaperTexture", 2D) = "white" {}
_Color ("Diffuse Color", Color) = (1, 1, 1, 1)
_OutlineWidth ("Outline", Range(0, 1)) = 0.1
_OutlineColor ("Outline Color", Color) = (0, 0, 0, 1)
_StrokeDensity ("Stroke Density", Range(1, 10)) = 5//線の密度
_BrightNess ("BrightNess", Range(0.5, 20)) = 1//影を明るく
[Toggle] _SelfShadow("Self Shadow", Float) = 1
[Toggle] _ConsiderNormal("Consider dot(Normal*Light)", Float) = 1
[Toggle] _Apply_Transparency("Apply Transparency", Float) = 0//MainTexのアルファでくり抜くか
_CutOut ("CutOut", Range(0, 1)) = 0.1//くり抜くアルファ値の上限
[Toggle] _UseGradation("Use Gradation", Float) = 0//階調化をなくす
[Toggle] _UseStroke("Use Stroke", Float) = 1//線を表示する
[Toggle] _Move("Move", Float) = 1
_Frec ("Frec", Range(0, 1)) = 0.5
_ShakeSize ("Shake Size", Range(0, 0.1)) = 0.015
_Stroke1 ("Stroke1", 2D) = "white" {}
_Stroke2 ("Stroke2 ", 2D) = "white" {}
}
SubShader
{
Tags
{
"Queue" = "AlphaTest"
"RenderType" = "AlphaTest"
}
LOD 100
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
bool _Move;
float _Frec;
bool _Apply_Transparency;
float _CutOut;
float _ShakeSize;
float rand(float2 co)
{
return frac(sin(dot(co.xy, float2(12.9898, 78.233))) * 43758.5453);
}
ENDCG
Pass
{
Name "OUTLINE"
Cull Front
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
float _OutlineWidth;
float4 _OutlineColor;
v2f vert (appdata v)
{
v2f o;
if(_Move){
float time = floor(sin(_Time.x)*_Frec*200);
float r = rand(float2(time,time))*_ShakeSize;
o.pos = UnityObjectToClipPos(v.vertex+ v.normal*_OutlineWidth/50) +r ;
}else{
o.pos = UnityObjectToClipPos(v.vertex+ v.normal*_OutlineWidth/50);
}
o.uv =v.texcoord;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = fixed4(_OutlineColor.rgb,1);
if(_Apply_Transparency) {
if(tex2D(_MainTex, i.uv).a <=_CutOut){
discard;
}
}
return col;
}
ENDCG
}
Pass
{
Name "BASE"
Tags
{
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase nolightmap nodirlightmap nodynlightmap novertexlight
#include "UnityLightingCommon.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
struct appdata {
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
float2 worldPos : TEXCOORD1;
half3 worldNormal:TEXCOORD2;
SHADOW_COORDS(3)
};
fixed4 _Color;
sampler2D _PaperTex;
float4 _PaperTex_ST;
sampler2D _Stroke1;
sampler2D _Stroke2;
float _StrokeDensity;
float _BrightNess;
bool _UseGradation;
bool _UseStroke;
bool _SelfShadow;
bool _ConsiderNormal;
v2f vert (appdata v)
{
v2f o;
if(_Move){
float time = floor(sin(_Time.x)*_Frec*200);
float r = rand(float2(time,time))*_ShakeSize;
o.pos = UnityObjectToClipPos(v.vertex)+r ;
o.worldPos= ComputeScreenPos(o.pos).xy + r;
}else{
o.pos = UnityObjectToClipPos(v.vertex);
o.worldPos= ComputeScreenPos(o.pos).xy;
}
o.uv =v.texcoord;
o.normal = UnityObjectToWorldNormal(v.normal);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
TRANSFER_SHADOW(o)
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
col *= fixed4(_Color.rgb,1);
if(_SelfShadow){
fixed4 shadow = SHADOW_ATTENUATION(i);
half NdotL = saturate(dot(i.worldNormal, _WorldSpaceLightPos0.xyz));
fixed4 diff = NdotL * _LightColor0;
col *= diff * shadow;
}
col *= _BrightNess;
half nl = saturate(dot(i.normal, _WorldSpaceLightPos0.xyz)*col.rgb);
if(!_ConsiderNormal){
nl = col.rgb;
}
fixed4 col2 =tex2D(_PaperTex, i.uv)*1.1;
fixed2 scrPos = i.worldPos.xy* _StrokeDensity;
if(_UseGradation){
if( nl <= 0.2f ){
col2 *= tex2D(_Stroke1, scrPos)*nl*4;
}
else if(nl <= 0.7f){
col2 *= tex2D(_Stroke2, scrPos)*nl*2;
}
}else{
if( nl <= 0.01f ){
col2 *= tex2D(_Stroke1, scrPos)*0.5;
}
else if( nl <= 0.1f ){
col2 *= tex2D(_Stroke1, scrPos)*0.7;
}
else if( nl <= 0.2f ){
col2 *= tex2D(_Stroke1, scrPos)*0.9;
}
else if( nl <= 0.3f ){
col2 *= tex2D(_Stroke2, scrPos)*0.8;
}
else if(nl <= 0.4f){
col2 *= tex2D(_Stroke2, scrPos)*1;
}
else if(nl <= 0.5f){
col2 *= tex2D(_Stroke2, scrPos)*1.3;
}
}
if(!_UseStroke){
col2.rgb = dot(col.rgb, fixed3(0.3, 0.59, 0.11));
}
if(_Apply_Transparency) {
if(tex2D(_MainTex, i.uv).a <=_CutOut){
discard;
}
}
return col2;
}
ENDCG
}
Pass
{
Tags{ "LightMode"="ShadowCaster" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_shadowcaster
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 texcoord : TEXCOORD0;
};
struct v2f
{
V2F_SHADOW_CASTER;
};
v2f vert (appdata v)
{
v2f o;
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
return o;
}
fixed4 frag (v2f i) : SV_Target
{
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
}
}
やたら設定項目が多い。
1つ目のPassでは指定した色でアウトラインを表示してます。_OutlineWidth分だけ法線方向に押し出して塗りつぶすやつです
これだとUnityのCubeとか法線方向がアレなやつはちょっと崩れます。まあデッサン風で太さの最大値もそんなに大きくないのでそこまで目立たないと思う、多分
2つ目のPassでは濃度を階調化してデッサンっぽいテクスチャを表示してます。
if(_Move){
float time = floor(sin(_Time.x)*_Frec*200);
float r = rand(float2(time,time))*0.02;
o.pos = UnityObjectToClipPos(v.vertex)+r ;
o.worldPos= ComputeScreenPos(o.pos).xy + r;
}else{
o.pos = UnityObjectToClipPos(v.vertex);
o.worldPos= ComputeScreenPos(o.pos).xy;
}
ここでは指定された場合一定時間ごとにテクスチャとアウトラインを揺らしています。
手書きの動画だと線が揺れるのでこうすることで自然になるかなと なったかはわからない
疑似乱数生成するやつはこちらを参考にしました。
half nl = saturate(dot(i.normal, _WorldSpaceLightPos0.xyz)*col.rgb);
if(!_ConsiderNormal){
nl = col.rgb;
}
fixed4 col2 =tex2D(_PaperTex, i.uv)*1.1;
fixed2 scrPos = i.worldPos.xy* _StrokeDensity;
if(_UseGradation){
if( nl <= 0.2f ){
col2 *= tex2D(_Stroke1, scrPos)*nl*4;
}
else if(nl <= 0.7f){
col2 *= tex2D(_Stroke2, scrPos)*nl*2;
}
}else{
if( nl <= 0.01f ){
col2 *= tex2D(_Stroke1, scrPos)*0.5;
}
else if( nl <= 0.1f ){
col2 *= tex2D(_Stroke1, scrPos)*0.7;
}
else if( nl <= 0.2f ){
col2 *= tex2D(_Stroke1, scrPos)*0.9;
}
else if( nl <= 0.3f ){
col2 *= tex2D(_Stroke2, scrPos)*0.8;
}
else if(nl <= 0.4f){
col2 *= tex2D(_Stroke2, scrPos)*1;
}
else if(nl <= 0.5f){
col2 *= tex2D(_Stroke2, scrPos)*1.3;
}
}
ここでは法線ベクトルとライトの位置との内積を取って、他のオブジェクトからの影込みで計算した色をかけてます。
そうすると頂点の明るさがでてくるので、適当にしきい値を設定して階調化します。
なんとなくテクスチャは増やしたくなかったからハッチテクスチャは横方向だけのやつと縦横二方向のだけのやつを用意して適当に濃さを調整して使いまわしました。線の太さを一定にするためにテクスチャはスクリーンにマッピングされたテクスチャを描画してます。モデルが動いても線はそのまま、みたいな感じになるので違和感を感じるかもしれない
透過テクスチャはアルファ値が一定以下の場合Discardすることでどうにかしてます。TransParent使うと影が表示されなくなってしまうので…
このDiscardはアウトラインのPassでもしないとアウトライン分だけ残ってしまうことにしばらく気づかなかった
3つ目のPassは他のオブジェクトに影を落とすやつらしい こちらを参考にしました
静物に適用する分には結構それっぽくなったんじゃないかね
若留ちゃんの3Dモデルにも適用してみたけどこっちはまだちょっと違和感がある気がする。がんばりたい
素晴らしい!これはシェーダアセットとして公開されたりはしないのでしょうか?プログラミングがわからない素人ですが使ってみたいです
ありがとうございます( ⓛ ω ⓛ *)
使用しているテクスチャが再配布禁止なのですぐには配布できないのですが,近いうちにテクスチャも自分で作成してから配布しようと思います.
ありがとうございます!有料でも使いたいほどクオリティが高いので、楽しみにお待ちしています
[…] デッサン風シェーダーを作ってみる | うさぎ流星群 […]