Home Manual Reference Source

src/loader/S3MeshLoaderMQO.js

import S3System from "../basic/S3System.js";
import S3Mesh from "../basic/S3Mesh.js";
import S3Vector from "../math/S3Vector.js";

/**
 * パス名操作・ファイルパスの解決用ヘルパークラス
 *
 * - MQOファイル内や外部ファイルへの参照(テクスチャパス等)を絶対パスに変換するために利用されます。
 * - `getAbsolutePath()` でファイルの絶対パスを計算し、`getParent()` で親ディレクトリのパスも取得できます。
 * - 内部的にパスの区切りを正規化(バックスラッシュ→スラッシュ)します。
 */
class File {
	/**
	 * ファイルインスタンスを生成します。
	 * @param {string} pathname ファイルパスやURL
	 */
	constructor(pathname) {
		/**
		 * 正規化済みパス
		 * @type {string}
		 */
		this.pathname = pathname.replace(/\\/g, "/");
	}

	/**
	 * ファイルの絶対パスを取得します。
	 * - http(s)の場合はそのまま
	 * - 相対パスの場合は現在のURLから解決
	 *
	 * @returns {string} 絶対パス(URL形式)
	 */
	getAbsolutePath() {
		if (/$http/.test(this.pathname)) {
			return this.pathname;
		}
		let name = window.location.toString();
		if (!/\/$/.test(name)) {
			name = name.match(/.*\//)[0];
		}
		const namelist = this.pathname.split("/");
		for (let i = 0; i < namelist.length; i++) {
			if (namelist[i] === "" || namelist[i] === ".") {
				continue;
			}
			if (namelist[i] === "..") {
				name = name.substring(0, name.length - 1).match(/.*\//)[0];
				continue;
			}
			name += namelist[i];
			if (i !== namelist.length - 1) {
				name += "/";
			}
		}
		return name;
	}

	/**
	 * 親ディレクトリのパスを取得します。
	 * @returns {string} 親ディレクトリの絶対パス
	 */
	getParent() {
		const x = this.getAbsolutePath().match(/.*\//)[0];
		return x.substring(0, x.length - 1);
	}
}

/**
 * Metasequoia(MQO)形式による3DCGメッシュデータの入出力ユーティリティ
 *
 * - S3MeshLoader.TYPE.MQO として S3MeshLoader から利用されます。
 * - メタセコイア(*.mqo)フォーマットのテキストをS3Meshに変換(インポート)、またはS3Meshからテキスト出力(エクスポート)します。
 * - 標準的なMQOの構文に加え、一部簡易パース(手動修正を要する場合もあり)。
 *
 * ※ テクスチャやUV、マテリアルの色・強度なども一部対応しています。
 */
const S3MeshLoaderMQO = {
	/**
	 * メッシュデータの入出力形式名
	 * @type {string}
	 */
	name: "MQO",

	/**
	 * Metasequoia(MQO)形式のテキストをS3Meshインスタンスに変換します(インポート)。
	 * ただしある程度手動で修正しないといけません。
	 *
	 * - MQO形式のテキスト(またはURL経由でダウンロード済みのテキスト)を解析し、
	 *   頂点・三角形面・マテリアル等をS3Meshに格納します。
	 * - テクスチャ名・UV座標・マテリアル強度・色・発光・反射等にも部分的に対応しています。
	 * - ファイル内の階層(オブジェクトブロック)・面(face)・材質(Material)を検出してパースします。
	 *
	 * @param {S3System} sys S3Systemインスタンス
	 * @param {S3Mesh} mesh メッシュインスタンス(空の状態で渡される)
	 * @param {string} text MQOファイル内容(テキスト)
	 * @param {string} [url] オプション: ファイルURLやパス
	 * @returns {boolean} パース成功時はtrue
	 *
	 * @example
	 * S3MeshLoaderMQO.input(sys, mesh, mqotext);
	 */
	input: function (sys, mesh, text, url) {
		let mqofile = null;
		let parent_dir = "./";
		if (url) {
			mqofile = new File(url);
			parent_dir = mqofile.getParent() + "/";
		}

		const lines = text.split("\n");
		const block_stack = [];
		let block_type = "none";
		let block_level = 0;
		let vertex_offset = 0;
		let vertex_point = 0;
		let face_offset = 0;
		let face_point = 0;
		/**
		 * 半角スペース区切りの文字列数値を数値型配列に変換します。
		 *
		 * @param {string} text 変換対象の文字列(例:"1.0 2.5 3.14")
		 * @returns {Array<number>} 数値型の配列
		 */
		const toNumberArray = function (text) {
			const x = text.split(" "),
				out = [];
			for (let i = 0; i < x.length; i++) {
				out[i] = parseFloat(x[i]);
			}
			return out;
		};

		/**
		 * "func(XXX)" の形式から、指定パラメータ名 parameter の括弧内の値を抜き出します。
		 *
		 * @param {string} text 対象となる1行分のテキスト
		 * @param {string} parameter 抜き出したいパラメータ名
		 * @returns {string} パラメータの中身
		 */
		const getValueFromPrm = function (text, parameter) {
			const x = text.split(" " + parameter + "(");
			if (x.length === 1) {
				return null; // パラメータが見つからない場合はnullを返す
			}
			return x[1].split(")")[0];
		};

		/**
		 * "func(XXX)" の形式から、数値パラメータを配列として取得します。
		 *
		 * @param {string} text 対象となる1行分のテキスト
		 * @param {string} parameter 抜き出したいパラメータ名
		 * @returns {Array<number>} 数値型配列(見つからなければ空配列)
		 */
		const getNumberFromPrm = function (text, parameter) {
			const value = getValueFromPrm(text, parameter);
			if (value === null) {
				return [];
			}
			return toNumberArray(value);
		};

		/**
		 * "func(XXX)" の形式から、ダブルクォート囲みのURLやファイル名を抽出します。
		 *
		 * @param {string} text 対象となる1行分のテキスト
		 * @param {string} parameter 抜き出したいパラメータ名
		 * @returns {string|null} 抜き出したURL文字列、またはnull(見つからなければ)
		 */
		const getURLFromPrm = function (text, parameter) {
			const value = getValueFromPrm(text, parameter);
			if (value === null) {
				return null;
			}
			const x = value.split('"');
			if (x.length !== 3) {
				return null;
			}
			return x[1];
		};
		// メインのパース処理
		for (let i = 0; i < lines.length; i++) {
			const trim_line = lines[i].replace(/^\s+|\s+$/g, "");
			const first = trim_line.split(" ")[0];
			if (trim_line.indexOf("{") !== -1) {
				if (first === "Object") {
					vertex_offset += vertex_point;
					face_offset += face_point;
					vertex_point = 0;
					face_point = 0;
				}
				// 階層に入る前の位置を保存
				block_stack.push(block_type);
				block_type = first;
				block_level++;
				continue;
			} else if (trim_line.indexOf("}") !== -1) {
				block_type = block_stack.pop();
				block_level--;
				continue;
			}
			if (block_type === "Thumbnail" || block_type === "none") {
				continue;
			}
			if (block_type === "Material") {
				const material_name = first.replace(/"/g, "");
				const material = sys.createMaterial();
				material.setName(material_name);
				let val;
				val = getNumberFromPrm(trim_line, "col");
				if (val.length !== 0) {
					material.setColor(new S3Vector(val[0], val[1], val[2], val[3]));
				}
				val = getNumberFromPrm(trim_line, "dif");
				if (val.length !== 0) {
					material.setDiffuse(val[0]);
				}
				val = getNumberFromPrm(trim_line, "amb");
				if (val.length !== 0) {
					material.setAmbient(new S3Vector(val[0], val[0], val[0]));
				}
				val = getNumberFromPrm(trim_line, "amb_col");
				if (val.length !== 0) {
					material.setAmbient(new S3Vector(val[0], val[1], val[2]));
				}
				val = getNumberFromPrm(trim_line, "emi");
				if (val.length !== 0) {
					material.setEmission(new S3Vector(val[0], val[0], val[0]));
				}
				val = getNumberFromPrm(trim_line, "emi_col");
				if (val.length !== 0) {
					material.setEmission(new S3Vector(val[0], val[1], val[2]));
				}
				val = getNumberFromPrm(trim_line, "spc");
				if (val.length !== 0) {
					material.setSpecular(new S3Vector(val[0], val[0], val[0]));
				}
				val = getNumberFromPrm(trim_line, "spc_col");
				if (val.length !== 0) {
					material.setSpecular(new S3Vector(val[0], val[1], val[2]));
				}
				val = getNumberFromPrm(trim_line, "power");
				if (val.length !== 0) {
					material.setPower(val[0]);
				}
				val = getNumberFromPrm(trim_line, "reflect");
				if (val.length !== 0) {
					material.setReflect(val[0]);
				}
				val = getURLFromPrm(trim_line, "tex");
				if (val) {
					material.setTextureColor(parent_dir + val);
				}
				val = getURLFromPrm(trim_line, "bump");
				if (val) {
					material.setTextureNormal(parent_dir + val);
				}
				mesh.addMaterial(material);
			} else if (block_type === "vertex") {
				const words = toNumberArray(trim_line);
				const vector = new S3Vector(words[0], words[1], words[2]);
				const vertex = sys.createVertex(vector);
				mesh.addVertex(vertex);
				vertex_point++;
			} else if (block_type === "face") {
				const facenum = parseInt(first);
				const v = getNumberFromPrm(trim_line, "V");
				const uv_a = getNumberFromPrm(trim_line, "UV");
				const uv = [];
				const material_array = getNumberFromPrm(trim_line, "M");
				const material = material_array.length === 0 ? 0 : material_array[0];
				if (uv_a.length !== 0) {
					for (let j = 0; j < facenum; j++) {
						uv[j] = new S3Vector(uv_a[j * 2], uv_a[j * 2 + 1], 0);
					}
				}
				for (let j = 0; j < facenum - 2; j++) {
					const ti =
						j % 2 === 0
							? sys.createTriangleIndex(j, j + 1, j + 2, v, material, uv)
							: sys.createTriangleIndex(j - 1, j + 1, j + 2, v, material, uv);
					mesh.addTriangleIndex(ti);
					face_point++;
				}
			}
		}
		return true;
	},

	/**
	 * S3MeshインスタンスをMetasequoia(MQO)形式のテキストに変換します(エクスポート)。
	 * ただしある程度手動で修正しないといけません。
	 *
	 * - MQO形式に従い、頂点座標・面情報・マテリアル情報等を出力します。
	 * - テクスチャやUV・発光などの追加情報も一部対応。
	 * - 出力後のテキストは、必要に応じて手動修正で他のソフトへインポート可能です。
	 *
	 * @param {S3Mesh} mesh 出力対象のメッシュ
	 * @returns {string} MQOフォーマットのテキストデータ
	 *
	 * @example
	 * const mqotext = S3MeshLoaderMQO.output(mesh);
	 */
	output: function (mesh) {
		const output = [];
		const vertex = mesh.getVertexArray();
		const triangleindex = mesh.getTriangleIndexArray();
		const material = mesh.getMaterialArray();

		// ヘッダ
		output.push("Metasequoia Document");
		output.push("Format Text Ver 1.0");
		output.push("");
		output.push("Scene {");
		output.push("	pos 0 0 1500");
		output.push("	lookat 0 0 0");
		output.push("	head -0.5236");
		output.push("	pich 0.5236");
		output.push("	ortho 0");
		output.push("	zoom2 5.0000");
		output.push("	amb 0.250 0.250 0.250");
		output.push("}");

		// 材質の出力
		output.push("Material " + material.length + " {");
		for (let i = 0; i < material.length; i++) {
			const mv = material[i];
			//  こんな感じにする必要がある・・・
			// "mat" shader(3) col(1.000 1.000 1.000 0.138) dif(0.213) amb(0.884) emi(0.301) spc(0.141) power(38.75) amb_col(1.000 0.996 0.000) emi_col(1.000 0.000 0.016) spc_col(0.090 0.000 1.000) reflect(0.338) refract(2.450)
			output.push(
				'\t"' +
					mv.name +
					'" col(1.000 1.000 1.000 1.000) dif(0.800) amb(0.600) emi(0.000) spc(0.000) power(5.00)'
			);
		}
		output.push("}");

		// オブジェクトの出力
		output.push('Object "obj1" {');
		{
			// 頂点の出力
			output.push("\tvertex " + vertex.length + " {");
			for (let i = 0; i < vertex.length; i++) {
				const vp = vertex[i].position;
				output.push("\t\t" + vp.x + " " + vp.y + " " + vp.z);
			}
			output.push("}");

			// 面の定義
			output.push("\tface " + triangleindex.length + " {");
			for (let i = 0; i < triangleindex.length; i++) {
				const ti = triangleindex[i];
				let line = "\t\t3";
				// 座標と材質は必ずある
				line += " V(" + ti.index[0] + " " + ti.index[1] + " " + ti.index[2] + ")";
				line += " M(" + ti.materialIndex + ")";
				// UVはないかもしれないので、条件を付ける
				if (ti.uv !== undefined && ti.uv[0] !== null) {
					line +=
						" UV(" +
						ti.uv[0].x +
						" " +
						ti.uv[0].y +
						" " +
						ti.uv[1].x +
						" " +
						ti.uv[1].y +
						" " +
						ti.uv[2].x +
						" " +
						ti.uv[2].y +
						")";
				}
				output.push(line);
			}
		}
		output.push("\t}");

		output.push("}");

		// End
		output.push("Eof\n");
		return output.join("\n");
	}
};

export default S3MeshLoaderMQO;