Three.jsでHTMLのbody内にシーンを追加する場合、new THREE.Scene();
で新たにシーンを作成しますが、1つのbody内に複数のシーンを作成して、スクロールなどで切り替えたい、というようなケースもあります。その場合、HTMLで要素にdata属性
を指定して複数のシーンを描画する、という方法を紹介します。
デモはこちら
data属性とは
HTML5では、<img src="photo.jpg" width="500" height="300">
のsrc
やwidth
,height
のような、標準で定められている「属性」の他に、独自の属性を開発者が付加できるdata属性
というものがあります。これは、接頭辞にdata-
をつけて、開発者が自由に名前をつけることができます。
<!-- div要素に`data-hoge`属性を追加 -->
<div data-hoge="piyo" id="fuga"> </div>
ここで指定したdata-hoge属性
は、JavaScriptで以下のようにして取得することができます。
let elem = document.getElementById("fuga");
console.log(elem.dataset.hoge);
// piyo と表示される
このdata属性を利用して、1つのbody要素の中に複数のThree.jsシーンを追加していきます。
HTMLにシーンの数だけ要素を作成
まず、作成したいシーンの数だけHTML上に要素を作成します。
今回は、box,corn,octahedronの3つのメッシュをそれぞれ1つ描画する3つのシーンを作成します。
それぞれの要素に、data-scene="box"
のようにdata属性を追加しておきます。
<!--boxを描画するシーン-->
<div data-scene="box">
<div class="wrap">
<h2>Box</h2>
</div>
</div>
<!--cornを描画するシーン-->
<div data-scene="corn">
<div class="wrap">
<h2>Corn</h2>
</div>
</div>
<!--octahedronを描画するシーン-->
<div data-scene="Octahedron">
<div class="wrap">
<h2>Octahedron</h2>
</div>
</div>
JavaScriptで配列として各シーンを定義
1つだけのシーンを作成
次に、JavaScriptでThree.jsのシーンを定義していきます。
まずは、body要素に1つのシーンだけを作成し、Boxを1個だけ描画してみます。
function init(){
//レンダラー
const renderer = new THREE.WebGLRenderer({
antialias:true,
});
renderer.setSize(window.innerWidth, window.innerHeight);
//data-scene属性を持つ要素にレンダラーを追加
const elem = document.querySelector("[data-scene]");
elem.appendChild(renderer.domElement);
//シーン
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x333333);
//カメラ
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 0, 10);
camera.lookAt(0,0,0);
scene.add(camera);
//ライティング
const ambientLight = new THREE.AmbientLight(0xffffff, 0.3);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff);
camera.add(directionalLight);
//Boxを1つだけシーンに追加
const geometry = new THREE.BoxGeometry(2,2,2);
const material = new THREE.MeshPhongMaterial({
color: 0xff0000,
});
let mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
//アニメーション用の時間
let time = new THREE.Clock();
render();
//毎フレーム実行されるレンダー関数
function render(){
requestAnimationFrame(render);
renderer.render(scene, camera);
mesh.rotation.x += Math.cos(time.getElapsedTime()) * 0.01;
mesh.rotation.y += Math.sin(time.getElapsedTime()) * 0.01;
}
}
//エントリーポイント
window.addEventListener("DOMContentLoaded", init);
Three.jsの基本的なコードですね。
ここから、複数のシーンを追加していきます。
複数のシーンを作成
data属性を利用して複数のシーンを描画する手順は、大まかに以下の流れになります。
- シーンをレンダリングする1つのレンダラーを用意する
- シーンを作成する関数
makeScene()
を作成する - 各シーンをオブジェクトの配列として定義し、
makeScene()
を実行する - HTMLから
data-scene属性
を持つ全ての要素を取得し、それぞれに各シーンを割り当てて行く - ブラウザの表示領域に入ったシーンだけを、レンダラーを使ってレンダリングする
まず、レンダラーを1つだけ用意します。
const renderer = new THREE.WebGLRenderer({
antialias:true,
});
次に、シーンを作成し戻り値としてsceneとcameraを返す関数makeScene()
を定義します。
1つだけのシーンを描画する場合、sceneとcameraは1つずつあれば事足りますが、
今回は複数のシーンを描画するので、関数として定義しておいて、各シーン内の作成時に関数として利用できるようにしておきます。
function makeScene(){
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, 2, 0.1, 1000);
camera.position.set(0, 0, 10);
camera.lookAt(0,0,0);
scene.add(camera);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.3);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff);
camera.add(directionalLight);
return {scene, camera};
}
次に、個別のシーンをオブジェクトの配列sceneInitFunctionsByName
として定義します。
この中で、先程作成したmakeScene()
関数を呼び出してシーンを作成します。
//各シーンを定義
const sceneInitFunctionsByName = {
//boxを描画するシーン
"box":() => {
const {scene, camera} = makeScene();
scene.background = new THREE.Color(0x333333);
const geometry = new THREE.BoxGeometry(2,2,2);
const material = new THREE.MeshPhongMaterial({
color: 0xff0000,
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
return (time, rect) => {
mesh.rotation.x = time;
mesh.rotation.y = time;
camera.aspect = rect.width / rect.height;
camera.updateProjectionMatrix();
renderer.render(scene, camera);
};
},
//cornを描画するシーン
"corn":() => {
const {scene, camera} = makeScene();
scene.background = new THREE.Color(0x111111);
const geometry = new THREE.ConeGeometry(1, 3, 8);;
const material = new THREE.MeshPhongMaterial({
color: 0x00ff00,
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
return (time, rect) => {
mesh.rotation.x = time;
mesh.rotation.z = time;
camera.aspect = rect.width / rect.height;
camera.updateProjectionMatrix();
renderer.render(scene, camera);
}
},
//octahedronを描画するシーン
"octahedron":() => {
const {scene, camera} = makeScene();
scene.background = new THREE.Color(0x222222);
const geometry = new THREE.OctahedronGeometry(2, 0);;
const material = new THREE.MeshPhongMaterial({
color: 0x0000ff,
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
return (time, rect) => {
mesh.rotation.x = time;
mesh.rotation.z = time;
camera.aspect = rect.width / rect.height;
camera.updateProjectionMatrix();
renderer.render(scene, camera);
}
}
}
次に、body要素からdata-scene属性
を持つすべての要素を探してきて、同じ値を持つシーンを割り当てます。
//全てのシーンを管理する配列
const sceneElements = [];
//配列にシーンを追加する関数
function addScene(elem, fn) {
const ctx = document.createElement("canvas").getContext("2d");
elem.appendChild(ctx.canvas);
sceneElements.push({elem, ctx, fn});
}
//HTMLから全てのdata-sceneを探してきて、要素の数だけシーンを描画
document.querySelectorAll("[data-scene]").forEach((elem) => {
//data-scene属性の値から、シーンの名前を取得する(box, corn, octahedron)
const sceneName = elem.dataset.scene;
//data-scene属性の値と同じ名前を持つシーンをsceneInitFunctionに代入する
const sceneInitFunction = sceneInitFunctionsByName[sceneName];
//data-scene属性を持つ要素内に、同じ名前のシーンを作成する
const sceneRenderFunction = sceneInitFunction(elem);
addScene(elem, sceneRenderFunction);
});
最後に、ブラウザの表示領域内(ViewPort)に入っているdata-scene属性
を持つ要素のシーンだけを描画するように、render()
関数を定義します。
//毎フレーム実行されるレンダー関数
function render(time){
time *= 0.001;
for (const {elem, fn, ctx} of sceneElements) {
//ブラウザで表示される領域(ViewPort)を取得
let rect = elem.getBoundingClientRect();
const {left, right, top, bottom, width, height} = rect;
const rendererCanvas = renderer.domElement;
// ViewPort内に収まっていない場合は、isOffscreenフラグを立てる
const isOffscreen =
bottom < 0 ||
top > window.innerHeight ||
right < 0 ||
left > window.innerWidth;
// isOfscreenフラグがない = ViewPort内に表示されていれば、シーンをレンダリングする
if (!isOffscreen) {
if (rendererCanvas.width < width || rendererCanvas.height < height) {
renderer.setSize(width, height, false);
}
if (ctx.canvas.width !== width || ctx.canvas.height !== height) {
ctx.canvas.width = width;
ctx.canvas.height = height;
}
renderer.setScissor(0, 0, width, height);
renderer.setViewport(0, 0, width, height);
fn(time, rect);
ctx.globalCompositeOperation = 'copy';
ctx.drawImage(
rendererCanvas,
0, rendererCanvas.height - height, width, height,
0, 0, width, height);
}
}
requestAnimationFrame(render);
}
これで、ページの読み込み時にrender関数
を呼び出せば、JavaScriptは完成です。
window.addEventListener("DOMContentLoaded", render);
最後に、作成したcanvas要素がフルスクリーンで表示されるように調整して終了です。
CSSでスクロール時の挙動を調整
*{
box-sizing: border-box;
margin: 0;
padding: 0;
list-style: none;
}
canvas{
width: 100%;
height: 100%;
}
*[data-scene]{
width: 100%;
height: 100vh;
position: relative;
z-index: 1;
.wrap{
position: absolute;
top: 0;
right: 1%;
left: 1%;
max-width: 700px;
height: 100%; //これ大事
margin: 0 auto;
padding: 2.0rem;
overflow-y: scroll;
&::-webkit-scrollbar{
display: none;
}
color: #ccc;
h3{
margin-top: 60px;
}
}
}
これで、1つのbody内に複数のシーンを描画できました。
ここからパララックスや慣性スクロールなどのインタラクションをつけていくと、Three.jsを利用したリッチなページを作成できるのではないかと思います!
全体のコードおよびデモはこちら