最近娘の風邪をもらって1週間くらい発熱して寝込んでいたフロントエンドチームの吉田です。
今回は数年前に自分で作ったThree.jsを使った構築の一部を紹介したいと思います。
目次
Three.js とは
まずThree.jsって何かというと、割と簡単に3Dを使ったコンテンツを制作できるJavaScriptライブラリです。JavaScriptの知識があれば扱えるのでお手軽ですよね。
今ではいろんなサイトでも使われており、環境によっては動作が重たかったりもしますが、上手く取り入れることで印象に残るコンテンツを制作することができます。
構築
本記事では、以下のような北海道を出力するところまでやっていきたいと思います。
まずは構築のための準備をしていきます。
HTMLの作成
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
</head>
<body>
<canvas id="canvas"></canvas>
<script src="https://unpkg.com/three@0.137.4/build/three.min.js"></script>
<script src="https://unpkg.com/three@0.137.4/examples/js/controls/OrbitControls.js"></script>
<script src="https://unpkg.com/three@0.137.4/examples/js/loaders/MTLLoader.js"></script>
<script src="https://unpkg.com/three@0.137.4/examples/js/loaders/OBJLoader.js"></script>
<script src="./assets/js/index.js"></script>
</body>
</html>
CDNで提供されているURLを使用しています。お好みで公式サイトからダウンロードしてきても良いと思います。今回は北海道のモデルをOBJで用意したため、OBJファイルを読み込むためのOBJLoaderとマテリアルを読み込むためのMTLLoaderを使いますので、それぞれのJSを読み込んでいます。
JavaScriptの作成
HTMLの用意ができたらJavaScriptを書いていきます。
まずは3Dを表示させるために必要なレンダラー、シーン、カメラ、ライトを作っていきます。
レンダラー
const container = document.getElementById('canvas')
// RENDRER
// ----------------------------------------
const renderer = new THREE.WebGLRenderer({ antialias: true, canvas: container })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
antialiasは物体の輪郭がギザギザになることを抑える処理でtrueにしてます。canvasにHTMLで用意したcanvasを渡します。
デフォルトだとサイズが小さいのでsetSizeで表示領域を設定します。
setPixelRatioでデバイスピクセル比を設定しておくことでモバイルで見たときも綺麗に表示されます。
シーン
// SCENE
// ----------------------------------------
const scene = new THREE.Scene()
作成したシーンにこの後作っていくカメラやライト、モデルなどを追加していきます。
カメラ
// CAMERA
// ----------------------------------------
// new THREE.PerspectiveCamera(画角, アスペクト比, 描画開始距離, 描画終了距離)
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 20000)
camera.position.set(0, 70, 100)
このカメラに映るものがcanvasに描画されます。
カメラ制御
// CONTROLS
// ----------------------------------------
const controls = new THREE.MapControls(camera, renderer.domElement)
controls.enableRotate = false
controls.enableDamping = true
controls.dampingFactor = 0.1
controls.maxDistance = 140
controls.minDistance = 80
enableDampingはドラッグするときのアニメーションを滑らかにするかどうかで、dampingFactorで度合いを決めます。
maxDistanceとminDistanceではズームの最大値、最小値を設定します。
ライト
// LIGHT
// ----------------------------------------
const dirLight = new THREE.DirectionalLight(0xffffff, 0.6)
dirLight.position.set(15, 30, 20)
dirLight.position.multiplyScalar(10)
scene.add(dirLight)
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x7b8d42, 0.6)
hemiLight.position.set(0, 50, 0)
scene.add(hemiLight)
ライトがないと真っ暗なままなので作っていきます。今回はDirectionalLightとHemisphereLightの2種類を作りました。
DirectionalLightは特定の方向に向かって放射され、どこまでも遠くまで照らしてくれます。オブジェクトの位置に関係なく光が当たります。
HemisphereLightは上からの光の色と下からの光の色を分けられるライトです。
地面の作成
環境の準備ができましたので、次はオブジェクトを生成・設置していきます。まずは地面を作っていきます。
// GROUND
// ----------------------------------------
const ground = new THREE.Mesh(new THREE.PlaneGeometry(20000, 20000), new THREE.MeshPhongMaterial({ color: 0x7b8d42 }))
ground.rotation.x = radian(-90)
scene.add(ground)
new THREE.PlaneGeometryで平面を作ります。デフォルトだとカメラに対して正面に平面が生成されているため、rotationで角度を変更します。radian()は角度を馴染みある90度や180度で扱えるようにするために用意した関数です。
function radian(angle) {
return (Math.PI / 180) * angle
}
角度を変更して地面に見えるようになったら、シーンに追加します。
3Dモデルの読み込み
北海道をThree.jsで作るのは大変なので、Blender等で予め作成していた3Dモデルを読み込みます。まずは読み込み準備をします。
const textureLoader = new THREE.TextureLoader()
const mtlLoader = new THREE.MTLLoader()
const objLoader = new THREE.OBJLoader()
今回は北海道のモデルと影用のテクスチャの2つを用意してます。Three.jsでも影を生成できますが、荒かったりしますし精度を上げると重くなるので、今回はテクスチャで表現します。
const hokkaidoShadow = new THREE.Mesh(
new THREE.PlaneGeometry(200, 120),
new THREE.MeshLambertMaterial({
map: textureLoader.load('./assets/textures/hokkaido_shadowmap.png'),
transparent: true,
})
)
hokkaidoShadow.rotation.x = radian(-90)
hokkaidoShadow.position.set(-0.5, 0.08, -0.4)
scene.add(hokkaidoShadow)
// hokkaido
const hokkaidoMesh = objLoader.load('./assets/models/hokkaido.obj', (obj) => {
obj.traverse((child) => {
const mesh = child
if (mesh.isMesh && mesh.name.match(/flag_base/)) {
mesh.material = new THREE.MeshLambertMaterial({ color: 0x6b3f31 })
} else if (mesh.isMesh && mesh.name.match(/flag_pole/)) {
mesh.material = new THREE.MeshLambertMaterial({ color: 0xc9caca })
} else if (mesh.isMesh && mesh.name.match(/flag_cloth/)) {
mesh.material = new THREE.MeshLambertMaterial({ color: 0xfcc800 })
} else {
mesh.material = new THREE.MeshLambertMaterial({ color: 0xffffff })
}
})
// setting & add
obj.position.set(0, -1, 0)
obj.scale.x = obj.scale.y = obj.scale.z = 1
scene.add(obj)
})
影は地面と同じような平面を生成し、そこにテクスチャを貼っています。MeshLambertMaterialのmapにテクスチャのパスを設定、transparentで透過させてます。あとは角度や位置を調整してシーンに追加します。
北海道のモデルはOBJLoaderを使います。まずはモデルを読み込み、その後各パーツに名前を振ってあるので、そのパーツにそれぞれ着色していきます。その後は影と同じく位置や大きさを調整してシーンに追加します。
アニメーション
requestAnimationFrame()でrenderer.render()を実行してアニメーションを設定します。
// ANIMATE
// ----------------------------------------
animate()
function animate() {
requestAnimationFrame(animate)
controls.update()
renderer.render(scene, camera)
}
設定できると以下のようにドラッグで動かすことができるようになります。
リサイズ処理
画面サイズを変更したときにcanvasのサイズも変わるように設定しておきます。
// RESIZE EVENT
// --------------------------------------------------
onWindowResize()
window.addEventListener('resize', onWindowResize)
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
}
さいごに
今回はThree.jsの基本的な部分での紹介でした。ここにRaycaster()を使ったりモデルのアニメーションの実装など様々なことができますが、それは次回以降で紹介できればと思います。全体を3Dで作らなくてもページの一部に使うことで表現の幅は広がっていくと思いますので、興味のある方は触ってみてください!