Three.jsをコツコツと勉強中です。Three.jsは3Dモデルを読み込んでCanvasに表示させることができます。とっても楽しくてワクワクする夢のある体験を提供できる夢のあるライブラリです。
Three.jsで3Dモデルをロード
Three.jsで少しずついろいろなものを作れるようになってきたので、3Dモデルも使ってみたくなりました。ガラスの島と航空機の3Dモデルを読み込んで、ふわふわと動かしたり、パーティクルを回転させたりして、浮遊感を楽しめるものを作ってみました!
まだまだ、勉強中ですが、興味を持つきっかけになってもらえれば、と作り方を記事にまとめてみました。
モデリングは自分ではまだ難しいので、3Dモデルの販売サイト、Sketchfabを利用しました。見ているだけでも楽しい3Dモデルがたくさんありますし、無料のモデルもあります。どれも、とても素敵なんです。今回は、2つの3Dモデルを読み込んで、非現実的な雰囲気をだしてみました。
完成したのはこちらです↓
サーバーにもアップしてみましたので、ぜひ遊びにいってみてください。最初の読み込みに時間がかかってもっさりなので、ローディング画面入れるとか、改善したいです。
デモはこちら! https://barairopop.kosodante.com/designsample/ThreeJS/
Glass Sphere Island by AnshiNoWara on Sketchfab
AirCraft by Dennis Haupt on free3D
※GlassSphere Islandは購入しました。AirCraftは、ドイツのアーティストの方が配布されていた無料モデルで、個人ライセンス(商用利用以外の個人的な使用ならOK)での利用が許可されていました。無料なのに、ハイクォリティすぎる。
Three.jsで3Dモデルを読み込むための準備
3Dモデルは、いろいろな種類のファイルがあり、Three.js ではさまざまな種類の3Dモデルのファイルを読み込むことができます。ただ、Webで扱うためなるべくファイルが軽くなるgltfが推奨されているそうです。
今回は、gltf形式のモデルを読み込んでみることにしました。
gltfでも重いよって場合は、Googleが開発したDracoという3Dデータ圧縮方式を使って圧縮すると、さらに軽くできるそうです。Node.jsのコマンドを使って圧縮するので、自分でもできそう。そのうちチャレンジしてみようと思います。
それでは、3Dモデルを読み込んでみたいと思います。
GLTFLoaderのインポート
最初にやることは、モジュールのインポートです。もし、他の形式のファイルを読み込む場合は、また別のローダーを読み込むためのモジュールをインポートする必要があります。
import * as THREE from 'three';
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
scene, camera, rendererを作る
つぎに、scene, camera, rendererを作ります。こちらを用意しておかないと、3Dモデルを読み込んでも視覚化できません。
ブラウザにThree.jsで何かを視覚化できるようにするには、メッシュ、シーン、カメラ、レンダリングなど、基本的な要素を作成する必要があります。
映像を作るような感じで、カメラ=camera、舞台=scene、カメラマン=rederer、mesh=役者を用意する、というイメージが近いかもしれません。
//Create Scene
const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0xffffff, 1,3000);
//Load Background
const backgroundTexture = new THREE.TextureLoader().load("./img/backimage.jpg");
scene.background = backgroundTexture;
//Create Camera
const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight);
camera.position.set(0,0,350);
scene.add(camera);
// Create WebGLRenderer and append its canvas to DOM
const renderer = new THREE.WebGLRenderer();
document.body.appendChild(renderer.domElement);
//Setsize
const setSize = () => {
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(devicePixelRatio || 1);
}
//Resize
const resize = () => {
setSize();
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
}
setSize();
window.addEventListener('resize', resize);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="module" src="./script.js"></script>
<title> ThreeJS DEMO</title>
</head>
<body>
</body>
</html>
背景画像の読み込み
通常のCanvasは真っ黒ですが、今回のように背景に画像を読み込みたい場合は、背景のテクスチャーを読み込んで、scene.backgroundに追加します。
時間やスクロールで、背景画像を変えても面白いかもですね。
//Load Background
const backgroundTexture = new THREE.TextureLoader().load("./img/backimage.jpg");
scene.background = backgroundTexture;
rendererのサイズ調整
ウィンドウサイズが変わった時のredererのサイズ調整の処理を実装しておくと、画面サイズが変わっても綺麗におさまるので安心です。
//Setsize
const setSize = () => {
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(devicePixelRatio || 1);
}
//Resize
const resize = () => {
setSize();
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
}
setSize();
window.addEventListener('resize', resize);
遠近感を出す
scene.fogというのは、遠くのものを暗くして、簡単に遠近感を出すためのプロパティです。陰影に色もつけることができます。パーティクルに遠近感をつけたかったので、追加しました。
//Create Scene
const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0xffffff, 1,3000);
3Dモデルの読み込み gltfファイル
やっと、3Dモデルを読み込みます!難しいことはあまりなく、ローダーでgltfファイルを読み込みます。テクスチャーの読み込みと似ています。
//Load Glass Sphere Island
const islandLoader = new GLTFLoader();
const islandWrap = new THREE.Object3D();
scene.add(islandWrap);
islandLoader.load("./models/scene.gltf",(gltf)=>{
const island = gltf.scene;
island.scale.set(0.03,0.03,0.03);
island.rotation.set(0.2,1,0);
island.position.set(0,0,0);
islandWrap.add(island);
}, undefined, function ( error ) {
console.error( error );
});
//Render
renderer.render(scene,camera); //一番最後に実行する場所に書く
うまく読み込まれないよって人は、エラー内容を確認&ファイルの置き場所があっているか、ファイルがちゃんとそろっているか確認してみてください。
scene.bin,scene.gltfと同じ階層に、texturesディレクトリを設置してテクスチャ画像を格納しておきます。
>models
>textures //テクスチャ画像をいれておく
scene.bin
scene.gltf
エラーが出てない場合は、3Dモデルが小さすぎて見えない or 大きすぎて見えない可能性があるので、スケールや位置を調整してみてください。
island.scale.set(0.03,0.03,0.03); //スケール
island.rotation.set(0.2,1,0); //角度
island.position.set(0,0,0); //位置
3Dモデルのサイズ調整
そのままだと、Glass Sphereの3Dモデルはとても大きいサイズだったので、サイズを変更をしました。後ほど、アニメーション処理をつけたいので、new THREE.Object3D()メソッドで親を作り、入れ子にしました。
//Load Glass Sphere Island
const islandLoader = new GLTFLoader();
const islandWrap = new THREE.Object3D(); //親をつくる
scene.add(islandWrap);
islandLoader.load("./models/scene.gltf",(gltf)=>{
const island = gltf.scene;
island.scale.set(0.03,0.03,0.03);
island.rotation.set(0.2,1,0);
island.position.set(0,0,0);
islandWrap.add(island); //親に子をいれる
}, undefined, function ( error ) {
console.error( error );
});
3Dモデルの読み込み objファイル
AirCraftの3Dモデルは、gltfファイルがなく、objファイルだったので、以下のようにモジュールを追加しました。
import * as THREE from 'three';
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { MTLLoader } from "three/examples/jsm/loaders/MTLLoader"; //追加
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader"; //追加
これも、サイズ調整して、入れ子構造にしました。
//Load AirCraft
let airCraftMtlLoader = new MTLLoader();
let airCraftObjLoader = new OBJLoader();
const airCraftWrap = new THREE.Object3D();
scene.add(airCraftWrap);
airCraftMtlLoader.load('./models/01.mtl', (materials) => {
materials.preload();
airCraftObjLoader.setMaterials(materials);
airCraftObjLoader.load('./models/01.obj', (object) => {
object.scale.set(8,8,8);
object.rotation.set(9.5,-61,10);
object.position.set(100,50,-200);
airCraftWrap.add(object);
})
});
ライトをつくる
このままでも、一応、視覚化できますが、明かりがないので真っ暗です。背景がなかったら、Canvasは黒いので3Dオブジェクトも見えないです。なので、ライトはモデルよりも、先につくったほうがいいかもです。

ライトを作ります。ライトの種類はいろいろありますが、とりあえずDirectionalLight(太陽光みたいな光)を作っておけば、ひとまず明るく見えます。
//Create directionLight
const directionlLight = new THREE.DirectionalLight( 0xE8D2CC,4.5 );
directionlLight.position.set( 10, 10, 50 ).normalize();
scene.add( directionlLight );
明るくなって、物体が見えるようになりました!

ちょっと物足りないので、スポットライトも追加します。
一緒にライトの向きがわかるように方向をしめしてくれる、ライトヘルパーをつくっておくと、ライトの向きがわかりやすいです。ライトヘルパーは線で方向を教えてくれます。
//Create spotLight
const spotLight = new THREE.SpotLight(0xE8D2CC, 200, 450, Math.PI / 15, 0.5,4.0);
spotLight.position.set(-220,200,-10);
scene.add(spotLight);
// Create spotLight Helper
// const lightHelper = new THREE.SpotLightHelper(spotLight);
// scene.add(lightHelper);

ライトの種類は有名なこちらのサイトの解説が非常にわかりやすく、参考になりました!
パーティクルを作る
もう少し非現実感を出すために、パーティクルを作りました。3Dモデルのまわりに飛んでいる四角いゴミみたいなやつです。

BoxBufferGeometryをループ処理たくさん作って、ランダム配置しています。
// create Group
const group = new THREE.Group();
scene.add(group);
const groupTexture = new THREE.TextureLoader().load("./img/pink.jpg");
const geometry = new THREE.BoxBufferGeometry(2, 2, 2);
const material = new THREE.MeshStandardMaterial({
color:0xF4E1F4,
roughness:0.5,
map:groupTexture
});
const length = 1000;
for (let i = 0; i < length; i++) {
const mesh = new THREE.Mesh(geometry, material);
mesh.position.x = (Math.random() - 0.3) * 1500;
mesh.position.y = (Math.random() - 0.5) * 1500;
mesh.position.z = (Math.random() - 0.5) * 1500;
mesh.rotation.x = Math.random() * 10 * Math.PI;
group.add(mesh);
}
Glass Islandに環境マッピングの追加
環境マッピングは、展開図のような画像を貼り付けることで、まるで周囲の景色や風景が映り込んでいるかのように見せる方法です。
ガラス部分がとてものっぺりしていたので、環境マッピングを追加して本物っぽい見た目になるようにしてみました。環境マッピングについてはまた違う記事にしようと思います。
// Load cubeMap
const PATH = './img/map/';
const FORMAT = '.jpg';
let urls = [
PATH + 'posx' + FORMAT,
PATH + 'negx' + FORMAT,
PATH + 'posy' + FORMAT,
PATH + 'negy' + FORMAT,
PATH + 'posz' + FORMAT,
PATH + 'negz' + FORMAT,
];
//Load cubeTexture
let cubeTextureLoader = new THREE.CubeTextureLoader();
const cubeTexture = cubeTextureLoader.load(urls);
//Load Glass Sphere Island
const islandLoader = new GLTFLoader();
const islandWrap = new THREE.Object3D();
scene.add(islandWrap);
islandLoader.load("./models/scene.gltf",(gltf)=>{
const island = gltf.scene;
island.traverse((object) => {
if(object.isMesh) {
object.material.envMap = cubeTexture;
object.material.shininess = 0.8;
object.material.metalness = 1.2,
object.material.roughness = 1.2,
object.material.refractionRatio = 0.5;
object.material.reflectivity = 1.2;
}})
island.scale.set(0.03,0.03,0.03);
island.rotation.set(0.2,1,0);
island.position.set(0,0,0);
islandWrap.add(island);
}, undefined, function ( error ) {
console.error( error );
});
ガラスみたいに反射しているのがわかるでしょうか。疑似的に、風景が反射しているように見せています。鏡みたいにすれば、もっとわかりやすいのですが、今回は控えめになりました。

角度を変えてみると、もう少しよくわかります。あんまりうまく調整できていませんが、ひとまずこれでよしとします。

アニメーションをつける
最後に、作った3Dモデルとパーティクルにアニメーションで動かしてみます。
// Animations
const tick = () => {
airCraftWrap.position.set(
10 * Math.sin(Date.now() / 600),
10 * Math.sin(Date.now() / 1500),
200 * Math.cos(Date.now() / 1800)
);
islandWrap.position.set(
5 * Math.sin(Date.now() / 1000),
5 * Math.sin(Date.now() / 1000),
5 * Math.cos(Date.now() / 1000)
);
group.rotateY(0.003);
group.rotateZ(0.003);
//Render
renderer.render(scene,camera);
}
//set Animation Loop
renderer.setAnimationLoop(tick);
完成したものがこちら。ガラスの島がふわふわ揺れる動きと、飛行機が伺っているような感じで飛ぶ動き、パーティクルは回転させています。
結構時間がかかりましたが、完成してよかったです。少しずつ改良を重ねて、TailwindCSSとあわせたポートフォリオを作ってサーバーにアップしたいです。
お付き合いありがとうございました!