iOS OpenGLES 動態貼紙實現

背景

動態貼紙是人臉特效中的一種的效果體現(基於人臉識別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是對我最好的支持和動力

簡書地址 www.jianshu.com/u/4cc792175…

參考連接 www.jianshu.com/p/122bedf3a…

相關文章
相關標籤/搜索