2021年05月22日 - WebVR・Three.js
平面から球体へモーフィングする頂点アニメーション
「EffectComposerでポストプロセッシング」に続き「points waves」を参考に、平面から球体へモーフィングする頂点アニメーションを試してみました。※Three.jsはr128を使用しています。
平面の頂点アニメーション
● 頂点の生成
まず、平面上に頂点を生成します。頂点座標と頂点の大きさをBufferGeometryに設定して、Pointsで頂点を生成します。
マテリアルは、RawShaderMaterialを設定します。
let time = 0; //平面の分割数 const separation = 100; const amountX = 50,amountY = 50; const particleNum = amountX * amountY; //頂点座標の型付配列 const positions = new Float32Array(particleNum * 3); //頂点の大きさの型付配列 const scales = new Float32Array(particleNum); let i = 0; for(let ix = 0; ix < amountX; ix++){ for(let iy = 0; iy < amountY; iy++){ //頂点座標の設定 positions[i * 3] = ix * separation - ((amountX * separation) / 2); positions[i * 3 + 1] = 0; positions[i * 3 + 2] = iy * separation - ((amountY * separation) / 2); //頂点の大きさの設定 scales[i] = 1; i ++; } } //バッファーオブジェクトを生成 const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position',new THREE.BufferAttribute(positions,3)); geometry.setAttribute('scale',new THREE.BufferAttribute(scales,1)); //RawShaderMaterial const material = new THREE.RawShaderMaterial({ vertexShader:vertexShader, fragmentShader:fragmentShader, }); //頂点の生成 particles = new THREE.Points(geometry,material); scene.add(particles);
● glsl.js
シェーダで頂点の形を円形にし、頂点の大きさを動作確認用に10.0にします。
const vertexShader =` attribute vec3 position; attribute float scale; uniform mat4 projectionMatrix; uniform mat4 modelViewMatrix; void main(void){ vec4 mvPosition = modelViewMatrix * vec4(position,1.0); //頂点の大きさ gl_PointSize = 10.0; gl_Position = projectionMatrix * mvPosition; } `; const fragmentShader =` precision highp float; void main(void){ //頂点を円形に設定 if(length(gl_PointCoord - vec2(0.5,0.5)) > 0.475){ discard; } gl_FragColor = vec4(1.0,1.0,1.0,1.0); } `; export { vertexShader, fragmentShader };
● 頂点のアニメーション
頂点をアニメーションさせます。
三角関数を使用して、頂点座標をY軸に波形上に、また頂点の大きさをアニメーションさせます。
function rendering(){ requestAnimationFrame(rendering); time ++; //頂点座標を取得 const positions = particles.geometry.attributes.position.array; //頂点の大きさを取得 const scales = particles.geometry.attributes.scale.array; let i = 0; for(let ix = 0; ix < amountX; ix++){ for(let iy = 0; iy < amountY; iy++){ //頂点座標をアニメーション positions[i * 3 + 1] = (Math.sin((ix + time * 0.1) * 0.3) * 50) + (Math.sin((iy + time * 0.1) * 0.5) * 50); //頂点の大きさをアニメーション scales[i] = (Math.sin((ix + time * 0.1) * 0.3) + 1) * 12.5 + (Math.sin((iy + time * 0.1) * 0.3) + 1) * 12.5; i ++; } } //更新を通知 particles.geometry.attributes.position.needsUpdate = true; particles.geometry.attributes.scale.needsUpdate = true; renderer.render(scene,camera); }
● シェーダの修正
シェーダを修正して、頂点の大きさをアニメーションさせます。
gl_PointSize = scale * (300.0 / -mvPosition.z);
球体の頂点アニメーション
平面の頂点と同じように、球体の頂点をアニメーションさせます。
BufferGeometryをSphereGeometryに変更して、アニメーション部分を変更します。SphereGeometryはr125からBufferGeometryを継承するようになったので、頂点座標と大きさをそのまま設定できます。
シェーダは変更ありません。
let time = 0; //SphereGeometry const geometry = new THREE.SphereGeometry(500,49,49); geometry.setAttribute('color',new THREE.BufferAttribute(colors,3)); geometry.setAttribute('scale',new THREE.BufferAttribute(scales,1)); const material = new THREE.RawShaderMaterial({ vertexShader:vertexShader, fragmentShader:fragmentShader, }); particles = new THREE.Points(geometry,material); scene.add(particles); function rendering(){ requestAnimationFrame(rendering); time ++; const positions = particles.geometry.attributes.position.array; const scales = particles.geometry.attributes.scale.array; let i = 0; for(let ix = 0; ix < amountX; ix++){ for(let iy = 0; iy < amountY; iy++){ //球体のアニメーション const p = new THREE.Vector3( positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2] ); p.normalize().multiplyScalar((Math.sin((ix + time * 0.1 * 0.8) * 0.3) * 10) + 500); positions[i * 3] = p.x; positions[i * 3 + 1] = p.y; positions[i * 3 + 2] = p.z; scales[i] = (Math.sin((ix + time * 0.1) * 0.3) + 1) * 12.5 + (Math.sin((iy + time * 0.1) * 0.3) + 1) * 12.5; i ++; } } particles.geometry.attributes.position.needsUpdate = true; particles.geometry.attributes.scale.needsUpdate = true; renderer.render(scene,camera); }
平面から球体へモーフィングする頂点アニメーション
モーフィングアニメーションは、平面と球体のバッファーオブジェクトを保持しておき、時間によって変数を切りかえて、保持しておいたバッファーオブジェクトからモーフィング後の頂点座標を取得してアニメーションさせます。
if(Math.floor(time) % 650 == 0){ //球体から平面へ if(shapeFlg == 'sphere'){ shapeFlg = 'plane'; animationFlg = 'animation'; particles.geometry = planeGeometry.clone(); //モーフィング終了 setTimeout(function(){ animationFlg = 'finish'; },1500); }else{ //平面から球体へ shapeFlg = 'sphere'; animationFlg = 'animation'; particles.geometry = sphereGeometry.clone(); } } const positions = particles.geometry.attributes.position.array; const scales = particles.geometry.attributes.scale.array; let i = 0; let p,p2; for(let ix = 0; ix < amountX; ix++){ for(let iy = 0; iy < amountY; iy++){ //頂点ベクトルを取得 p = new THREE.Vector3( positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2] ); //球体から平面 if(shapeFlg == 'sphere'){ //平面の頂点ベクトルを取得 p2 = new THREE.Vector3( planeGeometry.attributes.position.array[i * 3], planeGeometry.attributes.position.array[i * 3 + 1], planeGeometry.attributes.position.array[i * 3 + 2] ); //モーフィングアニメーション if(animationFlg == 'animation'){ positions[i * 3] += (p2.x - p.x) * 0.05; positions[i * 3 + 1] += (p2.y - p.y) * 0.05; positions[i * 3 + 2] += (p2.z - p.z) * 0.05; //モーフィング終了 if(positions[i * 3 + 1] <= 2.0){ animationFlg = 'finish'; } }else{ //モーフィング後のアニメーション positions[i * 3] += (p2.x - p.x) * 0.05; positions[i * 3 + 1] = (Math.sin((ix + time * 0.1) * 0.3) * 50) + (Math.sin((iy + time * 0.1) * 0.5) * 50); positions[i * 3 + 2] += (p2.z - p.z) * 0.05; } //平面から球体へ }else{ //球体の頂点ベクトルを取得 p2 = new THREE.Vector3( sphereGeometry.attributes.position.array[i * 3], sphereGeometry.attributes.position.array[i * 3 + 1], sphereGeometry.attributes.position.array[i * 3 + 2] ); //モーフィングアニメーション if(animationFlg == 'animation'){ positions[i * 3] += (p2.x - p.x) * 0.06; positions[i * 3 + 1] += (p2.y - p.y) * 0.06; positions[i * 3 + 2] += (p2.z - p.z) * 0.06; }else{ //モーフィング後のアニメーション p = new THREE.Vector3( positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2] ); p.normalize().multiplyScalar((Math.sin((ix + time * 0.1 * 0.8) * 0.3) * 3) + 500); positions[i * 3] += (p2.x - p.x) * 0.06; positions[i * 3 + 1] += (p2.y - p.y) * 0.06; positions[i * 3 + 2] += (p2.z - p.z) * 0.06; } } scales[i] = (Math.sin((ix + time * 0.1) * 0.3) + 1) * 12.5 + (Math.sin((iy + time * 0.1) * 0.3) + 1) * 12.5; i ++; } } particles.geometry.attributes.position.needsUpdate = true; particles.geometry.attributes.scale.needsUpdate = true;
● script.js
必要なライブラリを読み込みます。
<script src="js/preloadjs.min.js"></script> <script src="js/TweenMax.min.js"></script> <script src="js/script.js" type="module"></script>
完成した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, container, renderer } from './lib/basescene.js'; import { vertexShader, fragmentShader } from './glsl.js'; //=============================================================== // Init //=============================================================== window.addEventListener('load',function(){ init(); }); let orbitControls; let particles,sphereGeometry,planeGeometry; let shapeFlg = 'sphere'; let animationFlg = 'animation'; let time = 0; const amountX = 50,amountY = 50; function init(){ setLoading(); } function setLoading(){ TweenMax.to('.loader',0.1,{opacity:1}); TweenMax.to('#loader_wrapper',1,{ opacity:0, delay:1, onComplete: function(){ document.getElementById('loader_wrapper').style.display = 'none'; TweenMax.to('.loader',0,{opacity:0}); } }); threeWorld(); setLight(); setControll(); rendering(); } //=============================================================== // Create World //=============================================================== function threeWorld(){ renderer.outputEncoding = THREE.sRGBEncoding; const separation = 100; const particleNum = amountX * amountY; const positions = new Float32Array(particleNum * 3); const colors = new Float32Array(particleNum * 3); const scales = new Float32Array(particleNum); let i = 0; for(let ix = 0; ix < amountX; ix++){ for(let iy = 0; iy < amountY; iy++){ positions[i * 3] = ix * separation - ((amountX * separation) / 2); positions[i * 3 + 1] = 0; positions[i * 3 + 2] = iy * separation - ((amountY * separation) / 2); scales[i] = 1; const h = Math.round((i / particleNum) * 360); const s = 50; const l = 50; const color = new THREE.Color(`hsl(${h},${s}%,${l}%)`); colors[i * 3] = color.r; colors[i * 3 + 1] = color.g; colors[i * 3 + 2] = color.b; i ++; } } planeGeometry = new THREE.BufferGeometry(); planeGeometry.setAttribute('position',new THREE.BufferAttribute(positions,3)); planeGeometry.setAttribute('color',new THREE.BufferAttribute(colors,3)); planeGeometry.setAttribute('scale',new THREE.BufferAttribute(scales,1)); sphereGeometry = new THREE.SphereGeometry(500,49,49); sphereGeometry.setAttribute('color',new THREE.BufferAttribute(colors,3)); sphereGeometry.setAttribute('scale',new THREE.BufferAttribute(scales,1)); const geometry = new THREE.SphereGeometry(500,49,49); geometry.setAttribute('color',new THREE.BufferAttribute(colors,3)); geometry.setAttribute('scale',new THREE.BufferAttribute(scales,1)); const material = new THREE.RawShaderMaterial({ vertexShader:vertexShader, fragmentShader:fragmentShader, }); particles = new THREE.Points(geometry,material); scene.add(particles) } function setLight(){ const ambientlight = new THREE.AmbientLight(0xFFFFFF,1.0); 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; } function rendering(){ requestAnimationFrame(rendering); if(orbitControls){ orbitControls.update(); } time ++; if(Math.floor(time) % 650 == 0){ if(shapeFlg == 'sphere'){ shapeFlg = 'plane'; animationFlg = 'animation'; particles.geometry = planeGeometry.clone(); setTimeout(function(){ animationFlg = 'finish'; },1500); }else{ shapeFlg = 'sphere'; animationFlg = 'animation'; particles.geometry = sphereGeometry.clone(); } } const positions = particles.geometry.attributes.position.array; const scales = particles.geometry.attributes.scale.array; let i = 0; let p,p2; for(let ix = 0; ix < amountX; ix++){ for(let iy = 0; iy < amountY; iy++){ p = new THREE.Vector3( positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2] ); if(shapeFlg == 'sphere'){ p2 = new THREE.Vector3( planeGeometry.attributes.position.array[i * 3], planeGeometry.attributes.position.array[i * 3 + 1], planeGeometry.attributes.position.array[i * 3 + 2] ); if(animationFlg == 'animation'){ positions[i * 3] += (p2.x - p.x) * 0.05; positions[i * 3 + 1] += (p2.y - p.y) * 0.05; positions[i * 3 + 2] += (p2.z - p.z) * 0.05; if(positions[i * 3 + 1] <= 2.0){ animationFlg = 'finish'; } }else{ positions[i * 3] += (p2.x - p.x) * 0.05; positions[i * 3 + 1] = (Math.sin((ix + time * 0.1) * 0.3) * 50) + (Math.sin((iy + time * 0.1) * 0.5) * 50); positions[i * 3 + 2] += (p2.z - p.z) * 0.05; } }else{ p2 = new THREE.Vector3( sphereGeometry.attributes.position.array[i * 3], sphereGeometry.attributes.position.array[i * 3 + 1], sphereGeometry.attributes.position.array[i * 3 + 2] ); if(animationFlg == 'animation'){ positions[i * 3] += (p2.x - p.x) * 0.06; positions[i * 3 + 1] += (p2.y - p.y) * 0.06; positions[i * 3 + 2] += (p2.z - p.z) * 0.06; }else{ p = new THREE.Vector3( positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2] ); p.normalize().multiplyScalar((Math.sin((ix + time * 0.1 * 0.8) * 0.3) * 3) + 500); positions[i * 3] += (p2.x - p.x) * 0.06; positions[i * 3 + 1] += (p2.y - p.y) * 0.06; positions[i * 3 + 2] += (p2.z - p.z) * 0.06; } } scales[i] = (Math.sin((ix + time * 0.1) * 0.3) + 1) * 12.5 + (Math.sin((iy + time * 0.1) * 0.3) + 1) * 12.5; i ++; } } particles.geometry.attributes.position.needsUpdate = true; particles.geometry.attributes.scale.needsUpdate = true; particles.rotation.y = time * 0.05 * Math.PI / 180; renderer.render(scene,camera); }
● glsl.js
const vertexShader =` attribute vec3 position; attribute vec3 color; attribute float scale; uniform mat4 projectionMatrix; uniform mat4 modelViewMatrix; varying vec3 vColor; void main(void){ vColor = color; vec4 mvPosition = modelViewMatrix * vec4(position,1.0); gl_PointSize = scale * (300.0 / -mvPosition.z); gl_Position = projectionMatrix * mvPosition; } `; const fragmentShader =` precision highp float; varying vec3 vColor; void main(void){ if(length(gl_PointCoord - vec2(0.5,0.5)) > 0.475){ discard; } gl_FragColor = vec4(vColor,1.0); } `; export { vertexShader, fragmentShader };
● basescene.js
sceneやcameraなど基本的な設定を管理するbasescene.jsです。
//=============================================================== // Import Library //=============================================================== import * as THREE from './three_jsm/three.module.js'; //=============================================================== // Base scene //=============================================================== let scene,camera,container,renderer; init(); function init(){ scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera(75,window.innerWidth/window.innerHeight,1,10000); camera.position.set(0,400,1000); scene.add(camera); renderer = new THREE.WebGLRenderer({antialias:true}); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth,window.innerHeight); renderer.setClearColor(new THREE.Color(0x000000)); container = document.querySelector('#canvas_vr'); container.appendChild(renderer.domElement); window.addEventListener('resize',function(){ camera.aspect = window.innerWidth/window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth,window.innerHeight); },false); } export { scene, camera, container, renderer }
完成したデモになります。平面から球体へモーフィングする頂点アニメーションを試してみました。また、バッファオブジェクトにカラーをつけました。