Home Manual Reference Source

src/gl/S3GLSystem.js

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

import S3GLProgram from "./S3GLProgram.js";

import S3GLLight from "./S3GLLight.js";
import S3GLMaterial from "./S3GLMaterial.js";
import S3GLMesh from "./S3GLMesh.js";
import S3GLShader from "./S3GLShader.js";
import S3GLModel from "./S3GLModel.js";
import S3GLScene from "./S3GLScene.js";
import S3GLTexture from "./S3GLTexture.js";
import S3GLTriangleIndex from "./S3GLTriangleIndex.js";
import S3GLVertex from "./S3GLVertex.js";

/**
 * WebGLレンダリングシステムを管理するクラス。
 * シェーダー、テクスチャ、バッファオブジェクトの生成・管理、および描画制御を担当。
 * WebGLの初期化やプログラムのセットアップ、シーンの描画などの処理を含む。
 *
 * @class
 * @extends S3System
 * @module S3
 */
export default class S3GLSystem extends S3System {
	/**
	 * S3GLSystemインスタンスを生成します。
	 * WebGLコンテキストやプログラムの初期設定を行います。
	 */
	constructor() {
		super();

		/** @type {?S3GLProgram} 現在使用中のプログラム */
		this.program = null;

		/** @type {?WebGLRenderingContext} WebGLレンダリングコンテキスト */
		this.gl = null;

		/** @type {boolean} プログラムがセット済みかどうか */
		this.is_set = false;

		/** @type {Array<S3GLProgram>} 登録されているプログラムのリスト */
		this.program_list = [];

		/** @type {number} プログラムリストの識別ID */
		this.program_listId = 0;

		/** @type {HTMLCanvasElement} 描画に使うcanvas要素 */
		this.canvas = null;

		/** @type {?string} ダミーテクスチャのID文字列 */
		this._textureDummyId = undefined;

		/** @type {?WebGLTexture} ダミーテクスチャのWebGLTextureオブジェクト */
		this._textureDummyData = undefined;

		const that = this;

		/**
		 * @typedef {Object} S3GLFuncTextureCashEntry
		 * @property {WebGLTexture} texture WebGLテクスチャオブジェクト
		 * @property {number} count このテクスチャの参照カウント
		 */

		/**
		 * テクスチャキャッシュ全体の型定義。
		 * キーがテクスチャID(string)で、値がGLFuncTextureCashEntry型になります。
		 * @typedef {Object.<string, S3GLFuncTextureCashEntry>} S3GLFuncTextureCashTable
		 */

		/**
		 * テクスチャキャッシュ情報を管理するオブジェクトです。
		 * キーにテクスチャID(string)を持ち、値は
		 * { texture: WebGLTexture, count: number } のオブジェクト構造で、
		 * 生成済みWebGLTextureの使い回しや参照カウント管理に利用します。
		 *
		 * @type {S3GLFuncTextureCashTable}
		 */
		const glfunc_texture_cash = {};

		/**
		 * WebGLバッファ、テクスチャ、シェーダを作成・削除するユーティリティ関数群。
		 */
		this.glfunc = {
			/**
			 * 頂点バッファオブジェクト(VBO)を作成します。
			 * @param {Float32Array|Int32Array} data バッファデータ
			 * @returns {?WebGLBuffer} 作成したバッファオブジェクト
			 */
			createBufferVBO: function (data) {
				const gl = that.getGL();
				if (gl === null) {
					return null;
				}
				const vbo = gl.createBuffer();
				gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
				gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
				gl.bindBuffer(gl.ARRAY_BUFFER, null);
				return vbo;
			},

			/**
			 * インデックスバッファオブジェクト(IBO)を作成します。
			 * @param {Int16Array|Uint16Array} data インデックスバッファデータ
			 * @returns {?WebGLBuffer} 作成したインデックスバッファオブジェクト
			 */
			createBufferIBO: function (data) {
				const gl = that.getGL();
				if (gl === null) {
					return null;
				}
				const ibo = gl.createBuffer();
				gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
				gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW);
				gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
				return ibo;
			},

			/**
			 * 指定されたバッファを削除します。
			 * @param {WebGLBuffer} data 削除するバッファオブジェクト
			 * @returns {boolean} 成功時true
			 */
			deleteBuffer: function (data) {
				const gl = that.getGL();
				if (gl !== null) {
					return false;
				}
				gl.deleteBuffer(data);
				return true;
			},

			/**
			 * テクスチャオブジェクトを作成します。
			 * @param {string} id テクスチャの識別ID
			 * @param {ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} image テクスチャ画像
			 * @returns {?WebGLTexture} 作成したテクスチャオブジェクト
			 */
			createTexture: function (id, image) {
				if (
					!(image instanceof ImageData) &&
					!(image instanceof HTMLImageElement) &&
					!(image instanceof HTMLCanvasElement) &&
					!(image instanceof HTMLVideoElement)
				) {
					throw "createBufferTexture";
				}
				const gl = that.getGL();
				if (gl === null) {
					return null;
				}
				let texture = null;
				if (!glfunc_texture_cash[id]) {
					texture = gl.createTexture();
					gl.bindTexture(gl.TEXTURE_2D, texture);
					gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
					gl.generateMipmap(gl.TEXTURE_2D);
					gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
					const cash = {
						texture: texture,
						count: 0
					};
					glfunc_texture_cash[id] = cash;
				}
				texture = glfunc_texture_cash[id].texture;
				glfunc_texture_cash[id].count++;
				return texture;
			},

			/**
			 * 指定されたテクスチャを削除します。
			 * @param {string} id テクスチャの識別ID
			 */
			deleteTexture: function (id) {
				const gl = that.getGL();
				if (gl !== null) {
					if (glfunc_texture_cash[id]) {
						glfunc_texture_cash[id].count--;
						if (glfunc_texture_cash[id].count === 0) {
							gl.deleteTexture(glfunc_texture_cash[id].texture);
							delete glfunc_texture_cash[id];
						}
					}
				}
			},

			/**
			 * シェーダープログラムを作成します。
			 * @param {WebGLShader} shader_vertex 頂点シェーダ
			 * @param {WebGLShader} shader_fragment フラグメントシェーダ
			 * @returns {{program: WebGLProgram, is_error: boolean}} 作成結果
			 */
			createProgram: function (shader_vertex, shader_fragment) {
				const gl = that.getGL();
				if (gl === null) {
					return null;
				}
				let program = gl.createProgram();
				let is_error = false;
				gl.attachShader(program, shader_vertex);
				gl.attachShader(program, shader_fragment);
				gl.linkProgram(program);
				if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
					console.log("link error " + gl.getProgramInfoLog(program));
					gl.detachShader(program, shader_vertex);
					gl.detachShader(program, shader_fragment);
					gl.deleteProgram(program);
					program = null;
					is_error = true;
				}
				return {
					program: program,
					is_error: is_error
				};
			},

			/**
			 * シェーダープログラムを削除します。
			 * @param {WebGLProgram} program 削除するプログラム
			 * @param {WebGLShader} shader_vertex 頂点シェーダ
			 * @param {WebGLShader} shader_fragment フラグメントシェーダ
			 * @returns {boolean} 成功時true
			 */
			deleteProgram: function (program, shader_vertex, shader_fragment) {
				const gl = that.getGL();
				if (gl === null) {
					return false;
				}
				gl.detachShader(program, shader_vertex);
				gl.detachShader(program, shader_fragment);
				gl.deleteProgram(program);
				return true;
			},

			/**
			 * シェーダーを作成します。
			 * @param {number} sharder_type シェーダタイプ(gl.VERTEX_SHADER|gl.FRAGMENT_SHADER)
			 * @param {string} code シェーダのGLSLソースコード
			 * @returns {{shader: WebGLShader, is_error: boolean}} 作成結果
			 */
			createShader: function (sharder_type, code) {
				const gl = that.getGL();
				if (gl === null) {
					return null;
				}
				let shader = gl.createShader(sharder_type);
				let is_error = false;
				gl.shaderSource(shader, code);
				gl.compileShader(shader);
				if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
					console.log("compile error " + gl.getShaderInfoLog(shader));
					gl.deleteShader(shader);
					shader = null;
					is_error = true;
				}
				return {
					shader: shader,
					is_error: is_error
				};
			},

			/**
			 * 指定されたシェーダを削除します。
			 * @param {WebGLShader} shader 削除するシェーダ
			 * @returns {boolean} 成功時true
			 */
			deleteShader: function (shader) {
				const gl = that.getGL();
				if (gl === null) {
					return false;
				}
				gl.deleteShader(shader);
				return true;
			}
		};
	}

	/**
	 * WebGLコンテキストを取得します。
	 * @returns {WebGLRenderingContext} WebGLコンテキスト
	 */
	getGL() {
		return this.gl;
	}

	/**
	 * WebGLコンテキストが設定されているかを確認します。
	 * @returns {boolean} 設定済みの場合true
	 */
	isSetGL() {
		return this.gl !== null;
	}

	/**
	 * 描画対象となるCanvasを設定します。
	 * @param {HTMLCanvasElement} canvas 描画対象のCanvas要素
	 */
	setCanvas(canvas) {
		// 初期化色
		const gl = /** @type {WebGLRenderingContext} */ (
			canvas.getContext("webgl") || canvas.getContext("experimental-webgl")
		);

		this.canvas = canvas;
		this.gl = gl;
	}

	/**
	 * 新しいシェーダープログラムを生成し取得します。
	 * @returns {S3GLProgram} 新規生成されたシェーダープログラム
	 */
	createProgram() {
		const program = new S3GLProgram(this, this.program_listId);
		this.program_list[this.program_listId] = program;
		this.program_listId++;
		return program;
	}

	/**
	 * 登録されている全てのシェーダープログラムを破棄します。
	 */
	disposeProgram() {
		for (const key in this.program_list) {
			this.program_list[key].dispose();
			delete this.program_list[key];
		}
	}

	/**
	 * シェーダープログラムをアクティブにします。
	 * @param {S3GLProgram} glprogram アクティブに設定するシェーダープログラム
	 * @returns {boolean} 設定が成功した場合true
	 */
	setProgram(glprogram) {
		// nullの場合はエラーも無視
		if (glprogram === null) {
			return false;
		}
		// 明確な入力の誤り
		if (!(glprogram instanceof S3GLProgram)) {
			throw new Error("引数がS3GLProgramのインスタンスではありません。");
		}
		// 新規のプログラムなら保持しておく
		if (this.program === null) {
			this.program = glprogram;
		}
		// プログラムが取得できない場合は、ダウンロード中の可能性あり無視する
		const new_program = glprogram.getProgram();
		if (null === new_program) {
			return false;
		}
		// すでに動作中で、設定されているものと同一なら無視する
		if (this.program === glprogram && this.is_set) {
			return true;
		}
		// 新しいプログラムなのでセットする
		if (this.program !== null) {
			this.program.disuseProgram();
		}
		this.program = glprogram;
		this.program.useProgram();
		this.is_set = true;
	}

	/**
	 * 描画クリア処理を行います(背景色・深度バッファのリセット)。
	 * @returns {boolean} 成功時true
	 */
	clear() {
		if (this.gl === null) {
			return false;
		}
		const color = this.getBackgroundColor();
		this.gl.clearColor(color.x, color.y, color.z, color.w);
		this.gl.clearDepth(1.0);
		this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
		return true;
	}

	/**
	 * 指定されたインデックスサイズに基づいて要素を描画します。
	 * @param {number} indexsize インデックスバッファのサイズ
	 * @returns {boolean} 成功時true
	 */
	drawElements(indexsize) {
		if (!this.is_set) {
			return false;
		}
		this.gl.drawElements(this.gl.TRIANGLES, indexsize, this.gl.UNSIGNED_SHORT, 0);
		this.gl.flush();
		return true;
	}

	/**
	 * 指定したWebGLバッファを削除します。
	 * @param {WebGLBuffer} data 削除するバッファオブジェクト
	 * @returns {boolean} 成功時true
	 */
	deleteBuffer(data) {
		if (this.gl === null) {
			return false;
		}
		this.gl.deleteBuffer(data);
		return true;
	}

	/**
	 * 1x1ピクセルのダミーテクスチャ(WebGLTexture)を取得します。
	 * まだ生成されていない場合は新規作成します。テクスチャ未指定時の代替として利用されます。
	 * @returns {WebGLTexture} ダミーテクスチャのWebGLTextureオブジェクト
	 */
	_getDummyTexture() {
		if (this._textureDummyData === undefined) {
			const canvas = document.createElement("canvas");
			canvas.width = 1;
			canvas.height = 1;
			const context = canvas.getContext("2d");
			const imagedata = context.getImageData(0, 0, canvas.width, canvas.height);
			this._textureDummyId = this._createID();
			this._textureDummyData = this.glfunc.createTexture(this._textureDummyId, imagedata);
		}
		return this._textureDummyData;
	}

	/**
	 * 深度バッファのテストモードをWebGLで有効化します。
	 * 通常は自動的に呼ばれます。
	 * @returns {boolean} 成功時true
	 */
	_setDepthMode() {
		if (this.gl === null) {
			return false;
		}
		const gl = this.gl;
		gl.enable(gl.DEPTH_TEST);
		gl.depthFunc(gl.LEQUAL);
		return true;
	}

	/**
	 * WebGLのカリングモード(描画面の制御)を設定します。
	 * カリングの有無・前面/背面/両面の設定も行います。
	 * @returns {boolean} 成功時true
	 */
	_setCullMode() {
		if (this.gl === null) {
			return false;
		}
		const gl = this.gl;
		if (this.cullmode === S3System.CULL_MODE.NONE) {
			gl.disable(gl.CULL_FACE);
			return true;
		} else {
			gl.enable(gl.CULL_FACE);
		}
		if (this.frontface === S3System.FRONT_FACE.CLOCKWISE) {
			gl.frontFace(gl.CW);
		} else {
			gl.frontFace(gl.CCW);
		}
		if (this.cullmode === S3System.CULL_MODE.FRONT_AND_BACK) {
			gl.cullFace(gl.FRONT_AND_BACK);
		} else if (this.cullmode === S3System.CULL_MODE.BACK) {
			gl.cullFace(gl.BACK);
		} else if (this.cullmode === S3System.CULL_MODE.FRONT) {
			gl.cullFace(gl.FRONT);
		}
		return true;
	}

	/**
	 * 描画前処理として、アクティブなテクスチャIDをリセットします。
	 * 通常は内部的に呼ばれます。
	 */
	_bindStart() {
		this.program.resetActiveTextureId();
	}

	/**
	 * 描画後処理として、バインド状態の解放やクリーンアップを行います。
	 * (本実装では何もしていません。拡張用)
	 */
	_bindEnd() {}

	/**
	 * モデル・uniforms・名前と値を与えた場合のデータバインド処理を実行します。
	 * - 2引数: シェーダ変数名とデータをバインド
	 * - 1引数: S3GLModelならメッシュ情報をバインド
	 * - 1引数: uniforms情報ならすべてのuniformsをバインド
	 *
	 * @param {...any} args バインド対象
	 * @returns {number} 0以上は成功、モデルの場合はIBOインデックス数(モデルの場合)
	 */
	_bind() {
		if (!this.is_set) {
			return -1;
		}
		const prg = this.program;
		let index_lenght = 0;
		// p1が文字列、p2がデータの場合、データとして結びつける
		if (arguments.length === 2 && typeof arguments[0] === "string") {
			if (!prg.bindData(arguments[0], arguments[1])) {
				return -1;
			}
		}
		// 引数がモデルであれば、モデルとして紐づける
		else if (arguments.length === 1 && arguments[0] instanceof S3GLModel) {
			const mesh = arguments[0].getMesh();
			if (mesh instanceof S3GLMesh) {
				index_lenght = prg.bindMesh(mesh);
			}
		}
		// uniformsデータであれば、内部のデータを全て割り当てる
		else if (arguments.length === 1 && arguments[0].uniforms) {
			const uniforms = arguments[0].uniforms;
			for (const key in uniforms) {
				if (!prg.bindData(key, uniforms[key])) {
					return -1;
				}
			}
		}
		return index_lenght;
	}

	/**
	 * シーン全体を描画します。
	 * プログラム設定や深度・カリングモードの設定、各種Uniformやモデルバインド・描画を自動実行します。
	 * @param {S3GLScene} scene 描画対象のシーン
	 * @returns {void}
	 */
	drawScene(scene) {
		// プログラムを再設定
		this.setProgram(this.program);

		// まだ設定できていない場合は、この先へいかせない
		if (!this.is_set) {
			return;
		}

		// 画面の初期化
		this._setDepthMode();
		this._setCullMode();

		// 描写開始
		this._bindStart();

		// Sceneに関するUniform設定(カメラやライト設定など)
		this._bind(scene.getUniforms());

		// カメラの行列を取得する
		const VPS = scene.getCamera().getVPSMatrix(this.canvas);

		// モデル描写
		const models = scene.getModels();
		for (let i = 0; i < models.length; i++) {
			const model = models[i];
			const mesh = model.getMesh();
			if (mesh.isComplete() === false) {
				continue;
			}

			// モデルに関するUniform設定(材質の設定など)
			this._bind(model.getUniforms());

			// モデル用のBIND
			const M = this.getMatrixWorldTransform(model);
			const MV = this.mulMatrix(M, VPS.LookAt);
			const MVP = this.mulMatrix(MV, VPS.PerspectiveFov);
			this._bind("matrixWorldToLocal4", M.inverse4());
			this._bind("matrixLocalToWorld4", M);
			this._bind("matrixLocalToWorld3", M);
			this._bind("matrixLocalToPerspective4", MVP);

			const indexsize = this._bind(model);
			if (indexsize) {
				this.drawElements(indexsize);
			}
		}

		// 描写終了
		this._bindEnd();
	}

	/**
	 * 不要になったリソースを解放します(未実装)。
	 * @param {Object} obj 解放対象のオブジェクト
	 * @returns {void}
	 */
	_disposeObject(obj) {}

	/**
	 * GL用の頂点インスタンス(S3GLVertex)を生成します。
	 * @param {S3Vector} position 頂点座標
	 * @returns {S3GLVertex} 生成されたGL用頂点
	 */
	createVertex(position) {
		return new S3GLVertex(position);
	}

	/**
	 * GL用の三角形インデックスインスタンスを生成します。
	 * @param {number} i1 頂点1のインデックス
	 * @param {number} i2 頂点2のインデックス
	 * @param {number} i3 頂点3のインデックス
	 * @param {Array<number>} indexlist 頂点インデックス配列
	 * @param {number} [materialIndex] マテリアルインデックス
	 * @param {Array<S3Vector>} [uvlist] UV座標配列
	 * @returns {S3GLTriangleIndex} 生成されたGL用三角形インデックス
	 */
	createTriangleIndex(i1, i2, i3, indexlist, materialIndex, uvlist) {
		return new S3GLTriangleIndex(i1, i2, i3, indexlist, materialIndex, uvlist);
	}

	/**
	 * GL用のテクスチャインスタンスを生成します。
	 * @param {string|ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [name] テクスチャ名や画像データ
	 * @returns {S3GLTexture} 生成されたGL用テクスチャ
	 */
	createTexture(name) {
		return new S3GLTexture(this, name);
	}

	/**
	 * GL用のシーンインスタンスを生成します。
	 * @returns {S3GLScene} 生成されたGL用シーン
	 */
	createScene() {
		return new S3GLScene();
	}

	/**
	 * GL用のモデルインスタンスを生成します。
	 * @returns {S3GLModel} 生成されたGL用モデル
	 */
	createModel() {
		return new S3GLModel();
	}

	/**
	 * GL用のメッシュインスタンスを生成します。
	 * @returns {S3GLMesh} 生成されたGL用メッシュ
	 */
	createMesh() {
		return new S3GLMesh(this);
	}

	/**
	 * GL用のマテリアルインスタンスを生成します。
	 * @param {string} [name] マテリアル名
	 * @returns {S3GLMaterial} 生成されたGL用マテリアル
	 */
	createMaterial(name) {
		return new S3GLMaterial(this, name);
	}

	/**
	 * GL用のライトインスタンスを生成します。
	 * @returns {S3GLLight} 生成されたGL用ライト
	 */
	createLight() {
		return new S3GLLight();
	}

	/**
	 * GL用のカメラインスタンスを生成します。
	 * @returns {S3Camera} 生成されたGL用カメラ
	 */
	createCamera() {
		const camera = new S3Camera(/** @type {S3System} */ (/** @type {unknown} */ (this)));
		return camera;
	}
}