Blog ブログ

今回は、gltfというものを使用して、ワンランク上を目指したthree.jsの紹介をします。
web制作の仕事をしていると、「オシャレさ」とか「目立つ」「目を惹く感じ」といった
ちょっと曖昧なご要望をいただくこともあるかと思います。
そんな時、three.jsで3Dモデルを使用した構築の知見があると、お客様にご満足いただける機会が増えるのではと思いますので
今回はthree.js3Dモデル編ということでやっていきます!
ただ、今回は基礎的な「シーン」「カメラ」「レンダリング」あたりの説明は省きますので、
three.jsの基礎から知りたいという方は、
【three.js】3Dアニメーション three.js〜入門〜 をぜひチェックしてみてください!
今回は、ファンタジーな世界で綺麗な鳥が羽ばたいているようなものを作成します。
こんな感じ↓

早速ですがデモサイトと完成コードをご紹介します。
マウス操作で動きますので、ぜひドラッグして遊んでみてくださいね!
前回同様、難しそうに見えるかもしれませんが、そんなに大したことしてないのでご安心くださいませ!!
それでは早速やっていきましょう。
gltf、3Dモデルについて
まずはgltf、3Dモデルについて軽く説明します。
※読み飛ばしてOK
まず、Web上で立体的なオブジェクト(3Dモデル)を扱うとき、「どのような形式でデータを扱うか」はとても重要です。その中でも近年、特に注目されているのが「glTF(GL Transmission Format)」というファイル形式です。
⚫︎そもそも3Dモデルとは?
3Dモデルとは、立体的な形状や見た目をデジタル上に再現したデータのことです。キャラクター、建物、乗り物など、私たちが現実世界で目にするものを仮想空間に再現するために使われます。
3Dモデルは、主に以下の要素で構成されています:
- ジオメトリ(形状):点や面の集まりで構成されるオブジェクトの“かたち”
- マテリアル/テクスチャ:表面の色や質感(木の板、金属、皮など)
- アニメーション:動き(キャラクターの歩き方やドアの開閉など)
⚫︎glTF形式とは?
glTF(ジーエルティーエフ)は、Khronos Groupという団体が開発した3Dモデルのファイル形式です。「3D版のJPEG」とも呼ばれ、以下のような特徴があります:
- 軽量で読み込みが速い
- テクスチャ、アニメーション、マテリアルなどを1つのファイルにまとめられる
- Webやゲームエンジンなど、多くのプラットフォームで対応
Three.js でも glTF 形式の3Dモデルを簡単に読み込んで表示できます。そのため、現在では glTF はWeb上で3Dモデルを扱う際の“スタンダード”になりつつあります。
3Dモデルを表示してみよう
ひとまず、こんな感じにしてみます↓

まずは3Dモデルをダウンロードしてください。
わたしはこちらを使用しております。
※他の3Dモデルを使用する際は、アニメーションが含まれているものを選ぶと良いです
上記で紹介したサイトは、Sketchfab(スケッチファブ)というサイトで、
無料でダウンロードできますのでおすすめです。
さて、早速3Dモデルを表示していきましょう!
ダウンロードできたら、プロジェクトに入れていきましょう。
ディレクトリ構造はこんな感じ↓
.
├── index.html
├── index.js
├── models ←これ以下が3Dモデル用のファイルです!
│ └── bird
│ ├── license.txt
│ ├── scene.bin
│ ├── scene.gltf
│ └── textures
│ ├── MatI_Ride_FengHuang_01a_baseColor.png
│ ├── MatI_Ride_FengHuang_01a_emissive.png
│ ├── MatI_Ride_FengHuang_01b_baseColor.png
│ └── MatI_Ride_FengHuang_01b_emissive.png
全体コードはこちら↓
⚫︎index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<!-- three.jsを読み込む -->
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.175.0/build/three.module.js",
"three/examples/jsm/controls/OrbitControls.js": "https://cdn.jsdelivr.net/npm/three@0.175.0/examples/jsm/controls/OrbitControls.js",
"three/examples/jsm/loaders/GLTFLoader.js": "https://cdn.jsdelivr.net/npm/three@0.175.0/examples/jsm/loaders/GLTFLoader.js",
"three/examples/jsm/loaders/RGBELoader.js": "https://cdn.jsdelivr.net/npm/three@0.175.0/examples/jsm/loaders/RGBELoader.js"
}
}
</script>
<!-- index.jsを読み込む -->
<script type="module" src="./index.js"></script>
<!-- <script type="module" src="./index.js"></script> -->
</head>
<body>
<canvas id="myCanvas"></canvas>
</body>
</html>
⚫︎index.js
// 鳥を表示してみる
// Three.js本体とOrbitControlsをモジュールとして読み込み
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";//鳥用
// canvasを取得してレンダラー作成
const canvas = document.getElementById("myCanvas");
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
// シーンとカメラ作成
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 2, 6);
scene.background = new THREE.Color(0x000000);//鳥だけ用
//*---------------------
// 鳥用
//*---------------------
let model;//読み込んだ3Dモデル(gltf.scene)を格納する変数。
let mixer; //アニメーションを制御するためのAnimationMixerを格納する変数。
const clock = new THREE.Clock();//アニメーションの経過時間を測るための時計。
// モデル読み込み
const loader = new GLTFLoader();//3Dモデルを読み込むためのローダーを作成。
loader.load(//モデルの読み込み
"./models/bird/scene.gltf",
(gltf) => {
model = gltf.scene;//読み込んだ3Dモデルのシーン部分を取得
model.scale.set(0.01, 0.01, 0.01);//モデルを小さく縮小
model.position.set(0, 0, 0);//モデルの位置を原点に
scene.add(model);//モデルをThree.jsのシーンに追加
if (gltf.animations.length > 0) {
mixer = new THREE.AnimationMixer(model);//モデル用のアニメーションミキサーを作成
const action = mixer.clipAction(gltf.animations[0]);//最初のアニメーションクリップを取得
action.play();//アニメーションを再生
} else {
console.warn("このモデルにはアニメーションが含まれていません");
}
},
undefined,
(error) => {
console.error("モデル読み込みエラー:", error);
}
);
//*---------------------
// 鳥用ここまで
//*---------------------
// ライト
const directionalLight = new THREE.DirectionalLight(0xffffff, 8); // 白色の方向光を作成 第二引数は光の強さ
directionalLight.position.set(0, 10, 10); // ライトの位置を設定
directionalLight.lookAt(0, 0, 0); // 追加で方向を合わせる
scene.add(directionalLight);
// OrbitControls
const controls = new OrbitControls(camera, renderer.domElement);
// ウィンドウリサイズ対応
window.addEventListener("resize", () => {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
});
// アニメーションループ
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta(); // ← フレーム間の経過時間
mixer.update(delta); // ← これを忘れると動きません!
controls.update();
renderer.render(scene, camera);
}
animate();
ぜひvscodeのlive server等でお試しください!
少し説明
ある程度コード内にコメントアウトで説明を書いていますが、少し補足です。
⚫︎GLTFLoader
3Dモデル(.gltf/.glbファイル)を読み込むためのローダー
⚫︎load()
loader.load("./models/bird/scene.gltf", (gltf) => { ... }, undefined, (error) => { ... });
第一引数: モデルファイルのパス
第二引数: 読み込み成功時のコールバック
第三引数: 読み込み進捗(今回は未使用)
第四引数: 読み込み失敗時のコールバック
3Dモデルを動かしてみよう
アニメーションされているのですでに動いているのですが、
鳥が自分であっちこっち飛び回るようにしてみます。
animate()関数内に記述することで動かすことが可能です。
ひとまず完成コードはこちら↓
// アニメーションループ
let goingForward = true; // 初期状態は前進
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta(); // ← フレーム間の経過時間
mixer.update(delta); // ← これを忘れると動きません!
//鳥を動かしてみる
if (model) {
if (goingForward) {
model.position.z -= 0.03;
model.rotation.y -= 0.01;
if (model.position.z < -8) goingForward = false; // 折り返し地点
} else {
model.position.z += 0.03;
model.rotation.y += 0.01;
if (model.position.z > 0) goingForward = true; // 戻りきったら再び前進
}
}
controls.update();
renderer.render(scene, camera);
}
animate();
「model.position.z -= 0.02;」で遠くに行くように動きますが、
遠くに行ったっきり帰って来てくれないので
goingForwardで条件分岐し一定時間経ったら逆の動きをするようにしています。
⚫︎いろいろな動きの紹介
いろんな動かし方ができるのでぜひ試してみてください!
model.position.z -= 0.02;//遠くに行く
model.rotation.y -= 0.02;//回転
model.position.x = Math.sin(Date.now() * 0.001) * 1.0; // X方向に揺れる
model.position.y += Math.sin(Date.now() * 0.001) * 0.03; //上下に揺れる
背景をつけてみよう
3Dモデルにリアルな雰囲気を出すためには、背景も重要な要素です。真っ黒な空間にモデルだけが浮かんでいる状態だと、どうしても物足りなさを感じてしまいますよね。
そこで今回は、「スカイドーム(HDRI)」を使って、背景を追加してみます。
※スカイドームとは、360度の画像のようなものです
まずはHDRIのファイルを手に入れましょう。
私はこちらのサイトからダウンロードしました。
※具体的にどれをダウンロードしたかは忘れました
プロジェクトに設置できたら、こちらのコードを追記してみましょう。
※シーンとカメラ作成の下あたり
※importは上の方に
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';//スカイドーム用
//*---------------------
// スカイドーム用
//*---------------------
const rgbeLoader = new RGBELoader();
const pmremGenerator = new THREE.PMREMGenerator(renderer);
pmremGenerator.compileEquirectangularShader();
rgbeLoader.load('./textures/cape_hill_1k.hdr', (texture) => {
const envMap = pmremGenerator.fromEquirectangular(texture).texture;
scene.environment = envMap; // 環境光マップ
scene.background = envMap; // 背景にも使う
texture.dispose();
pmremGenerator.dispose();
});
//*---------------------
// スカイドーム用ここまで
//*---------------------
少し説明
3Dシーンにリアルな空や風景の背景を加えたいときに使われるのが「HDR(ハイダイナミックレンジ)画像」です。このコードは、そのHDR画像を**スカイドーム(環境背景)**として読み込み、Three.jsのシーンに適用するためのものです。
⚫︎RGBELoader
const rgbeLoader = new RGBELoader();
HDR画像(.hdrファイル、環境マップやスカイドーム用)を読み込むためのローダー
⚫︎PMREMGenerator・compileEquirectangularShader()
const pmremGenerator = new THREE.PMREMGenerator(renderer);
pmremGenerator.compileEquirectangularShader();
「環境光マップ(環境ライティング)用のテクスチャ」を生成し、
読み込むHDR画像の形式(緯度経度マッピング)に対応する準備をする
⚫︎load()
rgbeLoader.load('./textures/cape_hill_1k.hdr', (texture) => {
HDR画像(この場合は cape_hill_1k.hdr
)を読み込み、読み込みが完了すると、コールバック関数の中が実行される
const envMap = pmremGenerator.fromEquirectangular(texture).texture;
読み込んだHDR画像を元に、Three.jsが利用できる環境マップ(envMap
)に変換
「光の反射」や「環境の明るさ」にリアルな影響を与える
scene.environment = envMap; // 環境光マップ
scene.background = envMap; // 背景にも使う
scene.environment
に設定すると、3Dオブジェクトが環境光を反映するscene.background
にも同じマップを設定することで、シーンの背景としても表示される
texture.dispose();
pmremGenerator.dispose();
メモリを節約するため、もう使わないオブジェクトを破棄(dispose()
)
シーンの雰囲気がぐっとリアルになりましたね!
完成コード
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<!-- three.jsを読み込む -->
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.175.0/build/three.module.js",
"three/examples/jsm/controls/OrbitControls.js": "https://cdn.jsdelivr.net/npm/three@0.175.0/examples/jsm/controls/OrbitControls.js",
"three/examples/jsm/loaders/GLTFLoader.js": "https://cdn.jsdelivr.net/npm/three@0.175.0/examples/jsm/loaders/GLTFLoader.js",
"three/examples/jsm/loaders/RGBELoader.js": "https://cdn.jsdelivr.net/npm/three@0.175.0/examples/jsm/loaders/RGBELoader.js"
}
}
</script>
<!-- index.jsを読み込む -->
<script type="module" src="./index.js"></script>
<!-- <script type="module" src="./index.js"></script> -->
</head>
<body>
<canvas id="myCanvas"></canvas>
</body>
</html>
index.js
// Three.js本体とOrbitControlsをモジュールとして読み込み
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";//鳥用
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';//スカイドーム用
// canvasを取得してレンダラー作成
const canvas = document.getElementById("myCanvas");
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
// シーンとカメラ作成
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 3, 6);
//*---------------------
// スカイドーム用
//*---------------------
const rgbeLoader = new RGBELoader();
const pmremGenerator = new THREE.PMREMGenerator(renderer);
pmremGenerator.compileEquirectangularShader();
rgbeLoader.load('./textures/cape_hill_1k.hdr', (texture) => {
const envMap = pmremGenerator.fromEquirectangular(texture).texture;
scene.environment = envMap; // 環境光マップ
scene.background = envMap; // 背景にも使う
texture.dispose();
pmremGenerator.dispose();
});
//*---------------------
// スカイドーム用ここまで
//*---------------------
//*---------------------
// 鳥用
//*---------------------
let model;
let mixer;
const clock = new THREE.Clock();
// モデル読み込み(鳥用)
const loader = new GLTFLoader();
loader.load(
"./models/bird/scene.gltf",
(gltf) => {
model = gltf.scene;
model.scale.set(0.01, 0.01, 0.01);
model.position.set(0, 0, 0);
scene.add(model);
if (gltf.animations.length > 0) {
mixer = new THREE.AnimationMixer(model);
const action = mixer.clipAction(gltf.animations[0]);
action.play();
} else {
console.warn("このモデルにはアニメーションが含まれていません");
}
},
undefined,
(error) => {
console.error("モデル読み込みエラー:", error);
}
);
//*---------------------
// 鳥用ここまで
//*---------------------
// ライト
const directionalLight = new THREE.DirectionalLight(0xffffff, 8); // 白色の方向光を作成 第二引数は光の強さ
directionalLight.position.set(0, 10, 10); // ライトの位置を設定
directionalLight.lookAt(0, 0, 0); // 追加で方向を合わせる
scene.add(directionalLight);
// OrbitControls
const controls = new OrbitControls(camera, renderer.domElement);
// ウィンドウリサイズ対応
window.addEventListener("resize", () => {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
});
// アニメーションループ
let goingForward = true; // 初期状態は前進
function animate() {
requestAnimationFrame(animate);
//*---------------------
// 鳥用
//*---------------------
const delta = clock.getDelta(); // ← フレーム間の経過時間
mixer.update(delta); // ← これを忘れると動きません!
// // 🐦 毎フレームちょっとずつZ方向に前進(例えば0.01ずつ)
if (model) {
if (goingForward) {
model.position.z -= 0.03;
model.rotation.y -= 0.01;
if (model.position.z < -8) goingForward = false; // 折り返し地点
} else {
model.position.z += 0.03;
model.rotation.y += 0.01;
if (model.position.z > 0) goingForward = true; // 戻りきったら再び前進
}
}
//*---------------------
// 鳥用ここまで
//*---------------------
controls.update();
renderer.render(scene, camera);
}
animate();
まとめ
いかがでしたでしょうか。
今回は3Dモデルを使用しましたが、3Dモデル自体を自作しても面白そうですね!
お疲れ様でした!