最近公司的項(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)行圖案的繪制欣福。最終效果如下圖所示:
本文將從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)行圖形的繪制和渲染说榆。
那么問(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)范圍則為屏幕寬高但狭。
所以如果我們?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):
繪制流程
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ù)沿彭。
在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
,uniform
和varying
. 其區(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的代碼,其中path
為vertex.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);
效果如圖所示:
可以看到繪制出來(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)效果如下所示:
繪制直線的代碼如下所示监徘,其中幾何類型傳入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é)果如圖所示:
由于本文最開(kāi)始的效果里面只用到了點(diǎn)和線的繪制墓卦,所以繪制最基本的三角形,讀者可以自行嘗試户敬,這邊就不再贅述了落剪。
紋理貼圖
除了圖元之外,OpenGL還有紋理的概念尿庐。簡(jiǎn)單來(lái)說(shuō)就是把圖像數(shù)據(jù)顯示到我們所繪制的圖元上著榴,以使圖元表示的物體更真實(shí)。我們首先來(lái)看下紋理的坐標(biāo)系屁倔,如下圖所示:
紋理坐標(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)行后效果如下圖所示:
視頻繪制
好了,有了上面的理論基礎(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)一些幫助和收獲,也歡迎大家留言討論梆掸。