Skip to content

Instantly share code, notes, and snippets.

@mao-test-h
Created August 9, 2021 15:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mao-test-h/48f6067e2d6cf9ad18283d1df6d8be27 to your computer and use it in GitHub Desktop.
Save mao-test-h/48f6067e2d6cf9ad18283d1df6d8be27 to your computer and use it in GitHub Desktop.
RenderTexture.GetNativeTexturePtr()からネイティブ側でpngに変換+保存
#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
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