“未來早已到來,只是尚未流行。”——K.K
最近,由於業務的需求,筆者的團隊終於邁進了3d時代。
其實,早在2017年,筆者便開始嘗試前端的3d探索,因為當時主要的業務場景是運營活動,求新、創新便是活動的特性。不過,當時由於種種原因,最后未能落地,但未曾想到,會在3年后有了落地的時刻:

動態效果請查看這個demo: gis3dmodel
這也是筆者第一次在正式場景中做這種嘗試,而且由於時效性特別強,中間過程伴隨着各種各樣的問題,盡管最終效果也沒有盡善盡美,但也算是里程碑式的第一步,心想着記錄下這次探索的積累,於是才有了此文。
其實,與當時相比,現在的業內的web3d環境已經有了很大的改觀:前有U3D大量的商業案例成熟的開發模式,后有微軟為babylon.js站台力挺,遠不像當年只有3d領域three.js一家獨奏的時代,不過比較下來,由於three.js海量的資料與文獻,最終成為了筆者選擇它作為團隊撬開3d時代大門的鑰匙(筆者這里直接略過了webgl,而直接選擇了封裝好一定功能上層庫):

相比2d時代,除了原本的舞台(scene)、渲染器(renderer)之外,新加入了光源(light)、攝像頭(camera),以用來在繪圖區域,描述一個虛擬的3d場景。而原本在場景中的元素,也變得復雜了起來:有geometry來描述它們的幾何形狀,有material來描述他們的材質,然后共同作用與object3d及其子類上,最終成為虛擬3d空間中的2d或3d角色。
不過,要真正把three.js介紹完按筆者目前的熟悉程度,還是遠遠不夠的,所以就以上做一個簡單的介紹吧,然后回到筆者的實踐中來。
在筆者的業務場景中,主要述求是需要在一個3d場景中基於GIS信息繪制2d地圖對象,然后給它添加一些光柱、輝光等效果存在着一個和上圖類似的基於GIS信息的2d地圖對象,需要解決經緯度到虛擬三維空間坐標轉換的問題,還有諸如漸變線條、鏡頭轉動效果、輝光效果等等或模型上或交互上諸多問題的確認和解決,但整體來講,其中最具有挑戰性的卻是一種新的工作流的建立:需要統一PM、UE/I、FE的想法,因為設計師提供的是一張靜態的設計稿,而FE需要實現的則是包含比2d交互更多的3d交互效果(還有說鏡頭效果等)。
不過工作流的問題比較虛,還是簡單整理點實際坑:
1.經緯度轉換
其實拿到經緯度的時候,聰明的你一定會先有一個疑問,經緯度類似於球面坐標,你要在平面繪制,難道不需要先做墨卡托投影嗎?筆者剛開始也有這個疑問,不過經過對數據源的了解,其實我們拿到的gis數據就是已經做過墨卡托投影的了,所以就不必畫蛇添足呢。
另外,筆者需要將一連串經緯度數據,繪制到虛擬三維空間中去,而這些經緯度信息,由於本身的精度問題,直接繪制會導致整個地圖在視覺上特別的小,那么如何解決呢?當然是做縮放咯。另外,筆者原以為需要對經緯度做球面坐標到平面坐標的轉變,但是查閱資料后發現,並沒有這個必要,於是就只需要進行縮放了,而縮放的邏輯也比較簡單:
// 經緯度的最小值和最大值需在外層完成取值
function zoomXY(coords) {
let per = (maxX - minX) / (maxY - minY);
return coords.map(({ x, y }) => ({
x: (x - minX) / (maxX - minX) * 100,
y: (y - minY) / (maxY - minY) * 100 * per
}))
}
筆者先找到這些經緯度的最大值與最小值,在把他們按照原本的xy比例映射到x總長為100的區間上去,對原本都在31.XXX的維度進行放大,讓他們能夠比較清晰的呈現在畫布上。
2.漸變線繪制
這個就更簡單了,因為筆者使用了業界一個泛用性比較多的庫——threejs,在它的官方demo中便提供了一種線性材質LineMaterial,更夠讓使用者自己定義Line的顏色:
for (let i = 0, len = coords.length; i < len; i++) {
const geometry = new LineGeometry();
const color = new THREE.Color();
const positions = [];
const colors = [];
for (let j = 0, _len = coords[i].length; j < _len; j++) {
if (j < _len / 2) {
color.setHSL(.56 + .05 * j / (coords[i].length / 2), 1, .49 + .01 * j / (coords[i].length / 2));
} else {
color.setHSL(.61 - .05 * j / coords[i].length, 1, .5 - .01 * j / coords[i].length);
}
colors.push(color.r, color.g, color.b);
positions.push(coords[i][j].x, coords[i][j].y, 0);
}
geometry.setPositions(positions);
geometry.setColors(colors);
const matLine = new LineMaterial({
color: 0xffffff,
linewidth: LINEWIDTH,
vertexColors: true,
dashed: false,
});
const line = new Line2(geometry, matLine);
值得注意的是,因為筆者的場景需要保證顏色的平滑過渡,所以筆者在前半段進行了顏色HSL值的遞增,后半段進行遞減,最終實現首尾閉合。同時,這個材質還有個特殊的地方是需要在RaF中進行逐幀更新:
matLine.resolution.set(this._dom.clientWidth, this._dom.clientHeight)
3.光柱光暈輝光等效果
這個就是比較基礎的貼材質的功夫了,邊緣的輝光筆者使用了Tween函數和scale去動態改變sprite的大小,做到讓兩個輝光能夠跟着不規則路徑進行“勻速運動”。另一方面,對於光暈和光柱則更簡單了:
function getShiningCylinder(imgReousrce) {
const texture = new THREE.TextureLoader().load(imgReousrce);
texture.wrapS = THREE.RepeatWrapping;
texture.repeat.set(10, 1);
const geometry = new THREE.CylinderGeometry(radius, radius, height, 32, 1, true);
const material = new THREE.MeshBasicMaterial({
color: 0xffff00,
map: texture,
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide,
transparent: true,
opacity: 1,
depthWrite: false,
})
// 因為時間倉猝,並未進行類的抽象,而是直接以閉包實現的丑陋代碼還請見諒
const cylinder = new THREE.Mesh(geometry, material);
cylinder.rotation.x = Math.PI / 2;
cylinder.position.set(0, 0, height / 2);
cylinder.update = () => {
cylinder._counter += .5;
let angle = cylinder._counter * Math.PI / 180;
cylinder._texture.offset.x = angle;
if (cylinder.scale.y > 0) {
cylinder.scale.x += 0.06;
cylinder.scale.z += 0.06;
cylinder.scale.y -= 0.005;
cylinder.position.set(cylinder.position.x, cylinder.position.y, height / 2 * cylinder.scale.y);
} else {
cylinder.scale.set(1, 1, 1);
cylinder.position.set(cylinder.position.x, cylinder.position.y, cylinder._orginZ);
}
}
return cylinder;
}
只要給一個開口的圓柱體貼上材質,然后逐幀改變它的位置信息,就能夠實現。
不過,在整個過程中,筆者也發現threejs的文檔體系整體並不是很完善,對於很多屬性和方法的描述也不是很清楚,都需要開發者基於個人經驗去做出決策,算是個不大不小的缺點。同時,由於threejs api的過於原子性特點,也讓筆者產生了希望基於threejs打造一個小而美的3d引擎的想法。
與此同時,筆者在完成了團隊3d可視化能力從0到1的突破后,也需要將這些經驗的方法輸送給團隊的其他同學,而想到此處,筆者在激動之余,更深深的覺得:
“未來早已到來,只是尚未流行。”
寫在最后
其實,本次的gis模型只是小試牛刀而已,緊接着就要去呈現數字城市了,其中筆者感覺又將扔掉多年的C撿了起來,開始拾掇GLSL,簡單的來說,three.js提供的普通材質還不足以覆蓋筆者面臨的業務場景

圖片來源於網絡
更細致的紋理效果,很難依靠操作材質來實施,所以需要對紋理進行“定制”,而在web3d時代,webgl提供的定制材質的方式之一便是GLSL(OpenGL Shading Language),簡單來說主要是使用着色器(shader),下面則是openGL中的兩種着色器的工作流,主要就是從頂點着色器進行圖元裝配(primitive assembly),然后對它進行光柵化(Rasterization),之后再藉由片元着色器進行計算、混合、防抖,並進行一些必要裁剪(主要是超出顯示區域的部分)。

最后,將數據推入幀緩沖區(frame buffer),再將它最終繪制到屏幕上。而GLSL正是能夠通過C語言操作去頂點着色器和片元着色器,影響最終輸出結果的語言,有了它才能夠做出更精致的效果。
再寫就扯遠了,筆者團隊對於web3d時代的探索才剛剛起步,希望有朝一日,也能夠孵化出比肩業內優秀方案的web3d解決方案。
