2020年07月29日

Three.jsで星空を制作

BufferGeometryで頂点アニメーション」で頂点アニメーションを試しましたが、パーティクルの練習にThree.jsで星空を制作しました。※Three.jsはr118を使用しています。

Three.jsで星空を制作

● 頂点を生成

BufferGeometryを使用して頂点を生成し、球状に配置します。頂点を球状に配置するには、極座標を直交座標に変換する下記数式を使用します。

x = r sin θ cos φ
y = r sin θ sin φ
z = r cos θ

※θ(シータ), φ(ファイ)

BufferGeometryについては、「Three.jsのBufferGeometry」を参考にしてください。マテリアルは、動作確認用にPointsMaterialを使用します。

//半径
const r = 50;

//頂点数
const starsNum = 30000;

//バッファーオブジェクトの生成
const geometry = new THREE.BufferGeometry();

//型付配列で頂点座標を設定
const positions = new Float32Array(starsNum * 3);

//球状に配置する頂点座標を設定
for(let i = 0; i < starsNum; i++){
	const theta = Math.PI * Math.random();
	const phi = Math.PI * Math.random() * 2;

	positions[i * 3] = r * Math.sin(theta) * Math.cos(phi);
	positions[i * 3 + 1] = r * Math.sin(theta) * Math.sin(phi);
	positions[i * 3 + 2] = r * Math.cos(theta);
}

//バッファーオブジェクトのattributeに頂点座標を設定
geometry.setAttribute('position',new THREE.BufferAttribute(positions,3));

const material = new THREE.PointsMaterial({
	size:0.3
});

const points = new THREE.Points(geometry,material);
scene.add(points);

● 頂点アニメーション

シェーダを使用して、星が瞬くような頂点アニメーションを制作します。シェーダについては、「Three.jsでシェーダ(GLSL)入門」を参考にしてください。

マテリアルは「RawShaderMaterial」に変更します。

//バーテックスシェーダ
const vertexShader =`
	precision mediump float;

	uniform mat4 modelViewMatrix;
	uniform mat4 projectionMatrix;

	attribute vec3 position;
	attribute vec3 customColor;
	attribute float size;

	varying vec3 vColor;

	void main(){

		//視点座標系における頂点座標を算出
		vec4 mvPosition = modelViewMatrix * vec4(position,1.0);

		//頂点サイズを算出
		gl_PointSize = size * (1.0 / length(mvPosition.xyz));

		gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
		vColor = customColor;
	}
`;

//フラグメントシェーダ
const fragmentShader =`
	precision mediump float;

	uniform sampler2D texture;
	varying vec3 vColor;

	void main(){

		//頂点にテクスチャを設定
		vec4 texcel = texture2D(texture,gl_PointCoord);

		//頂点色とテクスチャを積算して、描画色を設定
		gl_FragColor = vec4(vColor,1.0) * texcel;
	}
`;

let points;

const r = 50;
const starsNum = 30000;

const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(starsNum * 3);

//型付配列で頂点カラーを設定
const colors = new Float32Array(starsNum * 3);

//型付配列で頂点サイズを設定
const sizes = new Float32Array(starsNum);

for(let i = 0; i < starsNum; i++){
	const theta = Math.PI * Math.random();
	const phi = Math.PI * Math.random() * 2;

	positions[i * 3] = r * Math.sin(theta) * Math.cos(phi);
	positions[i * 3 + 1] = r * Math.sin(theta) * Math.sin(phi);
	positions[i * 3 + 2] = r * Math.cos(theta);

	//頂点カラーを設定
	colors[i * 3] = 1.0;
	colors[i * 3 + 1] = 1.0;
	colors[i * 3 + 2] = 1.0;

	//頂点サイズを設定
	sizes[i] = 300;
}

geometry.setAttribute('position',new THREE.BufferAttribute(positions,3));

//バッファーオブジェクトのattributeに頂点カラーを設定
geometry.setAttribute('customColor',new THREE.BufferAttribute(colors,3));

//バッファーオブジェクトのattributeに頂点サイズを設定
geometry.setAttribute('size',new THREE.BufferAttribute(sizes,1));

//テクスチャ画像を転送
const uniforms = {
	texture:{type:'t',value:new THREE.TextureLoader().load('./img/star.png')}
};

//RawShaderMaterial
const material = new THREE.RawShaderMaterial({
	uniforms:uniforms,
	vertexShader:vertexShader,
	fragmentShader:fragmentShader,
	transparent:true,
	blending:THREE.AdditiveBlending,
	depthTest:false
});

points = new THREE.Points(geometry,material);
scene.add(points);

let step = 0;

function rendering(){
	requestAnimationFrame(rendering);

	step ++;

	//頂点サイズを更新
	const sizes = points.geometry.attributes.size;
	for(let i = 0; i < sizes.array.length; i++){
		sizes.array[i] = 300 * (1 + Math.sin(0.1 * i + step * 0.025));
	}

	//更新を通知するフラグ
	sizes.needsUpdate = true;

	renderer.render(scene,camera);
}

● ヒッパルコス星表のCSVの読み込み

このままでも星空のように見えますが、「WebGLで宇宙をつくる」を参考に「ヒッパルコス星表」を使用して、星空のリアリティを向上させます。

ダウンロードしたヒッパルコス星表のCSVを読み込んで、配列に変換します。ヒッパルコス星表の基礎データは2つに分かれているため、1つにまとめ、恒星色を設定するための星座線恒星データも読み込ます。

//表示する視聴級の設定
const starGrade = 8.0;
let hipColor,hipA,hipB,hipArray;
let starsNum;

//星座線恒星データの読み込み
getCsv('./data/hip_constellation_line_star.csv');

//CSVの読み込みと配列への変換関数
function getCsv(url){

	//CSVの読み込み
	const xhr = new XMLHttpRequest();
	xhr.open('get',url,true);
	xhr.send();

	//CSVの読み込み完了時の処理
	xhr.onload = function(){

		//CSVを配列に変換
		const array  = xhr.responseText.split('\n');
		const res = [];

		for(let i = 0; i < array.length; i++){

			if(array[i] == '') break;

			res[i] = array[i].split(',');

			for(let j = 0; j < res[i].length; j++){
				if(res[i][j].match(/\-?\d+(.\d+)?(e[\+\-]d+)?/)){
					res[i][j] = parseFloat(res[i][j].replace('"',''));
				}
			}
		}

		switch(url){
			case('./data/hip_constellation_line_star.csv'):

				//星座線恒星データ
				hipColor = res;

				//基礎データAの読み込み
				getCsv('./data/hip_lite_a.csv');
			break;

			case('./data/hip_lite_a.csv'):

				//基礎データA
				hipA = res;

				//基礎データBの読み込み
				getCsv('./data/hip_lite_b.csv');
			break;

			case('./data/hip_lite_b.csv'):

				//基礎データB
				hipB = res;

				//基礎データA、基礎データBを1つに結合
				hipArray = hipA.concat(hipB);

				//星数のカウント
				starsNum = 0;
				for(let i = 0; i < hipArray.length; i++){
					if(hipArray[i][8] < starGrade){
						starsNum++;
					}
				}
			break;
		}
	}
}

● 星の座標とサイズ、恒星の色の反映

ヒッパルコス星表のCSVから読み込んだ星の情報を反映します。

let starSizesArray = [];

let j = 0;
for(let i = 0; i < hipArray.length; i++){

	if(hipArray[i][8] < starGrade){
		//星の座標を設定
		const a = (hipArray[i][1] + (hipArray[i][2] + hipArray[i][3] / 60) / 60) * 15 * Math.PI / 180;
		const f = (hipArray[i][4] == 0) ? -1 : 1;
		const c = f * (hipArray[i][5] + (hipArray[i][6] + hipArray[i][7] / 60) / 60) * Math.PI / 180;

		positions[j * 3] = r * Math.cos(a) * Math.cos(c);
		positions[j * 3 + 1] = r * Math.sin(a) * Math.cos(c);
		positions[j * 3 + 2] = r * Math.sin(c);

		//星のサイズを設定
		let size = 1 / hipArray[i][8] * 20;
		if(10 < size) size = 10;
		if(hipArray[i][8] < 0) size = 10;

		sizes[j] = size * 55;
		starSizesArray.push(sizes[j]);

		//恒星色を設定
		colors[j * 3] = Math.random() * 0.1 + 0.9;;
		colors[j * 3 + 1] = Math.random() * 0.1 + 0.9;;
		colors[j * 3 + 2] = Math.random() * 0.1 + 0.9;;

		setStarsColor(hipArray[i],j);

		j++;
	}
}

//恒星色を設定する関数
function setStarsColor(array,j){
	for(let i = 0; i < hipColor.length; i++){

		if(array[0] == hipColor[i][0]){
			const bv = hipColor[i][11];
			const t = 9000 / (bv + 0.85);

			let c_x,c_y;

			if(1667 <= t && t <= 4000){
				c_x = -0.2661239 * Math.pow(10,9) / Math.pow(t,3) - 0.2343580 * Math.pow(10,6) / Math.pow(t,2) + 0.8776956 * Math.pow(10,3) / t + 0.179910;
			}else if(4000 < t && t <= 25000){
				c_x = -3.0258469 * Math.pow(10,9) / Math.pow(t,3) + 2.1070379 * Math.pow(10,6) / Math.pow(t,2) + 0.2226347 * Math.pow(10,3) / t + 0.240390;
			}

			if(1667 <= t && t <= 2222){
				c_y = -1.1063814 * Math.pow(c_x,3) - 1.34811020 * Math.pow(c_x,2) + 2.18555832 * c_x - 0.20219683;
			}else if(2222 < t && t <= 4000){
				c_y = -0.9549476 * Math.pow(c_x,3) - 1.37418593 * Math.pow(c_x,2) + 2.09137015 * c_x - 0.16748867;
			}else if(4000 < t && t <=25000){
				c_y = 3.0817580 * Math.pow(c_x,3) - 5.87338670 * Math.pow(c_x,2) + 3.75112997 * c_x - 0.37001483;
			}

			const y = 1.0;
			const x = (y / c_y) * c_x;
			const z = (y / c_y) * (1 - c_x - c_y);

			let r = (3.240970 * x) - (1.537383 * y) - (0.498611 * z);
			let g = (-0.969244 * x) + (1.875968 * y) + (0.041555 * z);
			let b = (0.055630 * x) + (0.203977 * y) + (1.056972 * z);

			colors[j * 3] = r;
			colors[j * 3 + 1] = g;
			colors[j * 3 + 2] = b;
		}
	}
}

let step = 0;

function rendering(){
	requestAnimationFrame(rendering);

	if(orbitControls){
		orbitControls.update();
	}

	step ++;

	const sizes = points.geometry.attributes.size;
	for(let i = 0; i < sizes.array.length; i++){

		//星のサイズを反映するため、starSizesArray[i]に変更
		sizes.array[i] = starSizesArray[i] * (1 + Math.sin(0.1 * i + step * 0.025));
	}
	sizes.needsUpdate = true;

	renderer.render(scene,camera);
}

● script.js

完成したscript.jsになります。

//===============================================================
// Import Library
//===============================================================
import * as THREE from './lib/three_jsm/three.module.js';
import { OrbitControls } from './lib/three_jsm/OrbitControls.js';
import { scene, camera, renderer } from './lib/basescene.js';

//===============================================================
// Init
//===============================================================
window.addEventListener('load',function(){
   init();
});

let orbitControls;

const starGrade = 8.0;
let hipColor,hipA,hipB,hipArray;
let starsNum,starSizesArray;
let points;

function init(){
	setLoading();
}

function setLoading(){
	TweenMax.to('.loader',0.1,{opacity:1});

	getCsv('./data/hip_constellation_line_star.csv');

	function getCsv(url){
		const xhr = new XMLHttpRequest();
		xhr.open('get',url,true);
		xhr.send();

		xhr.onload = function(){
			const array  = xhr.responseText.split('\n');
			const res = [];

			for(let i = 0; i < array.length; i++){

				if(array[i] == '') break;

				res[i] = array[i].split(',');

				for(let j = 0; j < res[i].length; j++){
					if(res[i][j].match(/\-?\d+(.\d+)?(e[\+\-]d+)?/)){
						res[i][j] = parseFloat(res[i][j].replace('"',''));
					}
				}
			}

			switch(url){
				case('./data/hip_constellation_line_star.csv'):
					hipColor = res;
					getCsv('./data/hip_lite_a.csv');
				break;

				case('./data/hip_lite_a.csv'):
					hipA = res;
					getCsv('./data/hip_lite_b.csv');
				break;

				case('./data/hip_lite_b.csv'):
					hipB = res;
					hipArray = hipA.concat(hipB);

					starsNum = 0;
					for(let i = 0; i < hipArray.length; i++){
						if(hipArray[i][8] < starGrade){
							starsNum++;
						}
					}

					threeWorld();
					setLight();
					setControll();
					rendering();

					TweenMax.to('#loader_wrapper',1,{
				        opacity:0,
				        delay:1,
				        onComplete: function(){
				            document.getElementById('loader_wrapper').style.display = 'none';
				            TweenMax.to('.loader',0,{opacity:0});
				        }
    				});
				break;
			}
		}
	}
}

//===============================================================
// Create World
//===============================================================
function threeWorld(){
	const gridHelper = new THREE.GridHelper(100,100);
	gridHelper.position.y = -0.5;
	scene.add(gridHelper);

	const vertexShader =`
		precision mediump float;

		uniform mat4 modelViewMatrix;
		uniform mat4 projectionMatrix;

		attribute vec3 position;
		attribute vec3 customColor;
		attribute float size;

		varying vec3 vColor;

		void main(){
			vec4 mvPosition = modelViewMatrix * vec4(position,1.0);
			gl_PointSize = size * (1.0 / length(mvPosition.xyz));
			gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
			vColor = customColor;
		}
	`;

	const fragmentShader =`
		precision mediump float;

		uniform sampler2D texture;
		varying vec3 vColor;

		void main(){
			vec4 texcel = texture2D(texture,gl_PointCoord);
			gl_FragColor = vec4(vColor,1.0) * texcel;
		}
	`;

	const r = 50;
	starSizesArray = [];

	const geometry = new THREE.BufferGeometry();
	const positions = new Float32Array(starsNum * 3);
	const colors = new Float32Array(starsNum * 3);
	const sizes = new Float32Array(starsNum);

	let j = 0;
	for(let i = 0; i < hipArray.length; i++){

		if(hipArray[i][8] < starGrade){
			const a = (hipArray[i][1] + (hipArray[i][2] + hipArray[i][3] / 60) / 60) * 15 * Math.PI / 180;
			const f = (hipArray[i][4] == 0) ? -1 : 1;
			const c = f * (hipArray[i][5] + (hipArray[i][6] + hipArray[i][7] / 60) / 60) * Math.PI / 180;

			positions[j * 3] = r * Math.cos(a) * Math.cos(c);
			positions[j * 3 + 1] = r * Math.sin(a) * Math.cos(c);
			positions[j * 3 + 2] = r * Math.sin(c);

			let size = 1 / hipArray[i][8] * 20;
			if(10 < size) size = 10;
			if(hipArray[i][8] < 0) size = 10;

			sizes[j] = size * 55;
			starSizesArray.push(sizes[j]);

			colors[j * 3] = Math.random() * 0.1 + 0.9;;
			colors[j * 3 + 1] = Math.random() * 0.1 + 0.9;;
			colors[j * 3 + 2] = Math.random() * 0.1 + 0.9;;

			setStarsColor(hipArray[i],j);

			j++;
		}
	}

	function setStarsColor(array,j){
		for(let i = 0; i < hipColor.length; i++){

			if(array[0] == hipColor[i][0]){
				const bv = hipColor[i][11];
				const t = 9000 / (bv + 0.85);

				let c_x,c_y;

				if(1667 <= t && t <= 4000){
					c_x = -0.2661239 * Math.pow(10,9) / Math.pow(t,3) - 0.2343580 * Math.pow(10,6) / Math.pow(t,2) + 0.8776956 * Math.pow(10,3) / t + 0.179910;
				}else if(4000 < t && t <= 25000){
					c_x = -3.0258469 * Math.pow(10,9) / Math.pow(t,3) + 2.1070379 * Math.pow(10,6) / Math.pow(t,2) + 0.2226347 * Math.pow(10,3) / t + 0.240390;
				}

				if(1667 <= t && t <= 2222){
					c_y = -1.1063814 * Math.pow(c_x,3) - 1.34811020 * Math.pow(c_x,2) + 2.18555832 * c_x - 0.20219683;
				}else if(2222 < t && t <= 4000){
					c_y = -0.9549476 * Math.pow(c_x,3) - 1.37418593 * Math.pow(c_x,2) + 2.09137015 * c_x - 0.16748867;
				}else if(4000 < t && t <=25000){
					c_y = 3.0817580 * Math.pow(c_x,3) - 5.87338670 * Math.pow(c_x,2) + 3.75112997 * c_x - 0.37001483;
				}

				const y = 1.0;
				const x = (y / c_y) * c_x;
				const z = (y / c_y) * (1 - c_x - c_y);

				let r = (3.240970 * x) - (1.537383 * y) - (0.498611 * z);
				let g = (-0.969244 * x) + (1.875968 * y) + (0.041555 * z);
				let b = (0.055630 * x) + (0.203977 * y) + (1.056972 * z);

				colors[j * 3] = r;
				colors[j * 3 + 1] = g;
				colors[j * 3 + 2] = b;
			}
		}
	}

	geometry.setAttribute('position',new THREE.BufferAttribute(positions,3));
	geometry.setAttribute('customColor',new THREE.BufferAttribute(colors,3));
	geometry.setAttribute('size',new THREE.BufferAttribute(sizes,1));

	const uniforms = {
		texture:{type:'t',value:new THREE.TextureLoader().load('./img/star.png')}
	};

	const material = new THREE.RawShaderMaterial({
		uniforms:uniforms,
		vertexShader:vertexShader,
		fragmentShader:fragmentShader,
		transparent:true,
		blending:THREE.AdditiveBlending,
		depthTest:false
	});

	points = new THREE.Points(geometry,material);
	scene.add(points);
}

function setLight(){
	const ambientlight = new THREE.AmbientLight(0x333333);
	scene.add(ambientlight);
}

function setControll(){
	document.addEventListener('touchmove',function(e){e.preventDefault();},{passive:false});
	orbitControls = new OrbitControls(camera,renderer.domElement);
	orbitControls.enableDamping = true;
	orbitControls.dampingFactor = 0.5;
}

let step = 0;

function rendering(){
	requestAnimationFrame(rendering);

	if(orbitControls){
		orbitControls.update();
	}

	step ++;

	const sizes = points.geometry.attributes.size;
	for(let i = 0; i < sizes.array.length; i++){
		sizes.array[i] = starSizesArray[i] * (1 + Math.sin(0.1 * i + step * 0.025));
	}
	sizes.needsUpdate = true;

	points.rotation.y = -step / 60 * 0.001;

	renderer.render(scene,camera);
}

完成したデモになります。ヒッパルコス星表を使用しすることで、リアリティのある綺麗な星空を制作することができます。Three.jsのパーティクルの練習なので、パソコンとスマホで見ることができるようにしました。

  • このエントリーをはてなブックマークに追加

関連記事

前の記事へ

立方体にシェーダを設定