
今回はThree.jsを使って3Dモデルの簡単なコンテンツを作成してみました。
ずいぶん、久しぶりとなってしまいましたが、よろしくお願いします。
Three.jsとは
今更ですが、簡単に説明しますと….
Three.jsは、Webブラウザ上で3Dグラフィックスを描画するためのJavaScriptライブラリです。WebGLをより扱いやすくした仕組みになっており、立体的なオブジェクトの表示や回転、光や影の表現、画像テクスチャの貼り付け、カメラ操作などを比較的シンプルなコードで実装できます。Webサイトの演出やインタラクティブなコンテンツ、3Dモデルの表示など、通常のCSSやJavaScriptだけでは難しい表現を実現できるのが大きな特長です。
基本設定
まずはThree.jsを設定しましょう。
一番簡単なのは、CDNからThree.jsを直接importする方法なので今回はCDNを利用します。
HTML
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js Test</title>
<style>
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
#three-canvas {
width: 100%;
height: 100vh;
}
#three-canvas canvas {
display: block;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="three-canvas"></div>
<script type="module" src="./function.js"></script>
</body>
</html>
J S(function.js)
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js';
/* --------------------------------
サイズ
-------------------------------- */
let width = window.innerWidth;
let height = window.innerHeight;
/* --------------------------------
シーン
-------------------------------- */
const scene = new THREE.Scene();
/* --------------------------------
カメラ
-------------------------------- */
const camera = new THREE.PerspectiveCamera(
50,
width / height,
0.1,
1000
);
camera.position.z = 5;
/* --------------------------------
レンダラー
-------------------------------- */
const renderer = new THREE.WebGLRenderer({
alpha: true,
antialias: true
});
renderer.setSize(width, height);
renderer.setPixelRatio(
Math.min(window.devicePixelRatio, 2)
);
/* --------------------------------
HTMLへcanvas追加
-------------------------------- */
const canvasContainer = document.getElementById('three-canvas');
if (!canvasContainer) {
throw new Error('#three-canvas が見つかりません。');
}
canvasContainer.appendChild(renderer.domElement);
/* --------------------------------
ライト
-------------------------------- */
// 平行光
const dirLight = new THREE.DirectionalLight(
0xffffff,
2
);
dirLight.position.set(1, 1, 3);
scene.add(dirLight);
// 環境光
const ambLight = new THREE.AmbientLight(
0xffffff,
0.5
);
scene.add(ambLight);
/* --------------------------------
オブジェクト
-------------------------------- */
const geometry = new THREE.TorusKnotGeometry(
1,
0.35,
100,
16
);
const material = new THREE.MeshLambertMaterial({
color: 0x55ffff
});
const obj001 = new THREE.Mesh(
geometry,
material
);
scene.add(obj001);
/* --------------------------------
リサイズ
-------------------------------- */
window.addEventListener('resize', () => {
width = window.innerWidth;
height = window.innerHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
renderer.setPixelRatio(
Math.min(window.devicePixelRatio, 2)
);
});
/* --------------------------------
アニメーション
-------------------------------- */
function animate() {
requestAnimationFrame(animate);
obj001.rotation.x += 0.01;
obj001.rotation.y += 0.01;
renderer.render(
scene,
camera
);
}
animate();
ここでまず確認するポイントは 以下3つ。
1:function.js の先頭をCDN読み込みにする
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js';
2:typeはmoduleにする
<script type="module" src="./function.js"></script>
3:セレクタ名の確認
<div id="three-canvas"></div>
Three.jsは「3Dの舞台セットを作る道具箱」のようなもので
- 舞台(scene)を用意し
- カメラ(camera)を設置し
- 照明(light)を計算し
- オブジェクト(3Dの物体=cubeやsphereなど)を登場させ
- アニメーション(動き)を加える
という考え方になります。
ここまでの設定で以下のようになります。
オブジェクトの形を変える
形を変える場合はgeometryを調整します。
const geometry = new THREE.TorusKnotGeometry( 1, 0.35, 100, 16 );
MeshLambertMaterial は色や光沢などの「表面」を決めるものなので、形を変える場合は TorusKnotGeometry を別のGeometryに変更します。
たとえば、球体ならこうです。
const geometry = new THREE.SphereGeometry( 1.2, 64, 64 );
立方体なら、
const geometry = new THREE.BoxGeometry( 1.8, 1.8, 1.8 );
ドーナツ形なら、
const geometry = new THREE.TorusGeometry( 1, 0.35, 32, 100 );
正二十面体っぽい形なら、
const geometry = new THREE.IcosahedronGeometry( 1.3, 1 );
さらにThree.jsなら、既成の形だけではなく、頂点そのものを動かして有機的な形に変形させることもできます。たとえば球を少しデコボコした液体・雲・アメーバのような形にすることも可能です。
自転する地球を作ってみる
スクロールに合わせて自転する地球を作ってみます。
これはThree.jsに加えてGSAP ScrollTriggerを使用します。
HTML
<!-- ======================================== INTRO ======================================== --> <section class="intro"> <div class="intro-inner"> <p>SCROLL DOWN</p> <h1>EARTH</h1> </div> </section> <!-- ======================================== THREE.JS ======================================== --> <section class="earth-section"> <div class="earth-sticky"> <!-- Three.js Canvas --> <div id="three-canvas"></div> <!-- テキスト --> <div class="earth-content"> <p class="earth-subtitle"> OUR PLANET </p> <h2 class="earth-title"> EARTH </h2> </div> </div> </section> <!-- ======================================== NEXT ======================================== --> <section class="next-section"> <div class="next-inner"> <p>NEXT CONTENT</p> <h2> 次のコンテンツ </h2> </div> </section> <script src="js/gsap.min.js"></script> <script src="js/ScrollTrigger.min.js"></script> <script type="module" src="function.js"></script>
CSS
body {
background: #000;
color: #fff;
font-family:
Arial,
Helvetica,
sans-serif;
}
/* --------------------------------
INTRO
-------------------------------- */
.intro {
width: 100%;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #111;
}
.intro-inner {
text-align: center;
}
.intro p {
margin-bottom: 20px;
font-size: 14px;
letter-spacing: 0.2em;
}
.intro h1 {
font-size: clamp(70px, 12vw, 180px);
line-height: 1;
}
/* --------------------------------
EARTH
-------------------------------- */
.earth-section {
position: relative;
width: 100%;
height: 300vh;
background: #000;
}
.earth-sticky {
position: sticky;
top: 0;
width: 100%;
height: 100vh;
overflow: hidden;
}
/* Three.js */
#three-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
#three-canvas canvas {
display: block;
width: 100%;
height: 100%;
}
/* --------------------------------
テキスト
-------------------------------- */
.earth-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
text-align: center;
pointer-events: none;
}
.earth-subtitle {
margin-bottom: 15px;
font-size: 14px;
letter-spacing: 0.3em;
}
.earth-title {
font-size: clamp(80px, 15vw, 220px);
line-height: 1;
color: rgba(255, 255, 255, 0.8);
}
/* --------------------------------
NEXT
-------------------------------- */
.next-section {
width: 100%;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #eee;
color: #111;
}
.next-inner {
text-align: center;
}
.next-inner p {
margin-bottom: 15px;
font-size: 13px;
letter-spacing: 0.2em;
}
.next-inner h2 {
font-size: clamp(40px, 7vw, 100px);
}
JS
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js';
/* --------------------------------
GSAP
-------------------------------- */
gsap.registerPlugin(ScrollTrigger);
/* --------------------------------
DOM
-------------------------------- */
const canvasContainer =
document.getElementById('three-canvas');
if (!canvasContainer) {
throw new Error(
'#three-canvas が見つかりません。'
);
}
/* --------------------------------
サイズ
-------------------------------- */
let width = window.innerWidth;
let height = window.innerHeight;
/* --------------------------------
Scene
-------------------------------- */
const scene =
new THREE.Scene();
/* --------------------------------
Camera
-------------------------------- */
const camera =
new THREE.PerspectiveCamera(
45,
width / height,
0.1,
100
);
camera.position.set(
0,
0,
6
);
/* --------------------------------
Renderer
-------------------------------- */
const renderer =
new THREE.WebGLRenderer({
alpha: true,
antialias: true
});
renderer.setSize(
width,
height
);
renderer.setPixelRatio(
Math.min(
window.devicePixelRatio,
2
)
);
renderer.outputColorSpace =
THREE.SRGBColorSpace;
canvasContainer.appendChild(
renderer.domElement
);
/* --------------------------------
Earth Group
-------------------------------- */
const earthGroup =
new THREE.Group();
scene.add(
earthGroup
);
/* --------------------------------
Texture Loader
-------------------------------- */
const textureLoader =
new THREE.TextureLoader();
/* --------------------------------
Earth Texture
-------------------------------- */
/*
HTMLから見た画像パスではなく、
ブラウザからアクセスできるURLを指定します。
例:
/img/earth.jpg
./img/earth.jpg
*/
const earthTexture =
textureLoader.load(
'./img/earth.jpg',
// 読み込み成功
() => {
console.log(
'earth.jpg loaded'
);
},
undefined,
// 読み込み失敗
(error) => {
console.error(
'earth.jpg load error:',
error
);
}
);
/* --------------------------------
Texture Color
-------------------------------- */
earthTexture.colorSpace =
THREE.SRGBColorSpace;
/* --------------------------------
Earth Geometry
-------------------------------- */
const earthGeometry =
new THREE.SphereGeometry(
1.6,
64,
64
);
/* --------------------------------
Earth Material
-------------------------------- */
const earthMaterial =
new THREE.MeshStandardMaterial({
map: earthTexture,
roughness: 0.7,
metalness: 0
});
/* --------------------------------
Earth Mesh
-------------------------------- */
const earth =
new THREE.Mesh(
earthGeometry,
earthMaterial
);
earthGroup.add(
earth
);
/* --------------------------------
地軸
-------------------------------- */
earthGroup.rotation.z =
THREE.MathUtils.degToRad(
23.4
);
/* --------------------------------
Atmosphere
-------------------------------- */
const atmosphereGeometry =
new THREE.SphereGeometry(
1.64,
64,
64
);
const atmosphereMaterial =
new THREE.MeshBasicMaterial({
color: 0x66aaff,
transparent: true,
opacity: 0.06,
side: THREE.BackSide
});
const atmosphere =
new THREE.Mesh(
atmosphereGeometry,
atmosphereMaterial
);
earthGroup.add(
atmosphere
);
/* --------------------------------
Light
-------------------------------- */
const directionalLight =
new THREE.DirectionalLight(
0xffffff,
3
);
directionalLight.position.set(
4,
2,
5
);
scene.add(
directionalLight
);
const ambientLight =
new THREE.AmbientLight(
0xffffff,
0.8
);
scene.add(
ambientLight
);
/* --------------------------------
ScrollTrigger
スクロールで地球を2回転
-------------------------------- */
gsap.to(
earth.rotation,
{
y: Math.PI * 4,
ease: 'none',
scrollTrigger: {
trigger: '.earth-section',
start: 'top top',
end: 'bottom bottom',
scrub: 1,
invalidateOnRefresh: true
}
}
);
/* --------------------------------
スクロールで少し拡大
-------------------------------- */
gsap.fromTo(
earthGroup.scale,
{
x: 0.8,
y: 0.8,
z: 0.8
},
{
x: 1.15,
y: 1.15,
z: 1.15,
ease: 'none',
scrollTrigger: {
trigger: '.earth-section',
start: 'top top',
end: 'bottom bottom',
scrub: 1
}
}
);
/* --------------------------------
Render
-------------------------------- */
function render() {
requestAnimationFrame(
render
);
renderer.render(
scene,
camera
);
}
render();
/* --------------------------------
Resize
-------------------------------- */
function resize() {
width =
window.innerWidth;
height =
window.innerHeight;
camera.aspect =
width / height;
camera.updateProjectionMatrix();
renderer.setSize(
width,
height
);
renderer.setPixelRatio(
Math.min(
window.devicePixelRatio,
2
)
);
ScrollTrigger.refresh();
}
window.addEventListener(
'resize',
resize
);
今回は Three.jsで地球風の球体を表示し、GSAP ScrollTriggerのscrubでスクロール量に完全連動して自転させる構成にしています。SphereGeometryで球体を作り、ScrollTriggerでrotation.yをアニメーションさせる形です。Three.js公式にもSphereGeometryが用意されており、ScrollTriggerのscrubはスクロール位置とアニメーション進行を同期させる用途です。
今回はまず仕組みが分かりやすいように、100vhの地球表示エリアを固定しながら、スクロールすると地球が2回転するデモにしています。
自転する地球の応用版を作ってみる
今度は、スクロールで回転しながら布がひらりと解けるように、地球から1枚の世界地図にする変化するアニメーションです。
HTMLは変わりません。
CSSは以下の部分を変更します。
CSS
.earth-section {
position: relative;
width: 100%;
height: 500vh;
background: #000;
}
#three-canvas {
position: sticky;
top: 0;
width: 100%;
height: 100vh;
overflow: hidden;
}
JS
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js';
/* --------------------------------
GSAP
-------------------------------- */
gsap.registerPlugin(ScrollTrigger);
/* --------------------------------
DOM
-------------------------------- */
const canvasContainer =
document.getElementById('three-canvas');
if (!canvasContainer) {
throw new Error(
'#three-canvas が見つかりません。'
);
}
/* --------------------------------
サイズ
-------------------------------- */
let width =
window.innerWidth;
let height =
window.innerHeight;
/* --------------------------------
Scene
-------------------------------- */
const scene =
new THREE.Scene();
/* --------------------------------
Camera
-------------------------------- */
const camera =
new THREE.PerspectiveCamera(
45,
width / height,
0.1,
100
);
camera.position.set(
0,
0,
6
);
/* --------------------------------
Renderer
-------------------------------- */
const renderer =
new THREE.WebGLRenderer({
alpha: true,
antialias: true
});
renderer.setSize(
width,
height
);
renderer.setPixelRatio(
Math.min(
window.devicePixelRatio,
2
)
);
renderer.outputColorSpace =
THREE.SRGBColorSpace;
canvasContainer.appendChild(
renderer.domElement
);
/* --------------------------------
Texture Loader
-------------------------------- */
const textureLoader =
new THREE.TextureLoader();
/* --------------------------------
Earth Texture
-------------------------------- */
const earthTexture =
textureLoader.load(
'./img/earth.jpg',
() => {
console.log(
'earth.jpg loaded'
);
},
undefined,
(error) => {
console.error(
'earth.jpg load error:',
error
);
}
);
earthTexture.colorSpace =
THREE.SRGBColorSpace;
/* --------------------------------
地球設定
-------------------------------- */
const SEGMENTS_X =
128;
const SEGMENTS_Y =
64;
const EARTH_RADIUS =
1.6;
/* --------------------------------
最終世界地図サイズ
-------------------------------- */
const MAP_WIDTH =
5.2;
const MAP_HEIGHT =
2.6;
/* --------------------------------
Geometry
-------------------------------- */
const geometry =
new THREE.PlaneGeometry(
1,
1,
SEGMENTS_X,
SEGMENTS_Y
);
/* --------------------------------
Geometry Attributes
-------------------------------- */
const positionAttribute =
geometry.attributes.position;
const uvAttribute =
geometry.attributes.uv;
/* --------------------------------
球体座標
-------------------------------- */
const spherePositions =
new Float32Array(
positionAttribute.count * 3
);
/* --------------------------------
平面座標
-------------------------------- */
const planePositions =
new Float32Array(
positionAttribute.count * 3
);
/* --------------------------------
球体と平面の座標を作成
-------------------------------- */
for (
let i = 0;
i < positionAttribute.count;
i++
) {
const u =
uvAttribute.getX(i);
const v =
uvAttribute.getY(i);
/* ----------------------------
球体
---------------------------- */
const longitude =
(u - 0.5) *
Math.PI *
2;
const latitude =
(v - 0.5) *
Math.PI;
const x =
EARTH_RADIUS *
Math.cos(latitude) *
Math.sin(longitude);
const y =
EARTH_RADIUS *
Math.sin(latitude);
const z =
EARTH_RADIUS *
Math.cos(latitude) *
Math.cos(longitude);
spherePositions[
i * 3
] = x;
spherePositions[
i * 3 + 1
] = y;
spherePositions[
i * 3 + 2
] = z;
/* ----------------------------
世界地図
---------------------------- */
const px =
(u - 0.5) *
MAP_WIDTH;
const py =
(v - 0.5) *
MAP_HEIGHT;
planePositions[
i * 3
] = px;
planePositions[
i * 3 + 1
] = py;
planePositions[
i * 3 + 2
] = 0;
}
/* --------------------------------
初期状態
-------------------------------- */
for (
let i = 0;
i < positionAttribute.count;
i++
) {
positionAttribute.setXYZ(
i,
spherePositions[
i * 3
],
spherePositions[
i * 3 + 1
],
spherePositions[
i * 3 + 2
]
);
}
positionAttribute.needsUpdate =
true;
geometry.computeVertexNormals();
/* --------------------------------
Material
-------------------------------- */
const material =
new THREE.MeshStandardMaterial({
map:
earthTexture,
roughness:
0.75,
metalness:
0,
side:
THREE.DoubleSide
});
/* --------------------------------
Earth
-------------------------------- */
const earth =
new THREE.Mesh(
geometry,
material
);
scene.add(
earth
);
/* --------------------------------
地球初期角度
-------------------------------- */
earth.rotation.x =
THREE.MathUtils.degToRad(
-5
);
earth.rotation.z =
THREE.MathUtils.degToRad(
23.4
);
/* --------------------------------
Light
-------------------------------- */
const directionalLight =
new THREE.DirectionalLight(
0xffffff,
3
);
directionalLight.position.set(
4,
2,
5
);
scene.add(
directionalLight
);
const ambientLight =
new THREE.AmbientLight(
0xffffff,
0.9
);
scene.add(
ambientLight
);
/* --------------------------------
Animation State
-------------------------------- */
const animationState = {
/*
0 = 球体
1 = 平面
*/
morph:
0,
/*
シルク波
*/
wave:
0
};
/* --------------------------------
画面いっぱいになるScaleを取得
-------------------------------- */
function getFullscreenScale() {
/*
カメラからオブジェクトまでの距離
*/
const distance =
camera.position.z -
earth.position.z;
/*
画面に見えている高さ
*/
const visibleHeight =
2 *
Math.tan(
THREE.MathUtils.degToRad(
camera.fov / 2
)
) *
distance;
/*
画面に見えている横幅
*/
const visibleWidth =
visibleHeight *
camera.aspect;
/*
地図を画面いっぱいにする倍率
coverと同じ考え方
*/
const scaleX =
visibleWidth /
MAP_WIDTH;
const scaleY =
visibleHeight /
MAP_HEIGHT;
return Math.max(
scaleX,
scaleY
);
}
/* --------------------------------
Geometry変形
-------------------------------- */
function updateGeometry() {
const morph =
animationState.morph;
const wave =
animationState.wave;
for (
let i = 0;
i < positionAttribute.count;
i++
) {
const index =
i * 3;
/* ----------------------------
球体座標
---------------------------- */
const sx =
spherePositions[
index
];
const sy =
spherePositions[
index + 1
];
const sz =
spherePositions[
index + 2
];
/* ----------------------------
平面座標
---------------------------- */
const px =
planePositions[
index
];
const py =
planePositions[
index + 1
];
const pz =
planePositions[
index + 2
];
/* ----------------------------
Morph
---------------------------- */
let x =
THREE.MathUtils.lerp(
sx,
px,
morph
);
let y =
THREE.MathUtils.lerp(
sy,
py,
morph
);
let z =
THREE.MathUtils.lerp(
sz,
pz,
morph
);
/* ----------------------------
シルク強度
開始・終了時は0
中間だけ強くする
---------------------------- */
const silkAmount =
Math.sin(
morph *
Math.PI
) *
wave;
const uvX =
uvAttribute.getX(i);
const uvY =
uvAttribute.getY(i);
/* ----------------------------
大きな横波
---------------------------- */
const wave1 =
Math.sin(
uvX *
Math.PI *
6 +
morph *
Math.PI *
2
);
/* ----------------------------
細かい縦波
---------------------------- */
const wave2 =
Math.sin(
uvY *
Math.PI *
5 -
morph *
Math.PI *
3
);
/* ----------------------------
Z方向
---------------------------- */
z +=
wave1 *
0.30 *
silkAmount;
z +=
wave2 *
0.13 *
silkAmount;
/* ----------------------------
Y方向
---------------------------- */
y +=
wave1 *
0.08 *
silkAmount;
/* ----------------------------
X方向も少し揺らす
---------------------------- */
x +=
wave2 *
0.05 *
silkAmount;
positionAttribute.setXYZ(
i,
x,
y,
z
);
}
positionAttribute.needsUpdate =
true;
geometry.computeVertexNormals();
}
/* --------------------------------
GSAP Timeline
-------------------------------- */
const earthTimeline =
gsap.timeline({
scrollTrigger: {
trigger:
'.earth-section',
start:
'top top',
end:
'bottom bottom',
scrub:
1,
invalidateOnRefresh:
true
}
});
/* --------------------------------
01
1回転目
-------------------------------- */
earthTimeline.to(
earth.rotation,
{
y:
Math.PI *
2,
duration:
1,
ease:
'none'
}
);
/* --------------------------------
02
2回転目
-------------------------------- */
earthTimeline.to(
earth.rotation,
{
y:
Math.PI *
4,
duration:
1,
ease:
'none'
}
);
/* --------------------------------
03
地軸を水平へ
-------------------------------- */
earthTimeline.to(
earth.rotation,
{
x:
0,
z:
0,
duration:
0.3,
ease:
'power2.inOut'
}
);
/* --------------------------------
04
シルク開始
-------------------------------- */
earthTimeline.to(
animationState,
{
wave:
1,
duration:
0.25,
ease:
'power2.out'
}
);
/* --------------------------------
05
球体
↓
世界地図
同時に少し拡大
-------------------------------- */
earthTimeline.to(
animationState,
{
morph:
1,
duration:
1.5,
ease:
'power2.inOut',
onUpdate:
updateGeometry
}
);
/* --------------------------------
06
シルク波を消す
-------------------------------- */
earthTimeline.to(
animationState,
{
wave:
0,
duration:
0.5,
ease:
'power2.out',
onUpdate:
updateGeometry
}
);
/* --------------------------------
07
世界地図を画面いっぱいへ
-------------------------------- */
earthTimeline.to(
earth.scale,
{
x: () =>
getFullscreenScale(),
y: () =>
getFullscreenScale(),
z: () =>
getFullscreenScale(),
duration:
1,
ease:
'power2.inOut'
}
);
/* --------------------------------
Render
-------------------------------- */
function render() {
requestAnimationFrame(
render
);
renderer.render(
scene,
camera
);
}
render();
/* --------------------------------
Resize
-------------------------------- */
function resize() {
width =
window.innerWidth;
height =
window.innerHeight;
/* ----------------------------
Camera
---------------------------- */
camera.aspect =
width / height;
camera.updateProjectionMatrix();
/* ----------------------------
Renderer
---------------------------- */
renderer.setSize(
width,
height
);
renderer.setPixelRatio(
Math.min(
window.devicePixelRatio,
2
)
);
/* ----------------------------
ScrollTrigger
---------------------------- */
ScrollTrigger.refresh();
}
/* --------------------------------
Resize Event
-------------------------------- */
window.addEventListener(
'resize',
resize
);
全体の流れとしては
地球が2回転 → 地軸が水平 → シルク状に展開 → 世界地図になる → そのまま画面全体を覆う
という感じです。
重要なのは、今回は「地球のMeshを消して、別の世界地図画像をフェードイン」しているわけではない」ことです。
img/earth.jpg が最初から最後まで同じMeshに貼られていて、球体の頂点位置から、平面の頂点位置へ直接変形しています。BufferGeometryは頂点位置やUVを保持できるため、このような頂点単位の変形ができます。
なので、かなり「地球そのものがほどけて地図になる」感じになります。
まとめ
Three.jsは、Webブラウザ上で3D表現を手軽に実装できるJavaScriptライブラリです。基本的には「シーン」「カメラ」「レンダラー」を用意し、その中に3Dオブジェクトやライト、テクスチャなどを配置して制作します。さらにGSAPやScrollTriggerと組み合わせれば、スクロールに連動した回転・移動・変形など、Webサイトならではのインタラクティブな演出も可能です。3Dモデルやシェーダーを活用すれば表現の幅はさらに広がります。Webサイトに「見る」だけでなく「体験する」要素が求められる中、Three.jsは今後も魅力的なWeb表現をつくる選択肢のひとつになりそうです。