2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

MoAR - Museum of ARAdvent Calendar 2022

Day 14

おばけが窓から窓へ移動しながら掃除機に吸い込まれる動き

Last updated at Posted at 2022-12-13

MoAR のコンテンツのひとつ、"Ghost Building" に出てくるおばけのモーションを作ったときのお話。

おばけが窓から窓へ移動する



ビルにある複数の窓とエントランスのドアのどれかからどれかへランダムに移動します。
いちいち手で動きつけてられないのでプログラムでなんとかします。
窓の位置を適当にモデリングします。
windows
Blender のメッシュ情報を読んで窓位置のセンターを JSON に書き出す Python プログラムを書きます。

import bpy
import json
import os

def convert_vertex(co):
    return [co[0], co[2], -co[1]]

objs = bpy.context.selected_objects

all = {}
for obj in objs:
    if obj.type != "MESH":
        continue

    data = {}
    m = obj.matrix_world
    for p in obj.data.polygons:
        normal = obj.rotation_euler.to_matrix() @ p.normal
        data["normal"] = convert_vertex(normal)
        data["vertices"] = [
            convert_vertex(m @ obj.data.vertices[i].co) for i in p.vertices
        ]
        data["center"] = convert_vertex(m @ p.center)
    all[obj.name] = data

path = os.path.join(os.path.expanduser("~/Desktop/"), "ghost-windows.json")
with open(path, "w") as f:
    json.dump(all, f, indent=4)

窓の座標がわかったのでそれらをつなぐスプライン曲線を書きます。
普通のベジェ曲線だとなめらかに複数セグメントをつないでくのがめんどいので、以前 C# で書いた Natural Cubic Spline を Swift に移植しました。

Natural Cubic Spline てのは C2 continuity な曲率の変化がなめらかなスプライン。Illustratorとかで適当にベジェ曲線を描くとハンドルが直線でも各セグメント間の曲率が急激に変わるので実はそんなになめらかじゃない。というのをベジェ曲線ベースで VR ジェットコースターのレールを設計して乗って気づきました。

ビルの形状が上から見ると扇型になっているのでその中心と窓座標からいい感じに座標を計算してスプラインのコントロールポイントにします。
path
コード的にはこんな感じ。

static func createFlyLine(start: SCNVector3, goal: SCNVector3) -> NaturalCubicSpline {
    let v = start - GhostSceneController.origin
    let d = sqrt(v.x * v.x + v.z * v.z) + 5
    let a = atan2(start.z - GhostSceneController.origin.z, start.x - GhostSceneController.origin.x)
    let b = atan2(goal.z - GhostSceneController.origin.z, goal.x - GhostSceneController.origin.x)
    let c = (a + b) / 2
    let middle = SCNVector3(cos(c) * d, (start.y + goal.y) / 2, sin(c) * d) + GhostSceneController.origin

    let startVector = v.normalized()
    let goalVector = (goal - GhostSceneController.origin).normalized()

    let points = [
        start + startVector * -3.0,
        start,
        start + startVector * 2.0,
        middle,
        goal + goalVector * 2.0,
        goal,
        goal + goalVector * -3.0,
    ]
    return NaturalCubicSpline(points: points)
}

あとは適当にこのスプライン曲線上を動かします。
ただし直接このスプライン曲線から取得した座標値をおばけオブジェクトに割り当てると、逆からきたおばけや混雑した場合にすり抜けてしまいます。(おばけなのでありかも…
なので、実際にはおばけオブジェクトには球体の当たり判定(SCNPhysicsBody)を設定してその球体をひっぱるようにして動かしています。(最初の動画の8秒あたり、上からきたのと下からきたのがコツっとあたっています。

おばけが掃除機に吸い込まれる

掃除機がおばけの方向を向いた瞬間、おばけは掃除機に吸い込まれていきます。
初期実装では窓から窓へ移動するのと同じく、吸い込まれた瞬間の座標から掃除機中心座標に向けたスプライン曲線を設定してそのスプラインに沿って動かしていました。



でも NG が出たので別の動かし方を考えました。
Swift で直接動きを定義しました。

func calculateTornadoPosition(t: Float) -> simd_float3 {
    let start = simd_make_float3(simd_mul(self.inverseCleanerTransform, simd_make_float4(self.tornadoStart, 1.0)))
    let d = simd_float3(0, -1, 0) - start
    let r = sqrt(d.x * d.x + d.z * d.z)
    let a = atan2(d.z, d.x)
    let tt = 1.0 - t
    let rr = max(0.1, r * powf(tt, 2.5))
    let aa = a - powf(t, 2.5) * self.tornadoAngle
    let p = simd_float4(rr * -cos(aa), start.y + d.y * t, rr * -sin(aa), 1.0)
    return simd_make_float3(simd_mul(self.cleanerTransform, p))
}

図にするとこんな感じ。
tornado.png
この動きだけでは魔封波っぽい吸い込まれた感じにならないのでメッシュを変形させてさらに吸い込まれ感を演出しました。
メッシュ変形には SCNMaterialshaderModifier を使いました。Shader Modifier は Metal のシェーダーを全部書かなくても必要な部分だけ書き換えられるので便利です。(さらに GLSL から Metal に変換もしてくれたりします。

uniform mat4 u_tornadoTransform;
uniform mat4 u_inverseTornadoTransform;
uniform float enableTornado;

if (enableTornado > 0.0) {
    float4 pos = u_inverseTornadoTransform * u_modelTransform * _geometry.position; // convert to tornado local coords
    pos.y = max(0.0, pos.y);
    float y = pos.y * 0.35;
    float limit = y * y * 0.35 + 0.2;
    float s = min(1.0, limit);
    float r = sqrt(pos.x * pos.x + pos.z * pos.z) * s;
    r = min(r, limit);
    float a = atan2(pos.z, pos.x);
    float t = max(0.0, min(1.0, (5.0 - y) / 5.0));
    //a -= pow(t, 3.0) * 8.0;
    pos.x = cos(a) * r;
    pos.z = sin(a) * r;
    _geometry.position = u_inverseModelTransform * u_tornadoTransform * pos; // convert back to model coords
}

吸い込まれモーションの Swift コードと近くて掃除機に近づくにつれて回転角が増えて回転半径が小さくなっていきます。ここまで実装するとこうなります。



あとはパラメータ微調整して完成。

掃除機がおばけを飲み込む

吸い込まれたあとにホースがふくらんでゴクリと飲み込む感じが出したいということでエフェクトを追加しました。
これは簡単でホースのメッシュを通過位置に応じて中心から外に移動させればいいだけです。ただし単純に実装してしまうと同時に1箇所しか膨らませられなくなってしまうのでそこはちょっとアイデアが必要でした。

func update() {
    let now = CFAbsoluteTimeGetCurrent()
    let dt = now - simd_double4(startTime[0], startTime[1], startTime[2], startTime[3])
    var p = simd_float4(dt) * 4.0
    let data = Data(bytes: &p, count: MemoryLayout<simd_float4>.size)
    for material in materials {
        material.setValue(data, forKey: "position")
    }
}
uniform vec4 position;
float l = length(_geometry.position.xy);
float p = smoothstep(0.2, 0.1, l);
vec4 d = (cos(clamp(_geometry.position.z + 0.9 - position, vec4(0.0), vec4(1.0)) * pi * 2 + pi) + 1.0) * 1.3 * p;
//_geometry.position.xy *= max(d.x, max(d.y, max(d.z, d.w)));
_geometry.position.xy *= d.x + d.y + d.z + d.w + 1.0;

コード的にはこうなっていて position に4つの通過開始時刻を押し込んで、Geometry の Shader Modifier で4箇所全部膨らませて合計しています。



最初はこの動画のように膨らむ最大値を決めてたのだけど(↑のコードのコメントアウトしてるほう)、複数同時に通ったときによりデカくなっていいかなーと合計することにしました。

ホースがちょい揺れるとか

ずっと画面に固定されてるのがアレだなーとホースが追随してくるような動きを実装しようと思ってたけど時間切れ〜。

(エフェクト作るのは楽しいな〜

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?