最近要作一個基站站點的可視化呈現項目。 咱們首先嚐試的是三維的可視化技術來程序,可是客戶反饋的狀況是他們的客戶端電腦比較差,性能效率都會很差,甚至有的仍是雲主機。 所以咱們先作了一個性能比較極致的3Ddemo,以下圖所示:
前端
爲了可以儘量的性能最優,因此想了各類性能優化手段。固然效果上也會有折扣,這個demo與咱們自己的一些產品好比3D機房等相比較,效果上面確定有了很大的差距。不過性能方面仍是很不錯的。
然而,很不幸,客戶在拿到demo測試以後,不滿意...。性能還算湊合,但他們還以爲效果不夠酷。node
配置很低,又要性能高、又要效果炫。這隻能化爲一句話:
程序員
彷佛陷入了絕境...
然而 絕處每每逢生,絕處每每有新的但願、新的機會。算法
忽然想到的是2.5D,這是一種僞3D效果,可是隻能體現一個鏡頭角度的顯示效果,不能實現鏡頭的旋轉效果。性能優化
其實在很早的時候,咱們就有一些2.5D的雛形的東西,好比分層拓撲圖和2.5D節點。分層拓撲圖甚至能夠追溯到Java時代。以下圖所示:架構
把以前的2.5D源代碼拿過來讀一遍。讀了以後,總的思路:主要經過拼湊三個平行四邊形來模擬這種3D的效果,技術沒有體系。佈局
這種思路對於對象的位置定位和對齊會比較難,開發難度自己也比較大,另外要實現一些好的效果,難度也比較大,要知道客戶對於效果的要求並不低。性能
所以須要想出新的技術思路,最好是有成體系的思路,要擺脫以前的技術思路。固然並不容易,當時我並無什麼好的思路,有不少疑惑,有不少迷茫。以後的不少天裏面,都是這種狀態。測試
事情的起色在一次出差。優化
在拜訪一個大客戶回酒店的路上,我走在馬路上,個人腦中忽然蹦出一個想法,爲何不借助3D的思路和部分算法呢,2.5D要呈現的不就是3D的效果嗎?所謂2.5D,顧名思義,就是取幾勺2D技術,再取幾勺3D的技術,一塊兒放到鍋裏炒一炒,爲啥要侷限在2D的技術。
我自己研究3D技術不少年,對於3D的相關技術也算是很熟練,忽然,彷佛全部的事情的想通了,一套成體系的2.5D技術開始在心中生根,發芽,生長。
個人心裏很欣喜。(可是表面很平靜)
這個事情告訴咱們一個道理,弄不懂的問題,不要死摳,多出去走走,說不定就想通了。😄
接下來,我自信滿滿的和客戶溝通,開始着手寫相關的技術驗證demo,其中涉及到一些技術會在後面說明。demo最終獲得了客戶的承認,最終咱們也拿下了這個項目。
而於我,本身創造了一套2.5D相關的技術體系,也算是一個小小的成就吧。
這是一次創做,而創做是讓人愉悅的事情。
所謂的2.5D,就是經過2D繪製技術,實現3D的渲染效果。而這其中,勢必須要用到一部分3D的技術:
爲了可以實現2.5D的效果,咱們須要把原來的平面二維空間延伸到三維立體空間。三維立體空間中存在着X、Y、Z三個座標軸,比原來的二維空間多出了一個Z座標軸。
固然,三維空間定義是爲了模型定義、模型位置定位和後續的投影算法。最終的繪製仍是會回到二維空間進行。
在真正的三維中,須要經過obj等模型文件來定義模型。 在2.5D中,只須要定義一個立方體的模型便可。 前面說過,2.5D只是呈現了三維對象的某個角度的一個面,所以其模型只須要這個面的一張圖片便可,圖片就是模型。
之因此要定義一個立方體的模型,是爲了圖片可以擺在合適的位置,以及約束合適的大小和長寬比。 這對於模型的擺放和對齊有很重要的意義。立方體在這裏就相似真實模型的包圍體。
經過指定寬、高、深等屬性,即可以定義一個立方體。代碼以下所示:
setSize3: function(w, h, d) { var oldValue = { w: this._width3, h: this._height3, d: this._depth3, }; this._width3 = w; this._height3 = h; this._depth3 = d; this.firePropertyChange('size3', oldValue, { w: w, h: h, d: d }); },
同時能夠指定立方體的三維座標位置,代碼以下:
setPosition: function(x, y, z) { var oldValue = this.getPosition(); this._position = { x: x, y: y, z: z, }; this.firePropertyChange('position', oldValue, this._position); },
投影算法是三維圖形學中很重要的一環。 投影算法主要有透視投影算法和平行投影算法。 2.5D中須要使用的是平行投影(也只能使用平行投影算法)
投影算法算是比較關鍵的一步。
要定義投影算法,咱們首先要模擬一個平行鏡頭,經過平行鏡頭定義鏡頭的位置,角度等,並由這些參數定義出一個投影的矩陣:
/** * 計算變換矩陣,變換矩陣由鏡頭參數決定 */ calMVMatrix: function() { var angle = this.getAngle3(), vAngle = this.getVAngle3(), radius = this.getRadius3(), viewMatrix = mat4.create(), projectMatrix = mat4.create(), mvMatrix = mat4.create(), winWidth = 1, winHeight = 1; mat4.lookAt( viewMatrix, [ radius * Math.cos(vAngle) * Math.sin(angle), -radius * Math.sin(vAngle), radius * Math.cos(vAngle) * Math.cos(angle), ], [0, 0, 0], [0, 1, 0] ); mat4.ortho( projectMatrix, -winWidth / 2, winWidth / 2, -winHeight / 2, winHeight / 2, 0.1, 1000 ); mat4.multiply(mvMatrix, projectMatrix, viewMatrix); this.mvMatrix = mvMatrix; },
上述代碼中,定義投影矩陣使用了gl-matrix.js這個包。
在定義了投影矩陣以後,即可以經過投影算法計算出立方體上面每一個頂點在平面座標上的位置:
/** * 佈局,前面四個點 p1 - p4, 後面 四個點p 5 - p8 * * p8 p7 * * p5 p6 * * p4 p3 * * p1 p2 * */ var points1 = [ { x: -w3 / 2 + pos.x, y: -h3 / 2 + pos.y, z: d3 / 2 + pos.z, }, // p1 { x: w3 / 2 + pos.x, y: -h3 / 2 + pos.y, z: d3 / 2 + pos.z, }, // p2 { x: w3 / 2 + pos.x, y: h3 / 2 + pos.y, z: d3 / 2 + pos.z, }, // p3 { x: -w3 / 2 + pos.x, y: h3 / 2 + pos.y, z: d3 / 2 + pos.z, }, // p4 { x: -w3 / 2 + pos.x, y: -h3 / 2 + pos.y, z: -d3 / 2 + pos.z, }, // p5 { x: w3 / 2 + pos.x, y: -h3 / 2 + pos.y, z: -d3 / 2 + pos.z, }, // p6 { x: w3 / 2 + pos.x, y: h3 / 2 + pos.y, z: -d3 / 2 + pos.z, }, // p7 { x: -w3 / 2 + pos.x, y: h3 / 2 + pos.y, z: -d3 / 2 + pos.z, }, // p8 ]; var points = (this._points = []); points1.forEach(function(point) { var newPoint = self.getPositionByRotate( point, pos, rotationX, rotationY, rotationZ ); points.push({ x: newPoint[0], y: newPoint[1], z: newPoint[2], }); }); var ps = (this._projectPoints = points.map(function(point) { return self.getProjectionPoint(point); }));
有了8個頂點的投影點以後,能夠繪製邊框效果、能夠繪製顏色填充效果,也能夠繪製圖片填充的效果。
把幾個面的點按照順序組織起來,便可以繪製邊框的效果。 以下代碼所示:
drawPoints: function (ctx, points, close, dash, fill, borderColor, image) {
if (!points || points.length == 0) {
return;
}
ctx.beginPath();
ctx.strokeStyle = "black";
if (borderColor) {
ctx.strokeStyle = borderColor;
}
ctx.lineWidth = 1;
ctx.fillStyle = 'rgb(102,204,255)';
if (dash) {
ctx.setLineDash([4, 4]);
ctx.strokeStyle = 'rgba(0,0,0,0.5)';
} else {
ctx.setLineDash([1, 0]);
}
ctx.moveTo(points[0].x, points[0].y);
for (var i = 1; i < points.length; i++) { var p = points[i]; ctx.lineTo(p.x, p.y); } if (close) { ctx.lineTo(points[0].x, points[0].y); } ctx.closePath(); ctx.stroke();
}
最終的繪製效果以下圖所示:
要繪製填充顏色的立方體,只須要在上邊的繪製中加上這行代碼便可:
if (fill) { ctx.fill(); // drawImageInPoints(ctx, image, points); }
最終的繪製效果以下:
繪製圖片的時候,並不須要每一個面都去繪製圖片,只須要把圖片繪製到立方體投影的8個頂點所佔據的區域裏面,須要作到的是,其8個頂點的位置正好和圖片的頂點重合,好比下圖:
首先計算出投影頂點所佔據的二維區域大小:
/** * 根據points中的8個點,找出包裹8個點的最小rect * * @param {Array} points - 8個點的2d座標 * @returns {Object} - rect */ getRect: function(points) { var minX, minY, maxX, maxY; points.forEach(function(point) { if (minX == null) { minX = maxX = point.x; minY = maxY = point.y; } else { minX = Math.min(minX, point.x); maxX = Math.max(maxX, point.x); minY = Math.min(minY, point.y); maxY = Math.max(maxY, point.y); } }); return { x: minX, y: minY, width: maxX - minX, height: this.getElement().getClient('reflect') ? (maxY - minY) * 2 : maxY - minY, }; },
而後在該區域直接繪製圖片:
ctx.drawImage( image, 0, image.height - 20, image.width, 20, rect.x, rect.y + rect.height - 20, rect.width, 20 );
最終繪製效果以下圖:
有了立方體模型以後,即可以搭建地面 牆面場景效果,因爲地面、牆面均可以使用立方體來組成。 所以能夠很方便的搭建出來,只須要把相關的立方體模型設置好尺寸,添加到場景中便可:
var node1 = new twaver.Node2_5({ styles: { 'body.type': 'vector', }, name: 'TWaver', centerLocation: { x: 300, y: 200 }, width: 800 / 1, height: 360 /.775, }); node1.setImage(null); node1.setPosition(00,0,100); node1.setWidth3(1000); node1.setHeight3(10); node1.setDepth3(1200); // node1.setStyle('top.image','image0'); // ToDo 定義樣式規則 // node1.setStyle('top.image.rule','pattern'); // node1.setClient('receiveShadow',true); box.add(node1); var node1 = new twaver.Node2_5({ styles: { 'body.type': 'vector', }, name: 'TWaver', }); node1.setImage(null); node1.setPosition(-250,155,-500); node1.setWidth3(500); node1.setHeight3(300); node1.setDepth3(1); box.add(node1); var node1 = new twaver.Node2_5({ styles: { 'body.type': 'vector', }, name: 'TWaver', }); node1.setImage(null); node1.setPosition(250,105,-500); node1.setWidth3(500); node1.setHeight3(200); node1.setDepth3(1); node1.setStyle('front.image','weilan'); // ToDo 定義樣式規則 box.add(node1);
最終的顯示效果以下:
對於地面的貼圖和牆面的光照效果,會在後續講解。
第一彈講述到這裏,先上一張總體的效果瞅瞅:
歡迎關注公衆號「ITman彪叔」。彪叔,擁有10多年開發經驗,現任公司系統架構師、技術總監、技術培訓師、職業規劃師。在計算機圖形學、WebGL、前端可視化方面有深刻研究。對程序員思惟能力訓練和培訓、程序員職業規劃有濃厚興趣。