动态贴纸是人脸特效中的一种的效果体现(基于人脸识别SDK)。比如抖音、快手等短视频应用,或者美颜相机、美图秀秀等相机类应用。动态贴纸最常用的是2D,3D贴纸这里不做介绍。
2D贴纸分为静态和动态两种,2D 静态贴纸的素材只有一张图片,动态贴纸则是用多张图片,以序列帧形式渲染出来。下面我们来介绍一下具体实现。
实现
一般来说贴纸是要做成动态下载的,所以我们需要构建一个Json。如下结构可以根据业务需求自行拓展。资源结构 (F_CatMustache 和 F_MouceHeart 图片文件夹, 格式统一 fileName_000.png)
config.json
配置格式
{
"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
},
]
}
渲染
1、构建视椎体:
- (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坐标的计算。
2、计算顶点和变换矩阵
- (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 进行调整
3、获取纹理
根据系统毫秒数、每个节点缓存的开始毫秒数和节点持续时间算出当前帧数 int frameIndex = (int)(([MKTool getCurrentTimeMillis] - nodeMillis) / node.duration);
-(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];
}
4、shader 相对来说就比较简单了
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]https://github.com/ymkil/MagicCamera
原文链接: https://juejin.cn/post/6844903976438071310
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。