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のパーティクルの練習なので、パソコンとスマホで見ることができるようにしました。