日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
SpriteJS:圖形庫造輪子的那些事兒

從 2017 年到 2020 年,我花了大約 4 年的時間,從零到一,實現(xiàn)了一個可切換 WebGL 和 Canvas2D 渲染的,跨平臺支持瀏覽器、SSR、小程序,基于 DOM 結(jié)構(gòu)和支持響應式的,高性能支持批量渲染、針對可視化場景優(yōu)化、支持 WebWorker 的圖形系統(tǒng)——SpriteJS。

固安網(wǎng)站建設(shè)公司創(chuàng)新互聯(lián)公司,固安網(wǎng)站設(shè)計制作,有大型網(wǎng)站制作公司豐富經(jīng)驗。已為固安成百上千提供企業(yè)網(wǎng)站建設(shè)服務(wù)。企業(yè)網(wǎng)站搭建\成都外貿(mào)網(wǎng)站建設(shè)公司要多少錢,請找那個售后服務(wù)好的固安做網(wǎng)站的公司定做!

?在這個“造輪子”過程中,我一步步將一個很簡陋的渲染庫,變成一個能夠支撐可視化應用和游戲開發(fā)的,還算不錯的一個圖形庫,其中有許多積累,也有許多思考。因為畢竟是兩年多前的研究,有些細節(jié)可能記得不是特別清晰,其中有些特性也許已經(jīng)有點過時,但我想,還是有不少內(nèi)容能給大家?guī)韰⒖己蛦l(fā)。

1. 原始需求:和渲染無關(guān)

2017 年底的時候,我還在奇虎 360 負責奇舞團。奇舞團是一個中臺前端團隊,支持很多 360 的業(yè)務(wù)需求,其中包括一些 toB 的需求,這些需求中有不少可視化圖表和態(tài)勢感知大屏。大概在 2015-2016 年,我們的同學就開始用 D3 來完成可視化項目,因為 D3 具有很高的靈活性。有些同學將 D3 簡單歸類為一種可視化渲染框架,實際上這種想法是錯誤的。D3 并不是可視化框架,而是一個數(shù)據(jù)驅(qū)動引擎。

嚴格來說,D3 關(guān)心的是數(shù)據(jù)的組織,它并不關(guān)心數(shù)據(jù)最終渲染的結(jié)果,但是,D3 的數(shù)據(jù)組織形式是基于樹狀結(jié)構(gòu)的,因為它天然契合樹狀結(jié)構(gòu)的渲染形式。正因為如此,所以一般來說,D3 的官方例子都是用 DOM 或 SVG 渲染,這是因為基于 DOM 樹的渲染和 D3 的樹狀數(shù)據(jù)組織形式是絕配。

  • 使用 DOM 渲染的 D3 柱狀圖:

查看代碼:https://code.juejin.cn/pen/7160491257892962339

  • 使用 SpriteJS 渲染:

查看代碼:https://code.juejin.cn/pen/7160553901123436557

1.1  與 DOM 的一致性

為了達到上面的效果,SpriteJS 參考瀏覽器 DOM API,進行了適配:

  • ??https://github.com/spritejs/spritejs/blob/master/src/node/node.js??
  • ??https://github.com/spritejs/spritejs/blob/master/src/node/group.js??
  • ??https://github.com/spritejs/spritejs/blob/master/src/attribute/node.js??
  • ??https://github.com/spritejs/spritejs/blob/master/src/document/index.js??
  • ??https://github.com/spritejs/spritejs/blob/master/src/selector/index.js??

1.2  SpriteJS & DOM & D3

理論上,操作 SpriteJS 元素和操作 DOM 元素完全一樣,二者差異極小。

查看代碼:https://code.juejin.cn/pen/7160568056672944159

這種一致性使得 SpriteJS 完全可以和 D3 配合使用,靈活解決非常復雜的可視化問題:??http://spritejs.com/#/zh-cn/guide/d3??

2.  設(shè)計一個圖形系統(tǒng)的“骨架”

2.1  坐標系的選擇

在圖形系統(tǒng)的設(shè)計中,首先要確定默認坐標系。理論上講,任何一種直角坐標系,甚至非直角坐標系(比如極坐標)都可以作為默認坐標系,在歐式幾何中,這些坐標系都可以自由轉(zhuǎn)換。不過,考慮與 DOM 的一致性,采用瀏覽器默認的坐標系是一個極好的選擇。

對于 WebGL 渲染來說,我們需要將頂點坐標轉(zhuǎn)換成 WebGL 坐標,在這里,我們采用根據(jù) canvas 的坐標動態(tài)設(shè)置 projectionMatrix 即可:??https://github.com/mesh-js/mesh.js/blob/master/src/renderer.js#L181???

updateResolution() {
const {width, height} = this.canvas;
const m1 = [ // translation
1, 0, 0,
0, 1, 0,
-width / 2, -height / 2, 1,
];
const m2 = [ // scale
2 / width, 0, 0,
0, -2 / height, 0,
0, 0, 1,
];
const m3 = mat3(m2) * mat3(m1);
this.projectionMatrix = m3;
if(this[_glRenderer]) {
this[_glRenderer].gl.viewport(0, 0, width, height);
}
}
attribute vec3 a_vertexPosition;
attribute vec3 a_vertexTextureCoord;
varying vec3 vTextureCoord;
uniform mat3 viewMatrix;
uniform mat3 projectionMatrix;
void main() {
gl_PointSize = 1.0;
vec3 pos = projectionMatrix * viewMatrix * vec3(a_vertexPosition.xy, 1.0);
gl_Position = vec4(pos.xy, 1.0, 1.0);
vTextureCoord = a_vertexTextureCoord;
}

2.2  圖層、樹形結(jié)構(gòu)與元素類型

SpriteJS 用 Scene 表示場景,一個 Layer 表示一個圖層,在這里,我的設(shè)計是一個 Layer 對應一個畫布,即默認每個 Layer 都是獨立的 Canvas 元素。這么做有優(yōu)點也有缺點,是一種設(shè)計上的取舍。

優(yōu)點是,每個 Layer 彼此獨立,Layer 間不必考慮繪制次序,可以充分利用 WebWorker 這樣的多線程來并行繪制,而且邏輯上比較簡單,如果需要在多層響應事件,只需要注意事件處理的次序。缺點是如果分多層繪制,有可能產(chǎn)生較多 Canvas 對象實例,比較耗內(nèi)存。

  • 多線程繪制 

查看代碼:https://code.juejin.cn/pen/7089291575993303071

前面說過,SpriteJS 采用類似樹狀結(jié)構(gòu)來管理元素,Scene、Layer 和 Group 都是容器,而其他類型的圖形元素掛載在容器上。

SpriteJS 的元素類型比較多,一共有超過十五種圖形元素,如下圖所示。

這些元素可以分為兩類,一類是 Block 元素,包括 Sprite、Label 和 Group,一類是 Path 元素,包括各種圖形。這兩類元素中,Block 比較類似于 DOM 元素,占據(jù)矩形區(qū)域,有盒模型,有 border、padding、margin,可以計算大?。籔ath 比較類似于 SVG 元素,通過 Path2D 構(gòu)成矢量形狀,有 stroke 和 fill 兩類渲染,但不計算大?。ú还?Path 還是 Block 都能計算 boundingClientRect)。

Group 比較特殊,SpriteJS v3 里,它默認不計算大小,但繼承它的 Layer 和 Scene 會計算大小。在 v2 中,Group 計算大小,而且能夠做區(qū)域剪裁和設(shè)置 clipPath。v3 里,Group 主要的作用是給分組元素設(shè)置統(tǒng)一的 transform。之所以這樣設(shè)計,牽扯到 WebGL 的渲染模型。在后續(xù)會詳細解釋。

考慮到擴展性,用戶可以通過 spritejs.registerNode 注冊自定義節(jié)點元素。??https://github.com/spritejs/spritejs/blob/master/src/document/index.js#L15??

registerNode 的作用是注冊一個唯一的 nodeName 到 spritejs 的文檔樹上,這樣節(jié)點掛載之后,通過 getElementById、querySelector 等等就可以找到這個節(jié)點。

2.3  屬性更新和重繪機制

SpriteJS 與一般的圖形庫不同,通常情況下,一般的圖形庫會使用一個動畫定時器來以固定幀率刷新畫布。但 SpriteJS 采用的是屬性變化時的異步更新機制。

  • ??https://github.com/spritejs/spritejs/blob/master/src/attribute/node.js#L190??
  • ??https://github.com/spritejs/spritejs/blob/master/src/node/node.js#L430??

具體原理如下圖所示:

這里有些需要注意的細節(jié):

  1. 不是所有的屬性改變都會觸發(fā) render,比如 className、ID 等改變不會觸發(fā)。
  2. 有些屬性改變不僅觸發(fā) render,還需要觸發(fā)其他操作,比如 anchor、border 等屬性的變化,需要重新計算圖形元素的輪廓(后面會講);zIndex 的變化,導致對 group 的 children 的 renderOrder 進行重排。

這樣設(shè)計的好處顯而易見,可以盡量減少不必要的重繪和其他計算,從而提高整體性能。

2.4  外部 Ticker

雖然 SpriteJS 有自己的更新機制,但是一些外部庫,比如 ThreeJS 或者 ClayGL,有自己的更新邏輯,所以 SpriteJS 增加了手動控制的設(shè)計,以方便與外部庫配合。??http://spritejs.com/#/zh-cn/guide/ticker??

2.5  跨平臺

SpriteJS 在實現(xiàn)的時候,盡量不使用瀏覽器原生提供的能力,除非是標準的 Canvas 和 WebGL API。針對瀏覽器、NodeJS、微信小程序、微信小游戲等不同的環(huán)境,通過 polyfill 進行適配。??https://github.com/spritejs/spritejs/tree/master/src/platform??

為了在 NodeJS 中集成 WebGL 和 Canvas 環(huán)境,做了下面這個庫:??https://github.com/akira-cn/node-canvas-webgl??

3.  盒模型、事件、動畫等

3.1  盒模型設(shè)計

對 Block 類型的元素,SprteJS 采用標準的 DOM 盒模型,可以設(shè)置 border、padding 各屬性,并可以通過 boxSizing 屬性切換盒模型方式。

查看代碼:https://code.juejin.cn/pen/7160923382119137317

3.2 事件機制

  • 事件模型、坐標轉(zhuǎn)換
  • ??https://github.com/spritejs/spritejs/blob/master/src/event/event.js??

  • ??https://github.com/spritejs/spritejs/blob/master/src/event/pointer-events.js??

視口寬高:[viewportWidth, viewportHeight]

畫布寬高:[resolutionWidth, resolutionHeight]

偏移量:[offsetLeft, offsetTop]

為什么會產(chǎn)生偏移量,詳細見屏幕適配。

  • 事件派發(fā)和命中
    ??https://github.com/spritejs/spritejs/blob/master/src/node/layer.js#L179??
    ??https://github.com/spritejs/spritejs/blob/d8d7b8f232fe3c44ace11c5775892371bed44a1e/src/node/node.js#L419??
    ??https://github.com/mesh-js/mesh.js/blob/master/src/mesh2d.js#L840??

采用對每個三角網(wǎng)格進行命中檢測(此處有優(yōu)化空間,可以先排序用二分查找快速確定范圍):?

function inTriangle(p1, p2, p3, point) {
const a = p2.copy().sub(p1);
const b = p3.copy().sub(p2);
const c = p1.copy().sub(p3);

const u1 = point.copy().sub(p1);
const u2 = point.copy().sub(p2);
const u3 = point.copy().sub(p3);

const s1 = Math.sign(a.cross(u1));
let p = a.dot(u1) / a.length ** 2;
if(s1 === 0 && p >= 0 && p <= 1) return true;

const s2 = Math.sign(b.cross(u2));
p = b.dot(u2) / b.length ** 2;
if(s2 === 0 && p >= 0 && p <= 1) return true;

const s3 = Math.sign(c.cross(u3));
p = c.dot(u3) / c.length ** 2;
if(s3 === 0 && p >= 0 && p <= 1) return true;

return s1 === s2 && s2 === s3;
}

3.3  動畫的設(shè)計

  • Sprite-Timeline
    ??https://github.com/spritejs/sprite-timeline??

為了實現(xiàn)可以在時間軸按照任意速度播放動畫,包括正向播放和回放,在任意時間點可以跳躍,實時切換播放狀態(tài)和時間軸狀態(tài),設(shè)計了 sprite-timeline 庫。

這個庫的設(shè)計是:

  1. 創(chuàng)建一個 Timeline 對象,它基于當前時間線和 playbackRate 來計算時間,playbackRate 可以是任意數(shù),所以時間可以停止,也可以回溯。playbackRate 的設(shè)置和改變會影響 Timeline 對象的 currentTime。
  2. 除了 currentTime 屬性,Timeline 對象還有一個 entropy(熵)屬性,它和 currentTime 的不同是,如果 playbackRate 為負數(shù),currentTime 會回溯,但 entropy 始終增加。
  3. Timeline 對象可以 fork,fork 出的新對象以被 fork 的 Timeline 對象的 currentTime 為時間線。這意味著 Timeline 對象可以嵌套,在 SpriteJS 中,所有元素會默認 fork 它的 parent 的 timeline 對象,所以當我們把 layer 的 timeline 的 playbackRate 設(shè)置為 0 的時候,這個 layer 中所有的動畫就都會暫停。

查看代碼:https://code.juejin.cn/pen/7160950394573553695

  • Sprite-animator
    基于 timeline 封裝,參考 Web Animations API - Web APIs | MDN( ??https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API?? )
  • Animation & Transition
    ??http://spritejs.com/#/zh-cn/effect?id=%e8%bf%87%e6%b8%a1-transition??
  • Transition-reverse

  • 查看代碼:https://code.juejin.cn/pen/7089261885949739016

  • Path Transition

  • 查看代碼:https://code.juejin.cn/pen/7160959750509690921

  • Play Animations

  • 查看代碼:https://code.juejin.cn/pen/7088265547250401293

  • Async frame animations

  • 查看代碼:https://code.juejin.cn/pen/7088238218914562088

4. 從 2D 到 WebGL

在 Sprite 1.0 和 2.0 的時候,主要是使用 Canvas2D 渲染,直到 3.0,我重寫了底層引擎,開始默認采用 WebGL 渲染。

4.1  輪廓和網(wǎng)格

為了便于 WebGL 處理幾何圖形,尤其是 Path 的解析,我實現(xiàn)了一個底層渲染引擎 GitHub - mesh-js/mesh.js: A graphics system born for visualization( ??https://github.com/mesh-js/mesh.js?? ),將 2D 幾何圖形分解成輪廓和網(wǎng)格對象,這有點像是 ThreeJS 中的 Geometry 和 Material,只不過因為我們要處理的實際上是 2D 圖形,所以模型更加簡單。

在 mesh.js 中,要繪制一個幾何圖形,我們先構(gòu)建該元素的輪廓(Figure/Contours),然后再根據(jù)輪廓創(chuàng)建網(wǎng)格對象。經(jīng)過這樣兩個步驟之后,我們就可以將幾何圖形繪制出來,這個過程其實比較像 Canvas2D,只是比 Canvas2D 稍復雜一點點。

查看代碼:https://code.juejin.cn/pen/7160967356489924622

4.2  三角剖分

眾所周知,WebGL 的基本圖元只有點、線、三角形等,要繪制多邊形,我們需要將圖形進行三角剖分。對任意多邊形進行三角剖分,有許多成熟算法,我選擇的是 GLU Tessellator。

  • ??https://github.com/mesh-js/mesh.js/blob/master/src/tess2/index.js??
  • ??https://github.com/mesh-js/mesh.js/blob/master/src/triangulate-contours/index.js??

我通過一系列工具庫 parse-svg-path、normalize-svg-path、svg-path-contours(??https://github.com/mesh-js/mesh.js/tree/master/src/svg-path-contours??)將 SVGPath 轉(zhuǎn)換成多邊形的頂點列表,這里就不重復造輪子了,有些工具庫有點小 bug,我給順手修了一下。

獲得頂點之后,對頂點進行三角剖分,就可以得到三角網(wǎng)格的拓撲結(jié)構(gòu),通過這個拓撲結(jié)構(gòu)創(chuàng)建 mesh2d 對象。

4.3 Stroke

如果不常用 WebGL 渲染,很難想象,對 Canvas2D 來說非常簡單的繪制帶寬度折線這類需求,會難住 WebGL 開發(fā)者。

其實這個問題已經(jīng)有比較經(jīng)典的解決方案,就是用擠壓(extrude polyline)曲線技術(shù)來實現(xiàn)。有兩種方法,一種是用 JS 算頂點,另一種是在 shader 中進行處理。為了靈活實現(xiàn) Canvas2D 中的“線帽(lineCap)”效果,SpriteJS 采用 JS 計算的方式來處理。

如上圖所示,黑色折線是原始的 1 個像素寬度的折線,藍色虛線組成的是我們最終要生成的帶寬度曲線,紅色虛線是頂點移動的方向。因為折線兩個端點的擠壓只和一條線段的方向有關(guān),而轉(zhuǎn)角處頂點的擠壓和相鄰兩條線段的方向都有關(guān),所以頂點移動的方向,我們要分兩種情況討論。

首先,是折線的端點。假設(shè)線段的向量為(x, y),因為它移動方向和線段方向垂直,所以我們只要沿法線方向移動它就可以了。根據(jù)垂直向量的點積為 0,我們很容易得出頂點的兩個移動方向為(-y, x)和(y, -x)。如下圖所示:

端點擠壓方向確定了,接下來要確定轉(zhuǎn)角的擠壓方向了,我們還是看示意圖。

如上圖,我們假設(shè)有折線 abc,b 是轉(zhuǎn)角。我們延長 ab,就能得到一個單位向量 v1,反向延長 bc,可以得到另一個單位向量 v2,那么擠壓方向就是向量 v1+v2 的方向,以及相反的 -(v1+v2) 的方向。

現(xiàn)在我們得到了擠壓方向,接下來就需要確定擠壓向量的長度。

首先是折線端點的擠壓長度,它等于 lineWidth 的一半。而轉(zhuǎn)角的擠壓長度就比較復雜了,我們需要再計算一下。

綠色這條輔助線應該等于 lineWidth 的一半,而它又恰好是 v1+v2 在綠色這條向量方向的投影,所以,我們可以先用向量點積求出紅色虛線和綠色虛線夾角的余弦值,然后用 lineWidth 的一半除以這個值,得到的就是擠壓向量的長度了。

具體用 JavaScript 實現(xiàn)的代碼如下所示:??https://github.com/mesh-js/mesh.js/blob/master/src/extrude-contours/stroke.js?

function extrudePolyline(gl, points, {thickness = 10} = {}) {
const halfThick = 0.5 * thickness;
const innerSide = [];
const outerSide = [];

// 構(gòu)建擠壓頂點
for(let i = 1; i < points.length - 1; i++) {
const v1 = (new Vec2()).sub(points[i], points[i - 1]).normalize();
const v2 = (new Vec2()).sub(points[i], points[i + 1]).normalize();
const v = (new Vec2()).add(v1, v2).normalize(); // 得到擠壓方向
const norm = new Vec2(-v1.y, v1.x); // 法線方向
const cos = norm.dot(v);
const len = halfThick / cos;
if(i === 1) { // 起始點
const v0 = new Vec2(...norm).scale(halfThick);
outerSide.push((new Vec2()).add(points[0], v0));
innerSide.push((new Vec2()).sub(points[0], v0));
}
v.scale(len);
outerSide.push((new Vec2()).add(points[i], v));
innerSide.push((new Vec2()).sub(points[i], v));
if(i === points.length - 2) { // 結(jié)束點
const norm2 = new Vec2(v2.y, -v2.x);
const v0 = new Vec2(...norm2).scale(halfThick);
outerSide.push((new Vec2()).add(points[points.length - 1], v0));
innerSide.push((new Vec2()).sub(points[points.length - 1], v0));
}
}
...
}

4.4  批量繪制

因為我們繪制 2D 圖形,通常這些圖形可視為同一材質(zhì),所以我們能夠?qū)⑦@些圖形網(wǎng)格數(shù)據(jù)全部壓縮到一個大的類型數(shù)組中進行批量繪制。

??https://github.com/mesh-js/mesh.js/blob/master/src/utils/compress.js??

4.5  Shader & Pass

SpriteJS 可以使用自定義 shader 創(chuàng)建 Program,將 Program 賦給繪圖元素進行繪制。

查看代碼:https://code.juejin.cn/pen/7088623553993506852

我們可以在渲染管線中應用多個 shader 組成管道進行渲染,有一種特定的渲染管道叫做后期處理通道,SpriteJS 支持定義后期處理通道。

查看代碼:https://code.juejin.cn/pen/7088626022244941839

5. 關(guān)于性能優(yōu)化的那些事兒

5.1 性能的直觀感受

SpriteJS 針對可視化場景進行了性能優(yōu)化??梢暬瘓鼍爸杏写罅恐貜突蝾愃菩螤畹膸缀螆D形,因此用合并頂點批量渲染的方式會很有效。

查看代碼:https://code.juejin.cn/pen/7088268165032968223

查看代碼:https://code.juejin.cn/pen/7088274902167322631

5.2  auto Blending 和輪廓更新

WebGL 在顏色混合的時候比較消耗性能,因此 mesh-js 對元素做了判斷,如果當前繪制的元素都沒有 alpha 通道(透明度),那么不會開啟顏色混合,否則再開啟顏色混合。

在 SpriteJS 中,元素的大部分樣式改變,比如 transform、position、bgcolor 等等,不涉及輪廓的變化,這些情況下,我們不用重新計算輪廓,所以我們將元素輪廓計算好之后緩存起來,大部分情況下我們不需要重復計算。只有一些特殊屬性,比如 Path 的 d、lineWidth、lineCap、Block 的 border 等改變,才需要重新計算輪廓。

5.3  Seal & Cloud

??http://spritejs.com/#/zh-cn/guide/performance??

Seal 是一種特殊的方式,當我們使用一個 group 來組合一組圖形時,如果只是需要使用固定的圖形拓撲結(jié)構(gòu),我們可以使用 group 的 seal 方法將子元素的幾何圖形合并成為 group 的幾何圖形。這樣 group 的幾何圖形將被合并的幾何圖形替代,成為一個單一的元素被渲染,并且不再能夠改變幾何圖形(但是依然可以改變位置、transform、顏色等等屬性)。

seal 生效的時候,原子元素的屬性將失效,由 group 的屬性替代。

當我們用 group 構(gòu)建組合圖形的時候,這種特殊方式能夠大大提升渲染性能。

查看代碼:https://code.juejin.cn/pen/7088273623122706466

對于繪制完全重復的幾何圖形,我們還可以利用 WebGL 的來進行渲染。

查看代碼:https://code.juejin.cn/pen/7088273623122706466

查看代碼:https://code.juejin.cn/pen/7088274222738505732

5.4  關(guān)于 Shader 的性能開銷

有一條需要格外注意:盡量使用條件編譯代替條件分支

6. 一些細節(jié),屏幕適配等

  • 黏連模式:??http://spritejs.com/#/zh-cn/guide/resolution??
  • 資源加載:??http://spritejs.com/#/zh-cn/guide/resource??

本文名稱:SpriteJS:圖形庫造輪子的那些事兒
網(wǎng)站路徑:http://www.dlmjj.cn/article/dpiceog.html