こんにちは。
プログラマーの中村です。
今回は昨今のトゥーンレンダリングにおいて重要な、
輪郭線(アウトライン)について、書いていきたいと思います。
検証環境はUnityを使用します。
輪郭線(アウトライン)とは
最近のゲームではアニメのような描画が増えてきています。
このような描画方法はトゥーンレンダリング、NPR(Non-photorealistic rendering)などと呼ばれています。
トゥーンレンダリングにおける重要な要素の一つとして、
輪郭線(アウトライン)が挙げられます。
キャラクターや背景などを縁取ることで、
アニメにおける線画を表現できます。
(実際にモデルを描画することで塗りを表現することになります。)
輪郭線の描画方法
輪郭線を描画する方法は日々研究されていますが、
最もよく使われている手法は、背面法や反転ポリゴン押し出し法などと呼ばれています。
簡単に説明すると、描画面を裏側にしたポリゴンを、
頂点シェーダーにおいて法線方向に拡大する手法です。
Web上にはさらに詳しい手法の説明やシェーダーの解説もあるので、興味のある方は検索してみてください。
今回使用したアウトラインのシェーダー
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 |
Shader "Custom/Outline" { Properties { _Color( "Color", Color ) = ( 1,1,1,1 ) _OutlineColor( "Outline Color", Color ) = ( 0, 0, 0, 0 ) _OutlineWidth( "Outline Width", Float ) = 1 } SubShader { Tags { "RenderType" = "Opaque" } Pass { Cull Front//描画面を裏側にする CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" float4 _OutlineColor; float _OutlineWidth; struct v2f { float4 position : SV_POSITION; }; v2f vert( float4 position : POSITION, float3 normal : NORMAL ) { v2f o; float3 normalWS = UnityObjectToWorldNormal( normal ); float3 positionWS = mul( unity_ObjectToWorld, position ); o.position = UnityWorldToClipPos( positionWS + normalWS * _OutlineWidth );//法線方向に押し出す return o; } float4 frag( v2f IN ) : SV_Target { return _OutlineColor; } ENDCG } Pass { //実際の描画パス } } } |
背面法の問題点
背面法にはいくつか問題がありますが、
最も有名なものとして、ハードエッジのモデルで綺麗にアウトラインが描画できないことが挙げられます。
ソフトエッジは面を滑らかにしたい時に使用し、ハードエッジは角ばらせたい時に使います。
したがって背面法では、下記画像のような箱状のものや、髪の毛、剣などの尖ったもので、
描画が崩れる場合があります。
ハードエッジの輪郭線を改善する
上述の問題点はモデルがハードエッジを使用しているため発生していました。
ちなみにソフトエッジ、ハードエッジとは主に法線の方向を指します。
つまり、輪郭線に使用する法線のみソフトエッジを使用すれば、
もう少し綺麗に輪郭線を描画できるはずです。
ここで注意したいのは、オブジェクトの描画に使用する通常の法線もソフトエッジにしてしまうと、
オブジェクトの陰影も変わってしまいます。
そのため今回は頂点カラーに対して法線情報を埋め込みます。
ソフトエッジを頂点カラーに埋め込むプログラム
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 |
const float error = 1e-8f; public static void BakeNormal( 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; Color[] softEdges = new Color[ normals.Length ]; for ( int i = 0; i < vertexCount; i++ ) { Vector3 softEdge = Vector3.zero; for ( int j = 0; j < vertexCount; j++ ) { var v = vertices[ i ] - vertices[ j ]; if ( v.sqrMagnitude < error ) { softEdge += normals[ j ]; } } softEdge.Normalize(); softEdges[ i ] = new Color( softEdge.x, softEdge.y, softEdge.z, 0 ); } mesh.colors = softEdges; } } |
輪郭線シェーダーも頂点カラーを参照するように書き換えます。
1 2 3 4 5 6 7 8 9 |
v2f vert( float4 position : POSITION, float3 normal : NORMAL, float4 color : COLOR ) { v2f o; float3 normalWS = UnityObjectToWorldNormal( color.rgb );//頂点カラーを使う float3 positionWS = mul( unity_ObjectToWorld, position ); o.position = UnityWorldToClipPos( positionWS + normalWS * _OutlineWidth );//法線方向に押し出す return o; } |
結果
上述の手法で描画した画像右側の箱は、
途切れなく輪郭線が描画されていて、少しだけ綺麗になったと言えます。
しかしながら、この手法には様々な問題点が存在します。
例えば頂点カラーに焼くことでスキニングの影響を受けられず、
固定の法線になってしまいます。
この問題の解決方法はまたの機会に書けたらと思います。
まとめ
今回はプログラムでソフトエッジを計算し、それを輪郭線用法線として用いる手法を紹介しました。
最近の描画技術の発展は目覚ましく、輪郭線一つとっても様々な手法が存在します。
そのような手法を積極的に取り入れていくことで、クオリティの高いゲームを開発していきたいですね。