[C#]STLファイルの読み書き

スポンサーリンク
C#投稿画像800x180C#
スポンサーリンク

STLファイルは3次元CADをお使いの方でなければ、一般の方にはあまり馴染みのないファイルだったと思いますが、3Dプリンタの普及に伴って認知度が高まったような気がします。

STLファイルフォーマットを解説します。

すぐに使えるソースコード付きです。

この記事の対象読者
  • STLファイルについて知りたい
  • テキスト(ASCII)形式やバイナリ形式のフォーマットを知りたい
  • すぐに使えるソースコードが欲しい

STLファイルとは

STLファイルとは、三次元形状を表現するデータ用のファイルフォーマットの一つです。

STLとは「Standard Triangulated Language」または「Standard Tessellation Language」の略称です。
簡単に言えば、複数の三角形データを格納できる、テキストファイルまたはバイナリファイルです。
一般的にファイルの拡張子には「.stl」が使われます。
詳しくは Wikipedia をご覧ください。

どんな時に使うの?

古くから三次元形状の交換用に使われています。

ただ、持っているデータが複数の三角形データだけなので、高度な表現が求められるCADではメインのフォーマットとしては使われていません。

近年、3Dプリンタの普及に伴って、3Dプリンタで造形したいモデルデータの交換用として、一般的に知られるようになったのではないかと思います。

STLファイルの構成要素とデータ形式

STLファイルの構成要素

STLファイルは、基本的にヘッダー(任意の文字列)と複数の三角形で構成されます。
※テキスト形式には、フッター(任意の文字列) も有ります。(後述)
※バイナリ形式には、三角形の枚数も有ります。(後述)

三角形1枚分は、三角形を構成する3つの頂点座標(各頂点はそれぞれ X,Y,Z の3要素)と、1つの法線ベクトル(X,Y,Z の3要素)で構成されます。

STLファイルのデータ形式

STLファイルには、テキスト(ASCII)形式とバイナリ形式の2種類のデータ形式があり、形式ごとにフォーマットが異なります。

データ形式特徴
テキスト形式テキストデータなので、人が見ても中身が分かりますが、
データ量は多くなりますので、ファイルの読み書きに時間がかかります。
バイナリ形式普通の人は中を見ても分からないです。
テキスト形式と比較して、データ量は少なくなりますので、
ファイルの読み書きが高速になります。

STLファイルフォーマット

テキスト形式

データ構成(ファイル全体)

テキスト形式のファイルの中身を見てみます。
三角形が2枚のみの場合です。

青枠内の文字がヘッダーとフッターです、それぞれ1行使って任意のテキストを設定できます。
赤枠内の文字が三角形1枚分のデータです、7行使われています。
構成としては、ヘッダーとフッターの間に必要な枚数分の三角形データが挟まっています。

データ構成(行単位)

データの各行は、「キーワード(赤の太字)+データ(黒の細字)」で構成されています。

各行を説明します。

キーワードデータ説明
solid任意のヘッダーテキストファイルの先頭行で、任意のテキストを記載する行です
facet normal法線ベクトル三角形1枚分の先頭行で、法線ベクトルを記載する行です
outer loop無し次の行から頂点座標です
vertex頂点座標頂点座標を記載する行です
endloop無し頂点座標が終わった事を示す行です
endfacet無し三角形1枚分が終わった事を示す行です
endsolid任意のフッターテキストファイルの最終行で、任意のテキストを記載する行です

注意点としては、キーワードの単語間、キーワードとデータの間、ベクトルや頂点座標のX/Y/Zの間には、それぞれ空白が必要です。

それと、ベクトルや頂点座標のX/Y/Z値は、e表記法(eを使った指数表記)が使われます。

バイナリ形式

テキスト形式と違い、中身を見ても分からないので、データ構造を説明します。
最初にヘッダーテキストと三角形の枚数が入っています、その後に枚数分の三角形データが連続して入っています。

最初のヘッダーテキストと三角形の枚数のデータ構造です。

C#の型説明
byte[80]ヘッダーテキスト(80バイト分)
uint三角形の枚数

三角形1枚分のデータ構造です。

C#の型説明
float[3]法線ベクトルのXYZ
float[3]1点目頂点座標のXYZ
float[3]2点目頂点座標のXYZ
float[3]3点目頂点座標のXYZ
ushort使われませんが2バイト分必要です。

ソースコード

STLファイルの読み書きができるソースコード全文を、一番最後に掲載しています。
テキスト形式とバイナリ形式の、どちらも含まれています。

使い方

先ずは StlFile クラスインスタンスを生成します。

プロパティ

StlFile クラスは、次の3つのプロパティを持っていますので、
書き込む場合は、プロパティにデータを入れてから書き込みメソッドを呼びます。
読み込む場合は、読み込みメソッドを呼ぶと、この3つのプロパティにデータがセットされます。

プロパティ


// ヘッダーテキスト
public byte[] Header { get; set; }

// フッターテキスト
public byte[] Footer { get; set; }

// ファセット(三角形)配列
public Facet[] Facets { get; set; }

読み書きメソッド

読み書きするメソッドです。

読み書きメソッド


// テキスト(ASCII)形式でのSTLファイル書き込み
public bool WriteAscii(string filePath)

// テキスト(ASCII)形式でのSTLファイル読み込み
public bool ReadAscii(string filePath)

// バイナリ形式でのSTLファイル書き込み
public bool WriteBinary(string filePath)

// バイナリ形式でのSTLファイル読み込み
public bool ReadBinary(string filePath)

最後に

最後まで読んでいただいて、ありがとうございました。
日夜頑張るプログラマーにとって、少しでもお役に立てたなら光栄です。

StlFile.cs


using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;

namespace CodingSquareCS
{
	/// <summary>
	/// STLファイル
	/// </summary>
	public class StlFile
	{
		#region Constructions
		/// <summary>
		/// コンストラクタ
		/// </summary>
		public StlFile()
		{
		}
		#endregion Constructions

		#region Constants
		/// <summary>
		/// バイナリ形式I/O時の有効最大ヘッダバイト数
		/// </summary>
		private const int HeaderLength = 80;
		#endregion Constants

		#region Properties
		/// <summary>
		/// ヘッダーテキスト
		/// </summary>
		/// <remarks>
		/// 初期値は null なので必要に応じて領域確保してください
		/// バイナリ形式のI/Oでは最大80バイトまで
		/// </remarks>
		public byte[] Header { get; set; }

		/// <summary>
		/// フッターテキスト
		/// </summary>
		/// <remarks>
		/// 初期値は null なので必要に応じて領域確保してください
		/// ASCII形式のI/Oのみ利用
		/// </remarks>
		public byte[] Footer { get; set; }

		/// <summary>
		/// ファセット(三角形)配列
		/// </summary>
		/// <remarks>
		/// 初期値は null なので必要に応じて領域確保してください
		/// </remarks>
		public Facet[] Facets { get; set; }
		#endregion Properties

		#region Methods
		/// <summary>
		/// バイナリ形式でのSTLファイル書き込み
		/// </summary>
		/// <param name="filePath">ファイルパス</param>
		/// <returns>正常終了した場合は true、その他の場合は false</returns>
		/// <remarks>前提としてリトルエンディアンとする</remarks>
		public bool WriteBinary(string filePath)
		{
			// filePath が入っていない場合はエラーとする
			if (filePath == null)
				return false;

			// ファセットデータが無い場合はエラー
			if (Facets == null || Facets.Length == 0)
				return false;

			try
			{
				// バイナリファイルの書き込み
				using (var writer = new BinaryWriter(new FileStream(filePath, FileMode.Create, FileAccess.Write)))
				{
					// ヘッダ書き込み用配列
					byte[] header = new byte[HeaderLength];

					// プロパティのヘッダに中身が有る場合
					if (Header != null && 1 <= Header.Length)
					{
						// プロパティのヘッダを、最大80バイトまで、ヘッダ書き込み用配列にコピーする
						int length = HeaderLength < Header.Length ? HeaderLength : Header.Length;
						Array.Copy(Header, 0, header, 0, length);
					}

					// ヘッダ書き込み
					writer.Write(header, 0, HeaderLength);

					// ファセットの枚数書き込み
					uint size = (uint)Facets.Length;
					writer.Write(size);

					// 全ファセット書き込み
					ushort buff = 0;
					foreach (var facet in Facets)
					{
						writer.Write(facet.Normal.X);
						writer.Write(facet.Normal.Y);
						writer.Write(facet.Normal.Z);
						writer.Write(facet.Vertex1.X);
						writer.Write(facet.Vertex1.Y);
						writer.Write(facet.Vertex1.Z);
						writer.Write(facet.Vertex2.X);
						writer.Write(facet.Vertex2.Y);
						writer.Write(facet.Vertex2.Z);
						writer.Write(facet.Vertex3.X);
						writer.Write(facet.Vertex3.Y);
						writer.Write(facet.Vertex3.Z);
						writer.Write(buff);
					}
				}
			}
			catch (Exception)
			{
				return false;
			}
			return true;
		}

		/// <summary>
		/// バイナリ形式でのSTLファイル読み込み
		/// </summary>
		/// <param name="filePath">ファイルパス</param>
		/// <returns>正常終了した場合は true、その他の場合は false</returns>
		/// <remarks>前提としてリトルエンディアンとする</remarks>
		public bool ReadBinary(string filePath)
		{
			// filePath が null か、ファイルが存在しない場合はエラーとする
			if (filePath == null || File.Exists(filePath) == false)
				return false;

			try
			{
				// バイナリファイルの読み込み
				using (var reader = new BinaryReader(new FileStream(filePath, FileMode.Open, FileAccess.Read)))
				{
					// ヘッダ読み込み
					Header = reader.ReadBytes(HeaderLength);

					// ファセットの枚数読み込み
					uint size = reader.ReadUInt32();

					// ファイルの残りのバイト数
					long rest = reader.BaseStream.Length - reader.BaseStream.Position;

					// ファセット1枚分のバイト数
					const int FacetLength = 50;

					// ファイルの残りのバイト数が、求められるファセットの枚数分のバイト数より少なければエラー
					if (rest < FacetLength * size)
						return false;

					// 全ファセット読み込み
					Facets = new Facet[size];
					for (int i = 0; i < size; ++i)
					{
						// ファセット1個分のバイト配列読み込み
						byte[] bytes = reader.ReadBytes(FacetLength);

						// ファセットデータ生成と配列への格納
						int index = 0;
						const int offset = sizeof(float);
						Facets[i] = new Facet(
							new Vertex(
								BitConverter.ToSingle(bytes, index),
								BitConverter.ToSingle(bytes, index += offset),
								BitConverter.ToSingle(bytes, index += offset)),
							new Vertex(
								BitConverter.ToSingle(bytes, index += offset),
								BitConverter.ToSingle(bytes, index += offset),
								BitConverter.ToSingle(bytes, index += offset)),
							new Vertex(
								BitConverter.ToSingle(bytes, index += offset),
								BitConverter.ToSingle(bytes, index += offset),
								BitConverter.ToSingle(bytes, index += offset)),
							new Vertex(
								BitConverter.ToSingle(bytes, index += offset),
								BitConverter.ToSingle(bytes, index += offset),
								BitConverter.ToSingle(bytes, index += offset))
						);
					}
				}
			}
			catch (Exception)
			{
				return false;
			}
			return true;
		}

		/// <summary>
		/// テキスト(ASCII)形式でのSTLファイル書き込み
		/// </summary>
		/// <param name="filePath">ファイルパス</param>
		/// <returns>正常終了した場合は true、その他の場合は false</returns>
		public bool WriteAscii(string filePath)
		{
			// filePath が入っていない場合はエラーとする
			if (filePath == null)
				return false;

			// ファセットデータが無い場合はエラー
			if (Facets == null || Facets.Length == 0)
				return false;

			try
			{
				// 上書きの書き込みモードでファイルを開く
				using (var writer = new StreamWriter(filePath, false, Encoding.ASCII))
				{
					// ヘッダ書き込み
					string header = Header == null ? null : Encoding.ASCII.GetString(Header);
					writer.WriteLine("solid " + header);

					// 全ファセットデータ書き込み
					foreach (var facet in Facets)
					{
						writer.WriteLine("  facet normal " + ToText(facet.Normal));
						writer.WriteLine("    outer loop");
						writer.WriteLine("      vertex " + ToText(facet.Vertex1));
						writer.WriteLine("      vertex " + ToText(facet.Vertex2));
						writer.WriteLine("      vertex " + ToText(facet.Vertex3));
						writer.WriteLine("    endloop");
						writer.WriteLine("  endfacet");
					}

					// フッタ書き込み
					string footer = Footer == null ? null : Encoding.ASCII.GetString(Footer);
					writer.WriteLine("endsolid " + footer);

					// 頂点データをテキストに変換
					string ToText(in Vertex vec)
					{
						return
							vec.X.ToString("e") + " " +
							vec.Y.ToString("e") + " " +
							vec.Z.ToString("e");
					}
				}
			}
			catch (Exception)
			{
				return false;
			}
			return true;
		}

		/// <summary>
		/// テキスト(ASCII)形式でのSTLファイル読み込み
		/// </summary>
		/// <param name="filePath">ファイルパス</param>
		/// <returns>正常終了した場合は true、その他の場合は false</returns>
		public bool ReadAscii(string filePath)
		{
			// filePath が null か、ファイルが存在しない場合はエラーとする
			if (filePath == null || File.Exists(filePath) == false)
				return false;

			try
			{
				// テキストファイルの読み込み
				using (StreamReader reader = new StreamReader(filePath, Encoding.ASCII))
				{
					// ファセットデータの一時格納用リスト
					var facets = new List<Facet>();

					// ファセット1個分のデータ生成
					var facet = new Facet();

					// 頂点読み込み時、何個目の頂点か
					int vertexNumber = 0;

					// ファイル末尾まで繰り返す
					while (!reader.EndOfStream)
					{
						// ファイルの一行を読み込む
						string line = reader.ReadLine();

						// 一行が空ならスルー
						if (line == null || line.Length <= 0)
							continue;

						// 一行の先頭の空白文字を削除
						line = line.TrimStart();

						if (line.StartsWith("vertex "))
						{
							var text = line.Remove(0, 7);
							switch (vertexNumber)
							{
								case 0: TextToVertex(text, ref facet.Vertex1); break;
								case 1: TextToVertex(text, ref facet.Vertex2); break;
								case 2: TextToVertex(text, ref facet.Vertex3); break;
							}
							vertexNumber++;
						}
						else if (line.StartsWith("facet normal "))
						{
							var text = line.Remove(0, 13);
							TextToVertex(text, ref facet.Normal);
						}
						else if (line.StartsWith("endfacet"))
						{
							facets.Add(facet);
							facet = new Facet();
							vertexNumber = 0;
						}
						else if (line.StartsWith("solid "))
						{
							var header = line.Remove(0, 6);
							Header = Encoding.ASCII.GetBytes(header);
						}
						else if (line.StartsWith("endsolid "))
						{
							var footer = line.Remove(0, 9);
							Footer = Encoding.ASCII.GetBytes(footer);
						}

						// テキストを頂点データに変換する
						void TextToVertex(string text, ref Vertex vec)
						{
							var values = text.Split(' ');
							vec.X = float.Parse(values[0], NumberStyles.Float);
							vec.Y = float.Parse(values[1], NumberStyles.Float);
							vec.Z = float.Parse(values[2], NumberStyles.Float);
						}
					}

					// ファセットデータの一時格納用リストを配列化してメンバーに設定
					Facets = facets.ToArray();
				}
			}
			catch (Exception)
			{
				return false;
			}
			return true;
		}
		#endregion Methods
	}

	/// <summary>
	/// 頂点
	/// </summary>
	public struct Vertex
	{
		#region Constructions
		/// <summary>
		/// コンストラクタ
		/// </summary>
		public Vertex(float x, float y, float z)
		{
			X = x;
			Y = y;
			Z = z;
		}
		#endregion Constructions

		#region Properties
		/// <summary>
		/// X成分
		/// </summary>
		public float X { get; set; }

		/// <summary>
		/// Y成分
		/// </summary>
		public float Y { get; set; }

		/// <summary>
		/// Z成分
		/// </summary>
		public float Z { get; set; }
		#endregion Properties
	}

	/// <summary>
	/// ファセット(3頂点と1法線で表現する三角形)
	/// </summary>
	public class Facet
	{
		#region Constructions
		/// <summary>
		/// コンストラクタ
		/// </summary>
		public Facet()
		{
		}

		/// <summary>
		/// コンストラクタ
		/// </summary>
		public Facet(in Vertex normal, in Vertex vertex1, in Vertex vertex2, in Vertex vertex3)
		{
			Normal = normal;
			Vertex1 = vertex1;
			Vertex2 = vertex2;
			Vertex3 = vertex3;
		}
		#endregion Constructions

		#region Properties
		/// <summary>
		/// 法線
		/// </summary>
		public Vertex Normal;

		/// <summary>
		/// 1点目頂点
		/// </summary>
		public Vertex Vertex1;

		/// <summary>
		/// 2点目頂点
		/// </summary>
		public Vertex Vertex2;

		/// <summary>
		/// 3点目頂点
		/// </summary>
		public Vertex Vertex3;
		#endregion Properties
	}
}

開発環境:Windows 10 / Visual Studio 2019 / .NET Framework 4.8
ソースコードは Windows 環境での動作を前提としています。
ご自由にお使いいただいて構いませんが、自己責任でお願いします。

スポンサーリンク
スポンサーリンク
スポンサーリンク
C#
スポンサーリンク
スポンサーリンク
スポンサーリンク
スポンサーリンク
スポンサーリンク
スポンサーリンク
スポンサーリンク
スポンサーリンク
〜 コーディング広場 Coding Square 〜

コメント

タイトルとURLをコピーしました