這篇文章給大家介紹OpenGL 實(shí)踐中如何進(jìn)行貝塞爾曲線繪制,內(nèi)容非常詳細(xì),感興趣的小伙伴們可以參考借鑒,希望對(duì)大家能有所幫助。
讓客戶滿意是我們工作的目標(biāo),不斷超越客戶的期望值來(lái)自于我們對(duì)這個(gè)行業(yè)的熱愛(ài)。我們立志把好的技術(shù)通過(guò)有效、簡(jiǎn)單的方式提供給客戶,將通過(guò)不懈努力成為客戶在信息化領(lǐng)域值得信任、有價(jià)值的長(zhǎng)期合作伙伴,公司提供的服務(wù)項(xiàng)目有:國(guó)際域名空間、虛擬空間、營(yíng)銷軟件、網(wǎng)站建設(shè)、瀘溪網(wǎng)站維護(hù)、網(wǎng)站推廣。
說(shuō)到貝塞爾曲線,大家肯定都不陌生,網(wǎng)上有很多關(guān)于介紹和理解貝塞爾曲線的優(yōu)秀文章和動(dòng)態(tài)圖。
以下兩個(gè)是比較經(jīng)典的動(dòng)圖了。
二階貝塞爾曲線:
image
三階貝塞爾曲線:
image
由于在工作中經(jīng)常要和貝塞爾曲線打交道,所以簡(jiǎn)單說(shuō)一下自己的理解:
現(xiàn)在假設(shè)我們要在坐標(biāo)系中繪制一條直線,直線的方程很簡(jiǎn)單,就是 y=x ,很容易得到下圖:
image.png
現(xiàn)在我們限制一下 x 的取值范圍為 0~1 的閉區(qū)間,那么可以得出 y 的取值范圍也是 0~1。
而在 0~1 的區(qū)間范圍內(nèi),x 能取的數(shù)有多少個(gè)呢?答案當(dāng)然是無(wú)數(shù)個(gè)了。
image.png
同理,y 的取值個(gè)數(shù)也是有無(wú)數(shù)個(gè)。每一個(gè) x 都有唯一的 y 與之對(duì)應(yīng),一個(gè) (x,y) 在坐標(biāo)系上就是一個(gè)點(diǎn)。
所以最終得到的 0~1 區(qū)間的線段,實(shí)際上是由無(wú)數(shù)的點(diǎn)組成的。
那么這條線段有多長(zhǎng)呢?長(zhǎng)度是由 x 的取值范圍來(lái)決定的,若 x 的取值為 0~2,那么線段就長(zhǎng)了一倍。
另外,如果 x 的取值范圍不是無(wú)數(shù)個(gè),而是以 0.05 的間距從 0 到 1 之間遞增,那么得到的就是一串點(diǎn)了。
由于 點(diǎn) 是一個(gè)理想狀態(tài)下的描述,在數(shù)學(xué)上點(diǎn)是沒(méi)有寬高、沒(méi)有面積的。
但是,如果你在草稿紙上繪制一個(gè)點(diǎn),不管你用到是鉛筆、毛筆、水筆還是畫筆,一個(gè)點(diǎn)總是要占面積的。
毛筆畫一個(gè)點(diǎn)的面積可能需要鉛筆畫幾十個(gè)點(diǎn)了。
在實(shí)際生活中,如果要以 0.05 的間距在第一幅坐標(biāo)系圖中畫出 x 在 0~1 區(qū)間的一串點(diǎn),最終結(jié)果就和直接畫一條線段沒(méi)啥差別了。
這就是現(xiàn)實(shí)和理想的差別了。理想一串點(diǎn),現(xiàn)實(shí)一條線。
我們把這個(gè)邏輯放到手機(jī)屏幕上。
手機(jī)屏幕上的最小顯示單位就是像素了,一個(gè) 1920 * 1080 的屏幕指的就是各方向上像素點(diǎn)的數(shù)量。
假如繪制一條和屏幕一樣寬的線段,一個(gè)點(diǎn)最小就算一個(gè)像素,最多也就 1080 個(gè)點(diǎn)了。
點(diǎn)占的像素越多,那么實(shí)際繪制時(shí)需要的點(diǎn)的數(shù)量越少,這也算是潛在的優(yōu)化項(xiàng)了。
說(shuō)完直線,再回到貝塞爾曲線上。
曲線和直線都有一個(gè)共同點(diǎn),它們都有各自特定的方程,只不過(guò)我們用的直線例子比較簡(jiǎn)單,既 y = x ,一眼看出計(jì)算結(jié)果。
直線方程 y = x,在數(shù)學(xué)上可以這么描述:y 是關(guān)于 x 的函數(shù),既 y = F(x) ,其中 x 的取值決定了該直線的長(zhǎng)度。
根據(jù)上面的理解,這個(gè)長(zhǎng)度的直線實(shí)際又是由在 x 的取值范圍內(nèi)對(duì)應(yīng)的無(wú)數(shù)個(gè)點(diǎn)組成的。
反觀貝塞爾曲線方程以及對(duì)應(yīng)的圖形如下:
二階貝塞爾曲線:其中,P0 和 P2 是起始點(diǎn),P1 是控制點(diǎn)。
image.png
image.png
三階貝塞爾曲線其中,P0 和 P3 是起始點(diǎn),P1 和 P2 是控制點(diǎn)。
image.png
image.png
不難理解,假設(shè)我們要繪制一條曲線,肯定要有起始和結(jié)束點(diǎn)來(lái)指定曲線的范圍曲線。
而控制點(diǎn)就是指定該曲線的弧度,或者說(shuō)指定該曲線的彎曲走向,不同的控制點(diǎn)得出的曲線繪制結(jié)果是不一樣的。
另外,可以觀察到,無(wú)論是幾階貝塞爾曲線,都會(huì)有參數(shù) t 以及 t 的取值范圍限定。
t 在 0~1 范圍的閉區(qū)間內(nèi),那么 t 的取值個(gè)數(shù)實(shí)際上就有無(wú)數(shù)個(gè)了,這時(shí)的 t 就可以理解成上面介紹直線中講到的 x 。
這樣一來(lái),就可以把起始點(diǎn)、控制點(diǎn)當(dāng)初固定參數(shù),那么貝塞爾曲線計(jì)算公式就成了 B = F(t) ,B 是關(guān)于 t 的函數(shù),而 t 的取值范圍為 0~1 的閉區(qū)間。
也就是說(shuō)貝塞爾曲線,選定了起始點(diǎn)和控制點(diǎn),照樣可以看成是 t 在 0~1 閉區(qū)間內(nèi)對(duì)應(yīng)的無(wú)數(shù)個(gè)點(diǎn)所組成的。
有了上面的闡述,在工(ban)程(zhuan)的角度上,就不難理解貝塞爾曲線到底怎么使用了。
Android 繪制貝塞爾曲線
Android 自帶貝塞爾曲線繪制 API ,通過(guò) Path 類的 quadTo 和 cubicTo 方法就可以完成繪制。
1 // 構(gòu)建 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);
這里的繪制實(shí)際上就是把貝塞爾曲線計(jì)算的方程式交給了 Android 系統(tǒng)內(nèi)部去完成了,參數(shù)傳遞上只傳遞了起始點(diǎn)和控制點(diǎn)。
我們可以通過(guò)自己的代碼來(lái)計(jì)算這個(gè)方程式從而對(duì)邏輯上獲得更多控制權(quán),也就是把曲線拆分成許多個(gè)點(diǎn)組成,如果點(diǎn)的尺寸比較大,甚至可以減少點(diǎn)的個(gè)數(shù)實(shí)現(xiàn)同樣的效果,達(dá)到繪制優(yōu)化的目的。
OpenGL 繪制
通過(guò) OpenGL 可以實(shí)現(xiàn)我們上述的方案,把曲線拆分成多個(gè)點(diǎn)組成。這種方案要求我們?cè)?CPU 上去計(jì)算貝塞爾曲線方程,根據(jù) t 的每一個(gè)取值,計(jì)算出一個(gè)貝塞爾點(diǎn),用 OpenGL 去繪制上這個(gè)點(diǎn)。
這個(gè)點(diǎn)的繪制可以采用 OpenGL 中畫三角形 GL_TRIANGLES 的形式去繪制,這樣就可以給點(diǎn)帶上紋理效果,不過(guò)這里面的坑略多,起始點(diǎn)和控制點(diǎn)都是運(yùn)行時(shí)動(dòng)態(tài)可變的實(shí)現(xiàn)難度會(huì)大于固定不變的。
這里先介紹另一種方案,這種方案實(shí)現(xiàn)比較簡(jiǎn)單也能達(dá)到優(yōu)化效果,我們可以把貝塞爾曲線的計(jì)算方程式交給 GPU, 在 OpenGL Shader 中去完成。
這樣一來(lái),我們只要給定起始點(diǎn)和控制點(diǎn),中間計(jì)算貝塞爾曲線去填補(bǔ)點(diǎn)的過(guò)程就交給 Shader 去完成了。
另外,通過(guò)控制 t 的數(shù)量,我們可以控制貝塞爾點(diǎn)填補(bǔ)的疏密。
t 越大,填補(bǔ)的點(diǎn)越多,超過(guò)一定閾值后,不會(huì)對(duì)繪制效果有提升,反而影響性能。
t 越小,那么貝塞爾曲線就退化成一串點(diǎn)組成了。所以說(shuō) t 的取值范圍也能對(duì)繪制起到優(yōu)化作用。
繪制效果如下圖所示:
image
以下就是實(shí)際的代碼部分了,關(guān)于 OpenGL 的基礎(chǔ)理論部分可以參考之前寫過(guò)的文章和公眾號(hào),就不再闡述了。
在 Shader 中定義一個(gè)函數(shù),實(shí)現(xiàn)貝塞爾方程:
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 中自帶的函數(shù)優(yōu)化一波:
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}
接下來(lái)就是具體的頂點(diǎn)著色器 shader :
1// 對(duì)應(yīng) t 數(shù)據(jù)的傳遞 2attribute float aData; 3// 對(duì)應(yīng)起始點(diǎn)和結(jié)束點(diǎn) 4uniform vec4 uStartEndData; 5// 對(duì)應(yīng)控制點(diǎn) 6uniform vec4 uControlData; 7// mvp 矩陣 8uniform mat4 u_MVPMatrix; 910void main() {11 vec4 pos;12 pos.w = 1.0;13 // 取出起始點(diǎn)、結(jié)束點(diǎn)、控制點(diǎn)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 // 計(jì)算貝塞爾點(diǎn)的函數(shù)調(diào)用21 vec2 point = fun2(p0, p1, p2, p3, t);22 // 定義點(diǎn)的 x,y 坐標(biāo)23 pos.xy = point;24 // 要繪制的位置25 gl_Position = u_MVPMatrix * pos;26 // 定義點(diǎn)的尺寸大小27 gl_PointSize = 20.0;28}
代碼中的 uStartEndData 對(duì)應(yīng)起始點(diǎn)和結(jié)束點(diǎn),uControlData 對(duì)應(yīng)兩個(gè)控制點(diǎn)。
這兩個(gè)變量的數(shù)據(jù)傳遞通過(guò) glUniform4f 方法就好了:
1 mStartEndHandle = glGetUniformLocation(mProgram, "uStartEndData"); 2 mControlHandle = glGetUniformLocation(mProgram, "uControlData"); 3 // 傳遞數(shù)據(jù),作為固定值 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 了,它對(duì)應(yīng)的就是 t 在 0~1 閉區(qū)間的劃分的數(shù)量。
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 }
以上函數(shù)就是把 t 在 0~1 閉區(qū)間分成 Const.NUM_POINTS 份,每一份的值都存在 tData 數(shù)組中,最后通過(guò) glVertexAttribPointer 函數(shù)傳遞給 Shader 。
最后實(shí)際繪制時(shí),我們采用 GL_POINTS 的形式繪制就好了。
1 GLES20.glDrawArrays(GLES20.GL_POINTS, 0, Const.NUM_POINTS );
關(guān)于OpenGL 實(shí)踐中如何進(jìn)行貝塞爾曲線繪制就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到。