Copernicusノードからカメラ情報を取得し、カメラ光線を求める

# はじめに
Copernicusノードを使用した時、 **カメラの情報ってどうやって取得するんだろう？** それができればカメラ光線が定義できるのでWrangle COPでintersect関数を使用して光線を飛ばせるのにと思い、今回はその方法を紹介したいと思います。

# シーンの平面について
カメラから見たシーンの平面の素材を書き出す場合の基本的なCopernicusノードのネットワークは図のような構成になると思います。
まず、このようなネットワークでビューポートに表示される平面について見ていきます。

![Copernicusネットワーク](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/4d946f05-8a3c-3cd6-862f-47575168b6b3.png)

1. Camera Import COPを使用して、カメラを取り込む。
1. SOP Import COPを使用して、シーンを取り込む。
1. Rasterize Setup COPを使用して、そのシーンのジオメトリをラスター化するための事前準備をする。
1. Rasterize Geometry COPを使用して、ジオメトリをラスター化する。

Copernicusが搭載されてSolarisと同様にScene Importがあるのかなと思ってたのですが、COPコンテキストにシーンを取り込むのはSOP Import COPしかなさそうです。
Scene Import COPみたいなのあったら嬉しいです。
現状だと、SOP Import COPの中でObject Merge SOPを追加して、
Object1パラメータに、
```command:エクプレッション
`run("opfind -p ルートパス -n * -t geo")`
```
![ObjectMerge](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/d6a318c4-dd49-d873-a42a-32b837d65b35.png)
のようにすることで、指定したルートパス以下のすべてのジオメトリを読み込むことができます。

これでRasterize Gemetry COPでP平面を出力してみたのですが、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/6057d394-6832-a8dc-1f4b-492f7d35ea58.png)

そのRasterize Gemetry COPが表示する平面からカメラまでの距離って何なのか気になりませんか？
自分は気になったので調べました。この距離は以下の式で表現されていました。

```c++:カメラからこの平面までの距離d
d = Focal Lenght*2.0 / Aperture
```

次に、この平面からカメラの情報を取得したいです。
それはカメラノードを参照すれば情報が取得できるだろと言われそうですが、
**COPコンテキスト内だけで完結させてカメラ情報を取得したい** のです。

ネットワークエディタでCamera Import COPノードを中クリックしてノードInfoを表示すると、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/9cf20638-be91-b939-8615-9459f134b75a.png)
COPノード自体がカメラ情報を所有してることがわかりました。
この情報をパラメータエクスプレッションとかWrangleとかOpenCLとかで利用したいのですが、どうやって取得すればよいのか随分悩みました。
結局、PythonでノードのinfoTree (https://www.sidefx.com/ja/docs/houdini/hom/hou/NodeInfoTree.html) の **ブランチのブランチ** から取得できました(取得する方法に悩んだのは、まさかブランチのブランチに情報が隠れてたとは思いませんでした)。

```python:Infoツリー情報を返すPythonコード
output = hou.node("Camera Importノードのパス").infoTree().branches()["COP Info"].branches()["camera_ref"].rows()

#結果
(('camera_ref', 'layer, use_count 3'), ('dataWindow', '1280×720'), ('buffer', '1280
×720×1, float32, wrap, constant values, onCPU'), ('camera', 'persp, aperture: 41.4
214, focal: 50, camera: (0, 0, 2.41421), fstop: 5.6, focus: 5 shutter: -0.25-0.25'),
 ('xform', '[0.8925389352890299, 0.15737869562426265, 0.42261826174069944, 0, 0.0577
1513718295637, 0.8895619774524623, -0.4531538935183249, 0, -0.4472619053005489, 0.42
884896459531124, 0.7848855672213958, 0, -1.9202153352182654, 2.964668061398222, 3.10
5120536646928, 1]'))
```

ここで注意すべき事は、この **xform** はカメラのトランスフォーム行列ではなく、 **シーン平面のトランスフォーム行列** です。


# カメラのトランスフォーム行列とApertureとFocal Lengthを取得するパラメータエクスプレッションを作成する

今回は、Wrangle COP上に、カメラのトランスフォーム行列とApertureとFocalLengthの値が文字列として格納されるSpareパラメータを作成したいと思います。

1. Wrangle COP上にStringタイプのパラメータを追加、名前をcameInfoにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/1035f238-948e-fb99-4867-c5b0afc83bac.png)
1. "camInfo" SpareパラメータのテキストフィールドでAltを押しながらEキーを押してエディタを開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/ba2b5cd9-ce9a-9434-8392-2f87736edcd5.png)
1. エクスプレッション言語をPython Expressionに変更して、以下のエクスプレッションを入力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/33a5925f-4b28-95d9-434a-addd7714777d.png)

```python:カメラのトランスフォーム行列とApertureとFolcal Lengthを文字列で返すPythonエクスプレッション
outputBranches = hou.node("../Camera_Import").infoTree().branches()["COP Info"].branches()
cameraRef = None
for i in outputBranches:
        cameraRef = outputBranches[i]
        break
cameraInfo = dict(cameraRef.rows())

output = ""
for i in cameraInfo["xform"].strip("[]").split(","):
        output+= i.strip() + " "

aperture = ""
focal = ""
for i in cameraInfo["camera"].split(","):
        if "aperture" in i:
                aperture = i.split(":")[-1].strip()
        elif "focal" in i:
                focal = i.split(":")[-1].strip()
        if aperture!="" and focal!="":
                break

output += aperture+" "+focal

return output
```

```text:トランスフォーム行列16個のfloatとApertureとFocal Lengthをスペースで区切った文字列
0.8925389352890299 0.15737869562426265 0.42261826174069944 0
0.05771513718295637 0.8895619774524623 -0.4531538935183249 0
-0.4472619053005489 0.42884896459531124 0.7848855672213958 0
-1.9202153352182654 2.964668061398222 3.105120536646928 1
41.4214 50
```

これでCOPコンテキスト内でカメラ情報を取得し、Spareパラメータとして参照できるようになりました。

# Wrangle COPでintersect関数をカメラ光線として使用する
Wrangle COPでは、`@P`、`@ix`、`@iy`、`@resx`、`@resy`とか使用できます。
`@ix`と`@iy`は平面のX方向、Y方向の1ピクセル単位の位置です。
`@resx`と`@resy`は平面のX方向、Y方向の解像度です。
`@P`はシーン平面のグローバル座標です。
ということは、`@P`から`カメラ原点座標`を引けばカメラ光線の方向が求まります。
今回は、Rasterize Geometry COPを使用せずに、Wrangle COPを使用してP平面を定義したいと思います。

Wrangle COPには上記の"camInfo"Spareパラメータとエクスプレッションを設定した上で、
以下の2つの入力を用意して
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/e7c139f2-4d8f-9644-793c-a86b91a6f7c3.png)
以下のコードを入力します。

```c++:カメラから@Pに向かったカメラ光線がオブジェクトに当たった位置を出力する
string camInfo = chs("camInfo");
string tokens[]=split(camInfo);

matrix mat =ident();
float transform[]={0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
for(int i=0;i<16;i++){
    transform[i]= atof(tokens[i]);
}
mat = set(transform);
float focal_length = atof(tokens[-1]);
float aperture = atof(tokens[-2]);

vector cameraPos = {0.0,0.0,0.0};
cameraPos+=set(0.0,0.0,focal_length*2/aperture);
cameraPos*=mat;
vector dir = normalize(@P - cameraPos);

vector pos;
vector uv;
int prim = intersect(1, cameraPos, 10000.0*dir, pos, uv);
if (prim>-1) {
    @C =pos;
} else {
    @C = 0.0;
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/8370e08c-f200-d2a7-5a4e-0d02ae98c4e9.png)
このノード構成だけで、P平面を表現することができました。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/a8c7c420-1efa-8c9a-f746-e40487d1e1cb.png)

# シーン平面をXY平面に置いた場合
シーン平面をカメラのフラスタム領域に配置するよりは、Match Camera COPを使用してシーン平面を常にXY平面に置きたいことでしょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/2a778c66-48d4-69ec-21d0-8438c7a4a393.png)

そうした場合、カメラ光線は、上記のWrangleコードでは動作しません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/35c5c230-7e4a-378c-f3ea-c4c5f92e5c21.png)

XY平面のP座標とカメラのXYZ軸の関係からカメラ光線を計算する必要があります。
これは、Houdiniのドキュメント( https://www.sidefx.com/ja/docs/houdini/ref/cameralenses.html )に載っている図のようにFocal LengthとApertureの関係性から求めることができます。

![fov.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/385d9bfc-dbb2-fb42-16d4-14bee87048e0.png)

コードは以下のようになります。

```c++:シーン平面をMatch Cameraでトランスフォームさせた場合のカメラ光線
string camInfo = chs("camInfo");
string tokens[]=split(camInfo);

matrix mat =ident();
float transform[]={1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
for(int i=0;i<16;i++){
    transform[i]= atof(tokens[i]);
}
mat = set(transform);

float focal_length = atof(tokens[-1]);
float aperture = atof(tokens[-2]);
float theta = atan(0.5f*aperture/focal_length);

vector cameraPos = {0.0,0.0,0.0};
cameraPos+=set(0.0,0.0,focal_length*2/aperture);
cameraPos*=mat;

float distance = -0.5f/tan(theta);
matrix3 rot = (matrix3)mat;
vector camX = rot*{1,0,0};
vector camY = rot*{0,1,0};
vector camZ = rot*{0,0,1};
vector dir = normalize(0.5*@P.x*camX + 0.5*@P.y*camY + distance*camZ);

vector pos;
vector uv;
int prim = intersect(1, cameraPos, 10000.0*dir, pos, uv);
if (prim>-1) {
    @C =pos;
} else {
    @C = 0.0;
}
```

すると、以下の図のように正しい挙動になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/85f7f4f8-8eb8-f140-1e8d-f4ab0fbc03a7.png)


P平面だけじゃ寂しいので、intersect関数を使用してコンターを求めてみました。
![2024-12-09 23_49_19-Window.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/b00a03c7-2364-5989-8d45-b4e1a414dc64.png)



シーンファイル:
https://drive.google.com/file/d/1F-TC9pyAd3lC-r_YargDbXKtfExoy3uY/view?usp=drive_link

検証環境:
Houdini20.5.410Py3.11

# まとめ
- SOP Import COPでもシーン上のジオメトリをまるごとインポートする方法があるよ。
- CopernicusノードからPythonエクスプレッションを使用することでカメラ情報を取得することができるよ。
- Rasterize Geometry COPはパックプリミティブに対して動作しなかった。代わりにWrangle COPでintersect関数を使って光線を飛ばすことでパックプリミティブに対してP平面を出力することができたよ。