我 与 unity 之间有趣的事情

2,482 阅读4分钟

我正在参加「掘金·启航计划」

前言

项目接入了 unity ,想找一下突破点,到现在功能也是越做越多,前段时间都忙着开发,现在偷得浮生半日闲,也来写下与 unity 之间发生的有趣事情。上一篇写的是 iOS嵌入虚拟引擎unity3d ,有兴趣的小伙伴也可以去看下。下面就写了2个例子来聊聊。

dylib

当时呢,项目是想做一个根据声音来让虚拟偶像动嘴的功能,有点像游戏直播里面弄一个动物替代自己。

image.png

只要人说话,它的动物嘴就会张开,大体如此吧。

一开始,安卓是先和 unity 调研的,然后发现了一个插件,是又支持 iOS 又支持安卓, 然后联调起来,2个端呢,效果都不错,好事对吧。接着就是一顿开发,忘乎所以。

最后当我们上线要提交到 AppStore 审核,就杯具了。

ITMS-90429: Invalid Swift Support - The files libswiftDarwin.dylib, libswiftMetal.dylib, libswiftCoreAudio.dylib, libswiftsimd.dylib, libswiftQuartzCore.dylib, libswiftos.dylib, libswiftObjectiveC.dylib, libswiftDispatch.dylib, libswiftAccelerate.dylib, libswiftCoreGraphics.dylib, libswiftCoreFoundation.dylib, libswiftUIKit.dylib, libswiftCoreMedia.dylib, libswiftAVFoundation.dylib, libswiftCore.dylib, libswiftFoundation.dylib, libswiftCoreImage.dylib aren’t at the expected location /Payload/App.app/Frameworks. Move the file to the expected location, rebuild your app using the current public (GM) version of Xcode, and resubmit it.

然后才发现,unity 使用的插件是个动态库。。。

完犊子了,苹果是不支持自己制作的 dylib 上商店了,然后就没有然后了。。。

录制视频

unity 需要录制视频,当时呢也是安卓先去联调的,我们后面跟着做,还想着可以减少工作量,然后听了方案,是 unity 会一帧一帧传过来,然后客户端再生成一个视频。这方案没有问题,只是说怎么一帧一帧的接收,听他们说用 openGL

那好吧,行动起来,我们也有openGL ES,于是我花了2天去学习了这方面关于纹理的知识。然后再试了不知道多少次后,最后还是不能把一帧转为图片。急啊!!!然后就无意看到这个。。。

screenshot-20221019-120026.png

谁说iOS 使用 openGL ES 去做的呢,这,那我赶紧去看 Metal的东西。

功夫不负有心人,终于查到了如何把纹理转成图片了。

+ (nullable CIImage *)imageWithMTLTexture:(id<MTLTexture>)texture
                                  options:(nullable NSDictionary<CIImageOption, id> *)options NS_AVAILABLE(10_11, 9_0);
// 根据纹理ID 创建图片
- (UIImage *)createImageByMTlTexture:(id)MTLTexture
{
    CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
    NSDictionary* pDic = [NSDictionary dictionaryWithObject:(__bridge_transfer id)colorSpace forKey:kCIImageColorSpace];
    CIImage *outPutImage = [CIImage imageWithMTLTexture:MTLTexture options:nil];
    return [UIImage imageWithCIImage:outPutImage];
}

那转成图片可以,意味着我们的确是拿到有效信息了。

那这时候我们要知道的视频写入需要的是CVPixelBufferRef, CVPixelBufferRef是核心视频像素缓冲区。

- (BOOL)appendPixelBuffer:(CVPixelBufferRef)pixelBuffer withPresentationTime:(CMTime)presentationTime;

CMSampleBufferRef又可以转成CVPixelBufferRefCMSampleBufferRef表示一帧音频/视频数据。

CM_EXPORT
CVImageBufferRef CM_NULLABLE CMSampleBufferGetImageBuffer(
	CMSampleBufferRef CM_NONNULL sbuf);

CVPixelBufferRef又可以转成CMSampleBufferRefCMSampleBufferRef这一帧视频数据是包含像素图片类型数据,还需要填写其它信息,如时间之类的。

//将CVPixelBufferRef转换成CMSampleBufferRef
-(CMSampleBufferRef)pixelBufferToSampleBuffer:(CVPixelBufferRef)pixelBuffer {
    CMSampleBufferRef sampleBuffer;
   CMTime frameTime = CMTimeMakeWithSeconds([[NSDate date] timeIntervalSince1970], 1000000000);
   CMSampleTimingInfo timing = {frameTime, frameTime, kCMTimeInvalid};
   CMVideoFormatDescriptionRef videoInfo = NULL;
   CMVideoFormatDescriptionCreateForImageBuffer(NULL, pixelBuffer, &videoInfo);

   OSStatus status = CMSampleBufferCreateForImageBuffer(kCFAllocatorDefault, pixelBuffer, true, NULL, NULL, videoInfo, &timing, &sampleBuffer);
   if (status != noErr) {
       NSLog(@"Failed to create sample buffer with error %d.", (int)status);
   }
   CVPixelBufferRelease(pixelBuffer);
   if(videoInfo)
       CFRelease(videoInfo);
   return sampleBuffer;
}

那最后得出的结论就是只要把纹理idtexture转成CVPixelBufferRef就可以了。

CVPixelBufferRef是一种像素图片类型,我们设置尺寸大小为 1080*1920

// 创建CVPixelBufferRef
-(CVPixelBufferRef)createPixelBufferWithSize:(CGSize)size {
    const void *keys[] = {
        kCVPixelBufferMetalCompatibilityKey,
        kCVPixelBufferIOSurfacePropertiesKey,
    };
    const void *values[] = {
        (__bridge const void *)([NSNumber numberWithBool:YES]),
        (__bridge const void *)([NSDictionary dictionary])
    };
    
    OSType bufferPixelFormat = kCVPixelFormatType_32ARGB;
    
    CFDictionaryRef optionsDictionary = CFDictionaryCreate(NULL, keys, values, 2, NULL, NULL);
    
    CVPixelBufferRef pixelBuffer = NULL;
    CVPixelBufferCreate(kCFAllocatorDefault,
                        size.width,
                        size.height,
                        bufferPixelFormat,
                        optionsDictionary,
                        &pixelBuffer);
    
    CFRelease(optionsDictionary);
    
    return pixelBuffer;
}

颜色空间CGColorSpaceRef我们设置为kCGColorSpaceGenericRGBLinear就可以了。

然后我们把图渲染到给定的 CVPixelBufferRef就可以了。

- (CVPixelBufferRef)createCVPixelBufferRefByTexture:(id)texture {
    CVPixelBufferRef pixelBuffer = [self createPixelBufferWithSize:CGSizeMake(1080,1920)];
    static CIContext *_ciContext;
    CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGBLinear);
    NSDictionary* pDic = [NSDictionary dictionaryWithObject:(__bridge_transfer id)colorSpace forKey:kCIImageColorSpace];
    CIImage *outputImage = [CIImage imageWithMTLTexture:texture options:pDic];
    if(outputImage) {
        if( _ciContext == nil) {
            _ciContext =  [CIContext context];
        }
        [_ciContext render: outputImage toCVPixelBuffer: pixelBuffer bounds:[outputImage extent] colorSpace:CGColorSpaceCreateDeviceRGB()];
    }
    CGColorSpaceRelease(colorSpace);
    return pixelBuffer;
}

经过层层转换,最后我们解决了如何一帧一帧录制成视频,真不容易啊。

最后

本文写到这里,除了展示了一些功能个例的注意事项,更想说的对于 unity 嵌入,参考固然需要,更多还是需要依赖我们iOS本身,用自己适合的东西去思考,去实现才对。