mcpField
T
threejs-scene-builder

threejs-scene-builder

github.com/jarrodmedrano開発281 回インストール40 週間前に更新
リポジトリ

threejs-scene-builder について

threejs-scene-builder は、Three.js と React Three Fiber による 3D ウェブシーン開発向けスキルです。シーン構築、描画、アニメーション、キャラクター、プロシージャル生成、NPC の動作、物理の例を扱います。

主な機能

  1. 01Three.js のシーン構築と描画
  2. 02React Three Fiber の統合
  3. 03キャラクターアニメーションとリターゲティング
  4. 04地形とモデルのプロシージャル生成
  5. 05NPC の動作と物理の例

活用シーン

  1. 01インタラクティブな 3D ウェブシーンの作成
  2. 02アニメーションキャラクターやアバターの追加
  3. 03プロシージャル環境と NPC の試作

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jarrodmedrano/jarrod-claude-skills --skill threejs-scene-builder

インストール後、Claude Code で/threejs-scene-builderを入力して読み込まれたか確認します。

同梱ファイルscripts/build-scene.ts

詳細

Three.js & React Three Fiber - 3D 開発完全ガイド

Three.js と React Three Fiber(R3F)を使用して高度な 3D ウェブ体験を作成するための包括的なガイドです。シーンのセットアップ、キャラクターシステム、NPC、プロシージャル生成、アニメーションのリターゲティング、物理演算、インタラクティブなゲームプレイを扱います。

使用する場面

  • 「Three.js のシーンを作成」/「3D シーンをセットアップ」
  • 「React Three Fiber をセットアップ」/「R3F アプリを作成」
  • 「WebGL ビジュアライゼーションを生成」/「インタラクティブな 3D グラフィックスを作成」
  • 「3D キャラクターを追加」/「アバターシステムをセットアップ」
  • 「NPC AI を作成」/「ゲームキャラクターを追加」
  • 「プロシージャル 3D 生成」/「3D コンテンツを生成」
  • 「アニメーションをリターゲティング」/「Mixamo から Three.js へ」
  • 「キャラクターコントローラーをセットアップ」/「物理演算を追加」
  • 「Ready Player Me の統合」
  • 「Three.js で 3D ゲームを作成」

手順

1. Three.js のインストール
npm install three
# TypeScript types
npm install --save-dev @types/three
2. 基本的なシーンのセットアップ

TypeScript:

// scene.ts
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';

export class SceneManager {
  private scene: THREE.Scene;
  private camera: THREE.PerspectiveCamera;
  private renderer: THREE.WebGLRenderer;
  private controls: OrbitControls;
  private animationId: number | null = null;

  constructor(container: HTMLElement) {
    // Scene
    this.scene = new THREE.Scene();
    this.scene.background = new THREE.Color(0x1a1a1a);
    this.scene.fog = new THREE.Fog(0x1a1a1a, 10, 50);

    // Camera
    this.camera = new THREE.PerspectiveCamera(
      75,
      window.innerWidth / window.innerHeight,
      0.1,
      1000
    );
    this.camera.position.set(5, 5, 5);
    this.camera.lookAt(0, 0, 0);

    // Renderer
    this.renderer = new THREE.WebGLRenderer({
      antialias: true,
      alpha: true,
    });
    this.renderer.setSize(window.innerWidth, window.innerHeight);
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    this.renderer.shadowMap.enabled = true;
    this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    container.appendChild(this.renderer.domElement);

    // Controls
    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
    this.controls.enableDamping = true;
    this.controls.dampingFactor = 0.05;
    this.controls.minDistance = 2;
    this.controls.maxDistance = 20;

    // Lights
    this.setupLights();

    // Handle resize
    window.addEventListener('resize', () => this.onWindowResize());

    // Start animation
    this.animate();
  }

  private setupLights() {
    // Ambient light
    const ambient = new THREE.AmbientLight(0xffffff, 0.4);
    this.scene.add(ambient);

    // Directional light (sun)
    const directional = new THREE.DirectionalLight(0xffffff, 0.8);
    directional.position.set(5, 10, 5);
    directional.castShadow = true;
    directional.shadow.camera.left = -10;
    directional.shadow.camera.right = 10;
    directional.shadow.camera.top = 10;
    directional.shadow.camera.bottom = -10;
    directional.shadow.mapSize.width = 2048;
    directional.shadow.mapSize.height = 2048;
    this.scene.add(directional);

    // Hemisphere light
    const hemisphere = new THREE.HemisphereLight(0xffffbb, 0x080820, 0.3);
    this.scene.add(hemisphere);
  }

  private onWindowResize() {
    this.camera.aspect = window.innerWidth / window.innerHeight;
    this.camera.updateProjectionMatrix();
    this.renderer.setSize(window.innerWidth, window.innerHeight);
  }

  private animate() {
    this.animationId = requestAnimationFrame(() => this.animate());
    this.controls.update();
    this.renderer.render(this.scene, this.camera);
  }

  public addObject(object: THREE.Object3D) {
    this.scene.add(object);
  }

  public dispose() {
    if (this.animationId !== null) {
      cancelAnimationFrame(this.animationId);
    }
    this.renderer.dispose();
    this.controls.dispose();
  }
}

// Usage
const container = document.getElementById('canvas-container')!;
const sceneManager = new SceneManager(container);

// Add a cube
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
cube.castShadow = true;
cube.receiveShadow = true;
sceneManager.addObject(cube);
3. 高度なマテリアルのセットアップ
// materials.ts
import * as THREE from 'three';

export class MaterialLibrary {
  // PBR Material
  static createPBRMaterial(options: {
    color?: number;
    roughness?: number;
    metalness?: number;
    normalMap?: THREE.Texture;
    roughnessMap?: THREE.Texture;
  }) {
    return new THREE.MeshStandardMaterial({
      color: options.color ?? 0xffffff,
      roughness: options.roughness ?? 0.5,
      metalness: options.metalness ?? 0.5,
      normalMap: options.normalMap,
      roughnessMap: options.roughnessMap,
    });
  }

  // Glass Material
  static createGlassMaterial() {
    return new THREE.MeshPhysicalMaterial({
      color: 0xffffff,
      metalness: 0,
      roughness: 0,
      transmission: 1,
      thickness: 0.5,
    });
  }

  // Glowing Material
  static createGlowMaterial(color: number = 0x00ff00) {
    return new THREE.MeshBasicMaterial({
      color,
      transparent: true,
      opacity: 0.8,
      blending: THREE.AdditiveBlending,
    });
  }

  // Toon Material
  static createToonMaterial(color: number = 0x00ff00) {
    return new THREE.MeshToonMaterial({
      color,
      gradientMap: this.createGradientTexture(),
    });
  }

  private static createGradientTexture() {
    const canvas = document.createElement('canvas');
    canvas.width = 256;
    canvas.height = 1;
    const ctx = canvas.getContext('2d')!;
    const gradient = ctx.createLinearGradient(0, 0, 256, 0);
    gradient.addColorStop(0, '#000000');
    gradient.addColorStop(0.5, '#808080');
    gradient.addColorStop(1, '#ffffff');
    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, 256, 1);
    return new THREE.CanvasTexture(canvas);
  }
}
4. ジオメトリヘルパー
// geometries.ts
import * as THREE from 'three';

export class GeometryHelpers {
  static createGroundPlane(size: number = 20) {
    const geometry = new THREE.PlaneGeometry(size, size);
    const material = new THREE.MeshStandardMaterial({
      color: 0x808080,
      roughness: 0.8,
      metalness: 0.2,
    });
    const plane = new THREE.Mesh(geometry, material);
    plane.rotation.x = -Math.PI / 2;
    plane.receiveShadow = true;
    return plane;
  }

  static createSkybox(textureLoader: THREE.TextureLoader, path: string) {
    const loader = new THREE.CubeTextureLoader();
    const texture = loader.load([
      `${path}/px.jpg`, `${path}/nx.jpg`,
      `${path}/py.jpg`, `${path}/ny.jpg`,
      `${path}/pz.jpg`, `${path}/nz.jpg`,
    ]);
    return texture;
  }

  static createParticles(count: number = 1000) {
    const geometry = new THREE.BufferGeometry();
    const positions = new Float32Array(count * 3);

    for (let i = 0; i < count * 3; i++) {
      positions[i] = (Math.random() - 0.5) * 20;
    }

    geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));

    const material = new THREE.PointsMaterial({
      size: 0.05,
      color: 0xffffff,
      transparent: true,
      opacity: 0.8,
      blending: THREE.AdditiveBlending,
    });

    return new THREE.Points(geometry, material);
  }
}
5. アニメーションシステム
// animation.ts
import * as THREE from 'three';

export class AnimationController {
  private mixer: THREE.AnimationMixer;
  private actions: Map<string, THREE.AnimationAction> = new Map();

  constructor(model: THREE.Object3D, animations: THREE.AnimationClip[]) {
    this.mixer = new THREE.AnimationMixer(model);

    animations.forEach((clip, index) => {
      const action = this.mixer.clipAction(clip);
      this.actions.set(clip.name || `animation_${index}`, action);
    });
  }

  play(name: string, fadeIn: number = 0.5) {
    const action = this.actions.get(name);
    if (action) {
      action.reset().fadeIn(fadeIn).play();
    }
  }

  stop(name: string, fadeOut: number = 0.5) {
    const action = this.actions.get(name);
    if (action) {
      action.fadeOut(fadeOut);
    }
  }

  update(deltaTime: number) {
    this.mixer.update(deltaTime);
  }
}
6. モデルの読み込み
// loader.ts
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';

export class ModelLoader {
  private gltfLoader: GLTFLoader;
  private textureLoader: THREE.TextureLoader;
  private loadingManager: THREE.LoadingManager;

  constructor(onProgress?: (progress: number) => void) {
    this.loadingManager = new THREE.LoadingManager();

    if (onProgress) {
      this.loadingManager.onProgress = (url, loaded, total) => {
        onProgress((loaded / total) * 100);
      };
    }

    // Setup DRACO loader for compressed models
    const dracoLoader = new DRACOLoader(this.loadingManager);
    dracoLoader.setDecoderPath('/draco/');

    this.gltfLoader = new GLTFLoader(this.loadingManager);
    this.gltfLoader.setDRACOLoader(dracoLoader);

    this.textureLoader = new THREE.TextureLoader(this.loadingManager);
  }

  async loadGLTF(url: string): Promise<THREE.Group> {
    return new Promise((resolve, reject) => {
      this.gltfLoader.load(
        url,
        (gltf) => {
          gltf.scene.traverse((child) => {
            if (child instanceof THREE.Mesh) {
              child.castShadow = true;
              child.receiveShadow = true;
            }
          });
          resolve(gltf.scene);
        },
        undefined,
        reject
      );
    });
  }

  async loadTexture(url: string): Promise<THREE.Texture> {
    return new Promise((resolve, reject) => {
      this.textureLoader.load(url, resolve, undefined, reject);
    });
  }
}
7. ポストプロセッシング
// post-processing.ts
import * as THREE from 'three';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass';
import { SSAOPass } from 'three/examples/jsm/postprocessing/SSAOPass';

export class PostProcessing {
  private composer: EffectComposer;

  constructor(
    renderer: THREE.WebGLRenderer,
    scene: THREE.Scene,
    camera: THREE.Camera
  ) {
    this.composer = new EffectComposer(renderer);

    // Render pass
    const renderPass = new RenderPass(scene, camera);
    this.composer.addPass(renderPass);

    // Bloom pass
    const bloomPass = new UnrealBloomPass(
      new THREE.Vector2(window.innerWidth, window.innerHeight),
      1.5,  // strength
      0.4,  // radius
      0.85  // threshold
    );
    this.composer.addPass(bloomPass);

    // SSAO pass
    const ssaoPass = new SSAOPass(scene, camera);
    ssaoPass.kernelRadius = 16;
    this.composer.addPass(ssaoPass);
  }

  render() {
    this.composer.render();
  }

  resize(width: number, height: number) {
    this.composer.setSize(width, height);
  }
}
8. パフォーマンスの最適化
// optimization.ts
import * as THREE from 'three';

export class PerformanceOptimizer {
  static optimizeGeometry(geometry: THREE.BufferGeometry) {
    // Merge vertices
    geometry.computeVertexNormals();
    geometry.normalizeNormals();
    return geometry;
  }

  static createLOD(geometries: THREE.BufferGeometry[], distances: number[]) {
    const lod = new THREE.LOD();

    geometries.forEach((geometry, index) => {
      const material = new THREE.MeshStandardMaterial();
      const mesh = new THREE.Mesh(geometry, material);
      lod.addLevel(mesh, distances[index]);
    });

    return lod;
  }

  static enableInstancing(
    geometry: THREE.BufferGeometry,
    material: THREE.Material,
    count: number,
    positions: THREE.Vector3[]
  ) {
    const mesh = new THREE.InstancedMesh(geometry, material, count);

    const matrix = new THREE.Matrix4();
    positions.forEach((pos, i) => {
      matrix.setPosition(pos);
      mesh.setMatrixAt(i, matrix);
    });

    mesh.instanceMatrix.needsUpdate = true;
    return mesh;
  }

  static setupFrustumCulling(camera: THREE.Camera, objects: THREE.Object3D[]) {
    const frustum = new THREE.Frustum();
    const projScreenMatrix = new THREE.Matrix4();

    return () => {
      camera.updateMatrixWorld();
      projScreenMatrix.multiplyMatrices(
        camera.projectionMatrix,
        camera.matrixWorldInverse
      );
      frustum.setFromProjectionMatrix(projScreenMatrix);

      objects.forEach((obj) => {
        obj.visible = frustum.intersectsObject(obj);
      });
    };
  }
}
9. React との統合
// ThreeCanvas.tsx
import React, { useEffect, useRef } from 'react';
import { SceneManager } from './scene';

export const ThreeCanvas: React.FC = () => {
  const containerRef = useRef<HTMLDivElement>(null);
  const sceneManagerRef = useRef<SceneManager | null>(null);

  useEffect(() => {
    if (!containerRef.current) return;

    // Initialize scene
    sceneManagerRef.current = new SceneManager(containerRef.current);

    // Cleanup
    return () => {
      sceneManagerRef.current?.dispose();
    };
  }, []);

  return (
    <div
      ref={containerRef}
      style={{ width: '100vw', height: '100vh' }}
    />
  );
};
10. 完全な例
// main.ts
import * as THREE from 'three';
import { SceneManager } from './scene';
import { GeometryHelpers } from './geometries';
import { MaterialLibrary } from './materials';
import { ModelLoader } from './loader';

async function main() {
  const container = document.getElementById('app')!;
  const scene = new SceneManager(container);

  // Add ground plane
  const ground = GeometryHelpers.createGroundPlane();
  scene.addObject(ground);

  // Add particles
  const particles = GeometryHelpers.createParticles(5000);
  scene.addObject(particles);

  // Load model
  const loader = new ModelLoader((progress) => {
    console.log(`Loading: ${progress.toFixed(0)}%`);
  });

  try {
    const model = await loader.loadGLTF('/models/scene.gltf');
    model.scale.set(2, 2, 2);
    scene.addObject(model);
  } catch (error) {
    console.error('Failed to load model:', error);
  }

  // Add rotating cube with PBR material
  const cubeGeo = new THREE.BoxGeometry();
  const cubeMat = MaterialLibrary.createPBRMaterial({
    color: 0x2194ce,
    roughness: 0.3,
    metalness: 0.8,
  });
  const cube = new THREE.Mesh(cubeGeo, cubeMat);
  cube.position.y = 1;
  scene.addObject(cube);

  // Animation loop
  function animate() {
    requestAnimationFrame(animate);
    cube.rotation.y += 0.01;
  }
  animate();
}

main();
ベストプラクティス

推奨事項:

  • BufferGeometry を使用する
  • フラスタムカリングを有効にする
  • 多数のオブジェクトにはインスタンス化メッシュを使用する
  • テクスチャを最適化する
  • 遠くのオブジェクトには LOD を使用する
  • ジオメトリとマテリアルを破棄する
  • シャドウマップの数を制限する
  • ポストプロセッシングは控えめに使用する

避けるべきこと:

  • レンダリングループ内でオブジェクトを作成する
  • LOD なしで高ポリゴンモデルを使用する
  • リソースの破棄を忘れる
  • ライトを多用する
  • テクスチャ圧縮を省略する
  • モバイルでデバイス本来のピクセル比でレンダリングする
  • 不必要に毎フレーム uniform を更新する

🚀 パート 2:React Three Fiber(R3F)

React Three Fiber は Three.js 用の React レンダラーで、宣言的なコンポーネントベースの 3D 開発を可能にします。

インストール
npm install three @react-three/fiber
# Ecosystem libraries
npm install @react-three/drei @react-three/postprocessing
npm install postprocessing
npm install --save-dev @types/three
基本的な R3F シーン
// App.tsx
import { useRef, useState } from 'react';
import { Canvas, useFrame, MeshProps } from '@react-three/fiber';
import { OrbitControls, Environment, Sky } from '@react-three/drei';
import * as THREE from 'three';

function Box(props: MeshProps) {
  const meshRef = useRef<THREE.Mesh>(null);
  const [hovered, setHover] = useState(false);
  const [active, setActive] = useState(false);

  useFrame((state, delta) => {
    if (meshRef.current) {
      meshRef.current.rotation.x += delta;
    }
  });

  return (
    <mesh
      {...props}
      ref={meshRef}
      scale={active ? 1.5 : 1}
      onClick={() => setActive(!active)}
      onPointerOver={() => setHover(true)}
      onPointerOut={() => setHover(false)}
    >
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
    </mesh>
  );
}

export default function App() {
  return (
    <Canvas camera={{ position: [0, 0, 5], fov: 75 }}>
      <ambientLight intensity={0.5} />
      <directionalLight position={[10, 10, 5]} intensity={1} />
      <Box position={[0, 0, 0]} />
      <OrbitControls />
      <Sky sunPosition={[100, 20, 100]} />
      <Environment preset="sunset" />
    </Canvas>
  );
}
高度な R3F シーンマネージャー
// Scene.tsx
import { useRef, useMemo } from 'react';
import { Canvas, useFrame, useThree } from '@react-three/fiber';
import {
  OrbitControls,
  PerspectiveCamera,
  Environment,
  ContactShadows,
  useGLTF,
  useTexture,
  Html,
  Stats,
} from '@react-three/drei';
import {
  EffectComposer,
  Bloom,
  DepthOfField,
  Vignette,
  SSAO,
} from '@react-three/postprocessing';
import * as THREE from 'three';

// Scene configuration
interface SceneConfig {
  enablePostProcessing?: boolean;
  enableShadows?: boolean;
  enableStats?: boolean;
  toneMappingExposure?: number;
}

// Main scene component
function Scene({ config }: { config: SceneConfig }) {
  const { gl, scene } = useThree();

  // Configure renderer
  useMemo(() => {
    gl.shadowMap.enabled = config.enableShadows ?? true;
    gl.shadowMap.type = THREE.PCFSoftShadowMap;
    gl.toneMapping = THREE.ACESFilmicToneMapping;
    gl.toneMappingExposure = config.toneMappingExposure ?? 1;
    scene.background = new THREE.Color(0x1a1a1a);
  }, [gl, scene, config]);

  return (
    <>
      {config.enableStats && <Stats />}

      {/* Lighting */}
      <ambientLight intensity={0.3} />
      <directionalLight
        position={[10, 10, 5]}
        intensity={1}
        castShadow
        shadow-mapSize={[2048, 2048]}
        shadow-camera-left={-10}
        shadow-camera-right={10}
        shadow-camera-top={10}
        shadow-camera-bottom={-10}
      />
      <hemisphereLight args={[0xffffbb, 0x080820, 0.2]} />

      {/* Environment */}
      <Environment preset="city" background={false} />

      {/* Your 3D content here */}
    </>
  );
}

// App with Canvas wrapper
export default function App() {
  return (
    <Canvas shadows dpr={[1, 2]}>
      <Scene config={{ enablePostProcessing: true, enableShadows: true }} />
      <OrbitControls makeDefault />

      {/* Post-processing */}
      <EffectComposer>
        <Bloom luminanceThreshold={1} intensity={0.5} />
        <DepthOfField focusDistance={0.01} focalLength={0.2} bokehScale={3} />
        <SSAO />
        <Vignette opacity={0.5} />
      </EffectComposer>
    </Canvas>
  );
}
R3F フックとベストプラクティス
// hooks/useAnimatedModel.ts
import { useRef, useEffect } from 'react';
import { useGLTF, useAnimations } from '@react-three/drei';
import * as THREE from 'three';

export function useAnimatedModel(modelPath: string, animationName?: string) {
  const group = useRef<THREE.Group>(null);
  const { scene, animations } = useGLTF(modelPath);
  const { actions, names } = useAnimations(animations, group);

  useEffect(() => {
    if (animationName && actions[animationName]) {
      actions[animationName]?.play();
    } else if (names.length > 0 && actions[names[0]]) {
      actions[names[0]]?.play();
    }
  }, [actions, animationName, names]);

  return { group, scene, actions, names };
}

// Usage
function AnimatedCharacter() {
  const { group, scene } = useAnimatedModel('/models/character.glb', 'Idle');

  return (
    <group ref={group}>
      <primitive object={scene} />
    </group>
  );
}
Drei ライブラリ - 必須ヘルパー
import {
  // Cameras & Controls
  PerspectiveCamera, OrthographicCamera, OrbitControls,
  FlyControls, PointerLockControls, TrackballControls,

  // Staging
  Environment, Sky, Stars, Cloud, ContactShadows,
  BakeShadows, AccumulativeShadows, RandomizedLight,

  // Abstractions
  Box, Sphere, Plane, Cone, Cylinder, Torus,
  Text, Text3D, Html, Billboard,

  // Loaders
  useGLTF, useFBX, useTexture, useCubeTexture,

  // Performance
  Instances, Instance, Merged, Detailed,

  // Misc
  useHelper, Grid, GizmoHelper, Stats, PerformanceMonitor,
  Center, Bounds, Shadow, Reflector
} from '@react-three/drei';

// Example: Performance-optimized instances
function Trees({ count = 100 }) {
  const { nodes } = useGLTF('/models/tree.glb');

  return (
    <Instances limit={count} geometry={nodes.tree.geometry}>
      <meshStandardMaterial color="green" />
      {Array.from({ length: count }, (_, i) => (
        <Instance
          key={i}
          position={[
            (Math.random() - 0.5) * 50,
            0,
            (Math.random() - 0.5) * 50,
          ]}
          rotation={[0, Math.random() * Math.PI * 2, 0]}
          scale={0.5 + Math.random() * 0.5}
        />
      ))}
    </Instances>
  );
}

🎮 パート 3:キャラクターシステムとアニメーション

キャラクターアニメーションシステム
// characterAnimation.ts
import * as THREE from 'three';

export class CharacterAnimationController {
  private mixer: THREE.AnimationMixer;
  private actions: Map<string, THREE.AnimationAction> = new Map();
  private currentAction: THREE.AnimationAction | null = null;

  constructor(model: THREE.Object3D, animations: THREE.AnimationClip[]) {
    this.mixer = new THREE.AnimationMixer(model);

    animations.forEach((clip) => {
      const action = this.mixer.clipAction(clip);
      this.actions.set(clip.name, action);
    });
  }

  play(name: string, fadeTime: number = 0.3) {
    const nextAction = this.actions.get(name);
    if (!nextAction) return;

    if (this.currentAction && this.currentAction !== nextAction) {
      this.currentAction.fadeOut(fadeTime);
    }

    nextAction.reset().fadeIn(fadeTime).play();
    this.currentAction = nextAction;
  }

  crossFade(fromName: string, toName: string, duration: number = 0.3) {
    const fromAction = this.actions.get(fromName);
    const toAction = this.actions.get(toName);

    if (fromAction && toAction) {
      fromAction.fadeOut(duration);
      toAction.reset().fadeIn(duration).play();
      this.currentAction = toAction;
    }
  }

  setWeight(name: string, weight: number) {
    const action = this.actions.get(name);
    if (action) action.setEffectiveWeight(weight);
  }

  update(deltaTime: number) {
    this.mixer.update(deltaTime);
  }

  getAction(name: string) {
    return this.actions.get(name);
  }
}
SkeletonUtils を使ったアニメーションのリターゲティング
// retargeting.ts
import * as THREE from 'three';
import { SkeletonUtils } from 'three/examples/jsm/utils/SkeletonUtils';

export class AnimationRetargeter {
  /**
   * Retarget animation from one skeleton to another
   * Based on: https://github.com/upf-gti/retargeting-threejs
   */
  static retargetAnimation(
    sourceClip: THREE.AnimationClip,
    targetSkeleton: THREE.Skeleton,
    sourceSkeleton: THREE.Skeleton
  ): THREE.AnimationClip {
    // Use SkeletonUtils for retargeting
    const retargetedClip = SkeletonUtils.retargetClip(
      targetSkeleton.bones[0],
      sourceSkeleton.bones[0],
      sourceClip,
      {}
    );

    return retargetedClip;
  }

  /**
   * Load Mixamo animation and apply to custom character
   */
  static async loadMixamoAnimation(
    fbxPath: string,
    targetModel: THREE.Object3D,
    loader: any
  ): Promise<THREE.AnimationClip[]> {
    return new Promise((resolve, reject) => {
      loader.load(
        fbxPath,
        (fbx: any) => {
          // Extract animations
          const animations = fbx.animations;

          // Find skeletons
          const targetSkeleton = this.findSkeleton(targetModel);
          const sourceSkeleton = this.findSkeleton(fbx);

          if (!targetSkeleton || !sourceSkeleton) {
            reject(new Error('Skeleton not found'));
            return;
          }

          // Retarget all animations
          const retargetedClips = animations.map((clip: THREE.AnimationClip) =>
            this.retargetAnimation(clip, targetSkeleton, sourceSkeleton)
          );

          resolve(retargetedClips);
        },
        undefined,
        reject
      );
    });
  }

  private static findSkeleton(object: THREE.Object3D): THREE.Skeleton | null {
    let skeleton: THREE.Skeleton | null = null;

    object.traverse((child) => {
      if (child instanceof THREE.SkinnedMesh && child.skeleton) {
        skeleton = child.skeleton;
      }
    });

    return skeleton;
  }
}
Mixamo 統合ワークフロー
// mixamoIntegration.ts
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader';
import * as THREE from 'three';

export class MixamoCharacter {
  private model: THREE.Group;
  private mixer: THREE.AnimationMixer;
  private animations: Map<string, THREE.AnimationClip> = new Map();

  constructor(model: THREE.Group) {
    this.model = model;
    this.mixer = new THREE.AnimationMixer(model);
  }

  /**
   * Load character with embedded animations (GLB from Mixamo)
   */
  static async loadWithAnimations(url: string): Promise<MixamoCharacter> {
    const loader = new GLTFLoader();

    return new Promise((resolve, reject) => {
      loader.load(
        url,
        (gltf) => {
          const character = new MixamoCharacter(gltf.scene);

          // Add animations
          gltf.animations.forEach((clip) => {
            character.animations.set(clip.name, clip);
          });

          resolve(character);
        },
        undefined,
        reject
      );
    });
  }

  /**
   * Add separate animation file (FBX without skin)
   */
  async addAnimation(name: string, fbxPath: string): Promise<void> {
    const loader = new FBXLoader();

    return new Promise((resolve, reject) => {
      loader.load(
        fbxPath,
        (fbx) => {
          if (fbx.animations.length > 0) {
            this.animations.set(name, fbx.animations[0]);
          }
          resolve();
        },
        undefined,
        reject
      );
    });
  }

  play(name: string, loop: boolean = true) {
    const clip = this.animations.get(name);
    if (!clip) return;

    const action = this.mixer.clipAction(clip);
    action.loop = loop ? THREE.LoopRepeat : THREE.LoopOnce;
    action.play();

    return action;
  }

  update(deltaTime: number) {
    this.mixer.update(deltaTime);
  }

  getModel() {
    return this.model;
  }
}

// Usage Example
async function setupMixamoCharacter() {
  // Method 1: Load character with animations
  const character = await MixamoCharacter.loadWithAnimations(
    '/models/character-with-animations.glb'
  );

  // Method 2: Load character and add animations separately
  const character2 = await MixamoCharacter.loadWithAnimations(
    '/models/character.glb'
  );
  await character2.addAnimation('walk', '/animations/walking.fbx');
  await character2.addAnimation('run', '/animations/running.fbx');

  character2.play('walk');
}
Ready Player Me の統合
// ReadyPlayerMeAvatar.tsx
import { useEffect, useRef } from 'react';
import { useGLTF, useAnimations } from '@react-three/drei';
import * as THREE from 'three';

interface AvatarProps {
  avatarId: string;
  animationUrl?: string;
  position?: [number, number, number];
}

export function ReadyPlayerMeAvatar({
  avatarId,
  animationUrl,
  position = [0, 0, 0]
}: AvatarProps) {
  const group = useRef<THREE.Group>(null);
  const avatarUrl = `https://models.readyplayer.me/${avatarId}.glb`;

  const { scene, animations } = useGLTF(avatarUrl);
  const { actions, names } = useAnimations(animations, group);

  useEffect(() => {
    // Apply better lighting to RPM avatars
    scene.traverse((child) => {
      if (child instanceof THREE.Mesh) {
        child.castShadow = true;
        child.receiveShadow = true;

        // Fix material issues
        if (child.material) {
          child.material.envMapIntensity = 1;
        }
      }
    });
  }, [scene]);

  useEffect(() => {
    // Play first animation or idle
    const actionName = names.includes('Idle') ? 'Idle' : names[0];
    if (actionName && actions[actionName]) {
      actions[actionName]?.play();
    }
  }, [actions, names]);

  return (
    <group ref={group} position={position}>
      <primitive object={scene} />
    </group>
  );
}

// Preload avatars for better performance
useGLTF.preload('https://models.readyplayer.me/your-avatar-id.glb');

🤖 パート 4:NPC AI とゲーム内の動作

ゲーム AI 向け Yuka.js の統合
npm install yuka
// npcAI.ts
import * as YUKA from 'yuka';
import * as THREE from 'three';

export class NPCController {
  private entityManager: YUKA.EntityManager;
  private time: YUKA.Time;
  private vehicle: YUKA.Vehicle;
  private mesh: THREE.Mesh;

  constructor(mesh: THREE.Mesh) {
    this.mesh = mesh;
    this.entityManager = new YUKA.EntityManager();
    this.time = new YUKA.Time();

    // Create vehicle (NPC)
    this.vehicle = new YUKA.Vehicle();
    this.vehicle.maxSpeed = 3;
    this.vehicle.maxForce = 10;
    this.vehicle.setRenderComponent(mesh, this.sync);

    this.entityManager.add(this.vehicle);
  }

  // Sync Three.js mesh with Yuka entity
  private sync(entity: YUKA.GameEntity, renderComponent: THREE.Object3D) {
    renderComponent.position.copy(entity.position as any);
    renderComponent.quaternion.copy(entity.rotation as any);
  }

  // Seek behavior - move towards target
  seekTarget(targetPosition: THREE.Vector3) {
    const seekBehavior = new YUKA.SeekBehavior(
      new YUKA.Vector3(targetPosition.x, targetPosition.y, targetPosition.z)
    );
    this.vehicle.steering.add(seekBehavior);
  }

  // Flee behavior - run away from threat
  fleeFrom(threatPosition: THREE.Vector3) {
    const fleeBehavior = new YUKA.FleeBehavior(
      new YUKA.Vector3(threatPosition.x, threatPosition.y, threatPosition.z)
    );
    this.vehicle.steering.add(fleeBehavior);
  }

  // Wander behavior - random movement
  enableWander() {
    const wanderBehavior = new YUKA.WanderBehavior();
    wanderBehavior.radius = 5;
    wanderBehavior.distance = 10;
    wanderBehavior.jitter = 2;
    this.vehicle.steering.add(wanderBehavior);
  }

  // Follow path behavior
  followPath(waypoints: THREE.Vector3[]) {
    const path = new YUKA.Path();
    waypoints.forEach((wp) => {
      path.add(new YUKA.Vector3(wp.x, wp.y, wp.z));
    });
    path.loop = true;

    const followPathBehavior = new YUKA.FollowPathBehavior(path, 0.5);
    this.vehicle.steering.add(followPathBehavior);
  }

  // Obstacle avoidance
  enableObstacleAvoidance(obstacles: YUKA.GameEntity[]) {
    const obstacleAvoidance = new YUKA.ObstacleAvoidanceBehavior(obstacles);
    this.vehicle.steering.add(obstacleAvoidance);
  }

  update() {
    const delta = this.time.update().getDelta();
    this.entityManager.update(delta);
  }

  getVehicle() {
    return this.vehicle;
  }
}
ステートマシンを使った高度な NPC
// npcStateMachine.ts
import * as YUKA from 'yuka';

export class NPCWithStates extends YUKA.GameEntity {
  private stateMachine: YUKA.StateMachine<NPCWithStates>;
  private states: {
    idle: YUKA.State<NPCWithStates>;
    patrol: YUKA.State<NPCWithStates>;
    chase: YUKA.State<NPCWithStates>;
    attack: YUKA.State<NPCWithStates>;
    flee: YUKA.State<NPCWithStates>;
  };

  public target: YUKA.GameEntity | null = null;
  public health: number = 100;
  public detectionRadius: number = 10;
  public attackRange: number = 2;

  constructor() {
    super();
    this.stateMachine = new YUKA.StateMachine(this);
    this.states = this.setupStates();
    this.stateMachine.currentState = this.states.idle;
  }

  private setupStates() {
    // Idle State
    const idleState = new YUKA.State('idle');
    idleState.enter = () => console.log('NPC is idle');
    idleState.execute = (npc: NPCWithStates) => {
      // Check for nearby targets
      if (npc.detectTarget()) {
        npc.stateMachine.changeState(npc.states.chase);
      }
    };

    // Patrol State
    const patrolState = new YUKA.State('patrol');
    patrolState.execute = (npc: NPCWithStates) => {
      // Patrol logic
      if (npc.detectTarget()) {
        npc.stateMachine.changeState(npc.states.chase);
      }
    };

    // Chase State
    const chaseState = new YUKA.State('chase');
    chaseState.enter = () => console.log('NPC is chasing');
    chaseState.execute = (npc: NPCWithStates) => {
      if (!npc.target) {
        npc.stateMachine.changeState(npc.states.idle);
        return;
      }

      const distance = npc.position.distanceTo(npc.target.position);

      if (distance <= npc.attackRange) {
        npc.stateMachine.changeState(npc.states.attack);
      } else if (distance > npc.detectionRadius) {
        npc.stateMachine.changeState(npc.states.idle);
      }
    };

    // Attack State
    const attackState = new YUKA.State('attack');
    attackState.execute = (npc: NPCWithStates) => {
      if (!npc.target) {
        npc.stateMachine.changeState(npc.states.idle);
        return;
      }

      const distance = npc.position.distanceTo(npc.target.position);
      if (distance > npc.attackRange) {
        npc.stateMachine.changeState(npc.states.chase);
      }
    };

    // Flee State
    const fleeState = new YUKA.State('flee');
    fleeState.enter = () => console.log('NPC is fleeing');
    fleeState.execute = (npc: NPCWithStates) => {
      if (npc.health > 30) {
        npc.stateMachine.changeState(npc.states.idle);
      }
    };

    return {
      idle: idleState,
      patrol: patrolState,
      chase: chaseState,
      attack: attackState,
      flee: fleeState,
    };
  }

  detectTarget(): boolean {
    // Implement target detection logic
    return false; // Placeholder
  }

  update(delta: number): this {
    this.stateMachine.update();
    return this;
  }
}
React Three Fiber の NPC コンポーネント
// NPCComponent.tsx
import { useRef, useEffect } from 'react';
import { useFrame } from '@react-three/fiber';
import { useGLTF } from '@react-three/drei';
import { NPCController } from './npcAI';
import * as THREE from 'three';

interface NPCProps {
  modelPath: string;
  initialPosition: [number, number, number];
  behavior: 'wander' | 'patrol' | 'chase';
  waypoints?: [number, number, number][];
  target?: THREE.Vector3;
}

export function NPC({
  modelPath,
  initialPosition,
  behavior,
  waypoints,
  target
}: NPCProps) {
  const { scene } = useGLTF(modelPath);
  const groupRef = useRef<THREE.Group>(null);
  const npcController = useRef<NPCController | null>(null);

  useEffect(() => {
    if (!groupRef.current) return;

    // Find the first mesh in the scene hierarchy
    // For production, use scene.getObjectByName() or traverse to find specific mesh
    let mesh: THREE.Mesh | undefined;
    scene.traverse((child) => {
      if (!mesh && child instanceof THREE.Mesh) {
        mesh = child;
      }
    });

    if (!mesh) return;

    npcController.current = new NPCController(mesh);

    // Setup behavior
    switch (behavior) {
      case 'wander':
        npcController.current.enableWander();
        break;
      case 'patrol':
        if (waypoints) {
          const points = waypoints.map(wp => new THREE.Vector3(...wp));
          npcController.current.followPath(points);
        }
        break;
      case 'chase':
        if (target) {
          npcController.current.seekTarget(target);
        }
        break;
    }
  }, [behavior, waypoints, target, scene]);

  useFrame(() => {
    npcController.current?.update();
  });

  return (
    <group ref={groupRef} position={initialPosition}>
      <primitive object={scene} />
    </group>
  );
}

🎲 パート 5:プロシージャル生成

プロシージャルなキャラクター生成
// proceduralCharacter.ts
import * as THREE from 'three';

interface CharacterParameters {
  height: number;        // 0.5 - 1.5 (multiplier)
  bodyWidth: number;     // 0.7 - 1.3
  headSize: number;      // 0.8 - 1.2
  armLength: number;     // 0.8 - 1.2
  legLength: number;     // 0.9 - 1.1
  skinColor: THREE.Color;
  hairColor: THREE.Color;
}

export class ProceduralCharacterGenerator {
  /**
   * Generate a procedural humanoid character
   * Inspired by parametric models like SMPL
   */
  static generate(params: CharacterParameters): THREE.Group {
    const character = new THREE.Group();

    // Base measurements
    const baseHeight = 1.7 * params.height;
    const headHeight = 0.25 * params.headSize;
    const torsoHeight = 0.45 * baseHeight;
    const legHeight = 0.45 * baseHeight * params.legLength;
    const armLength = 0.35 * baseHeight * params.armLength;

    // Head
    const headGeo = new THREE.SphereGeometry(
      headHeight / 2,
      32,
      32
    );
    const headMat = new THREE.MeshStandardMaterial({
      color: params.skinColor
    });
    const head = new THREE.Mesh(headGeo, headMat);
    head.position.y = baseHeight - headHeight / 2;
    head.castShadow = true;
    character.add(head);

    // Torso
    const torsoGeo = new THREE.BoxGeometry(
      0.3 * params.bodyWidth,
      torsoHeight,
      0.2
    );
    const torso = new THREE.Mesh(torsoGeo, headMat);
    torso.position.y = baseHeight - headHeight - torsoHeight / 2;
    torso.castShadow = true;
    character.add(torso);

    // Arms
    const armGeo = new THREE.CylinderGeometry(0.04, 0.04, armLength, 8);
    const leftArm = new THREE.Mesh(armGeo, headMat);
    leftArm.position.set(
      -0.15 * params.bodyWidth - 0.04,
      baseHeight - headHeight - armLength / 2 - 0.05,
      0
    );
    leftArm.castShadow = true;
    character.add(leftArm);

    const rightArm = leftArm.clone();
    rightArm.position.x *= -1;
    character.add(rightArm);

    // Legs
    const legGeo = new THREE.CylinderGeometry(0.06, 0.05, legHeight, 8);
    const leftLeg = new THREE.Mesh(legGeo, headMat);
    leftLeg.position.set(
      -0.08,
      legHeight / 2,
      0
    );
    leftLeg.castShadow = true;
    character.add(leftLeg);

    const rightLeg = leftLeg.clone();
    rightLeg.position.x *= -1;
    character.add(rightLeg);

    // Hair (simple)
    const hairGeo = new THREE.SphereGeometry(headHeight / 2 + 0.02, 32, 32);
    const hairMat = new THREE.MeshStandardMaterial({
      color: params.hairColor
    });
    const hair = new THREE.Mesh(hairGeo, hairMat);
    hair.position.copy(head.position);
    hair.position.y += 0.02;
    hair.scale.set(1, 0.6, 1);
    character.add(hair);

    return character;
  }

  /**
   * Generate random character parameters
   */
  static randomParams(): CharacterParameters {
    return {
      height: 0.8 + Math.random() * 0.4,
      bodyWidth: 0.85 + Math.random() * 0.3,
      headSize: 0.9 + Math.random() * 0.2,
      armLength: 0.9 + Math.random() * 0.2,
      legLength: 0.95 + Math.random() * 0.1,
      skinColor: new THREE.Color().setHSL(
        0.08 + Math.random() * 0.05,
        0.3 + Math.random() * 0.3,
        0.4 + Math.random() * 0.3
      ),
      hairColor: new THREE.Color().setHSL(
        Math.random(),
        0.5 + Math.random() * 0.5,
        0.2 + Math.random() * 0.3
      ),
    };
  }
}

// Usage
const params = ProceduralCharacterGenerator.randomParams();
const character = ProceduralCharacterGenerator.generate(params);
scene.add(character);
プロシージャルな地形生成
npm install simplex-noise
// proceduralTerrain.ts
import * as THREE from 'three';
import { SimplexNoise } from 'simplex-noise';

export class TerrainGenerator {
  static generate(
    width: number,
    depth: number,
    segments: number,
    heightScale: number = 10
  ): THREE.Mesh {
    const geometry = new THREE.PlaneGeometry(
      width,
      depth,
      segments,
      segments
    );

    const noise = new SimplexNoise();
    const vertices = geometry.attributes.position.array;

    for (let i = 0; i < vertices.length; i += 3) {
      const x = vertices[i];
      const z = vertices[i + 1];

      // Multi-octave noise for realistic terrain
      let height = 0;
      height += noise.noise2D(x * 0.01, z * 0.01) * heightScale;
      height += noise.noise2D(x * 0.05, z * 0.05) * (heightScale * 0.25);
      height += noise.noise2D(x * 0.1, z * 0.1) * (heightScale * 0.125);

      vertices[i + 2] = height;
    }

    geometry.computeVertexNormals();
    geometry.rotateX(-Math.PI / 2);

    // Vertex coloring based on height
    const colors: number[] = [];
    for (let i = 0; i < vertices.length; i += 3) {
      const height = vertices[i + 2];
      const color = new THREE.Color();

      if (height < 0) {
        color.setHex(0x3a7bc8); // Water
      } else if (height < 2) {
        color.setHex(0xc2b280); // Sand
      } else if (height < 5) {
        color.setHex(0x567d46); // Grass
      } else {
        color.setHex(0x8b7355); // Mountain
      }

      colors.push(color.r, color.g, color.b);
    }

    geometry.setAttribute(
      'color',
      new THREE.Float32BufferAttribute(colors, 3)
    );

    const material = new THREE.MeshStandardMaterial({
      vertexColors: true,
      flatShading: true,
    });

    return new THREE.Mesh(geometry, material);
  }
}
プロシージャルな建物
// proceduralBuilding.ts
import * as THREE from 'three';

export class BuildingGenerator {
  static generateBuilding(
    floors: number = 3 + Math.floor(Math.random() * 7),
    floorHeight: number = 3,
    width: number = 5 + Math.random() * 5,
    depth: number = 5 + Math.random() * 5
  ): THREE.Group {
    const building = new THREE.Group();

    // Building material
    const wallMaterial = new THREE.MeshStandardMaterial({
      color: new THREE.Color().setHSL(0, 0, 0.6 + Math.random() * 0.2),
      roughness: 0.8,
    });

    // Create floors
    for (let i = 0; i < floors; i++) {
      const floorGeo = new THREE.BoxGeometry(width, floorHeight, depth);
      const floor = new THREE.Mesh(floorGeo, wallMaterial);
      floor.position.y = i * floorHeight + floorHeight / 2;
      floor.castShadow = true;
      floor.receiveShadow = true;
      building.add(floor);

      // Add windows
      BuildingGenerator.addWindows(floor, width, depth, floorHeight);
    }

    // Roof
    const roofGeo = new THREE.ConeGeometry(
      Math.max(width, depth) * 0.7,
      floorHeight,
      4
    );
    const roofMat = new THREE.MeshStandardMaterial({ color: 0x8b4513 });
    const roof = new THREE.Mesh(roofGeo, roofMat);
    roof.position.y = floors * floorHeight + floorHeight / 2;
    roof.rotation.y = Math.PI / 4;
    roof.castShadow = true;
    building.add(roof);

    return building;
  }

  private static addWindows(
    floor: THREE.Mesh,
    width: number,
    depth: number,
    height: number
  ) {
    const windowMaterial = new THREE.MeshStandardMaterial({
      color: 0x87ceeb,
      emissive: 0x87ceeb,
      emissiveIntensity: 0.3,
    });

    const windowSize = 0.8;
    const windowDepth = 0.1;

    // Front and back windows
    for (let i = -1; i <= 1; i += 2) {
      const windowGeo = new THREE.BoxGeometry(
        windowSize,
        windowSize,
        windowDepth
      );
      const window = new THREE.Mesh(windowGeo, windowMaterial);
      window.position.set(0, 0, (depth / 2 + windowDepth / 2) * i);
      floor.add(window);
    }
  }
}

⚡ パート 6:物理演算とキャラクターコントローラー

Rapier 物理演算の統合
npm install @react-three/rapier
// PhysicsScene.tsx
import { Physics, RigidBody, CuboidCollider } from '@react-three/rapier';
import { Canvas } from '@react-three/fiber';

function PhysicsScene() {
  return (
    <Canvas>
      <Physics gravity={[0, -9.81, 0]}>
        {/* Ground */}
        <RigidBody type="fixed">
          <mesh rotation={[-Math.PI / 2, 0, 0]}>
            <planeGeometry args={[100, 100]} />
            <meshStandardMaterial color="gray" />
          </mesh>
        </RigidBody>

        {/* Dynamic objects */}
        <RigidBody position={[0, 5, 0]}>
          <mesh castShadow>
            <boxGeometry />
            <meshStandardMaterial color="red" />
          </mesh>
        </RigidBody>
      </Physics>

      <ambientLight intensity={0.5} />
      <directionalLight position={[10, 10, 5]} castShadow />
    </Canvas>
  );
}
物理演算を使ったキャラクターコントローラー
// CharacterController.tsx
import { useRef, useEffect } from 'react';
import { useFrame } from '@react-three/fiber';
import { RigidBody, CapsuleCollider, RapierRigidBody } from '@react-three/rapier';
import { useKeyboardControls } from '@react-three/drei';
import * as THREE from 'three';

interface CharacterControllerProps {
  position?: [number, number, number];
}

export function CharacterController({ position = [0, 2, 0] }: CharacterControllerProps) {
  const rigidBodyRef = useRef<RapierRigidBody>(null);
  const [, get] = useKeyboardControls();

  const velocity = useRef(new THREE.Vector3());
  const direction = useRef(new THREE.Vector3());

  const moveSpeed = 5;
  const jumpForce = 5;

  useFrame((state, delta) => {
    if (!rigidBodyRef.current) return;

    const { forward, backward, left, right, jump } = get();

    // Get current velocity
    const currentVel = rigidBodyRef.current.linvel();
    velocity.current.set(currentVel.x, currentVel.y, currentVel.z);

    // Calculate movement direction
    direction.current.set(0, 0, 0);
    if (forward) direction.current.z -= 1;
    if (backward) direction.current.z += 1;
    if (left) direction.current.x -= 1;
    if (right) direction.current.x += 1;

    direction.current.normalize();

    // Apply movement
    velocity.current.x = direction.current.x * moveSpeed;
    velocity.current.z = direction.current.z * moveSpeed;

    // Jump
    if (jump && Math.abs(currentVel.y) < 0.1) {
      velocity.current.y = jumpForce;
    }

    rigidBodyRef.current.setLinvel(velocity.current, true);
  });

  return (
    <RigidBody
      ref={rigidBodyRef}
      position={position}
      lockRotations={true}
    >
      <CapsuleCollider args={[0.5, 0.5]} />
      <mesh castShadow>
        <capsuleGeometry args={[0.5, 1]} />
        <meshStandardMaterial color="blue" />
      </mesh>
    </RigidBody>
  );
}

// Keyboard controls setup
import { KeyboardControls } from '@react-three/drei';

const keyboardMap = [
  { name: 'forward', keys: ['ArrowUp', 'KeyW'] },
  { name: 'backward', keys: ['ArrowDown', 'KeyS'] },
  { name: 'left', keys: ['ArrowLeft', 'KeyA'] },
  { name: 'right', keys: ['ArrowRight', 'KeyD'] },
  { name: 'jump', keys: ['Space'] },
];

function App() {
  return (
    <KeyboardControls map={keyboardMap}>
      <Canvas>
        {/* Your scene */}
        <CharacterController />
      </Canvas>
    </KeyboardControls>
  );
}

📚 パート 7:学術資料と研究

キャラクターアニメーションとリターゲティング

主な概念:

  • IK(逆運動学):目標位置に到達するための関節角度を計算します(例:手の配置)
  • FK(順運動学):関節角度から末端位置を計算します
  • リターゲティング:異なる骨格構造間でアニメーションを転送します

研究分野:

  1. モーションリターゲティング技術

    • 骨格対応関係の構築
    • 転送中の動作特性の保持
    • IK ベースと FK ベースのアプローチ
  2. 実装に関する参考資料:

    • 基本的なリターゲティングには SkeletonUtils(Three.js)
    • upf-gti/retargeting-threejs - オープンソースのリターゲティングソルバー
    • 手動でリターゲティングするための Blender を使った Mixamo ワークフロー
パラメトリック人体モデル
SMPL(Skinned Multi-Person Linear)
  • 約100個のパラメーターで体型とポーズを表現するパラメトリックモデル
  • 用途:アバターの作成、バーチャル試着、モーションキャプチャ
  • SMPL-X の JavaScript ライブラリを使って Web に実装できます

最新のアプローチ:

  • AvatarForge:AI 駆動のマルチモーダルなアバター生成
  • SmartAvatar:VLM ベースのパラメトリックアバター作成
  • FaceMaker:顔の特徴を手続き的に生成

学術論文:

  1. 「FaceMaker—A Procedural Face Generator to Foster Character Design Research」
  2. 「Parametric 3D human modeling with biharmonic SMPL」
  3. 「AvatarCraft: Transforming Text into Neural Human Avatars」
ゲーム AI と経路探索

ステアリング行動(Craig Reynolds):

  • Seek、Flee、Pursue、Evade
  • Wander、Arrive、Obstacle Avoidance
  • Path Following、Leader Following

経路探索アルゴリズム:

  • 最適な経路を求める A*(A-Star)
  • 3D 環境向けのナビゲーションメッシュ
  • 移動する障害物に対応する動的経路探索

ステートマシン:

  • 行動制御に有限ステートマシン(FSM)を使用
  • 複雑な NPC 向けの階層ステートマシン
  • 目標指向型アクションプランニング(GOAP)

実装:

  • Yuka.js - Web 向けの包括的なゲーム AI ライブラリ
  • Three.js および React Three Fiber との統合
プロシージャル生成

ノイズ関数:

  • Perlin Noise - 滑らかで自然に見えるランダム性
  • Simplex Noise - パフォーマンスが向上し、方向性のアーティファクトが発生しない
  • リアルな地形を生成するマルチオクターブノイズ

プロシージャル技法:

  • 有機的な構造(樹木、植物)を生成する L-System
  • レベル生成に Wave Function Collapse を使用
  • ボリューム地形に Marching Cubes を使用
  • キャラクターのバリエーションにパラメトリックモデルを使用

🎯 ベストプラクティスのまとめ

React Three Fiber

推奨事項:

  • アニメーションには setInterval ではなく useFrame を使用する
  • 一般的なタスクには drei のヘルパーを活用する
  • 繰り返し使うジオメトリには <Instances> を使用する
  • useEffect で適切なクリーンアップを実装する
  • モデルの遅延読み込みには Suspense を使用する
  • パフォーマンスを向上させるため、concurrent モードを有効にする

避けるべきこと:

  • レンダリング中に Three.js オブジェクトを作成する(useMemo を使用する)
  • ジオメトリやマテリアルの破棄を忘れる
  • ライトを使いすぎる(パフォーマンスに影響します)
  • モバイルデバイスで dpr の制限を省略する
キャラクターシステム

推奨事項:

  • Web では GLTFLoader を使用する(可能であれば FBX は使用しない)
  • fadeIn/fadeOut を使ったアニメーションのブレンドを実装する
  • useGLTF.preload() で読み込んだモデルをキャッシュする
  • 素早いプロトタイピングには Mixamo を使用する
  • 適切なスケルトン階層を実装する

避けるべきこと:

  • 互換性のないスケルトン構造を混在させる
  • リターゲティング時のボーン名の検証を省略する
  • 毎フレーム、アニメーションミキサーを更新するのを忘れる
  • 本番環境で非圧縮モデルを読み込む
NPC とゲーム AI

推奨事項:

  • 本番環境で利用可能な AI には Yuka.js を使用する
  • 複雑な行動にはステートマシンを実装する
  • AI の更新頻度を最適化する(毎フレーム更新しない)
  • NPC の数が多い場合は空間分割を使用する
  • 知覚システム(視覚、聴覚)を追加する

避けるべきこと:

  • 毎フレーム経路探索を実行する
  • 行動の優先順位付けシステムを省略する
  • AI のパフォーマンス予算を考慮し忘れる
  • 行動の値をハードコードする(設定を使用する)
物理演算

推奨事項:

  • Web で最高のパフォーマンスを得るには Rapier を使用する
  • 高速で移動するオブジェクトには CCD を実装する
  • 複雑な形状には複合コライダーを使用する
  • 物理演算の更新を 60Hz に制限する
  • 静止オブジェクトにはスリープ状態を実装する

避けるべきこと:

  • あらゆる用途にメッシュコライダーを使用する
  • 衝突レイヤー/マスクを省略する
  • レンダリングのフレームレートで物理演算を実行する
  • 物理オブジェクトのクリーンアップを忘れる

🔗 重要なリソース

ドキュメント
モデルのリソース
学習

✅ 完全チェックリスト

基本設定
  • Three.js または R3F をインストール済み
  • シーン、カメラ、レンダラーを設定済み
  • ライティングシステムを実装済み
  • コントロールを追加済み(OrbitControls/PointerLock)
  • キャンバスサイズをレスポンシブに設定済み
キャラクターシステム
  • GLTF/GLB ローダーを実装済み
  • アニメーションシステムが動作中
  • キャラクターコントローラー(必要な場合)
  • Ready Player Me または Mixamo を統合済み
  • アニメーションブレンディングが機能中
ゲーム機能(該当する場合)
  • Yuka.js を使用した NPC AI
  • Rapier を使用した物理演算
  • 衝突検出
  • 状態管理
  • 入力処理
プロシージャル生成(該当する場合)
  • 地形生成
  • キャラクターのバリエーション
  • プロシージャルな建物/小道具
  • ノイズベースのシステム
パフォーマンス
  • 繰り返し使用するオブジェクトにインスタンシングを使用
  • LOD システムを実装済み
  • 視錐台カリングを有効化済み
  • テクスチャを最適化済み
  • シャドウマップを最適化済み
  • ポストプロセッシングを最適化済み
本番環境対応
  • エラーバウンダリーを追加済み
  • 読み込み状態を実装済み
  • モバイル向けに最適化済み
  • 適切なクリーンアップ/破棄処理
  • アセットの先読み
  • パフォーマンスのモニタリング

コメント