Android平常學習:OpenGL 實踐之貝塞爾曲線繪製

說到貝塞爾曲線,你們確定都不陌生,網上有不少關於介紹和理解貝塞爾曲線的優秀文章和動態圖。canvas

如下兩個是比較經典的動圖了。數組

二階貝塞爾曲線:ide

image

三階貝塞爾曲線:函數

image

因爲在工做中常常要和貝塞爾曲線打交道,因此簡單說一下本身的理解:性能

如今假設咱們要在座標系中繪製一條直線,直線的方程很簡單,就是 y=x ,很容易獲得下圖:優化

image.png

如今咱們限制一下 x 的取值範圍爲 0~1 的閉區間,那麼能夠得出 y 的取值範圍也是 0~1。3d

而在 0~1 的區間範圍內,x 能取的數有多少個呢?答案固然是無數個了。code

image.png

同理,y 的取值個數也是有無數個。每個 x 都有惟一的 y 與之對應,一個 (x,y) 在座標系上就是一個點。orm

因此最終獲得的 0~1 區間的線段,其實是由無數的點組成的。blog

那麼這條線段有多長呢?長度是由 x 的取值範圍來決定的,若 x 的取值爲 0~2,那麼線段就長了一倍。

另外,若是 x 的取值範圍不是無數個,而是以 0.05 的間距從 0 到 1 之間遞增,那麼獲得的就是一串點了。

因爲 點 是一個理想狀態下的描述,在數學上點是沒有寬高、沒有面積的。
可是,若是你在草稿紙上繪製一個點,無論你用到是鉛筆、毛筆、水筆仍是畫筆,一個點老是要佔面積的。
毛筆畫一個點的面積可能須要鉛筆畫幾十個點了。

在實際生活中,若是要以 0.05 的間距在第一幅座標系圖中畫出 x 在 0~1 區間的一串點,最終結果就和直接畫一條線段沒啥差異了。

這就是現實和理想的差異了。理想一串點,現實一條線。


咱們把這個邏輯放到手機屏幕上。

手機屏幕上的最小顯示單位就是像素了,一個 1920 * 1080 的屏幕指的就是各方向上像素點的數量。

假如繪製一條和屏幕同樣寬的線段,一個點最小就算一個像素,最多也就 1080 個點了。

點佔的像素越多,那麼實際繪製時須要的點的數量越少,這也算是潛在的優化項了。


說完直線,再回到貝塞爾曲線上。

曲線和直線都有一個共同點,它們都有各自特定的方程,只不過咱們用的直線例子比較簡單,既 y = x ,一眼看出計算結果。

直線方程 y = x,在數學上能夠這麼描述:y 是關於 x 的函數,既 y = F(x) ,其中 x 的取值決定了該直線的長度。

根據上面的理解,這個長度的直線實際又是由在 x 的取值範圍內對應的無數個點組成的。

反觀貝塞爾曲線方程以及對應的圖形以下:

  • 二階貝塞爾曲線:其中,P0 和 P2 是起始點,P1 是控制點。

image.png

image.png

  • 三階貝塞爾曲線其中,P0 和 P3 是起始點,P1 和 P2 是控制點。

image.png

image.png


不難理解,假設咱們要繪製一條曲線,確定要有起始和結束點來指定曲線的範圍曲線。

而控制點就是指定該曲線的弧度,或者說指定該曲線的彎曲走向,不一樣的控制點得出的曲線繪製結果是不同的。

另外,能夠觀察到,不管是幾階貝塞爾曲線,都會有參數 t 以及 t 的取值範圍限定。

t 在 0~1 範圍的閉區間內,那麼 t 的取值個數實際上就有無數個了,這時的 t 就能夠理解成上面介紹直線中講到的 x 。

這樣一來,就能夠把起始點、控制點當初固定參數,那麼貝塞爾曲線計算公式就成了 B = F(t) ,B 是關於 t 的函數,而 t 的取值範圍爲 0~1 的閉區間。

也就是說貝塞爾曲線,選定了起始點和控制點,照樣能夠當作是 t 在 0~1 閉區間內對應的無數個點所組成的。

有了上面的闡述,在工(ban)程(zhuan)的角度上,就不難理解貝塞爾曲線到底怎麼使用了。


Android 繪製貝塞爾曲線

Android 自帶貝塞爾曲線繪製 API ,經過 Path 類的 quadTo 和 cubicTo 方法就能夠完成繪製。

1 // 構建 path 路徑,也就是選取
 2 path.reset();
 3 path.moveTo(p0x, p0y);
 4 // 繪製二階貝塞爾曲線
 5 path.quadTo(p1x, p1y, p2x, p2y);
 6 path.moveTo(p0x, p0y);
 7 path.close();
 8
 9 // 最後的繪製操做
10 canvas.drawPath(path, paint);

這裏的繪製實際上就是把貝塞爾曲線計算的方程式交給了 Android 系統內部去完成了,參數傳遞上只傳遞了起始點和控制點。

咱們能夠經過本身的代碼來計算這個方程式從而對邏輯上得到更多控制權,也就是把曲線拆分紅許多個點組成,若是點的尺寸比較大,甚至能夠減小點的個數實現一樣的效果,達到繪製優化的目的。

OpenGL 繪製

經過 OpenGL 能夠實現咱們上述的方案,把曲線拆分紅多個點組成。這種方案要求咱們在 CPU 上去計算貝塞爾曲線方程,根據 t 的每個取值,計算出一個貝塞爾點,用 OpenGL 去繪製上這個點。

這個點的繪製能夠採用 OpenGL 中畫三角形 GL_TRIANGLES 的形式去繪製,這樣就能夠給點帶上紋理效果,不過這裏面的坑略多,起始點和控制點都是運行時動態可變的實現難度會大於固定不變的。

這裏先介紹另外一種方案,這種方案實現比較簡單也能達到優化效果,咱們能夠把貝塞爾曲線的計算方程式交給 GPU, 在 OpenGL Shader 中去完成。

這樣一來,咱們只要給定起始點和控制點,中間計算貝塞爾曲線去填補點的過程就交給 Shader 去完成了。

另外,經過控制 t 的數量,咱們能夠控制貝塞爾點填補的疏密。

t 越大,填補的點越多,超過必定閾值後,不會對繪製效果有提高,反而影響性能。

t 越小,那麼貝塞爾曲線就退化成一串點組成了。因此說 t 的取值範圍也能對繪製起到優化做用。

繪製效果以下圖所示:

image

如下就是實際的代碼部分了,關於 OpenGL 的基礎理論部分能夠參考以前寫過的文章和公衆號,就再也不闡述了。

在 Shader 中定義一個函數,實現貝塞爾方程:

1vec2 fun(in vec2 p0, in vec2 p1, in vec2 p2, in vec2 p3, in float t){
2 float tt = (1.0 - t) * (1.0 -t);
3 return tt * (1.0 -t) *p0 
4 + 3.0 * t * tt * p1 
5 + 3.0 * t *t *(1.0 -t) *p2 
6 + t *t *t *p3;
7}

該方程能夠利用 Shader 中自帶的函數優化一波:

1vec2 fun2(in vec2 p0, in vec2 p1, in vec2 p2, in vec2 p3, in float t)
2{
3 vec2 q0 = mix(p0, p1, t);
4 vec2 q1 = mix(p1, p2, t);
5 vec2 q2 = mix(p2, p3, t);
6 vec2 r0 = mix(q0, q1, t);
7 vec2 r1 = mix(q1, q2, t);
8 return mix(r0, r1, t);
9}

接下來就是具體的頂點着色器 shader :

1// 對應 t 數據的傳遞
 2attribute float aData;
 3// 對應起始點和結束點
 4uniform vec4 uStartEndData;
 5// 對應控制點
 6uniform vec4 uControlData;
 7// mvp 矩陣
 8uniform mat4 u_MVPMatrix;
 9
10void main() {
11 vec4 pos;
12 pos.w = 1.0;
13 // 取出起始點、結束點、控制點
14 vec2 p0 = uStartEndData.xy;
15 vec2 p3 = uStartEndData.zw;
16 vec2 p1 = uControlData.xy;
17 vec2 p2 = uControlData.zw;
18 // 取出 t 的值
19 float t = aData;
20 // 計算貝塞爾點的函數調用
21 vec2 point = fun2(p0, p1, p2, p3, t);
22 // 定義點的 x,y 座標
23 pos.xy = point;
24 // 要繪製的位置
25 gl_Position = u_MVPMatrix * pos;
26 // 定義點的尺寸大小
27 gl_PointSize = 20.0;
28}

代碼中的 uStartEndData 對應起始點和結束點,uControlData 對應兩個控制點。

這兩個變量的數據傳遞經過 glUniform4f 方法就行了:

1 mStartEndHandle = glGetUniformLocation(mProgram, "uStartEndData");
 2 mControlHandle = glGetUniformLocation(mProgram, "uControlData");
 3 // 傳遞數據,做爲固定值
 4 glUniform4f(mStartEndHandle,
 5 mStartEndPoints[0],
 6 mStartEndPoints[1],
 7 mStartEndPoints[2],
 8 mStartEndPoints[3]);
 9 glUniform4f(mControlHandle,
10 mControlPoints[0],
11 mControlPoints[1],
12 mControlPoints[2],
13 mControlPoints[3]);

另外重要的變量就是 aData 了,它對應的就是 t 在 0~1 閉區間的劃分的數量。

1 private float[] genTData() {
2 float[] tData = new float[Const.NUM_POINTS];
3 for (int i = 0; i < tData.length; i ++) {
4 float t = (float) i / (float) tData.length;
5 tData[i] = t;
6 }
7 return tData;
8 }

以上函數就是把 t 在 0~1 閉區間分紅 Const.NUM_POINTS 份,每一份的值都存在 tData 數組中,最後經過 glVertexAttribPointer 函數傳遞給 Shader 。

最後實際繪製時,咱們採用 GL_POINTS 的形式繪製就行了。

1 GLES20.glDrawArrays(GLES20.GL_POINTS, 0, Const.NUM_POINTS );

以上就是 OpenGL 繪製貝塞爾曲線的小實踐。

相關文章
相關標籤/搜索