MoAR の "Watching" という作品をつくるときの目玉を描画方法あれこれの話。
こっち向く
"Watching" は壁が壊れてその奥から巨大な目が AR 体験してるその人のほうをギョロリと向くという作品です。
体験者の位置は Geospatial API で取得できているのでとりあえず目玉をその体験者に向くように実装してみると、
なーんかおかしい。ビルで隠れている部分をひっぺがして皮膚を半透明にして目玉がよく見えるようにしてみると、
いちおう目玉はちゃんとこっちを見ているんだが眼球の開口部よりさらに下にむけないと体験者に向かない位置関係になってておかしい。しかも破壊された穴からは黒目中心が見えない位置になってる。つまり体験者の位置に応じて穴からいい感じに眼球が見えるようにダイナミックにモデルを再配置しないといけない。
Blender でシミュレーションしてみた図。眼球がちゃんと体験者の位置から見えるようにするためには目全体の位置を大幅に動かす必要があるのがよくわかる。
というわけで体験者と壁の穴中心をつなぐベクトルを延長した先に目オブジェクトを移動するようにしてみました。
体験者の位置が変わっても壁の穴の先に黒目がちゃんと見えるようになりました。この実装では目の開口部の下端に黒目中心が位置するようにさらに調整がはいっています。(真正面よりもこっち見られてる感があるんす
いろいろはみ出てるのを隠す
ちゃんとこっちを見るようになったのはいいものの、位置関係的にすべてをビルの中に押し込めることができなくていろいろ見えてはいけない部分が見えちゃっています。隠します。
はみ出てるのを隠すなら見えてるとこ以外を描かなきゃいいってことで SceneKit でのステンシルバッファの使い方を調べてみると SCNTechnique 使えばステンシル使えるっぽいってことでゴニョゴニョいじくり倒すも全然ステンシルが動いてる気配がないので、しょうがないので普通にカラーバッファにマスク形状を描いてシェーダーで合成することにしました。
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
でシェーダーで全部合成。
#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 的なものが搭載されてるのでそれでレンダリングパスを確認するのがよいです。
できた
というわけで目をこっちに向けるだけなら楽勝〜と思ってたけど意外といろいろハマりましたけどなんとかできました。ひとつ対応しきれなかったのが最初に壊れて飛んでく破片の影の描画。一番最初に破壊部分のテスト実装してたときは描けてたんですけど、SCNTechnique を使うようにしたら描けなくなっちゃった。やり方はもちろんあるんだろうけど時間切れでしたー。