2018年6月25日

Unity 法線を書きかえて草木をやわらかく見せる


法線情報を書きかえることで板ポリを並べて作った草むらや木の見た目を調整する方法をメモメモ…


※このページの内容の動作確認にはUnity5.6を使用しています。

上の画像は、葉っぱのテクスチャを貼りつけた板ポリ(Quad)をひたすら球状に配置しただけのオブジェクトです。「わっさ~」と茂っていてなんだかかわいいですね。 ただ、左と右で受ける印象は少し違います。左側は個々の葉っぱの陰のつき方がバラバラでなんだか統一感がないように見えますよね。一方、右側は陰にグラデーションがかかっていて全体的にまとまりがありふわっとして見えます。

実は、左右のオブジェクトの違いはたった1つだけです。それは「各頂点の法線方向」です。

各頂点のもつ法線を赤い線で示してみる 一気にウニ感が増した

左側の葉っぱの法線方向は、それぞれの板ポリの面の方向そのままです。板ポリの向きはほぼランダムなので、当然面の向きもバラバラになります。一方、右側の葉っぱの法線方向には「板ポリの向きは無視して放射状に外側を向く」ようにスクリプトから変更を加えています。つまり右側は嘘をついているわけです。

実際の形状(それぞれの板ポリ)は完全無視して、あたかも各頂点が球体の表面上にあるかのように法線を設定してやることで、光の当たり方や陰のつき方が全体的に球体っぽくまとまり、イイ感じの見た目になります。場合によっては左側のジャギジャギした感じのほうが適しているときもありますが、板ポリを誤魔化してなめらかに見せたいときはこういう方法が手軽で便利だと思います。

法線を変えると陰のつき方も変わる

たとえば、有名どころのインディー系タイトルでいうと The WitnessRiME なんかは見た感じこのテクニックを使ってるっぽいです(間違ってたらすみません…)。

法線方向を修正するのは3Dモデリングソフトなどでも可能だと思いますが、今回はUnity上でスクリプトからちゃちゃっと変更してみました。というわけで参考までにコードをそのまま載っけておきます。

  1. using System.Collections.Generic;
  2. using UnityEngine;
  3.  
  4. #if UNITY_EDITOR
  5. using UnityEditor;
  6. #endif
  7.  
  8. [RequireComponent(typeof(MeshFilter))]
  9. public class MeshNormalSetter : MonoBehaviour
  10. {
  11. // もとになるメッシュ 指定しない場合はMeshFilterが保持するメッシュを直接使用する
  12. public Mesh sourceMesh;
  13.  
  14. // 中心点 指定しない場合はオブジェクトの原点を使用する
  15. public Transform center;
  16.  
  17. // 新しく作成した法線をもとの法線と合成させる割合 0でもとの法線から変化なし 1で完全に新しい法線
  18. [Range(0, 1)]
  19. public float combineRate = 1f;
  20.  
  21. [ContextMenu("Set Normals")]
  22. public void SetNormals()
  23. {
  24.   MeshFilter targetFilter = GetComponent<MeshFilter>();
  25.  
  26.   // もとになるメッシュを取得
  27.   Mesh sourceMesh = this.sourceMesh ? this.sourceMesh : targetFilter.sharedMesh;
  28.   if (sourceMesh == null) return;
  29.  
  30.   // 新しく法線データを作成
  31.   Vector3 centerPos = center ? center.position : transform.position;
  32.   List<Vector3> newNormals = new List<Vector3>();
  33.   for (int i = 0; i < sourceMesh.vertexCount; i++)
  34.   {
  35.    Vector3 centerToVertex = sourceMesh.vertices[i] - transform.InverseTransformPoint(centerPos);
  36.    newNormals.Add(Vector3.Lerp(sourceMesh.normals[i], centerToVertex.normalized, combineRate));
  37.   }
  38.  
  39.   // 新しくメッシュを作成
  40.   Mesh newMesh = Instantiate(sourceMesh) as Mesh;
  41.   newMesh.SetNormals(newNormals);
  42.  
  43.   newMesh.name = sourceMesh.name;
  44.   if (!newMesh.name.Contains("(Normals Changed!)")) newMesh.name += " (Normals Changed!)";
  45.  
  46.   // メッシュを適用
  47.   #if UNITY_EDITOR
  48.   Undo.RegisterCreatedObjectUndo(newMesh, "Set Normals");
  49.   Undo.RecordObject(targetFilter, "Applied To MeshFilter");
  50.   #endif
  51.  
  52.   targetFilter.mesh = newMesh;
  53. }
  54. }

使い方は、まずこのスクリプトをMeshFilterのついたオブジェクトにくっつけます。次に、もとになるメッシュ(インポートしたオリジナルのメッシュデータ)をsourceMeshへ指定。あとはインスペクタ上の歯車マークを押して出てくるメニュー(スクリプト名を右クリックでもOK)から"Set Normals"を選んで実行すれば完了です。法線を変更したメッシュを新たに作成しそれをMeshFilterに自動で割り当てます。

ちなみに実際に法線を計算しているのはこの部分だけです。

  1. for (int i = 0; i < sourceMesh.vertexCount; i++)
  2. {
  3. Vector3 centerToVertex = sourceMesh.vertices[i] - transform.InverseTransformPoint(centerPos);
  4.  newNormals.Add( Vector3.Lerp(sourceMesh.normals[i], centerToVertex.normalized, combineRate) );
  5. }

頂点座標がローカル(オブジェクト内での座標)なのに対して中心点の座標はグローバルになっているので、まずは transform.InverseTransformPoint を使って中心点の座標をグローバル座標からローカル座標に変換しています。そのうえで「中心点→各頂点」のベクトルを新しい法線として計算。Vector3.Lerpを使って「どの程度の割合で新しい法線を適用するか」を決めているというわけです。

中心点(center)を本来の原点の位置から少しずらすとまた違った陰のつき方になります。あるいは法線の求め方を全く違ったものに書き換えても面白いかもしれません。たとえば単純に法線を Vector3.up とすれば実際のメッシュの面の向きとは関係なくすべての法線が上向きになります。地面から垂直に生えている草などの場合、法線を上向きにしてみると意外になめらかな陰のつき方になったりします。ぜひいろいろ試してみてください!

その他、Unityに限らず頂点の法線情報を書きかえるテクニックはこちらのページなどで詳しく解説されています。

0 件のコメント:

コメントを投稿