トゥーンシェーディング(セルシェーディング)はアニメ風の塗りを表現します。
今回は3Dを描画するときのトゥーンレンダリングではなくて、画像をそれっぽく変換してみたいと思います。
することを簡単に書くと、明るさを数段階に分けることとエッジ抽出です。
今回はやりたいことと考え方のまとめで、上手くは行きません。
明るさの段階化
3Dレンダリングであれば拡散反射(多くはランバート反射)をステップ関数などで分ければできそうですが、画像だと明るさを計算するところからです。
RGBベクトルの長さでもいいんですがせっかくなので明るさを分離する色空間を使ってみます。
YUV色空間
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
precision highp float; varying vec2 uv; uniform sampler2D t; void main(){ mat3 rgb2yuv=mat3( .2126,.7152,.0722, -.114572,-.385428,.5, .5,-.454153,-.045847 ); mat3 yuv2rgb=mat3( 1.,0.,1.5748, 1.,-.187324,-.468124, 1.,1.8556,0. ); vec3 rgb=texture2D(t,uv).rgb; vec3 yuv=rgb*rgb2yuv; yuv.x=yuv.x<.3?.3:yuv.x<.5?.5:1.; gl_FragColor=vec4(yuv*yuv2rgb,1); } |
古いアドベンチャーゲームのような感じがします。
ベタ塗り感を出すには少しノイズがきつい。
HSV色空間
hsv変換の書き方は簡潔なのものあるんですが、わかりやすさ重視で。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
precision highp float; varying vec2 uv; uniform sampler2D t; vec3 rgb2hsv(vec3 rgb){ float ma=max(max(rgb.r,rgb.g),rgb.b); float mi=min(min(rgb.r,rgb.g),rgb.b); float s=ma!=0.?(ma-mi)/ma:.0,v=ma; float h=ma-mi; if(h==0.); else if(ma==rgb.r)h=(rgb.g-rgb.b)/h; else if(ma==rgb.g)h=(rgb.b-rgb.r)/h+2.; else if(ma==rgb.b)h=(rgb.r-rgb.g)/h+4.; if(h<.0)h+=6.; return vec3(h,s,v); } vec3 hsv2rgb(vec3 hsv){ float h=hsv.x,s=hsv.y,v=hsv.z; h=mod(h,6.); float r,g,b; float ma=v; float mi=ma-s*ma; float d=ma-mi; if(h<=1.){ r=ma; g=h*d+mi; b=mi; }else if(h<2.){ r=(2.-h)*d+mi; g=ma; b=mi; }else if(h<3.){ r=mi; g=ma; b=(h-2.)*d+mi; }else if(h<4.){ r=mi; g=(4.-h)*d+mi; b=ma; }else if(h<5.){ r=(h-4.)*d+mi; g=mi; b=ma; }else{ r=ma; g=mi; b=(6.-h)*d+mi; } return vec3(r,g,b); } void main(){ vec4 c=texture2D(t,uv); vec3 hsv=rgb2hsv(c.rgb); hsv.z=hsv.z<.5?.3:hsv.z<.7?.7:1.; gl_FragColor=vec4(hsv2rgb(hsv),1); } |
HSVの明るさは max(rgb)
のため明るくなりがちなので、閾値を上げる。
こっちの方が好きかな。
エッジ抽出
これは3Dと違って法線が使えないため困難です。模様とかがあればアウトですね。
適当な方法として8方向ラプラシアンフィルタでも使ってみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
precision highp float; varying vec2 uv; uniform sampler2D t; uniform vec2 resolution; void main(){ vec2 p=gl_FragCoord.xy/resolution; vec2 of=1./resolution; vec3 di=vec3(1,0,-1); vec3 color=8.*texture2D(t,p).rgb; color-=texture2D(t,p+of*di.xx).rgb; color-=texture2D(t,p+of*di.xy).rgb; color-=texture2D(t,p+of*di.xz).rgb; color-=texture2D(t,p+of*di.yx).rgb; color-=texture2D(t,p+of*di.yz).rgb; color-=texture2D(t,p+of*di.zx).rgb; color-=texture2D(t,p+of*di.zy).rgb; color-=texture2D(t,p+of*di.zz).rgb; float g=dot(color,vec3(.2126,.7152,.0722)); gl_FragColor=vec4(vec3(step(.2,g)),1); //gl_FragColor=texture2D(t,p); //if(g>.2)gl_FragColor.rgb=vec3(0); } |
合わせ
平滑化:ガウシアンフィルタ
ラプラシアンフィルタに限らずエッジ検出ではノイズが大敵なので前処理として平滑化を行うことが多いです。
ラプラシアンガウシアン(LoG)フィルタも有名ですね。
明るさの段階わけもノイズっぽいので、共通の前処理としてガウシアンフィルタをかけたものを使うことにします。
それっぽければいいので 3x3
の離散近似フィルタです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
precision highp float; varying vec2 uv; uniform sampler2D t; uniform vec2 resolution; void main(){ vec2 p=gl_FragCoord.xy/resolution; vec2 of=1./resolution; vec3 di=vec3(1,0,-1); vec3 color; color+=1.*texture2D(t,p+of*di.xx).rgb; color+=2.*texture2D(t,p+of*di.xy).rgb; color+=1.*texture2D(t,p+of*di.xz).rgb; color+=2.*texture2D(t,p+of*di.yx).rgb; color+=4.*texture2D(t,p).rgb; color+=2.*texture2D(t,p+of*di.yz).rgb; color+=1.*texture2D(t,p+of*di.zx).rgb; color+=2.*texture2D(t,p+of*di.zy).rgb; color+=1.*texture2D(t,p+of*di.zz).rgb; color*=1./16.; gl_FragColor=vec4(color,1); } |
これで同様の処理をやり直します。
少しは良くなったような気はしますが、この路線は無理そうです。
所感
エッジ検出がとにかくきつい感じがする。
キャニー法とかも気が乗れば試してみたい。
明るさの段階わけはヒストグラム平坦化とかでもっとそれっぽくできないかな。
平滑化をもう少し効かせるだけでもそれっぽくなりそうな感じはします。