threejs-scene-builder について
threejs-scene-builder は、Three.js と React Three Fiber による 3D ウェブシーン開発向けスキルです。シーン構築、描画、アニメーション、キャラクター、プロシージャル生成、NPC の動作、物理の例を扱います。
主な機能
- 01Three.js のシーン構築と描画
- 02React Three Fiber の統合
- 03キャラクターアニメーションとリターゲティング
- 04地形とモデルのプロシージャル生成
- 05NPC の動作と物理の例
活用シーン
- 01インタラクティブな 3D ウェブシーンの作成
- 02アニメーションキャラクターやアバターの追加
- 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(順運動学):関節角度から末端位置を計算します
- リターゲティング:異なる骨格構造間でアニメーションを転送します
研究分野:
-
モーションリターゲティング技術
- 骨格対応関係の構築
- 転送中の動作特性の保持
- IK ベースと FK ベースのアプローチ
-
実装に関する参考資料:
- 基本的なリターゲティングには SkeletonUtils(Three.js)
- upf-gti/retargeting-threejs - オープンソースのリターゲティングソルバー
- 手動でリターゲティングするための Blender を使った Mixamo ワークフロー
パラメトリック人体モデル
SMPL(Skinned Multi-Person Linear)
- 約100個のパラメーターで体型とポーズを表現するパラメトリックモデル
- 用途:アバターの作成、バーチャル試着、モーションキャプチャ
- SMPL-X の JavaScript ライブラリを使って Web に実装できます
最新のアプローチ:
- AvatarForge:AI 駆動のマルチモーダルなアバター生成
- SmartAvatar:VLM ベースのパラメトリックアバター作成
- FaceMaker:顔の特徴を手続き的に生成
学術論文:
- 「FaceMaker—A Procedural Face Generator to Foster Character Design Research」
- 「Parametric 3D human modeling with biharmonic SMPL」
- 「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 システムを実装済み
- 視錐台カリングを有効化済み
- テクスチャを最適化済み
- シャドウマップを最適化済み
- ポストプロセッシングを最適化済み
本番環境対応
- エラーバウンダリーを追加済み
- 読み込み状態を実装済み
- モバイル向けに最適化済み
- 適切なクリーンアップ/破棄処理
- アセットの先読み
- パフォーマンスのモニタリング
コメント
Markdown 対応