2020年08月08日 - WebVR・Three.js
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のパーティクルの練習なので、パソコンとスマホで見ることができるようにしました。


