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

