Created
August 9, 2021 15:45
-
-
Save mao-test-h/48f6067e2d6cf9ad18283d1df6d8be27 to your computer and use it in GitHub Desktop.
RenderTexture.GetNativeTexturePtr()からネイティブ側でpngに変換+保存
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import <Metal/Metal.h> | |
#import <UnityFramework/UnityFramework-Swift.h> | |
#ifdef __cplusplus | |
extern "C" { | |
#endif | |
// P/Invoke code. | |
// [DllImport("__Internal", EntryPoint = "encodeToPNG2")] | |
// static extern string EncodeToPNG(IntPtr nativeTexturePtr, string fileName); | |
// > encodeMethod(targetRenderTexture.GetNativeTexturePtr(), fileName); | |
const char* encodeToPNG1(unsigned char* nativeTexturePtr, const char* fineName) { | |
NSString* str = [NSString stringWithCString:fineName encoding:NSUTF8StringEncoding]; | |
id <MTLTexture> texture = (__bridge id <MTLTexture>) (void*) nativeTexturePtr; | |
NSString* path = [NativeImageEncoder convert1WithTexture:texture fileName:str]; | |
const char* strPtr = (char*) [path UTF8String]; | |
char* result = (char*) malloc(strlen(strPtr) + 1); | |
strcpy(result, strPtr); | |
result[strlen(strPtr)] = '\0'; | |
return result; | |
} | |
const char* encodeToPNG2(unsigned char* nativeTexturePtr, const char* fineName) { | |
NSString* str = [NSString stringWithCString:fineName encoding:NSUTF8StringEncoding]; | |
id <MTLTexture> texture = (__bridge id <MTLTexture>) (void*) nativeTexturePtr; | |
NSString* path = [NativeImageEncoder convert2WithTexture:texture fileName:str]; | |
const char* strPtr = (char*) [path UTF8String]; | |
char* result = (char*) malloc(strlen(strPtr) + 1); | |
strcpy(result, strPtr); | |
result[strlen(strPtr)] = '\0'; | |
return result; | |
} | |
#ifdef __cplusplus | |
} | |
#endif |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
import CoreImage | |
import Accelerate | |
import MobileCoreServices.UTType | |
public class NativeImageEncoder: NSObject { | |
// NOTE: Unityで言う`Application.temporaryCachePath`と同じ位置に保存 | |
static let cachePath = NSSearchPathForDirectoriesInDomains( | |
.cachesDirectory, .userDomainMask, true)[0] | |
static let mtlDevice = MTLCreateSystemDefaultDevice()! | |
static let ciContext = CIContext(mtlDevice: mtlDevice) | |
// MARK:- public methods | |
@objc public static func convert1(texture: MTLTexture, fileName: String) -> String { | |
return convertFromCIContext(texture: texture, fileName: fileName) | |
} | |
@objc public static func convert2(texture: MTLTexture, fileName: String) -> String { | |
return convertFromCGImage(texture: texture, fileName: fileName) | |
} | |
// MARK:- private methods | |
/// MTLTexture -> CIImage - UIImageベースの変換 | |
/// NOTE: UIImage().pngData()!を使うとiPhone 12 ProMaxで0.2sec近く掛かってゴリラ | |
static func convertFromUIImage(texture: MTLTexture, fileName: String) -> String { | |
// NOTE: Unityからは`.bgra8Unorm`で来る想定が有る | |
precondition(texture.pixelFormat == .bgra8Unorm) | |
guard let mtlTexture = texture.makeTextureView(pixelFormat: .bgra8Unorm_srgb), | |
let ciImage = CIImage(mtlTexture: mtlTexture) else { | |
fatalError("failed") | |
} | |
// MTLTextureをCIImage -> UIImageに変換しつつ、pngに変換するAPIを叩く | |
// NOTE: こいつが0.2sec近く掛かってクッソ遅いのでゴリラ | |
let uiImage = UIImage(ciImage: ciImage) | |
let data = uiImage.pngData()! | |
// pngの保存 | |
let fullPath = "\(cachePath)/\(fileName).png" | |
let url = URL(fileURLWithPath: fullPath) | |
do { | |
try data.write(to: url) | |
} catch { | |
fatalError("\(error)") | |
} | |
return fullPath; | |
} | |
/// バックエンドにMetalを利用したCIContextベースの変換 | |
/// NOTE: 似た方式でJPEGへの変換も対応可能 (`ciContext.jpegRepresentation`がある) | |
static func convertFromCIContext(texture: MTLTexture, fileName: String) -> String { | |
// NOTE: Unityからは`.bgra8Unorm`で来る想定が有る | |
precondition(texture.pixelFormat == .bgra8Unorm) | |
// Unityから渡ってきたMTLTextureをsRGBに変換しつつ、 | |
// CoreImageベースでMTLTextureをpngに圧縮 | |
guard let mtlTexture = texture.makeTextureView(pixelFormat: .bgra8Unorm_srgb), | |
let ciImage = CIImage(mtlTexture: mtlTexture), | |
let data = ciContext.pngRepresentation( | |
of: ciImage, | |
format: .RGBA8, | |
colorSpace: CGColorSpaceCreateDeviceRGB()) else { | |
fatalError("failed") | |
} | |
// pngの保存 | |
let fullPath = "\(cachePath)/\(fileName).png" | |
let url = URL(fileURLWithPath: fullPath) | |
do { | |
try data.write(to: url) | |
} catch { | |
fatalError("\(error)") | |
} | |
return fullPath; | |
} | |
/// MetalでMTLTextureを変換しつつCGImageに変換して保存 | |
static func convertFromCGImage(texture: MTLTexture, fileName: String) -> String { | |
let newTexture = texture.makeTextureView(pixelFormat: .bgra8Unorm_srgb)! | |
let commandQueue = mtlDevice.makeCommandQueue()! | |
let commandBuffer = commandQueue.makeCommandBuffer()! | |
let blitEncoder = commandBuffer.makeBlitCommandEncoder()! | |
blitEncoder.copy( | |
from: texture, sourceSlice: 0, sourceLevel: 0, | |
sourceOrigin: MTLOrigin(x: 0, y: 0, z: 0), | |
sourceSize: MTLSizeMake(texture.width, texture.height, texture.depth), | |
to: newTexture, destinationSlice: 0, destinationLevel: 0, | |
destinationOrigin: MTLOrigin(x: 0, y: 0, z: 0)) | |
blitEncoder.endEncoding() | |
commandBuffer.commit() | |
commandBuffer.waitUntilCompleted() | |
let ciImage = CIImage(mtlTexture: newTexture)! | |
let cgImage = ciContext.createCGImage(ciImage, from: ciImage.extent)! | |
// pngの保存 | |
// NOTE: ここの処理が重め | |
let fullPath = "\(cachePath)/\(fileName).png" | |
let url = URL(fileURLWithPath: fullPath) | |
if let imageDestination = CGImageDestinationCreateWithURL( | |
url as CFURL, kUTTypePNG, 1, nil) { | |
CGImageDestinationAddImage(imageDestination, cgImage, nil) | |
CGImageDestinationFinalize(imageDestination) | |
} | |
return fullPath; | |
} | |
/// MetalでMTLTextureを変換しつつCGImageに変換して保存 (2) | |
@objc public static func convertFromCGImage2(texture: MTLTexture, fileName: String) -> String { | |
// MTLTexture間でsRGB変換 | |
let newTexture = texture.makeTextureView(pixelFormat: .bgra8Unorm_srgb)! | |
let commandQueue = mtlDevice.makeCommandQueue()! | |
let commandBuffer = commandQueue.makeCommandBuffer()! | |
let blitEncoder = commandBuffer.makeBlitCommandEncoder()! | |
blitEncoder.copy( | |
from: texture, sourceSlice: 0, sourceLevel: 0, | |
sourceOrigin: MTLOrigin(x: 0, y: 0, z: 0), | |
sourceSize: MTLSizeMake(texture.width, texture.height, texture.depth), | |
to: newTexture, destinationSlice: 0, destinationLevel: 0, | |
destinationOrigin: MTLOrigin(x: 0, y: 0, z: 0)) | |
blitEncoder.endEncoding() | |
commandBuffer.commit() | |
commandBuffer.waitUntilCompleted() | |
// MTLTextureの読み取り用ののバッファを確保 | |
// NOTE: 地味に2番目に時間掛かってる | |
let width = newTexture.width | |
let height = newTexture.height | |
let pixelByteCount = 4 * MemoryLayout<UInt8>.size | |
let imageBytesPerRow = width * pixelByteCount | |
let imageByteCount = imageBytesPerRow * height | |
let imageBytes = UnsafeMutableRawPointer.allocate( | |
byteCount: imageByteCount, alignment: pixelByteCount) | |
defer { | |
imageBytes.deallocate() | |
} | |
newTexture.getBytes( | |
imageBytes, | |
bytesPerRow: imageBytesPerRow, | |
from: MTLRegionMake2D(0, 0, width, height), | |
mipmapLevel: 0) | |
// フォーマットの変換など | |
convertBuffers(imageBytes, width: width, height: height) | |
// CGImageの作成 | |
guard let bitmapContext = CGContext( | |
data: nil, | |
width: width, | |
height: height, | |
bitsPerComponent: 8, | |
bytesPerRow: imageBytesPerRow, | |
space: CGColorSpaceCreateDeviceRGB(), | |
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { | |
fatalError("非対応") | |
} | |
bitmapContext.data?.copyMemory(from: imageBytes, byteCount: imageByteCount) | |
let cgImage = bitmapContext.makeImage()! | |
// pngの保存 | |
// NOTE: ここの処理が重め | |
let fullPath = "\(cachePath)/\(fileName).png" | |
let url = URL(fileURLWithPath: fullPath) | |
if let imageDestination = CGImageDestinationCreateWithURL( | |
url as CFURL, kUTTypePNG, 1, nil) { | |
CGImageDestinationAddImage(imageDestination, cgImage, nil) | |
CGImageDestinationFinalize(imageDestination) | |
} | |
return fullPath; | |
} | |
// ref: | |
// - https://stackoverflow.com/questions/52920497/swift-metal-save-bgra8unorm-texture-to-png-file | |
// - https://qiita.com/codelynx/items/e7d7da8621901467b64a | |
static func convertBuffers(_ bytes: UnsafeMutableRawPointer, width: Int, height: Int) { | |
// use Accelerate framework to convert from BGRA to RGBA | |
var sourceBuffer = vImage_Buffer(data: bytes, | |
height: vImagePixelCount(height), | |
width: vImagePixelCount(width), | |
rowBytes: width * 4) | |
var destBuffer = vImage_Buffer(data: bytes, | |
height: vImagePixelCount(height), | |
width: vImagePixelCount(width), | |
rowBytes: width * 4) | |
var swizzleMask: [UInt8] = [2, 1, 0, 3] // BGRA -> RGBA | |
vImagePermuteChannels_ARGB8888(&sourceBuffer, &destBuffer, &swizzleMask, vImage_Flags(kvImageNoFlags)) | |
// flipping image vertically | |
var flippedBuffer = vImage_Buffer(data: bytes, | |
height: vImagePixelCount(height), | |
width: vImagePixelCount(width), | |
rowBytes: width * 4) | |
vImageVerticalReflect_ARGB8888(&destBuffer, &flippedBuffer, 0) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment