エンジニアの副業は週1からでも可能?副業の例や探し方も解説
- ITエンジニア
- 副業
Webブラウザ上で3DCGを扱うための仕組みであるWebGL。そのWebGLを扱うにあたって活用されているのが、無料で利用できるJavaScriptライブラリ『three.js』です。
今回はこのthree.jsとTweenMax(GSAP)を使用して、インタラクティブエフェクトを作成する方法をご紹介します。
なお、エフェクトはBestServedBoldのHolographic-Interactionsを参考にしています。
ひとつめのデモでは完成された実用的な例をご紹介しましたが、これから作成過程をご紹介するデモは調整およびアレンジが可能です。
また前述のとおり、今回ご紹介するエフェクトはBestServedBoldによるHolographic-Interactionsを参考にしています。
エフェクトを作成するにあたってコアとなっているのが、「マウスの動きに連動してランダムなエレメントグリッドを生成する」という発想です。
グリッドの各エレメントは、マウスの位置からエレメントの中心までの距離に基づいて、Yの位置、回転値、尺度値を更新します。
マウスがエレメントに近づけば近づくほど、エレメントが大きく表示されます。
また半径を定義することによって、半径内のエレメントへの影響をコントロールします。半径が大きければ大きいほど、マウスを動かしたときに反応するエレメント数が増えます。
まずデモ用にHTMLページを設定しましょう。
コードはすべてキャンバスエレメント内で実行されるため、HTMLページ自体はシンプルです。
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="target" content="all">
<meta http-equiv="cleartype" content="on">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<title>Repulsive Force Interavtion</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/96/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.3/TweenMax.min.js"></script>
</head>
<body>
</body>
</html>
コードからもわかるとおり、CDNからthree.jsとTweenMaxにリンクしています。
次に2点間の距離を計算し、値をマッピングして、角度をラジアンに変換するためのヘルパー関数を定義しましょう。
const radians = (degrees) => {
return degrees * Math.PI / 180;
}
const distance = (x1, y1, x2, y2) => {
return Math.sqrt(Math.pow((x1 - x2), 2) + Math.pow((y1 - y2), 2));
}
const map = (value, start1, stop1, start2, stop2) => {
return (value - start1) / (stop1 - start1) * (stop2 - start2) + start2
}
グリッドを構築する前に、使用するオブジェクトを定義しましょう。
// credits for the RoundedBox mesh - Dusan Bosnjak
import RoundedBoxGeometry from 'roundedBox';
class Box {
constructor() {
this.geom = new RoundedBoxGeometry(.5, .5, .5, .02, .2);
this.rotationX = 0;
this.rotationY = 0;
this.rotationZ = 0;
}
}
class Cone {
constructor() {
this.geom = new THREE.ConeBufferGeometry(.3, .5, 32);
this.rotationX = 0;
this.rotationY = 0;
this.rotationZ = radians(-180);
}
}
class Torus {
constructor() {
this.geom = new THREE.TorusBufferGeometry(.3, .12, 30, 200);
this.rotationX = radians(90);
this.rotationY = 0;
this.rotationZ = 0;
}
}
メインクラスの中に、セットアップ用の関数を作成しましょう。
setup() {
// handles mouse coordinates mapping from 2D canvas to 3D world
this.raycaster = new THREE.Raycaster();
this.gutter = { size: 1 };
this.meshes = [];
this.grid = { cols: 14, rows: 6 };
this.width = window.innerWidth;
this.height = window.innerHeight;
this.mouse3D = new THREE.Vector2();
this.geometries = [
new Box(),
new Tourus(),
new Cone()
];
window.addEventListener('mousemove', this.onMouseMove.bind(this), { passive: true });
// we call this to simulate the initial position of the mouse cursor
this.onMouseMove({ clientX: 0, clientY: 0 });
}
onMouseMove({ clientX, clientY }) {
this.mouse3D.x = (clientX / this.width) * 2 - 1;
this.mouse3D.y = -(clientY / this.height) * 2 + 1;
}
createScene() {
this.scene = new THREE.Scene();
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(this.renderer.domElement);
}
カメラをシーンに追加しましょう。
createCamera() {
this.camera = new THREE.PerspectiveCamera(20, window.innerWidth / window.innerHeight, 1);
// set the distance our camera will have from the grid
this.camera.position.set(0, 65, 0);
// we rotate our camera so we can get a view from the top
this.camera.rotation.x = -1.57;
this.scene.add(this.camera);
}
箱型、円錐型、ドーナツ型のオブジェクトをランダムに配置するためのヘルパーを作成しましょう。
getRandomGeometry() {
return this.geometries[Math.floor(Math.random() * Math.floor(this.geometries.length))];
}
ジオメトリとマテリアルに基づいてメッシュを作成するためのヘルパーを作成しましょう。
getMesh(geometry, material) {
const mesh = new THREE.Mesh(geometry, material);
mesh.castShadow = true;
mesh.receiveShadow = true;
return mesh;
}
ここでやっとランダムなエレメントをグリッドにレイアウトできました。
createGrid() {
// create a basic 3D object to be used as a container for our grid elements so we can move all of them together
this.groupMesh = new THREE.Object3D();
const meshParams = {
color: '#ff00ff',
metalness: .58,
emissive: '#000000',
roughness: .18,
};
// we create our material outside the loop to keep it more performant
const material = new THREE.MeshPhysicalMaterial(meshParams);
for (let row = 0; row < this.grid.rows; row++) {
this.meshes[row] = [];
for (let col = 0; col < this.grid.cols; col++) {
const geometry = this.getRandomGeometry();
const mesh = this.getMesh(geometry.geom, material);
mesh.position.set(col + (col * this.gutter.size), 0, row + (row * this.gutter.size));
mesh.rotation.x = geometry.rotationX;
mesh.rotation.y = geometry.rotationY;
mesh.rotation.z = geometry.rotationZ;
// store the initial rotation values of each element so we can animate back
mesh.initialRotation = {
x: mesh.rotation.x,
y: mesh.rotation.y,
z: mesh.rotation.z,
};
this.groupMesh.add(mesh);
// store the element inside our array so we can get back when need to animate
this.meshes[row][col] = mesh;
}
}
//center on the X and Z our group mesh containing all the grid elements
const centerX = ((this.grid.cols - 1) + ((this.grid.cols - 1) * this.gutter.size)) * .5;
const centerZ = ((this.grid.rows - 1) + ((this.grid.rows - 1) * this.gutter.size)) * .5;
this.groupMesh.position.set(-centerX, 0, -centerZ);
this.scene.add(this.groupMesh);
}
次に、色の効果を出すためにAmbientLightを追加しましょう。
addAmbientLight() {
const light = new THREE.AmbientLight('#2900af', 1);
this.scene.add(light);
}
リアルなタッチにするために、スポットライトのような効果をつけられるSpotLightも追加しましょう。
addSpotLight() {
const ligh = new THREE.SpotLight('#e000ff', 1, 1000);
ligh.position.set(0, 27, 0);
ligh.castShadow = true;
this.scene.add(ligh);
}
さらに四角く平面を照らすRectAreaLightも追加します。
addRectLight() {
const light = new THREE.RectAreaLight('#0077ff', 1, 2000, 2000);
light.position.set(5, 50, 50);
light.lookAt(0, 0, 0);
this.scene.add(light);
}
最後にポイントを照らすPointLightを追加すれば、ライティングは完成です。
addPointLight(color, position) {
const light = new THREE.PointLight(color, 1, 1000, 1);
light.position.set(position.x, position.y, position.z);
this.scene.add(light);
}
次に、マウスカーソルがホバーできる場所のマッピングオブジェクトとして機能する図形を追加しましょう。
addFloor() {
const geometry = new THREE.PlaneGeometry(100, 100);
const material = new THREE.ShadowMaterial({ opacity: .3 });
this.floor = new THREE.Mesh(geometry, material);
this.floor.position.y = 0;
this.floor.receiveShadow = true;
this.floor.rotateX(- Math.PI / 2);
this.scene.add(this.floor);
}
最後に、アニメーションの処理をハンドルする関数を追加しましょう。これはrequestAnimationFrame内のすべてのフレームに呼び出されます。
draw() {
// maps our mouse coordinates from the camera perspective
this.raycaster.setFromCamera(this.mouse3D, this.camera);
// checks if our mouse coordinates intersect with our floor shape
const intersects = this.raycaster.intersectObjects([this.floor]);
if (intersects.length) {
// get the x and z positions of the intersection
const { x, z } = intersects[0].point;
for (let row = 0; row < this.grid.rows; row++) {
for (let col = 0; col < this.grid.cols; col++) {
// extract out mesh base on the grid location
const mesh = this.meshes[row][col];
// calculate the distance from the intersection down to the grid element
const mouseDistance = distance(x, z,
mesh.position.x + this.groupMesh.position.x,
mesh.position.z + this.groupMesh.position.z);
// based on the distance we map the value to our min max Y position
// it works similar to a radius range
const maxPositionY = 10;
const minPositionY = 0;
const startDistance = 6;
const endDistance = 0;
const y = map(mouseDistance, startDistance, endDistance, minPositionY, maxPositionY);
// based on the y position we animate the mesh.position.y
// we don´t go below position y of 1
TweenMax.to(mesh.position, .4, { y: y < 1 ? 1 : y });
// create a scale factor based on the mesh.position.y
const scaleFactor = mesh.position.y / 2.5;
// to keep our scale to a minimum size of 1 we check if the scaleFactor is below 1
const scale = scaleFactor < 1 ? 1 : scaleFactor;
// animates the mesh scale properties
TweenMax.to(mesh.scale, .4, {
ease: Back.easeOut.config(1.7),
x: scale,
y: scale,
z: scale,
});
// rotate our element
TweenMax.to(mesh.rotation, .7, {
ease: Back.easeOut.config(1.7),
x: map(mesh.position.y, -1, 1, radians(45), mesh.initialRotation.x),
z: map(mesh.position.y, -1, 1, radians(-90), mesh.initialRotation.z),
y: map(mesh.position.y, -1, 1, radians(90), mesh.initialRotation.y),
});
}
}
}
}
これでインタラクティブエフェクトは完成です。今回ご紹介したデザイン以外にも、グリッドのレイアウトを変えたり、オブジェクトを増やしたりするなど、さまざまな可能性を秘めています。
以下はグリッドのレイアウトを変更した例です。
また以下のように、カメラの角度を変えても面白いかもしれません。
今回のチュートリアルはお楽しみいただけたでしょうか。ぜひこのチュートリアルをベースにして、オリジナルの作品を作ってみてください!
(原文:Ion D. Filho 翻訳:Asuka Nakajima)