[USD/C++] ArPathmapResolverを作ろう [Github]

# はじめに

この記事は [Universal Scene Description Advent Calender 2024](https://qiita.com/advent-calendar/2024/usd) の14日目の記事です

今回は **Asset Resolution 2.0** で **ArPathmapResolver** を作成した話をしたいと思います！
せっかくなので**ビルドも試せる**ように、ソースコードも Github にアップしています

https://github.com/yu-kita89/USD_PathmapResolver

また、進めるにあたっての補助として、用語等を説明した記事も投稿しています

https://qiita.com/yu_kita/items/e2c39e91529492d6370b

どうぞよろしくお願いします
それでは、いきましょう！

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233156/61b17bba-d3e8-a600-6833-2312ad3aa3ba.gif)

## ArPathmapResolver

**ArPathmapResolver** ですが簡単に言えば、

**ArDefaultResolver** を拡張して **HOUDINI_PATHMAP** が機能するように調整した
カスタムリゾルバーです

これを作成しようと思ったきっかけは、WindowsとLinux間でデータのやり取りをする際に、USDのパスが上手くマップされず、その結果読み込めない事があったためです

***

### HOUDINI_PATHMAP

Houdiniには **HOUDINI_PATHMAP** という環境変数が用意されており、下記のような形でファイルパスをマップする事が出来るのですが、

どうやら、SolarisやHuskではUSDのパス解決においては、対応していないようでした
```python
{ "c:/temp": "/tmp", "/mnt/render": "//storage/share/render" }
# c:/temp/hoge/fuga -> /tmp/hoge/fuga
```

USDでファイルを書き出す時に、相対パスでの書き出しを心がけてはいるものの、
重めのキャッシュなど、別ドライブや、別マウントされた場所へと保存する事もあり、

そうすると絶対パスで保存されたUSDのパスが、**OS間で解決できない状態でした。**

また、それがリファレンスされている場合は、参照先がどんどん深くなっていくので
表面上のパスだけではなく、**参照されているパスも全て解決する必要** があります

***

### VFX-UsdAssetResolver

そこで、何かできる事はないかと調べたところ、
カスタムリゾルバーであれば解決できそうでした。

ちょうどその頃に **LucaScheller** さんの [VFX-UsdAssetResolver](https://github.com/LucaScheller/VFX-UsdAssetResolver) がGithubで公開され、

https://lucascheller.github.io/VFX-UsdAssetResolver/overview.html

それを見てみると、自分がやりたい事と近い内容だったり、**HoudiniでビルドされたUSDを使ってビルドしていたので**、開発環境を整えるコストも低く、これならば...と思いカスタムリゾルバーを作る事にしました。

***

とはいえ、完成されたソースを見ても、自分には
**何が必要で、何が必要でないかもわからない状態**だったので
まずは、細かく段階を踏んで作っていく事にしました。

なので、ここからは自分が踏んでいった工程を実際にビルドしてもらいながら
**各項で説明**していければと思います

# 準備

__ArPathmapResolver__ を作るにあたって、
まずはどういうアプローチで作るかを考えていきたいと思います

## Asset Resolution

今回は **LucaScheller** さんの [USD Survival Guide](https://lucascheller.github.io/VFX-UsdSurvivalGuide/pages/core/plugins/assetresolver.html) から図をお借りして

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233156/d687762f-be73-7831-c180-db02824234e0.jpeg)

**Asset Resolution** では、**Asset Identifier** とよばれる
`@...@` で囲まれた文字列を解決する事で、
実際のアセットのパスがどこにあるのかを特定する事ができます

### Asset Resolver / Asset Identifier
**Asset Identifier** は実際のファイルパスを指している訳ではなく、
アセットを区別するためのタグのようなもので、
**Asset Resolver** によって、これを実際のファイルパスやURIへと変換しています

### Asset Resolver Context

その際に **Asset Resolver Context** を **Asset Resolver** に関連付ける事で、
特定の動作を変更したり、異なる検索パスを指定したりする事ができます

***

より詳しく知りたい方は、いつもお世話になっている **[@fereria](https://qiita.com/fereria)** さんのページや、先程の **LucaScheller** さんのページもご覧下さい

https://fereria.github.io/reincarnation_tech/usd/asset_resolution_01

https://lucascheller.github.io/VFX-UsdSurvivalGuide/pages/core/plugins/assetresolver.html

https://openusd.org/dev/api/ar_page_front.html

***

という事で、今回に関しては **Asset Identifier** を特に変えなくてもよさそうだったので、
**Asset Resolver** と **Asset Resolver Context** を調整して
カスタムリゾルバーを作っていく事にします

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233156/35d2e860-167e-2646-1d38-a3f204194529.jpeg)

## 目次

ここからはいくつかの **ステップに分けて**説明していきたいと思います

**Step1.** [まずはビルドしてみる!](#step1-まずはビルドしてみる)
**Step2.** [改めて実装してみる](#step2-ardefaultresolverを改めて実装してみる)
**Step3.** [Pythonモジュールを作成する](#step3-pythonモジュールを作成する)
**Step4.** [ArPathmapResolverを作成する](#step4-arpathmapresolverを作成する)

また、これらの過程の中で、
**このCMakeのオプション何で使うの？** とか **このマクロって何してるんだ？**
という事があったので、それらを **用語集** として用意したので
そちらもご確認して頂けると嬉しいです

https://qiita.com/yu_kita/items/e2c39e91529492d6370b

## 開発環境
**OS :**
`AlmaLinux8`, `Windows10`

**コンパイラ**:
`gcc 8.5.0`, `MSVC 19.29.30153.0`

**ビルドツール**:
`cmake 3.25+`

**バージョン :**
`Houdini 20.0.653`,

**ソースコード管理**
`git`

**エディタ:**
`VSCode`

# Step1. まずはビルドしてみる！

ここでは、まずHoudiniでビルドされたUSDから、
**ヘッダーやライブラリを読み込んでビルドする事を目標** に進めていきたいと思います

## Gitからリポジトリを読み込む

```
git clone -b learning/jp_step1_simple_build https://github.com/yu-kita89/USD_PathmapResolver.git USD_PathmapResolver_Step1
```
上記のコマンドで `USD_PathmapResolver_Step1` をクローンするか、
[ここ](https://github.com/yu-kita89/USD_PathmapResolver/tree/learning/jp_step1_simple_build) からダウンロードして下さい

## フォルダ構成
フォルダ構成は下記のようになっています。__各ファイルに説明書き__ もしているので
ここでは、ざっくりと概要のみ説明します
```text
 USD_PathmapResolver_Step1
 ├── CMakeLists.txt            # メインの処理をするcmakeファイル
 └── src
     ├── CMakeLists.txt        # 実際にライブラリなどをビルドするcmakeファイル
     ├── debugCodes.cpp        # デバッグを有効にするためのファイル
     ├── debugCodes.h
     ├── pathmapResolver.json  # Houdini用のパッケージファイル
     ├── plugInfo.json.in      # プラグインを読み込むためのリソースファイル
     ├── resolver.cpp          # AssetResolverのメイン処理
     └── resolver.h            
```

### `CMakeLists.txt`

:::note
- コンパイラなどのプロジェクトの設定
- ライブラリやヘッダーを読み込むためのディレクトリを設定
- `src` ディレクトリを読み込み
:::

:::note warn
ソース内でPythonに関する記述をしていない場合でも、
内部的に参照しているため、Pythonのヘッダーやライブラリの指定が必要になります
:::

### `/src/CMakeLists.txt`

:::note
- リンクするライブラリやヘッダーの設定
- ライブラリの作成
- `plugInfo.json.in` を `plugInfo.json` に変換
- インストールするファイルの設定
:::

### `/src/debugCodes`

:::note
TF_DEBUGを利用可能にするための設定
:::

### `/src/pathmapResolver.json`

:::note
Houdiniでライブラリやリソースを読み込むためのパッケージ
[Houdiniパッケージ](https://www.sidefx.com/ja/docs/houdini/ref/plugins.html) こちらも参考に
:::

:::note
Linuxだとパッケージ内で __PXR_PLUGINPATH_NAME__ が有効にならないので
代わりに **HOUDINI_USD_DSO_PATH** を設定しています
:::

### `/src/plugInfo.json.in`

:::note
プラグインの登録に必要な情報を記述するためのファイルのテンプレート
最終的に `@...@` で囲まれた部分の変数は値に置き換わります

参考 : [`plugInfo.json`](https://qiita.com/yu_kita/items/e2c39e91529492d6370b#pluginfojson) | [`configure_file`](https://qiita.com/yu_kita/items/e2c39e91529492d6370b#configure_file)
:::

### `/src/resolver`

:::note
アセットパスを解決するための処理をここに記述
今回は単純なデバッグメッセージを表示しているだけです
:::

## ビルド
早速ビルドしていきますが、
ビルドする前に __HFS の環境変数を設定する必要__ があるので忘れないようにしましょう

```text:環境変数の設定
# cmd
set HFS=C:\Program Files\Side Effects Software\Houdini 20.0.653
# sh
export HFS=/opt/hfs20.0.653
```
cmakeを使ってビルドします　`USD_PathmapResolver_Step1` に移動して
```text:USD_PathmapResolver_Step1
mkdir build
cd build
cmake --fresh ..
cmake --build . --config Release --clean-first
cmake --install .
```
cmakeは下記のように一度に実行する事もできます
```
cmake --fresh .. && cmake --build . --config Release --clean-first && cmake --install .
```

:::note warn
### ビルドができない場合
- **HFS の環境変数はセットされているか**

`echo`コマンドなどで確認してみて下さい
その時に `"..."` のように `"` が一緒に含まれていないか注意して下さい
```shell
# Windows
echo %HFS% # C:\Program Files\Side Effects Software\Houdini 20.0.653

# Linux
echo $HFS  # /opt/hfs20.0.653
```

***

- **ジェネレーターは有効なものが指定されているか**

ビルドディレクトリにある、`CMakeCache.txt` にジェネレーターに関する情報が保存されているので、そちらを確認して下さい

また、`-G` や `-DCMAKE_CXX_COMPILER` を使ってジェネレーターを明示する事ができます
```
cmake -G "Visual Studio 16 2019" ..
cmake -DCMAKE_CXX_COMPILER="C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.28.29333\bin\Hostx64\x64\cl.exe" ..
```

***

- **ライブラリやヘッダーの指定が合っているか**

`-DCMAKE_VERBOSE_MAKEFILE` `--trace` `--debug-output` `--verbose` などで詳細を確認してみてください
```
cmake -DCMAKE_VERBOSE_MAKEFILE=TRUE ..
cmake --trace ..
cmake --debug-output ..
cmake --build . --verbose
```
***
- **ビルドタイプがReleaseではなく、Debugになっていないか**

cmakeのコマンドラインオプションでも明示的に指定する事ができます
```
cmake -DCMAKE_BUILD_TYPE=Release ..
```
ただ、 **Visual Studioを使っている場合は上記のオプションが効かない**可能性もあるので、ビルド時に下記を設定する事でビルドタイプを強制することができます
```
cmake --build . --config Release
```
***

- **過去にbuildしたキャッシュなどが残っていないか**

`CMakeCache.txt` が残ったまま再度ビルドすると、前回の設定が再利用されてしまうので、`--fresh` オプションを使って `CMakeCache.txt` をクリアにしましょう
```
cmake --fresh ..
```

また、ビルド時に `--clearn-first` を使う事で、一度クリーンにしてからビルドを実行する事ができるので、こちらも合わせて使用しましょう
```
cmake --build . --clean-first
```
:::

## 確認

ビルドが完了したら、`dist` ディレクトリ以下が下記のようになっていると思います
```text
 USD_PathmapResolver_Step1/dist
 └── pathmapResolver
     ├── lnx # Linux環境でビルドした場合
     │   ├── lib
     │   │   └── pathmapResolver.so
     │   ├── packages
     │   │   └── pathmapResolver.json
     │   └── resources
     │       └── plugInfo.json
     └── win # Windows環境でビルドした場合
         ├── lib
         │   ├── pathmapResolver.dll
         │   └── pathmapResolver.lib #インポートライブラリ
         ├── packages
         │   └── pathmapResolver.json
         └── resources
             └── plugInfo.json
```
それでは確認していきます

### 環境変数の設定

先程書き出した、`pathmapResolver.json` のパッケージを読み込めるように
**HOUDINI_PACKAGE_DIR** の環境変数を設定します

また、デバッグ用に **TD_DEBUG**、**HOUDINI_PACKAGE_VERBOSE** もセットします

::: note warn
`dist` 直下の `pathmapResolver` フォルダは移動してもらっても構いませんが、それ以下の階層は変更しないように注意して下さい
:::

```text:環境変数の設定
# cmd ※HOUDINI_PACKAGE_DIRは適宜変更して下さい
set HOUDINI_PACKAGE_DIR=C:\path\to\USD_PathmapResolver_Step1\dist\pathmapResolver\win\packages
set HOUDINI_PACKAGE_VERBOSE=1
set TF_DEBUG=AR_*

# sh ※HOUDINI_PACKAGE_DIRは適宜変更して下さい
export HOUDINI_PACKAGE_DIR=/path/to/USD_PathmapResolver_Step1/dist/pathmapResolver/lnx/packages
export HOUDINI_PACKAGE_VERBOSE=1
export TF_DEBUG=AR_*
```

### Houdiniで確認

セットしたら、続いて **Hython** を起動します
※ Houdiniを立ち上げてもらっても大丈夫です
```text:Hython
# Windows
"C:\Program Files\Side Effects Software\Houdini 20.0.653\bin\hython.exe"

# Linux
/opt/hfs20.0.653/bin/hython
```
その後、Pythonで下記コマンドを実行しましょう
```python
from pxr import Ar
resolver = Ar.GetResolver()
resolver.Resolve("Hello!")
```
コンソール上に下記のようなログがでていれば、正常にビルドされています

```
※...は省略
Loading: .../pathmapResolver/lnx/packages/pathmapResolver.json
...
ArGetResolver(): Found primary asset resolver types: [ArPathmapResolver, ArDefaultResolver]
ArGetResolver(): Using asset resolver ArPathmapResolver from plugin .../pathmapResolver/lnx/lib/pathmapResolver.so for primary resolver
...
[Resolve] "Hello!"
```

# Step2. ArDefaultResolverを改めて実装してみる
無事にビルドする事ができたので、次は

**`ArDefaultResolver` `ArDefaultResolverContext` クラスを
改めて実装してみたいと思います**

全てではなく、必要な関数のみをオーバーライドしていきます

## Gitからリポジトリを読み込む

```
git clone -b learning/jp_step2_reimplement https://github.com/yu-kita89/USD_PathmapResolver.git USD_PathmapResolver_Step2
```
上記のコマンドで `USD_PathmapResolver_Step2` をクローンするか、
[ここ](https://github.com/yu-kita89/USD_PathmapResolver/tree/learning/jp_step2_reimplement) からダウンロードして下さい

## フォルダ構成
[前回](#フォルダ構成)から `resolverContext` ファイルが増えました
主に、変更箇所がある部分のみ説明していきたいとおもいます
```
 USD_PathmapResolver_Step2
!├── CMakeLists.txt
 └── src
!    ├── CMakeLists.txt
!    ├── debugCodes.cpp
!    ├── debugCodes.h
     ├── pathmapResolver.json
     ├── plugInfo.json.in
!    ├── resolver.cpp
!    ├── resolver.h
+    ├── resolverContext.cpp # AssetResolverContextのメイン処理
+    └── resolverContext.h
```

### `CMakeLists.txt`

:::note
- ライブラリやヘッダーのディレクトリに **boost** を追加
:::

### `/src/CMakeLists.txt`

:::note
- リンクするライブラリやヘッダーに **boost** を追加
- ライブラリのソースに `resolverContext` を追加
:::

:::note warn
**boost** ライブラリは直接使用していませんが、
`resolverContext` で内部的に参照しているため、
**boost** のヘッダーやライブラリの指定が必要になります
:::

### `/src/debugCodes`

:::note
TF_DEBUGに **AR_PATHMAPRESOLVER_CONTEXT** を追加
:::

### `/src/resolver`

:::note
関数などを改めて実装しています、
デバッグメッセージのみを追加して、挙動は `ArDefaultResolver` と同じ結果となるようにしています

- **下記関数などを追加**
  - static:
    - _IsFileRelative
    - _IsRelativePath
    - _IsSearchPath
    - _ParseSearchPaths
    - _ResolveAnchored
    - _ArPathmapResolverFallbackContext
  - public:
    - SetDefaultSearchPath
  - protected:
    - _Resolve
    - _CreateDefaultContext
    - _CreateDefaultContextForAsset
    - _CreateContextFromString
  - private:
    - _GetCurrentContextPtr
:::

### /src/resolverContext

:::note
こちらも、関数などを改めて実装しています

- **下記関数などを追加**
  - public:
    - `==` オペレータ
    - GetAsString
    - hash_value
    - ArGetDebugString
  
:::

### Resolveのフローチャート
`ArDefaultResolver` で `Resolve` 関数が呼ばれた時の流れをフローチャートにしました
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233156/ef4081b2-ad27-7fd4-a6c0-b7b68957568a.jpeg)

## ビルド
[Step1](#ビルド) と手順は同じなので、そちらを参考に

## 確認
[Step1](#確認) でHoudiniを立ち上げるまでの手順は同じですが、
**HOUDINI_PACKAGE_DIR** の場所は、今回のビルドで書き出された場所へと適宜変更をお願いします

今回は下記Pythonコマンドを実行してみます
```python
from pxr import Ar
resolver = Ar.GetResolver()
resolver.Resolve("Hello!")
resolver.Resolve("bin")
```

コンソール上に下記のようなログがでていれば、正常にビルドされています

```
※...は省略
Loading: .../pathmapResolver/lnx/packages/pathmapResolver.json
...
ArGetResolver(): Found primary asset resolver types: [ArPathmapResolver, ArDefaultResolver]
ArGetResolver(): Using asset resolver ArPathmapResolver from plugin .../pathmapResolver/lnx/lib/pathmapResolver.so for primary resolver
...
[Resolve] Trying to resolve the path. : "Hello!"
[Resolve]   The path is relative.
[Resolve]   The path is search path.
[Resolve]   Searching from "/opt/hfs20.0.653".
[Resolve]   The path could not be resolved, returning an empty path.
Ar.ResolvedPath()
...
[Resolve] Trying to resolve the path. : "bin"
[Resolve]   The path is relative.
[Resolve]   The path is search path.
[Resolve]   Searching from "/opt/hfs20.0.653".
[Resolve]   The path is resolved : "/opt/hfs20.0.653/bin".
Ar.ResolvedPath('/opt/hfs20.0.653/bin')
```

:::note warn
この段階では、Pythonでコンテキストを作成しようとしても、C++で派生したコンテキストのクラスがPythonで使える状態ではないためエラーになります。
```python:
from pxr import Ar
resolver = Ar.GetResolver()
resolver.CreateDefaultContext()
# TypeError: No to_python (by-value) converter found for C++ type: ...
```
:::

# Step3. Pythonモジュールを作成する
関数を改めて実装する事ができたので、次は

**Pythonからモジュールを呼び出して、
コンテキスト等を作成できるようにしていきたいと思います**
 
## Gitからリポジトリを読み込む

```
git clone -b learning/jp_step3_python-module-setup https://github.com/yu-kita89/USD_PathmapResolver.git USD_PathmapResolver_Step3
```
上記のコマンドで `USD_PathmapResolver_Step3` をクローンするか、
[ここ](https://github.com/yu-kita89/USD_PathmapResolver/tree/learning/jp_step3_python-module-setup) からダウンロードして下さい

## フォルダ構成
今回からさらにファイルが増えました
```
 USD_PathmapResolver_Step3
!├── CMakeLists.txt
 └── src
!    ├── CMakeLists.txt
+    ├── __init__.py             # Pythonモジュールを定義してラップされたクラスをインポート
     ├── debugCodes.cpp
     ├── debugCodes.h
+    ├── module.cpp              # Pythonで使用するためのクラスや関数を宣言、呼び出し
+    ├── moduleDeps.cpp          # Pythonで使用するためのライブラリを登録
!    ├── pathmapResolver.json
     ├── plugInfo.json.in
     ├── resolver.cpp
     ├── resolver.h
     ├── resolverContext.cpp
     ├── resolverContext.h
+    ├── wrapResolver.cpp        # Resolverをラップするための関数
+    └── wrapResolverContext.cpp # ResolverContextをラップするための関数
```
### `CMakeLists.txt`

:::note
- Pythonモジュールを作成するための変数を追加
:::

### `/src/CMakeLists.txt`

:::note
- Pythonモジュールで使用するためのライブラリを作成
:::

### `/src/__init__.py`

:::note
- Pythonモジュールを定義してラップされたクラスをインポートするためのファイル
:::

### `/src/module`

:::note
- Pythonで使用できるように、ラップするための関数を宣言、呼び出し
- 関数は [wrapResolver](#srcwrapresolver) | [wrapResolverContext](#srcwrapresolvercontext) 内で実装します
:::

### `/src/moduleDeps`

:::note
- Pythonモジュールが依存しているライブラリを適切に読み込めるようにするための処理
:::

### `/src/pathmapResolver.json`

:::note
- `PYTHON_PATH` を追加
:::

### `/src/wrapResolver`

:::note
`- boost::python` で `Resolver` クラスを登録するための関数を定義
:::

### `/src/wrapResolverContext`

:::note
`- boost::python` で `ResolverContext` クラスを登録するための関数を定義
:::


## ビルド
[Step1](#ビルド) と手順は同じなので、そちらを参考に

## 確認
ビルドが完了したら、`dist` ディレクトリ以下が下記のようになっていると思います
```text
 USD_PathmapResolver_Step3/dist
 └── pathmapResolver
     ├── lnx
     │   ├── lib
     │   │   ├── pathmapResolver.so
+    │   │   └── python
+    │   │       └── usdCustom # Pythonパッケージ
+    │   │           └── PathmapResolver
+    │   │               ├── __init__.py
+    │   │               └── _pathmapResolver.so
     │   ├── packages
     │   │   └── pathmapResolver.json
     │   └── resources
     │       └── plugInfo.json
     └── win
         ├── lib
         │   ├── pathmapResolver.dll
         │   ├── pathmapResolver.lib
+        │   └── python
+        │       └── usdCustom # Pythonパッケージ
+        │           └── PathmapResolver
+        │               ├── __init__.py
+        │               ├── _pathmapResolver.lib
+        │               └── _pathmapResolver.pyd
         ├── packages
         │   └── pathmapResolver.json
         └── resources
             └── plugInfo.json
```
Pythonに関係するファイルが追加されています
それでは確認していきます

[Step1](#確認) でHoudiniを立ち上げるまでの手順は同じですが、
**HOUDINI_PACKAGE_DIR** の場所は、今回のビルドで書き出された場所へと適宜変更をお願いします

今回は下記Pythonコマンドを実行してみます
```python
import os
from pxr import Ar
Ar.GetResolver()
from usdCustom import PathmapResolver

resolver = Ar.GetUnderlyingResolver()
ctx = resolver.CreateContextFromString(os.environ["HFS"])
with Ar.ResolverContextBinder(ctx):
    resolver.Resolve("houdini/usd/assets/rubbertoy/rubbertoy.usd")
```

コンソール上に下記のようなログがでていれば、正常にビルドされています

```
※...は省略
Loading: .../pathmapResolver/lnx/packages/pathmapResolver.json
...
ArGetResolver(): Found primary asset resolver types: [ArPathmapResolver, ArDefaultResolver]
ArGetResolver(): Using asset resolver ArPathmapResolver from plugin .../pathmapResolver/lnx/lib/pathmapResolver.so for primary resolver
...
[Resolve] Trying to resolve the path. : "houdini/usd/assets/rubbertoy/rubbertoy.usd"
[Resolve]   The path is relative.
[Resolve]   The path is search path.
[Resolve]   Searching from "/opt/hfs20.0.653".
[Resolve]   The path is resolved : "/opt/hfs20.0.653/houdini/usd/assets/rubbertoy/rubbertoy.usd".
Ar.ResolvedPath('/opt/hfs20.0.653/houdini/usd/assets/rubbertoy/rubbertoy.usd')
```

# Step4. ArPathmapResolverを作成する
これで、`ArDefaultResolver` の機能はある程度確認できたので、最後に

__ArPathmapResolver を作成していきたいと思います__

## 仕様

今回作成するカスタムリゾルバーの仕様を決めたいと思います。

まずは、環境変数で設定されたパスマップを
USDのアセット解決時に、利用できるようにします。

また、柔軟に対応できるように、リゾルバーには直接組み込まず、
コンテキストによって変更できるようにします

使用する環境変数はデフォルトだと `HOUDINI_PATHMAP` ですが、
`AR_PATHMAP_ENVIRONMENT` という環境変数、または
`SetDefaultPathmapEnvironment` という関数を使って変更出来るようにします

さらに、`CreateContextFromString` で直接パスマップを指定できるようにします
書き方は `,` を区切り文字としてそれ以降をパスマップとして扱うようにします

***

**まとめると**

:::note
***
- **パスマップの設定:**
  - デフォルトでは **`HOUDINI_PATHMAP`** 環境変数を使用してパスマップを設定
  - **`AR_PATHMAP_ENVIRONMENT`** 環境変数や、
    **`SetDefaultPathmapEnvironment`** 関数を使って、
    使用する環境変数を変更できるように
```sh:
# cmd
set ANY_PATHMAP={'C:':'D:'}
set AR_PATHMAP_ENVIRONMENT=ANY_PATHMAP
# sh
export ANY_PATHMAP="{'C:':'D:'}"
set AR_PATHMAP_ENVIRONMENT=ANY_PATHMAP
```
```python:Python
from pxr import Ar
Ar.GetResolver()
from usdCustom import PathmapResolver
resolver = Ar.GetUnderlyingResolver()
resolver.SetDefaultPathmapEnvironment("ANY_PATHMAP")
```
***
- **コンテキストに応じたパスマップの変更:**
  - パスマップの設定は、コンテキストに基づいて
    **柔軟に切り替えられるように**する
***
- **`CreateContextFromString` によるパスマップ指定:**
  - `CreateContextFromString` 関数に渡された文字列でもパスマップは指定可能
  - **文字列の `,` 以降をパスマップとして扱い**、それに基づいてリゾルバーがパスマップを設定
```python:Python
from pxr import Ar
Ar.GetResolver()
from usdCustom import PathmapResolver
resolver = Ar.GetUnderlyingResolver()
resolver.CreateContextFromString("hoge:fuga,{'C:':'D:'}")
"""
Search path: [
    hoge
    fuga
]
Pathmap dict: [
    C: : D:
]
"""
```
***
:::

## Gitからリポジトリを読み込む

```
git clone -b learning/jp_step4_create-arpathmapresolver https://github.com/yu-kita89/USD_PathmapResolver.git USD_PathmapResolver_Step4
```
上記のコマンドで `USD_PathmapResolver_Step4` をクローンするか、
[ここ](https://github.com/yu-kita89/USD_PathmapResolver/tree/learning/jp_step4_create-arpathmapresolver) からダウンロードして下さい

## フォルダ構成
```
 USD_PathmapResolver_Step3
 ├── CMakeLists.txt
 └── src
     ├── CMakeLists.txt
     ├── __init__.py
     ├── debugCodes.cpp
     ├── debugCodes.h
     ├── module.cpp
!    ├── moduleDeps.cpp
     ├── pathmapResolver.json
     ├── plugInfo.json.in
!    ├── resolver.cpp
!    ├── resolver.h
!    ├── resolverContext.cpp
!    ├── resolverContext.h
!    ├── wrapResolver.cpp
!    └── wrapResolverContext.cpp
```

### `/src/moduleDeps`

:::note
- 依存するライブラリに `vt` `js` を追加
:::

### `/src/resolver`

:::note
- **下記関数を調整**
  - static:
    - _ParseSearchPaths
    - _ResolveAnchored
    - _ArPathmapResolverFallbackContext
  - public:
    - SetDefaultSearchPath
  - protected:
    - _Resolve
    - _CreateContextFromString
- **下記関数を追加**
  - static:
    - _ParsePathmapDict
  - public:
    - SetDefaultPathmapEnvironment
:::

### `/src/resolverContext`

:::note
- パスマップ辞書を引数に持つコンストラクタを追加
- **下記関数を調整**
  - public:
    - `==` オペレータ
    - GetAsString
    - hash_value
- **下記関数を追加**
  - public:
    - GetPathmapDict
:::

### `/src/wrapResolver`

:::note
- `SetDefaultPathmapEnvironment` 静的関数の追加
:::

### `/src/wrapResolverContext`

:::note
- パスマップ辞書を引数に持つコンストラクタや、
  `GetPathmapDict` 関数の追加
:::

### パスマップのフローチャート
結果 `ArDefaultResolver` の `Resolve` 関数の途中に処理を入れた形になりました

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233156/be6fdf9f-d499-e9a2-157a-eb71fdc37ed5.jpeg)

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233156/d1031771-f756-a917-05b7-50c35a50e039.jpeg)

## ビルド
[Step1](#ビルド) と手順は同じなので、そちらを参考に

## 確認
### 環境変数の設定

今回はパスマップを適用するための環境変数も追加したいと思います

```text:環境変数の設定
# cmd ※HOUDINI_PACKAGE_DIRは適宜変更して下さい
set HOUDINI_PACKAGE_DIR=C:\path\to\USD_PathmapResolver_Step1\dist\pathmapResolver\win\packages
set HOUDINI_PACKAGE_VERBOSE=1
set TF_DEBUG=AR_*
set CUSTOM_PATHMAP={'foo':'var'}
set AR_PATHMAP_ENVIRONMENT=CUSTOM_PATHMAP

# sh ※HOUDINI_PACKAGE_DIRは適宜変更して下さい
export HOUDINI_PACKAGE_DIR=/path/to/USD_PathmapResolver_Step1/dist/pathmapResolver/lnx/packages
export HOUDINI_PACKAGE_VERBOSE=1
export TF_DEBUG=AR_*
export CUSTOM_PATHMAP="{'foo':'var'}"
export AR_PATHMAP_ENVIRONMENT=CUSTOM_PATHMAP
```

### Houdiniで確認

セットしたら、続いて **Hython** を起動します
※ Houdiniを立ち上げてもらっても大丈夫です
```text:Hython
# Windows
"C:\Program Files\Side Effects Software\Houdini 20.0.653\bin\hython.exe"

# Linux
/opt/hfs20.0.653/bin/hython
```
その後、Pythonで下記コマンドを実行しましょう
```python
import os
from pxr import Ar
# Initialize
Ar.GetResolver()
# Moduleの読み込み
from usdCustom import PathmapResolver

# パスマップのキーとバリューをセット
k = os.path.join(os.environ["HFS"],"houdini/usd/assets/hoge")
v = os.path.join(os.environ["HFS"],"houdini/usd/assets/rubbertoy")
# 間違ったパスをセット
path = os.path.join(k,"rubbertoy.usd")

# Resolverの取得
resolver = Ar.GetUnderlyingResolver()
resolver.Resolve(path) # Unresolve

# パスマップの環境変数を変更
os.environ["HOUDINI_PATHMAP"] = "{'%s':'%s'}"%(k,v)
resolver.SetDefaultPathmapEnvironment("HOUDINI_PATHMAP")
resolver.Resolve(path) # Resolve

# 再度パスマップを変更
resolver.SetDefaultPathmapEnvironment("CUSTOM_PATHMAP")
resolver.Resolve(path) # Unresolve

# コンテキストを作成、バインドして解決
ctx = resolver.CreateContextFromString(",%s"%(os.environ["HOUDINI_PATHMAP"]))
with Ar.ResolverContextBinder(ctx):
    resolver.Resolve(path) # Resolve
```
コンソール上に下記のようなログがでていれば、正常にビルドされています

```
※...は省略
Loading: .../pathmapResolver/lnx/packages/pathmapResolver.json
...
ArGetResolver(): Found primary asset resolver types: [ArPathmapResolver, ArDefaultResolver]
ArGetResolver(): Using asset resolver ArPathmapResolver from plugin .../pathmapResolver/lnx/lib/pathmapResolver.so for primary resolver
...
[DefaultPathmapEnvironment] Set : "CUSTOM_PATHMAP"
Search path: [
    /opt/hfs20.0.653
]
Pathmap dict: [
    foo : var
]
[Resolve] Trying to resolve the path. : "/opt/hfs20.0.653/houdini/usd/assets/hoge/rubbertoy.usd"
[Resolve]   The path could not be resolved, returning an empty path.
Ar.ResolvedPath()
[SetDefaultPathmapEnvironment] Set : "HOUDINI_PATHMAP"
Search path: [
    /opt/hfs20.0.653
]
Pathmap dict: [
    /opt/hfs20.0.653/houdini/usd/assets/hoge : /opt/hfs20.0.653/houdini/usd/assets/rubbertoy
]
[Resolve] Trying to resolve the path. : "/opt/hfs20.0.653/houdini/usd/assets/hoge/rubbertoy.usd"
[Resolve]   Mapped path from "/opt/hfs20.0.653/houdini/usd/assets/hoge" to "/opt/hfs20.0.653/houdini/usd/assets/rubbertoy".
[Resolve]   The path is resolved : "/opt/hfs20.0.653/houdini/usd/assets/rubbertoy/rubbertoy.usd".
Ar.ResolvedPath('/opt/hfs20.0.653/houdini/usd/assets/rubbertoy/rubbertoy.usd')
[SetDefaultPathmapEnvironment] Set : "CUSTOM_PATHMAP"
Search path: [
    /opt/hfs20.0.653
]
Pathmap dict: [
    foo : var
]
[Resolve] Trying to resolve the path. : "/opt/hfs20.0.653/houdini/usd/assets/hoge/rubbertoy.usd"
[Resolve]   The path could not be resolved, returning an empty path.
Ar.ResolvedPath()
[CreateContextFromString] Creating context from string. : ",{'/opt/hfs20.0.653/houdini/usd/assets/hoge':'/opt/hfs20.0.653/houdini/usd/assets/rubbertoy'}"
[CreateContextFromString]   Search path : ""
[CreateContextFromString]   Pathmap     : "{'/opt/hfs20.0.653/houdini/usd/assets/hoge':'/opt/hfs20.0.653/houdini/usd/assets/rubbertoy'}"
Search path: [ ]
Pathmap dict: [
    /opt/hfs20.0.653/houdini/usd/assets/hoge : /opt/hfs20.0.653/houdini/usd/assets/rubbertoy
]
[Resolve] Trying to resolve the path. : "/opt/hfs20.0.653/houdini/usd/assets/hoge/rubbertoy.usd"
[Resolve]   Mapped path from "/opt/hfs20.0.653/houdini/usd/assets/hoge" to "/opt/hfs20.0.653/houdini/usd/assets/rubbertoy".
[Resolve]   The path is resolved : "/opt/hfs20.0.653/houdini/usd/assets/rubbertoy/rubbertoy.usd".
Ar.ResolvedPath('/opt/hfs20.0.653/houdini/usd/assets/rubbertoy/rubbertoy.usd')
```

***

**GUI上でも確認してみましょう**

`Rubber Toy` ノードからアセットを読み込んで、リファレンスパスを
`$HH/usd/assets/rubbertoy/rubbertoy.usd` から
`$HH/usd/assets/hoge/rubbertoy.usd` に変更します
ここで `Rubber Toy` が読み込めなくなります

`Configure Stage` ノードを作成して `Rubber Toy` のインプットに接続
`Number of Resolver Context Strings` のパラメータを1にセットして
`Value` パラメータに下記をセット
```
,{"$HH/usd/assets/hoge":"$HH/usd/assets/rubbertoy"}
```

`Rubber Toy` ノードの `Reload File` ボタンを押す
`Rubber Toy`が読み込めたら正常に動作しています

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233156/61b17bba-d3e8-a600-6833-2312ad3aa3ba.gif)

# おわりに

お疲れ様でした！
色々と書いていく内に凄く長くなってしまいました...

今回は Houdini を使って `ArPathmapResolver` を作成しましたが
他のDCCツールでも置き換えたりすれば、同じようにビルドできるかと思います

これはシンプルなカスタムリゾルバーの一例でしたが、
カスタムリゾルバーを作成したい！という人の一助になれば幸いです

僕自身も、色んな人の記事で凄く勉強させてもらっているので
こういった情報の共有をこれからも続けていければなと思います！

ありがとうございました！