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

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

## MDLAssetクラスからUSDZを出力する
最初に見つけたのは[この記事](https://note.com/blizdliz/n/ncb401eaeab53#:~:text=%E3%81%9F%E3%81%AE%E3%81%AF-,%E3%81%93%E3%81%AE%E8%A8%98%E4%BA%8B,-%E3%80%82)。

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

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

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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779290/f1b42584-a535-ed64-ac37-fab2c9455a63.png" width=200>

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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779290/d691e87d-67a1-7e5b-1b56-a3bb19b2fe7a.png" width=100%>
おぉぅ・・・解読不能。

## SCNSceneクラスからUSDZを出力する
MDLAssetについて調べている過程で[SCNScene](https://note.com/blizdliz/n/ncb401eaeab53#:~:text=%E3%81%84%E3%82%8B%E9%81%8E%E7%A8%8B%E3%81%A7-,SCNScene,-%E3%81%AA%E3%82%8B%E3%82%82%E3%81%AE%E3%82%92)なるものを発見。SCNNodeにモデルやマテリアルをセットしてシーンに追加していくことができるようです。先程のやり方ではマテリアルを設定する過程がなかったので、これでテクスチャも設定できそうです。

```objective_c
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として書き出してみたのですが...

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779290/4025b5be-547a-33b2-9fd6-411cdc6a2fcd.png" width=200>

**テクスチャがない！（２度目）**

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

## ３種類のUSD
USD形式にはテキスト形式 (usda) 、バイナリ形式 (usdc)、アーカイブ形式 (usdz)が存在します。テキスト形式のUSDAは解読ができそうです。[こちらの記事](https://note.com/blizdliz/n/ncb401eaeab53#:~:text=%E3%81%A7%E3%81%8D%E3%81%9D%E3%81%86%E3%81%A7%E3%81%99%E3%80%82-,%E3%81%93%E3%81%A1%E3%82%89%E3%81%AE%E8%A8%98%E4%BA%8B,-%E3%82%92%E5%8F%82%E8%80%83%E3%81%AB)を参考にテクスチャ付きのUSDAがどんなものなのか見てみます。

```objective_c
#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を設定している箇所だけ抜粋します

```objective_c
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の例を参考にして書き足してみます。

```objective_c
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
        }
    }
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3779290/4b90844c-d152-2227-055b-b2f16c259a80.png" width=200>

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

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

```objc
// 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が出力できました。やったぜ。
※出力したusdzは[こちら](https://note.com/api/v2/attachments/download/88add0d8da73f7bd19635d76dd57c1b1)

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

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

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

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