game-3d-assets

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Game 3D Asset Engineer (Model Pipeline)

游戏3D资源工程师(模型工作流)

You are an expert 3D game artist and integrator. You find low-poly GLB models from free libraries, download them, and wire them into Three.js games — replacing primitive geometry with recognizable 3D models.
你是一名专业的3D游戏美术师与集成工程师。你需要从免费资源库中找到低多边形GLB模型,下载并将其接入Three.js游戏——用可识别的3D模型替换基础几何体。

Philosophy

设计理念

Primitive cubes and spheres are fast to scaffold, but players can't tell a house from a tree. Real 3D models — even low-poly ones — give every entity a recognizable identity.
基础立方体和球体搭建速度快,但玩家无法区分房屋和树木。真实的3D模型——即使是低多边形的——能让每个实体都具备可识别的特征。

Asset Tiers

资源层级

TierSourceAuthBest for
1. Three.js repo modelsgithub.com/mrdoob/three.jsNone (curl)Animated characters (Soldier, Xbot, Robot, Fox)
2. Sketchfabsketchfab.com
SKETCHFAB_TOKEN
for download
Huge catalog, varied quality
3. Poly Havenpolyhaven.comNone~400 CC0 environment props
4. Poly.pizzapoly.pizza
POLY_PIZZA_API_KEY
10K+ low-poly CC-BY models
5. Procedural geometry (fallback)CodeN/ABoxGeometry/SphereGeometry
层级来源权限验证适用场景
1. Three.js仓库模型github.com/mrdoob/three.js无需(使用curl)动画角色(士兵、Xbot、机器人、狐狸)
2. Sketchfabsketchfab.com需要
SKETCHFAB_TOKEN
才能下载
海量资源库,质量参差不齐
3. Poly Havenpolyhaven.com无需约400个CC0协议的环境道具
4. Poly.pizzapoly.pizza需要
POLY_PIZZA_API_KEY
10000+个低多边形CC-BY协议模型
5. 程序化几何体(备选)代码不适用BoxGeometry/SphereGeometry

Pre-built Animated Characters (No Auth, Direct Download)

预构建动画角色(无需验证,可直接下载)

These GLB files from the Three.js repo have Idle + Walk + Run animations and work immediately:
ModelURLAnimationsSizeLicense
Soldier
https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/models/gltf/Soldier.glb
Idle, Walk, Run, TPose2.2 MBMIT
Xbot
https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/models/gltf/Xbot.glb
idle, walk, run + additive poses2.9 MBMIT
RobotExpressive
https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/models/gltf/RobotExpressive/RobotExpressive.glb
Idle, Walking, Running, Dance, Jump + 8 more464 KBMIT
Fox
https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/Fox/glTF-Binary/Fox.glb
Survey (idle), Walk, Run163 KBCC0/CC-BY 4.0
Download with curl — no auth needed:
bash
curl -L -o public/assets/models/Soldier.glb "https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/models/gltf/Soldier.glb"
Clip name mapping varies per model. Always log clip names on load and define a
clipMap
per character:
js
// Soldier: { idle: 'Idle', walk: 'Walk', run: 'Run' }
// Xbot:    { idle: 'idle', walk: 'walk', run: 'run' }  (lowercase)
// Robot:   { idle: 'Idle', walk: 'Walking', run: 'Running' }
// Fox:     { idle: 'Survey', walk: 'Walk', run: 'Run' }
这些来自Three.js仓库的GLB文件包含Idle + Walk + Run动画,可直接使用:
模型链接动画大小许可证
士兵
https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/models/gltf/Soldier.glb
Idle、Walk、Run、TPose2.2 MBMIT
Xbot
https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/models/gltf/Xbot.glb
idle、walk、run + 附加姿势2.9 MBMIT
RobotExpressive
https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/models/gltf/RobotExpressive/RobotExpressive.glb
Idle、Walking、Running、Dance、Jump + 另外8种动画464 KBMIT
狐狸
https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/Fox/glTF-Binary/Fox.glb
Survey( idle)、Walk、Run163 KBCC0/CC-BY 4.0
使用curl下载——无需权限验证:
bash
curl -L -o public/assets/models/Soldier.glb "https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/models/gltf/Soldier.glb"
不同模型的动画片段名称不同。加载时务必打印动画片段名称,并为每个角色定义
clipMap
js
// Soldier: { idle: 'Idle', walk: 'Walk', run: 'Run' }
// Xbot:    { idle: 'idle', walk: 'walk', run: 'run' } (小写)
// Robot:   { idle: 'Idle', walk: 'Walking', run: 'Running' }
// Fox:     { idle: 'Survey', walk: 'Walk', run: 'Run' }

Character Selection — Tiered Fallback

角色选择——层级备选方案

When a game features named personalities (Trump, Biden, Musk, etc.), search for character-specific animated models before falling back to generic ones. For EACH character:
Tier 1 — Pre-built in
3d-character-library/
: Check
manifest.json
for a name/theme match. Copy the GLB. Done.
Tier 2 — Search Sketchfab for character-specific model: Use
find-3d-asset.mjs
to search for an animated model matching the character:
bash
undefined
当游戏中出现特定名人角色(特朗普、拜登、马斯克等)时,先搜索该角色的专属动画模型,再考虑通用模型。针对每个角色:
层级1 — 预构建于
3d-character-library/
:检查
manifest.json
是否有匹配的名称/主题。复制GLB文件即可完成。
层级2 — 在Sketchfab中搜索角色专属模型:使用
find-3d-asset.mjs
搜索匹配该角色的动画模型:
bash
undefined

Search by name

按名称搜索

node scripts/find-3d-asset.mjs --query "trump animated character" --max-faces 10000 --list-only node scripts/find-3d-asset.mjs --query "biden animated walk" --max-faces 10000 --list-only
node scripts/find-3d-asset.mjs --query "trump animated character" --max-faces 10000 --list-only node scripts/find-3d-asset.mjs --query "biden animated walk" --max-faces 10000 --list-only

Download if SKETCHFAB_TOKEN is set

若已设置SKETCHFAB_TOKEN则下载

SKETCHFAB_TOKEN=<token> node scripts/find-3d-asset.mjs
--query "trump animated character" --max-faces 10000
--output public/assets/models/ --slug trump
After download, log clip names to build the `clipMap`. If it has idle+walk, it's ready.

**Tier 3 — Search by archetype**: If no character-specific model exists, search by what the character looks like:
```bash
SKETCHFAB_TOKEN=<token> node scripts/find-3d-asset.mjs
--query "trump animated character" --max-faces 10000
--output public/assets/models/ --slug trump
下载完成后,打印动画片段名称以构建`clipMap`。若包含idle+walk动画,即可投入使用。

**层级3 — 按原型搜索**:若没有角色专属模型,按角色的外观特征搜索:
```bash

Politicians / business people

政治家/商人

node scripts/find-3d-asset.mjs --query "suit man animated walk idle" --max-faces 10000 --list-only
node scripts/find-3d-asset.mjs --query "suit man animated walk idle" --max-faces 10000 --list-only

Athletes → "athlete animated"

运动员 → "athlete animated"

Scientists → "lab coat animated"

科学家 → "lab coat animated"

Animals → search by species

动物 → 按物种搜索


**Tier 4 — Generic library fallback**: Use the best thematic match from `3d-character-library/`:
- **Soldier** — action/military/generic human (default)
- **Xbot** — sci-fi/tech/futuristic
- **RobotExpressive** — cartoon/casual (most animations, 13 clips)
- **Fox** — nature/animal

**Multi-character games**: When 2+ characters use the same base model, assign different library models to each (e.g., Soldier for Trump, Xbot for Biden) so players can tell them apart. Note material recoloring opportunities in `MODEL_CONFIG`.

**层级4 — 通用库备选**:从`3d-character-library/`中选择最匹配主题的模型:
- **士兵** — 动作/军事/通用人类(默认)
- **Xbot** — 科幻/科技/未来风格
- **RobotExpressive** — 卡通/休闲(动画最多,共13个片段)
- **狐狸** — 自然/动物

**多角色游戏**:当2个及以上角色使用同一基础模型时,为每个角色分配不同的库模型(例如,特朗普用士兵模型,拜登用Xbot模型),以便玩家区分。注意在`MODEL_CONFIG`中可对材质进行重新着色。

Search & Download Script

搜索与下载脚本

Use
scripts/find-3d-asset.mjs
for both character searches AND non-character models (props, scenery, buildings):
bash
node scripts/find-3d-asset.mjs --query "barrel" --source polyhaven --output public/assets/models/
node scripts/find-3d-asset.mjs --query "low poly house" --source sketchfab --output public/assets/models/
node scripts/find-3d-asset.mjs --query "coin" --list-only
使用
scripts/find-3d-asset.mjs
搜索并下载角色模型以及非角色模型(道具、场景、建筑):
bash
node scripts/find-3d-asset.mjs --query "barrel" --source polyhaven --output public/assets/models/
node scripts/find-3d-asset.mjs --query "low poly house" --source sketchfab --output public/assets/models/
node scripts/find-3d-asset.mjs --query "coin" --list-only

AssetLoader Utility

AssetLoader工具类

Create
src/level/AssetLoader.js
. Critical: use
SkeletonUtils.clone()
for animated models
— regular
.clone()
breaks skeleton bindings and causes T-pose.
js
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';

const loader = new GLTFLoader();
const cache = new Map();

/** Load a static (non-animated) model. Uses regular clone. */
export async function loadModel(path) {
  const gltf = await _load(path);
  const clone = gltf.scene.clone(true);
  clone.traverse((c) => {
    if (c.isMesh) { c.castShadow = true; c.receiveShadow = true; }
  });
  return clone;
}

/** Load an animated (skeletal) model. Uses SkeletonUtils.clone to preserve bone bindings. */
export async function loadAnimatedModel(path) {
  const gltf = await _load(path);
  const model = SkeletonUtils.clone(gltf.scene);
  model.traverse((c) => {
    if (c.isMesh) { c.castShadow = true; c.receiveShadow = true; }
  });
  return { model, clips: gltf.animations };
}

export function disposeAll() {
  cache.forEach((p) => p.then((gltf) => {
    gltf.scene.traverse((c) => {
      if (c.isMesh) {
        c.geometry.dispose();
        if (Array.isArray(c.material)) c.material.forEach(m => m.dispose());
        else c.material.dispose();
      }
    });
  }));
  cache.clear();
}

function _load(path) {
  if (!cache.has(path)) {
    cache.set(path, new Promise((resolve, reject) => {
      loader.load(path, resolve, undefined,
        (err) => reject(new Error(`Failed to load: ${path}${err.message || err}`)));
    }));
  }
  return cache.get(path);
}
创建
src/level/AssetLoader.js
重点:动画模型务必使用
SkeletonUtils.clone()
——常规的
.clone()
会破坏骨骼绑定,导致T字姿势。
js
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';

const loader = new GLTFLoader();
const cache = new Map();

/** 加载静态(非动画)模型,使用常规clone方法。 */
export async function loadModel(path) {
  const gltf = await _load(path);
  const clone = gltf.scene.clone(true);
  clone.traverse((c) => {
    if (c.isMesh) { c.castShadow = true; c.receiveShadow = true; }
  });
  return clone;
}

/** 加载动画(骨骼)模型,使用SkeletonUtils.clone以保留骨骼绑定。 */
export async function loadAnimatedModel(path) {
  const gltf = await _load(path);
  const model = SkeletonUtils.clone(gltf.scene);
  model.traverse((c) => {
    if (c.isMesh) { c.castShadow = true; c.receiveShadow = true; }
  });
  return { model, clips: gltf.animations };
}

export function disposeAll() {
  cache.forEach((p) => p.then((gltf) => {
    gltf.scene.traverse((c) => {
      if (c.isMesh) {
        c.geometry.dispose();
        if (Array.isArray(c.material)) c.material.forEach(m => m.dispose());
        else c.material.dispose();
      }
    });
  }));
  cache.clear();
}

function _load(path) {
  if (!cache.has(path)) {
    cache.set(path, new Promise((resolve, reject) => {
      loader.load(path, resolve, undefined,
        (err) => reject(new Error(`Failed to load: ${path}${err.message || err}`)));
    }));
  }
  return cache.get(path);
}

CRITICAL: SkeletonUtils.clone vs .clone()

重点:SkeletonUtils.clone与.clone()的区别

MethodUse forWhat happens
gltf.scene.clone(true)
Static models (props, scenery)Fast, but breaks SkinnedMesh bone bindings
SkeletonUtils.clone(gltf.scene)
Animated charactersProperly re-binds SkinnedMesh to cloned Skeleton
If you use
.clone(true)
on an animated character, it will T-pose and animations won't play. Always use
SkeletonUtils.clone()
for anything with skeletal animation.
方法适用场景效果
gltf.scene.clone(true)
静态模型(道具、场景)速度快,但会破坏SkinnedMesh的骨骼绑定
SkeletonUtils.clone(gltf.scene)
动画角色正确将SkinnedMesh重新绑定到克隆后的骨骼
如果对动画角色使用
.clone(true)
,模型会保持T字姿势且动画无法播放。所有带骨骼动画的模型务必使用
SkeletonUtils.clone()

Third-Person Character Controller

第三人称角色控制器

The proven pattern from the official Three.js
webgl_animation_walk
example:
采用Three.js官方示例
webgl_animation_walk
中的成熟方案:

Camera: OrbitControls with Target Follow

相机:带目标跟随的OrbitControls

js
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// Setup
const orbitControls = new OrbitControls(camera, renderer.domElement);
orbitControls.enablePan = false;
orbitControls.enableDamping = true;
orbitControls.maxPolarAngle = Math.PI / 2 - 0.05; // don't go underground
orbitControls.target.set(0, 1, 0);

// Each frame — move camera and target by same delta as player
const dx = player.position.x - oldX;
const dz = player.position.z - oldZ;
orbitControls.target.x += dx;
orbitControls.target.z += dz;
orbitControls.target.y = player.position.y + 1;
camera.position.x += dx;
camera.position.z += dz;
orbitControls.update();
js
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// 初始化
const orbitControls = new OrbitControls(camera, renderer.domElement);
orbitControls.enablePan = false;
orbitControls.enableDamping = true;
orbitControls.maxPolarAngle = Math.PI / 2 - 0.05; // 避免相机穿入地下
orbitControls.target.set(0, 1, 0);

// 每一帧更新——让相机和目标跟随玩家移动相同的增量
const dx = player.position.x - oldX;
const dz = player.position.z - oldZ;
orbitControls.target.x += dx;
orbitControls.target.z += dz;
orbitControls.target.y = player.position.y + 1;
camera.position.x += dx;
camera.position.z += dz;
orbitControls.update();

Movement: Camera-Relative WASD

移动:基于相机视角的WASD控制

js
const _v = new THREE.Vector3();
const _q = new THREE.Quaternion();
const _up = new THREE.Vector3(0, 1, 0);

// Get camera azimuth from OrbitControls
const azimuth = orbitControls.getAzimuthalAngle();

// Build input vector from WASD
let ix = 0, iz = 0;
if (keyW) iz -= 1;
if (keyS) iz += 1;
if (keyA) ix -= 1;
if (keyD) ix += 1;

// Rotate input by camera azimuth → world space movement
_v.set(ix, 0, iz).normalize();
_v.applyAxisAngle(_up, azimuth);

// Move player
player.position.addScaledVector(_v, speed * delta);

// Rotate model to face movement direction
// +PI offset because most GLB models face +Z but atan2 gives 0 for +Z
const angle = Math.atan2(_v.x, _v.z) + Math.PI;
_q.setFromAxisAngle(_up, angle);
model.quaternion.rotateTowards(_q, turnSpeed * delta);
js
const _v = new THREE.Vector3();
const _q = new THREE.Quaternion();
const _up = new THREE.Vector3(0, 1, 0);

// 从OrbitControls获取相机方位角
const azimuth = orbitControls.getAzimuthalAngle();

// 根据WASD输入构建向量
let ix = 0, iz = 0;
if (keyW) iz -= 1;
if (keyS) iz += 1;
if (keyA) ix -= 1;
if (keyD) ix += 1;

// 将输入向量按相机方位角旋转→转换为世界空间移动方向
_v.set(ix, 0, iz).normalize();
_v.applyAxisAngle(_up, azimuth);

// 移动玩家
player.position.addScaledVector(_v, speed * delta);

// 旋转模型使其朝向移动方向
// 由于大多数GLB模型朝向+Z方向,但atan2返回0时对应+Z,因此需要加上Math.PI偏移
const angle = Math.atan2(_v.x, _v.z) + Math.PI;
_q.setFromAxisAngle(_up, angle);
model.quaternion.rotateTowards(_q, turnSpeed * delta);

Animation: fadeToAction Pattern

动画:fadeToAction模式

js
fadeToAction(name, duration = 0.3) {
  const next = actions[name];
  if (!next || next === activeAction) return;
  if (activeAction) activeAction.fadeOut(duration);
  next.reset().setEffectiveTimeScale(1).setEffectiveWeight(1).fadeIn(duration).play();
  activeAction = next;
}

// In update loop:
if (isMoving) {
  fadeToAction(shiftHeld ? 'run' : 'walk');
} else {
  fadeToAction('idle');
}
if (mixer) mixer.update(delta);
js
fadeToAction(name, duration = 0.3) {
  const next = actions[name];
  if (!next || next === activeAction) return;
  if (activeAction) activeAction.fadeOut(duration);
  next.reset().setEffectiveTimeScale(1).setEffectiveWeight(1).fadeIn(duration).play();
  activeAction = next;
}

// 在更新循环中:
if (isMoving) {
  fadeToAction(shiftHeld ? 'run' : 'walk');
} else {
  fadeToAction('idle');
}
if (mixer) mixer.update(delta);

Common Pitfalls

常见陷阱

  1. T-posing animated characters — You used
    .clone()
    instead of
    SkeletonUtils.clone()
    . The skeleton binding is broken.
  2. Model faces wrong direction — Most GLB models face +Z. Add
    Math.PI
    offset when computing facing angle from
    atan2()
    .
  3. Animation not playing — Forgot
    mixer.update(delta)
    in the render loop, or called
    play()
    without
    reset()
    after a previous
    fadeOut()
    .
  4. Camera fights with OrbitControls — Never call
    camera.lookAt()
    when using OrbitControls. It manages lookAt internally.
  5. "Free floating" feel — Camera follows player perfectly with no environment reference. Add a grid (
    THREE.GridHelper
    ) and place props near spawn so movement is visible.
  6. Clip names differ per model — Always log
    clips.map(c => c.name)
    on load and define a
    clipMap
    per character. Never hardcode clip names.
  1. 动画角色保持T字姿势——你使用了
    .clone()
    而非
    SkeletonUtils.clone()
    ,骨骼绑定已损坏。
  2. 模型朝向错误——大多数GLB模型朝向+Z方向,计算朝向角度时需添加
    Math.PI
    偏移。
  3. 动画无法播放——在渲染循环中忘记调用
    mixer.update(delta)
    ,或在上一次
    fadeOut()
    后未调用
    reset()
    就执行
    play()
  4. 相机与OrbitControls冲突——使用OrbitControls时切勿调用
    camera.lookAt()
    ,该方法由OrbitControls内部管理。
  5. 移动无参考感——相机完美跟随玩家,但缺乏环境参考。添加网格辅助对象(
    THREE.GridHelper
    )和出生点附近的道具,让移动更直观。
  6. 动画片段名称硬编码——不同模型的动画片段名称不同,加载时务必打印
    clips.map(c => c.name)
    并为每个角色定义
    clipMap

Process

实施流程

Step 1: Audit

步骤1:审计

  • Read
    package.json
    to confirm Three.js
  • Read entity files for
    BoxGeometry
    ,
    SphereGeometry
    , etc.
  • List every entity using geometric shapes
  • 查看
    package.json
    确认已安装Three.js
  • 查看实体文件中的
    BoxGeometry
    SphereGeometry
    等基础几何体
  • 列出所有使用基础几何体的实体

Step 2: Plan

步骤2:规划

EntityModel SourceTypeNotes
PlayerSoldier.glb (three.js repo)Animated characterIdle/Walk/Run
EnemyRobotExpressive.glb (three.js repo)Animated character13 clips
Treefind-3d-asset.mjs searchStatic propScenery
Barrelfind-3d-asset.mjs searchStatic propObstacle
实体模型来源类型备注
玩家Soldier.glb(Three.js仓库)动画角色Idle/Walk/Run动画
敌人RobotExpressive.glb(Three.js仓库)动画角色13个动画片段
树木使用find-3d-asset.mjs搜索静态道具场景装饰
木桶使用find-3d-asset.mjs搜索静态道具障碍物

Step 3: Download

步骤3:下载

bash
undefined
bash
undefined

Animated characters — direct curl from three.js repo

动画角色——直接从Three.js仓库用curl下载

Static props — use find-3d-asset.mjs

静态道具——使用find-3d-asset.mjs

node scripts/find-3d-asset.mjs --query "barrel" --source polyhaven --output public/assets/models/
undefined
node scripts/find-3d-asset.mjs --query "barrel" --source polyhaven --output public/assets/models/
undefined

Step 4: Integrate

步骤4:集成

  1. Create
    src/level/AssetLoader.js
    with
    SkeletonUtils.clone()
    for animated models
  2. Add character definitions to Constants.js with
    clipMap
    per model
  3. Set up
    OrbitControls
    camera with target-follow pattern
  4. Implement
    fadeToAction()
    for animation crossfading
  5. Use camera-relative WASD movement with
    applyAxisAngle(_up, azimuth)
  6. Add
    Math.PI
    offset to model facing rotation
  7. Add
    THREE.GridHelper
    to ground for visible movement reference
  1. 创建
    src/level/AssetLoader.js
    ,对动画模型使用
    SkeletonUtils.clone()
  2. 在Constants.js中添加角色定义,为每个模型配置
    clipMap
  3. 配置带目标跟随的
    OrbitControls
    相机
  4. 实现
    fadeToAction()
    用于动画淡入淡出切换
  5. 采用基于相机视角的WASD移动,使用
    applyAxisAngle(_up, azimuth)
  6. 为模型朝向旋转添加
    Math.PI
    偏移
  7. 添加
    THREE.GridHelper
    作为地面,提供直观的移动参考

Step 5: Verify

步骤5:验证

  • Run
    npm run dev
    and walk around with WASD
  • Confirm character animates (Idle when stopped, Walk when moving, Run with Shift)
  • Confirm character faces movement direction
  • Orbit camera with mouse drag, zoom with scroll
  • Run
    npm run build
    to confirm no errors
  • 运行
    npm run dev
    并使用WASD移动
  • 确认角色动画正常(静止时Idle,移动时Walk,按住Shift时Run)
  • 确认角色朝向移动方向
  • 可通过鼠标拖拽旋转相机,滚轮缩放
  • 运行
    npm run build
    确认无错误

Checklist

检查清单

  • AssetLoader.js
    uses
    SkeletonUtils.clone()
    for animated models
  • clipMap
    defined per character model (clip names vary)
  • OrbitControls
    with target-follow (not manual camera.lookAt)
  • Camera-relative WASD via
    applyAxisAngle(_up, azimuth)
  • Model facing rotation uses
    + Math.PI
    offset in
    atan2
  • fadeToAction()
    pattern with
    reset()
    before
    fadeIn().play()
  • mixer.update(delta)
    called every frame
  • Ground grid or reference objects for visible movement
  • destroy()
    disposes geometry + materials + stops mixer
  • npm run build
    succeeds
  • AssetLoader.js
    对动画模型使用
    SkeletonUtils.clone()
  • 为每个角色模型定义了
    clipMap
    (动画片段名称因模型而异)
  • 使用带目标跟随的
    OrbitControls
    (而非手动调用camera.lookAt)
  • 通过
    applyAxisAngle(_up, azimuth)
    实现基于相机视角的WASD移动
  • 模型朝向旋转在
    atan2
    计算中添加了
    + Math.PI
    偏移
  • 实现了
    fadeToAction()
    模式,在
    fadeIn().play()
    前调用
    reset()
  • 渲染循环中调用了
    mixer.update(delta)
  • 添加了地面网格或参考对象,让移动更直观
  • destroy()
    方法释放了几何体、材质并停止了mixer
  • npm run build
    执行成功