動態貼紙是人臉特效中的一種的效果體現(基於人臉識別SDK)。好比抖音、快手等短視頻應用,或者美顏相機、美圖秀秀等相機類應用。動態貼紙最經常使用的是2D,3D貼紙這裏不作介紹。2D貼紙分爲靜態和動態兩種,2D 靜態貼紙的素材只有一張圖片,動態貼紙則是用多張圖片,以序列幀形式渲染出來。下面咱們來介紹一下具體實現。node
通常來講貼紙是要作成動態下載的,因此咱們須要構建一個Json。以下結構能夠根據業務需求自行拓展。 資源結構 (F_CatMustache 和 F_MouceHeart 圖片文件夾, 格式統一 fileName_000.png)git
config.json
配置格式github
{
"name" : "白小貓", // 用於 UI 顯示
"icon" : "baixiaomaohuxu_icon.png", // UI icon
"nodes" : [ // 每一個模型能夠有多個特效組合
{
"type" : "2dAnim", // 模型類型: 如:`2dAnim`、`3dModel` (3D 貼紙)、`3dAnim` (3D 動畫) 等
"dirname" : "F_CatMustache", // 存放素材的文件夾名稱,如第一個特效的素材所有存放在 F_CatMustache 文件夾下
"facePos" : 46, // 人臉關鍵點的中心點
"startIndex" : 1, // 貼紙相對於人臉關鍵點中的起始點,跟結束點一塊兒用於計算貼紙在人臉上的寬
"endIndex" : 31, // 人臉關鍵點中的結束點
"offsetX" : 0, // 貼紙x軸偏移量
"offsetY" : 0, // 貼紙y軸偏移量
"ratio" : 1, // 貼紙縮放倍數(相對於人臉)
"number" : 72, // 素材圖片的個數。即dirname文件夾下圖片的總數。
"width" : 200, // 素材圖片的分辨率,同一個dirname下的素材圖片分辨率都要相同。
"height" : 100,
"duration" : 100, // 每張圖片的播放時間,以毫秒爲單位。不一樣dirname下的素材圖片的duration能夠不一樣。
"isloop" : 1, // dirname下全部素材圖片都播放完一遍以後,是否從新循環播放。1:循環播放,0:不循環播放。
"maxcount" : 5 // 最大支持人臉數
},
{
"type" : "2dAnim",
"dirname" : "F_MouceHeart",
"facePos" : 45,
"startIndex" : 52,
"endIndex" : 43,
"offsetX" : -1.2,
"offsetY" : -0.3,
"ratio" : 1,
"number" : 72,
"width" : 200,
"height" : 150,
"duration" : 100,
"isloop" : 1,
"maxcount" : 5
},
]
}
複製代碼
一、構建視椎體:json
- (void)generateTransitionMatrix {
float mRatio = outputFramebuffer.size.width/outputFramebuffer.size.height;
_projectionMatrix = GLKMatrix4MakeFrustum(-mRatio, mRatio, -1, 1, 3, 9);
_viewMatrix = GLKMatrix4MakeLookAt(0, 0, 6, 0, 0, 0, 0, 1, 0);
}
複製代碼
這裏構建的視椎體加入的長寬比,而且視點(0.0, 0.0, 6.0)
跟近平面 3
恰好是兩倍,以便後續ndc座標的計算。緩存
二、計算頂點和變換矩陣oop
- (void)drawFaceNode:(MKNodeModel *)node withfaceInfo:(MKFaceInfo *)faceInfo {
GLuint textureId = [self getNodeTexture:node]; // 獲取紋理
if (textureId <= 0) return;
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
[outputFramebuffer activateFramebuffer];
[_program use];
GLfloat tempPoint[8];
CGFloat mImageWidth = MKLandmarkManager.shareManager.detectionWidth;
CGFloat mImageHeight = MKLandmarkManager.shareManager.detectionHeight;
float stickerWidth = getDistance(([faceInfo.points[node.startIndex] CGPointValue].x * 0.5 + 0.5) * mImageWidth,
([faceInfo.points[node.startIndex] CGPointValue].y * 0.5 + 0.5) * mImageHeight, ([faceInfo.points[node.endIndex] CGPointValue].x * 0.5 + 0.5) * mImageWidth, ([faceInfo.points[node.endIndex] CGPointValue].y * 0.5 + 0.5) * mImageHeight);
float stickerHeight = stickerWidth * node.height/node.width;
float centerX = 0.0f;
float centerY = 0.0f;
centerX = ([faceInfo.points[node.facePos] CGPointValue].x * 0.5 + 0.5) * mImageWidth;
centerY = ([faceInfo.points[node.facePos] CGPointValue].y * 0.5 + 0.5) * mImageHeight;
centerX = centerX / mImageHeight * ProjectionScale;
centerY = centerY / mImageHeight * ProjectionScale;
// 求出真正的中心點頂點座標,這裏因爲frustumM設置了長寬比,所以ndc座標計算時須要變成mRatio:1,這裏須要轉換一下
float ndcCenterX = (centerX - outputFramebuffer.size.width/outputFramebuffer.size.height) * ProjectionScale;
float ndcCenterY = (centerY - 1.0f) * ProjectionScale;
// 貼紙的寬高在ndc座標系中的長度
float ndcStickerWidth = stickerWidth / mImageHeight * ProjectionScale;
float ndcStickerHeight = ndcStickerWidth * (float) node.height / (float) node.width;
// ndc偏移座標
float offsetX = (stickerWidth * node.offsetX) / mImageHeight * ProjectionScale;
float offsetY = (stickerHeight * node.offsetY) / mImageHeight * ProjectionScale;
// 根據偏移座標算出錨點的ndc 座標
float anchorX = ndcCenterX + offsetX;
float anchorY = ndcCenterY + offsetY;
// 貼紙實際的頂點座標
tempPoint[0] = anchorX - ndcStickerWidth;
tempPoint[1] = anchorY - ndcStickerHeight;
tempPoint[2] = anchorX + ndcStickerWidth;
tempPoint[3] = anchorY - ndcStickerHeight;
tempPoint[4] = anchorX - ndcStickerWidth;
tempPoint[5] = anchorY + ndcStickerHeight;
tempPoint[6] = anchorX + ndcStickerWidth;
tempPoint[7] = anchorY + ndcStickerHeight;
// 紋理座標
static const GLfloat textureCoordinates[] = {
0.0f, 0.0f,
1.0f, 0.0f,
0.0f, 1.0f,
1.0f, 1.0f,
};
// 歐拉角
float pitchAngle = faceInfo.pitch;
float yawAngle = faceInfo.yaw;
float rollAngle = -faceInfo.roll;
_modelViewMatrix = GLKMatrix4Identity;
// 移到貼紙中心
_modelViewMatrix = GLKMatrix4Translate(_modelViewMatrix, ndcCenterX, ndcCenterY, 0);
_modelViewMatrix = GLKMatrix4RotateZ(_modelViewMatrix, rollAngle);
_modelViewMatrix = GLKMatrix4RotateY(_modelViewMatrix, yawAngle);
_modelViewMatrix = GLKMatrix4RotateX(_modelViewMatrix, pitchAngle);
// 平移回到原來構建的視椎體的位置
_modelViewMatrix = GLKMatrix4Translate(_modelViewMatrix, -ndcCenterX, -ndcCenterY, 0);
GLKMatrix4 mvpMatrix = GLKMatrix4Multiply(_projectionMatrix, _viewMatrix);
mvpMatrix = GLKMatrix4Multiply(mvpMatrix, _modelViewMatrix);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, textureId);
glUniform1i(_inputTextureUniform, 3);
glUniformMatrix4fv(_mvpMatrixSlot, 1, GL_FALSE, mvpMatrix.m);
glVertexAttribPointer(_positionAttribute, 2, GL_FLOAT, 0, 0, tempPoint);
glEnableVertexAttribArray(_positionAttribute);
glVertexAttribPointer(_inTextureAttribute, 2, GL_FLOAT, 0, 0, textureCoordinates);
glEnableVertexAttribArray(_inTextureAttribute);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glDisable(GL_BLEND);
}
複製代碼
注: 歐拉角 需根據不一樣的SDK 進行調整動畫
三、獲取紋理ui
根據系統毫秒數、每一個節點緩存的開始毫秒數和節點持續時間算出當前幀數 int frameIndex = (int)(([MKTool getCurrentTimeMillis] - nodeMillis) / node.duration);
spa
-(GLuint )getNodeTexture:(MKNodeModel *)node {
uint64_t nodeMillis = 0;
// 獲取 node 緩存的開始毫秒數,如爲空則獲取當前系統毫秒數,並緩存爲node開始毫秒數
if (_nodeFrameTime[node.dirname] == nil) {
nodeMillis = [MKTool getCurrentTimeMillis];
_nodeFrameTime[node.dirname] = [[NSNumber alloc] initWithUnsignedLongLong:nodeMillis];
} else {
nodeMillis = [_nodeFrameTime[node.dirname] unsignedLongLongValue];
}
// 計算出當前幀數
int frameIndex = (int)(([MKTool getCurrentTimeMillis] - nodeMillis) / node.duration);
// 對比 素材總數,判斷是否重複播放
if (frameIndex >= node.number) {
if (node.isloop) {
_nodeFrameTime[node.dirname] = [[NSNumber alloc] initWithUnsignedLongLong:[MKTool getCurrentTimeMillis]];
frameIndex = 0;
} else {
return 0;
}
}
// 根據幀數獲取對應圖片資源
NSString *imageName = [NSString stringWithFormat:@"%@_%03d.png",node.dirname,frameIndex];
NSString *path = [node.filePath stringByAppendingPathComponent:imageName];
UIImage *image = [UIImage imageWithContentsOfFile:path];
// 暫時採用 GPUImage 獲取紋理,後續進行提取
GPUImagePicture *picture1 = [[GPUImagePicture alloc] initWithImage:iamge];
GPUImageFramebuffer *frameBuffer1 = [picture1 framebufferForOutput];
return [frameBuffer1 texture];
}
複製代碼
四、shader 相對來講就比較簡單了3d
NSString *const kMKGPUImageDynamicSticker2DVertexShaderString = SHADER_STRING
(
attribute vec3 vPosition;
attribute vec2 in_texture;
varying vec2 textureCoordinate;
uniform mat4 u_mvpMatrix;
void main()
{
gl_Position = u_mvpMatrix * vec4(vPosition, 1.0);
textureCoordinate = in_texture;
}
);
複製代碼
代碼已上傳MagicCamera,你的star和fork是對我最好的支持和動力