シーン構築

R3F は薄いラッパなので、シーン作りは Three.js そのものの知識が直に効く。 ライト・マテリアル・ジオメトリ・カメラを R3F の流儀で書くとどうなるかをまとめる。

シーンの最低限

オブジェクトを「見せる」には少なくとも カメラ + ライト + マテリアルが必要。 MeshStandardMaterialMeshPhysicalMaterial はライトがないと真っ黒。

最小の絵が出るシーン
<Canvas camera={{ position: [3, 2, 5], fov: 50 }} shadows>
  <color attach="background" args={["#0b0d12"]} />
  <ambientLight intensity={0.4} />
  <directionalLight position={[3, 5, 4]} intensity={1.2} castShadow />

  <mesh castShadow receiveShadow>
    <boxGeometry />
    <meshStandardMaterial color="#7dd3fc" />
  </mesh>

  <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.5, 0]} receiveShadow>
    <planeGeometry args={[10, 10]} />
    <meshStandardMaterial color="#1f2430" />
  </mesh>
</Canvas>

<color attach="background" /> はシーンの background プロパティに THREE.Color を attach する書き方。

ライト

JSX用途
<ambientLight>全方向から一様。陰影なし。底上げ用。不可
<hemisphereLight>上半球と下半球で別の色。屋外擬似 GI。不可
<directionalLight>並行光。太陽光。
<pointLight>点光源。電球。距離減衰あり。
<spotLight>スポット。angle/penumbra
<rectAreaLight>矩形面光源。Standard/Physical のみ反応不可

3点照明の典型

<>
  <ambientLight intensity={0.3} />
  <directionalLight position={[5, 5, 5]} intensity={1.5} castShadow />        {/* キー */}
  <directionalLight position={[-5, 3, -3]} intensity={0.8} color="#aaccff" /> {/* フィル */}
  <directionalLight position={[0, 4, -8]} intensity={1.2} color="#ffaaaa" /> {/* リム */}
</>
屋内っぽさ・屋外っぽさ

環境光(IBL)を入れると一気にリアルになる。drei の <Environment preset="city" /> で 1行で導入できる。Standard / Physical Material は環境光に反応する。

影を出すには3点セットが必要:

  1. <Canvas shadows> で shadowmap 有効化
  2. 影を落とす光源(directionalLight / spotLight / pointLight)に castShadow
  3. 影を落とすメッシュに castShadow、影を受けるメッシュに receiveShadow
<directionalLight
  position={[3, 5, 4]}
  intensity={1.2}
  castShadow
  shadow-mapSize-width={2048}
  shadow-mapSize-height={2048}
  shadow-camera-far={20}
  shadow-camera-left={-5}
  shadow-camera-right={5}
  shadow-camera-top={5}
  shadow-camera-bottom={-5}
  shadow-bias={-0.0005}
  shadow-normalBias={0.02}
/>

shadow camera を可視化

範囲が合っていないと影が切れる。デバッグ時はヘルパーで可視化:

import { useHelper } from "@react-three/drei"
import { CameraHelper } from "three"

function Sun() {
  const ref = useRef<THREE.DirectionalLight>(null!)
  useHelper(ref.current?.shadow && { current: ref.current.shadow.camera }, CameraHelper)
  return <directionalLight ref={ref} castShadow position={[5, 10, 5]} />
}

静的シーンは影を「焼く」

動かないライト・メッシュなら1回計算して固定:

import { BakeShadows } from "@react-three/drei"

<Canvas shadows>
  <BakeShadows />
  ...
</Canvas>

柔らかい接地影

全シャドウを焼けない時、床への影だけを綺麗に出すなら drei の <ContactShadows />:

import { ContactShadows } from "@react-three/drei"

<ContactShadows
  position={[0, -0.5, 0]}
  opacity={0.5}
  scale={10}
  blur={2.5}
  far={4}
/>

ジオメトリ

Three.js の組み込みジオメトリは全部使える。よく使うのは:

JSX主な引数
<boxGeometry args={[w, h, d]} />幅・高さ・奥行き
<sphereGeometry args={[r, ws, hs]} />半径・縦横セグメント
<planeGeometry args={[w, h]} />平面
<cylinderGeometry args={[topR, bottomR, h, segs]} />円柱
<coneGeometry args={[r, h, segs]} />円錐
<torusGeometry args={[r, tube, radSeg, tubSeg]} />ドーナツ
<torusKnotGeometry />結び目
<icosahedronGeometry args={[r, detail]} />正二十面体(球の近似に)
<capsuleGeometry args={[r, length, capSeg, radSeg]} />カプセル
<tubeGeometry args={[curve, segs, r, radSeg]} />カーブに沿ったチューブ
<extrudeGeometry args={[shape, opts]} />2D Shape の押し出し

BufferGeometry を自前で組む

function Triangle() {
  const positions = useMemo(() => new Float32Array([
     0,  1, 0,
    -1, -1, 0,
     1, -1, 0,
  ]), [])

  return (
    <mesh>
      <bufferGeometry>
        <bufferAttribute
          attach="attributes-position"
          args={[positions, 3]}
        />
      </bufferGeometry>
      <meshBasicMaterial color="lime" wireframe />
    </mesh>
  )
}

マテリアル

JSX性質
<meshBasicMaterial>ライト無視。常に同じ明るさ。HUD向き。
<meshLambertMaterial>軽い拡散反射。古典的。
<meshPhongMaterial>古典 PBR 風。
<meshStandardMaterial>定番 PBRroughness/metalness
<meshPhysicalMaterial>Standard を拡張。clearcoat/transmission(ガラス)。
<meshToonMaterial>セルシェーディング。
<meshNormalMaterial>面法線で着色(デバッグ)。
<meshMatcapMaterial>matcap テクスチャでライト不要のリッチ表現。
<shaderMaterial>自作 GLSL(シェーダ参照)。

テクスチャを当てる

drei の useTexture が一番楽。useLoader(TextureLoader, ...) でも同じ結果。

import { useTexture } from "@react-three/drei"

function Wall() {
  const props = useTexture({
    map:          "/wall_color.jpg",
    normalMap:    "/wall_normal.jpg",
    roughnessMap: "/wall_rough.jpg",
    aoMap:        "/wall_ao.jpg",
  })
  return (
    <mesh>
      <planeGeometry args={[3, 3]} />
      <meshStandardMaterial {...props} />
    </mesh>
  )
}

マテリアルの動的調整

function Glass() {
  return (
    <mesh>
      <sphereGeometry args={[1, 64, 64]} />
      <meshPhysicalMaterial
        transmission={1}
        thickness={0.5}
        roughness={0.05}
        ior={1.5}
        clearcoat={1}
        clearcoatRoughness={0}
      />
    </mesh>
  )
}

カメラ

Canvas の camera prop で済むなら一番楽。明示的に置きたい時は drei の <PerspectiveCamera makeDefault /> がきれい。

カメラを動かす

追従カメラの典型パターン

function ChaseCam({ target }: { target: THREE.Object3D }) {
  const tmp = useRef(new THREE.Vector3()).current
  useFrame((state) => {
    target.getWorldPosition(tmp)
    state.camera.position.lerp(tmp.clone().add(new THREE.Vector3(0, 2, 5)), 0.1)
    state.camera.lookAt(tmp)
  })
  return null
}

グルーピング

<group>THREE.Group。位置・回転・スケールを子ごと変換する箱。

function Robot() {
  return (
    <group position={[0, 0, 0]} rotation={[0, Math.PI / 4, 0]}>
      <Body />
      <Head position={[0, 1, 0]} />
      <Arm position={[0.5, 0.5, 0]} />
      <Arm position={[-0.5, 0.5, 0]} />
    </group>
  )
}

<primitive> で既存の Three.js オブジェクトを差し込む

const gltf = useLoader(GLTFLoader, "/model.glb")
return <primitive object={gltf.scene} position={[0, 0, 0]} />

同じインスタンスを複数置きたい時は drei の <Clone>:

import { Clone } from "@react-three/drei"

const { scene } = useGLTF("/tree.glb")

<>
  <Clone object={scene} position={[0, 0, 0]} />
  <Clone object={scene} position={[3, 0, 0]} />
  <Clone object={scene} position={[6, 0, 0]} />
</>

背景 / 環境

import { Environment, Sky } from "@react-three/drei"

<Environment preset="sunset" background />
{/* または */}
<Sky sunPosition={[100, 20, 100]} />

Fog(霧)

{/* 線形フォグ */}
<fog attach="fog" args={["#0b0d12", 5, 30]} />

{/* 指数フォグ */}
<fogExp2 attach="fog" args={["#0b0d12", 0.05]} />

シーンの探索 / traverse

ロードされた GLTF の中身を一括加工する時:

function Tweaked() {
  const { scene } = useGLTF("/model.glb")
  useEffect(() => {
    scene.traverse((o) => {
      if (o.isMesh) {
        o.castShadow = true
        o.receiveShadow = true
        o.material.envMapIntensity = 1.5
      }
    })
  }, [scene])
  return <primitive object={scene} />
}

ヘルパー

デバッグ可視化。drei の useHelper が便利。

import { useHelper } from "@react-three/drei"
import { BoxHelper } from "three"

function Box() {
  const ref = useRef<THREE.Mesh>(null!)
  useHelper(ref, BoxHelper, "yellow")
  return <mesh ref={ref}> ... </mesh>
}

軸とグリッド:

<axesHelper args={[3]} />          {/* X赤 Y緑 Z青 */}
<gridHelper args={[10, 10]} />

シーン全体に効く drei コンポーネント

<Stage /> の例

import { Stage } from "@react-three/drei"

<Canvas shadows>
  <Stage environment="city" intensity={0.5} adjustCamera shadows="contact">
    <MyModel />
  </Stage>
</Canvas>

これだけでライティング・接地影・カメラ調整が全部入る。プロトタイプに最強。