Twitter
2021年05月22日 - Three.js・WebVR

平面から球体へモーフィングする頂点アニメーション

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);

	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 }

完成したデモになります。平面から球体へモーフィングする頂点アニメーションを試してみました。また、バッファオブジェクトにカラーをつけました。

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

関連記事

前の記事へ

EffectComposerでポストプロセッシング

次の記事へ

Three.jsでペーパーアニメーション