iOS OpenGLES 动态贴纸实现

·  阅读 2003

背景

动态贴纸是人脸特效中的一种的效果体现(基于人脸识别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,你的star和fork是对我最好的支持和动力

简书地址 www.jianshu.com/u/4cc792175…

参考链接 www.jianshu.com/p/122bedf3a…

分类:
iOS
标签:
分类:
iOS
标签:
收藏成功!
已添加到「」, 点击更改