面向前端开发者的 Three.js 快速入门指南。详细讲解如何通过 CDN(结合现代 importmap)或 npm 工程化方式搭建首个包含旋转立方体、光照与自适应画布的 3D 场景,剖析核心三要素与渲染循环机制。
Three.js 快速上手:用 npm 或 CDN 搭建第一个 3D 场景
时间:2026-11-05
浏览:203
文章目录
在 Web 页面中呈现三维场景的核心逻辑,在于建立由场景容器、透视相机、网格模型与渲染器构成的协同闭环。本文分别针对单文件快速体验的 CDN 模式与面向工程化项目的 npm 模式,提供立即可运行的代码范例,并逐层剖析坐标投影、光影配合与逐帧渲染机制,帮助读者在浏览器中稳定跑通第一个旋转 3D 立方体。
一、Three.js 核心概念:构成 3D 场景的基本骨架
在原生 WebGL 中,开发者需要编写着色器语言(GLSL)、绑定顶点缓冲与计算投影矩阵,代码门槛极高。Three.js 将底层复杂的图形接口封装成了符合直觉的对象模型。无论通过 CDN 还是 npm 引入,任何基础三维应用都必须装配五个关键构件:
- 场景(Scene):所有 3D 实体的逻辑根容器,充当整个虚拟三维世界的舞台。
- 相机(Camera):观察舞台的视角入口,最常用的是具备近大远小特性的透视相机(
PerspectiveCamera)。 - 网格(Mesh):场景中具体的可见实体,由几何结构(
Geometry)和外观材质(Material)组装而成。 - 光源(Light):提供照射与明暗过渡,使物体表面具备立体感和光影质感。
- 渲染器(WebGLRenderer):计算相机视角下的场景几何数据与着色效果,最终将像素绘制到 HTML5 的
<canvas>画布中。
二、方案一:通过 CDN 零构建快速运行(适合快速验证与教学演示)
如果只是想快速验证一段 3D 逻辑、编写课件示例或在 CodePen/JSFiddle 等在线平台演示,不需要搭建脚手架,直接引入 CDN 是最轻量的方式。
在过去,通过 <script src="..."> 引入脚本会向全局注入一个 window.THREE 变量。现代 Three.js(自 r137 起)已全面转向原生 ES 模块化(ESM)。推荐使用现代浏览器原生支持的 <script type="importmap"> 来映射模块标识符,保持与标准 npm 项目一致的代码书写体验。
1. 完整单文件 index.html 代码
创建一个名为 index.html 的本地文件,复制以下完整代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js CDN 快速上手</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
background-color: #1a1a1a;
}
#webgl-canvas {
width: 100vw;
height: 100vh;
display: block;
}
</style>
<!-- 关键:使用 importmap 映射模块路径到 CDN -->
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
</head>
<body>
<canvas id="webgl-canvas"></canvas>
<script type="module">
import * as THREE from 'three';
// 1. 初始化场景
const scene = new THREE.Scene();
// 2. 初始化透视相机 (视场角 75, 宽高比, 近剪裁面 0.1, 远剪裁面 1000)
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
// 将相机沿 Z 轴向外移动,否则相机会停在坐标原点 (0, 0, 0) 内部
camera.position.z = 4;
// 3. 初始化渲染器并绑定到页面现有 canvas 上
const canvas = document.getElementById('webgl-canvas');
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true // 开启边缘抗锯齿
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// 4. 创建一个带有几何体与材质的网格立方体
const geometry = new THREE.BoxGeometry(1.5, 1.5, 1.5);
// 使用支持光影漫反射的标准材质 MeshStandardMaterial
const material = new THREE.MeshStandardMaterial({
color: 0xad8b58,
roughness: 0.3,
metalness: 0.2
});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 5. 添加光源(标准材质需要光照才能显色)
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
dirLight.position.set(5, 5, 5);
scene.add(dirLight);
// 6. 监听窗口缩放,更新相机宽高比与渲染器尺寸
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});
// 7. 渲染循环函数
function animate() {
requestAnimationFrame(animate);
// 每帧自转旋转量
cube.rotation.x += 0.01;
cube.rotation.y += 0.015;
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>
type="module")时,浏览器会基于安全策略限制本地直接双击打开(file:// 协议会触发 CORS 跨域拦截)。你必须使用本地 HTTP 服务器运行此文件。例如在当前文件夹终端中执行 npx serve 或 Python 内置命令 python3 -m http.server 8000,随后访问 http://localhost:8000。
三、方案二:通过 npm 与现代化构建工具搭建(适合实际项目与生产环境)
在正式开发项目中,通常需要引入第三方模型加载器(如 GLTFLoader)、控制器(OrbitControls)、动画计算库(GSAP)以及代码压缩与类型支持。此时推荐使用 npm 包管理器结合构建工具(如 Vite)。
1. 步骤与关键命令
以速度极快的 Vite 为例,打开终端执行以下步骤:
第一步:初始化前端脚手架项目
npm create vite@latest three-npm-starter -- --template vanilla
第二步:进入目录并安装 Three.js 核心库
cd three-npm-starter
npm install three
第三步:启动本地热更新开发服务器
npm run dev
控制台将输出本地访问地址(例如 http://localhost:5173/)。
2. 项目核心文件组织与源码
在项目中保持结构简洁,主要修改以下三个文件:
(1) index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Three.js npm 工程化场景</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<canvas id="webgl"></canvas>
<script type="module" src="./main.js"></script>
</body>
</html>
(2) style.css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
background-color: #12181f;
}
#webgl {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
outline: none;
}
(3) main.js
import * as THREE from 'three';
// 从 three/addons/ 中引入轨道交互控制器 OrbitControls
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// 获取画布 DOM
const canvas = document.querySelector('#webgl');
// 1. 场景
const scene = new THREE.Scene();
// 2. 几何体与网格
const geometry = new THREE.BoxGeometry(1.6, 1.6, 1.6);
const material = new THREE.MeshStandardMaterial({
color: 0x394957,
roughness: 0.25,
metalness: 0.65
});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 3. 光照系统
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambientLight);
const pointLight = new THREE.PointLight(0xad8b58, 20, 50);
pointLight.position.set(3, 4, 3);
scene.add(pointLight);
// 4. 视角与相机
const sizes = {
width: window.innerWidth,
height: window.innerHeight
};
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100);
camera.position.set(2.5, 2.5, 4);
scene.add(camera);
// 5. 渲染器
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true
});
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// 6. 添加鼠标交互控制器
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true; // 开启阻尼惯性效果
// 7. 响应窗口变动
window.addEventListener('resize', () => {
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});
// 8. 动画与渲染循环
const clock = new THREE.Clock();
const tick = () => {
const elapsedTime = clock.getElapsedTime();
// 让立方体沿自身轴向转动,同时略微上下浮动
cube.rotation.y = elapsedTime * 0.8;
cube.position.y = Math.sin(elapsedTime * 1.5) * 0.2;
// 更新控制器阻尼状态
controls.update();
renderer.render(scene, camera);
window.requestAnimationFrame(tick);
};
tick();
四、空间原理:三维坐标系与透视相机视锥体
初学者搭建场景时常遇到“满屏空白无显示”的情况,其本质是对 Three.js 默认空间规则缺乏理解。
Three.js 默认采用右手坐标系统:当面朝屏幕时,水平向右为 +X 轴,垂直向上为 +Y 轴,垂直屏幕向外(朝向观察者自身)为 +Z 轴。
当调用 new THREE.BoxGeometry() 生成立方体并添加到场景中时,默认几何中心恰好位于原点 (0, 0, 0)。而透视相机的初始坐标同样也是 (0, 0, 0)。如果不主动执行 camera.position.z = 4,相机就会被包裹在立方体内部;又因为 MeshStandardMaterial 默认只渲染几何体的外表面(前表面),从内部看就是一片漆黑。
五、动起来:渲染循环与自适应窗口
静态场景只需调用一次 renderer.render(scene, camera)。但如果想要立方体持续转动或支持鼠标视角拖拽,就需要让浏览器以每秒约 60 次的频率连续计算并重绘屏幕。
1. 为什么必须使用 requestAnimationFrame 而非 setInterval
requestAnimationFrame 是浏览器专门为动画渲染设计的原生 API。它具备两个核心优势:
- 屏幕刷新率精准对齐:浏览器会在准备进行下一帧屏幕重绘前调用指定函数,避免了
setInterval因计时器偏差导致的丢帧或撕裂。 - 节能与后台暂停:当用户切换浏览器标签页或窗口最小化时,浏览器会自动挂起回调调用,节约 CPU/GPU 功耗。
2. 窗口缩放自适应(Resize Handling)
当浏览器窗口大小被用户拖动调整时,如果不对 3D 视口进行同步更新,画面就会产生严重的拉伸或被挤压畸变。自适应必须包含三个联动操作:
- 更新相机的纵横比属性:
camera.aspect = window.innerWidth / window.innerHeight; - 重算相机投影矩阵:
camera.updateProjectionMatrix();(必须显式调用,告知底层矩阵重新生成)。 - 同步画布尺寸与渲染视口:
renderer.setSize(window.innerWidth, window.innerHeight);
六、初学者最易遇到的 4 个典型问题与排查方法
在搭建第一个 3D 场景的过程中,以下四个问题覆盖了超过 80% 的初期故障:
-
画面漆黑一片且控制台没有报错
排查优先级一:检查相机 Z 轴位置。如果未执行
camera.position.z = 5,相机将默认停留在立方体中心。
排查优先级二:检查材质与灯光的搭配。如果使用了MeshStandardMaterial或MeshLambertMaterial这类需要光照计算的材质,但场景中漏掉了AmbientLight或DirectionalLight,由于没有任何光线射入,物体会显示为完全不反射光线的纯黑色。若想快速排查材质问题,可暂时换用不需要任何光照的基础材质MeshBasicMaterial({ color: 0x00ff00 })。 -
直接双击 index.html 报错或 CDN 无法加载
控制台常报
Access to script at '...' from origin 'null' has been blocked by CORS policy。浏览器对原生 ES 模块(<script type="module">)执行严格的同源策略保护,不允许以file:///协议直接执行。必须使用任何轻量静态服务器(如 VS Code 的 Live Server 插件、Node.js 的serve或 Python 的http.server)通过 HTTP/HTTPS 协议承载。 -
缩放浏览器窗口后画面比例失调或边缘锯齿严重
在
resize监听器中,仅修改renderer.setSize是不够的,必须同时更新camera.aspect并调用camera.updateProjectionMatrix()。此外在 Retina 或高分辨率屏上,需配置renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)),避免边缘模糊,同时限制最大倍率为 2,以防移动设备 3 倍屏引起显存与 GPU 算力浪费。 -
npm 安装后无法直接解构引用扩展模块
自新版本起,像
OrbitControls、GLTFLoader这类附加模块并不在import { OrbitControls } from 'three'的根包中,必须通过官方子路径import { OrbitControls } from 'three/addons/controls/OrbitControls.js'进行引用。
七、总结与下一步扩展
搭建第一个 Three.js 场景的整个过程,可以高度浓缩为一个清晰的组装公式:
3D 输出 = WebGLRenderer ( Scene [ 网格几何体 + 材质外观 + 光源配置 ] , PerspectiveCamera ) + requestAnimationFrame 渲染循环
在成功跑通并理解旋转立方体后,接下来可以沿着以下两个方向继续深入:
- 增加视角拖拽控制器(OrbitControls):允许用户通过鼠标左键旋转视角、滚轮缩放、右键平移,获得直观的交互体验。
- 加载真实 3D 资产(GLTF / GLB):通过
GLTFLoader载入外部由 Blender 等专业三维软件导出的工业模型、人物角色或建筑场景,迈入更复杂的专业 Web3D 应用开发。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。