與OpenGL ES的第一次約會(huì)

最近公司的項(xiàng)目中需要實(shí)現(xiàn)一個(gè)實(shí)時(shí)視頻繪制的功能环凿,在相機(jī)中根據(jù)識(shí)別到的人臉點(diǎn)位信息揍瑟,對(duì)指定的點(diǎn)之間繪制出圖案來(lái)引導(dǎo)用戶药蜻。出于性能的考量正压,決定采用OpenGL ES來(lái)進(jìn)行圖案的繪制欣福。最終效果如下圖所示:

image

本文將從OpenGL的基礎(chǔ)理論開(kāi)始,由淺入深蔑匣,直至實(shí)現(xiàn)上圖的繪制效果劣欢。任何理論都不如現(xiàn)實(shí)具體棕诵,所以要想真正了解一門(mén)技術(shù),必須從實(shí)際項(xiàng)目應(yīng)用中去學(xué)習(xí)和實(shí)踐凿将。好了校套,我們開(kāi)始吧!

OpenGL ES

OpenGL(Open Graphics Library)是指定義了一個(gè)跨編程語(yǔ)言牧抵、跨平臺(tái)的編程接口規(guī)格的專業(yè)的圖形程序接口笛匙。其主要用于三維圖像的繪制(當(dāng)然,二維也可以)犀变,是一個(gè)功能強(qiáng)大妹孙,調(diào)用方便的底層圖形庫(kù)。而OpenGL ES則是OpenGL針對(duì)移動(dòng)端的輕量級(jí)版本获枝,簡(jiǎn)化了部分方法和數(shù)據(jù)類型蠢正,比如所有的圖形都是由點(diǎn)、線和三角形組成省店。

我們知道在iOS中有兩套常用的繪圖框架嚣崭。如下圖所示,分別是UIKit和Core Graphics. 其中UIKit主要是用UIBezierPath來(lái)實(shí)現(xiàn)圖形的繪制懦傍,實(shí)際上UIBezierPath是對(duì)Core Graphics框架的進(jìn)一步封裝雹舀。而Core Graphics則是使用Quartz2D做引擎,并且和OpenGL ES一樣粗俱,在GPU上進(jìn)行圖形的繪制和渲染说榆。

image

那么問(wèn)題來(lái)了,既然有這么多圖形繪制框架寸认,為什么要使用OpenGL呢签财?在計(jì)算機(jī)系統(tǒng)中CPU和GPU是協(xié)同工作的,CPU準(zhǔn)備好顯示數(shù)據(jù)后提交到GPU進(jìn)行渲染偏塞,GPU渲染后將結(jié)果放入幀緩沖區(qū)荠卷,再經(jīng)過(guò)數(shù)模轉(zhuǎn)換最終由顯示器顯示出圖像內(nèi)容。由此可見(jiàn)烛愧,盡可能讓CPU和GPU各司其職發(fā)揮作用是提高渲染效率的關(guān)鍵。
而OpenGL則讓我們能夠直接訪問(wèn)GPU掂碱,并且引入了緩存的概念來(lái)提升圖形渲染的效率怜姿。

坐標(biāo)系

首先我們來(lái)看下OpenGL的坐標(biāo)系,如下圖所示疼燥,以屏幕中心原點(diǎn)沧卢,坐標(biāo)范圍為-1到1之間。而我們平常接觸的UIKit的坐標(biāo)則是以屏幕左上角為原點(diǎn)醉者,坐標(biāo)范圍則為屏幕寬高但狭。

image

所以如果我們?cè)谄聊簧贤ㄟ^(guò)OpenGL繪制圖案就需要將UIKit的坐標(biāo)系轉(zhuǎn)換到OpenGL坐標(biāo)系(這里主要討論2D繪圖披诗,因此我們暫時(shí)忽略O(shè)penGL的z軸),坐標(biāo)轉(zhuǎn)換的公式應(yīng)該不難總結(jié)出來(lái):

image

繪制流程

OpenGL ES 2.0的渲染流程如圖所示立磁,其中需要我們控制的為Vertex Data呈队,Vertex Shader和Fragment Shader這三步。Vertex Data就是我們傳入的頂點(diǎn)繪制數(shù)據(jù)唱歧,這里的頂點(diǎn)可以是表征點(diǎn)宪摧,線或者三角形的數(shù)據(jù)。Vertex Shader和Fragment Shader這兩步是可編程的颅崩,也就是我們?cè)谙旅鎸⒁?jiàn)到的.glsl文件几于。Vertex Shader負(fù)責(zé)處理每一個(gè)點(diǎn)的頂點(diǎn)數(shù)據(jù),而Fragment Shader則是針對(duì)像素?cái)?shù)據(jù)的沿后,其負(fù)責(zé)處理每個(gè)像素?cái)?shù)據(jù)沿彭。

image

在OpenGL中,除非加載有效的頂點(diǎn)(Vertex Shader)和片段(Fragment Shader)著色器尖滚,否則不會(huì)繪制任何幾何圖形喉刘。我們先來(lái)看一個(gè)最基本的頂點(diǎn)著色器:

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

第一行聲明了一個(gè)名為position的4分量向量,并在main函數(shù)里面賦值給gl_Position變量熔掺。這里的gl_Position就是代表我們需要處理的頂點(diǎn)饱搏,也就是上圖中的Vertex Data數(shù)據(jù)。

在shader中一共有三種變量類型attribute, uniformvarying. 其區(qū)別為:uniform變量是外部程序傳遞給shader的變量置逻;attribute變量只能在vertex shader中使用推沸,為外部程序傳遞給vertex shader的變量;varying變量則是vertex和fragment shader之間做數(shù)據(jù)傳遞用的券坞。

我們接著再來(lái)看片段著色器的一段代碼:

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

第一行是聲明著色器中浮點(diǎn)變量的默認(rèn)精度鬓催。接著在main函數(shù)里面賦值每個(gè)像素的顏色值,這里我們賦值vec4(1.0, 0.0, 0.0, 1.0)代表每個(gè)像素點(diǎn)的顏色都是紅色恨锚。

基本圖元

使用OpenGL繪制圖形一般都是從繪制一個(gè)三角形開(kāi)始宇驾,因?yàn)檫@個(gè)過(guò)程包括了OpenGL ES的三種基本元素: 點(diǎn),線和三角猴伶。在OpenGL中课舍,任何復(fù)雜的三維模型都是由這三個(gè)基本的幾何圖元組成的。

編譯著色器

頂點(diǎn)和像素的處理都是在shader中實(shí)現(xiàn)的他挎,所以我們要想使用shader就需要在運(yùn)行時(shí)動(dòng)態(tài)編譯源碼以得到一個(gè)著色器對(duì)象筝尾。幸運(yùn)的是,編譯shader的流程是固定的办桨,而且已經(jīng)有很多現(xiàn)成的開(kāi)源代碼實(shí)現(xiàn)筹淫。其大概步驟如下所示:

首先是編譯shader的代碼,其中pathvertex.glsl或者vertex.glsl文件的存放路徑呢撞,而type則是用來(lái)區(qū)分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;
}

現(xiàn)在我們有了編譯之后的shader對(duì)象,接下來(lái)需要把它鏈接到OpenGL的glProgram上摧阅,讓它可以在GPU上run起來(lái)汰蓉。代碼如下所示:

program = glCreateProgram();

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

glLinkProgram(program);
    
GLint status;
glGetProgramiv(program, GL_LINK_STATUS, &status);

完成上面的步驟后,我們就可以用programe來(lái)和shader交互了逸尖,比如賦值給頂點(diǎn)shader的position變量:

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

幾何圖元

有了上面的介紹古沥,我們就可以開(kāi)始繪圖了。所有幾何圖元的繪制都是通過(guò)調(diào)用glDrawArrays實(shí)現(xiàn)的:

glDrawArrays (GLenum mode, GLint first, GLsizei count);

這里的mode為幾何形狀類型娇跟,主要有點(diǎn)岩齿,線和三角形三種:

#define GL_POINTS           0x0000  // 點(diǎn)     -> 默認(rèn)為方形
#define GL_LINES            0x0001  // 線段   -> 可不連續(xù)
#define GL_LINE_LOOP        0x0002  // 線圈   -> 首尾相連的線段
#define GL_LINE_STRIP       0x0003  // 線段帶 -> 相鄰線段共享頂點(diǎn)
#define GL_TRIANGLES        0x0004  // 三角形 -> 三個(gè)頂點(diǎn)連接
#define GL_TRIANGLE_STRIP   0x0005  // 三角帶 -> 相鄰三角共享邊
#define GL_TRIANGLE_FAN     0x0006  // 三角扇 -> 所有三角共享頂點(diǎn)

繪制點(diǎn)代碼如下所示,其中幾何類型傳入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)紅色       
}; // 共有三組數(shù)據(jù)苞俘,表示三個(gè)點(diǎn)

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

// 對(duì)于position每個(gè)數(shù)值包含3個(gè)分量盹沈,即3個(gè)byte,兩組數(shù)據(jù)間間隔6個(gè)GLfloat
// 同樣,對(duì)于color每個(gè)數(shù)值含3個(gè)分量吃谣,但數(shù)據(jù)開(kāi)始的指針位置為跳過(guò)3個(gè)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); 

效果如圖所示:

image

可以看到繪制出來(lái)的點(diǎn)默認(rèn)為方點(diǎn)乞封,那如果要繪制圓點(diǎn)呢?為了讓OpenGL ES 2.0把點(diǎn)繪制成圓形而非矩形岗憋,需要處理光柵化后的點(diǎn)所包含的像素?cái)?shù)據(jù)肃晚,思路是,忽略半徑大于0.5的點(diǎn)仔戈,從而實(shí)現(xiàn)圓點(diǎn)繪制关串。在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;
}

運(yùn)行后,可以看到圓點(diǎn)效果如下所示:

image

繪制直線的代碼如下所示监徘,其中幾何類型傳入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); // 設(shè)置線寬為5
glDrawArrays(GL_LINES, 0, 4); 

對(duì)于線段晋修,如果兩點(diǎn)之間的顏色值不同,則OpenGL會(huì)默認(rèn)產(chǎn)生漸變色效果凰盔,具體繪制結(jié)果如圖所示:

image

由于本文最開(kāi)始的效果里面只用到了點(diǎn)和線的繪制墓卦,所以繪制最基本的三角形,讀者可以自行嘗試户敬,這邊就不再贅述了落剪。

紋理貼圖

除了圖元之外,OpenGL還有紋理的概念尿庐。簡(jiǎn)單來(lái)說(shuō)就是把圖像數(shù)據(jù)顯示到我們所繪制的圖元上著榴,以使圖元表示的物體更真實(shí)。我們首先來(lái)看下紋理的坐標(biāo)系屁倔,如下圖所示:

image

紋理坐標(biāo)的范圍為0到1之間。紋理坐標(biāo)的原點(diǎn)為圖片的左下角暮胧,其和OpenGL繪制坐標(biāo)系的對(duì)應(yīng)關(guān)系如示意圖上箭頭所示锐借,在紋理貼圖的時(shí)候我們需要確保坐標(biāo)點(diǎn)映射關(guān)系與上圖一致问麸。

要實(shí)現(xiàn)紋理的繪制需要兩個(gè)信息,一個(gè)是紋理的坐標(biāo)钞翔,另一個(gè)則是紋理的內(nèi)容严卖。紋理的內(nèi)容簡(jiǎn)單來(lái)說(shuō),就是把iOS中的UIImage轉(zhuǎn)換為OpenGL ES中的texture數(shù)據(jù)布轿。

- (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;
}

有了紋理對(duì)象后哮笆,接下來(lái)我們需要在頂點(diǎn)著色器和片段著色器中轉(zhuǎn)化坐標(biāo)和紋理信息,也就是進(jìn)行采樣渲染汰扭。頂點(diǎn)著色器如下所示:

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

上述代碼中的aTexcoord用來(lái)接受紋理坐標(biāo)信息稠肘,然后傳遞給片段著色器中定義的varying變量vTexcoord。這樣就傳遞了紋理坐標(biāo)信息萝毛。片段著色器代碼如下所示:

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

這里的uTexture就是我們的紋理项阴,而vTexcoord則是紋理坐標(biāo)。有了坐標(biāo)和紋理信息后就可以通過(guò)texture2D函數(shù)進(jìn)行采樣笆包。簡(jiǎn)單來(lái)說(shuō)环揽,就是取出每個(gè)坐標(biāo)點(diǎn)像素的顏色信息賦給OpenGL進(jìn)行繪制,而圖片的數(shù)據(jù)就是由每個(gè)點(diǎn)的顏色像素值所組成的矩陣信息庵佣,因此歉胶,有了紋理和像素間的顏色映射關(guān)系后,就可以通過(guò)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繪制坐標(biāo)
    -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[] = { // 紋理坐標(biāo)
    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的繪制坐標(biāo)验毡,紋理坐標(biāo)為coords, 這兩個(gè)坐標(biāo)需要與上圖的坐標(biāo)對(duì)應(yīng)關(guān)系相符合才能正確顯示出圖片衡创。運(yùn)行后效果如下圖所示:

image

視頻繪制

好了,有了上面的理論基礎(chǔ)晶通,我們可以來(lái)實(shí)現(xiàn)文章開(kāi)篇所示的實(shí)時(shí)視頻繪制了璃氢。對(duì)于視頻流的獲取以及OpenGL的繪制環(huán)境我們采用GPUImage來(lái)實(shí)現(xiàn),人臉識(shí)別的算法采用公司自有視覺(jué)引擎(免費(fèi)開(kāi)放使用狮辽,下載地址為虹軟視覺(jué)AI引擎開(kāi)放平臺(tái))當(dāng)然也可以使用CoreImage框架的CIDetector人臉識(shí)別類一也。

@interface PVTStickerFilter : GPUImageFilter

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

@end

首先繼承GPUImageFilter類,并定義一個(gè)人臉點(diǎn)位數(shù)組用來(lái)接收人臉識(shí)別引擎?zhèn)魅氲狞c(diǎn)位信息喉脖。需要注意的是椰苟,相機(jī)獲取的圖像默認(rèn)在內(nèi)存中是逆時(shí)針90度存放的,所以我們獲取的點(diǎn)位需要順時(shí)針旋轉(zhuǎn)90度才是我們?cè)谌【翱蛑锌吹降膱D像树叽。另外舆蝴,如果是前置攝像頭,默認(rèn)會(huì)有鏡像效果,因此還需要將點(diǎn)位沿Y軸翻轉(zhuǎn)180度洁仗。

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

對(duì)于某個(gè)點(diǎn)(x, y)順時(shí)針旋轉(zhuǎn)90度后坐標(biāo)為(imageHeight - y, x), 如果是鏡像效果的點(diǎn)层皱,則還需要再繞Y軸旋轉(zhuǎn)180度,最終的坐標(biāo)為(y, x)赠潦。

從效果圖中可以看到叫胖,我們要實(shí)現(xiàn)的為左右兩邊對(duì)稱線條的動(dòng)畫(huà)繪制。效果圖中一共繪制了三組線條她奥,我們就其中一組來(lái)分析下其原理瓮增。具體點(diǎn)位為鼻梁左下角點(diǎn)(x67, y67)到眉毛左內(nèi)側(cè)點(diǎn)(x24, y24)的線段繪制,以及鼻梁右下角點(diǎn)(x70, y70)到眉毛右內(nèi)側(cè)點(diǎn)(x29, y29)的線段繪制哩俭。同時(shí)(x24, y24)(x29, y29)在動(dòng)畫(huà)的最后還需要顯示圓點(diǎn)绷跑。

根據(jù)前文的分析,在繪制點(diǎn)位之前我們還需要把視頻圖像幀的坐標(biāo)轉(zhuǎn)換為OpenGL的坐標(biāo)系携茂,也就是把上面幾個(gè)點(diǎn)位的坐標(biāo)轉(zhuǎn)換到-1到1之間你踩。轉(zhuǎn)換公式前文已給出:

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 ;

有了這些點(diǎn)位,我們可以很容易的使用glDrawArrays(GL_LINES, 0, 4)來(lái)繪制出線段讳苦。但是這邊有兩個(gè)問(wèn)題需要解決带膜,一是如何繪制虛線,二是如何實(shí)現(xiàn)繪制的動(dòng)畫(huà)鸳谜。

對(duì)于虛線的繪制膝藕,OpenGL ES 2.0沒(méi)有直接的API可以實(shí)現(xiàn),所以我們需要換一種思路咐扭,將虛線轉(zhuǎn)換為若干直線的連續(xù)繪制芭挽。具體思路為,一個(gè)長(zhǎng)度為10像素的虛線(x1, 0)(x10, 0)蝗肪,我們將它切斷為5個(gè)長(zhǎng)度為1像素線段繪制袜爪。即繪制(x1, 0)(x2, 0)的線段,(x3, 0)(x4, 0)的線段薛闪,(x5, 0)(x6, 0)的線段辛馆,(x7, 0)(x8, 0)的線段,(x9, 0)(x10, 0)的線段豁延。

所以昙篙,首先我們需要根據(jù)繪制虛線的長(zhǎng)度來(lái)給整條線段分段,比如我們定義每段虛線的長(zhǎng)度為0.01诱咏,那么就可以計(jì)算出來(lái)兩個(gè)點(diǎn)位之間的線段需要分為多少片段線來(lái)繪制:

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

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

GLsizei s_24_67 = [self stepsOfLineWidth:w_24_67 height:h_24_67]; // 需要?jiǎng)澐譃槎嗌賯€(gè)片段線
GLsizei s_29_70 = [self stepsOfLineWidth:w_29_70 height:h_29_70]; // 需要?jiǎng)澐譃槎嗌賯€(gè)片段線

計(jì)算片段性的函數(shù)如下所示苔可,其中PVT_DASH_LENGTH為每段虛線的長(zhǎng)度:

- (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);

好了袋狞,虛線的問(wèn)題我們解決了焚辅,我們?cè)賮?lái)看看如何實(shí)現(xiàn)繪制的動(dòng)畫(huà)映屋。其實(shí)思路很簡(jiǎn)單,比如我們要在4秒內(nèi)逐步繪制出線段(由于需要繪制虛線同蜻,我們分成了100個(gè)線段片)秧荆,那么,我們?cè)谙鄼C(jī)每幀數(shù)據(jù)回調(diào)來(lái)的時(shí)候判斷下當(dāng)前幀距離第一幀已經(jīng)間隔了多次時(shí)間埃仪,假設(shè)間隔了1秒,那就是對(duì)于這一幀圖像我們需要繪制出四分之一的長(zhǎng)度陕赃,也就是將25個(gè)線段片塞到OpenGL里面去繪制卵蛉。以此類推,如果超過(guò)了4秒么库,那么再清零重頭計(jì)算傻丝。在4秒的時(shí)候應(yīng)該是繪制整條線段的完整長(zhǎng)度。

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

    [super newFrameReadyAtTime:frameTime atIndex:textureIndex];
}

首先記錄下當(dāng)前幀的時(shí)間诉儒,以便在后面計(jì)算當(dāng)前幀距離第一幀的累積時(shí)間葡缰。

- (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);
}

然后計(jì)算出當(dāng)前幀根據(jù)總的動(dòng)畫(huà)時(shí)間應(yīng)該繪制到哪一步:

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

最后一步則是將計(jì)算好的片段數(shù)傳給OpenGL進(jìn)行繪制,需要注意的時(shí)候當(dāng)累積時(shí)間超過(guò)了動(dòng)畫(huà)時(shí)間后需要將累積時(shí)間清零忱反,從而實(shí)現(xiàn)動(dòng)畫(huà)的連續(xù)展示泛释。這里的_frameDuration即是動(dòng)畫(huà)時(shí)間。

- (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...
}

虛線和動(dòng)畫(huà)的問(wèn)題都解決了温算,現(xiàn)在還剩最后一個(gè)需求怜校,在動(dòng)畫(huà)結(jié)束的時(shí)候在(x24, y24)和(x29, y29)處繪制圓點(diǎn)。對(duì)于圓點(diǎn)的繪制注竿,前文有提到可以直接繪制點(diǎn)茄茁,然后在FragmentShader.glsl中修改忽略半徑大于0.5的即可實(shí)現(xiàn)圓點(diǎn)繪制。但是由于我們需要同時(shí)繪制點(diǎn)和線巩割,且使用同一個(gè)Fragment Shader文件裙顽,所以難以區(qū)分當(dāng)前是繪制點(diǎn)還是線,不能直接在Shader中忽略半徑大于0.5的點(diǎn)宣谈,因此我們這邊對(duì)于圓點(diǎn)直接采用幾何方法繪制愈犹。具體的幾何原理可以參照這篇博文。

#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的深度不亞于學(xué)習(xí)一門(mén)新語(yǔ)言蒲祈,萬(wàn)丈高樓平地起甘萧,希望本文的總結(jié)可以給想入門(mén)的同學(xué)帶來(lái)一些幫助和收獲,也歡迎大家留言討論梆掸。

參考文章

  1. OpenGL ES入門(mén)及繪制一個(gè)三角形
  2. 仿QQ視屏動(dòng)畫(huà)特效-人臉識(shí)別
  3. 從0打造一個(gè)GPUImage
  4. 學(xué)習(xí)OpenGL ES之繪制更多的圖形
  5. OpenGL ES 3.0 數(shù)據(jù)可視化 1:繪制圓點(diǎn)
  6. OpenGL ES入門(mén)03-OpenGL ES圓形繪制
  7. OpenGL ES入門(mén)05-OpenGL ES 紋理貼圖
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末扬卷,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子酸钦,更是在濱河造成了極大的恐慌怪得,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異徒恋,居然都是意外死亡蚕断,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)入挣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)亿乳,“玉大人,你說(shuō)我怎么就攤上這事径筏「鸺伲” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵滋恬,是天一觀的道長(zhǎng)聊训。 經(jīng)常有香客問(wèn)我,道長(zhǎng)恢氯,這世上最難降的妖魔是什么带斑? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮勋拟,結(jié)果婚禮上勋磕,老公的妹妹穿的比我還像新娘。我一直安慰自己指黎,他們只是感情好朋凉,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著醋安,像睡著了一般杂彭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上吓揪,一...
    開(kāi)封第一講書(shū)人閱讀 51,155評(píng)論 1 299
  • 那天亲怠,我揣著相機(jī)與錄音,去河邊找鬼柠辞。 笑死团秽,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的叭首。 我是一名探鬼主播习勤,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼焙格!你這毒婦竟也來(lái)了图毕?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤眷唉,失蹤者是張志新(化名)和其女友劉穎予颤,沒(méi)想到半個(gè)月后囤官,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蛤虐,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年党饮,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片驳庭。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡刑顺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出饲常,到底是詐尸還是另有隱情捏检,我是刑警寧澤,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布不皆,位于F島的核電站,受9級(jí)特大地震影響熊楼,放射性物質(zhì)發(fā)生泄漏霹娄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一鲫骗、第九天 我趴在偏房一處隱蔽的房頂上張望犬耻。 院中可真熱鬧,春花似錦执泰、人聲如沸枕磁。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)计济。三九已至,卻和暖如春排苍,著一層夾襖步出監(jiān)牢的瞬間沦寂,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工淘衙, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留传藏,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓彤守,卻偏偏與公主長(zhǎng)得像毯侦,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子具垫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容