2019年2月25日

Unity メッシュの体積を計算する


メッシュの体積を取得する方法をメモメモ…



※このページの内容の動作確認にはUnity2018.2を使用しています。
外部ツールを使わず、Unity内でオブジェクトの体積を計算する方法がないかググったところ、Unityのコミュニティでいくつか方法が紹介されていました。

Rigidbodyを利用する方法

1つ目は Rigidbody の SetDensity メソッドを利用する方法です。このメソッドは、密度を指定すると自動でmass(質量)の値を設定してくれるもので、密度を1にすると質量の値が実質的に体積と同じになります。

_rigidbody.SetDensity(1f);         // 密度を 1 にする
float volume = _rigidbody.mass;    // 質量 = 体積になる

解決策としてはシンプルなのですが、この方法には問題があります。実はこれはメッシュではなくオブジェクトについているコライダーの体積なのです。しかも Box Collider や Sphere Collider、Convex に設定した Mesh Collider といったシンプルなコライダーにしか適用できません。Concave な(つまり凹みのある)コライダーだとエラーになります。単純な形状のメッシュであれば大丈夫なのですが、複雑な形の体積を取得するにはちょっと厳しいと思います。

計算によって求める方法

というわけで、次はメッシュの三角面の各座標から算出する方法です。紹介されていたコードはこちら↓


float VolumeOfMesh(Mesh mesh)
{
 if (mesh == null) return 0;

 Vector3[] vertices = mesh.vertices;
 int[] triangles = mesh.triangles;

 float volume = 0;
 for (int i = 0; i < triangles.Length; i += 3)
 {
  Vector3 p1 = vertices[triangles[i + 0]];
  Vector3 p2 = vertices[triangles[i + 1]];
  Vector3 p3 = vertices[triangles[i + 2]];
  volume += SignedVolumeOfTriangle(p1, p2, p3);
 }
 return Mathf.Abs(volume);
}

float SignedVolumeOfTriangle(Vector3 p1, Vector3 p2, Vector3 p3)
{
 float v321 = p3.x * p2.y * p1.z;
 float v231 = p2.x * p3.y * p1.z;
 float v312 = p3.x * p1.y * p2.z;
 float v132 = p1.x * p3.y * p2.z;
 float v213 = p2.x * p1.y * p3.z;
 float v123 = p1.x * p2.y * p3.z;
 return (1.0f / 6.0f) * (-v321 + v231 + v312 - v132 - v213 + v123);
}


正直これを見ただけではどういう内容の処理なのか、ちんぷんかんぷんでした。そこでさらにググってみたところ、非常に分かりやすいページを発見。

モデルの体積を計算する | 試行錯誤
https://shikousakugo.wordpress.com/2013/08/25/volume/

上記の SignedVolumeOfTriangle メソッドが一体何をやっているのかというと、原点と三角面の各頂点を結んでできる三角錐の体積を算出しているのです。つまり、メッシュを三角錐に分割しそれらを足し合わせることで、全体の体積を求めているというわけですね。

ベクトルABCによって作られる平行六面体の体積は A・(B×C) として計算できます。「・」は内積、「×」は外積です。これを「スカラー三重積」といいます。ベクトルABCによって作られる三角錐の体積を求めるには、この平行六面体の体積を1/6すればいいので、 A・(B×C) / 6 となりますね。この内積と外積の計算を地道に展開して計算したのが上記のコードだったというわけです(詳しい計算式や性質はスカラー三重積とベクトル三重積 | 高校数学の美しい物語がわかりやすいです)。

でも、UnityのVector3には内積(Dot Product)と外積(Cross Product)を計算するメソッドが標準で用意されているので、実は SignedVolumeOfTriangle メソッドは次のように書くこともできます。


float SignedVolumeOfTriangle(Vector3 p1, Vector3 p2, Vector3 p3)
{
 return Vector3.Dot(p1, Vector3.Cross(p2, p3)) / 6.0f;
}


1行!シンプル!多少の精度誤差や計算速度などの違いはあるかもしれませんが、結果はほぼ同じになります。短いしどういう処理なのかすぐわかるのでいいですね。


ただ、これで三角錐を利用して計算するということは理解できましたが、でもいくら「メッシュを三角錐に分割」すると言っても、そんなにうまくいくものでしょうか? だってもしローカルの原点座標がモデルの外側にあったら、その三角錐がモデルから飛び出すことになってしまいます。それを足し合わせるだけで本当に正確な体積が計算できるのでしょうか?

結論からいうと、三角面が表を向いているか裏を向いているかによって外積の符号(プラスかマイナスか)が変わるので、結果的にうまいこといきます。三次元空間で考えると複雑すぎて頭が爆発するので、平面に落として考えましょう。さきほど紹介したページの図を参考にするとこんな感じになります。


オレンジ色が表向きの面、水色が裏向きの面です。それぞれの三角形の面積を単純に足し合わせると、符号が違うのでうまく打ち消し合い、モデルの内部の範囲だけが残ります。原点がどこにあろうが一緒です。なんだか不思議ですね。ただし、この方法だと最終的な面積がマイナスになる可能性があるので、必ず絶対値にしないといけません。上記のコードの16行目に Mathf.Abs がついているのはこの理由です。


 return Mathf.Abs(volume);


三角面が交差していたり欠けていたり、Plane のように閉じていないメッシュだったりする場合は正確な体積が算出できないので注意してください。フォーラムのコメントでは「Convexなメッシュにしか有効じゃない!」みたいなことも指摘されているのですが、そんなことはないと思います。ざっと試した限りでは、凹みのある複雑な形状のメッシュでも不自然な値にはなっていないですし、理屈から考えても大丈夫なはずなのですが…。ただ、これ以上は知識がないので断言はできません…詳しい方がいたら教えてもらえると有難いです…。

ま、何はともあれ、一応これでメッシュ自体の体積が計算できるようになりました。あとはオブジェクトのスケールを加味すれば、シーン内に配置されている実際のオブジェクトの体積がわかります。


using UnityEngine;

public class MeshVolume : MonoBehaviour {

 float meshVolume;

 void Start () {

  MeshFilter meshFilter = GetComponent<MeshFilter>();
  if (meshFilter == null) return;

  // メッシュの体積を計算する
  Mesh mesh = meshFilter.sharedMesh;
  meshVolume = CalculateMeshVolume(mesh);
 }
 
 void Update () {
  
  // メッシュの体積に現在のオブジェクトのスケールを掛け合わせる
  Vector3 scale = transform.lossyScale;
  float volume = meshVolume * scale.x * scale.y * scale.z;

  // 丸めて表示する
  Debug.Log(System.Math.Round(volume, 3));
 }

 float CalculateMeshVolume(Mesh mesh)
 {
  if (mesh == null) return 0;

  Vector3[] vertices = mesh.vertices;
  int[] triangles = mesh.triangles;

  float volume = 0;
  for (int i = 0; i < triangles.Length; i += 3)
  {
   Vector3 p1 = vertices[triangles[i + 0]];
   Vector3 p2 = vertices[triangles[i + 1]];
   Vector3 p3 = vertices[triangles[i + 2]];
   volume += Vector3.Dot(p1, Vector3.Cross(p2, p3)) / 6.0f;
  }
  return Mathf.Abs(volume);
 }
}


体積計算の処理は重いので Update 内で実行するのはやめましょう。今回は Start で算出していますが、実際ゲームで利用する場合はエディタ上であらかじめ計算しておくのがパフォーマンス的に一番良いと思います。オブジェクトのスケールは、親オブジェクトのスケールも含めて適用したいので transform.localScale ではなく transform.lossyScale を使っています。これも状況に応じて使い分けてください。


フォーラムの回答では上記の2つの方法の他に Voxelization(ボクセル化) というアプローチを挙げているものもありました。一旦メッシュを細かいボクセル状に変換し、その数から体積を調べるという、たぶん「定積分」に近い考え方なんでしょうかね。が、今回は睡魔が襲ってきたのでこのくらいにしておきます…


0 件のコメント:

コメントを投稿