Skip to content

239. 综合实战:开放世界(四十)

Published:

你可以试下这个开放世界:

https://summer-afternoon.vlucendo.com/

2026-03-29 15.21.02.gif

天空中有一些飞鸟,显得更真实。

我们也加一下这个。

https://sketchfab.com/3d-models/birds-3a9bb97be78944f9bffc23fb25c2154e

image.png

下载这个模型,放 public 目录:

image.png

然后加载下:

创建 src/birds.js

import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { loadingManager } from './loading.js';

const loader = new GLTFLoader(loadingManager);

const group = new THREE.Group();

const birdsPosition = { x: -15, y: 6, z: -12 };

export const loadPromise = loader.loadAsync('./birds.glb');

export let birdsModel = null;

let mixer = null;
const clock = new THREE.Clock();

loadPromise.then((gltf) => {
  const root = gltf.scene;
  root.traverse((child) => {
    if (child.isMesh) {
      child.castShadow = true;
      child.receiveShadow = true;
    }
  });

  root.position.set(birdsPosition.x, birdsPosition.y, birdsPosition.z);
  group.add(root);
  birdsModel = root;

  if (gltf.animations && gltf.animations.length > 0) {
    mixer = new THREE.AnimationMixer(root);
    for (let i = 0; i < gltf.animations.length; i++) {
      mixer.clipAction(gltf.animations[i]).play();
    }
  }
});

export function updateBirds() {
  if (mixer) {
    mixer.update(clock.getDelta());
  }
}

export default group;

加载模型,播放骨骼动画。

在 main.js 里引入下:

image.png

image.png

看下效果:

2026-03-29 15.44.36.gif

可以看到,鸟就已经渲染出来了,并且播放了飞行的骨骼动画。

然后我们做一下随机的位置移动效果。

原理也很简单:

image.png

指定一个随机的目标点位置 Vector3

image.png

每帧在 updateBirds() 里:

  1. toTarget = currentTarget - group.position,指向目标的方向。
  2. 若 dist < REACH_DIST,认为到达,重新 pickTarget(),并立刻用新目标重算 toTarget
  3. 把方向单位化,沿该方向移动:

就是随机目标位置,如果没有到达目标就分成几步来移动。

import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { loadingManager } from './loading.js';

// 与 mesh.js 中 groundSize=100 一致:可飞行范围约为 [-50,50] x [-50,50]
const WORLD_HALF = 50;
const EDGE_MARGIN = 5;
const BOUNDS = WORLD_HALF - EDGE_MARGIN;

// 贴地低空,避免飞太高跑出视野
const MIN_Y = 2;
const MAX_Y = 9;
const REACH_DIST = 3;
const FLIGHT_SPEED = 3;

const loader = new GLTFLoader(loadingManager);

const group = new THREE.Group();

export const loadPromise = loader.loadAsync('./birds.glb');

export let birdsModel = null;

let mixer = null;
const clock = new THREE.Clock();

const currentTarget = new THREE.Vector3();
const toTarget = new THREE.Vector3();

let flightReady = false;

function randomInRange() {
  return (Math.random() * 2 - 1) * BOUNDS;
}

function pickTarget() {
  currentTarget.set(
    randomInRange(),
    MIN_Y + Math.random() * (MAX_Y - MIN_Y),
    randomInRange()
  );
}

loadPromise.then((gltf) => {
  const root = gltf.scene;
  root.traverse((child) => {
    if (child.isMesh) {
      child.castShadow = true;
      child.receiveShadow = true;
    }
  });

  root.position.set(0, 0, 0);
  group.add(root);
  birdsModel = root;

  group.position.set(
    randomInRange(),
    MIN_Y + Math.random() * (MAX_Y - MIN_Y),
    randomInRange()
  );
  pickTarget();

  if (gltf.animations && gltf.animations.length > 0) {
    mixer = new THREE.AnimationMixer(root);
    for (let i = 0; i < gltf.animations.length; i++) {
      mixer.clipAction(gltf.animations[i]).play();
    }
  }

  flightReady = true;
});

export function updateBirds() {
  const dt = clock.getDelta();
  if (mixer) {
    mixer.update(dt);
  }
  if (!flightReady) {
    return;
  }

  toTarget.copy(currentTarget).sub(group.position);
  let dist = toTarget.length();
  if (dist < REACH_DIST) {
    pickTarget();
    toTarget.copy(currentTarget).sub(group.position);
    dist = toTarget.length();
  }
  if (dist < 1e-4) {
    return;
  }
  toTarget.multiplyScalar(1 / dist);

  const step = Math.min(FLIGHT_SPEED * dt, dist);
  group.position.addScaledVector(toTarget, step);

  // 模型前向与移动方向相反时 + PI
  group.rotation.y = Math.atan2(toTarget.x, toTarget.z) + Math.PI;
}

export default group;

2026-03-29 16.43.00.gif

这样,我们就实现了同款鸟儿飞行的效果。

对比下:

https://summer-afternoon.vlucendo.com/

2026-03-29 15.21.02.gif

案例代码上传了小册仓库

总结

这节我们实现了鸟儿飞行的效果。

首先找了鸟的模型,带有飞行的骨骼动画。

然后通过随机确定目标位置,然后分步移动。

这样就可以实现鸟儿飞行的效果了,在 3D 场景内随机移动位置。

评论