Vue 3 + Three.js 实战:5分钟搞定TIFF地形渲染(附完整代码)
最近在做一个智慧城市项目,需要把无人机采集的高程数据实时呈现在Web端。甲方要求既要能三维立体展示地形起伏,又要能快速加载,最好还能和现有的Vue 3技术栈无缝集成。我一开始尝试用传统GIS平台,发现要么太重,要么定制化太麻烦。后来把目光转向了Three.js,配合Vue 3的响应式特性,居然在5分钟内就搞定了TIFF地形的渲染,效果还出奇的好。
如果你也在做类似的地理信息可视化项目,特别是需要处理DEM(数字高程模型)数据,那么这篇文章就是为你准备的。我会从零开始,手把手带你搭建一个完整的Vue 3 + Three.js项目,实现TIFF文件的快速加载、解析和三维渲染。整个过程不需要复杂的配置,也不需要依赖庞大的GIS框架,用最轻量的方式解决实际问题。
1. 环境搭建与项目初始化
1.1 创建Vue 3 + TypeScript项目
我习惯用Vite作为构建工具,它的启动速度和热更新体验都相当不错。打开终端,执行以下命令:
npm create vue@latest vue3-threejs-tiff
创建过程中,选择TypeScript、Vue Router和Pinia(可选),ESLint和Prettier根据个人习惯选择。项目创建完成后,进入目录并安装基础依赖:
cd vue3-threejs-tiff
npm install
1.2 安装Three.js核心依赖
Three.js是WebGL的封装库,让我们能用更简单的方式操作3D图形。安装核心包和类型定义:
npm install three
npm install -D @types/three
提示:虽然Three.js本身是用JavaScript写的,但@types/three提供了完整的TypeScript类型支持,能极大提升开发体验,避免低级错误。
1.3 安装TIFF处理库
TIFF文件格式比较复杂,特别是地理空间TIFF(GeoTIFF)还包含了坐标系统、高程值等元数据。我们需要专门的库来解析:
npm install geotiff
npm install -D @types/geotiff
geotiff是目前最成熟的JavaScript TIFF解析库,支持多波段、压缩、坐标系等特性。对于DEM数据,它还能直接提取高程矩阵,非常方便。
1.4 项目结构设计
在开始编码前,先规划一下项目结构。我的习惯是这样的:
src/
├── components/
│ ├── TerrainViewer.vue # 主渲染组件
│ └── ControlPanel.vue # 控制面板组件
├── utils/
│ ├── tiffLoader.ts # TIFF文件加载器
│ └── terrainGenerator.ts # 地形生成器
├── assets/
│ └── dem/ # 存放TIFF文件
└── views/
└── HomeView.vue # 主页面
这种结构清晰明了,业务逻辑和工具函数分离,后期维护起来也方便。
2. TIFF文件解析与数据提取
2.1 理解DEM TIFF的数据结构
在开始写代码之前,有必要了解一下DEM TIFF文件里到底存了什么。DEM(Digital Elevation Model)本质上是一个二维矩阵,每个像素值代表该位置的海拔高度。比如一个1000×1000的TIFF文件,就有100万个高程点。
地理空间TIFF比普通图片TIFF多了几个关键信息:
- 地理变换参数(GeoTransform):定义了像素坐标如何映射到真实世界坐标
- 坐标系(CRS):比如WGS84、UTM等
- NoData值:表示无效数据的特殊值
// 一个典型的地理变换参数数组
const geoTransform = [
topLeftX, // 左上角X坐标
pixelWidth, // 像素宽度(经度方向)
0, // 旋转参数(通常为0)
topLeftY, // 左上角Y坐标
0, // 旋转参数(通常为0)
-pixelHeight // 像素高度(纬度方向,通常为负值)
];
2.2 实现TIFF加载器
创建src/utils/tiffLoader.ts文件,实现TIFF文件的加载和解析:
import { fromUrl, fromBlob, GeoTIFF } from 'geotiff';
export interface TerrainData {
width: number;
height: number;
elevation: Float32Array; // 高程数据
bounds: {
minX: number;
maxX: number;
minY: number;
maxY: number;
};
noDataValue: number | null;
}
export class TiffLoader {
/**
* 从URL加载TIFF文件
*/
static async loadFromUrl(url: string): Promise<TerrainData> {
try {
const tiff = await fromUrl(url);
const image = await tiff.getImage();
// 读取栅格数据
const raster = await image.readRasters();
const elevation = raster[0] as Float32Array;
// 获取地理信息
const bbox = image.getBoundingBox();
const geoKeys = image.getGeoKeys();
return {
width: image.getWidth(),
height: image.getHeight(),
elevation,
bounds: {
minX: bbox[0],
minY: bbox[1],
maxX: bbox[2],
maxY: bbox[3]
},
noDataValue: geoKeys.GDAL_NODATA || null
};
} catch (error) {
console.error('TIFF加载失败:', error);
throw error;
}
}
/**
* 从文件对象加载TIFF
*/
static async loadFromFile(file: File): Promise<TerrainData> {
const arrayBuffer = await file.arrayBuffer();
const tiff = await fromBlob(new Blob([arrayBuffer]));
return this.loadFromGeoTIFF(tiff);
}
/**
* 数据归一化处理
* 将高程值映射到0-1范围,便于Three.js渲染
*/
static normalizeElevation(data: TerrainData): {
normalized: Float32Array;
min: number;
max: number;
} {
let min = Infinity;
let max = -Infinity;
// 找出有效数据的最大最小值
for (let i = 0; i < data.elevation.length; i++) {
const value = data.elevation[i];
if (data.noDataValue !== null && value === data.noDataValue) {
continue;
}
if (value < min) min = value;
if (value > max) max = value;
}
// 归一化到0-1
const normalized = new Float32Array(data.elevation.length);
const range = max - min;
for (let i = 0; i < data.elevation.length; i++) {
const value = data.elevation[i];
if (data.noDataValue !== null && value === data.noDataValue) {
normalized[i] = 0;
} else {
normalized[i] = (value - min) / range;
}
}
return { normalized, min, max };
}
}
这个加载器做了几件重要的事情:
- 解析TIFF文件的栅格数据和地理信息
- 处理NoData值(无效数据点)
- 提供数据归一化方法,方便后续渲染
2.3 处理大型TIFF文件的技巧
实际项目中经常会遇到几百MB甚至GB级别的TIFF文件,直接全部加载到内存肯定不行。这里有几个优化技巧:
分块加载策略:
// 只加载感兴趣区域(ROI)
async function loadRegion(url: string, bbox: number[]) {
const tiff = await fromUrl(url);
const image = await tiff.getImage();
// 计算像素范围
const [minX, minY, maxX, maxY] = bbox;
const width = Math.ceil((maxX - minX) / image.getResolution()[0]);
const height = Math.ceil((maxY - minY) / image.getResolution()[1]);
// 读取指定区域
const window = [minX, minY, maxX, maxY];
const raster = await image.readRasters({ window });
return raster;
}
金字塔层级(LOD)处理: 对于超大型文件,可以预先生成多个分辨率版本,根据视图距离动态切换。虽然geotiff库本身支持多分辨率TIFF,但更常见的做法是在服务端预处理。
Web Worker异步处理: 把耗时的TIFF解析放到Web Worker中,避免阻塞主线程:
// 在主线程中
const worker = new Worker('./tiffWorker.js');
worker.postMessage({ type: 'load', url: 'terrain.tif' });
worker.onmessage = (event) => {
const terrainData = event.data;
// 更新Three.js场景
};
// 在Web Worker中
importScripts('https://unpkg.com/geotiff/dist-browser/geotiff.js');
// ... 处理TIFF解析
3. Three.js地形网格生成
3.1 从高程数据到三维网格
有了高程数据,下一步就是把它变成Three.js能渲染的几何体。最常用的方法是创建PlaneGeometry,然后根据高程值调整每个顶点的高度:
import * as THREE from 'three';
export class TerrainGenerator {
/**
* 生成地形几何体
*/
static createTerrainGeometry(
elevation: Float32Array,
width: number,
height: number,
options: {
maxHeight?: number; // 最大高度缩放
widthSegments?: number; // 宽度分段数
heightSegments?: number; // 高度分段数
} = {}
): THREE.BufferGeometry {
const {
maxHeight = 100,
widthSegments = width - 1,
heightSegments = height - 1
} = options;
// 创建平面几何体
const geometry =

1793

被折叠的 条评论
为什么被折叠?



