Three.js案例
Three.js实现飞机沿着特定路线飞行

Three.js 实现飞机沿着特定路线飞行

在这个示例中,我们将通过 Three.js 来构建一个包含场景(Scene)、相机(Camera)、渲染器(Renderer)、飞机模型以及飞行路径的小型三维环境。效果类似一个初级版的“飞行模拟器”:一架飞机沿着一条预设的曲线飞行,同时伴随天空与光照效果。

模型准备

访问

https://sketchfab.com/3d-models/low-poly-airplane-65cc7c4349174f7bbb20ed70206377b5#download (opens in a new tab)

下载模型

场景、相机与渲染器

  1. 使用 THREE.Scene() 来创建一个场景容器,并通过 scene.fog 增加雾效,以提升空间层次感。
  2. 使用 THREE.PerspectiveCamera 来生成透视相机,并通过 camera.position.set(0,5,10) 放置到略微俯视的角度。
  3. 使用 THREE.WebGLRenderer 进行渲染,并通过开启 antialiasshadowMap 以及合适的 toneMapping 配置来提升视觉表现。
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { Sky } from "three/examples/jsm/objects/Sky";
import { FogExp2 } from "three";
 
import "./style.css";
 
// 创建场景
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0xdfe9f3, 0.02); // 添加雾效果
 
// 创建相机
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
camera.position.set(0, 5, 10);
 
// 创建渲染器
const renderer = new THREE.WebGLRenderer({
  antialias: true,
  alpha: true,
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 0.5;
document.body.appendChild(renderer.domElement);

添加天空盒(Sky)

  1. 使用 new Sky() 来创建天空对象,并通过 sky.scale.setScalar(1000) 让天空能够包围整个场景。
  2. 使用 turbidity, rayleigh, mieCoefficient 等参数来微调天空的光学特性,从而达到想要的颜色和散射效果。
  3. 使用 sunPosition 来指定太阳位置,通过球面坐标计算来模拟更真实的光照角度。
// 添加天空
const sky = new Sky();
sky.scale.setScalar(1000);
scene.add(sky);
 
// 设置天空材质参数
const sun = new THREE.Vector3();
const uniforms = sky.material.uniforms;
uniforms["turbidity"].value = 10; // 大气浑浊度
uniforms["rayleigh"].value = 2; // 瑞利散射系数
uniforms["mieCoefficient"].value = 0.005; // 米氏散射系数
uniforms["mieDirectionalG"].value = 0.8; // 光线与视角的相位函数
 
// 设置太阳位置
const phi = THREE.MathUtils.degToRad(90 - 2);
const theta = THREE.MathUtils.degToRad(180);
sun.setFromSphericalCoords(1, phi, theta);
uniforms["sunPosition"].value.copy(sun);

轨道控制器 (OrbitControls)

  1. 使用 OrbitControls 来允许用户在浏览器里通过鼠标或触摸手势自由旋转、缩放和移动相机视角。
  2. 使用 enableDampingdampingFactor 来提供更平滑的视角移动体验。
  3. 使用 maxDistanceminDistance 来限制相机的最远和最近距离,避免视角拉得过远或过近。
// 添加轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.maxDistance = 50;
controls.minDistance = 3;

飞机模型的加载

缩放(Scale)与旋转(Rotation)

  1. 使用 gltf.scene.scale 来调整飞机模型的大小,让它与场景的比例更匹配。
  2. 使用 gltf.scene.rotation 来给模型做适当旋转,确保其朝向正确。
  3. 使用 gltf.scene.traverse 来遍历模型内所有网格,统一开启或配置阴影以及材质的属性。
// 创建一个组来存放飞机模型
const airplane = new THREE.Group();
scene.add(airplane);
 
// 使用 GLTFLoader 加载飞机模型
const loader = new GLTFLoader();
loader.load(
  "/airplane/scene.gltf",
  (gltf) => {
    // 缩放与旋转模型
    gltf.scene.scale.set(0.01, 0.01, 0.01);
    gltf.scene.rotation.set(0, Math.PI, 0);
 
    // 遍历子网格并设置阴影
    gltf.scene.traverse((child) => {
      if (child.isMesh) {
        child.castShadow = true;
        child.receiveShadow = true;
        if (child.material) {
          child.material.envMapIntensity = 1;
          child.material.needsUpdate = true;
        }
      }
    });
 
    airplane.add(gltf.scene);
  },
  (progress) => {
    console.log("加载进度:", (progress.loaded / progress.total) * 100 + "%");
  },
  (error) => {
    console.error("模型加载出错:", error);
  }
);

如果还想改变模型在场景内的位置(例如略微抬高到地面之上),可以使用 gltf.scene.position.set(0,1,0)

光源设置

  1. 使用 AmbientLight 来保证场景不会出现完全漆黑的区域,提供柔和的整体亮度。
  2. 使用 DirectionalLight(方向光)作为主要光源,并设置它的位置和阴影参数,让飞机模型产生逼真的投影。
  3. 我们需要添加补光 fillLight 来让相反方向也有一定亮度,避免一面过亮、一面过暗。
// 环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
 
// 主光源(方向光)
const mainLight = new THREE.DirectionalLight(0xffffff, 1);
mainLight.position.set(5, 5, 5);
mainLight.castShadow = true;
mainLight.shadow.mapSize.width = 2048;
mainLight.shadow.mapSize.height = 2048;
mainLight.shadow.camera.near = 0.1;
mainLight.shadow.camera.far = 100;
mainLight.shadow.camera.left = -20;
mainLight.shadow.camera.right = 20;
mainLight.shadow.camera.top = 20;
mainLight.shadow.camera.bottom = -20;
scene.add(mainLight);
 
// 补光
const fillLight = new THREE.DirectionalLight(0x8088ff, 0.4);
fillLight.position.set(-5, 3, -5);
scene.add(fillLight);

飞行路径的创建

  1. 使用 CatmullRomCurve3 来生成一条平滑的样条曲线,它可以帮助我们设置飞机的飞行动作,让曲线在五个关键点之间自然过渡。
  2. 我们可以随时增减控制点,或调整它们的坐标,以改变飞机的飞行轨迹。
// 创建飞行路径
const curve = new THREE.CatmullRomCurve3([
  new THREE.Vector3(-10, 0, 0),
  new THREE.Vector3(-5, 4, 5),
  new THREE.Vector3(0, 0, 0),
  new THREE.Vector3(5, 4, -5),
  new THREE.Vector3(10, 0, 0),
]);

可视化路径(虚线)

  1. 使用 curve.getPoints(50) 来采样 50 个点,以便将曲线绘制出来。
  2. 使用 LineDashedMaterial 来渲染虚线效果,并调用 computeLineDistances() 来计算虚线的间隔。
  3. 我们将生成的 pathLine 添加到场景中,以清晰显示飞机的移动路线。
// 可视化路径
const points = curve.getPoints(50);
const pathGeometry = new THREE.BufferGeometry().setFromPoints(points);
const pathMaterial = new THREE.LineDashedMaterial({
  color: 0xffffff,
  dashSize: 0.5,
  gapSize: 0.3,
  opacity: 0.5,
  transparent: true,
});
const pathLine = new THREE.Line(pathGeometry, pathMaterial);
pathLine.computeLineDistances();
scene.add(pathLine);

动画循环:让飞机沿曲线飞行

  1. 使用 requestAnimationFrame(animate) 来在每帧更新并渲染场景,实现流畅的动画效果。
  2. 使用 curve.getPoint(progress) 来确定飞机在曲线上的位置,并使用 curve.getTangent(progress) 来计算飞机沿曲线的方向,从而让飞机始终面朝前方。curve.getTangent(progress) 会先计算在曲线对应点处的微分值(或通过数值方式计算导数),然后将该结果标准化(单位化),得到一个长度为 1 的方向向量
  3. 使用 controls.update() 来确保轨道控制器在用户操作后能实时刷新视图。
// 动画参数
let progress = 0;
const speed = 0.001;
 
function animate() {
  requestAnimationFrame(animate);
 
  // 更新飞机位置
  progress += speed;
  if (progress > 1) progress = 0;
 
  const point = curve.getPoint(progress);
  airplane.position.copy(point);
 
  // 计算飞机朝向
  const tangent = curve.getTangent(progress);
  const up = new THREE.Vector3(0, 1, 0);
  const matrix = new THREE.Matrix4();
  matrix.lookAt(new THREE.Vector3(0, 0, 0), tangent, up);
  airplane.quaternion.setFromRotationMatrix(matrix);
 
  // 更新轨道控制器
  controls.update();
 
  // 渲染场景
  renderer.render(scene, camera);
}
 
// 处理窗口大小变化
window.addEventListener("resize", () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});
 
// 开始动画
animate();

代码

github

https://github.com/calmound/threejs-demo/tree/main/airplane (opens in a new tab)

gitee

https://gitee.com/calmound/threejs-demo/tree/main/airplane (opens in a new tab)