第一个场景:场景、相机与渲染器
three.js 是一个通用的三维渲染库,负责把几何体、材质和灯光画到 WebGL 画布上。 它不提供地图、投影、瓦片或地理坐标,这些都要自己补。先跑通一个立方体,再谈地理数据。
它是什么,不是什么
把 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)。