OpenGL ES 繪製貝塞爾曲線

該原創文章首發於微信公衆號:字節流動

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

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

什麼是貝塞爾曲線

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

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

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

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

一階貝塞爾曲線
一階貝塞爾曲線公式
一階貝塞爾曲線

二階貝塞爾曲線
二階貝塞爾曲線公式
二階貝塞爾曲線

三階貝塞爾曲線
三階貝塞爾曲線公式

三階貝塞爾曲線

通過上述公式,我們設置好起始點,終止點和控制點,貝塞爾曲線就是由 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);

繪製的貝塞爾曲線:
繪製多條貝塞爾曲線

接下來我們基於貝塞爾曲線去繪製曲邊扇形(填充曲線與 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);

實現代碼路徑:
Android_OpenGLES_3_0

參考

Sound Visualization on Android: Drawing a Cubic Bezier with OpenGL ES
https://glumes.com/post/opengl/opengl-draw-bezier-line/

聯繫與交流

我的公衆號

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章