與OpenGL ES的第一次約會

最近公司的項目中須要實現一個實時視頻繪製的功能,在相機中根據識別到的人臉點位信息,對指定的點之間繪製出圖案來引導用戶。出於性能的考量,決定採用OpenGL ES來進行圖案的繪製。最終效果以下圖所示:html

本文將從OpenGL的基礎理論開始,由淺入深,直至實現上圖的繪製效果。任何理論都不如現實具體,因此要想真正瞭解一門技術,必須從實際項目應用中去學習和實踐。好了,咱們開始吧!算法

OpenGL ES

OpenGL(Open Graphics Library)是指定義了一個跨編程語言、跨平臺的編程接口規格的專業的圖形程序接口。其主要用於三維圖像的繪製(固然,二維也能夠),是一個功能強大,調用方便的底層圖形庫。而OpenGL ES則是OpenGL針對移動端的輕量級版本,簡化了部分方法和數據類型,好比全部的圖形都是由點、線和三角形組成。編程

咱們知道在iOS中有兩套經常使用的繪圖框架。以下圖所示,分別是UIKit和Core Graphics. 其中UIKit主要是用UIBezierPath來實現圖形的繪製,實際上UIBezierPath是對Core Graphics框架的進一步封裝。而Core Graphics則是使用Quartz2D作引擎,而且和OpenGL ES同樣,在GPU上進行圖形的繪製和渲染。數組

那麼問題來了,既然有這麼多圖形繪製框架,爲何要使用OpenGL呢?在計算機系統中CPU和GPU是協同工做的,CPU準備好顯示數據後提交到GPU進行渲染,GPU渲染後將結果放入幀緩衝區,再通過數模轉換最終由顯示器顯示出圖像內容。因而可知,儘量讓CPU和GPU各司其職發揮做用是提升渲染效率的關鍵。 而OpenGL則讓咱們可以直接訪問GPU,而且引入了緩存的概念來提高圖形渲染的效率。緩存

座標系

首先咱們來看下OpenGL的座標系,以下圖所示,以屏幕中心原點,座標範圍爲-1到1之間。而咱們日常接觸的UIKit的座標則是以屏幕左上角爲原點,座標範圍則爲屏幕寬高。bash

因此若是咱們在屏幕上經過OpenGL繪製圖案就須要將UIKit的座標系轉換到OpenGL座標系(這裏主要討論2D繪圖,所以咱們暫時忽略OpenGL的z軸),座標轉換的公式應該不難總結出來:markdown

繪製流程

OpenGL ES 2.0的渲染流程如圖所示,其中須要咱們控制的爲Vertex Data,Vertex Shader和Fragment Shader這三步。Vertex Data就是咱們傳入的頂點繪製數據,這裏的頂點能夠是表徵點,線或者三角形的數據。Vertex Shader和Fragment Shader這兩步是可編程的,也就是咱們在下面將要見到的.glsl文件。Vertex Shader負責處理每個點的頂點數據,而Fragment Shader則是針對像素數據的,其負責處理每一個像素數據。框架

在OpenGL中,除非加載有效的頂點(Vertex Shader)和片斷(Fragment Shader)着色器,不然不會繪製任何幾何圖形。咱們先來看一個最基本的頂點着色器:編程語言

// vertex.glsl
attribute vec4 position; 
void main(void) {
    gl_Position = position; 
}
複製代碼

第一行聲明瞭一個名爲position的4份量向量,並在main函數裏面賦值給gl_Position變量。這裏的gl_Position就是表明咱們須要處理的頂點,也就是上圖中的Vertex Data數據。函數

在shader中一共有三種變量類型attribute, uniformvarying. 其區別爲:uniform變量是外部程序傳遞給shader的變量;attribute變量只能在vertex shader中使用,爲外部程序傳遞給vertex shader的變量;varying變量則是vertex和fragment shader之間作數據傳遞用的。

咱們接着再來看片斷着色器的一段代碼:

// fragment.glsl
precision mediump float;
void main(void) {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); 
}
複製代碼

第一行是聲明着色器中浮點變量的默認精度。接着在main函數裏面賦值每一個像素的顏色值,這裏咱們賦值vec4(1.0, 0.0, 0.0, 1.0)表明每一個像素點的顏色都是紅色。

基本圖元

使用OpenGL繪製圖形通常都是從繪製一個三角形開始,由於這個過程包括了OpenGL ES的三種基本元素: 點,線和三角。在OpenGL中,任何複雜的三維模型都是由這三個基本的幾何圖元組成的。

編譯着色器

頂點和像素的處理都是在shader中實現的,因此咱們要想使用shader就須要在運行時動態編譯源碼以獲得一個着色器對象。幸運的是,編譯shader的流程是固定的,並且已經有不少現成的開源代碼實現。其大概步驟以下所示:

首先是編譯shader的代碼,其中pathvertex.glsl或者vertex.glsl文件的存放路徑,而type則是用來區分shader的種類,即Vertex Shader或者Fragment Shader着色器。

- (GLuint)compileShader:(NSString *)path type:(GLenum)type source:(GLchar *)source
{
    NSError *error          = nil;
    NSString *shaderContent = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error];
    
    if (!shaderContent) NSLog(@"%@", error.localizedDescription);
    
    const char *shaderUTF8 = [shaderContent UTF8String];
    GLint length           = (GLint)[shaderContent length];
    GLuint shader          = glCreateShader(type);
    
    glShaderSource(shader, 1, &shaderUTF8, &length);
    
    glCompileShader(shader);
    
    GLint status;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
    
    if (status == GL_FALSE) { glDeleteShader(shader); exit(1); }
    
    return shader;
}
複製代碼

如今咱們有了編譯以後的shader對象,接下來須要把它連接到OpenGL的glProgram上,讓它能夠在GPU上run起來。代碼以下所示:

program = glCreateProgram();

glAttachShader(program, vertShader);
glAttachShader(program, fragShader);

glLinkProgram(program);
    
GLint status;
glGetProgramiv(program, GL_LINK_STATUS, &status);
複製代碼

完成上面的步驟後,咱們就能夠用programe來和shader交互了,好比賦值給頂點shader的position變量:

GLuint attrib_position = glGetAttribLocation(program, "position");
glEnableVertexAttribArray(attrib_position);
glVertexAttribPointer(attrib_position, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (char *)points);
複製代碼

幾何圖元

有了上面的介紹,咱們就能夠開始繪圖了。全部幾何圖元的繪製都是經過調用glDrawArrays實現的:

glDrawArrays (GLenum mode, GLint first, GLsizei count);
複製代碼

這裏的mode爲幾何形狀類型,主要有點,線和三角形三種:

#define GL_POINTS 0x0000 // 點 -> 默認爲方形
#define GL_LINES 0x0001 // 線段 -> 可不連續
#define GL_LINE_LOOP 0x0002 // 線圈 -> 首尾相連的線段
#define GL_LINE_STRIP 0x0003 // 線段帶 -> 相鄰線段共享頂點
#define GL_TRIANGLES 0x0004 // 三角形 -> 三個頂點鏈接
#define GL_TRIANGLE_STRIP 0x0005 // 三角帶 -> 相鄰三角共享邊
#define GL_TRIANGLE_FAN 0x0006 // 三角扇 -> 全部三角共享頂點
複製代碼

繪製點代碼以下所示,其中幾何類型傳入GL_POINTS

static GLfloat points[] = { // 前三位表示位置x, y, z 後三位表示顏色值r, g, b 
    0.0f, 0.5f, 0, 0, 0, 0, // 位置爲( 0.0, 0.5, 0.0); 顏色爲(0, 0, 0)黑色
   -0.5f, 0.0f, 0, 1, 0, 0, // 位置爲(-0.5, 0.0, 0.0); 顏色爲(1, 0, 0)紅色 
    0.5f, 0.0f, 0, 1, 0, 0  // 位置爲( 0.5, 0.0, 0.0); 顏色爲(1, 0, 0)紅色 
}; // 共有三組數據,表示三個點

GLuint attrib_position = glGetAttribLocation(program, "position");
glEnableVertexAttribArray(attrib_position);
GLuint attrib_color    = glGetAttribLocation(program, "color");
glEnableVertexAttribArray(attrib_color);

// 對於position每一個數值包含3個份量,即3個byte,兩組數據間間隔6個GLfloat
// 一樣,對於color每一個數值含3個份量,但數據開始的指針位置爲跳過3個position的GLFloat大小
glVertexAttribPointer(attrib_position, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (char *)points);
glVertexAttribPointer(attrib_color, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (char *)points + 3 * sizeof(GLfloat));
 
glDrawArrays(GL_POINTS, 0, 3); 
複製代碼

效果如圖所示:

能夠看到繪製出來的點默認爲方點,那若是要繪製圓點呢?爲了讓OpenGL ES 2.0把點繪製成圓形而非矩形,須要處理光柵化後的點所包含的像素數據,思路是,忽略半徑大於0.5的點,從而實現圓點繪製。在FragmentShader.glsl修改代碼以下:

// FragmentShader.glsl
varying lowp vec4 fragColor;

void main(void) {
    if (length(gl_PointCoord - vec2(0.5, 0.5)) > 0.5) {
        discard;
    }
    gl_FragColor = fragColor;
}
複製代碼

運行後,能夠看到圓點效果以下所示:

繪製直線的代碼以下所示,其中幾何類型傳入GL_LINES

static GLfloat lines[] = { 
    0.0f, 0.0f, 1, 1, 1, 1,
    0.5f, 0.5f, 0, 0, 0, 0,
    0.0f, 0.0f, 0, 1, 0, 0,
   -0.5f, 0.0f, 0, 0, 0, 1,
};

glVertexAttribPointer(attrib_position, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (char *)lines);
glVertexAttribPointer(attrib_color, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (char *)lines + 3 * sizeof(GLfloat));
 
glLineWidth(5); // 設置線寬爲5
glDrawArrays(GL_LINES, 0, 4); 
複製代碼

對於線段,若是兩點之間的顏色值不一樣,則OpenGL會默認產生漸變色效果,具體繪製結果如圖所示:

因爲本文最開始的效果裏面只用到了點和線的繪製,因此繪製最基本的三角形,讀者能夠自行嘗試,這邊就再也不贅述了。

紋理貼圖

除了圖元以外,OpenGL還有紋理的概念。簡單來講就是把圖像數據顯示到咱們所繪製的圖元上,以使圖元表示的物體更真實。咱們首先來看下紋理的座標系,以下圖所示:

紋理座標的範圍爲0到1之間。紋理座標的原點爲圖片的左下角,其和OpenGL繪製座標系的對應關係如示意圖上箭頭所示,在紋理貼圖的時候咱們須要確保座標點映射關係與上圖一致。

要實現紋理的繪製須要兩個信息,一個是紋理的座標,另外一個則是紋理的內容。紋理的內容簡單來講,就是把iOS中的UIImage轉換爲OpenGL ES中的texture數據。

- (GLuint)textureFromImage:(UIImage *)image 
{
    CGImageRef imageRef = [image CGImage];
    size_t w = CGImageGetWidth (imageRef);
    size_t h = CGImageGetHeight(imageRef);
    
    GLubyte *textureData        = (GLubyte *)malloc(w * h * 4);
    CGColorSpaceRef colorSpace  = CGColorSpaceCreateDeviceRGB();
    
    NSUInteger bytesPerPixel    = 4;
    NSUInteger bytesPerRow      = bytesPerPixel * w;
    NSUInteger bitsPerComponent = 8;
    
    CGContextRef context = CGBitmapContextCreate(textureData,
                                                 w,
                                                 h,
                                                 bitsPerComponent, 
                                                 bytesPerRow, 
                                                 colorSpace,
                                                 kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
    CGContextTranslateCTM(context, 0, h);
    CGContextScaleCTM(context, 1.0f, -1.0f);
    CGContextDrawImage(context, CGRectMake(0, 0, w, h), imageRef);
    
    glEnable(GL_TEXTURE_2D);
    GLuint texName;
    glGenTextures(1, &texName);
    glBindTexture(GL_TEXTURE_2D, texName);
    
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    
    glTexImage2D(GL_TEXTURE_2D, 
                 0, 
                 GL_RGBA, 
                 (GLsizei)w, 
                 (GLsizei)h, 
                 0,
                 GL_RGBA, 
                 GL_UNSIGNED_BYTE, 
                 textureData);
    
    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);
    free(textureData);
    
    return texName;
}
複製代碼

有了紋理對象後,接下來咱們須要在頂點着色器和片斷着色器中轉化座標和紋理信息,也就是進行採樣渲染。頂點着色器以下所示:

// vertex.glsl
attribute vec4 aPosition; 
attribute vec2 aTexcoord;
varying   vec2 vTexcoord;
void main(void) {
    gl_Position = aPosition; 
    vTexcoord   = aTexcoord;
}
複製代碼

上述代碼中的aTexcoord用來接受紋理座標信息,而後傳遞給片斷着色器中定義的varying變量vTexcoord。這樣就傳遞了紋理座標信息。片斷着色器代碼以下所示:

// fragment.glsl
precision mediump   float;
uniform   sampler2D uTexture;
varying   vec2      vTexcoord;
void main(void) {
    gl_FragColor = texture2D(uTexture, vTexcoord);
}
複製代碼

這裏的uTexture就是咱們的紋理,而vTexcoord則是紋理座標。有了座標和紋理信息後就能夠經過texture2D函數進行採樣。簡單來講,就是取出每一個座標點像素的顏色信息賦給OpenGL進行繪製,而圖片的數據就是由每一個點的顏色像素值所組成的矩陣信息,所以,有了紋理和像素間的顏色映射關係後,就能夠經過OpenGL顯示整張圖片了。完成了上述操做以後,最後一步就是激活紋理並渲染了,代碼以下所示:

GLuint tex_name = [self textureFromImage:[UIImage imageNamed:@"ryan.jpg"]];

glActiveTexture(GL_TEXTURE5);
glBindTexture(GL_TEXTURE_2D, tex_name);
glUniform1i(uTexture, 5);

const GLfloat vertices[] = { // OpenGL繪製座標
    -0.5, -0.25, 0,   
     0.5, -0.25, 0,   
    -0.5,  0.25, 0,   
     0.5,  0.25, 0 }; 
glEnableVertexAttribArray(aPosition);
glVertexAttribPointer(aPosition, 3, GL_FLOAT, GL_FALSE, 0, vertices);

static const GLfloat coords[] = { // 紋理座標
    0, 0,
    1, 0,
    0, 1,
    1, 1
};

glEnableVertexAttribArray(aTexcoord);
glVertexAttribPointer(aTexcoord, 2, GL_FLOAT, GL_FALSE, 0, coords);

glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
複製代碼

代碼中的vertices爲OpenGL的繪製座標,紋理座標爲coords, 這兩個座標須要與上圖的座標對應關係相符合才能正確顯示出圖片。運行後效果以下圖所示:

視頻繪製

好了,有了上面的理論基礎,咱們能夠來實現文章開篇所示的實時視頻繪製了。對於視頻流的獲取以及OpenGL的繪製環境咱們採用GPUImage來實現,人臉識別的算法採用公司自有視覺引擎(免費開放使用,下載地址爲虹軟視覺AI引擎開放平臺)固然也可使用CoreImage框架的CIDetector人臉識別類。

@interface PVTStickerFilter : GPUImageFilter

@property (nonatomic, copy) NSArray<NSValue *> *facePoints;

@end
複製代碼

首先繼承GPUImageFilter類,並定義一我的臉點位數組用來接收人臉識別引擎傳入的點位信息。須要注意的是,相機獲取的圖像默認在內存中是逆時針90度存放的,因此咱們獲取的點位須要順時針旋轉90度纔是咱們在取景框中看到的圖像。另外,若是是前置攝像頭,默認會有鏡像效果,所以還須要將點位沿Y軸翻轉180度。

[self.facePoints enumerateObjectsUsingBlock:^(NSValue *obj, NSUInteger idx, BOOL *stop) {
    CGPoint point = [obj CGPointValue];
    [mPs addObject:[NSValue valueWithCGPoint:CGPointMake(point.y, point.x)]];
}];
複製代碼

對於某個點(x, y)順時針旋轉90度後坐標爲(imageHeight - y, x), 若是是鏡像效果的點,則還須要再繞Y軸旋轉180度,最終的座標爲(y, x)

從效果圖中能夠看到,咱們要實現的爲左右兩邊對稱線條的動畫繪製。效果圖中一共繪製了三組線條,咱們就其中一組來分析下其原理。具體點位爲鼻樑左下角點(x67, y67)到眉毛左內側點(x24, y24)的線段繪製,以及鼻樑右下角點(x70, y70)到眉毛右內側點(x29, y29)的線段繪製。同時(x24, y24)(x29, y29)在動畫的最後還須要顯示圓點。

根據前文的分析,在繪製點位以前咱們還須要把視頻圖像幀的座標轉換爲OpenGL的座標系,也就是把上面幾個點位的座標轉換到-1到1之間。轉換公式前文已給出:

CGFloat x67 = 2 * [mPs[67] CGPointValue].x / frameWidth - 1.f;
CGFloat y67 = 1 - 2 * [mPs[67] CGPointValue].y / frameHeight ;

CGFloat x24 = 2 * [mPs[24] CGPointValue].x / frameWidth - 1.f;
CGFloat y24 = 1 - 2 * [mPs[24] CGPointValue].y / frameHeight ;

CGFloat x70 = 2 * [mPs[70] CGPointValue].x / frameWidth - 1.f;
CGFloat y70 = 1 - 2 * [mPs[70] CGPointValue].y / frameHeight ;

CGFloat x29 = 2 * [mPs[29] CGPointValue].x / frameWidth - 1.f;
CGFloat y29 = 1 - 2 * [mPs[29] CGPointValue].y / frameHeight ;
複製代碼

有了這些點位,咱們能夠很容易的使用glDrawArrays(GL_LINES, 0, 4)來繪製出線段。可是這邊有兩個問題須要解決,一是如何繪製虛線,二是如何實現繪製的動畫。

對於虛線的繪製,OpenGL ES 2.0沒有直接的API能夠實現,因此咱們須要換一種思路,將虛線轉換爲若干直線的連續繪製。具體思路爲,一個長度爲10像素的虛線(x1, 0)(x10, 0),咱們將它切斷爲5個長度爲1像素線段繪製。即繪製(x1, 0)(x2, 0)的線段,(x3, 0)(x4, 0)的線段,(x5, 0)(x6, 0)的線段,(x7, 0)(x8, 0)的線段,(x9, 0)(x10, 0)的線段。

因此,首先咱們須要根據繪製虛線的長度來給整條線段分段,好比咱們定義每段虛線的長度爲0.01,那麼就能夠計算出來兩個點位之間的線段須要分爲多少片斷線來繪製:

CGFloat w_24_67 = (x24 - x67); // 兩點之間的x軸距離
CGFloat h_24_67 = (y24 - y67); // 兩點之間的y軸距離

CGFloat w_29_70 = (x29 - x70); // 兩點之間的x軸距離
CGFloat h_29_70 = (y29 - y70); // 兩點之間的y軸距離

GLsizei s_24_67 = [self stepsOfLineWidth:w_24_67 height:h_24_67]; // 須要劃分爲多少個片斷線
GLsizei s_29_70 = [self stepsOfLineWidth:w_29_70 height:h_29_70]; // 須要劃分爲多少個片斷線
複製代碼

計算片斷性的函數以下所示,其中PVT_DASH_LENGTH爲每段虛線的長度:

- (GLsizei)stepsOfLineWidth:(CGFloat)w height:(CGFloat)h
{
    CGFloat a_w = fabs(w);
    CGFloat a_h = fabs(h);
    GLsizei s   = a_w / (PVT_DASH_LENGTH * cos(atan(a_h / a_w)));
    
    return ((s % 2) ? s : ++s) + 1;
}
複製代碼

而後將全部的線段片塞到OpenGL中繪製,代碼以下:

GLsizei total_s = s_24_67 + s_29_70;
GLfloat *lines  = (GLfloat *)malloc(sizeof(GLfloat) * total_s * 3);

for (int i = 0; i < s_24_67; i++) {
    CGFloat xt = x67 + (CGFloat)i/(CGFloat)(s_24_67-1) * w_24_67;
    CGFloat yt = y67 + (CGFloat)i/(CGFloat)(s_24_67-1) * h_24_67;
    int   idx  = i * 3;
    lines[idx] = xt; lines[idx+1] = yt; lines[idx+2] = 0;
}
for (int i = 0; i < s_29_70; i++) {
    CGFloat xt = x70 + (CGFloat)i/(CGFloat)(s_29_70-1) * w_29_70;
    CGFloat yt = y70 + (CGFloat)i/(CGFloat)(s_29_70-1) * h_29_70;
    int   idx  = s_24_67 * 3 + i * 3;
    lines[idx] = xt; lines[idx+1] = yt; lines[idx+2] = 0;
}

glVertexAttribPointer(_position, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (char *)lines);
glLineWidth(2.5);
glDrawArrays(GL_LINES, 0, total_s);
複製代碼

好了,虛線的問題咱們解決了,咱們再來看看如何實現繪製的動畫。其實思路很簡單,好比咱們要在4秒內逐步繪製出線段(因爲須要繪製虛線,咱們分紅了100個線段片),那麼,咱們在相機每幀數據回調來的時候判斷下當前幀距離第一幀已經間隔了屢次時間,假設間隔了1秒,那就是對於這一幀圖像咱們須要繪製出四分之一的長度,也就是將25個線段片塞到OpenGL裏面去繪製。以此類推,若是超過了4秒,那麼再清零重頭計算。在4秒的時候應該是繪製整條線段的完整長度。

- (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex
{
    _currentTime = frameTime;

    [super newFrameReadyAtTime:frameTime atIndex:textureIndex];
}
複製代碼

首先記錄下當前幀的時間,以便在後面計算當前幀距離第一幀的累積時間。

- (void)calcAccumulatorTime
{
    NSTimeInterval interval = 0;
    
    if (CMTIME_IS_VALID(_lastTime)) {
        interval = CMTimeGetSeconds(CMTimeSubtract(_currentTime, _lastTime));
    }
    _lastTime       = _currentTime;
    _accumulator   += interval;
    
    _frameDuration  = _stepsIdx == 3 ? PVT_FRAME_DURATION / 2.f : PVT_FRAME_DURATION;
    
    CGFloat sumTime = _accumulator + interval;
    _accumulator    = MIN(sumTime, _frameDuration);
}
複製代碼

而後計算出當前幀根據總的動畫時間應該繪製到哪一步:

- (GLsizei)animationIdxWithStep:(GLsizei)step
{
    CGFloat s_scale = _accumulator / _frameDuration;
    GLsizei s_index = ceil(s_scale * step);
    
    return (s_index % 2) ? ++s_index : s_index;
}
複製代碼

最後一步則是將計算好的片斷數傳給OpenGL進行繪製,須要注意的時候當累積時間超過了動畫時間後須要將累積時間清零,從而實現動畫的連續展現。這裏的_frameDuration便是動畫時間。

- (void)renderToTextureWithVertices:(const GLfloat *)vertices textureCoordinates:(const GLfloat *)textureCoordinates;
{
    [self calcAccumulatorTime];

    GLsizei s_24_67_index = [self animationIdxWithStep:s_24_67];
    GLsizei s_29_70_index = [self animationIdxWithStep:s_29_70];

    GLsizei total_s = s_24_67_index + s_29_70_index;
    GLfloat *lines  = (GLfloat *)malloc(sizeof(GLfloat) * total_s * 3);
    
    for (int i = 0; i < s_24_67_index; i++) {
        CGFloat xt = x67 + (CGFloat)i/(CGFloat)(s_24_67_index-1) * w_24_67 * s_index_scale;
        CGFloat yt = y67 + (CGFloat)i/(CGFloat)(s_24_67_index-1) * h_24_67 * s_index_scale;
        int   idx  = i * 3;
        lines[idx] = xt; lines[idx+1] = yt; lines[idx+2] = 0;
    }
    for (int i = 0; i < s_29_70_index; i++) {
        CGFloat xt = x70 + (CGFloat)i/(CGFloat)(s_29_70_index-1) * w_29_70 * s_index_scale;
        CGFloat yt = y70 + (CGFloat)i/(CGFloat)(s_29_70_index-1) * h_29_70 * s_index_scale;
        int   idx  = s_24_67_index * 3 + i * 3;
        lines[idx] = xt; lines[idx+1] = yt; lines[idx+2] = 0;
    }
    
    if (_accumulator == _frameDuration) {
        _accumulator = 0.f;
    }
    
    // to do drawing work...
}
複製代碼

虛線和動畫的問題都解決了,如今還剩最後一個需求,在動畫結束的時候在(x24, y24)和(x29, y29)處繪製圓點。對於圓點的繪製,前文有提到能夠直接繪製點,而後在FragmentShader.glsl中修改忽略半徑大於0.5的便可實現圓點繪製。可是因爲咱們須要同時繪製點和線,且使用同一個Fragment Shader文件,因此難以區分當前是繪製點仍是線,不能直接在Shader中忽略半徑大於0.5的點,所以咱們這邊對於圓點直接採用幾何方法繪製。具體的幾何原理能夠參照這篇博文。

#define PVT_CIRCLE_SLICES 100
#define PVT_CIRCLE_RADIUS 0.015

- (void)drawCircleWithPositionX:(CGFloat)x y:(CGFloat)y radio:(CGFloat)radio
{
    glLineWidth(2.0);
    
    GLfloat *vertext = (GLfloat *)malloc(sizeof(GLfloat) * PVT_CIRCLE_SLICES * 3);
    
    memset(vertext, 0x00, sizeof(GLfloat) * PVT_CIRCLE_SLICES * 3);
    
    float a     = PVT_CIRCLE_RADIUS; // horizontal radius
    float b     = a * radio;         // fWidth / fHeight;
    
    float delta = 2.0 * M_PI / PVT_CIRCLE_SLICES;
    
    for (int i = 0; i < PVT_CIRCLE_SLICES; i++) {
        GLfloat cx   = a * cos(delta * i) + x;
        GLfloat cy   = b * sin(delta * i) + y;
        int   idx    = i * 3;
        vertext[idx] = cx; vertext[idx+1] = cy; vertext[idx+2] = 0;
    }
    
    glVertexAttribPointer(_position, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (char *)vertext);
    glDrawArrays(GL_TRIANGLE_FAN, 0, PVT_CIRCLE_SLICES);
    
    free(vertext);
}
複製代碼

OpenGL ES的深度不亞於學習一門新語言,萬丈高樓平地起,但願本文的總結能夠給想入門的同窗帶來一些幫助和收穫,也歡迎你們留言討論。

參考文章

  1. OpenGL ES入門及繪製一個三角形
  2. 仿QQ視屏動畫特效-人臉識別
  3. 從0打造一個GPUImage
  4. 學習OpenGL ES之繪製更多的圖形
  5. OpenGL ES 3.0 數據可視化 1:繪製圓點
  6. OpenGL ES入門03-OpenGL ES圓形繪製
  7. OpenGL ES入門05-OpenGL ES 紋理貼圖
相關文章
相關標籤/搜索