見出し画像

objective-cで3DオブジェクトをUSDZとして(強引に)出力する

objective-c(swiftでも可)で3DモデルをUSDZとして出力する方法を模索した過程を、備忘録も兼ねてまとめました。

MDLAssetクラスからUSDZを出力する

最初に見つけたのはこの記事

記事によると、MDLAssetクラスのexportメソッドを使ってOBJファイルをUSDZに変換できるとのこと。MDLAssetクラスはUSDZを直接出力できないので、USDCとして出力してからUSDZに拡張子を変更しているようです。

拡張子を変えるだけで何とかなるのか?と思いつつも実践。その結果...

ファイルが正常に開けませんでした。

しかし、USDCとしては出力できていたのでそちらも確認してみました。すると...

スクリーンショット 2021-03-29 17.28.08

モデルはあるがテクスチャが貼られていない!

テクスチャがないということは、ファイルパスが正しく指定されていないのでは?と思い、ダメ元でUSDCファイルを直接覗いてみました。

スクリーンショット 2021-03-29 13.50.39

おぉぅ・・・解読不能。

SCNSceneクラスからUSDZを出力する

MDLAssetについて調べている過程でSCNSceneなるものを発見。SCNNodeにモデルやマテリアルをセットしてシーンに追加していくことができるようです。先程のやり方ではマテリアルを設定する過程がなかったので、これでテクスチャも設定できそうです。

NSURL *objUrl = [NSURL fileURLWithPath:"objのファイルパス"];
// MDLAssetを初期化
MDLAsset *objAsset = [[MDLAsset alloc]initWithURL:objUrl];
// シーンを初期化
SCNScene *scene = [SCNScene scene];
// MDLAssetを使ってノードを初期化
SCNNode *node = [SCNNode nodeWithMDLObject:[objAsset objectAtIndex:0]];
// マテリアルを生成
SCNMaterial *material = [SCNMaterial material];
// テクスチャを設定
material.diffuse.contents = [UIImage imageWithContentsOfFile:"画像のファイルパス"];
// マテリアルをノードに追加
node.geometry.firstMaterial = material;
// シーンに追加する
[scene.rootNode addChildNode:node];
// usdzとして書き出す
[scene writeToURL:usdzUrl options:nil delegate:nil progressHandler:nil];

そしてUSDZとして書き出してみたのですが...

スクリーンショット 2021-03-29 17.28.08

テクスチャがない!(2度目)

SCNMaterialに設定した内容はUSDに反映されない?・・・埒が明かなくなってきたので、USDについて改めて調べることにしました。

3種類のUSD

USD形式にはテキスト形式 (usda) 、バイナリ形式 (usdc)、アーカイブ形式 (usdz)が存在します。テキスト形式のUSDAは解読ができそうです。こちらの記事を参考にテクスチャ付きのUSDAがどんなものなのか見てみます。

#usda 1.0

def Xform "hello"
{
   def Mesh "mesh"
   {
       uniform bool doubleSided = 0
       int[] faceVertexCounts = [4]
       int[] faceVertexIndices = [0, 1, 2, 3]
       rel material:binding = </hello/material_0>
       normal3f[] normals = [(0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0)]
       point3f[] points = [(-1, 0, -1), (-1, 0, 1), (1, 0, 1), (1, 0, -1)]
       texCoord2f[] primvars:st = [(0, 0), (1, 0), (1, 1), (0, 1)] (
           interpolation = "varying"
       )
   }

   def Material "material_0"
   {
       token inputs:frame:stPrimvarName = "st"
       token outputs:surface.connect = </hello/material_0/PBRShader.outputs:surface>

       def Shader "PBRShader"
       {
           uniform token info:id = "UsdPreviewSurface"
           color3f inputs:diffuseColor.connect = </hello/material_0/diffuseTexture.outputs:rgb>
           float inputs:metallic = 0.3
           float inputs:roughness = 0.3
           token outputs:surface
       }

       def Shader "stReader"
       {
           uniform token info:id = "UsdPrimvarReader_float2"
           token inputs:varname.connect = </hello/material_0.inputs:frame:stPrimvarName>
           float2 outputs:result
       }

       def Shader "diffuseTexture"
       {
           uniform token info:id = "UsdUVTexture"
           asset inputs:file = @tile_image.png@
           float2 inputs:st.connect = </hello/material_0/stReader.outputs:result>
           float3 outputs:rgb
       }
   }
}

"diffuseTexture"の中で画像ファイルを指定していることがわかります。

次にSCNSceneクラスからUSDAとして書き出した結果を見てみます。
※Materialを設定している箇所だけ抜粋します

def Scope "Materials"
{
   def Material "Material_0"
   {
       token outputs:surface.connect = </_20210328_163514/Materials/Material_0/surfaceShader.outputs:surface>

       def Shader "surfaceShader"
       {
           uniform token info:id = "UsdPreviewSurface"
           color3f inputs:emissiveColor = (0, 0, 0)
           normal3f inputs:normal = (1, 0.99999994, 1)
           float inputs:occlusion = 1
           float inputs:opacity = 1
           token outputs:surface
       }
   }
}

おや?画像ファイルを指定している箇所がありませんね。

画像を指定する記述があれば、3Dモデルに画像が貼られるはずです。
というわけで、diffuseTextureの例を参考にして書き足してみます。

def Scope "Materials"
{
   def Material "Material_0"
   {
       token outputs:surface.connect = </_20210328_163514/Materials/Material_0/surfaceShader.outputs:surface>

       def Shader "surfaceShader"
       {
            uniform token info:id = "UsdPreviewSurface"
            color3f inputs:emissiveColor = (0, 0, 0)
            color3f inputs:diffuseColor.connect = </_20210328_163514/Materials/Material_0/diffuseTexture.outputs:rgb>
            normal3f inputs:normal = (1, 0.99999994, 1)
            float inputs:occlusion = 1
            float inputs:opacity = 1
            token outputs:surface
        }
        
        def Shader "diffuseTexture"
        {
            uniform token info:id = "UsdUVTexture"
            asset inputs:file = @20210328_163514.png@
            float2 inputs:st.connect = </_20210328_163514/Materials/Material_0/stReader.outputs:result>
            float3 outputs:rgb
        }
    }
}

スクリーンショット 2021-03-29 17.30.23

画像が貼られた3Dモデルが表示されました

このUSDAファイルをUSDZに変換してみます。

// usdaをusdzにして保存する
MDLAsset *usdaAsset = [[MDLAsset alloc]initWithURL:usdaUrl];
SCNScene *scene = [SCNScene scene];
SCNNode *node = [SCNNode nodeWithMDLObject:[usdaAsset objectAtIndex:0]];
[scene.rootNode addChildNode:node];
[scene writeToURL:usdzUrl options:nil delegate:nil progressHandler:nil];

テクスチャ付きのUSDZが出力できました。やったぜ。

まとめ

というわけで最終的に以下の方法で出力できました。

・OBJファイルをUSDAとして出力
・USDAファイルにMaterialに画像ファイルを指定する記述を追記して上書き
・USDAファイルをUSDZとして出力

USDAファイルへの追記もスクリプトから実行できるので、結果的にはobjective-cのみでOBJからUSDZの変換ができました。

・・・なんというか、かなり強引な感じなのでもっとスマートなやり方があれば教えて欲しいです。


いいなと思ったら応援しよう!