注:正常在WEB上显示三维地形首选Cesium,本文内容仅作为研究,展示文章用DEM制作通用三维地形模型中制作的局部三维地形模型
Cesium是可以很容易的实现在WEB端三维地形的,下面的图是分别是使用基于Cesium的Mars3D和超图的iClient3D出来的效果。不过Cesium终究是基于地球的,比较适合大块区域的展示。和文章用DEM制作通用三维地形模型里做的模型效果来比,还是差点意思,资源占用也很高。加上因为三维模型都是笛卡尔坐标系,我们在制作模型的时候也使用了高斯克吕格投影坐标系,直接整个模型加到球形的Cesium里再缩放到一个县的范围那么大,必然是不能处处对应准的,所以那篇文章的成果就不太适合用Cesium展示了。
理论上只要支持gltf的webgl库比如three.js等都是可以展示我再上篇文章中生成的地形模型的,我使用的babylon.js,我在之前文章蓝牙Beacon室内定位全栈里有用过,功能比较强大,可以说是一个WebGL的三维引擎也不为过。
babylon.js的使用比较方便,引入babylon.js和加载gltf的babylonjs.loaders.js,使用canvas进行渲染。
DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>三维地形title>
<link rel="stylesheet" media="all" href="./css/index.css">
<script src="./lib/babylon/babylon.js">script>
<script src="./lib/babylon/loaders/babylonjs.loaders.js">script>
head>
<body>
<canvas id="renderCanvas">canvas>
body>
<script src="./js/projection.js">script>
<script src="./js/index.js">script>
html>
首先初始化引擎和场景
let _canvas,_engine,_scene,_camera;
_canvas = document.getElementById('renderCanvas');
_engine = new BABYLON.Engine(_canvas, true);
_scene = createScene();
_engine.runRenderLoop(function() {
if (_scene.activeCamera) {
_scene.render();
}
});
//创建场景
const createScene = function() {
const scene = new BABYLON.Scene(_engine);
_camera = new BABYLON.ArcRotateCamera('camera', Math.PI/2, Math.PI/4, 30, new BABYLON.Vector3(0, 0, 0));
_camera.inputs.attached.mousewheel.wheelPrecision = 8;
_camera.inputs.attached.pointers.panningSensibility = 250;
_camera.inputs.attached.pointers.angularSensibilityX = 5000;
_camera.inputs.attached.pointers.angularSensibilityY = 5000;
_camera.attachControl(_canvas, true);
const light = new BABYLON.HemisphericLight('light', new BABYLON.Vector3(0, 1, 0));
return scene;
}
在创建引擎和场景的时候,为了方便调试,可以在场景中把坐标轴展示出来,很可惜babylon.js不原生支持坐标轴展示,需要手动画上去。
//创建坐标轴
const showAxis = function (size) {
const makeTextPlane = function (text, color, size) {
const dynamicTexture = new BABYLON.DynamicTexture('DynamicTexture', 50, _scene, true);
dynamicTexture.hasAlpha = true;
dynamicTexture.drawText(text, 5, 40, 'bold 36px Arial', color, 'transparent', true);
const plane = new BABYLON.Mesh.CreatePlane('TextPlane', size, _scene, true);
plane.material = new BABYLON.StandardMaterial('TextPlaneMaterial', _scene);
plane.material.backFaceCulling = false;
plane.material.specularColor = new BABYLON.Color3(0, 0, 0);
plane.material.diffuseTexture = dynamicTexture;
return plane;
};
const axisX = BABYLON.Mesh.CreateLines('axisX', [
new BABYLON.Vector3.Zero(), new BABYLON.Vector3(size, 0, 0), new BABYLON.Vector3(size * 0.95, 0.05 * size, 0),
new BABYLON.Vector3(size, 0, 0), new BABYLON.Vector3(size * 0.95, -0.05 * size, 0)
], _scene);
axisX.color = new BABYLON.Color3(1, 0, 0);
const xChar = makeTextPlane('X', 'red', size / 10);
xChar.position = new BABYLON.Vector3(0.9 * size, -0.05 * size, 0);
const axisY = BABYLON.Mesh.CreateLines('axisY', [
new BABYLON.Vector3.Zero(), new BABYLON.Vector3(0, size, 0), new BABYLON.Vector3(-0.05 * size, size * 0.95, 0),
new BABYLON.Vector3(0, size, 0), new BABYLON.Vector3(0.05 * size, size * 0.95, 0)
], _scene);
axisY.color = new BABYLON.Color3(0, 1, 0);
const yChar = makeTextPlane('Y', 'green', size / 10);
yChar.position = new BABYLON.Vector3(0, 0.9 * size, -0.05 * size);
const axisZ = BABYLON.Mesh.CreateLines('axisZ', [
new BABYLON.Vector3.Zero(), new BABYLON.Vector3(0, 0, size), new BABYLON.Vector3(0, -0.05 * size, size * 0.95),
new BABYLON.Vector3(0, 0, size), new BABYLON.Vector3(0, 0.05 * size, size * 0.95)
], _scene);
axisZ.color = new BABYLON.Color3(0, 0, 1);
const zChar = makeTextPlane('Z', 'blue', size / 10);
zChar.position = new BABYLON.Vector3(0, 0.05 * size, 0.9 * size);
};
加载DEM glb文件
//加载DEM
BABYLON.SceneLoader.LoadAssetContainer('./','asset/dem.glb' , _scene, function (container) {
container.meshes[0].id = '__dem__';
container.meshes[0].name = '__dem__';
container.addAllToScene();
});
毕竟这是一个地理数据,难免需要加一些其他地理数据,因此需要实现坐标转换。这里需要再次说明,之前所有的数据制作流程都是使用的高斯克吕格投影投影,为什么要是用高斯克吕格投影请看我的文章三维GIS建模不要用墨卡托投影,高斯克吕格投影投影和经纬度之间的坐标转换请看我的文章蓝牙Beacon室内定位全栈的移动端展示模型
部分。单就我这个DEMO来说,还有一点点不同,我使用的坐标系是CGCS2000_3_Degree_GK_Zone_37而不是CGCS2000_3_Degree_GK_CM_111E(两者有啥区别以及为什么会有两种以后有机会再说),加上我们建模的时候不是直接按图像的尺寸来的,有一个缩放,因此在经纬度转成高斯克吕格投影坐标系之后,还需要做一个转换到当前的三维空间坐标,代码如下:
//经纬度转场景坐标
const LngLat2XY = function(lng,lat)
{
const prjPosition = _projection.LngLat2XY(lng,lat);
const dx = (prjPosition[0] + 37500000) - _center[0];
const dy = prjPosition[1] - _center[1];
const x = dx/_cellSize/100;
const y = dy/_cellSize/100;
return [-x,-y]
}
其中_center
和_cellSize
从DEM影像数据的属性信息来。
然后我们就可以按位置加入一些模型了,比如
//加载模型
BABYLON.SceneLoader.LoadAssetContainer('./','asset/camera.glb' , _scene, function (container) {
container.meshes[0].id = 'camera_1';
container.meshes[0].name = 'camera_1';
container.meshes[0].scaling = new BABYLON.Vector3(15, 15, 15);
const xy = LngLat2XY(111.288,30.485)
container.meshes[0].position = new BABYLON.Vector3(xy[0], 2, xy[1]);
container.addAllToScene();
const points = [
new BABYLON.Vector3(xy[0], -0.5, xy[1]),
new BABYLON.Vector3(xy[0], 2, xy[1])
];
//位置点虚线
const line = BABYLON.MeshBuilder.CreateDashedLines("camera_line", {
points: points,
dashSize: 50,
gapSize: 25,
dashNb: 10});
line.color = new BABYLON.Color3(0, 0, 1);
});
还可以在上面加Geojson数据,但是我没找到怎么让线贴着地形走的方式。
const request = new XMLHttpRequest();
request.open('get','./asset/XZQ.geojson');
request.send();
request.onload = ()=>{
if (request.status == 200) {
const features = JSON.parse(request.responseText).features
features.forEach(feature => {
const coords = feature.geometry.coordinates[0][0]
const positons = []
for (let index = 0; index < coords.length; index++) {
const coord = coords[index];
const xy = LngLat2XY(coord[0],coord[1])
positons.push(new BABYLON.Vector3(xy[0], 0.5, xy[1]))
}
const xzqLine = BABYLON.MeshBuilder.CreateLines(`xzqLine_${feature.properties.XZQDM}`, {points: positons});
xzqLine.color = new BABYLON.Color3(1, 1, 1);
});
}
}
我想,既然画上去的线没法贴地,那我可不可以给模型贴材质贴图呢。于是,我把要在上面展示的数据转换坐标系到CGCS2000_3_Degree_GK_Zone_37,通过GeoServer发布成WMS服务,将通过WMS请求回来的图片当做材质贴图贴在模型上。代码如下:
const url = `${_geoserverUrl}/map/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&FORMAT=image/png&TRANSPARENT=true&STYLES&LAYERS=map:xzq&SRS=EPSG:4525&WIDTH=${_imgSize[0]}&HEIGHT=${_imgSize[1]}&BBOX=${xmin},${ymin},${xmax},${ymax}`
let orgMat = container.meshes[1].material;
orgMat.bumpTexture = new BABYLON.Texture(url, _scene);
orgMat.bumpTexture.vScale = -1
这里需要注意,请求里的WIDTH和HEIGHT要和建模时使用的图片成比例,我是直接使用的原尺寸。请求里面的BBOX的最大值最小值一定要是用DEM影像属性里的范围,不能错。SRS要是用对应的高斯克吕格坐标系,也不能错。
最终效果就如图了。