畢業兩年,一直在地圖相關的公司工作,雖然不是 GIS 出身,但是也對地圖有些耳濡目染;最近在看 WebGl 的東西,就拿 MapboxGL 做了一個關於 WebGL 的三維資料渲染的 DEMO 練手。
首先大致看了一下 MapboxGL 的 GLGS 到圖層的一個結構:
大體就是先做 WebGl 的 Shader 程式碼放進 Painter(WebGL 的 Context 就在這個物件裡面) 裡面,然後通過 Source 層去載入處理需要的資料(包括向量和柵格資料),把資料通過 Tile 物件傳進 Render 裡面,去做一些 WebGL 的資料處理和渲染,然後扔進 Tile 裡面傳入到 Layer 層,最後就是一些樣式和事件的管理。MapboxGL 大體就說這麼多,下面就是 WebGL 的三維資料處理和渲染以及新增衛星影像紋理的過程(程式碼實在太多,只寫出部分關鍵步驟程式碼):
第一步:拿到需要渲染的資料片(瓦片形式)
// 序列化瓦片地址,將資料瓦片的 xyz 座標計算出來
let url = normalizeURL(
tile.coord.url(this.tiles, null, this.scheme),
this.url,
this.tileSize
);
...
// 用 MapboxGl 封裝的獲取二進位制資料格式的 Ajax 請求拿到二進位制資料
tile.request = ajax.getArrayBuffer(url, done.bind(this));
...
// 將資料進行轉碼處理成 JS 物件,並傳遞給 tile
tile.pixelObj = pixelObj; // 處理好的資料
...
複製程式碼
第二步:在 Render 裡面拿到資料和 Painter,去做資料片的渲染:
const divisions = 257;
let vertexPositionData = new Float32Array(divisions * divisions * 3);
const pixels = pixelObj.pixels[0];
if (coord.vertexPositionData) {
// 做了快取優化
console.log('快取', 'coord');
vertexPositionData = coord.vertexPositionData;
} else {
console.time('vertex');
// 全資料量
for (let i = 0; i < divisions; ++i) {
for (let j = 0; j < divisions; ++j) {
const bufferLength = (i * divisions + j) * 3;
let dem = parseInt(pixels[bufferLength / 3]);
if (!dem || dem === -3) {
// 對於無效資料給一個預設值(PS: DEM 高程資料質量不高 )
dem = -1000;
}
vertexPositionData[bufferLength] = j * SCALE;
vertexPositionData[bufferLength + 1] = i * SCALE * 1;
vertexPositionData[bufferLength + 2] = dem;
}
}
// 計算資料處理的耗時,優化的時候要用
console.timeEnd('vertex');
coord.vertexPositionData = vertexPositionData;
}
const indexData = getIndex(divisions);
const FSIZE = vertexPositionData.BYTES_PER_ELEMENT;
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertexPositionData, gl.STATIC_DRAW);
const aPosiLoc = gl.getAttribLocation(gl.program, "a_Position");
gl.vertexAttribPointer(aPosiLoc, 3, gl.FLOAT, false, FSIZE * 3, 0);
gl.enableVertexAttribArray(aPosiLoc);
// 設定索引
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexData, gl.STATIC_DRAW);
// https://stackoverflow.com/questions/28324162/webgl-element-array-buffers-not-working
gl.getExtension('OES_element_index_uint');
gl.drawElements(gl.TRIANGLES, indexData.length, gl.UNSIGNED_INT, 0);
...
// 生成索引,WebGL 的渲染有兩種方式,一種是 drawElements,一種是 drawArray,我們這裡採用第一種
function getIndex(divisions) {
if (drawLerc3D.indexData) {
return drawLerc3D.indexData;
}
console.time('獲取索引');
const indexData = [];
// 這個是全資料量渲染
// for (let row = 0; row < divisions - 1; ++row) {
// for (let i = 0; i < divisions; ++i) {
// const base = row * divisions + i;
// if (i < divisions - 1) {
// indexData.push(base);
// indexData.push(base + 1);
// indexData.push(base + divisions);
// indexData.push(base + 1);
// indexData.push(base + divisions);
// indexData.push(base + divisions + 1);
// }
// }
// }
// 這是一半資料(PS: 這是為了優化,犧牲一些精度)
for (let row = 0; row < divisions - 2; row += 2) {
for (let i = 0; i < divisions; i += 2) {
const base = row * divisions + i;
if (i < divisions - 2) {
indexData.push(base);
indexData.push(base + 2);
indexData.push(base + divisions * 2);
indexData.push(base + 2);
indexData.push(base + divisions * 2);
indexData.push(base + divisions * 2 + 2);
}
}
}
console.timeEnd('獲取索引');
drawLerc3D.indexData = new Uint32Array(indexData);
return drawLerc3D.indexData;
}
複製程式碼
第三步:編寫 GLSL,在 GPU 裡面處理不同高度對應渲染的不同顏色值
vertex shader
// 視角矩陣
uniform mat4 u_matrix;
// 頂點位置資料
attribute vec3 a_Position;
// 紋理資料,貼圖衛星影像
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
// 高程資料
varying float dem;
void main(){
dem = a_Position.z;
gl_Position = u_matrix * vec4(a_Position.x, a_Position.y, dem * 32.0, 1.0);
v_texCoord = a_texCoord;
}
複製程式碼
fragment shader
// precision lowp float;
// uniform float u_brightness_low;
// uniform float u_brightness_high;
// 顏色
// varying vec3 v_Color;
varying float dem;
// 紋理
uniform sampler2D u_image;
varying vec2 v_texCoord;
// 根據不同高程取不同顏色
vec4 getColor() {
// 顏色陣列
const int COLORS_SIZE = 11;
vec3 colors[COLORS_SIZE];
// 對 dem 進行歸一化
float n_dem = -2.0 * (dem / 6000.0 - 0.5);
const float MINDEM = -1.0;
const float MAXDEM = 1.0;
const float STEP = (MAXDEM - MINDEM) / float(COLORS_SIZE - 1);
int index = int(ceil((n_dem - MINDEM) / STEP));
colors[10] = vec3(0.3686274509803922,0.30980392156862746,0.6352941176470588);
colors[9] = vec3(0.19607843137254902,0.5333333333333333,0.7411764705882353);
colors[8] = vec3(0.4, 0.7607843137254902,0.6470588235294118);
colors[7] = vec3(0.6705882352941176,0.8666666666666667,0.6431372549019608);
colors[6] = vec3(0.9019607843137255,0.9607843137254902,0.596078431372549);
colors[5] = vec3(1.0, 1.0, 0.7490196078431373);
colors[4] = vec3(0.996078431372549,0.8784313725490196,0.5450980392156862);
colors[3] = vec3(0.9921568627450981,0.6823529411764706,0.3803921568627451);
colors[2] = vec3(0.9568627450980393,0.42745098039215684,0.2627450980392157);
colors[1] = vec3(0.8352941176470589,0.24313725490196078,0.30980392156862746);
colors[0] = vec3(0.6196078431372549,0.00392156862745098,0.25882352941176473);
if(index > 10){
return vec4(0.3, 0.3, 0.9, 0.5);
}
if(index < 0){
index = 0;
}
for (int i = 0; i < COLORS_SIZE; i++) {
if (i == index) return vec4(colors[i], 1.0);
}
}
void main(){
// 用顏色渲染 DEM 資料,和紋理二選一
gl_FragColor = getColor();
// 用紋理(衛星影像)渲染效果
gl_FragColor = texture2D(u_image, v_texCoord / 256.0 / 32.0);
}
複製程式碼
最後:在 MapboxGL 裡面使用我們自己定義的 Source 和 Layer
map.addSource('DEMImgSource', { //高程資料
"type": "DEM3D",
"tiles": [
'http://xxx.xxx.xxx.xxx/{x}/{y}/{z}',
],
"tileSize": 512,
// 谷歌瓦片地址,用來渲染紋理貼圖
"rasterUrl": 'http://www.google.cn/maps/vt?lyrs=s@189&gl=cn&x={x}&y={y}&z={z}',
// 高德的
// "rasterUrl": 'https://webst04.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}'
});
map.addLayer({ // layer
'id': 'DEMlayer',
'type': 'DEM3D',
'source': 'DEMImgSource'
});
複製程式碼
最終的渲染效果(顏色渲染):
因為資料量實在是太大(一般整張3D螢幕渲染需要40張瓦片,每張都有256*256個資料點),一開始沒有做優化的時候非常卡,根本無法進行地圖拖動和縮放,後來將資料進行快取,頂點資訊進行精簡,瓦片大小進行放大(一螢幕只需要20張資料片渲染)得到的效果就很不錯了,拖動和縮放基本比較流暢,體驗和正常地圖差別不大。紋理渲染效果:
不得不說好像還是顏色渲染的視覺效果更(yao)好(yan)一(jian)些(huo)~
對於 WebGL 方向上的探索一些大公司也有一些成果: 高德 Loca:lbs.amap.com/api/javascr…
百度 Echarts: echarts.baidu.com/examples/in…
UBER: deck.gl/
等等,所以對於 WebGL 的前景個人覺得在資料視覺化、高精地圖(無人駕駛)等方面還是有很多價值的~
第一次寫文章,很多地方可能沒有解釋清楚,歡迎拍磚~