Three.js 快速上手:用 npm 或 CDN 搭建第一个 3D 场景

作者:像素漫游

时间:2026-11-05

浏览:203

面向前端开发者的 Three.js 快速入门指南。详细讲解如何通过 CDN(结合现代 importmap)或 npm 工程化方式搭建首个包含旋转立方体、光照与自适应画布的 3D 场景,剖析核心三要素与渲染循环机制。

文章目录

在 Web 页面中呈现三维场景的核心逻辑,在于建立由场景容器、透视相机、网格模型与渲染器构成的协同闭环。本文分别针对单文件快速体验的 CDN 模式与面向工程化项目的 npm 模式,提供立即可运行的代码范例,并逐层剖析坐标投影、光影配合与逐帧渲染机制,帮助读者在浏览器中稳定跑通第一个旋转 3D 立方体。

一、Three.js 核心概念:构成 3D 场景的基本骨架

在原生 WebGL 中,开发者需要编写着色器语言(GLSL)、绑定顶点缓冲与计算投影矩阵,代码门槛极高。Three.js 将底层复杂的图形接口封装成了符合直觉的对象模型。无论通过 CDN 还是 npm 引入,任何基础三维应用都必须装配五个关键构件:

  • 场景(Scene):所有 3D 实体的逻辑根容器,充当整个虚拟三维世界的舞台。
  • 相机(Camera):观察舞台的视角入口,最常用的是具备近大远小特性的透视相机(PerspectiveCamera)。
  • 网格(Mesh):场景中具体的可见实体,由几何结构(Geometry)和外观材质(Material)组装而成。
  • 光源(Light):提供照射与明暗过渡,使物体表面具备立体感和光影质感。
  • 渲染器(WebGLRenderer):计算相机视角下的场景几何数据与着色效果,最终将像素绘制到 HTML5 的 <canvas> 画布中。
Three.js 快速上手:用 npm 或 CDN 搭建第一个 3D 场景
图 1:Three.js 核心对象装配拓扑关系图

二、方案一:通过 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>
运行方式注意事项 使用现代 ES 模块规范(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 快速上手:用 npm 或 CDN 搭建第一个 3D 场景
图 2:CDN 与 npm 两种搭建方式的实施路径对照

四、空间原理:三维坐标系与透视相机视锥体

初学者搭建场景时常遇到“满屏空白无显示”的情况,其本质是对 Three.js 默认空间规则缺乏理解。

Three.js 默认采用右手坐标系统:当面朝屏幕时,水平向右为 +X 轴,垂直向上为 +Y 轴,垂直屏幕向外(朝向观察者自身)为 +Z 轴。

当调用 new THREE.BoxGeometry() 生成立方体并添加到场景中时,默认几何中心恰好位于原点 (0, 0, 0)。而透视相机的初始坐标同样也是 (0, 0, 0)。如果不主动执行 camera.position.z = 4,相机就会被包裹在立方体内部;又因为 MeshStandardMaterial 默认只渲染几何体的外表面(前表面),从内部看就是一片漆黑。

Three.js 快速上手:用 npm 或 CDN 搭建第一个 3D 场景
图 3:右手坐标系统与透视相机投影视锥体

五、动起来:渲染循环与自适应窗口

静态场景只需调用一次 renderer.render(scene, camera)。但如果想要立方体持续转动或支持鼠标视角拖拽,就需要让浏览器以每秒约 60 次的频率连续计算并重绘屏幕。

1. 为什么必须使用 requestAnimationFrame 而非 setInterval

requestAnimationFrame 是浏览器专门为动画渲染设计的原生 API。它具备两个核心优势:

  • 屏幕刷新率精准对齐:浏览器会在准备进行下一帧屏幕重绘前调用指定函数,避免了 setInterval 因计时器偏差导致的丢帧或撕裂。
  • 节能与后台暂停:当用户切换浏览器标签页或窗口最小化时,浏览器会自动挂起回调调用,节约 CPU/GPU 功耗。

2. 窗口缩放自适应(Resize Handling)

当浏览器窗口大小被用户拖动调整时,如果不对 3D 视口进行同步更新,画面就会产生严重的拉伸或被挤压畸变。自适应必须包含三个联动操作:

  1. 更新相机的纵横比属性:camera.aspect = window.innerWidth / window.innerHeight;
  2. 重算相机投影矩阵:camera.updateProjectionMatrix();(必须显式调用,告知底层矩阵重新生成)。
  3. 同步画布尺寸与渲染视口:renderer.setSize(window.innerWidth, window.innerHeight);
Three.js 快速上手:用 npm 或 CDN 搭建第一个 3D 场景
图 4:基于 requestAnimationFrame 的渲染循环与生命周期

六、初学者最易遇到的 4 个典型问题与排查方法

在搭建第一个 3D 场景的过程中,以下四个问题覆盖了超过 80% 的初期故障:

  1. 画面漆黑一片且控制台没有报错

    排查优先级一:检查相机 Z 轴位置。如果未执行 camera.position.z = 5,相机将默认停留在立方体中心。
    排查优先级二:检查材质与灯光的搭配。如果使用了 MeshStandardMaterial 或 MeshLambertMaterial 这类需要光照计算的材质,但场景中漏掉了 AmbientLight 或 DirectionalLight,由于没有任何光线射入,物体会显示为完全不反射光线的纯黑色。若想快速排查材质问题,可暂时换用不需要任何光照的基础材质 MeshBasicMaterial({ color: 0x00ff00 })。

  2. 直接双击 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 协议承载。

  3. 缩放浏览器窗口后画面比例失调或边缘锯齿严重

    在 resize 监听器中,仅修改 renderer.setSize 是不够的,必须同时更新 camera.aspect 并调用 camera.updateProjectionMatrix()。此外在 Retina 或高分辨率屏上,需配置 renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)),避免边缘模糊,同时限制最大倍率为 2,以防移动设备 3 倍屏引起显存与 GPU 算力浪费。

  4. npm 安装后无法直接解构引用扩展模块

    自新版本起,像 OrbitControls、GLTFLoader 这类附加模块并不在 import { OrbitControls } from 'three' 的根包中,必须通过官方子路径 import { OrbitControls } from 'three/addons/controls/OrbitControls.js' 进行引用。

Three.js 快速上手:用 npm 或 CDN 搭建第一个 3D 场景
图 5:选型决策矩阵与运行前关键排查清单

七、总结与下一步扩展

搭建第一个 Three.js 场景的整个过程,可以高度浓缩为一个清晰的组装公式:

Three.js 核心运行公式 3D 输出 = WebGLRenderer ( Scene [ 网格几何体 + 材质外观 + 光源配置 ] , PerspectiveCamera ) + requestAnimationFrame 渲染循环

在成功跑通并理解旋转立方体后,接下来可以沿着以下两个方向继续深入:

  • 增加视角拖拽控制器(OrbitControls):允许用户通过鼠标左键旋转视角、滚轮缩放、右键平移,获得直观的交互体验。
  • 加载真实 3D 资产(GLTF / GLB):通过 GLTFLoader 载入外部由 Blender 等专业三维软件导出的工业模型、人物角色或建筑场景,迈入更复杂的专业 Web3D 应用开发。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章