エッジに対してWrangleする

# はじめに
**Attribute Wrangle SOP** を使用してコードをゴリゴリ書く時、
**Run Over** でそのコードをどのエレメントに対して走らせるのか指定するわけですが、Detail、Primitives、Points、Verticesが選べるのに **Edgesがありません。**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/c79c0c6b-582d-e571-a779-21583d3b3a95.png)
エッジに対して何かしらの処理をしたい場合はどうすれば良いのでしょう？
エッジはプリミティブ、ポイント、頂点を利用して代用することが可能です。しかし、回りくどいのです。
ここでは、エッジを処理するのに扱いやすいものとしてハーフエッジの簡単な使い方について説明したいと思います。


# ハーフエッジとは?
ハーフエッジについてはヘルプがわかりやすいです。
これは別にHoudini用語でもなくCGのジオメトリのトポロジー表現の考え方の1つです。ヘルプにはこういう説明もちゃんとあるから好きです。
https://www.sidefx.com/ja/docs/houdini/vex/halfedges
そんな難しく構える必要はまったくなくて、頂点とポイントの関係と似ていて、エッジにはプライマリハーフエッジとセカンダリハーフエッジが存在します。

- 複数のフェースと共有してるエッジにはプライマリハーフエッジとセカンダリハーフエッジの両方が存在する
- 共有してないエッジ(開いたサーフェスの境界エッジとか穴の境界エッジ)はプライマリハーフエッジのみを持つ。

ここを抑えておけばWrangleでエッジの扱いはそんなに難しくないです。

# ハーフエッジを利用してエッジを処理するコード
```c++:Run Over = Point
//Point Wrangle SOPを使ってエッジを処理するための基本コード
i[]@primaryHalfEdge;
int primaryHalfEdge = pointhedge(0, @ptnum);
while ( primaryHalfEdge != -1 )
{
    if(hedge_isprimary(0, primaryHalfEdge))
    {
        int srcPtnum = hedge_srcpoint(0, primaryHalfEdge);//ハーフエッジの始点
        if(@ptnum==srcPtnum)
        {
            //ここの中でエッジ単位の処理をすることができる。
            //ポイントアトリビュートにハーフエッジ番号を格納して結果を確認する。
            push(@primaryHalfEdge,primaryHalfEdge);
        }    
    }
    primaryHalfEdge = pointhedgenext(0, primaryHalfEdge);
}; 
```
Boxに対してPoint Wrangle SOPを接続して結果を見ると、各ポイントのprimaryHalfEdgeユーザアトリビュートにハーフエッジ番号が格納されているのがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/94b22f22-2bf1-5df5-4070-b750093f3a86.png)
ボックスだとエッジの数は12本なのでprimaryHalfEdgeユーザアトリビュートのサイズと同じになっているのがわかります。

# 非共有エッジ(Unshared Edge)を調べる
ハーフエッジ関数を利用すると、Wrangleで非共有エッジを簡単に調べることができます。
```c++:Run Over = Point
int primaryHalfEdge = pointhedge(0, @ptnum);
while ( primaryHalfEdge != -1 )
{
    if(hedge_isprimary(0, primaryHalfEdge))
    {
        int srcPtnum = hedge_srcpoint(0, primaryHalfEdge);//ハーフエッジの始点
        if(@ptnum==srcPtnum)
        {
            //ここの中でエッジ単位の処理を
            //等価ハーフエッジの数が1なら非共有エッジすることができる。
            if(hedge_equivcount(0, primaryHalfEdge)==1)
            {
                int dstPtnum = hedge_dstpoint(0, primaryHalfEdge);//ハーフエッジの終点
                //ハーフエッジの始点と終点を結んだ直線を作成する
                int newLine = addprim(0, "polyline");
                addvertex(0, newLine, srcPtnum);
                addvertex(0, newLine, dstPtnum);
                //追加されたプリミティブを特定できるように"line"ユーザアトリビュートを追加する
                addprimattrib(0,"line",0);
                setprimattrib(0,"line",newLine,1,"set");
            }
        }    
    }
    primaryHalfEdge = pointhedgenext(0, primaryHalfEdge);
}; 
```
これを実行すると、下図のように非共有エッジにラインを追加することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/ae74ecc9-535e-56e5-91ce-edee834cfb4c.png)

# ハーフエッジを利用した応用例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/10704b20-c17d-3034-5dfb-3438f660d2c4.png)
イカ蟹さんのカメラから見て輪郭となる部分のエッジを検出して、そのエッジ部分にラインを作成したいと思います。
この実装の考え方としては、
1. プライマリハーフエッジを取得する
1. セカンダリハーフエッジを取得する
1. hedge_prim関数を使って、プライマリハーフエッジとセカンダリハーフエッジに属する各フェース番号を取得し、そこから各法線を取得する
1. カメラの視点からエッジの中点を向いたベクトルを取得する
1. そのベクトルから見て、その2枚のフェース間のエッジが輪郭となっているエッジを見つける

これで輪郭が抽出できます。

```c++:Run Over = Point
//カメラから見たモデルのエッジにラインを生成する
matrix camMatrix = optransform(chs("camera"));
vector eye = cracktransform(0, 0, 0, {0,0,0}, camMatrix);

int primaryHalfEdge = pointhedge(0, @ptnum);
int check;
while ( primaryHalfEdge != -1 )
{
    if(hedge_isprimary(0, primaryHalfEdge))
    {
        int srcPtnum = hedge_srcpoint(0, primaryHalfEdge);
        if(@ptnum==srcPtnum)
        {
            int dstPtnum = hedge_dstpoint(0, primaryHalfEdge);
            int secondaryHalfEdge = pointedge(0, dstPtnum, srcPtnum);
            int primaryPrim = hedge_prim(0, primaryHalfEdge);
            int secondaryPrim = hedge_prim(0, secondaryHalfEdge);      
            
            vector primaryNormal = prim_normal(0,primaryPrim,{0,0,0});
            vector secondaryNormal = prim_normal(0,secondaryPrim,{0,0,0});           
            
            vector srcP = pointattrib(0,"P",srcPtnum,check);
            vector destP = pointattrib(0,"P",dstPtnum,check);
            vector midPoint = ( srcP + destP )/2.0;
            vector eyeDir = normalize(midPoint - eye);
            vector edgeDir = normalize(destP - srcP);          
 
            vector projectedEyeDir = normalize(-dot(eyeDir,edgeDir)*edgeDir + eyeDir);
            vector projectedPrimaryNormal = normalize(-dot(primaryNormal,edgeDir)*edgeDir + primaryNormal);
            vector projectedSecondaryNormal = normalize(-dot(secondaryNormal,edgeDir)*edgeDir + secondaryNormal);
            
            float primaryCheck = dot(projectedPrimaryNormal,projectedEyeDir);
            float sedoncaryCheck = dot(projectedSecondaryNormal,projectedEyeDir);
            
            int test = 0;
            if (primaryCheck < 0.0 && sedoncaryCheck < 0.0) test = 1;
            if (primaryCheck > 0.0 && sedoncaryCheck > 0.0) test = 1;
            
            if( test == 1 ){
                primaryHalfEdge = pointhedgenext(0, primaryHalfEdge);
                continue;
            }
            
            float angle1 = 90.0-acos(dot(projectedPrimaryNormal,projectedEyeDir))*180.0/PI;
            float angle2 = 90.0-acos(dot(projectedSecondaryNormal,projectedEyeDir))*180.0/PI;

            if(angle1 > 0.0 || angle2 > 0.0){
                int newLine = addprim(0, "polyline");
                addvertex(0, newLine, srcPtnum); // 0 is point number 0
                addvertex(0, newLine, dstPtnum);
                addprimattrib(0,"line",0);
                setprimattrib(0,"line",newLine,1,"set");
            }            
        }
    }
    primaryHalfEdge = pointhedgenext(0, primaryHalfEdge);
}; 
```
結果は下図のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/57cd634a-a9ca-a162-a4d1-3e6b25c6bc21.png)

似たような機能として実はSideFX LabsにExtract Silhouetteなるノード( https://www.sidefx.com/ja/docs/houdini/nodes/sop/labs--extract_silhouette-1.0.html )があるのですが、ローレベルでもうちょっと細かく制御したいとなると上記の手法が役に立ちます。

左側が今回の実装したもの、右側がExtract Silhouetteの結果です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/3d072d7d-0068-aecc-122c-83ce842e17d7.png)

このhipファイルはこちらです。
https://drive.google.com/file/d/1wn-YlnpDVX5egKHHUZaDbc7oHVGRHZjh/view?usp=share_link

検証環境
Houdini19.5.435Python3.9