在 WebGIS 开发中,三维可视化不再局限于地球 / 地图场景 ------ 纯三维建模(Three.js)能更灵活地展示地理数据的空间形态与分析结果。Turf.js 作为前端空间分析核心库,可完成地理坐标处理、距离计算、质心分析等基础操作;Three.js 则专注于三维几何体创建、材质渲染、交互控制,两者结合可实现 "地理数据 → 空间分析 → 三维建模 → 可视化着色" 的完整流程。本文将通过实战案例,带你掌握 Turf.js 与 Three.js 的结合使用方法,实现面要素三维地形建模、线要素三维路径生成、空间分析结果着色三大核心功能。
一、技术栈说明
- 框架:Vue3(Composition API +
<script setup>) - 空间分析:Turf.js(@turf/turf v7+,核心 API:
polygon、lineString、centroid、distance) - 三维建模:Three.js r158+(核心能力:几何体创建、材质渲染、轨道控制、顶点着色)
- 辅助控件:OrbitControls(Three.js 官方轨道控制器)
- UI 组件库:Element Plus(卡片、栅格、输入框、按钮)
- 样式:Less(模块化样式管理)
- 核心功能:经纬度转平面坐标、面要素挤出三维地形、线要素生成平滑路径、按空间距离顶点着色、三维场景交互控制
二、环境搭建(复用前序环境)
若已完成前序文章的 Vue3 环境搭建,仅需新增 Three.js 依赖;若未搭建,执行以下命令:
bash
# 1. 初始化Vue3项目(如需新建)
npm create vite@latest turf-three-demo -- --template vue
cd turf-three-demo
npm install
# 2. 安装核心依赖
npm install @turf/turf element-plus @element-plus/icons-vue three --save
三、核心功能实现:地理数据三维建模组件
1. 组件完整代码(可直接复用)
javascript
<template>
<div class="turf-three-modeler">
<el-card class="panel">
<div class="header">
<h2>Turfjs+Three.js:地理数据的三维建模应用</h2>
<div class="desc">
基于面要素创建三维地形;线要素生成三维路径;空间分析结果着色
</div>
</div>
<!-- 控制面板 -->
<div class="controls">
<el-row :gutter="12">
<el-col :span="12">
<div class="control-card">
<div class="subtitle">
面要素坐标 (GeoJSON 坐标数组)
</div>
<el-input
v-model="polygonCoordsStr"
type="textarea"
:rows="6"
placeholder="示例: [[116.39,39.90],[116.41,39.90],[116.41,39.92],[116.39,39.92],[116.39,39.90]]"
/>
<div class="inline">
<el-input-number
v-model="terrainHeight"
:min="10"
:max="500"
:step="10"
size="small"
class="inline-item"
placeholder="地形高度"
/>
<el-select
v-model="colorMode"
size="small"
class="inline-item"
>
<el-option
label="按质心距离着色"
value="centroid"
/>
<el-option
label="按路径距离着色"
value="path"
/>
</el-select>
<el-button
type="primary"
size="small"
@click="buildTerrain"
class="inline-item"
>生成地形</el-button
>
</div>
</div>
</el-col>
<el-col :span="12">
<div class="control-card">
<div class="subtitle">
线要素坐标 (GeoJSON 坐标数组)
</div>
<el-input
v-model="lineCoordsStr"
type="textarea"
:rows="6"
placeholder="示例: [[116.395,39.905],[116.405,39.915],[116.410,39.920]]"
/>
<div class="inline">
<el-input-number
v-model="pathRadius"
:min="1"
:max="50"
:step="1"
size="small"
class="inline-item"
placeholder="路径半径"
/>
<el-button
type="success"
size="small"
@click="buildPath"
class="inline-item"
>生成路径</el-button
>
<el-button
type="warning"
size="small"
@click="resetScene"
class="inline-item"
>清空</el-button
>
</div>
</div>
</el-col>
</el-row>
</div>
<!-- Three.js渲染容器 -->
<div class="canvas-wrap">
<div ref="threeEl" class="three-view"></div>
</div>
</el-card>
</div>
</template>
<script setup>
/**
* 组件意图:以最小投影近似将经纬度转为平面坐标,快速在 Three.js 中进行
* 可视化挤出与路径建模;强调交互演示与空间距离着色效果,而非地理精确投影。
*/
import { ref, onMounted, onBeforeUnmount } from "vue";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import {
polygon as turfPolygon,
lineString as turfLineString,
centroid as turfCentroid,
distance as turfDistance,
} from "@turf/turf";
// --- 1. DOM引用与Three.js核心对象管理 ---
const threeEl = ref(null); // Three.js渲染容器
let scene = null; // 场景
let camera = null; // 相机
let renderer = null; // 渲染器
let controls = null; // 轨道控制器
let terrainMesh = null; // 地形网格
let pathMesh = null; // 路径网格
let grid = null; // 网格辅助线
let light = null; // 环境光
let dirLight = null; // 方向光
// --- 2. 输入参数与状态管理 ---
// 面要素坐标字符串(便于粘贴GeoJSON坐标)
const polygonCoordsStr = ref(
"[[116.39,39.90],[116.41,39.90],[116.41,39.92]," +
"[116.39,39.92],[116.39,39.90]]"
);
// 线要素坐标字符串
const lineCoordsStr = ref(
"[[116.395,39.905],[116.405,39.915],[116.410,39.920]]"
);
const terrainHeight = ref(120); // 地形高度
const pathRadius = ref(6); // 路径半径
const colorMode = ref("centroid"); // 着色模式:质心距离/路径距离
// 简易比例尺:经纬度转平面坐标(米级),牺牲精度换取演示简便
const SCALE = 100000;
// --- 3. 生命周期钩子 ---
onMounted(() => {
initThree(); // 初始化Three.js场景
});
onBeforeUnmount(() => {
disposeThree(); // 销毁Three.js资源,防止内存泄漏
});
/**
* 初始化 Three.js 场景与交互控制
* 统一在挂载时完成渲染器/相机/光照的创建,避免重复实例化带来的资源浪费
* @throws {Error} 当容器尺寸不可用或 WebGL 初始化失败时可能抛错
*/
function initThree() {
// 1. 创建场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0xf7f7fb); // 浅蓝背景
// 2. 创建相机
const w = threeEl.value.clientWidth || 800;
const h = threeEl.value.clientHeight || 600;
camera = new THREE.PerspectiveCamera(60, w / h, 0.1, 50000);
camera.position.set(0, -300, 300); // 初始视角
// 3. 创建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true }); // 抗锯齿
renderer.setSize(w, h);
renderer.setPixelRatio(window.devicePixelRatio || 1); // 适配高清屏
renderer.outputColorSpace = THREE.SRGBColorSpace; // 正确的颜色空间
threeEl.value.appendChild(renderer.domElement);
// 4. 创建轨道控制器(交互控制)
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // 阻尼效果(平滑交互)
controls.dampingFactor = 0.1;
controls.target.set(0, 0, 50); // 控制器目标点
// 5. 添加网格辅助线(便于观察空间位置)
grid = new THREE.GridHelper(1200, 24, 0x999999, 0xdddddd);
grid.position.set(0, 0, 0);
scene.add(grid);
// 6. 添加光照(增强3D效果)
light = new THREE.AmbientLight(0xffffff, 0.6); // 环境光
scene.add(light);
dirLight = new THREE.DirectionalLight(0xffffff, 0.8); // 方向光
dirLight.position.set(300, -300, 400);
scene.add(dirLight);
// 7. 启动渲染循环
animate();
// 8. 监听窗口大小变化(自适应)
window.addEventListener("resize", handleResize);
}
/**
* 释放 Three.js 相关资源
* 在卸载阶段主动清理几何与材质,避免 WebGL 纹理与缓冲区泄漏
*/
function disposeThree() {
window.removeEventListener("resize", handleResize);
// 清理地形和路径网格
if (terrainMesh) clearMesh(terrainMesh);
if (pathMesh) clearMesh(pathMesh);
// 移除辅助网格
if (grid) scene.remove(grid);
// 销毁控制器
if (controls) controls.dispose();
// 销毁渲染器
if (renderer) {
renderer.dispose();
if (renderer.domElement && renderer.domElement.parentNode) {
renderer.domElement.parentNode.removeChild(renderer.domElement);
}
}
// 清空引用(便于GC回收)
scene = null;
camera = null;
renderer = null;
controls = null;
}
/**
* 视口变化时同步相机与渲染器尺寸
* 保持纵横比一致,避免画面被拉伸导致空间感失真
*/
function handleResize() {
if (!renderer || !camera || !threeEl.value) return;
const w = threeEl.value.clientWidth;
const h = threeEl.value.clientHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix(); // 更新相机投影矩阵
renderer.setSize(w, h);
}
/**
* 主渲染循环
* 启用阻尼控制需要在每帧更新,保证交互平滑
*/
function animate() {
if (!renderer) return;
requestAnimationFrame(animate); // 递归调用(60fps)
controls && controls.update(); // 更新控制器
renderer.render(scene, camera); // 渲染场景
}
/**
* 清空场景中的地形与路径
* 确保重复构建时不产生叠加与内存占用
*/
function resetScene() {
if (terrainMesh) {
clearMesh(terrainMesh);
terrainMesh = null;
}
if (pathMesh) {
clearMesh(pathMesh);
pathMesh = null;
}
}
/**
* 从场景移除并释放 Mesh 的几何与材质
* Three 中的 GPU 资源需要显式 dispose,否则会逐帧累积
* @param {THREE.Mesh} mesh - 目标网格
*/
function clearMesh(mesh) {
scene.remove(mesh);
mesh.geometry.dispose(); // 释放几何体
// 释放材质(支持数组材质)
if (Array.isArray(mesh.material)) {
mesh.material.forEach((m) => m.dispose());
} else {
mesh.material.dispose();
}
}
/**
* 解析坐标字符串为数组
* 允许用户以文本形式快速粘贴 GeoJSON 坐标
* @param {string} str - 形如 [[lon,lat],...] 的字符串
* @returns {Array<[number,number]>|null} 坐标数组或 null
*/
function parseCoords(str) {
try {
const arr = JSON.parse(str);
if (!Array.isArray(arr)) return null;
return arr;
} catch (e) {
console.error("坐标解析失败:", e);
return null;
}
}
/**
* 经纬度转近似平面坐标(以首次点为本地原点)
* 避免引入复杂投影,仅做可视化演示的相对坐标
* @param {number} lon - 经度
* @param {number} lat - 纬度
* @param {[number,number]} origin - 原点经纬度
* @returns {THREE.Vector2} 平面坐标
*/
function lonlatToXY(lon, lat, origin) {
return new THREE.Vector2(
(lon - origin[0]) * SCALE, // 经度转X坐标
(lat - origin[1]) * SCALE // 纬度转Y坐标
);
}
/**
* 将平面坐标还原为经纬度(相对原点)
* 在着色计算中需回到地理空间以使用 Turf 的距离函数
* @param {{x:number,y:number}} v - 平面坐标
* @param {[number,number]} origin - 原点经纬度
* @returns {[number,number]} 经纬度
*/
function vecToLonLat(v, origin) {
return [v.x / SCALE + origin[0], v.y / SCALE + origin[1]];
}
/**
* 生成棋盘格纹理
* 侧面使用重复纹理以增强形体可读性,避免纯色过于单调
* @param {number} [size=64] - 纹理尺寸
* @param {number[]} [colors=[0x8ecae6,0x219ebc]] - 两种颜色
* @returns {THREE.Texture} 纹理
*/
function genCheckerTexture(size = 64, colors = [0x8ecae6, 0x219ebc]) {
const c = document.createElement("canvas");
c.width = size;
c.height = size;
const ctx = c.getContext("2d");
const s = size / 8; // 每个格子的大小
// 绘制8x8棋盘格
for (let y = 0; y < 8; y++) {
for (let x = 0; x < 8; x++) {
ctx.fillStyle =
"#" + (x % 2 === y % 2 ? colors[0] : colors[1]).toString(16);
ctx.fillRect(x * s, y * s, s, s);
}
}
// 创建Three.js纹理
const tex = new THREE.CanvasTexture(c);
tex.wrapS = THREE.RepeatWrapping; // 水平重复
tex.wrapT = THREE.RepeatWrapping; // 垂直重复
tex.repeat.set(4, 4); // 重复次数
tex.colorSpace = THREE.SRGBColorSpace;
return tex;
}
/**
* 基于面要素构建可挤出的三维地形
* 用挤出顶面并着色演示空间分析结果在 3D 中的映射
*/
function buildTerrain() {
// 1. 解析坐标字符串
const coords = parseCoords(polygonCoordsStr.value);
if (!coords || coords.length < 4) {
console.warn("无效的面要素坐标,至少需要4个点");
return;
}
const origin = coords[0]; // 以第一个点为原点
// 2. 经纬度转平面坐标,创建Three.js形状
const shapePts = coords.map((p) => lonlatToXY(p[0], p[1], origin));
const shape = new THREE.Shape(
shapePts.map((v) => new THREE.Vector2(v.x, v.y))
);
// 3. 挤出几何体(创建三维地形)
const extrude = new THREE.ExtrudeGeometry(shape, {
depth: terrainHeight.value, // 挤出高度
bevelEnabled: false, // 禁用倒角(简化模型)
steps: 1, // 步数(1步即可)
});
// 4. 添加UV属性(纹理映射)
const uvAttr = new THREE.BufferAttribute(
new Float32Array(extrude.attributes.position.count * 2),
2
);
extrude.setAttribute("uv", uvAttr);
// 5. 应用顶点着色(按空间距离)
applyVertexColors(extrude, coords, origin);
// 6. 创建材质(顶面顶点着色,侧面纹理)
const tex = genCheckerTexture();
const matTop = new THREE.MeshStandardMaterial({
vertexColors: true, // 启用顶点颜色
});
const matSide = new THREE.MeshStandardMaterial({
map: tex, // 侧面纹理
});
// 7. 创建网格并设置阴影
const mesh = new THREE.Mesh(extrude, [matSide, matTop]);
mesh.castShadow = true; // 投射阴影
mesh.receiveShadow = true; // 接收阴影
// 8. 居中放置(提升交互体验)
const bbox = new THREE.Box3().setFromObject(mesh);
const center = bbox.getCenter(new THREE.Vector3());
mesh.position.z = 0;
mesh.position.x -= center.x;
mesh.position.y -= center.y;
// 9. 替换旧地形
if (terrainMesh) clearMesh(terrainMesh);
terrainMesh = mesh;
scene.add(terrainMesh);
}
/**
* 基于线要素生成三维管道路径
* 用 Catmull-Rom 曲线平滑路径,便于观察着色模式的路径距离效果
*/
function buildPath() {
// 1. 解析坐标字符串
const coords = parseCoords(lineCoordsStr.value);
if (!coords || coords.length < 2) {
console.warn("无效的线要素坐标,至少需要2个点");
return;
}
// 2. 确定原点(优先用地形原点,否则用线第一个点)
let origin = coords[0];
if (!origin && terrainMesh) {
origin = vecToLonLat(terrainMesh.position, [0, 0]);
}
// 3. 经纬度转三维坐标(Z轴为地形高度的60%)
const pts = coords.map(
(p) =>
new THREE.Vector3(
(p[0] - origin[0]) * SCALE,
(p[1] - origin[1]) * SCALE,
terrainHeight.value * 0.6 // 路径高度
)
);
// 4. 创建平滑曲线(Catmull-Rom)
const curve = new THREE.CatmullRomCurve3(pts, false, "centripetal", 0.5);
// 5. 创建管状几何体
const tube = new THREE.TubeGeometry(
curve, // 曲线
200, // 分段数(平滑度)
pathRadius.value, // 半径
16, // 圆周分段数
false // 不闭合
);
// 6. 创建材质和网格
const mat = new THREE.MeshStandardMaterial({ color: 0xff4d6d }); // 粉红色
const mesh = new THREE.Mesh(tube, mat);
// 7. 居中对齐
const bbox = new THREE.Box3().setFromObject(mesh);
const center = bbox.getCenter(new THREE.Vector3());
mesh.position.x -= center.x;
mesh.position.y -= center.y;
// 8. 替换旧路径
if (pathMesh) clearMesh(pathMesh);
pathMesh = mesh;
scene.add(pathMesh);
}
/**
* 计算并写入顶面顶点颜色
* 在顶面按空间距离归一化映射到渐变,以直观展示分析结果
* @param {THREE.BufferGeometry} geometry - 挤出几何
* @param {Array<[number,number]>} polyCoords - 面要素经纬度坐标
* @param {[number,number]} origin - 原点经纬度
*/
function applyVertexColors(geometry, polyCoords, origin) {
const positions = geometry.attributes.position;
const count = positions.count;
const colors = new Float32Array(count * 3); // 每个顶点3个颜色分量(RGB)
// 1. 创建Turf面要素,计算质心
const poly = turfPolygon([polyCoords]);
const c = turfCentroid(poly); // 面要素质心
// 2. 解析线要素(用于路径距离计算)
let line = null;
const lineCoords = parseCoords(lineCoordsStr.value);
if (lineCoords && lineCoords.length >= 2) {
line = turfLineString(lineCoords);
}
// 3. 计算每个顶点的距离值,并找到最大/最小值(用于归一化)
let min = Infinity;
let max = -Infinity;
const values = new Array(count);
for (let i = 0; i < count; i++) {
const vx = positions.getX(i);
const vy = positions.getY(i);
const vz = positions.getZ(i);
// 只处理顶面顶点(侧面不参与着色)
if (Math.abs(vz - terrainHeight.value) > 1e-3) {
values[i] = 0;
continue;
}
// 平面坐标转回经纬度
const lonlat = vecToLonLat({ x: vx, y: vy }, origin);
let d = 0;
// 按着色模式计算距离
if (colorMode.value === "path" && line) {
// 按到路径的最近距离计算
let md = Infinity;
for (let k = 0; k < line.geometry.coordinates.length; k++) {
const lc = line.geometry.coordinates[k];
const dd = turfDistance(lonlat, lc);
if (dd < md) md = dd;
}
d = md;
} else {
// 按到质心的距离计算
d = turfDistance(lonlat, c.geometry.coordinates);
}
values[i] = d;
// 更新最大/最小值
if (d < min) min = d;
if (d > max) max = d;
}
// 4. 归一化距离值,并映射到颜色
const norm = (v) => (max - min < 1e-9 ? 0 : (v - min) / (max - min));
for (let i = 0; i < count; i++) {
const t = norm(values[i]);
const col = lerpColor(t); // 插值颜色
// 写入颜色数组
colors[i * 3] = col.r;
colors[i * 3 + 1] = col.g;
colors[i * 3 + 2] = col.b;
}
// 5. 将颜色属性添加到几何体
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
}
/**
* 距离到颜色的分段线性插值
* 使用三色渐变提升中间值的可感知性,比单一两端渐变更清晰
* @param {number} t - [0,1] 的归一化值
* @returns {THREE.Color} 颜色
*/
function lerpColor(t) {
const c1 = new THREE.Color(0x2a9d8f); // 青绿色(近)
const c2 = new THREE.Color(0xf4a261); // 橙黄色(中)
const c3 = new THREE.Color(0xe76f51); // 砖红色(远)
if (t < 0.5) {
const k = t / 0.5;
return new THREE.Color().lerpColors(c1, c2, k);
}
const k = (t - 0.5) / 0.5;
return new THREE.Color().lerpColors(c2, c3, k);
}
</script>
<style scoped lang="less">
.turf-three-modeler {
padding: 12px;
.header {
margin-bottom: 8px;
h2 {
margin: 0;
font-size: 18px;
color: #303133;
}
.desc {
color: #666;
font-size: 12px;
margin-top: 4px;
}
}
.controls {
margin-top: 8px;
.control-card {
border: 1px solid #eee;
border-radius: 6px;
padding: 10px;
.subtitle {
font-weight: 600;
margin-bottom: 6px;
color: #606266;
font-size: 14px;
}
.inline {
display: flex;
gap: 8px;
align-items: center;
margin-top: 8px;
flex-wrap: wrap;
.inline-item {
min-width: 120px;
}
}
}
}
.canvas-wrap {
margin-top: 12px;
border: 1px solid #ddd;
border-radius: 6px;
overflow: hidden;
.three-view {
width: 100%;
height: 560px;
}
}
}
</style>
2. 核心代码深度解析
(1)Turf.js 与 Three.js 的核心衔接逻辑
javascript
// 1. Turf.js处理地理数据
const poly = turfPolygon([polyCoords]); // 创建面要素
const c = turfCentroid(poly); // 计算质心
const d = turfDistance(lonlat, c.geometry.coordinates); // 计算距离
// 2. 距离值归一化(0~1)
const norm = (v) => (v - min) / (max - min);
// 3. Three.js顶点着色
const col = lerpColor(norm(d)); // 距离映射到颜色
colors[i * 3] = col.r; // 红色分量
colors[i * 3 + 1] = col.g; // 绿色分量
colors[i * 3 + 2] = col.b; // 蓝色分量
// 4. 应用到几何体
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
- Turf.js 职责:地理坐标处理、空间分析(质心、距离计算);
- Three.js 职责:三维几何体创建、顶点颜色映射、可视化渲染;
- 核心转换:将 Turf.js 计算的地理距离值,归一化后映射为 Three.js 顶点颜色,实现 "分析结果可视化"。
(2)经纬度到平面坐标的转换
为简化演示,采用简易投影(非精确地理投影):
javascript
const SCALE = 100000; // 比例尺
function lonlatToXY(lon, lat, origin) {
return new THREE.Vector2(
(lon - origin[0]) * SCALE, // 经度差 × 比例尺 = X坐标
(lat - origin[1]) * SCALE // 纬度差 × 比例尺 = Y坐标
);
}
- 优点:实现简单,无需引入复杂投影库;
- 适用场景:小范围地理数据可视化(如城市级);
- 注意:大范围数据会有明显变形,生产环境需使用专业投影库(如 proj4js)。
(3)三维地形生成核心
使用 Three.js ExtrudeGeometry(挤出几何体)将二维面要素转为三维地形:
javascript
const extrude = new THREE.ExtrudeGeometry(shape, {
depth: terrainHeight.value, // 挤出高度
bevelEnabled: false, // 禁用倒角
steps: 1 // 步数
});
- 双面材质设计 :
- 顶面:顶点着色(展示空间分析结果);
- 侧面:棋盘格纹理(增强 3D 立体感)。
(4)三维路径生成核心
使用CatmullRomCurve3平滑线要素,再用TubeGeometry生成管状路径
javascript
// 创建平滑曲线
const curve = new THREE.CatmullRomCurve3(pts, false, "centripetal", 0.5);
// 创建管状几何体
const tube = new THREE.TubeGeometry(curve, 200, pathRadius.value, 16, false);
200:曲线分段数(值越大越平滑);16:圆周分段数(值越大越接近圆形);- 路径高度设置为地形高度的 60%,避免与地形重叠。
(5)顶点着色核心逻辑
顶点着色是展示空间分析结果的核心,实现步骤:
- 筛选顶面顶点:仅处理 z 坐标等于地形高度的顶点;
- 计算距离值:按着色模式(质心 / 路径)计算每个顶点的距离;
- 归一化:将距离值映射到 0~1 范围;
- 颜色插值:将归一化值映射到三色渐变(青→橙→红);
- 应用颜色 :将颜色数组添加到几何体的
color属性。
核心代码片段:
javascript
// 只处理顶面顶点
if (Math.abs(vz - terrainHeight.value) > 1e-3) continue;
// 计算距离
let d = 0;
if (colorMode.value === "path" && line) {
// 到路径的最近距离
let md = Infinity;
for (let k = 0; k < line.geometry.coordinates.length; k++) {
const lc = line.geometry.coordinates[k];
const dd = turfDistance(lonlat, lc);
if (dd < md) md = dd;
}
d = md;
} else {
// 到质心的距离
d = turfDistance(lonlat, c.geometry.coordinates);
}
// 归一化并映射颜色
const t = norm(d);
const col = lerpColor(t);
colors[i * 3] = col.r;
colors[i * 3 + 1] = col.g;
colors[i * 3 + 2] = col.b;
(6)关键优化点
-
资源管理:
- 显式释放 GPU 资源:
geometry.dispose()、material.dispose(); - 组件卸载时清理所有 Three.js 对象,防止内存泄漏;
- 重复生成时先移除旧网格,避免叠加。
- 显式释放 GPU 资源:
-
交互体验:
- 启用轨道控制器阻尼:
enableDamping = true,实现平滑旋转 / 缩放; - 几何体居中对齐:通过包围盒计算中心,提升交互体验;
- 网格辅助线:帮助用户判断空间位置。
- 启用轨道控制器阻尼:
-
性能优化:
- 禁用几何体倒角:
bevelEnabled = false,减少顶点数量; - 合理的分段数:路径分段 200,圆周分段 16,平衡平滑度与性能;
- 仅顶面着色:侧面使用纹理,减少计算量。
- 禁用几何体倒角:
四、功能效果演示
1. 基础效果
- 启动项目后,页面显示 Three.js 三维场景(带网格辅助线);
- 点击 "生成地形":基于默认面坐标生成三维地形(高度 120),顶面按质心距离着色(近青、中远橙、远红);
- 点击 "生成路径":基于默认线坐标生成平滑三维路径(半径 6),悬浮在地形上方;
- 切换着色模式为 "按路径距离":地形顶面颜色按到路径的最近距离重新着色;
- 调整地形高度 / 路径半径:重新生成后参数生效;
- 交互控制:鼠标拖拽旋转场景,滚轮缩放,右键平移。
2. 交互效果
| 操作 | 效果 |
|---|---|
| 调整地形高度输入框 | 重新生成的地形高度变化 |
| 切换着色模式 | 地形顶面颜色映射规则变化 |
| 调整路径半径 | 路径粗细变化 |
| 粘贴自定义 GeoJSON 坐标 | 生成对应形状的地形 / 路径 |
| 点击 "清空" | 移除地形和路径,保留场景 |
3. 关键参数示例
- 地形高度:10~500(默认 120),值越大地形越高;
- 路径半径:1~50(默认 6),值越大路径越粗;
- 着色模式 :
- 质心距离:颜色从质心向外渐变(近青远红);
- 路径距离:颜色从路径向外渐变(近青远红)。

五、代码仓库地址
完整代码已上传至 Gitee,可直接克隆运行:https://gitee.com/YAY-404/turfjs-vue3-demo
六、专栏地址
本文已同步至 CSDN 专栏,可查看更多 Turf.js 实战内容:https://blog.csdn.net/m0_72065108/article/details/155226062
七、实战拓展方向
-
精确地理投影:
- 集成 proj4js,实现 WGS84 到 Web Mercator 的精确投影;
- 支持自定义投影坐标系,适配不同区域的地理数据。
-
高级三维效果:
- 地形纹理映射:加载卫星影像纹理,贴合地形顶面;
- 路径动态效果:添加路径动画(如流动光效);
- 阴影优化:启用实时光影,增强立体感;
- 材质自定义:支持用户选择地形 / 路径颜色。
-
更多空间分析可视化:
- 缓冲区着色:按缓冲区距离着色地形;
- 坡度分析:计算地形坡度并着色;
- 等高线生成:基于高度生成等高线;
- 空间插值:基于离散点生成三维曲面。
-
交互能力扩展:
- 点选功能:点击地形显示对应经纬度和距离值;
- 框选缩放:支持框选区域放大;
- 数据导入:支持上传 GeoJSON 文件生成地形 / 路径;
- 场景保存:支持导出当前场景为图片。
-
性能优化:
- 几何体简化:使用 Simplify.js 简化面要素顶点;
- 层次细节(LOD):根据相机距离调整模型精度;
- Web Worker:将 Turf.js 计算放入 Web Worker,避免阻塞主线程;
- 批处理渲染:合并多个几何体,减少绘制调用。
八、常见问题排查
-
Three.js 场景不显示:
- 原因:渲染容器未设置宽高,或初始化时机过早;
- 解决方案:确保
.three-view设置width: 100%; height: 560px,在onMounted中初始化。
-
坐标解析失败:
- 原因:坐标字符串格式错误(如缺少中括号、逗号);
- 解决方案:检查坐标字符串是否符合 JSON 格式,示例:
[[116.39,39.90],[116.41,39.90]]。
-
顶点着色无效果:
- 原因:未启用
vertexColors: true,或顶面顶点判断错误; - 解决方案:确保材质设置
vertexColors: true,检查Math.abs(vz - terrainHeight.value) > 1e-3的判断逻辑。
- 原因:未启用
-
性能卡顿:
- 原因:几何体分段数过高,或顶点数量过多;
- 解决方案:降低
TubeGeometry的分段数(如 200→100),简化面要素坐标。
-
内存泄漏:
- 原因:未调用
dispose()释放几何体 / 材质; - 解决方案:在
clearMesh函数中显式释放资源,组件卸载时调用disposeThree()。
- 原因:未调用
总结
本文通过 Turf.js + Three.js 的结合,实现了 "地理数据 → 空间分析 → 三维建模 → 可视化着色" 的完整流程:
- 掌握了 Three.js 核心功能(场景、相机、渲染器、几何体、材质、交互控制);
- 实现了 Turf.js 空间分析结果(质心、距离)到 Three.js 顶点颜色的映射;
- 完成了三大核心功能:面要素三维地形、线要素三维路径、空间距离着色;
- 梳理了 Three.js 资源管理、性能优化、交互体验的关键要点。