概要
このブログでは、Three.jsを使用してクリスマスツリーを描画する方法を解説します。3Dグラフィックスの基本から、木や装飾、星などの構築までをステップバイステップで進めていきます。
今回は事前にモデリングしたデータを読み込んだりテクスチャを使うことはせずに、ジオメトリの図形を使って描画します。再現度はあまり高くありませんがご容赦ください。
先に完成イメージを載せておきます。
ジオメトリとは
three.js のジオメトリは、3Dシーンによく使われる図形を描画するために用意されている基本的な形状のデータです。THREE.BoxGeometry, THREE.SphereGeometry, THREE.CylinderGeometry などがその例です。
これらは、単なる形状データ(頂点や面の情報)を提供するもので、これらに対して材質(色、テクスチャ、光沢など)を適用して、実際に表示されるオブジェクトになります。
具体的には、ジオメトリをgeometry
、材質をmaterial
として定義した後、 new THREE.Mesh(geometry, material);
とすることで、3Dオブジェクトとして扱うことができます。
主なジオメトリには以下のものがあります。
基本的な幾何形状:
- THREE.BoxGeometry: 箱
- THREE.SphereGeometry: 球
- THREE.CylinderGeometry: 円柱
- THREE.ConeGeometry: 円錐
- THREE.PlaneGeometry: 平面
- THREE.RingGeometry: 輪っか
- THREE.CircleGeometry: 円
複雑な形状:
- THREE.BufferGeometry: より柔軟なデータ構造で、頂点データなどを直接操作して複雑な形状を定義できます
- THREE.LatheGeometry: 特定のパスを回転させて形状を作るときに使用されます
- THREE.TubeGeometry: 特定のパスに沿って円形を展開して形状を作るときに使用されます
- THREE.ExtrudeGeometry: 2D形状を3Dの立体にするときに使用されます
ジオメトリを使うメリットは、基本的な形を比較的簡単に3Dのシーンに配置することができます。また、複雑な形状をシンプルな形状を組み合わせることで作成することもできます。
必要な準備
1. 開発環境の準備
- HTMLファイル: 基本的なHTMLファイルを作成します。
- Three.jsライブラリ: CDN経由でThree.jsをインポートします。
以下はThree.jsを扱う際に基本となるHTMLファイルの書き方です。
Three.jsはモジュールをインポートして使うことを推奨していますので、
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Christmas Tree</title>
<style>
body { margin: 0; }
canvas { display: block; }
</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.167.0/build/three.module.js",
"OrbitControls": "https://cdn.jsdelivr.net/npm/three@0.167.0/examples/jsm/controls/OrbitControls.js"
}
}
</script>
</head>
<body>
<script type="module">
// ここに3D空間の描画の処理を書いていく
</script>
ステップ1: シーンとカメラのセットアップ
3Dオブジェクトを描画するために、シーン、カメラ、レンダラーを設定します。3D空間を照らすライトがないと描画した3Dオブジェクトが見えないので、忘れずに定義しましょう。ライトは、今回は簡易的に、空間全体を明るく照らす AmbientLight
とStandardMaterialの反射の質感を出すために DirectionalLight
を使用します。マウスで空間をコントロールできるように、OrbitControls
も作成しておきます。
setupCameraAndRenderer() {
const fieldOfView = 75; // カメラの視野角
const aspectRatio = window.innerWidth / window.innerHeight;
const nearPlane = 0.1;
const farPlane = 1000; // カメラの遠近感
this.camera = new THREE.PerspectiveCamera(fieldOfView, aspectRatio, nearPlane, farPlane);
this.camera.position.set(0, 15, 25);
this.renderer = new THREE.WebGLRenderer();
this.renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(this.renderer.domElement);
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
const ambientLightIntensity = 2.5;
const ambientLightColor = 0xffffff;
const ambientLight = new THREE.AmbientLight(ambientLightColor, ambientLightIntensity);
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight.position.set(10, 20, 10);
this.scene.add(directionalLight);
}
ステップ2: 空の作成
空もジオメトリで作れます。球のジオメトリを作って水色にし、空間全体を覆うようにします。
createSky() {
const skyGeometry = new THREE.SphereGeometry(100, 32, 32);
const skyMaterial = new THREE.MeshBasicMaterial({ color: 0x87ceeb, side: THREE.BackSide });
const sky = new THREE.Mesh(skyGeometry, skyMaterial);
this.scene.add(sky);
}
ステップ3: 地面の作成
木が立つ地面を作成します。ジオメトリのPlaneを使って描画します。
雪が降っている想定で、地面の色は白くしておきます。
createGround(groundSize) {
const groundGeometry = new THREE.PlaneGeometry(groundSize, groundSize);
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0xffffff,
roughness: 0.3,
side: THREE.DoubleSide,
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
this.scene.add(ground);
}
ステップ4: 木の幹と枝の作成
木の幹と枝を構築します。幹と枝はジオメトリの円柱で作り、色は簡易的に茶色っぽく設定しました。
createBranch(length, thickness) {
const branchMaterial = new THREE.MeshStandardMaterial({ color: 0x228B22 });
const branchGeometry = new THREE.CylinderGeometry(thickness, thickness / 4, length, 8);
branchGeometry.translate(0, -length / 2, 0);
branchGeometry.rotateZ(Math.PI / 2);
return new THREE.Mesh(branchGeometry, branchMaterial);
}
createTree(treeHeight, trunkRadius, trunkOffsetY) {
const tree = new THREE.Group();
const trunkScale = 0.1; // 幹のスケール
const trunk = new THREE.Mesh(
new THREE.CylinderGeometry(trunkRadius * trunkScale * 0.01, trunkRadius * trunkScale, treeHeight, 8),
new THREE.MeshStandardMaterial({ color: 0x8B4513 })
);
trunk.position.y = treeHeight / 2;
tree.add(trunk);
const numBranchLevels = Math.floor(treeHeight / 0.5);
const lengthScale = 0.8;
for (let i = trunkOffsetY; i <= numBranchLevels; i++) {
const y = (i / (numBranchLevels + trunkOffsetY)) * treeHeight + trunkOffsetY / 2;
const levelRadius = trunkRadius * (1 - i / numBranchLevels);
const branchLength = levelRadius * lengthScale;
const branchCount = 8 + Math.floor(Math.random() * 5);
for (let j = 0; j < branchCount; j++) {
const angle = (j / branchCount) * Math.PI * 2;
const branch = this.createBranch(branchLength, levelRadius * 0.08);
branch.position.set(Math.cos(angle) * levelRadius * 0.1, y, Math.sin(angle) * levelRadius * 0.1);
branch.rotation.z = angle;
branch.rotation.x = Math.PI / 2 - (i / numBranchLevels) * Math.PI / 4;
tree.add(branch);
}
}
this.scene.add(tree);
}
ステップ5: ツリーの装飾を追加
カラフルなオーナメント(飾り)をツリーに追加します。オーナメントはジオメトリの球で作成し、ランダムな位置と色で配置します。
createOrnaments(treeHeight, offsetY) {
const ornaments = new THREE.Group();
const ornamentColors = [0xaa0000, 0x0055aa, 0xaaaa00];
const ornamentRadius = 0.4;
const rows = 5; // 飾りの行数
const baseRadius = 4; // 基本的な半径 (調整可能)
const ornamentSpacing = 1.2; // 飾り間隔
for (let i = 0; i < rows; i++) {
const numOrnamentsPerRow = i * 2 + 2; // 行ごとの飾りの数
for (let j = 0; j < numOrnamentsPerRow; j++) {
const color = Math.random() * ornamentColors.length;
const ornament = new THREE.Mesh(
new THREE.SphereGeometry(ornamentRadius, 16, 16),
new THREE.MeshStandardMaterial({
color: ornamentColors[color | 0],
roughness: 0.5, // 鏡面反射の度合い
metalness: 0.7, // 金属っぽい質感を出す
})
);
// 円周上の均等な位置に飾りを配置
const angle = (i / numOrnamentsPerRow) * Math.PI * 2 + (j / numOrnamentsPerRow) * Math.PI * 2;
const distanceFromTree = baseRadius / (rows - i); // ツリーの周囲に配置する半径(調整可能)
const x = Math.cos(angle) * distanceFromTree;
const z = Math.sin(angle) * distanceFromTree;
const yScale = 0.8; // 飾りの高さのスケール(調整可能)
const yJitter = 0.5; // 飾りの高さのランダム要素(調整可能)
const y = (treeHeight * yScale) - (treeHeight * yScale - offsetY) * (i / rows) + (Math.random() * yJitter * 2 - yJitter); // ツリーの高さに合わせて飾りの高さを調整
ornament.position.set(x, y, z);
ornaments.add(ornament);
}
}
ornaments.position.y = 0; // ツリーのベースに合わせ調整
this.scene.add(ornaments);
}
ステップ6: 雲を追加
もう少しクリスマスツリーぽくするために、ツリーの周囲に雲を作ります。雲はジオメトリの球で作成し、螺旋状に木に配置します。
createClouds(treeHeight, offsetY) {
const cloudGeometry = new THREE.SphereGeometry(0.2, 32, 32);
const cloudMaterial = new THREE.MeshStandardMaterial({ color: 0xeeeeee, opacity: 0.9, transparent: true });
const numClouds = 120; // 雲の数
const cloudGroup = new THREE.Group();
this.scene.add(cloudGroup);
for (let i = 0; i < numClouds; i++) {
const cloud = new THREE.Mesh(cloudGeometry, cloudMaterial);
const cloudRadius = 0.8 - (i / (numClouds - 1)) * 0.6; // 螺旋の半径
const cloudScale = cloudRadius * 3.6; // 雲のサイズ調整
cloud.scale.set(cloudScale, cloudScale, cloudScale);
const angle = i * 0.3; // 螺旋の回転角度(調整可能)
const cloudHeightScale = 0.9; // 木に対する雲の高さのスケール(調整可能)
// 雲の高さ
const cloudHeight = offsetY + (treeHeight * cloudHeightScale - offsetY) * (i / (numClouds)); // ツリーの高さに合わせて雲の高さを調整
// 雲のX、Y座標を螺旋状に配置
const treeRadius = 4.0; // ツリーの周囲に雲を配置する半径(調整可能)
const cloudX = treeRadius * Math.cos(angle) * (numClouds - i) / numClouds;
const cloudY = treeRadius * Math.sin(angle) * (numClouds - i) / numClouds;
cloud.position.set(cloudX, cloudY, cloudHeight);
cloudGroup.add(cloud);
}
//雲の全体の回転と配置調整
cloudGroup.rotation.x = -Math.PI / 2; //全体の回転調整
cloudGroup.position.y = 0; //調整
}
ステップ7: 星の追加
ツリーの上部に星を追加します。少し形状が複雑なので、ジオメトリではなく、頂点とベジェ曲線を使用して 2D のパスを表現する方法も試します。THREE.Shape
を使って星形を定義し、THREE.ExtrudeGeometry
を使って3Dオブジェクトに変換します。
createStar(treeHeight) {
const createStarShape = (innerRadius, outerRadius, numPoints, scale) => {
const shape = new THREE.Shape();
const angleStep = Math.PI / numPoints;
for (let i = 0; i <= numPoints * 2; i++) {
const angle = i * angleStep;
const radius = i % 2 === 0 ? outerRadius * scale : innerRadius * scale;
const x = Math.sin(angle) * radius;
const y = Math.cos(angle) * radius;
shape[i === 0 ? 'moveTo' : 'lineTo'](x, y);
}
return shape;
};
const starShape = createStarShape(0.5, 1, 5, 0.6);
const starGeometry = new THREE.ExtrudeGeometry(starShape, { depth: 0.1, bevelEnabled: false });
const starMaterial = new THREE.MeshStandardMaterial({
color: 0xffff00,
roughness: 0.3,
metalness: 0.7
});
const star = new THREE.Mesh(starGeometry, starMaterial);
star.position.y = treeHeight;
this.scene.add(star);
}
ステップ8: アニメーション化
最後に、シーン全体をアニメーション化してマウスでコントロールできるようにします。requestAnimationFrame
を使用してループ処理を行い、カメラやオブジェクトの動きを更新します。
function animate() {
requestAnimationFrame(animate);
// カメラやオブジェクトの更新処理(必要なら回転など)
controls.update(); // カメラ操作のスムーズさを維持
renderer.render(scene, camera); // シーンとカメラを描画
}
animate(); // アニメーション開始
完成したコード
以上でクリスマスツリーは完成です。完成版コードも載せておきます。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Christmas Tree</title>
<style>
body { margin: 0; }
canvas { display: block; }
</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.167.0/build/three.module.js",
"OrbitControls": "https://cdn.jsdelivr.net/npm/three@0.167.0/examples/jsm/controls/OrbitControls.js"
}
}
</script>
</head>
<body>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'OrbitControls';
class ChristmasTree {
constructor() {
this.scene = new THREE.Scene();
const groundSize = 60;
const treeHeight = 16;
const trunkRadius = 6; // 幹の半径
const trunkOffsetY = 3; // 幹のオフセット
this.createGround(groundSize);
this.createTree(treeHeight, trunkRadius, trunkOffsetY);
this.createOrnaments(treeHeight, trunkOffsetY);
this.createCloud(treeHeight, trunkOffsetY);
this.createStar(treeHeight);
this.createSky();
this.init();
}
init() {
this.setupCameraAndRenderer();
this.animate();
}
setupCameraAndRenderer() {
const fieldOfView = 75; // カメラの視野角
const aspectRatio = window.innerWidth / window.innerHeight;
const nearPlane = 0.1;
const farPlane = 1000; // カメラの遠近感
this.camera = new THREE.PerspectiveCamera(fieldOfView, aspectRatio, nearPlane, farPlane);
this.camera.position.set(0, 25, 25);
this.renderer = new THREE.WebGLRenderer();
this.renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(this.renderer.domElement);
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
const ambientLightIntensity = 2.5;
const ambientLightColor = 0xffffff;
const ambientLight = new THREE.AmbientLight(ambientLightColor, ambientLightIntensity);
this.scene.add(ambientLight);
const light = new THREE.DirectionalLight(0xffffff, 1.5);
light.position.set(10, 20, 10);
this.scene.add(light);
}
createGround(groundSize) {
const groundGeometry = new THREE.PlaneGeometry(groundSize, groundSize);
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0xffffff,
roughness: 0.3,
side: THREE.DoubleSide,
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
this.scene.add(ground);
}
createBranch(length, thickness) {
const branchMaterial = new THREE.MeshStandardMaterial({ color: 0x228B22 });
const branchGeometry = new THREE.CylinderGeometry(thickness, thickness / 4, length, 8);
branchGeometry.translate(0, -length / 2, 0);
branchGeometry.rotateZ(Math.PI / 2);
return new THREE.Mesh(branchGeometry, branchMaterial);
}
createTree(treeHeight, trunkRadius, trunkOffsetY) {
const tree = new THREE.Group();
const trunkScale = 0.1; // 幹のスケール
const trunk = new THREE.Mesh(
new THREE.CylinderGeometry(trunkRadius * trunkScale * 0.01, trunkRadius * trunkScale, treeHeight, 8),
new THREE.MeshStandardMaterial({ color: 0x8B4513 })
);
trunk.position.y = treeHeight / 2;
tree.add(trunk);
const numBranchLevels = Math.floor(treeHeight / 0.5);
const lengthScale = 0.8;
for (let i = trunkOffsetY; i <= numBranchLevels; i++) {
const y = (i / (numBranchLevels + trunkOffsetY)) * treeHeight + trunkOffsetY / 2;
const levelRadius = trunkRadius * (1 - i / numBranchLevels);
const branchLength = levelRadius * lengthScale;
const branchCount = 8 + Math.floor(Math.random() * 5);
for (let j = 0; j < branchCount; j++) {
const angle = (j / branchCount) * Math.PI * 2;
const branch = this.createBranch(branchLength, levelRadius * 0.08);
branch.position.set(Math.cos(angle) * levelRadius * 0.1, y, Math.sin(angle) * levelRadius * 0.1);
branch.rotation.z = angle;
branch.rotation.x = Math.PI / 2 - (i / numBranchLevels) * Math.PI / 4;
tree.add(branch);
}
}
this.scene.add(tree);
}
createOrnaments(treeHeight, offsetY) {
const ornaments = new THREE.Group();
const ornamentColors = [0xaa0000, 0x0055aa, 0xaaaa00];
const ornamentRadius = 0.4;
const rows = 5; // 飾りの行数
const baseRadius = 4; // 基本的な半径 (調整可能)
const ornamentSpacing = 1.2; // 飾り間隔
for (let i = 0; i < rows; i++) {
const numOrnamentsPerRow = i * 2 + 2; // 行ごとの飾りの数
for (let j = 0; j < numOrnamentsPerRow; j++) {
const color = Math.random() * ornamentColors.length;
const ornament = new THREE.Mesh(
new THREE.SphereGeometry(ornamentRadius, 16, 16),
new THREE.MeshStandardMaterial({
color: ornamentColors[color | 0],
roughness: 0.5, // 鏡面反射の度合い
metalness: 0.7, // 金属っぽい質感を出す
})
);
// 円周上の均等な位置に飾りを配置
const angle = (i / numOrnamentsPerRow) * Math.PI * 2 + (j / numOrnamentsPerRow) * Math.PI * 2;
const distanceFromTree = baseRadius / (rows - i); // ツリーの周囲に配置する半径(調整可能)
const x = Math.cos(angle) * distanceFromTree;
const z = Math.sin(angle) * distanceFromTree;
const yScale = 0.8; // 飾りの高さのスケール(調整可能)
const yJitter = 0.5; // 飾りの高さのランダム要素(調整可能)
const y = (treeHeight * yScale) - (treeHeight * yScale - offsetY) * (i / rows) + (Math.random() * yJitter * 2 - yJitter); // ツリーの高さに合わせて飾りの高さを調整
ornament.position.set(x, y, z);
ornaments.add(ornament);
}
}
ornaments.position.y = 0; // ツリーのベースに合わせ調整
this.scene.add(ornaments);
}
createCloud(treeHeight, offsetY) {
const cloudGeometry = new THREE.SphereGeometry(0.2, 32, 32);
const cloudMaterial = new THREE.MeshStandardMaterial({ color: 0xeeeeee, opacity: 0.9, transparent: true });
const numClouds = 120; // 雲の数
const cloudGroup = new THREE.Group();
this.scene.add(cloudGroup);
for (let i = 0; i < numClouds; i++) {
const cloud = new THREE.Mesh(cloudGeometry, cloudMaterial);
const cloudRadius = 0.8 - (i / (numClouds - 1)) * 0.6; // 螺旋の半径
const cloudScale = cloudRadius * 3.6; // 雲のサイズ調整
cloud.scale.set(cloudScale, cloudScale, cloudScale);
const angle = i * 0.3; // 螺旋の回転角度(調整可能)
const cloudHeightScale = 0.9; // 木に対する雲の高さのスケール(調整可能)
// 雲の高さ
const cloudHeight = offsetY + (treeHeight * cloudHeightScale - offsetY) * (i / (numClouds)); // ツリーの高さに合わせて雲の高さを調整
// 雲のX、Y座標を螺旋状に配置
const treeRadius = 4.0; // ツリーの周囲に雲を配置する半径(調整可能)
const cloudX = treeRadius * Math.cos(angle) * (numClouds - i) / numClouds;
const cloudY = treeRadius * Math.sin(angle) * (numClouds - i) / numClouds;
cloud.position.set(cloudX, cloudY, cloudHeight);
cloudGroup.add(cloud);
}
//雲の全体の回転と配置調整
cloudGroup.rotation.x = -Math.PI / 2; //全体の回転調整
cloudGroup.position.y = 0; //調整
}
createStar(treeHeight) {
const createStarShape = (innerRadius, outerRadius, numPoints, scale) => {
const shape = new THREE.Shape();
const angleStep = Math.PI / numPoints;
for (let i = 0; i <= numPoints * 2; i++) {
const angle = i * angleStep;
const radius = i % 2 === 0 ? outerRadius * scale : innerRadius * scale;
const x = Math.sin(angle) * radius;
const y = Math.cos(angle) * radius;
shape[i === 0 ? 'moveTo' : 'lineTo'](x, y);
}
return shape;
};
const starShape = createStarShape(0.5, 1, 5, 0.6);
const starGeometry = new THREE.ExtrudeGeometry(starShape, { depth: 0.1, bevelEnabled: false });
const starMaterial = new THREE.MeshStandardMaterial({
color: 0xffff00,
roughness: 0.3,
metalness: 0.7
});
const star = new THREE.Mesh(starGeometry, starMaterial);
star.position.y = treeHeight;
this.scene.add(star);
}
createSky() {
const skyGeometry = new THREE.SphereGeometry(100, 32, 32);
const skyMaterial = new THREE.MeshBasicMaterial({ color: 0x87ceeb, side: THREE.BackSide });
const sky = new THREE.Mesh(skyGeometry, skyMaterial);
this.scene.add(sky);
}
animate() {
requestAnimationFrame(() => this.animate());
this.controls.update();
this.renderer.render(this.scene, this.camera);
}
}
const tree = new ChristmasTree();
</script>
</body>
</html>
まとめ
Three.jsで、シンプルなクリスマスツリーを作成してみました。ジオメトリの形状を組み合わせることで、3Dオブジェクトを表現する方法が分かったかと思います。今回の例では、本格的な表現を目指していませんが、なにかの形状を作ってみるのはThree.jsの基本的な操作を学ぶには良い練習問題になると思います。より高度な表現を目指したい方は、テクスチャやモデリングの知識を深めてみてください。メリークリスマス!