公開日
- 15 分で読めます
第12話 「命令の書き方、お願いの書き方」
Three.js ってなんだろう? カエル君と学ぶ、ブラウザ3Dの不思議な世界
注
この物語は、Three.jsを楽しく学ぶことを目的に、生成AIを活用して執筆されています。 技術的な情報の正確性には細心の注意を払っていますが、その内容がすべて真実であることを保証するものではありません。 あくまで学習の補助ツールとして、肩の力を抜いてお楽しみください。
登場人物紹介
- カエル君: 最近プログラミングを学び始めた元気なカエル。まだ知らないことばかりだけど、好奇心は人一倍。「〜ケロ」という口調が特徴。
- 三郎先生: 3DグラフィックスやWeb技術にとても詳しい物知り博士。どんな質問にも優しく答えてくれる。
第12話:命令の書き方、お願いの書き方
カエル君: 「先生、なんだか不思議なんだケロ。」
ある日の午後、カエル君はコードを眺めながら、不思議そうに三郎先生に話しかけました。 カエル君: 「three.jsで箱を出すときって、『ジオメトリを作れ!』『マテリアルを作れ!』『二つを合体させてメッシュにせよ!』『そしてシーンに追加せよ!』って、一つ一つ細かく”命令”している感じがするんだケロ。」
// 命令していく感じ
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
カエル君: 「でも、僕が前に見たHTMLを書くコードみたいに、ただ<cube color="green" />
みたいに、『緑色の箱が欲しいです!』って”お願い”するだけで、あとは良きに計らってくれる…みたいな書き方ができたら、もっと楽ちんなのにナーって思うんだケロ。」
三郎先生: 「カエル君!それは、プログラミングの非常に本質的な違いに気がついたね!素晴らしい着眼点だ。」
博士はカエル君の着眼点に感心し、深く頷きました。
三郎先生: 「それはね、『**手続き的(Imperative)**な書き方』と、『**宣言的(Declarative)**な書き方』の違いなんだよ。」
カエル君: 「てつづきてき…?せんげんてき…?」
三郎先生: 「そう。一つずつ解説しよう。
手続き的プログラミング - Howを伝える
三郎先生: 「カエル君が言った、three.jsの書き方がまさに手続き的なアプローチだ。これは、**『どのように(How)』**タスクを達成するか、その手順を一つ一つ細かくコンピュータに指示していくスタイルなんだ。料理のレシピのように、『ジャガイモの皮をむく』『次にそれを切る』『鍋に入れる』と、手順を具体的に記述していく。」
三郎先生: 「three.jsは、ブラウザで3Dグラフィックスを描画するための低レベルなAPIであるWebGLを、我々が扱いやすいようにしてくれるライブラリだ。パワフルで自由度が高い分、オブジェクトの生成から描画まで、開発者がその手順を細かく管理する必要があるんだね。」
宣言的プログラミング - Whatを伝える
三郎先生: 「そして、カエル君が『こうだったら良いな』と言った、<cube />
のような書き方が、宣言的なアプローチだ。これは、**『何を(What)』**実現したいか、その最終的な「状態」を記述するスタイルだよ。レストランで『カレーライスが食べたい』と注文するようなものだね。作り方の詳細はシェフ(フレームワーク)に任せる。」
三郎先生: 「HTMLや、React/VueといったモダンなUIフレームワークは、この宣言的なアプローチを積極的に採用している。開発者は『最終的に画面がどうなっていて欲しいか』を記述するだけで、その状態を実現するための具体的なDOM操作などの面倒な手続きは、フレームワークが裏で賢くやってくれるんだ。」
具体例:Webページのボタン操作
三郎先生: 「いまいちピンとこないかい?では、Webページにボタンを置いて、クリックしたらメッセージを出す、という簡単な例で見てみようか。」
昔ながらの手続き的な書き方
三郎先生: 「昔はね、このようにDOMを直接操作していたんだ。」
// 1. button要素を作る
const button = document.createElement('button');
// 2. ボタンのテキストを設定する
button.textContent = 'クリックしてね';
// 3. ボタンがクリックされた時の動作を決める
button.addEventListener('click', () => {
alert('こんにちは!');
});
// 4. 最後に、HTMLの本体(body)に追加する
document.body.appendChild(button);
三郎先生: 「『要素を作り』『設定し』『動作を加え』『追加する』。ここでも、一つ一つの手順を細かく命令しているだろう?」
モダンな宣言的な書き方(フレームワーク風)
三郎先生: 「それに対して、今のモダンなフレームワークを使うと、こんな風に書けるんだ。」
// ReactやVueのようなフレームワークのイメージ
function App() {
const handleClick = () => {
alert('こんにちは!');
};
// 『ボタンがあって、クリックしたらこう動いてほしい』とお願いするだけ
return <button onClick={handleClick}>クリックしてね</button>;
}
三郎先生: 「どうだい?後者の方が、実現したい『状態』、つまり『こういうボタンが欲しい』という気持ちが、そのままコードになっているように見えないかい?面倒なDOM操作の命令は、全てフレームワークが肩代わりしてくれるんだ。」
なぜThree.jsは手続き的なのか?
カエル君: 「なるほど!宣言的な方が、見た目もスッキリして分かりやすいケロ!じゃあ、どうしてthree.jsも、もっと宣言的にしてくれないんだケロか?」
三郎先生: 「核心を突く質問だね。それには、大きく分けて2つの理由があるんだ。
1. パフォーマンスと柔軟性の最大化
三郎先生: 「3Dグラフィックスの世界では、毎秒60回もの頻度で画面を更新する必要がある。その中で、たくさんのオブジェクトを動かしたり、光の計算をしたりと、膨大な処理が行われる。宣言的なライブラリは、その便利さのために、裏側で我々には見えない処理をたくさんやっている。それが時には、パフォーマンスの足かせになることもあるんだ。」
三郎先生: 「three.jsは、開発者が望めば、GPUのメモリ管理や描画のタイミングといった、非常に低いレイヤーまで手を出せるように作られている。つまり、『この処理は今やる必要がないから後回しにしよう』とか、『このデータはもう使わないからメモリから解放しよう』といった、極限の最適化を我々の手で直接命令できる。この完全な制御こそが、複雑で大規模な3Dアプリケーションを実現するための鍵なんだよ。」
カエル君: 「ひぇ〜!なんだかF1マシンの運転席みたいだケロ!スイッチがいっぱいで、速く走るためには全部自分で操作しなきゃいけない、みたいな…。」
カエル君は、少しだけ目を回しました。
2. 変化し続ける状態の管理
三郎先生: 「その通りだ、カエル君。そしてもう一つ。WebページのUIは、ユーザーがボタンを押すなど、何らかのアクションがあった時に変化するのが基本だ。しかし、3D空間では、オブジェクトが常にアニメーションしていたり、物理演算で互いに影響し合ったりと、状態が常に、そして連続的に変化し続ける。このような絶え間ない変化を管理するには、『毎フレーム、これをこう動かせ』と手続き的に命令する方が、直感的で効率が良い場面が多いんだ。」
カエル君: 「なるほど!アクションゲームのキャラクターみたいに、ずっと走り回ってるものを管理するには、『右へ行け!ジャンプしろ!』って、その都度命令する方が分かりやすいってことだケロね!」
カエル君は、得意なゲームに例えられて、ぱっと顔を輝かせました。
宣言的ライブラリの力:react-three-fiber
三郎先生: 「しかしカエル君、君の『宣言的に書きたい』という願いも、もちろん叶えることができるんだよ。そのために、react-three-fiber
のような、three.jsを宣言的に扱うための素晴らしいライブラリが存在するんだ。」
カエル君: 「わあ!それって、どういう仕組みなんだケロ?」
三郎先生: 「うむ。それは、three.jsの手続き的な部分をライブラリが肩代わりし、我々はReactのコンポーネントを書くことに集中できるようにしてくれるんだ。例を見てみようか。ここに、『マウスの位置に合わせて回転する箱』をそれぞれの方法で書いたものがある。」
three.jsで書いた場合(手続き的)
import * as THREE from 'three';
// ... scene, camera, rendererのセットアップ ...
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshNormalMaterial();
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// マウス座標を記録する変数
const mouse = new THREE.Vector2();
window.addEventListener('mousemove', (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
});
// アニメーションループ
function animate() {
requestAnimationFrame(animate);
// 毎フレーム、キューブの回転を更新する命令
cube.rotation.y += (mouse.x - cube.rotation.y) * 0.05;
cube.rotation.x += (-mouse.y - cube.rotation.x) * 0.05;
renderer.render(scene, camera);
}
animate();
react-three-fiber
で書いた場合(宣言的)
import { Canvas, useFrame } from '@react-three/fiber';
import { useRef } from 'react';
function Box() {
const meshRef = useRef();
// useFrameフックが、毎フレームの処理を抽象化してくれる
useFrame((state, delta) => {
// stateから直接マウス座標を取得できる
const { x, y } = state.mouse;
// ref経由でオブジェクトを操作
meshRef.current.rotation.y += (x - meshRef.current.rotation.y) * 0.05;
meshRef.current.rotation.x += (-y - meshRef.current.rotation.x) * 0.05;
});
// 『ここにあってほしいもの』をJSXで記述するだけ
return (
<mesh ref={meshRef}>
<boxGeometry />
<meshNormalMaterial />
</mesh>
);
}
function App() {
return (
<Canvas>
<Box />
</Canvas>
);
}
カエル君: 「うわー!全然違うケロ!最初のは、なんだか呪文の巻物みたいで、ちょっと読むのが大変…。でも、react-three-fiber
の方は、HTMLみたいにスッキリしていて、何がしたいのか一目で分かるケロ!<Box />
くんがいる!」
三郎先生: 「その通り! react-three-fiber
の方では、scene.add
やrenderer.render
、requestAnimationFrame
といったお決まりの命令が一切見当たらないだろう? 我々はただ、<Box />
という『状態』を宣言するだけ。するとライブラリが、裏側で良きに計らって、three.jsの手続き的なコードに変換してくれるんだ。これにより、Reactの持つ状態管理の仕組みや、コンポーネントの再利用性といった恩恵を、3Dの世界でも受けられるようになるんだよ。」
カエル君: 「すごい!つまり、難しいF1マシンの運転(three.js)を、優秀な自動運転システム(react-three-fiber)が手伝ってくれる、みたいなことだケロか!?それなら僕にもできそう!」
まとめ
カエル君: 「先生、ありがとうケロ!同じことをするのにも、全く違う書き方があるんだケロね!手続き的な自由度もすごいし、宣言的な分かりやすさも魅力的だケロ!両方とも仲良くなれそうな気がしてきたんだケロ!」
三郎先生: 「うむ。どちらが良い悪いではなく、適材適所だ。作りたいものや、チームのスキルに合わせて、最適な道具を選ぶことが大切なんだよ。その判断ができるようになったら、カエル君も立派な一人前のエンジニアだね。」
(第13話へつづく)