经纬志
首页 / three.js / 教程

第一个场景:场景、相机与渲染器

先看结论

three.js 是一个通用的三维渲染库,负责把几何体、材质和灯光画到 WebGL 画布上。 它不提供地图、投影、瓦片或地理坐标,这些都要自己补。先跑通一个立方体,再谈地理数据。

最后验证 2026-11-03 · three.js 0.160.0 · 更新 2026-10-27 · 引用本文

它是什么,不是什么

把 three.js 当成「浏览器里的三维渲染引擎」最准确。它把 WebGL 的缓冲区、着色器、 纹理、渲染状态封装成对象,让开发者用场景图的方式组织内容。地图能力不在它的职责范围内。

它提供它不提供
场景图、相机、渲染器经纬度、投影、坐标参考系
几何体、材质、光照、纹理瓦片地图、地名搜索、路网服务
模型加载(glTF 等)与动画地形切片调度、3D Tiles 流式加载
拾取、后处理、阴影测绘坐标系转换与精度管理

所以用 three.js 做地理可视化,第一件事永远是把经纬度或投影坐标换算成场景里的 x / y / z,并自己决定单位和比例。这一步见 与地理数据结合。

完整起步代码

下面这段代码没有构建工具,直接写在 HTML 里即可运行。ESM 版本通过 importmap 把裸模块名 three 映射到 CDN 地址,页面里就能写普通的 import。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    html, body { height: 100%; margin: 0; }
    #stage { height: 100%; }
  </style>
</head>
<body>
  <div id="stage"></div>

  <script type="importmap">
  {
    "imports": {
      "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"
    }
  }
  </script>

  <script type="module">
    import * as THREE from 'three';

    const stage = document.getElementById('stage');

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.setSize(stage.clientWidth, stage.clientHeight);
    renderer.outputColorSpace = THREE.SRGBColorSpace;
    stage.appendChild(renderer.domElement);

    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x0d1117);

    const camera = new THREE.PerspectiveCamera(
      45, stage.clientWidth / stage.clientHeight, 0.1, 100
    );
    camera.position.set(3, 2, 4);
    camera.lookAt(0, 0, 0);

    scene.add(new THREE.HemisphereLight(0xffffff, 0x223344, 1.2));
    const key = new THREE.DirectionalLight(0xffffff, 2.2);
    key.position.set(4, 6, 3);
    scene.add(key);

    const cube = new THREE.Mesh(
      new THREE.BoxGeometry(1.4, 1.4, 1.4),
      new THREE.MeshStandardMaterial({ color: 0x4f9ef8, roughness: 0.35 })
    );
    scene.add(cube);

    renderer.setAnimationLoop(function () {
      cube.rotation.y += 0.01;
      cube.rotation.x += 0.004;
      renderer.render(scene, camera);
    });
  </script>
</body>
</html>

三件套各自的职责

对象职责最常改的成员
Scene 场景图的根节点,所有物体、灯光、相机的父级 background、fog、environment
PerspectiveCamera 定义观察位置与投影方式,决定画面里看到什么 position、fov、aspect、near/far
WebGLRenderer 把场景画到 canvas,管理 GPU 状态与输出色彩 setSize、setPixelRatio、outputColorSpace、shadowMap

渲染循环里只做一件事:renderer.render(scene, camera)。 画面不会自动更新,属性改了也要下一帧重画才会生效。

三个必须理解的点

一、画布尺寸与像素比

setSize 设置的是 CSS 尺寸,setPixelRatio 决定实际绘制缓冲区的分辨率。 高分屏上不加限制地使用 devicePixelRatio,像素数会翻几倍, 复杂场景直接掉帧。示例里把上限压到 2,是常见的折中。

容器尺寸变化后必须同时更新相机宽高比与渲染器尺寸,否则模型会被拉扁。 这也是三维页面最常见的「变形」来源。

二、相机参数决定画面

fov 是垂直视野角,near 和 far 是可视深度范围。 把 near 设得过小、far 设得过大,深度精度会被摊薄, 远处两个近似共面的表面会出现闪烁,这类现象叫深度冲突。 城市尺度场景通常把 far 收到实际需要的范围,而不是直接写一万。

camera.lookAt() 只改变朝向,不改变位置。相机放进了场景图之后, 位置是相对父节点的,如果父节点有变换,世界坐标会叠加这层变换。

三、用 setAnimationLoop 驱动

renderer.setAnimationLoop(fn) 内部使用 requestAnimationFrame, 同时兼容 WebXR。不要自己再套一层 setInterval 或 setTimeout, 那样无法与屏幕刷新同步,还会在后台标签页里空转耗电。

时间增量用 Clock 的 getDelta() 取,不要假设每帧都是 16 毫秒。 标签页切走再回来时,一次 delta 可能达到几秒,动画会瞬移,所以通常要截断上限。

坐标系与单位

three.js 默认 Y 轴向上,右手坐标系,单位完全由使用者决定:一米、一千米、一个像素都可以。 这和 GIS 的习惯相反,GIS 通常 Z 轴向上、X 东 Y 北。 把测绘数据放进 three 时,常见做法是让 x = 东、y = 高程、 z = −北,把平面从 XY 搬到 XZ。

常见问题

画面全黑,但控制台没有报错

按顺序排查:容器高度是否为 0;相机是否对准了物体;场景里是否只有 MeshBasicMaterial 之外的材质却没有任何灯光;近裁剪面是否把物体切掉了。 前两项占绝大多数。

颜色发灰、和设计稿不一致

颜色贴图的 colorSpace 要设为 SRGBColorSpace, 渲染器的 outputColorSpace 也要设成 sRGB。 两端都正确,颜色才会按预期显示;只设一边,画面会明显偏暗或偏亮。

缩放窗口后画面被拉伸

只调用了 renderer.setSize,没有更新 camera.aspect 和 camera.updateProjectionMatrix()。两者必须一起改。

先把度量定下来。三维地理场景里最容易被忽略的不是渲染,而是单位。 一米对应几个场景单位、垂直方向是否夸张、原点取在哪里,这三件事最好在写第一行代码前定好, 后面所有坐标转换都依赖它们。

引用本文

经纬志(2026)。《第一个场景:场景、相机与渲染器》。https://www.jingweizhi.com/three/tutorial/basics.html(访问日期 2026-11-03)。