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 Web 体验的综合指南,涵盖场景设置、角色系统、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 的高多边形模型
  • 忘记释放资源
  • 使用过多光源
  • 跳过纹理压缩
  • 在移动设备上以设备原生像素比进行渲染
  • 不必要地逐帧更新 uniforms

🚀 第 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 与最佳实践
// 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 与游戏行为

使用 Yuka.js 集成游戏 AI
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):

  • 寻找、逃离、追逐、躲避
  • 游荡、抵达、避障
  • 路径跟随、跟随领队

寻路算法:

  • 使用 A*(A-Star)寻找最优路径
  • 用于 3D 环境的导航网格
  • 针对移动障碍物的动态寻路

状态机:

  • 用于行为控制的有限状态机(FSM)
  • 用于复杂 NPC 的分层状态机
  • 面向目标的行动规划(GOAP)

实现:

  • Yuka.js - 完整的 Web 游戏 AI 库
  • 与 Three.js 和 React Three Fiber 集成
程序化生成

噪声函数:

  • Perlin Noise - 平滑、自然的随机效果
  • Simplex Noise - 性能更佳,且没有方向性伪影
  • 用于生成逼真地形的多倍频噪声

程序化技术:

  • 用于生成有机结构(树木、植物)的 L-系统
  • 用于关卡生成的波函数坍缩
  • 用于体积地形的 Marching Cubes
  • 用于角色变化的参数化模型

🎯 最佳实践总结

React Three Fiber

应该:

  • 使用 useFrame 实现动画,而不是 setInterval
  • 利用 drei 辅助工具完成常见任务
  • 对重复几何体使用 <Instances>
  • 在 useEffect 中正确清理资源
  • 使用 Suspense 延迟加载模型
  • 启用 concurrent 模式以提升性能

不应该:

  • 在渲染过程中创建 Three.js 对象(使用 useMemo)
  • 忘记释放几何体/材质
  • 使用过多灯光(会影响性能)
  • 在移动设备上跳过 dpr 限制
角色系统

应该:

  • 在 Web 中使用 GLTFLoader(尽可能避免使用 FBX)
  • 使用 fadeIn/fadeOut 实现动画混合
  • 使用 useGLTF.preload() 缓存已加载的模型
  • 使用 Mixamo 快速制作原型
  • 正确设置骨架层级

不应该:

  • 混用不兼容的骨骼结构
  • 在重定向动画时跳过骨骼名称验证
  • 忘记在每一帧更新动画混合器
  • 在生产环境中加载未压缩的模型
NPC 与游戏 AI

应该:

  • 在生产环境中使用 Yuka.js,打造可直接投入使用的 AI
  • 使用状态机实现复杂行为
  • 优化 AI 更新频率(不必每帧更新)
  • NPC 数量较多时使用空间分区
  • 添加感知系统(视觉、听觉)

不应该:

  • 每帧运行寻路算法
  • 跳过行为优先级系统
  • 忘记为 AI 预留性能预算
  • 将行为值硬编码(应使用配置)
物理

应该:

  • 使用 Rapier 以获得最佳 Web 性能
  • 为快速移动的物体启用 CCD
  • 为复杂形状使用复合碰撞体
  • 将物理更新频率限制为 60 Hz
  • 为静态物体实现休眠状态

不应该:

  • 所有物体都使用网格碰撞体
  • 跳过碰撞层/遮罩设置
  • 按渲染帧率运行物理模拟
  • 忘记清理物理对象

🔗 必备资源

文档
模型资源
学习

✅ 完整检查清单

基础设置
  • 已安装 Three.js 或 R3F
  • 已配置场景、摄像机和渲染器
  • 已实现光照系统
  • 已添加控制器(OrbitControls/PointerLock)
  • 画布尺寸可响应变化
角色系统
  • 已实现 GLTF/GLB 加载器
  • 动画系统正常运行
  • 角色控制器(如有需要)
  • 已集成 Ready Player Me 或 Mixamo
  • 动画混合功能正常
游戏功能(如适用)
  • 使用 Yuka.js 实现 NPC AI
  • 使用 Rapier 实现物理效果
  • 碰撞检测
  • 状态管理
  • 输入处理
程序化生成(如适用)
  • 地形生成
  • 角色变体
  • 程序化建筑/道具
  • 基于噪声的系统
性能
  • 对重复对象使用实例化
  • 已实现 LOD 系统
  • 已启用视锥剔除
  • 纹理优化
  • 阴影贴图优化
  • 后期处理优化
生产就绪
  • 已添加错误边界
  • 已实现加载状态
  • 移动端优化
  • 妥善清理/释放资源
  • 资源预加载
  • 性能监控

评论

支持 Markdown