frontendBaby

公開日

- 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.addrenderer.renderrequestAnimationFrameといったお決まりの命令が一切見当たらないだろう? 我々はただ、<Box />という『状態』を宣言するだけ。するとライブラリが、裏側で良きに計らって、three.jsの手続き的なコードに変換してくれるんだ。これにより、Reactの持つ状態管理の仕組みや、コンポーネントの再利用性といった恩恵を、3Dの世界でも受けられるようになるんだよ。」

カエル君: 「すごい!つまり、難しいF1マシンの運転(three.js)を、優秀な自動運転システム(react-three-fiber)が手伝ってくれる、みたいなことだケロか!?それなら僕にもできそう!」

まとめ

カエル君: 「先生、ありがとうケロ!同じことをするのにも、全く違う書き方があるんだケロね!手続き的な自由度もすごいし、宣言的な分かりやすさも魅力的だケロ!両方とも仲良くなれそうな気がしてきたんだケロ!」

三郎先生: 「うむ。どちらが良い悪いではなく、適材適所だ。作りたいものや、チームのスキルに合わせて、最適な道具を選ぶことが大切なんだよ。その判断ができるようになったら、カエル君も立派な一人前のエンジニアだね。」


(第13話へつづく)