ブラウザ上で3次元・2次元のオブジェクトやグラフィックをレンダリングできるWebGL(Webグラフィックライブラリ)。そのWebGLを簡単に扱えるライブラリ「three.js」というものを用い、ウェブサイトに3Dオブジェクトやインタラクティブなアニメーションを取り入れ、表現を少し工夫してみようと思います。
今回は、この記事を通して以下のような 見出しの後ろに十二面体のオブジェクトがあるウェブページ を作ってみます。JavaScriptをある程度触ったことのある方であれば、比較的簡単に導入することができると思うので、ぜひ一読してみてください。
記事内の画像はすべてフリー画像素材サイト Pixabayで取得しています。
目次
1. HTMLとCSSで大枠を作成
今回は記事の見出しエリアをイメージして、HTMLとCSSでこのような見た目のページをコーディングをしました。
HTML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>three.js</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r127/three.min.js"></script> <script src="js/common.js"></script> <link href="css/style.css" rel="stylesheet" media="all"> </head> <body> <section class="kv-block outer-block"> <h1 class="main-title">Think different.</h1> <canvas id="canvas" class="fadeTarget"></canvas> <div id="kv" class="fadeTarget"></div> </section> <section class="outer-block contents-block"> <div class="inner-block"> <h2 class="title">タイトルタイトルタイトルタイトルタイトル</h2> <p class="text">テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p> <p class="text"> テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト </p> <p class="text">テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p> <p class="text"> テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト </p> </div> </section> </body> </html> |
見出しの部分はsection.kv-blockで囲み、背景画像を設定する#kvエリアと、オブジェクトを表示するcanvasエリア、そして見出しテキスト用のh2.main-titleの3つエリアを用意しています。すべて#kvエリアの中に入れてもよかったのですが、背景画像とオブジェクトの透明度をバラバラにしたかったので、影響が出ないようエリアを分けました。
余談ですが、見出しテキストには故スティーブ・ジョブズ氏の「Think different.」を引用しました。
CSS
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | @import url('https://fonts.googleapis.com/css2?family=Readex+Pro&family=Rock+Salt&display=swap'); .kv-block { height: 400px; position: relative; background-image: url("../img/img_01.jpg"); background-position: center; background-repeat: no-repeat; background-size: cover; } #canvas { width: 100%; height: 100%; } .main-title { font-family: 'Readex Pro', sans-serif; text-align: center; font-size: 40px; color: #fff; position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto; height: 1em; line-height: 1; text-shadow: 2px 2px 10px #808080; letter-spacing: 0.2em; pointer-events: none; z-index: 4; } @media screen and (max-width: 640px) { .main-title { font-size: 20px; } } .text + .text { margin-top: 1em; } .contents-block { padding: 40px 0; } .title { font-size: 26px; font-weight: bold; line-height: 1.6; margin-bottom: 10px; } @media screen and (max-width: 640px) { .title { font-size: 22px; line-height: 1.4; } } |
今回作成したcssは上記の通りです。英語のフォントを少しかっこよくしたかったので、Google fontの’Readex Pro‘をつかっています。
テキストの上にマウスカーソルを載せた際、ポインタが変わらないようにしたかったので pointer-events: none; を指定しています。
2. three.jsの調整
HTMLとCSSを作成したら、今度はthree.jsの調整をおこないます。前回のthree.jsの記事「JavaScript経験者におすすめ!ウェブサイトに3Dグラフィックを描画できるthree.js(WebGL)を使ってみよう」を参考に、ベースとなるthree.js用のJavaScriptを用意しましょう。
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // ページの読み込みを待つ window.addEventListener('load', init); // canvasのサイズを指定 const width = window.innerWidth; //ウインドウの横の長さ const height = 400; //エリアの縦の長さ function init() { // シーンを作る const scene = new THREE.Scene(); // カメラを作る const camera = new THREE.PerspectiveCamera(45, width / height); camera.position.set(0, 0, 1000); // x,y,z座標でカメラの場所を指定 // レンダラーを作る const canvasElement = document.querySelector('#canvas') //HTMLのcanvasのid const renderer = new THREE.WebGLRenderer({ canvas: canvasElement, }); // ライトを作る const light = new THREE.AmbientLight(0xFFFFFF, 1); //環境光源(色、光の強さ) scene.add(light); // 3Dオブジェクトを作る const geometry = new THREE.DodecahedronGeometry(300, 0); // DodecahedronGeometry 正十二面体(半径、詳細) const material = new THREE.MeshPhongMaterial(); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); } |
3Dオブジェクトを作る箇所では、正十二面体(DodecahedronGeometry)を指定しています。#canvasのサイズは、今回は400px固定にしたかったので、const height = 400; と記載しました。
背景を透明にする
1 2 3 4 5 6 7 8 | // レンダラーを作る const canvasElement = document.querySelector('#canvas') //HTMLのcanvasのid const renderer = new THREE.WebGLRenderer({ canvas: canvasElement, alpha: true, //追加 背景を透明にする }); |
背景に任意の画像を反映させたいため、canvasの背景色をなくしておきます。WebGLRendererにalpha: trueと記載し、背景を透明します。
アンチエイリアス追加
1 2 3 4 5 6 7 8 9 | // レンダラーを作る const canvasElement = document.querySelector('#canvas') //HTMLのcanvasのid const renderer = new THREE.WebGLRenderer({ canvas: canvasElement, alpha: true, //背景を透明にする antialias: true, //追加 アンチエイリアス }); |
WebGLRendererにantialias: trueと記載すると、物体の輪郭を滑らかにする「アンチエイリアス」を有効にできます。アンチエイリアスがあり・なしではこのように見た目が変わります。アンチエイリアスありの場合、フチのギザギザした感じが取れ、滑らかな見た目になっていることがわかるかと思います。
枠線の追加
1 2 3 4 5 6 7 8 9 10 11 12 13 | //追加 枠線を作成 const line = new THREE.LineSegments( new THREE.EdgesGeometry(geometry), new THREE.LineBasicMaterial({ color: 0x000000, // 線の色(今回は黒) }), ); const mesh = new THREE.Mesh(geometry, material); mesh.add(line); //追加 scene.add(mesh); |
画像同士が隣接する箇所は境界線がぼやっとしてしまいっていたため、境界線をはっきりさせるために、three.jsに用意されているLineSegmentsを用い、多面体の3Dオブジェクトに枠線を付けました。LineBasicMaterialにて色の指定ができます。meshに枠線用の変数(今回はline)を追加するのを忘れないようにしてください。
現時点でこのような見た目になりました。
マウスの動きを取得
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // 追加 マウスの動きを取得 let mouseX = 0, mouseY = 0; // マウス座標 let windowHalfX = window.innerWidth / 2; let windowHalfY = 200; function onDocumentMouseMove(event) { mouseX = (event.clientX - windowHalfX); mouseY = (event.clientY - windowHalfY); } document.addEventListener('mousemove', onDocumentMouseMove); function animationStart() { requestAnimationFrame(animationStart); camera.position.x += (mouseX - camera.position.x) * 0.05; camera.position.y += (- mouseY - camera.position.y) * 0.05; // 原点方向を見つめる camera.lookAt(scene.position); // レンダリング renderer.render(scene, camera); } animationStart(); |
マウスの動きを取得します。JavaScriptのmousemoveイベントを用いて、マウスが動いたときにマウスの位置を取得するようにしています。その下のファンクション animationStart にて、カメラの位置をマウスの動きに合わせて調整されるようになりました。
これでマウスを移動したほうへ多面体の向きが動くようになりました。
アニメーション
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // アニメ―ション function animationStart() { requestAnimationFrame(animationStart); camera.position.x += (mouseX - camera.position.x) * 0.05; camera.position.y += (- mouseY - camera.position.y) * 0.05; // 原点方向を見つめる camera.lookAt(scene.position); // 追加 3Dオブジェクトが回転する mesh.rotation.y += 0.005; mesh.rotation.x += 0.005; // レンダリング renderer.render(scene, camera); } animationStart(); |
オブジェクトを回転させるために rotation を使います。多面体がフレームごとに回転するようになります。
現時点で、マウスを動かさなければ回転をし続け、マウスを動かすとその方向へ多面体が少し動く、というアニメーションになっています。
背景色の明度を少し落とす
CSS
1 2 3 4 5 | #kv { opacity: 0.7; // 追加 } |
3Dオブジェクトとの差がわかりやすいよう、背景色の明度を少しだけ下げるために、#kvの透明度を0.7にしました。bodyの背景色に黒色を指定しているため、画像が黒みを帯びたようになります。
ウインドウのリサイズ対応
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // ウインドウのリサイズ対応 onWindowResize(); window.addEventListener('resize', onWindowResize); function onWindowResize() { // ウインドウ幅を取得 const width = window.innerWidth; // レンダラーのサイズを調整する renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(width, 400); windowHalfX = window.innerWidth / 2; windowHalfY = 200; // カメラのアスペクト比を正す camera.aspect = width / 400; camera.updateProjectionMatrix(); } |
このままでは、ウインドウの幅を変えても、ウインドウを読み込んだ際の位置に3Dオブジェクトが固定されてしまうため、ウインドウのリサイズ時に合わせてcanvas幅の調整をおこなうようにします。
これで、理想とするサイトが完成しました!
ここからさらにアレンジしていきます。
多面体画像の切り替え
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | const material = new THREE.MeshPhongMaterial({ map: texture01, // 追加 画像の読み込み }); (中略) // 画像 const loader = new THREE.TextureLoader(); const texture01 = loader.load('./img/img_01.jpg'); const texture02 = loader.load('./img/img_02.jpg'); const texture03 = loader.load('./img/img_03.jpg'); const texture04 = loader.load('./img/img_04.jpg'); const texture05 = loader.load('./img/img_05.jpg'); const textures = [ texture01, texture02, texture03, texture04, texture05, ] // 多面体の画像切り替え let count = -1; imgChange(); function imgChange() { count++; // カウントが画像の枚数と同じになると0に戻る if (count == textures.length) count = 0; material.map = textures[count]; setTimeout(imgChange, 4000); } |
せっかくなので、多面体の画像が4秒ごとに、各国の都市の写真へ自動で切り替わるようにしてみます。日本の秋葉原、アメリカのニューヨーク、フランスのローマ、オランダのアムステルダム、ドイツのミュンヘンの画像へ切り替わっていきます。
three.jsでは画像を読み込む際にはloader.load(‘画像へのパス’); と記述する必要があります。これで読み込んだ画像を配列「textures」に入れ、4秒ごとに画像のパスが変わるようにしています。
多面体のみ、画像が切り替わるようになりました。
KV画像の切り替え
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // KV背景の画像切り替え const backgrounds = [ "./img/img_05.jpg", "./img/img_01.jpg", "./img/img_02.jpg", "./img/img_03.jpg", "./img/img_04.jpg", ] const kvElement = document.querySelector('#kv') //HTMLのid bgChange(); function bgChange() { count++; // カウントが画像の枚数と同じになると0に戻る if (count == backgrounds.length) count = 0; kvElement.style.backgroundImage = 'url(' + backgrounds[count] + ')'; setTimeout(bgChange, 4000); } |
多面体の画像と同様に、KVの画像を切り替えます。KVでは背景画像として設定しているため、背景画像(backgroundImage)のパスが4秒ごとに切り替わるようにしました。
これで、多面体と背景の画像が連動して動くようになりました。
フェードを少し付ける
画像が切り替わりが急なので、少しだけフェードを付けてみました。
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | // 多面体の画像切り替え let count = -1; imgChange(); function imgChange() { count++; // カウントが画像の枚数と同じになると0に戻る if (count == textures.length) count = 0; material.map = textures[count]; setTimeout(imgChange, 4000); setTimeout(function () { // 追加 フェード setTimeout(function () { canvasElement.classList.remove("fadeInObj"); }, 2000), canvasElement.classList.add("fadeInObj") }, 4000); } function bgChange() { count++; // カウントが画像の枚数と同じになると0に戻る if (count == backgrounds.length) count = 0; kvElement.style.backgroundImage = 'url(' + backgrounds[count] + ')'; setTimeout(bgChange, 4000); setTimeout(function () { // 追加 フェード setTimeout(function () { kvElement.classList.remove("fadeInBg"); }, 2000), kvElement.classList.add("fadeInBg") }, 4000); } |
CSS
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | @keyframes fadeInObj { from { opacity: 0.5; } to { opacity: 1; } } .fadeInObj { animation-name: fadeInObj; } @keyframes fadeInBg { from { opacity: 0.4; } to { opacity: 0.7; } } .fadeInBg { animation-name: fadeInBg; } .fadeTarget{ animation-duration: .5s; animation-fill-mode: both; } |
切り替わる際、オブジェクトは0.5秒間で透明度が0.5から1になるように、背景画像は透明度が0.4から0.7(先ほど下げた値)にしています。背景を指定しているエリアとオブジェクトが親子関係になっていると、一括でアニメーションがあたってしまうため、分かれるように気を付けていただければと思います。
こちらでやっと完成です!
全体のソース
全体のソースはこちらから確認いただけます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | // ページの読み込みを待つ window.addEventListener('load', init); // canvasのサイズを指定 const width = window.innerWidth; //ウインドウの横の長さ const height = 400; //エリアの縦の長さ function init() { // シーンを作る const scene = new THREE.Scene(); // カメラを作る const camera = new THREE.PerspectiveCamera(45, width / height); camera.position.set(0, 0, 1000); // x,y,z座標でカメラの場所を指定 // レンダラーを作る const canvasElement = document.querySelector('#canvas') //HTMLのcanvasのid const renderer = new THREE.WebGLRenderer({ canvas: canvasElement, alpha: true, //背景を透明にする antialias: true, //アンチエイリアス }); // renderer.setSize(width, height); //サイズ // ライトを作る const light = new THREE.AmbientLight(0xFFFFFF, 1); //環境光源(色、光の強さ) scene.add(light); // 画像 const loader = new THREE.TextureLoader(); const texture01 = loader.load('./img/img_01.jpg'); const texture02 = loader.load('./img/img_02.jpg'); const texture03 = loader.load('./img/img_03.jpg'); const texture04 = loader.load('./img/img_04.jpg'); const texture05 = loader.load('./img/img_05.jpg'); const textures = [ texture01, texture02, texture03, texture04, texture05, ] // 3Dオブジェクトを作る const geometry = new THREE.DodecahedronGeometry(300, 0); // DodecahedronGeometry 正十二面体(半径、詳細) // 枠線を作成 const line = new THREE.LineSegments( new THREE.EdgesGeometry(geometry), // 線 new THREE.LineBasicMaterial({ color: 0x000000, // 線の色 }), ); const material = new THREE.MeshPhongMaterial({ map: texture01, }); const mesh = new THREE.Mesh(geometry, material); mesh.add(line); scene.add(mesh); // マウス let mouseX = 0, mouseY = 0; // マウス座標 let windowHalfX = window.innerWidth / 2; let windowHalfY = 200; function onDocumentMouseMove(event) { mouseX = (event.clientX - windowHalfX); mouseY = (event.clientY - windowHalfY); } document.addEventListener('mousemove', onDocumentMouseMove); // アニメ―ション function start() { requestAnimationFrame(start); camera.position.x += (mouseX - camera.position.x) * 0.05; camera.position.y += (- mouseY - camera.position.y) * 0.05; // 原点方向を見つめる camera.lookAt(scene.position); //球体が回転する mesh.rotation.y += 0.005; mesh.rotation.x += 0.005; // レンダリング renderer.render(scene, camera); } start(); // ウインドウのリサイズ対応 onWindowResize(); window.addEventListener('resize', onWindowResize); function onWindowResize() { // ウインドウ幅を取得 const width = window.innerWidth; // レンダラーのサイズを調整する renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(width, 400); windowHalfX = window.innerWidth / 2; windowHalfY = 200; // カメラのアスペクト比を正す camera.aspect = width / 400; camera.updateProjectionMatrix(); } // 多面体の画像切り替え let count = -1; imgChange(); function imgChange() { count++; // カウントが画像の枚数と同じになると0に戻る if (count == textures.length) count = 0; material.map = textures[count]; setTimeout(imgChange, 4000); setTimeout(function () { setTimeout(function () { kvElement.classList.remove("fadeInObj"); }, 2000), kvElement.classList.add("fadeInObj") }, 4000); } // KV背景の画像切り替え const backgrounds = [ "./img/img_05.jpg", "./img/img_01.jpg", "./img/img_02.jpg", "./img/img_03.jpg", "./img/img_04.jpg", ] const kvElement = document.querySelector('#kv') //HTMLののid bgChange(); function bgChange() { count++; // カウントが画像の枚数と同じになると0に戻る if (count == backgrounds.length) count = 0; kvElement.style.backgroundImage = 'url(' + backgrounds[count] + ')'; setTimeout(bgChange, 4000); setTimeout(function () { setTimeout(function () { kvElement.classList.remove("fadeInBg"); }, 2000), kvElement.classList.add("fadeInBg") }, 4000); } } |
応用1:サイトのトップページ風(英語)
このKVを応用し、サイトのトップページのようなデザインにすることもできるかと思います。
見出しエリアとして数字で高さを固定していた箇所を、ウインドウの縦の長さ(window.innerHeight)で取得するようにすると画面いっぱいに#canvasが広がるようになります。要素とテキストがより目立つので、メッセージ性の強い印象になるかと思います。
応用2:サイトのトップページ風(日本語)
サイトのトップページの日本語サイトバージョンを作ってみました。
多面体自体をぼわっと光らせるためには、ほかのオブジェクトと組み合わせる必要がありそうだったため、CSSのグラデーションを使い、画面の端のほうへいくと暗くなるような雰囲気で調整しました。
国際的な取引のある会社などであれば、3Dオブジェクトを球体にし、地球の画像を貼るなどしてもよさそうです。
応用3:環境マッピングを利用し、周りを反射しているように見せる
環境マッピングを用いて、オブジェクトの表面が鏡のように周りの風景を反射しているような表現にします。環境マッピングとは、オブジェクトの表面に周囲環境が映り込んでいるかのように見えるよう、擬似的に再現する方法です。そのため、実際に反射しているわけではありません。
今回は「キューブ環境マッピング」という四角形用の環境マッピングを利用します。画像を以下のようなキューブ環境マッピング用に切り出す必要があります。
画像の作成には以下のサイトを利用しました。1枚の画像から6面分の画像を抽出してくれるので、それを保存します。(本来はパノラマ画像から切り出すツールです。今回使っている画像はパノラマではないですが、6枚の画像が必要だったので利用させてもらいました)
【ツール】パノラマ画像→キューブマップ変換【JavaScript】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // キューブマップ用 画像 const path = './img/cubemap/'; const urls = [ path + 'img_01_r.png', //右 path + 'img_01_l.png', //左 path + 'img_01_u.png', //上 path + 'img_01_d.png', //下 path + 'img_01_f.png', //前 path + 'img_01_b.png' //後 ] const cubeTextureLoader = new THREE.CubeTextureLoader(); const textureMapping = cubeTextureLoader.load(urls); // オブジェクト const mappingGeometry = new THREE.DodecahedronGeometry(300, 0); //正十二面体(半径、詳細) const mappingMaterial = new THREE.MeshPhongMaterial({ envMap: textureMapping, reflectivity: 1, // 映り込み量 0~1を指定 }); const mapping = new THREE.Mesh(mappingGeometry, mappingMaterial); scene.add(mapping); |
キューブマップ用の画像6枚のパスを配列の中に入れ、画像をロードするためのクラス「CubeTextureLoader」で読み込みます。そして、envMapで画像を指定し、反射の量を設定できる reflectivityで 1(最大)を指定します。0になるにつれ反射しなくなります。
球体ではこのようになりました。
まとめ
今回はthree.jsを用いて、実際にサイトをアレンジする方法をいくつかまとめました。応用1・2にあったように、ブラウザの高さ・横幅いっぱいにcanvasを置くことで、サイトのトップページにも流用できそうなデザインに仕上げることができるかと思います。また、応用3の鏡のように周りを反射しているように見える「キューブ環境マッピング」もなかなか面白い表現ですよね。
背景にも「キューブ環境マッピング」を使うことで、空間をドラッグするとオブジェクトの表面も変わっていくような表現ができるので、かなり面白いんじゃないかなと思います。
オリジナルのモチーフなどを作るには、数学的な知識が必要になってしまいますが、three.jsに最初から用意されている球体や多面体といったオブジェクトであれば簡単に導入することができると思うので、サイトの雰囲気やイメージに合うものがあればぜひ活用してみてはいかがでしょうか。
参考サイト
カメラ
Three.jsのカメラの制御(ics.media)
線
LineSegments(three.js 公式ドキュメント)
マウスの動きを取得
three.js
webgl – vertex colors(three.js examples)
three.js/examples/webgl_geometry_colors.html(GitHub)
環境マッピング
【Three.js】環境マッピングその2 – デザインおしゃれ手帳
CubeTextureLoader – three.js公式ドキュメント