Turfjs+Three.js:地理数据的三维建模应用

在 WebGIS 开发中,三维可视化不再局限于地球 / 地图场景 ------ 纯三维建模(Three.js)能更灵活地展示地理数据的空间形态与分析结果。Turf.js 作为前端空间分析核心库,可完成地理坐标处理、距离计算、质心分析等基础操作;Three.js 则专注于三维几何体创建、材质渲染、交互控制,两者结合可实现 "地理数据 → 空间分析 → 三维建模 → 可视化着色" 的完整流程。本文将通过实战案例,带你掌握 Turf.js 与 Three.js 的结合使用方法,实现面要素三维地形建模、线要素三维路径生成、空间分析结果着色三大核心功能。

一、技术栈说明

  • 框架:Vue3(Composition API + <script setup>
  • 空间分析:Turf.js(@turf/turf v7+,核心 API:polygonlineStringcentroiddistance
  • 三维建模: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)顶点着色核心逻辑

顶点着色是展示空间分析结果的核心,实现步骤:

  1. 筛选顶面顶点:仅处理 z 坐标等于地形高度的顶点;
  2. 计算距离值:按着色模式(质心 / 路径)计算每个顶点的距离;
  3. 归一化:将距离值映射到 0~1 范围;
  4. 颜色插值:将归一化值映射到三色渐变(青→橙→红);
  5. 应用颜色 :将颜色数组添加到几何体的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)关键优化点
  1. 资源管理

    • 显式释放 GPU 资源:geometry.dispose()material.dispose()
    • 组件卸载时清理所有 Three.js 对象,防止内存泄漏;
    • 重复生成时先移除旧网格,避免叠加。
  2. 交互体验

    • 启用轨道控制器阻尼:enableDamping = true,实现平滑旋转 / 缩放;
    • 几何体居中对齐:通过包围盒计算中心,提升交互体验;
    • 网格辅助线:帮助用户判断空间位置。
  3. 性能优化

    • 禁用几何体倒角:bevelEnabled = false,减少顶点数量;
    • 合理的分段数:路径分段 200,圆周分段 16,平衡平滑度与性能;
    • 仅顶面着色:侧面使用纹理,减少计算量。

四、功能效果演示

1. 基础效果

  1. 启动项目后,页面显示 Three.js 三维场景(带网格辅助线);
  2. 点击 "生成地形":基于默认面坐标生成三维地形(高度 120),顶面按质心距离着色(近青、中远橙、远红);
  3. 点击 "生成路径":基于默认线坐标生成平滑三维路径(半径 6),悬浮在地形上方;
  4. 切换着色模式为 "按路径距离":地形顶面颜色按到路径的最近距离重新着色;
  5. 调整地形高度 / 路径半径:重新生成后参数生效;
  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

七、实战拓展方向

  1. 精确地理投影

    • 集成 proj4js,实现 WGS84 到 Web Mercator 的精确投影;
    • 支持自定义投影坐标系,适配不同区域的地理数据。
  2. 高级三维效果

    • 地形纹理映射:加载卫星影像纹理,贴合地形顶面;
    • 路径动态效果:添加路径动画(如流动光效);
    • 阴影优化:启用实时光影,增强立体感;
    • 材质自定义:支持用户选择地形 / 路径颜色。
  3. 更多空间分析可视化

    • 缓冲区着色:按缓冲区距离着色地形;
    • 坡度分析:计算地形坡度并着色;
    • 等高线生成:基于高度生成等高线;
    • 空间插值:基于离散点生成三维曲面。
  4. 交互能力扩展

    • 点选功能:点击地形显示对应经纬度和距离值;
    • 框选缩放:支持框选区域放大;
    • 数据导入:支持上传 GeoJSON 文件生成地形 / 路径;
    • 场景保存:支持导出当前场景为图片。
  5. 性能优化

    • 几何体简化:使用 Simplify.js 简化面要素顶点;
    • 层次细节(LOD):根据相机距离调整模型精度;
    • Web Worker:将 Turf.js 计算放入 Web Worker,避免阻塞主线程;
    • 批处理渲染:合并多个几何体,减少绘制调用。

八、常见问题排查

  1. Three.js 场景不显示

    • 原因:渲染容器未设置宽高,或初始化时机过早;
    • 解决方案:确保.three-view设置width: 100%; height: 560px,在onMounted中初始化。
  2. 坐标解析失败

    • 原因:坐标字符串格式错误(如缺少中括号、逗号);
    • 解决方案:检查坐标字符串是否符合 JSON 格式,示例:[[116.39,39.90],[116.41,39.90]]
  3. 顶点着色无效果

    • 原因:未启用vertexColors: true,或顶面顶点判断错误;
    • 解决方案:确保材质设置vertexColors: true,检查Math.abs(vz - terrainHeight.value) > 1e-3的判断逻辑。
  4. 性能卡顿

    • 原因:几何体分段数过高,或顶点数量过多;
    • 解决方案:降低TubeGeometry的分段数(如 200→100),简化面要素坐标。
  5. 内存泄漏

    • 原因:未调用dispose()释放几何体 / 材质;
    • 解决方案:在clearMesh函数中显式释放资源,组件卸载时调用disposeThree()

总结

本文通过 Turf.js + Three.js 的结合,实现了 "地理数据 → 空间分析 → 三维建模 → 可视化着色" 的完整流程:

  1. 掌握了 Three.js 核心功能(场景、相机、渲染器、几何体、材质、交互控制);
  2. 实现了 Turf.js 空间分析结果(质心、距离)到 Three.js 顶点颜色的映射;
  3. 完成了三大核心功能:面要素三维地形、线要素三维路径、空间距离着色;
  4. 梳理了 Three.js 资源管理、性能优化、交互体验的关键要点。
相关推荐
WeiXiao_Hyy23 分钟前
成为 Top 1% 的工程师
java·开发语言·javascript·经验分享·后端
吃杠碰小鸡40 分钟前
高中数学-数列-导数证明
前端·数学·算法
kingwebo'sZone1 小时前
C#使用Aspose.Words把 word转成图片
前端·c#·word
xjt_09011 小时前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农1 小时前
Vue 2.3
前端·javascript·vue.js
夜郎king2 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳2 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵3 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星3 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_3 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js