目玉だけこっち向く話

[MoAR](https://whatever.co/moar/) の "[Watching](https://whatever.co/moar/ja/watching/)" という作品をつくるときの目玉を描画方法あれこれの話。

# こっち向く

"Watching" は壁が壊れてその奥から巨大な目が AR 体験してるその人のほうをギョロリと向くという作品です。
体験者の位置は Geospatial API で取得できているのでとりあえず目玉をその体験者に向くように実装してみると、<br/><br/>

<iframe width="100%" height="700" src="https://www.youtube.com/embed/ZBeCo1EnXhs?rel=0" title="RPReplay Final1661938478" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><br/><br/>

なーんかおかしい。ビルで隠れている部分をひっぺがして皮膚を半透明にして目玉がよく見えるようにしてみると、<br/><br/>

<iframe width="100%" height="700" src="https://www.youtube.com/embed/wBDyJZs4xCI?rel=0" title="RPReplay Final1662191279" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><br/><br/>

いちおう目玉はちゃんとこっちを見ているんだが眼球の開口部よりさらに下にむけないと体験者に向かない位置関係になってておかしい。しかも破壊された穴からは黒目中心が見えない位置になってる。つまり体験者の位置に応じて穴からいい感じに眼球が見えるようにダイナミックにモデルを再配置しないといけない。
![Screen_Shot_2022-09-03_at_17_59_35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2595/5c3707aa-7b31-a0e5-1f30-a0790d391a7c.png)
Blender でシミュレーションしてみた図。眼球がちゃんと体験者の位置から見えるようにするためには目全体の位置を大幅に動かす必要があるのがよくわかる。
![Screen_Shot_2022-09-03_at_18_01_37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2595/5f666c7e-bc64-dd3d-e69b-903a75af3c3c.png)
というわけで体験者と壁の穴中心をつなぐベクトルを延長した先に目オブジェクトを移動するようにしてみました。<br/><br/>

<iframe width="100%" height="700" src="https://www.youtube.com/embed/8vUFWZeGdRs" title="Screen Recording 2022 09 05 at 22 01 31" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><br/><br/>

体験者の位置が変わっても壁の穴の先に黒目がちゃんと見えるようになりました。この実装では目の開口部の下端に黒目中心が位置するようにさらに調整がはいっています。（真正面よりもこっち見られてる感があるんす

# いろいろはみ出てるのを隠す

ちゃんとこっちを見るようになったのはいいものの、位置関係的にすべてをビルの中に押し込めることができなくていろいろ見えてはいけない部分が見えちゃっています。隠します。

はみ出てるのを隠すなら見えてるとこ以外を描かなきゃいいってことで SceneKit でのステンシルバッファの使い方を調べてみると SCNTechnique 使えばステンシル使えるっぽいってことでゴニョゴニョいじくり倒すも全然ステンシルが動いてる気配がないので、しょうがないので普通にカラーバッファにマスク形状を描いてシェーダーで合成することにしました。

```swift
var technique: SCNTechnique {
    let dict: [String: Any] = [
        "targets": [
            "hole_mask": [
                "type": "color",
            ],
            "eye_color": [
                "type": "color",
            ],
            "scene_color": [
                "type": "color",
            ],
        ],
        "passes": [
            "pass_hole_mask": [
                "draw": "DRAW_SCENE",
                "outputs": [
                    "color": "hole_mask",
                ],
                "includeCategoryMask": 2,
            ],
            "pass_eye": [
                "draw": "DRAW_SCENE",
                "outputs": [
                    "color": "eye_color",
                ],
                "includeCategoryMask": 4,
            ],
            "pass_scene": [
                "draw": "DRAW_SCENE",
                "outputs": [
                    "color": "scene_color",
                ],
                "includeCategoryMask": 1,
            ],
            "pass_composite": [
                "draw": "DRAW_QUAD",
                "metalVertexShader": "pass_through_vertex",
                "metalFragmentShader": "pass_through",
                "inputs": [
                    "holeMask": "hole_mask",
                    "eyeColor": "eye_color",
                    "sceneColor": "scene_color",
                ],
                "outputs": [
                    "color": "COLOR",
                ],
                "colorStates": [
                    "clear": true,
                    "clearColor": "sceneBackgrouond",
                ],
            ],
        ],
        "sequence": [
            "pass_hole_mask",
            "pass_eye",
            "pass_scene",
            "pass_composite",
        ],
    ]
    return SCNTechnique(dictionary: dict)!
}
```

`pass_hole_mask` で穴のマスク形状を描いて、`pass_eye` 目だけ描いて、`pass_scene` で目以外のいろいろを描いて、`pass_composite` でシェーダーで全部合成。

```cpp:pass_compositeで使うシェーダー
#include <metal_stdlib>
using namespace metal;
#include <SceneKit/scn_metal>

struct custom_vertex_t
{
    float4 position [[attribute(SCNVertexSemanticPosition)]];
};

struct out_vertex_t
{
    float4 position [[position]];
    float2 uv;
};

vertex out_vertex_t pass_through_vertex(custom_vertex_t in [[stage_in]],
                                        constant SCNSceneBuffer& scn_frame [[buffer(0)]])
{
    out_vertex_t out;
    out.position = in.position;
    out.uv = (in.position.xy * float2(1.0, -1.0) + 1.0) * 0.5;
    return out;
};

constexpr sampler s = sampler(coord::normalized,
                              address::repeat,
                              filter::linear);

fragment half4 pass_through(out_vertex_t vert [[stage_in]],
                            texture2d<float, access::sample> holeMask [[texture(0)]],
                            texture2d<float, access::sample> eyeColor [[texture(1)]],
                            texture2d<float, access::sample> sceneColor [[texture(2)]])
{
    float mask = holeMask.sample(s, vert.uv).r;
    float4 scene = sceneColor.sample(s, vert.uv);
    float4 eye = eyeColor.sample(s, vert.uv) * mask;
    return half4(mix(eye, scene, scene.a));
};
```

コードだけだと何がどうなってんのか脳内でシミュレーションしないとなんだけどさすがにそれは辛いし Xcode には Unity でいう Frame Debugger 的なものが搭載されてるのでそれでレンダリングパスを確認するのがよいです。 
![Screen_Shot_2022-09-06_at_10_37_06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2595/6edf4d89-4afd-7729-9796-7a0b85671011.png)

# できた

というわけで目をこっちに向けるだけなら楽勝〜と思ってたけど意外といろいろハマりましたけどなんとかできました。ひとつ対応しきれなかったのが最初に壊れて飛んでく破片の影の描画。一番最初に破壊部分のテスト実装してたときは描けてたんですけど、SCNTechnique を使うようにしたら描けなくなっちゃった。やり方はもちろんあるんだろうけど時間切れでしたー。