僕は人生においてまだ自分のドッペルゲンガーに会ったことがない。
まあドッペルゲンガーに会ったら死ぬらしいから会いたくはないけど。
そして、僕はまだ分身することが出来ない。
僕の中には十三尾が封印されているはずなので、チャクラは足りるはずなんだけどそれでも分身が出来ない。
そんな山も谷もない人生で出来ていないことを、Unity上でやってみよう。
僕の人生に何かを期待するより、ゲームでも作っていたほうがいいや。
この記事にはUnity2017.3.0f3を使用しています。
- はじめに
- シンプルなDiffuseシェーダーを作る
- 描画する頂点位置をずらしてみよう
- いっぱいドッペルゲンガーを作ってみよう
- スクリプトでマテリアルのパラメーターを設定する
- カメラに映らないとき
- そもそもバグがあったよ
- おわりに
はじめに
さて、オブジェクトを複数描画したいならGameObject
が複数あればこと足りるものです
だが、今回作るものはドッペルゲンガーや分身のような実態のないものだ。
そういうものに、GameObjectのような実態があるのはいかがなものか。
……うん、別に誰が決めたわけではないんだけど。
今回は一つのGameObject
で複数のオブジェクトを描画することでドッペルゲンガーを表現してみよう。
ということは、シェーダーを使って何とかしてみることになりそうだ。
よーし、シェーダーを勉強していくぞー!
シンプルなDiffuseシェーダーを作る
さて、シェーダーを一から作るのは面倒くさい大変なので、Unityの内蔵シェーダーを改良して作りたいものだ。
一番一般的なDiffuse
から作っていきたい。
しかし、Unityの内蔵シェーダーにあるDiffuse
シェーダーはサーフェイスシェーダーを使っている。
サーフェイスシェーダーから何か改良しようとするといろいろと面倒くさい大変なので、頂点シェーダーとフラグメントシェーダーを使う形に変更したい。
しかし、それもやっぱり面倒くさい大変だ。
はぁー……、誰かそういうことやってくれてないかなー。
(五、六行目前にはあったシェーダーに対するやる気は既に風前の灯となってしまった。)
調べたところ、Diffuse
シェーダーを一から作っているサイトを見つけてしまった。
うん、誰かが既にやっているのなら仕方ない。参考にさせていただこう。
Doppelganger.shader
Shader "Custom/Doppelganger" { Properties { _MainTex ("Texture", 2D) = "white" {} _Color("Color (RGB)", Color) = (1, 1, 1, 1) } SubShader { Pass { Tags { "LightMode" = "ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct v2f { half4 position : SV_POSITION; half2 uv : TEXCOORD0; half3 normal : TEXCOORD1; }; sampler2D _MainTex; half4 _MainTex_ST; fixed4 _Color; fixed4 _LightColor0; v2f vert( appdata_base v ) { v2f o = (v2f)0; o.position = UnityObjectToClipPos( v.vertex ); o.uv = TRANSFORM_TEX( v.texcoord, _MainTex ); o.normal = UnityObjectToWorldNormal( v.normal ); return o; } fixed4 frag( v2f i ) : SV_Target { i.normal = normalize( i.normal ); fixed4 color = (fixed4)0; fixed4 tex = tex2D( _MainTex, i.uv ); fixed3 lightColor = max(0, dot(i.normal, _WorldSpaceLightPos0.xyz)) * _LightColor0.rgb; color.rgb = tex.rgb * _Color.rgb * lightColor; return color; } ENDCG } } }
これでもいいんだけど、Unityの内蔵シェーダーに比べると色味が若干違う。
これはシーン内の環境光を計算に加えてないからだねー。
折角なので、それぐらいの処理は追加しておこう。
ええっと、確かドキュメントにその辺の処理の書き方が書いてあった気が。
Doppelganger.shader
CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" // 環境光計算のためにShadeSH9を使うため追加。 #include "UnityLightingCommon.cginc" struct v2f { half4 position : SV_POSITION; half2 uv : TEXCOORD0; half3 normal : TEXCOORD1; }; sampler2D _MainTex; half4 _MainTex_ST; fixed4 _Color; fixed4 _LightColor0; v2f vert( appdata_base v ) { v2f o = (v2f)0; o.position = UnityObjectToClipPos( v.vertex ); o.uv = TRANSFORM_TEX( v.texcoord, _MainTex ); o.normal = UnityObjectToWorldNormal( v.normal ); return o; } fixed4 frag( v2f i ) : SV_Target { i.normal = normalize( i.normal ); fixed4 color = (fixed4)0; fixed4 tex = tex2D( _MainTex, i.uv ); // 環境光の計算を追加。 fixed3 lightColor = max(0, dot(i.normal, _WorldSpaceLightPos0.xyz)) * _LightColor0.rgb + ShadeSH9( fixed4( i.normal, 1 ) ); color.rgb = tex.rgb * _Color.rgb * lightColor; return color; } ENDCG
なんかエラーが出たんだが。
Shader error in 'Custom/Doppelganger': redefinition of '_LightColor0' at line 35 (on d3d11)
あー、なるほど。
どうやら_LightColor0
はUnityLightingCommon.cginc
内で宣言されているようだ。
というわけで、_LightColor0
の変数宣言は削除だ。
Doppelganger.shader
CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" // 環境光計算のためにShadeSH9を使うため追加。 #include "UnityLightingCommon.cginc" struct v2f { half4 position : SV_POSITION; half2 uv : TEXCOORD0; half3 normal : TEXCOORD1; }; sampler2D _MainTex; half4 _MainTex_ST; fixed4 _Color; // UnityLightingCommon.cgincで既に宣言されている。 // fixed4 _LightColor0; v2f vert( appdata_base v ) { v2f o = (v2f)0; o.position = UnityObjectToClipPos( v.vertex ); o.uv = TRANSFORM_TEX( v.texcoord, _MainTex ); o.normal = UnityObjectToWorldNormal( v.normal ); return o; } fixed4 frag( v2f i ) : SV_Target { i.normal = normalize( i.normal ); fixed4 color = (fixed4)0; fixed4 tex = tex2D( _MainTex, i.uv ); // 環境光の計算を追加。 fixed3 lightColor = max(0, dot(i.normal, _WorldSpaceLightPos0.xyz)) * _LightColor0.rgb + ShadeSH9( fixed4( i.normal, 1 ) ); color.rgb = tex.rgb * _Color.rgb * lightColor; return color; } ENDCG
OK。
内蔵シェーダーと色味が似てきた。
あとは床に影が落ちてないぐらいだが、これもパパっと追加してしまおう。
適当な影描画のPassをUsePass
を使えばすぐに出来るはずだ。
Doppelganger.shader
SubShader { Pass { // 先ほどのDiffuse処理 ... } // StandardShaderの影の描画処理を利用する。 UsePass "Legacy Shaders/VertexLit/SHADOWCASTER" }
見た目上はほとんど同じものになりましたー。
ただ、このシェーダーでは自分自身に落ちる影の計算処理は記述していないので、ライトを下の方から当てるとこうなっちゃう。
内蔵シェーダーは床がライトを遮るので暗くなっているけど、自作シェーダーは下のほうが明るくなっている。
まぁそろそろ面倒くさくなってきたので、ここらへんでいいや。
本題に入ろう。
描画する頂点位置をずらしてみよう
実体のないドッペルゲンガーや分身を表現するために、実際のオブジェクトの位置と違う場所にオブジェクトが描画されるようにシェーダーを改良してみよう。
これにはエディタ上でオフセットプロパティを設定できるようにして、頂点シェーダーで渡された頂点位置をオフセット分ずらすようにしてみれば、それっぽくなるのではないだろうか。
試してみよう。
Doppelganger.shader
Shader "Custom/Doppelganger" { Properties { _MainTex ("Texture", 2D) = "white" {} _Color("Color (RGB)", Color) = (1, 1, 1, 1) _Offset("Offset Position", Vector) = (0, 0, 0, 0) } SubShader { Pass { Tags { "LightMode" = "ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "UnityLightingCommon.cginc" struct v2f { half4 position : SV_POSITION; half2 uv : TEXCOORD0; half3 normal : TEXCOORD1; }; sampler2D _MainTex; half4 _MainTex_ST; fixed4 _Color; half4 _Offset; v2f vert( appdata_base v ) { // 指定したオフセット分頂点の位置をずらす。 v.vertex.xyz += _Offset; v2f o = (v2f)0; o.position = UnityObjectToClipPos( v.vertex ); o.uv = TRANSFORM_TEX( v.texcoord, _MainTex ); o.normal = UnityObjectToWorldNormal( v.normal ); return o; } fixed4 frag( v2f i ) : SV_Target { i.normal = normalize( i.normal ); fixed4 color = (fixed4)0; fixed4 tex = tex2D( _MainTex, i.uv ); fixed3 lightColor = max(0, dot(i.normal, _WorldSpaceLightPos0.xyz)) * _LightColor0.rgb + ShadeSH9( fixed4( i.normal, 1 ) ); color.rgb = tex.rgb * _Color.rgb * lightColor; return color; } ENDCG } UsePass "Legacy Shaders/VertexLit/SHADOWCASTER" } }
あー、うん。
ずれたね。確かにオブジェクトの位置はずれているね。
影は元に位置にあるっぽいがね!
あー……、パパっと出来るから組み込んだ影に足を引っ張られたかぁ。
仕方ない、UsePass
で参考にしているシェーダーの処理をそのまま持ってきて、同じようにオフセット分ずらすようにしてみようか。
Doppelganger.shader
SubShader { Pass { // 先ほどのDiffuse処理 ... } Pass { Tags { "LightMode" = "ShadowCaster" } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma target 2.0 #pragma multi_compile_shadowcaster #pragma multi_compile_instancing #include "UnityCG.cginc" struct v2f { V2F_SHADOW_CASTER; UNITY_VERTEX_OUTPUT_STEREO }; half4 _Offset; v2f vert( appdata_base v ) { // 指定したオフセット分頂点の位置をずらす。 v.vertex.xyz += _Offset; v2f o; UNITY_SETUP_INSTANCE_ID(v); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) return o; } float4 frag( v2f i ) : SV_Target { SHADOW_CASTER_FRAGMENT(i) } ENDCG } }
おおっ、いいんじゃない。
ただ、このままではオブジェクトの描画位置をずらしただけ。
別に複数描画はされてない。
ここからさらに改良が必要だ。
いっぱいドッペルゲンガーを作ってみよう
オブジェクトをずらして描画するパスの処理が作れたのだから、あとは普通に描画するPassを付け加えてあげれば複数個描画されるようになるはずだ。
もっと多くのオブジェクトを描画したければ、もっとPassを追加してやればいい!
……本気でやる気か、そんな面倒くさいこと。
しかもPassを動的に増やすことは出来んような気がするので、二つドッペルゲンガーには二つドッペルゲンガーを作る用のシェーダーを、三つドッペルゲンガーには三つドッペルゲンガーを作る用のシェーダーを、という話になってくる。
すなわちこうなる。
これはひどい……。
うーん、どうしようかなぁ。
マテリアルをいっぱい作って設定するというのはどうだろう。
Renderer
のマテリアル設定にオフセットの位置をずらしたマテリアルを複数指定してみよう。
わーい、念願のドッペルゲンガーに会えたよー。
めっさWarning出とるけど。
スクリプトでマテリアルのパラメーターを設定する
しかし、これではマテリアルが山ほど必要になってくる。
それも管理が面倒だ。
スクリプトでマテリアルのプロパティを設定するようにしてみよう。
これならマテリアルは一つで十分なはずだ。
Doppelganger.cs
using UnityEngine; public class Doppelganger : MonoBehaviour { private readonly int OFFSET_ID = Shader.PropertyToID( "_Offset" ); [SerializeField] private Material m_material = null; [SerializeField] private Vector4[] m_offsets = null; private void Start() { if( m_material == null ) { return; } if( m_offsets == null || m_offsets.Length == 0 ) { return; } var renderer = GetComponent<Renderer>(); if( renderer == null ) { return; } var materials = new Material[ m_offsets.Length ]; for( int i = 0; i < materials.Length; ++i ) { materials[ i ] = new Material( m_material ); materials[ i ].SetVector( OFFSET_ID, m_offsets[ i ] ); } renderer.materials = materials; } } // class Doppelganger
これで同じように描画されるようになるのだけど、これはこれで問題になってくる。
スクリプトでパラメーターを変えているので、ランタイム実行時しか描画が確認できなくなってしまう。
それはそれで嫌だなー。
ランタイム時以外でも描画状態は確認できるようにしてしまおう。
確かそんなアトリビュートがあったような気がする。ExecuteInEditMode
だっけ?
あと、エディタ上でも動くのならば、Start()よりOnEnable()の方が都合がよさそうだなぁ。
エディタ上でコンポーネントのチェックボックスを切り替えるだけで呼び出せるようになりそうだし。
Doppelganger.cs
using UnityEngine; [ExecuteInEditMode] public class Doppelganger : MonoBehaviour { private readonly int OFFSET_ID = Shader.PropertyToID( "_Offset" ); [SerializeField] private Material m_material = null; [SerializeField] private Vector4[] m_offsets = null; private void OnEnable() { if( m_material == null ) { return; } if( m_offsets == null || m_offsets.Length == 0 ) { return; } var renderer = GetComponent<Renderer>(); if( renderer == null ) { return; } // EnableになるたびにMaterialを再生成しまくるのはどうかと思うが……。 var materials = new Material[ m_offsets.Length ]; for( int i = 0; i < materials.Length; ++i ) { materials[ i ] = new Material( m_material ); materials[ i ].SetVector( OFFSET_ID, m_offsets[ i ] ); } renderer.materials = materials; } } // class Doppelganger
これでエディタ上でも描画を確認できるようになるんだけど、このようなメッセージが表示されちゃう。
Instantiating material due to calling renderer.material during edit mode. This will leak materials into the scene. You most likely want to use renderer.sharedMaterial instead.
UnityEngine.Renderer:get_materials()
シーンにマテリアルがリークすっから、sharedMaterialの方がよくね? とのことだ。
うーん、その忠告はありがたいのだが、プロパティの値は違うからsharedMaterialを使うわけには……。
カメラに映らないとき
2018/01/20 追記
本来オブジェクトが描画されるあたりの位置がカメラの視錐台から外れたら、このシェーダーの描画がされなくなる対応を追加しましたよ。
そもそもバグがあったよ
2018/01/24 追記
オブジェクトの回転や拡大縮小を行うと、描画位置がおかしく成る不具合を修正しましたよ。
おわりに
いろいろ問題はあったけど、とりあえず目的は達成できたかな。
ただこの機能……、実際にどんな場合に使えばいいんだろうか。
……。
何も思いつかない……。