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 图像绘制到 HTMLcanvas
元素上。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
。touchstart
和touchend
事件监听器用于在移动设备上实现滑动手势,从而触发场景旋转。
现在,添加动画循环和窗口大小调整的逻辑。
// ... 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)