見出し画像

【three.js WebGL】3次元グラフの作り方

前回に開発した2次元カラーマップは2次元平面を構成する格子点に任意の色を設定することで物理量などを可視化することができるね。今回2次元平面の垂直成分を与えることで、3次元的な可視化を行うことができるね。次のアニメーションは前回と同様、次式で与えた値をに対応する描画色と垂直成分を与えているよ。

$$
F(x,y,t) = \sin(\frac{x}{2}+\omega t) \times \sin(\frac{y}{2}+\omega t)
$$

下図のようにワイヤーフレーム表示もできるよ。

上記の色指定方法はHSLだけれども、もちろんRGBで指定することもできるね。

Javascript プログラムソース

Javascriptプログラムソースを用意したよ。もしよかったら試してみてください。これからも応援よろしくお願いしまーす。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>3次元グラフのテスト</title>
<style>
*{padding:0px; margin:0px}
div#canvas-frame{
	width: 1200px;  /* 横幅 */
	height: 800px; /* 縦幅 */
	overflow:hidden;
}
</style>
<script type="importmap">
	{
		"imports": {
			"three": "https://unpkg.com/three@0.168.0/build/three.module.js",
			"three/addons/": "https://unpkg.com/three@0.168.0/examples/jsm/"
		}
	}
</script>
<script type="module">
import * as THREE from 'three';
import { TrackballControls } from 'three/addons/controls/TrackballControls.js';
////////////////////////////////////////////////////////////////////
// windowイベントの定義
////////////////////////////////////////////////////////////////////
window.addEventListener("load", function () {
	threeStart(); //Three.jsのスタート関数の実行
});
////////////////////////////////////////////////////////////////////
// Three.jsスタート関数の定義
////////////////////////////////////////////////////////////////////
function threeStart() {
	initThree();  //Three.js初期化関数の実行
	initLight();  //光源初期化関数の実行
	initObject(); //オブジェクト初期化関数の実行
	initCamera(); //カメラ初期化関数の実行
	loop();       //無限ループ関数の実行
}
////////////////////////////////////////////////////////////////////
// Three.js初期化関数の定義
////////////////////////////////////////////////////////////////////
//グローバル変数の宣言
let renderer,    //レンダラーオブジェクト
    scene,       //シーンオブジェクト
    canvasFrame; //キャンバスフレームのDOM要素
function initThree() {
	//キャンバスフレームDOM要素の取得
	canvasFrame = document.getElementById('canvas-frame');
	//レンダラーオブジェクトの生成
	renderer = new THREE.WebGLRenderer({ antialias: true });

	if (!renderer) alert('Three.js の初期化に失敗しました');
	//レンダラーのサイズの設定
	renderer.setSize(canvasFrame.clientWidth, canvasFrame.clientHeight);
	//キャンバスフレームDOM要素にcanvas要素を追加
	canvasFrame.appendChild(renderer.domElement);

	//レンダラークリアーカラーの設定
	renderer.setClearColor(0x000000, 1.0);

	//シーンオブジェクトの生成
	scene = new THREE.Scene();
}
////////////////////////////////////////////////////////////////////
// カメラ初期化関数の定義
////////////////////////////////////////////////////////////////////
//グローバル変数の宣言
let camera;    //カメラオブジェクト
let trackball;
function initCamera() {

	//カメラオブジェクトの生成
	camera = new THREE.PerspectiveCamera(30, canvasFrame.clientWidth / canvasFrame.clientHeight, 1, 10000);
	//カメラの位置の設定
	camera.position.set(10, -25, 25);
	//カメラの上ベクトルの設定
	camera.up.set(0, 0, 1);
	//カメラの中心位置ベクトルの設定
	camera.lookAt(new THREE.Vector3(0,0,0)); //トラックボール利用時は自動的に無効

	//トラックボールオブジェクトの宣言
	trackball = new TrackballControls(camera, canvasFrame);

	//トラックボール動作範囲のサイズとオフセットの設定
	trackball.screen.width = canvasFrame.clientWidth;                        //横幅
	trackball.screen.height = canvasFrame.clientHeight;                      //縦幅
	trackball.screen.offsetLeft = canvasFrame.getBoundingClientRect().left;  //左オフセット
	trackball.screen.offsetTop = canvasFrame.getBoundingClientRect().top;    //右オフセット

	//トラックボールの回転無効化と回転速度の設定
	trackball.noRotate = false;
	trackball.rotateSpeed = 2.0;

	//トラックボールの拡大無効化と拡大速度の設定
	trackball.noZoom = false;
	trackball.zoomSpeed = 4.0;

	//トラックボールのカメラ中心移動の無効化と中心速度の設定
	trackball.noPan = false;
	trackball.panSpeed = 1.0;
	trackball.target = new THREE.Vector3(0, 0, 0);

	//トラックボールのスタティックムーブの有効化
	trackball.staticMoving = true;
	//トラックボールのダイナミックムーブ時の減衰定数
	trackball.dynamicDampingFactor = 0.3;

}
////////////////////////////////////////////////////////////////////
// 光源初期化関数の定義
////////////////////////////////////////////////////////////////////
//グローバル変数の宣言
let directionalLight,  //平行光源オブジェクト
    ambientLight;      //環境光オブジェクト
function initLight() {
	//平行光源オブジェクトの生成
	directionalLight = new THREE.DirectionalLight(0xffffff, 2.0, 0);
	//平行光源オブジェクトの位置の設定
	directionalLight.position.set(0, -50, 50);
	//平行光源オブジェクトのシーンへの追加
	scene.add(directionalLight);

	//環境光オブジェクトの生成
	ambientLight = new THREE.AmbientLight(0x666666);
	//環境光オブジェクトのシーンへの追加
	scene.add(ambientLight);

}
////////////////////////////////////////////////////////////////////
// オブジェクト初期化関数の定義
////////////////////////////////////////////////////////////////////
let graph3D; //3次元グラフオブジェクト
let xmin = -16;
let xmax = 16;
let ymin = -12;
let ymax = 12;
let Nx = 120;
let Ny = 100;
let dx = (xmax - xmin) / Nx
let dy = (ymax - ymin) / Ny

function F( x, y, t ){
	let omega = Math.PI/60;
	return Math.sin( x/2 + t*omega ) * Math.sin( y/2 + t*omega ) ;
}
function initObject() {
	let values = []
	for (let i = 0; i <= Nx; i++) {
		values[ i ] = [];
		for (let j = 0; j <= Ny; j++) {
			let x = xmin + (xmax - xmin) * i / Nx;
			let y = ymin + (ymax - ymin) * j / Ny;
			values[ i ][ j ] = F( x, y, 0 );
		}
	}

	//3次元グラフオブジェクトの生成
	graph3D = new Graph3D( {
		Nx : Nx,   //頂点の数
		Ny : Ny,   //頂点の数
		lx : dx,   //格子間隔
		ly : dy,   //格子間隔
		min : -1,          //最小値
		max : 1,           //最大値
		wireframe : false, //ワイヤーフレーム表示
		opacity : 1.0,     //不透明度
		doubleSide : true, //両面描画
		hightScale : 3.0,    //高さのスケール
		colorParameter : {
			backWhite : false, //RGBモード時の0点を白とするフラグ
			HSLMode : true,   //HSLモード実行のフラグ
			H0 : 0.70,         //Hの始点(HSLモード時)
			Hw : 0.73,         //Hの変化の傾き(HSLモード時)
			S : 1,             //Sの値(HSLモード時)
			L : 0.5            //Lの値(HSLモード時)
		},
		fieldValues :values
	});

	//カラーマップオブジェクトのシーンへの追加
	scene.add( graph3D.CG );
}
////////////////////////////////////////////////////////////////////
// 無限ループ関数の定義
////////////////////////////////////////////////////////////////////
//グローバル変数の宣言
let step = 0; //ステップ数
let recorder;
function loop() {
	//トラックボールによるカメラオブジェクトのプロパティの更新
	trackball.update();

	//ステップ数のインクリメント
	let t = step;
	let Nx = graph3D.Nx;
	let Ny = graph3D.Ny;
	for (let i = 0; i <= Nx; i++) {
		for (let j = 0; j <= Ny; j++) {
			let x = xmin + (xmax - xmin) * i / Nx;
			let y = ymin + (ymax - ymin) * j / Ny;
			graph3D.setValue( i, j, F( x, y, t ));
		}
	}
	graph3D.update();

	//レンダリング
	renderer.render(scene, camera);
	
	//ステップ数のインクリメント
	step++;
	//「loop()」関数の呼び出し
	requestAnimationFrame(loop);


}

////////////////////////////////////////////////////////////////////
// 2次元カラーマップクラスの定義
////////////////////////////////////////////////////////////////////
class Colormap2D {
	constructor( parameter ){
		parameter = parameter || {};
		//格子点数
		this.Nx = parameter.Nx || 120;
		this.Ny = parameter.Ny || 100;
		//格子間隔
		this.lx = parameter.lx || 0.01;
		this.ly = parameter.ly || 0.01;
		//場の量の最小値と最大値
		this.min = (parameter.min !== undefined)? parameter.min : 0;   //最小値
		this.max = (parameter.max !== undefined)? parameter.max : 1;   //最大値
		//色関連パラメータ
		let p = parameter.colorParameter || {};
		this.colorParameter = {
			HSLMode : p.HSLMode || false,             //HSLモード実行のフラグ
			H0 : (p.H0 !== undefined )? p.H0 : 0.70,  //Hの始点(HSLモード時)
			Hw : (p.Hw !== undefined )? p.Hw : 0.73,  //Hの変化(HSLモード時)
			S : (p.S !== undefined )? p.S : 1,        //Sの値(HSLモード時)
			L : (p.L !== undefined )? p.L : 0.5,      //Lの値(HSLモード時)
			backWhite : p.backWhite || false          //中間値を白とするフラグ(RGBモード時)
		};
		//場の値(2重配列)
		this.fieldValues = parameter.fieldValues || [];
		//コンピュータ・グラフィックスオブジェクト
		this.CG = null;
		//カラーマップの初期化
		this.initColormap2D();
	}
	//2次元カラーマップを実現するために必要な初期設定を行う
	initColormap2D(){
		//形状オブジェクトの宣言
		let geometry = new THREE.BufferGeometry();
		//アトリビュート変数のサイズを指定
		let positions = []; //頂点座標
		let colors = [];    //頂点色
		//頂点座標の準備
		for (let i = 0; i <= this.Nx; i++) {
			for (let j = 0; j <= this.Ny; j++) {
				let nx = (-this.Nx / 2 + i) * this.lx;
				let ny = (-this.Ny / 2 + j) * this.ly;
				let nz = 0;
				//頂点座標データの追加
				positions.push( nx, ny, nz );
				colors.push(0, 0, 0)
			}
		}
		let indices = [];
		//面指定配列の準備
		for (let i = 0; i < this.Nx; i++) {
			for (let j = 0; j < this.Ny; j++) {
				//頂点番号
				let ii = (this.Ny + 1) * i + j;
				//面指定用頂点インデックスを追加
				indices.push( ii, ii + (this.Ny + 1), ii + (this.Ny + 1) + 1);
				//面指定用頂点インデックスを追加
				indices.push( ii, ii + (this.Ny + 1) + 1, ii + 1 );
			}
		}
		//アトリビュート変数に設定
		geometry.setIndex( indices );
		geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );
		geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ) );
		//材質オブジェクトの宣言と生成
		let material = new THREE.MeshBasicMaterial({ color:0xFFFFFF, vertexColors: true });
		//2次元格子オブジェクトの生成
		this.CG = new THREE.Mesh( geometry, material );
		//場の量の設定
		if( this.fieldValues.length > 0 ){
			//カラーマップの配色を更新
			this.update();
		} else {
			//2重配列の初期化
			for (let i = 0; i <= this.Nx; i++) {
				this.fieldValues[ i ] = [];
			}
		}
	}
	update( ){
		for (let i = 0; i <= this.Nx; i++) {
			for (let j = 0; j <= this.Ny; j++) {
				let c = this.getColor( i, j );
				//頂点番号
				let ii = (this.Ny + 1) * i + j;
				this.CG.geometry.attributes.color.array[3 * ii + 0] = c.r; 
				this.CG.geometry.attributes.color.array[3 * ii + 1] = c.g; 
				this.CG.geometry.attributes.color.array[3 * ii + 2] = c.b; 
			}
		}
		//頂点色データ更新
		this.CG.geometry.attributes.color.needsUpdate = true;
	}
	getColor ( i ,j ){
		//場の量の取得
		let f = this.fieldValues[i][j];
		//カラーマップ関連プロパティ
		let p = this.colorParameter;
		if( p.HSLMode ){
			let value;
			if (f < this.min) value = 0;
			else if (f > this.max) value = this.max - this.min;
			else value = f - this.min;
			let H = p.H0 - ( value / (this.max - this.min) ) * p.Hw;
			//L値の取得
			let L = p.L;
			//HSL形式で指定した色オブジェクトを返す
			return new THREE.Color( ).setHSL( H, p.S, L, THREE.SRGBColorSpace );
		} else {
			//描画範囲の中心値を取得
			let c = ( this.max + this.min ) / 2;
			//中心値よりも大きい場合
			let R = ( f > c )? ( f - c )/( this.max - c ) : 0;
			if( R > 1 ) R = 1;
			//中心値よりも小さい場合
			let B = ( f < c )? (c - f)/( c - this.min ) : 0;
			if( B > 1 ) B = 1;
			let G;
			//中心値の色を白色とする場合
			if( p.backWhite ){
				G = 1 - ( R + B );
				R = R + G;
				B = B + G;
			} else {
				G = 0;
			}
			return new THREE.Color().setRGB(R, G, B, THREE.SRGBColorSpace);
		}
	}
	setValue ( i , j, value ){
		this.fieldValues[i][j] = value || 0;
	}
}
////////////////////////////////////////////////////////////////////
// 3次元グラフ描画クラスの定義
////////////////////////////////////////////////////////////////////
class Graph3D extends Colormap2D{
	constructor( parameter ){
		//基底クラスのコンストラクタの実行
		super( parameter );		
		//ワイヤーフレーム描画の有無
		this.wireframe = parameter.wireframe || false;
		//不透明度
		this.opacity = parameter.opacity || 1;
		//両面描画の有無
		this.doubleSide = parameter.doubleSide || false;
		//描画色グラデーションの有無
		this.colorParameter.variable = ( parameter.colorParameter.variable !== undefined )? parameter.colorParameter.variable : true;
		//単一色描画の場合の描画色
		this.colorParameter.monocolor = parameter.colorParameter.monocolor || 0xFFFFFF;
		//3次元グラフのz成分の高さのスケール
		this.hightScale = parameter.hightScale || 1.0;
		//3次元グラフの初期化
		this.initGraph3D();
	}
	//3次元グラフオブジェクトの初期化
	initGraph3D( ){
		//単一色描画の場合のHSL値の設定
		if( !this.colorParameter.variable ){
			//HSL形式で指定するフラグを立てる
			this.colorParameter.HSLMode = true;
			//H値の変化の傾きを0とする
			this.colorParameter.Hw = 0;
			//単一描画色のHSL値の取得
			let hsl = new THREE.Color( 
				this.colorParameter.monocolor 
			).getHSL();
			//HSL値の指定
			this.colorParameter.H = hsl.h;
			this.colorParameter.S = hsl.s;
			this.colorParameter.L = hsl.l;
			//2次元格子オブジェクトの描画色の更新
			super.update();
		}
		//両面の描画の有無
		if( this.doubleSide ) this.CG.material.side = THREE.DoubleSide;
		//ワイヤーフレーム表示の有無
		this.CG.material.wireframe = this.wireframe;
		//半透明の場合
		if( this.opacity < 1 ) {
			//透明化
			this.CG.material.transparent = true;
			//不透明度を指定
			this.CG.material.opacity = this.opacity;
		}
		//3次元グラフオブジェクトの更新
		this.update();
	}
	//3次元グラフオブジェクトの更新
	update ( ){
		//頂点番号
		for (let i = 0; i <= this.Nx; i++) {
			for (let j = 0; j <= this.Ny; j++) {
				let c = this.getColor( i, j );
				//x方向とy方向の座標
				let nx = (-this.Nx / 2 + i) * this.lx;
				let ny = (-this.Ny / 2 + j) * this.ly;
				//場の量の取得
				let f = this.fieldValues[ i ][ j ];
				if( f > this.max ) f = this.max;
				if( f < this.min ) f = this.min;
				f = f - ( this.max + this.min ) / 2;
				let nz = f * this.hightScale;
				//頂点座標データの更新
				let ii = (this.Ny + 1) * i + j;
				this.CG.geometry.attributes.position.array[3 * ii + 0] = nx; 
				this.CG.geometry.attributes.position.array[3 * ii + 1] = ny; 
				this.CG.geometry.attributes.position.array[3 * ii + 2] = nz; 
			}
		}
		//座標点データ更新
		this.CG.geometry.attributes.position.needsUpdate = true;
		//半透明の場合
		if( this.opacity < 1 && !this.CG.material.transparent ) {
			//透明化
			this.CG.material.transparent = true;
			//不透明度を指定
			this.CG.material.opacity = this.opacity;
		} else if( this.opacity >= 1 && this.CG.material.transparent ){
			//透明化の解除
			this.CG.material.transparent = false;
			//不透明度を指定
			this.CG.material.opacity = 1;
		}
		//頂点色の更新
		if( this.colorParameter.variable ) super.update();
	}
}
</script>
</head>
<body>
	<div id="canvas-frame"></div><!-- canvas要素を配置するdiv要素 -->
</body>
</html>

この記事が気に入ったらサポートをしてみませんか?