Three.js案例
Three.js 行星展示项目实战教程

Three.js 行星展示项目实战教程

今天看到了一个案例,感觉不复杂,但是效果看起来还不错。一起逐步构建一个使用 Three.js 实现的交互式 3D 行星展示页面。

效果如下

项目初始化

在开始之前,需要先设置项目结构并安装必要的依赖库。

首先,通过 vite 安装项目,然后安装依赖

npm i three gspa tailwindcss @tailwindcss/vite

创建 HTML 骨架

接下来,创建项目的入口文件 index.html。这是浏览器加载的第一个文件,它将包含 3D 场景的画布和页面上的文本元素。

在项目根目录创建 index.html 文件,并填入以下内容:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="icon" href="./logo.png" />
    <title>The Planets</title>
    <link rel="stylesheet" href="./src/style.css" />
  </head>
  <body>
    <div class="w-full h-screen overflow-hidden">
      <div
        class="w-full h-screen absolute top-0 left-0 z-[2] text-white font-['LATO']"
      >
        <div
          class="absolute top-[4%] left-1/2 -translate-x-1/2 text-center font-['Bellina']"
        >
          <div class="heading w-full h-[4em] md:h-[8em] overflow-hidden">
            <h1 class="h-full text-5xl font-light tracking-tighter md:text-9xl">
              Earth
            </h1>
            <h1 class="h-full text-5xl font-light tracking-tighter md:text-9xl">
              Csilla
            </h1>
            <h1 class="h-full text-5xl font-light tracking-tighter md:text-9xl">
              Mars
            </h1>
            <h1 class="h-full text-5xl font-light tracking-tighter md:text-9xl">
              Venus
            </h1>
          </div>
 
          <p class="text-sm font-['LATO']">
            Lorem ipsum dolor, sit amet consectetur adipisicing elit.
          </p>
 
          <div
            class="h-px mt-4 w-96 bg-gradient-to-r from-transparent via-white to-transparent"
          ></div>
        </div>
      </div>
 
      <canvas id="canvas" class="bg-black"></canvas>
    </div>
 
    <script type="module" src="./src/main.js"></script>
  </body>
</html>

添加基础样式

为了让页面具有美观的布局和字体,需要创建 src/style.css 文件来定义样式。

src 目录下创建 style.css 文件,并添加以下代码:

@import "tailwindcss";
 
body,
html {
  margin: 0;
  padding: 0;
  overflow: hidden;
  touch-action: pan-y;
}
 
body {
  background-color: black;
}
 
@font-face {
  font-family: "LATO";
  src: url("/Fonts/Lato.ttf") format("truetype");
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}
 
@font-face {
  font-family: "Bellina";
  src: url("/Fonts/Bellina.ttf") format("truetype");
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}
  • @import "tailwindcss"; 引入了 Tailwind CSS 框架,用于快速构建 UI。
  • body, html 的样式重置了默认的边距和内边距,并禁用了页面滚动。
  • @font-face 规则用于加载自定义字体,以增强页面的视觉效果。

搭建 3D 场景

现在,开始编写核心的 3D 逻辑。创建 src/main.js 文件,并引入所需的库。

首先,从 three 和其他模块中导入必要的类。

import * as THREE from "three";
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js";
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass.js";
import gsap from "gsap";
  • RGBELoader 用于加载 HDR 环境贴图。
  • EffectComposer, RenderPass, UnrealBloomPass 用于实现后期处理效果,例如辉光。
  • gsap 是一个强大的动画库,用于创建平滑的过渡效果。

接下来,初始化渲染器、场景和相机,这是构成任何 Three.js 应用的基础。

// ... existing code ...
import gsap from "gsap";
 
const canvas = document.getElementById("canvas");
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setClearColor(0x000000);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
 
// Scene, Camera
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
  25,
  window.innerWidth / window.innerHeight,
  0.1,
  100
);
camera.position.set(0, 0, 10);
  • WebGLRenderer 是渲染器实例,它将场景和相机作为输入,并将 3D 图像绘制到 HTML canvas 元素上。antialias: true 用于开启抗锯齿。
  • Scene 是一个容器,用于存放所有 3D 对象、灯光和相机。
  • PerspectiveCamera 模拟了人眼的视觉效果,position.set(0, 0, 10) 将相机放置在 Z 轴上,朝向原点。

添加背景和环境光

为了让场景更加逼真,需要添加一个星空背景和环境光。

首先,加载一张星空图片作为场景的背景。

// ... existing code ...
camera.position.set(0, 0, 10);
 
// HDRI
new RGBELoader().load(
  "https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/1k/moonlit_golf_1k.hdr",
  (texture) => {
    texture.mapping = THREE.EquirectangularReflectionMapping;
    scene.environment = texture;
  }
);
 
// Stars
const starTexture = new THREE.TextureLoader().load("./stars.jpg");
starTexture.colorSpace = THREE.SRGBColorSpace;
const starSphere = new THREE.Mesh(
  new THREE.SphereGeometry(40, 64, 64),
  new THREE.MeshStandardMaterial({ map: starTexture, side: THREE.BackSide })
);
scene.add(starSphere);
  • RGBELoader 用于加载一个 .hdr 格式的高动态范围图像,并将其设置为场景的 environment。这会为场景中的所有物体提供基于图像的照明。
  • TextureLoader 用于加载普通的图像文件。./stars.jpg 将被用作一个巨大球体的纹理。
  • SphereGeometry 创建了一个球体几何体。
  • MeshStandardMaterial 是一种标准的物理渲染材质。map 属性指定了纹理,side: THREE.BackSide 确保纹理在球体内部可见,从而形成一个包裹整个场景的星空穹顶。
  • scene.add(starSphere) 将创建好的星空球体添加到场景中。

创建行星

接下来,创建环绕中心旋转的行星。

首先定义一个包含行星纹理路径的数组,然后遍历数组为每个行星创建一个带纹理的球体。

// ... existing code ...
scene.add(starSphere);
 
// Planets
const textures = [
  "./volcanic/color.png",
  "./earth/map.jpg",
  "./csilla/color.png",
  "./venus/map.jpg",
];
 
const spheres = new THREE.Group();
const spheresMesh = [];
 
textures.forEach((texPath, i) => {
  const tex = new THREE.TextureLoader().load(texPath);
  tex.colorSpace = THREE.SRGBColorSpace;
 
  const sphere = new THREE.Mesh(
    new THREE.SphereGeometry(1.44, 64, 64),
    new THREE.MeshStandardMaterial({ map: tex })
  );
 
  const angle = (i / textures.length) * Math.PI * 2;
  sphere.position.x = 4.3 * Math.cos(angle);
  sphere.position.z = 4.3 * Math.sin(angle);
 
  spheres.add(sphere);
  spheresMesh.push(sphere);
});
 
spheres.rotation.x = 0.14;
spheres.position.y = -0.65;
scene.add(spheres);
  • textures 数组存储了每个行星的纹理图片路径。
  • THREE.Group 是一个容器,用于将多个对象组合在一起,方便统一操作。
  • 循环遍历 textures 数组,为每个路径创建一个 Mesh
  • tex.colorSpace = THREE.SRGBColorSpace 确保纹理颜色在渲染时是正确的。
  • 通过三角函数 Math.cos(angle)Math.sin(angle) 计算每个球体的位置,使它们能够均匀地分布在一个圆形轨道上。
  • spheres.add(sphere) 将每个行星球体添加到一个 Group 中,最后将整个 Group 添加到场景中。

添加后期处理效果

为了让行星看起来有辉光效果,需要使用后期处理。

配置 EffectComposer 并添加 UnrealBloomPass 来实现辉光。

// ... existing code ...
spheres.position.y = -0.65;
scene.add(spheres);
 
// Post-processing Composer
const composer = new EffectComposer(renderer);
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
 
const bloomPass = new UnrealBloomPass(
  new THREE.Vector2(window.innerWidth, window.innerHeight),
  0.7,
  0.4,
  0.85
);
composer.addPass(bloomPass);
  • EffectComposer 是后期处理效果的管理器,它按顺序应用一系列的 "Pass"。
  • RenderPass 是最基础的 Pass,它负责将场景渲染出来。
  • UnrealBloomPass 用于创建辉光效果,使场景中明亮的区域产生光晕。参数分别控制了效果的分辨率、强度、半径和阈值。

实现动画与交互

为了让场景动起来并响应用户输入,需要创建一个动画循环,并添加事件监听器。

首先,创建 animate 函数,并添加行星自转的逻辑。

// ... existing code ...
composer.addPass(bloomPass);
 
// Text + Scroll
let headingIndex = 0;
const totalHeadings = textures.length;
let isScrolling = false;
 
function rotateScene(dir) {
  if (isScrolling) return;
  isScrolling = true;
 
  gsap.to(spheres.rotation, {
    y: `+=${dir * Math.PI / 2}`,
    duration: 2,
    ease: 'power2.inOut',
  });
 
  headingIndex = (headingIndex + dir + totalHeadings) % totalHeadings;
  const headings = document.querySelectorAll('.heading h1');
  gsap.to(headings, {
    y: `-${headingIndex * 100}%`,
    duration: 1.5,
    ease: 'power2.inOut'
  });
 
  setTimeout(() => (isScrolling = false), 2000);
}
 
// Scroll and Swipe
window.addEventListener('wheel', (e) => rotateScene(e.deltaY > 0 ? 1 : -1));
 
let touchStartX = 0;
window.addEventListener('touchstart', (e) => {
  touchStartX = e.touches[0].clientX;
});
window.addEventListener('touchend', (e) => {
  const diffX = e.changedTouches[0].clientX - touchStartX;
  if (Math.abs(diffX) > 50) rotateScene(diffX < 0 ? 1 : -1);
});
 
// Resize
window.addEventListener('resize', () => {
// ... existing code ...
  • rotateScene 函数是交互的核心。它使用 gsap 库来平滑地旋转行星组,并同步更新顶部的标题文本。
  • isScrolling 变量用作一个简单的节流阀,防止在动画期间触发新的动画。
  • window.addEventListener('wheel', ...) 监听鼠标滚轮事件,根据滚动方向调用 rotateScene
  • touchstarttouchend 事件监听器用于在移动设备上实现滑动手势,从而触发场景旋转。

现在,添加动画循环和窗口大小调整的逻辑。

// ... existing code ...
  if (Math.abs(diffX) > 50) rotateScene(diffX < 0 ? 1 : -1);
});
 
// Resize
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  composer.setSize(window.innerWidth, window.innerHeight);
});
 
// Animate
const clock = new THREE.Clock();
function animate() {
  requestAnimationFrame(animate);
  spheresMesh.forEach(s => s.rotation.y = clock.getElapsedTime() * 0.04);
  composer.render();
}
animate();
  • resize 事件监听器确保在浏览器窗口大小改变时,渲染器和相机的尺寸也相应更新,避免画面变形。
  • animate 函数是渲染循环的核心。requestAnimationFrame 会在每帧浏览器重绘前调用该函数。
  • clock.getElapsedTime() 获取自时钟创建以来经过的秒数,用它来驱动行星的持续自转。
  • composer.render() 取代了 renderer.render(),因为它会应用所有在 composer 中配置的后期处理效果。
  • animate() 在最后被调用一次,以启动整个动画循环。

代码

https://github.com/riki-k-dev/the-planets (opens in a new tab)