Home Manual Reference Source

src/gl/S3GLMesh.js

import S3Vector from "../math/S3Vector.js";
import S3System from "../basic/S3System.js";
import S3Mesh from "../basic/S3Mesh.js";
import S3GLTexture from "./S3GLTexture.js";
import S3GLSystem from "./S3GLSystem.js";
import S3GLVertex from "./S3GLVertex.js";
import S3GLTriangleIndex from "./S3GLTriangleIndex.js";
import S3GLMaterial from "./S3GLMaterial.js";
import S3GLTriangleIndexData from "./S3GLTriangleIndexData.js";

/**
 * WebGL用のメッシュ(立体形状データ)を管理するクラスです。
 * S3Meshを拡張し、WebGL描画に必要なVBOやIBO情報、GL用データ生成・解放機能などを持ちます。
 * モデルの描画時にGLにバインドできるバッファ形式への変換・管理も行います。
 *
 * @class
 * @extends S3Mesh
 * @module S3
 */
export default class S3GLMesh extends S3Mesh {
	/**
	 * S3GLMeshのインスタンスを生成します。
	 * @param {S3GLSystem} s3glsystem WebGLシステム(GLContext等の管理)インスタンス
	 */
	constructor(s3glsystem) {
		super(s3glsystem);

		/**
		 * S3GLSystem アクセス用
		 * @type {S3GLSystem}
		 */
		this._s3gl = s3glsystem;
	}

	/**
	 * メッシュの内部状態とWebGL用データ(gldata)を初期化します。
	 * 通常はコンストラクタから自動的に呼ばれます。
	 */
	_init() {
		super._init();

		/**
		 * WebGL用バッファデータ格納オブジェクト
		 * @type {S3GLMeshArrayData}
		 */
		this.gldata = null;

		/**
		 * GL用データのコンパイル状態
		 * @type {boolean}
		 */
		this.is_compile_gl = false;
	}

	/**
	 * このメッシュのクローン(複製)を生成します。
	 * @returns {S3GLMesh} 複製されたS3GLMeshインスタンス
	 */
	clone() {
		// @ts-ignore
		return /** @type {S3GLMesh} */ super.clone(S3GLMesh);
	}

	/**
	 * メッシュが保持する頂点配列を取得します。
	 * @returns {Array<S3GLVertex>} 頂点配列
	 */
	getVertexArray() {
		// @ts-ignore
		return /** @type {Array<S3GLVertex>} */ super.getVertexArray();
	}

	/**
	 * メッシュが保持する三角形インデックス配列を取得します。
	 * @returns {Array<S3GLTriangleIndex>} 三角形インデックス配列
	 */
	getTriangleIndexArray() {
		// @ts-ignore
		return /** @type {Array<S3GLTriangleIndex>} */ super.getTriangleIndexArray();
	}

	/**
	 * メッシュが保持するマテリアル配列を取得します。
	 * @returns {Array<S3GLMaterial>} マテリアル配列
	 */
	getMaterialArray() {
		// @ts-ignore
		return /** @type {Array<S3GLMaterial>} */ super.getMaterialArray();
	}

	/**
	 * 頂点( S3GLVertex またはその配列)をメッシュに追加します。
	 * @param {S3GLVertex|Array<S3GLVertex>} vertex 追加する頂点またはその配列
	 */
	addVertex(vertex) {
		// @ts-ignore
		super.addVertex(vertex);
	}

	/**
	 * 三角形インデックス( S3GLTriangleIndex またはその配列)をメッシュに追加します。
	 * 反転モード時は面を裏返して追加します。
	 * @param {S3GLTriangleIndex|Array<S3GLTriangleIndex>} ti 追加する三角形インデックスまたはその配列
	 */
	addTriangleIndex(ti) {
		// @ts-ignore
		super.addTriangleIndex(ti);
	}

	/**
	 * マテリアル( S3GLMaterial またはその配列)をメッシュに追加します。
	 * @param {S3GLMaterial|Array<S3GLMaterial>} material 追加するマテリアルまたはその配列
	 */
	addMaterial(material) {
		// @ts-ignore
		super.addMaterial(material);
	}

	/**
	 * WebGL用データがすでに作成済みかどうかを返します。
	 * @returns {boolean} 作成済みならtrue
	 */
	isCompileGL() {
		return this.is_compile_gl;
	}

	/**
	 * WebGL用データのコンパイル状態を設定します。
	 * @param {boolean} is_compile_gl コンパイル済みかどうか
	 */
	setCompileGL(is_compile_gl) {
		this.is_compile_gl = is_compile_gl;
	}

	/**
	 * 各三角形ごとに、WebGL用属性データ(頂点ごとの法線・接線等)を生成します。
	 * 頂点の共有を考慮して法線のスムージングも自動計算します。
	 * @returns {Array<S3GLTriangleIndexData>} 三角形ごとのGL用属性データリスト
	 */
	createTriangleIndexData() {
		const vertex_list = this.getVertexArray();
		const triangleindex_list = this.getTriangleIndexArray();

		/**
		 * @typedef {Object} S3GLNormalVector
		 * @property {S3Vector} normal 平面の法線
		 * @property {S3Vector} tangent UV座標による接線
		 * @property {S3Vector} binormal UV座標による従法線
		 */

		/**
		 * 三角形ごとのWebGL属性データリスト
		 * @type {Array<S3GLTriangleIndexData & { face : S3GLNormalVector }>}
		 */
		const tid_list = [];

		/**
		 * @typedef {"normal"|"tangent"|"binormal"} S3GLNormalListKey
		 */

		/**
		 * 面ごとの法線・接線・従法線名をまとめたオブジェクト
		 * @type {{ normal: boolean, tangent: boolean, binormal: boolean }}
		 */
		const normallist = {
			normal: false,
			tangent: false,
			binormal: false
		};

		// 各面の法線、接線、従法線を調べる
		for (let i = 0; i < triangleindex_list.length; i++) {
			const triangleindex = triangleindex_list[i];
			const index = triangleindex.index;
			const uv = triangleindex.uv;
			tid_list[i] = triangleindex.createGLTriangleIndexData();
			let vector_list = null;
			// 3点を時計回りで通る平面が表のとき
			if (this.sys.dimensionmode === S3System.DIMENSION_MODE.RIGHT_HAND) {
				vector_list = S3Vector.getNormalVector(
					vertex_list[index[0]].position,
					vertex_list[index[1]].position,
					vertex_list[index[2]].position,
					uv[0],
					uv[1],
					uv[2]
				);
			} else {
				vector_list = S3Vector.getNormalVector(
					vertex_list[index[2]].position,
					vertex_list[index[1]].position,
					vertex_list[index[0]].position,
					uv[2],
					uv[1],
					uv[0]
				);
			}

			tid_list[i].face = {
				normal: vector_list.normal,
				tangent: vector_list.tangent,
				binormal: vector_list.binormal
			};
		}

		// 素材ごとに、三角形の各頂点に、面の法線情報を追加する
		// 後に正規化する(平均値をとる)が、同じベクトルを加算しないようにキャッシュでチェックする

		/**
		 * マテリアルごと、頂点ごとの属性ベクトル情報リスト
		 * @type {Array<Array<{ normal: S3Vector, tangent: S3Vector, binormal: S3Vector }>>}
		 */
		const vertexdatalist_material = [];

		/**
		 * 各マテリアル・頂点ごと、法線等ベクトルごとのキャッシュ管理
		 * @type {Array<Array<{ normal: Object<string, boolean>, tangent: Object<string, boolean>, binormal: Object<string, boolean> }>>}
		 */
		const vertexdatalist_material_cash = [];
		for (let i = 0; i < triangleindex_list.length; i++) {
			const triangleindex = triangleindex_list[i];
			const material = triangleindex.materialIndex;
			const triangledata = tid_list[i];
			// 未登録なら新規作成する
			if (vertexdatalist_material[material] === undefined) {
				vertexdatalist_material[material] = [];
				vertexdatalist_material_cash[material] = [];
			}
			const vertexdata_list = vertexdatalist_material[material];
			const vertexdata_list_cash = vertexdatalist_material_cash[material];
			// 素材ごとの三角形の各頂点に対応する法線情報に加算していく
			for (let j = 0; j < 3; j++) {
				// 未登録なら新規作成する
				const index = triangleindex.index[j];
				if (vertexdata_list[index] === undefined) {
					vertexdata_list[index] = {
						normal: new S3Vector(0, 0, 0),
						tangent: new S3Vector(0, 0, 0),
						binormal: new S3Vector(0, 0, 0)
					};
					vertexdata_list_cash[index] = {
						normal: {},
						tangent: {},
						binormal: {}
					};
				}
				const vertexdata = vertexdata_list[index];
				const vertexdata_cash = vertexdata_list_cash[index];

				// 加算する
				for (const vector_name in normallist) {
					const key = /** @type {S3GLNormalListKey} */ (vector_name);
					if (triangledata.face[key] !== null) {
						// データが入っていたら加算する
						const id = triangledata.face[key].toHash(3);
						if (vertexdata_cash[key][id]) continue;
						vertexdata[key] = vertexdata[key].add(triangledata.face[key]);
						vertexdata_cash[key][id] = true;
					}
				}
			}
		}

		// マテリアルごとの頂点の法線を、正規化して1とする(平均値をとる)
		for (const material in vertexdatalist_material) {
			const vertexdata_list = vertexdatalist_material[material];
			for (const index in vertexdata_list) {
				const vertexdata = vertexdata_list[index];
				for (const vector_name in normallist) {
					const key = /** @type {S3GLNormalListKey} */ (vector_name);
					// あまりに小さいと、0で割ることになるためチェックする
					if (vertexdata[key].normFast() > 0.000001) {
						vertexdata[key] = vertexdata[key].normalize();
					}
				}
			}
		}

		// 面法線と、頂点(スムーズ)法線との角度の差が、下記より大きい場合は面法線を優先
		const SMOOTH = {};
		SMOOTH.normal = Math.cos((50 / 360) * (2 * Math.PI));
		SMOOTH.tangent = Math.cos((50 / 360) * (2 * Math.PI));
		SMOOTH.binormal = Math.cos((50 / 360) * (2 * Math.PI));

		// 最終的に三角形の各頂点の法線を求める
		for (let i = 0; i < triangleindex_list.length; i++) {
			const triangleindex = triangleindex_list[i];
			const material = triangleindex.materialIndex;
			const triangledata = tid_list[i];
			const vertexdata_list = vertexdatalist_material[material];

			// 法線ががあまりに違うのであれば、面の法線を採用する
			for (let j = 0; j < 3; j++) {
				const index = triangleindex.index[j];
				const vertexdata = vertexdata_list[index];
				for (const vector_name in normallist) {
					const key = /** @type {S3GLNormalListKey} */ (vector_name);
					let targetdata;
					if (triangledata.face[key]) {
						// 面で計算した値が入っているなら、
						// 面で計算した値と、頂点の値とを比較してどちらかを採用する
						const rate = triangledata.face[key].dot(vertexdata[key]);
						// 指定した度以上傾いていたら、面の法線を採用する
						targetdata = rate < SMOOTH[key] ? triangledata.face : vertexdata;
					} else {
						targetdata = vertexdata;
					}
					// コピー
					triangledata.vertex[key][j] = targetdata[key];
				}
			}
		}

		return tid_list;
	}

	/**
	 * IBO(インデックスバッファオブジェクト)データ構造
	 * @typedef {Object} S3GLMeshIBOData
	 * @property {number} array_length 配列の要素数(インデックス総数)
	 * @property {Int16Array} array インデックス値の配列(WebGL用)
	 * @property {WebGLBuffer} [data] GL生成後のバッファオブジェクト(未生成時はundefined)
	 */

	/**
	 * VBO(頂点バッファオブジェクト)1要素のデータ構造
	 * @typedef {Object} S3GLMeshVBOElement
	 * @property {string} name 属性名(例:"position", "normal", "uv" など)
	 * @property {number} dimension 配列の次元(例:位置なら3、UVなら2など)
	 * @property {typeof Float32Array | typeof Int32Array} datatype 使用する配列型
	 * @property {number} array_length 配列の要素数(全頂点×次元)
	 * @property {Float32Array | Int32Array} array 属性データ本体
	 * @property {WebGLBuffer} [data] GL生成後のバッファオブジェクト(未生成時はundefined)
	 */

	/**
	 * VBO(頂点バッファオブジェクト)全体のデータ構造
	 * @typedef {Object.<string, S3GLMeshVBOElement>} S3GLMeshVBOData
	 * 属性名(position/normal/uv等)→S3GLVBOElementの連想配列
	 */

	/**
	 * _getGLArrayDataの返却値(IBOとVBOまとめて返す構造)
	 * @typedef {Object} S3GLMeshArrayData
	 * @property {S3GLMeshIBOData} ibo インデックスバッファ情報
	 * @property {S3GLMeshVBOData} vbo 頂点バッファ情報
	 */

	/**
	 * メッシュ全体の頂点・インデックス情報をWebGL用のバッファ形式(VBO/IBO)に変換します。
	 * すでに計算済みなら再計算は行いません。
	 *
	 * - IBOはポリゴン(三角形)の頂点インデックス列
	 * - VBOは各頂点の属性(位置、法線、UV等)の配列
	 * - 戻り値の各dataプロパティは、GLバッファ生成後のみセットされます
	 *
	 * @returns {S3GLMeshArrayData} IBO/VBOデータをまとめたオブジェクト
	 */
	_getGLArrayData() {
		/**
		 * 頂点配列
		 * @type {Array<S3GLVertex>}
		 */
		const vertex_list = this.getVertexArray();

		/**
		 * 三角形インデックスデータ配列
		 * @type {Array<S3GLTriangleIndexData>}
		 */
		const triangleindex_list = this.createTriangleIndexData();

		/**
		 * 頂点ハッシュ文字列→頂点配列インデックスの対応表
		 * @type {Object<string, number>}
		 */
		const hashlist = {};

		let vertex_length = 0;

		/**
		 * 三角形ごとの頂点インデックス配列
		 * @type {Array<Int16Array>}
		 */
		const triangle = [];

		/**
		 * 属性ごとの頂点データリスト(raw属性値の配列)
		 * @type {Object<string, Array<any>>}
		 */
		const vertextypelist = {};

		// インデックスを再構築して、VBOとIBOを作る
		// 今の生データだと、頂点情報、素材情報がばらばらに保存されているので
		// 1つの頂点情報(位置、色等)を1つのセットで保存する必要がある
		// 面に素材が結びついているので、面が1つの頂点を共有していると
		// それらの面の素材情報によって、別の頂点として扱う必要がある
		// なので基本的には頂点情報を毎回作り直す必要があるが、
		// 1度作ったものと等しいものが必要であれば、キャッシュを使用する
		for (let i = 0; i < triangleindex_list.length; i++) {
			const triangleindex = triangleindex_list[i];

			/**
			 * 1つの三角形(face)に対する3頂点のインデックス番号リスト
			 * @type {Array<number>}
			 */
			const indlist = [];
			// ポリゴンの各頂点を調べる
			for (let j = 0; j < 3; j++) {
				// その頂点(面の情報(UVなど)も含めたデータ)のハッシュ値を求める
				const hash = triangleindex.getGLHash(j, vertex_list);
				// すでに以前と同一の頂点があるならば、その頂点アドレスを選択。ない場合は新しいアドレス
				const hit = hashlist[hash];
				indlist[j] = hit !== undefined ? hit : vertex_length;
				// 頂点がもしヒットしていなかったら
				if (hit === undefined) {
					// 頂点データを作成して
					const vertexdata = triangleindex.getGLData(j, vertex_list);
					hashlist[hash] = vertex_length;
					// 頂点にはどういった情報があるか分からないので、in を使用する。
					// key には、position / normal / color / uv などがおそらく入っている
					for (const key in vertexdata) {
						if (vertextypelist[key] === undefined) {
							vertextypelist[key] = [];
						}
						vertextypelist[key].push(vertexdata[key]);
					}
					vertex_length++;
				}
			}
			// 3つの頂点のインデックスを記録
			triangle[i] = new Int16Array(indlist);
		}

		// データ結合処理
		// これまでは複数の配列にデータが入ってしまっているので、
		// 1つの指定した型の配列に全てをまとめる必要がある

		let pt = 0;

		/**
		 * IBOデータ格納用
		 * @type {S3GLMeshIBOData}
		 */
		const ibo = {};
		{
			// IBOの結合(インデックス)
			ibo.array_length = triangleindex_list.length * 3;
			ibo.array = new Int16Array(ibo.array_length);
			pt = 0;
			for (let i = 0; i < triangleindex_list.length; i++) {
				for (let j = 0; j < 3; j++) {
					ibo.array[pt++] = triangle[i][j];
				}
			}
		}

		/**
		 * VBOデータ格納用
		 * @type {S3GLMeshVBOData}
		 */
		const vbo = {};
		{
			// VBOの結合(頂点)
			// 位置、法線、色などを、それぞれ1つの配列として記録する
			for (const key in vertextypelist) {
				const srcdata = vertextypelist[key];
				const dimension = srcdata[0].dimension;
				const dstdata = {};
				// 情報の名前(position / uv / normal など)
				dstdata.name = key;
				// 1つの頂点あたり、いくつの値が必要か。例えばUVなら2次元情報
				dstdata.dimension = srcdata[0].dimension;
				// 型情報 Float32Array / Int32Array なのかどうか
				dstdata.datatype = srcdata[0].datatype;
				// 配列の長さ
				dstdata.array_length = dimension * vertex_length;
				// 型情報と、配列の長さから、メモリを確保する
				dstdata.array = new dstdata.datatype.instance(dstdata.array_length);
				// data を1つの配列に結合する
				pt = 0;
				for (let i = 0; i < vertex_length; i++) {
					for (let j = 0; j < dimension; j++) {
						dstdata.array[pt++] = srcdata[i].data[j];
					}
				}
				// VBOオブジェクトに格納
				vbo[key] = dstdata;
			}
		}

		const arraydata = {};
		arraydata.ibo = ibo;
		arraydata.vbo = vbo;
		return arraydata;
	}

	/**
	 * WebGL用バッファ(IBO/VBO)やテクスチャなどのGLリソースを開放し、再利用不可にします。
	 * テクスチャを含むマテリアルのリソースも解放対象です。
	 * @returns {void}
	 */
	dispose() {
		// コンパイルしていなかったら抜ける
		if (!this.isCompileGL()) {
			return;
		}
		const gldata = this.getGLData();
		if (gldata !== null) {
			if (gldata.ibo !== undefined) {
				if (gldata.ibo.data !== undefined) {
					this._s3gl.glfunc.deleteBuffer(gldata.ibo.data);
				}
				delete gldata.ibo;
			}
			if (gldata.vbo !== undefined) {
				for (const key in gldata.vbo) {
					if (gldata.vbo[key].data !== undefined) {
						this._s3gl.glfunc.deleteBuffer(gldata.vbo[key].data);
					}
				}
				delete gldata.vbo;
			}
			{
				const material_list = this.getMaterialArray();
				for (let i = 0; i < material_list.length; i++) {
					const mat = material_list[i];
					mat.textureColor.dispose();
					mat.textureNormal.dispose();
				}
			}
		}
		delete this.gldata;
		this.gldata = null;
		this.setCompileGL(false);
		super.dispose();
	}

	/**
	 * メッシュのGLデータ(VBO/IBO)を取得・生成します。
	 * すでに生成済みならキャッシュを返します。
	 * メッシュが未完成または GLContext が未セットの場合はnullを返します。
	 * @returns {S3GLMeshArrayData|null} WebGL用バッファデータ(ibo, vbo等を含む)またはnull
	 */
	getGLData() {
		// すでに存在している場合は、返す
		if (this.isCompileGL()) {
			return this.gldata;
		}
		// 完成していない場合は null
		if (this.isComplete() === false) {
			return null;
		}
		// GLを取得できない場合も、この時点で終了させる
		if (!this._s3gl.isSetGL()) {
			return null;
		}
		const gldata = this._getGLArrayData(); // GL用の配列データを作成

		// IBO / VBO 用のオブジェクトを作成
		gldata.ibo.data = this._s3gl.glfunc.createBufferIBO(gldata.ibo.array);
		for (const key in gldata.vbo) {
			gldata.vbo[key].data = this._s3gl.glfunc.createBufferVBO(gldata.vbo[key].array);
		}
		// 代入
		this.gldata = gldata;
		this.setCompileGL(true);
		return this.gldata;
	}
}