天天看點

android opengl 貝塞爾曲線,OpenGL ES 繪制貝塞爾曲線

該原創文章首發于微信公衆号:位元組流動

android opengl 貝塞爾曲線,OpenGL ES 繪制貝塞爾曲線

基于貝塞爾曲線的曲邊扇形

最近要求為圖像設計流線型曲線邊框,想着可以用 OpenGL 繪制貝塞爾曲線,再加上模闆測試來實作,趁機嘗試一波。

什麼是貝塞爾曲線

android opengl 貝塞爾曲線,OpenGL ES 繪制貝塞爾曲線

運用貝塞爾曲線設計的汽車車身

貝塞爾曲線于 1962 年,由法國工程師皮埃爾·貝濟埃(Pierre Bézier)所廣泛發表,他運用貝塞爾曲線來為汽車的主體進行設計,可以設計出曲線形車身。

貝塞爾曲線主要用于二維圖形應用程式中的數學曲線,曲線主要由起始點,終止點和控制點組成,通過調整控制點,繪制的貝塞爾曲線形狀則會随之發生變化。貝塞爾曲線現在已廣泛用于計算機圖形,動畫,字型等,基本上每個現代圖形編輯器都支援它。

在一些部落格中比較常見的一階、二階和三階貝塞爾曲線( 公式中 t∈[0,1]):

一階貝塞爾曲線

android opengl 貝塞爾曲線,OpenGL ES 繪制貝塞爾曲線

一階貝塞爾曲線公式

android opengl 貝塞爾曲線,OpenGL ES 繪制貝塞爾曲線

一階貝塞爾曲線

二階貝塞爾曲線

android opengl 貝塞爾曲線,OpenGL ES 繪制貝塞爾曲線

二階貝塞爾曲線公式

android opengl 貝塞爾曲線,OpenGL ES 繪制貝塞爾曲線

二階貝塞爾曲線

三階貝塞爾曲線

android opengl 貝塞爾曲線,OpenGL ES 繪制貝塞爾曲線

三階貝塞爾曲線公式

android opengl 貝塞爾曲線,OpenGL ES 繪制貝塞爾曲線

三階貝塞爾曲線

通過上述公式,我們設定好起始點,終止點和控制點,貝塞爾曲線就是由 t∈[0,1] 區間對應的無數個點組成。

當然我們實際在裝置上繪制時,不可能繪制出無數個點,一般是根據螢幕像素的大小,對 t∈[0,1] 區間進行适當的等間隔插值,再由輸出的點組成我們要的貝塞爾曲線(此時肉眼分辨不出來兩點之間的距離,可以認為它們連成了一條線)。

Android Canvas 繪制貝塞爾曲線

Android 自定義 View 時,我們知道 Canvas 類有專門的 API 可以很友善地繪制貝塞爾曲線,但是通常性能較差,更不友善與圖像一起處理,因為本文的目的是利用貝塞爾曲線處理圖像。

path.reset();

path.moveTo(p0x, p0y);//設定起點

path.quadTo(p1x, p1y, p2x, p2y);//設定控制點

path.moveTo(p0x, p0y);//設定終止點

path.close();

canvas.drawPath(path, paint);

OpenGL ES 繪制貝塞爾曲線

OpenGL ES 的基本繪制機關是點、線和三角形,既然可以繪制點,隻需要基于上述公式計算出點,然後将其繪制出來,即可得到我們想要的貝塞爾曲線。

以繪制三階貝塞爾曲線為例,用 GLSL 實作該函數,然後我們從外部輸入一組 t 的取值數組,便可以得出一組對應的用于繪制三階貝塞爾曲線的點。

vec2 bezier_3order(in vec2 p0, in vec2 p1, in vec2 p2, in vec2 p3, in float t){

float tt = (1.0 - t) * (1.0 -t);

return tt * (1.0 -t) *p0 + 3.0 * t * tt * p1 + 3.0 * t *t *(1.0 -t) *p2 + t *t *t *p3;

}

借助于 GLSL 的内置混合函數 mix ,我們可以在用于繪制貝塞爾曲線的點之間進行插值,相當于對上述函數 bezier_3order 進行優化:

vec2 bezier_3order_mix(in vec2 p0, in vec2 p1, in vec2 p2, in vec2 p3, in float t)

{

vec2 q0 = mix(p0, p1, t);

vec2 q1 = mix(p1, p2, t);

vec2 q2 = mix(p2, p3, t);

vec2 r0 = mix(q0, q1, t);

vec2 r1 = mix(q1, q2, t);

return mix(r0, r1, t);

}

擷取 t 的取值數組,實際上就是對 t∈[0,1] 區間進行等間隔取值:

#define POINTS_NUM 256 //取 256 個點

#define POINTS_PRE_TRIANGLES 3

int tDataSize = POINTS_NUM * POINTS_PRE_TRIANGLES;

float *p_tData = new float[tDataSize];

for (int i = 0; i < tDataSize; i += POINTS_PRE_TRIANGLES) {

float t0 = (float) i / tDataSize;

float t1 = (float) (i + 1) / tDataSize;

float t2 = (float) (i + 2) / tDataSize;

p_tData[i] = t0;

p_tData[i + 1] = t1;

p_tData[i + 2] = t2;

}

完整的着色器腳本:

//頂點着色器

#version 300 es

layout(location = 0) in float a_tData;//t 取值數組

uniform vec4 u_StartEndData;//起始點和終止點

uniform vec4 u_ControlData;//控制點

uniform mat4 u_MVPMatrix;

uniform float u_Offset;//y軸方向做一個動态偏移

vec2 bezier_3order_mix(in vec2 p0, in vec2 p1, in vec2 p2, in vec2 p3, in float t)

{

vec2 q0 = mix(p0, p1, t);

vec2 q1 = mix(p1, p2, t);

vec2 q2 = mix(p2, p3, t);

vec2 r0 = mix(q0, q1, t);

vec2 r1 = mix(q1, q2, t);

return mix(r0, r1, t);

}

void main() {

vec4 pos;

pos.w = 1.0;

vec2 p0 = u_StartEndData.xy;

vec2 p3 = u_StartEndData.zw;

vec2 p1 = u_ControlData.xy;

vec2 p2 = u_ControlData.zw;

p0.y *= u_Offset;

p1.y *= u_Offset;

p2.y *= u_Offset;

p3.y *= u_Offset;

float t = a_tData;

vec2 point = fun2(p0, p1, p2, p3, t);

if (t < 0.0) //用于繪制三角形的時候起作用,類似于繪制扇形

{

pos.xy = vec2(0.0, 0.0);

}

else

{

pos.xy = point;

}

gl_PointSize = 4.0f;//設定點的大小

gl_Position = u_MVPMatrix * pos;

}

//片段着色器

#version 300 es

precision mediump float;

layout(location = 0) out vec4 outColor;

uniform vec4 u_Color;//設定繪制三角形或者點的顔色

void main()

{

outColor = u_Color;

}

繪制貝塞爾曲線:

GLUtils::setMat4(m_ProgramObj, "u_MVPMatrix", m_MVPMatrix);

GLUtils::setVec4(m_ProgramObj, "u_StartEndData", glm::vec4(-1, 0,

1, 0));

GLUtils::setVec4(m_ProgramObj, "u_ControlData", glm::vec4(-0.04f, 0.99f,

0.0f, 0.99f));

GLUtils::setVec4(m_ProgramObj, "u_Color", glm::vec4(1.0f, 0.3f, 0.0f, 1.0f));

float offset = (m_FrameIndex % 100) * 1.0f / 100;

offset = (m_FrameIndex / 100) % 2 == 1 ? (1 - offset) : offset;

GLUtils::setFloat(m_ProgramObj, "u_Offset", offset);

glDrawArrays(GL_POINTS, 0, POINTS_NUM * TRIANGLES_PER_POINT);

//旋轉 180 度後再繪制一條

UpdateMVPMatrix(m_MVPMatrix, 180, m_AngleY, (float) screenW / screenH);

GLUtils::setMat4(m_ProgramObj, "u_MVPMatrix", m_MVPMatrix);

glDrawArrays(GL_POINTS, 0, POINTS_NUM * TRIANGLES_PER_POINT);

繪制的貝塞爾曲線:

android opengl 貝塞爾曲線,OpenGL ES 繪制貝塞爾曲線

繪制多條貝塞爾曲線

接下來我們基于貝塞爾曲線去繪制曲邊扇形(填充曲線與 x 軸之間的區域),則需要 OpenGL 繪制三角形實作,還要重新輸入 t 的取值數組,使得每輸出 3 個點包含一個原點,類似于繪制扇形。

//繪制三角形,要重新輸入 t 的取值數組,使得每輸出 3 個點包含一個原點,前面着色器中 t<0 時輸出原點。

int tDataSize = POINTS_NUM * POINTS_PRE_TRIANGLES;

float *p_tData = new float[tDataSize];

for (int i = 0; i < tDataSize; i += POINTS_PRE_TRIANGLES) {

float t = (float) i / tDataSize;

float t1 = (float) (i + 3) / tDataSize;

p_tData[i] = t;

p_tData[i + 1] = t1;

p_tData[i + 2] = -1;

}

繪制曲邊扇形隻需要改變繪制模式,GL_POINTS 改為 GL_TRIANGLES 。

glDrawArrays(GL_TRIANGLES, 0, POINTS_NUM * POINTS_PRE_TRIANGLES);

當繪制多個曲邊扇形互相疊加時,可以通過混合去産生新的顔色(參看本文的第一副圖),防止最先繪制的曲邊扇形被覆寫,了解 OpenGLES 混合可以參考舊文Android OpenGL ES 3.0 開發(十二):混合。

glEnable(GL_BLEND);

glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_COLOR, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); // Screen blend mode

glBlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD);

參考

聯系與交流

技術交流可以添加我的微信:Byte-Flow