Posted at

UnityでSTLファイルの動的な読込み

STLデータは、3D CADソフトなどで使われているファイル形式の一つです。3Dプリンタの形状データとしても利用されていて、DMM.makeの3Dプリンタサービスでも対応しています。

STLファイルの構造はこちらの“STLファイルフォーマット”を参考にさせていただきました。

STLファイルはとてもわかりやすい形式なので手軽に扱うことが出来ます。モデルデータは三次元空間上の三角形の組み合わせになっています。四角形は2つの三角形を並べて作るように、どんな複雑な形状でも三角形で構成します。データの形式も3つの頂点(三次元空間の座標)で一つの三角形を表します。基本的にそれだけというデータ構造です。

その代わりに、色などの情報は持てなかったり、頂点情報に無駄が多かったりと、用途が限定されます。また、公式なファイルフォーマットがないので方言があるようです。

UnityではメッシュをMeshクラスで動的に作れるので、三角形の頂点とそれをつなぐ情報を流しこめばSTLファイルからUnityのメッシュデータを作れるということになります。

using System.IO;

using UnityEngine;

public class SimpleMeshGenerator : MonoBehaviour
{
/// <summary>
/// バイナリフォーマットのSTLファイルを読み込んでMeshで返す
/// </summary>
/// <param name="path">STLファイルのフルパス</param>
/// <returns>メッシュデータ</returns>
private Mesh StlToUnityMesh(string path)
{
var stream = File.OpenRead(path);
var reader = new BinaryReader(stream);

// "任意の文字列"を読み飛ばし
stream.Position = 80;

// "三角形の枚数"を読み込む
uint triangleNum = reader.ReadUInt32();

Vector3[] vert = new Vector3[triangleNum * 3];
int[] tri = new int[triangleNum * 3];

int triangleIndex = 0;

for(int i = 0; i < triangleNum; i++)
{
// "法線ベクトル"を読み飛ばす
stream.Position += 12;
// "頂点"データX, Y, Zを3個分
vert[triangleIndex + 0] = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle());
vert[triangleIndex + 1] = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle());
vert[triangleIndex + 2] = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle());

// 頂点を結ぶ三角形は、先ほど読み込んだ頂点番号をそのまま使う
tri[triangleIndex] = triangleIndex++;
tri[triangleIndex] = triangleIndex++;
tri[triangleIndex] = triangleIndex++;

// "未使用データ"を読み飛ばし
stream.Position += 2;
}

var mesh = new Mesh();
mesh.vertices = vert;
mesh.triangles = tri;
mesh.RecalculateNormals();
mesh.RecalculateBounds();

return mesh;
}

void Start ()
{
string filePath = Path.Combine(Application.dataPath, "STL-FILE.stl");
GetComponent<MeshFilter>().mesh = StlToUnityMesh(filePath);
}
}

上のコードでは、STLファイルの置き場所はUnityプロジェクトのAssetsフォルダ直下になります。Editor上でしか動きません。読み込ませるSTLファイルはバイナリフォーマットにしてください。STLファイルの置き場や読み方を変えれば、Unityエディタ外の実行環境でも動くと思います。


読み込み例

素材として、Art Deco Chess Piecesを利用させていただきました。(CC BYライセンス)

Unity Editor上でSTLファイルを読み込んでみたのですが、カメラに何も映らなかったのでシーンビューをみたらSTLファイルの原点がずれてました。

camera.png

カメラは(0, 0, -10)の位置で、STLを読み込んだメッシュデータは(0, 0, 0)の位置に置いてあるので真っ正面にモデルデータが表示されると思ったのですが、どうやら、このSTLデータは(0, 0, 0)を基準にしていないようです。ずれた位置(モデルデータの中心位置)は、mesh.boundsから取得できます。

Debug.Log("mesh.bounds:" + mesh.bounds.ToString());

// mesh.bounds:Center: (22.5, -87.5, 49.5), Extents: (22.5, 22.5, 50.5)

この場合、このモデルデータを表示しているゲームオブジェクトのPositionを(-22.5, 87.5, 49.5)に置くと、3D空間(ワールドの)の原点に配置することが出来ます。

ただし、拡大縮小や回転をさせる時は一工夫必要になります。というのも、ゲームオブジェクトの座標とモデルの中心がずれているので、ゲームオブジェクトを縮小(Scaleを変える)と、モデルデータの中心位置がゲームオブジェクトの中心を基準にして移動するのです。

拡大縮小(&回転)をするには次のようにします。


  1. 空のゲームオブジェクトを作成

  2. その中にモデルデータを含むゲームオブジェクトを子として配置する

  3. 子のローカル座標を移動せる

  4. 親となる空のゲームオブジェクトを拡大縮小(&回転)

resize.png

子(モデルデータが入っているゲームオブジェクト)のローカル座標移動は次のようなコードになります。

void Start ()

{
string filePath = Path.Combine(Application.dataPath, "deco_queen.stl");

var mesh = StlToUnityMesh(filePath);
GetComponent<MeshFilter>().mesh = mesh;
GetComponent<Transform>().localPosition = new Vector3(-mesh.bounds.center.x, -mesh.bounds.center.y, mesh.bounds.center.z); // 追加
}

または、拡縮開始時に一時的な空のゲームオブジェクトを作成して~といったことを実行中にすれば、ヒエラルキーを汚さずに済むと思います。

それから、移動させる前に回転してあると、mesh.boundsの判定結果が変わってくることに注意してください。その場合は、GetComponent<MeshRenderer>().boundsで取得される値を使うとよいです(最初からこちらを使えばいいのかも)。


頂点数が多いデータ対策

旧来のUnityでは、モデルデータの頂点数が65535個までとなっていました。Unity2017.3頃から、この制約を超える方法が提供されていますので、頂点の多いデータを扱う時は、次の1行を追加してください。

Mesh mesh = new Mesh();

mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; // 追加