e.blog

主にUnity/UE周りのことについてまとめていきます

UnityのCompute ShaderでCurl Noiseを実装(衝突判定編)

概要

今回は「衝突判定編」です。
前回の「流体編」の続編です。

edom18.hateblo.jp


さて、今回は論文で発表されたカールノイズの『衝突判定』について書きたいと思います。

実際に動いている動画↓


本題

カールノイズによる流体のような移動表現については前回書きました。
論文によると、これに加えて剛体などの境界を考慮した動きを実現できるそう。

境界による干渉(論文より抜粋)

境界による干渉について、論文では以下のように書かれています。
原文と、それに対する翻訳(Google翻訳+若干の意訳)を併記します。


境界(Boundaries)

Consider a motionless solid object in the flow. The boundary condition viscous flow must satisfy is \vec{v} = 0.

流れの中に動かないオブジェクトを考えてみる。
その境界条件粘性流は\vec{v} = 0を満たさなければならない。

This can be achieved simply by modulating the potential down to zero with a smoothed step function of distance, so that all the partial derivatives (and hence the curl) of the new potential are zero at the boundary.

これは、距離の平滑化された段階関数を用いてポテンシャルをゼロに変調することでシンプルに達成できる。
つまり、新しいポテンシャルのすべての偏微分(したがってカール)境界でゼロになる。

Of more interest in animation is the inviscid boundary condition, \vec{v} \cdot \vec{n} = 0, requiring that the component of velocity normal to the boundary is zero.

アニメーションで関心があるのは、非粘性境界条件(v \cdot n = 0)である。
必要なのは、境界に垂直な速度の成分がゼロであることだ。

allowing the fluid to slip past tangentially but not to flow through a solid. Most turbulent fluids have such small viscosities that this is a more reasonable approximation.

流体が接線方向に滑ることを可能にするが、固体を通して流れないようにする。
ほとんどの乱流は、これがより合理的な近似であるような小さな粘性を有する。

In two dimensions, note that our velocity field is just the 90◦ rotation of the gradient ∇ψ: if we want the velocity field to be tangent to the boundary, we need the gradient to be perpendicular to the boundary.

二次元では、速度場は勾配(\nabla \psi)の90°回転に過ぎないことに注意。
速度場を境界線に接したい場合は、境界線に対して垂直な勾配が必要だ。

This happens precisely when the boundary is an isocontour of ψ, i.e. when ψ has some constant value along the boundary.

これは、境界がψの等値であるとき、つまりψが境界に沿ってある一定の値を有するときに正確に起こる。

We can achieve this without eliminating the gradient altogether by modulating ψ with a ramp through zero based on distance to the closest boundary point.

最も近い境界点までの、距離に基いてゼロを通るRampでψを変調することによって、勾配を完全になくすことなくこれを達成できる。


ψ_{constrained}(\vec{x}) = ramp\biggl(\frac{d(\vec{x})}{d_0}\biggr) ψ(\vec{x}) ... (3)

where d(\vec{x}) is the distance to all solid boundaries and d_0 is the width of the modified region—when using noise with length scale L, it makes sense to set d_0 = L. We use the following smooth ramp:

d(\vec{x})は、すべてのソリッド境界までの距離であり、d_0は修正された領域の幅である。
長さスケールLのノイズを使用する場合、d_0 = Lに設定することは理にかなっている。
次の、平滑化Ramp関数を利用する:


\begin{eqnarray}
ramp(r){=}
\begin{cases}
1 & : r \geq 1 \\
\frac{15}{8}r - \frac{10}{8}{r}^3 + \frac{3}{8}{r}^5 &: 1 > r > -1 \\
-1 & : r \leq -1
\end{cases}
\end{eqnarray}
... (4)

In three dimensions things are a little more complicated.

3次元の場合はもう少し複雑だ。


α = \biggl|ramp\biggl(\frac{d(\vec{x})}{d_0}\biggr)\biggr|

\vec{n} be the normal to the boundary at the closest point to \vec{x}, we use

nx に最も近い点の境界の法線である。
したがって、次の式を使う。


\vec{ψ}_{constrained}(\vec{x}) = α\vec{ψ} (\vec{x}) + (1 − α) \hat{n} (\hat{n} \cdot \vec{ψ} (\vec{x})) ... (5)

That is, we ramp down the tangential component of \vec{ψ} near the boundary, but leave the normal component unchanged. This can be proven to give a tangential velocity field at smooth object boundaries, using the fact that \vec{n} is the gradient of signed distance from the boundary (hence its curl vanishes).

つまり、\vec{\psi}の接線成分を境界付近で下降(ramp down)させるが、通常の成分は変化させない。
これは、nが境界からの符号付き距離の勾配であるという事実を利用して、滑らかなオブジェクト境界で接線方向の速度場を与えることが証明される。
(したがってそのカールは消える)

Unfortunately, the normal field may be discontinuous along the medial axis of the geometry: na¨ıvely using equation (5) can result in spikes when we take the curl, particularly near sharp edges.

残念なことに、通常のフィールドは、ジオメトリの内側軸に沿って不連続である可能性がある。
式(5)を使用すると、カールを取るとき、特に鋭いエッジ付近でスパイクが発生する可能性がある。

This isn’t a problem for equation (3) since the distance field is Lipschitz continuous, which is adequate for our purposes.

これは、距離場がLipschitz連続であるため、式(3)の問題ではない。
これは、目的に対して適切である。

Thus within distance d_0 of edges flagged as sharp in the system we default to (3), i.e. drop the normal component.

したがって、システム内でシャープとフラグ立てられたエッジの距離d0内では、デフォルトで(3)になる。
すなわち、通常の成分を落とす。


さて、初見ではなんのこっちゃですが、(自分の浅い理解では)要するに、境界付近では接線方向にのみ速度を持たせてやることで、そちらに流れていくよね、ということを言いたいのだと思います。

そのことを示す式が以下です。


\vec{ψ}_{constrained}(\vec{x}) = α\vec{ψ} (\vec{x}) + (1 − α) \hat{n} (\hat{n} \cdot \vec{ψ} (\vec{x})) ... (5)

constrained(拘束)が示すように、境界付近での速度の方向を拘束し、流れを変えることを表す式です。

もう少し具体的に見てみましょう。
式の右辺を見ると、\vec{\psi}(\vec{x})に対してなにやら計算しています。

この\psiはポテンシャル、つまりカールノイズの計算部分です。
その得られた速度ベクトルに対してなにやら計算しているわけです。

式を少し簡単にしてみると、(\vec{\psi}(\vec{x})Pと置きます)


αP + (1 - α)P

ということです。

つまり、なんらかの係数に応じて徐々にそちらのベクトルを採用する、いわゆる線形補間的な計算ですね。

ただ、右側に置かれているほうはもう少し複雑で、正確には以下です。


(1 − α) \hat{n} (\hat{n} \cdot \vec{ψ} (\vec{x}))

ただ、これも分解して考えると、いわゆる「法線方向ベクトルにどれだけ沿っているか」の計算です。
一般的な計算に書き直してみると、


\hat{n}(\hat{n} \cdot \vec{x})

ですね。

コードで表してみる

さて、上記の部分をコードで表したのが以下になります。

境界の法線を計算する

式では、境界付近の法線を利用しています。
そして法線の計算は以下のようになります。

// 勾配(gradient)を計算する
// 基本的な考えは偏微分が法線ベクトルとなることを利用している?
float3 ComputeGradient(float3 p)
{
    const float e = 0.01f;

    // 偏微分するため、各軸の微小値を計算する
    const float3 dx = float3(e, 0, 0);
    const float3 dy = float3(0, e, 0);
    const float3 dz = float3(0, 0, e);

    float d = SampleDistance(p);
    float dfdx = SampleDistance(p + dx) - d;
    float dfdy = SampleDistance(p + dy) - d;
    float dfdz = SampleDistance(p + dz) - d;

    return normalize(float3(dfdx, dfdy, dfdz));
}

// 計算点から、障害物への距離を計算する
float SampleDistance(float3 p)
{
    float3 u = _SphereParam.xyz - p;
    float d = length(u);
    return d - _SphereParam.w;
}

なぜ偏微分したらそれが法線ベクトルになるのかについては以下を参照。

mathtrain.jp

また、距離関数については、(今回は)単純なSphereのみとしているので、比較的シンプルな式で距離計算が出来ています。
その他の距離関数については、こちらのサイトを参照ください。

modeling with distance functions

そして、ポテンシャルの計算を行っているコードが以下。

// α = ramp(d(x)/d0)
// ψ_constrainted(x) = αψ(x) + (1 - α)n(n・ψ(x))
float3 SamplePotential(float3 pos, float time)
{
    float3 normal = ComputeGradient(pos);
    float distance = SampleDistance(pos);

    float3 psi = float3(0, 0, 0);

    // 高さに応じて乱流の度合いを変化させる(上にいくほど拡散するように)
    float heightFactor = Ramp((pos.y - _PlumeBase) / _PlumeHeight);
    for (int i = 0; i < 3; i++)
    {
        float alpha = Ramp(abs(distance) / _NoiseScales[i]);

        float3 s = pos / _NoiseScales[i];

        float3 psi_i = Constraint(Pnoise(s), normal, alpha);
        psi += psi_i * heightFactor * _NoiseGain[i];
    }

    float3 risingForce = _SphereParam.xyz - pos;
    risingForce = float3(-risingForce.z, 0, risingForce.x);

    // ringの半径?
    // XZ平面の中心からの半径? RingRadius?
    float rr = sqrt(pos.x * pos.x + pos.z * pos.z);
    float temp = sqr(rr - _RingRadius) + sqr(rr + _RingRadius) + _RingFalloff;
    float invSecond = 1.0 / _RingPerSecond;
    float ringY = _PlumeCeiling;
    float alpha = Ramp(abs(distance) / _RingRadius);

    // 「煙の柱(Plume)」の下端以下になるまで繰り返す
    while (ringY > _PlumeBase)
    {
        // ringの位置とパーティクルのYの差分
        float ry = pos.y - ringY;

        float b = temp + sqr(ry);
        float rmag = _RingMagnitude / b;

        float3 rpsi = rmag * risingForce;
        psi += Constraint(rpsi, normal, alpha);
        ringY -= _RingSpeed * invSecond;
    }

    return psi;
}

なお、上記コードの乱流生成部分は以下の記事を参考にさせてもらいました。

prideout.net

カールノイズを実装したコンピュートシェーダ全文はこちら↓

github.com

論文翻訳メモ

さて、最後に、論文を理解するにあたってちょいちょい(Google翻訳などを利用しながら)翻訳していったものがあるので、せっかくなのでメモとして残しておこうと思います。

※ 注意! いくらかは自分の意訳部分があるので、完全な翻訳ではありません。

論文はこちら(PDF)


​速度は、ポテンシャル(ベクトル場)(\vec{ψ})に対して回転(curl)をかけたものとして得られる。


\vec{v} = \nabla \times ψ

カールされたベクトル場は自動的にダイバージェンスフリーとなる。

A classic vector calculus identity is that the curl of a smooth potential is automatically divergence-free


\nabla \cdot \nabla \times = 0

よって、


\nabla \cdot \vec{v} = 0

パーリンノイズN(\vec{x})を使ってベクトル場を形成する。
2Dの場合は、単純にψ=N

To construct a randomly varying velocity field we use Perlin noise N(\vec{x}) in our potential. In 2D, [tex:]\psi = N].

3Dの場合は、3要素(ベクトル)が必要となるが、同じノイズ関数を、明らかに大きなオフセットをかけた入力ベクトルを用いて表現する。
普段は、いくつかの異なったスケールを使ったオクターブノイズを乱流の形成のために使う。

In 3D we need three components for the potential: three apparently uncorrelated noise functions (a vector \vec{N}(\vec{x})) do the job, which in practice can be the same noise function evaluated at large offsets.


\psi(x, t) = A(x)\psi_T(x, t) + ramp\biggl(\frac{d(x)}{d_L}\biggr)\psi_L(x)

3Dの場合は、速度ノイズの接線成分のみ、ゼロに減少する。

In 3D, only tangential components of vector noise are ramped down.

シーン内のオブジェクトからゼロに減衰するか、または煙の列の高さに応じて変化させる。

decay to zero away from objects in the scene, or changing it according to the height in a column of smoke.

速度場 A(\vec{x})\vec{v}(\vec{x}) はもはやdivergence-freeを与えないが、それにカールのトリックで成す。

While simply modulating the velocity field A(\vec{x})\vec{v}(\vec{x}) no longer gives a divergnce-free field, modulating the potential, \vec{v} = \nabla \times (A(\vec{x})\psi(\vec{x})), does the trick.

他のポテンシャル(Other Potentials)

これまでは、動かない境界線に対するノイズだけを構築してきた。
もちろん、既存のプリミティブのフローを速度場に使って、よりリッチな機能を実現することもできる。
そして、ポテンシャルをより高めることができる。

So far we have only constructed noise that respects unmoving solid boundaries. While of course we can superimpose existing flow primitives on the velocity field for richer capabilities, we can also do more with the potential itself.

線形速度Vと角速度ωを持つRigidbodyに対応するポテンシャルを導出することはシンプルにできる。

It is simple to derive a potential corresponding to a rigid body motion with linear velocity \vec{V} and angular velocity \vec{ω} :


\vec{\psi}_{rigid}(\vec{x}) = \vec{V} \times (\vec{x} − \vec{x_0}) + \frac{{R}^2 - {|| \vec{x} − \vec{x_0} ||}^2}{2} \vec{ω}

ここで、\vec{x_0}は任意の参照点であり、Rは任意の参照レベルを表す。

where {x_0} is an arbitrary reference point and R is an arbitrary reference level.

Note:
同じvに対応する無限に多くのポテンシャルが常に存在することに気をつける。
スカラー場の勾配だけが異なるふたつのポテンシャルは、同じカールを有する。

Note that there are always infinitely many potentials corresponding to the same \vec{v}: any two potentials which differ by only the gradient of some scalar field have exactly the same curl.

動くRigidbodyの境界条件を満たすように修正したポテンシャル\vec{\psi}を仮定する。
まず、物体の表面上速度の法線成分をゼロにするために方程式(5)を使用し、\vec{\psi_0}を与える。
次に、x_0を剛体の中心として選択した式(6)を使用し、オブジェクトからゼロに滑らかにブレンドする。
Rブレンド領域の半径とすると、ブレンドされた回転項が単調に0に落ちる)

Suppose we have a potential \vec{ψ} we wish to modify to respect boundary conditions on a moving rigid object. First we use equation (5) to zero out the normal component of velocity on the object’s surface, giving \vec{ψ_0}. Then we use equation (6) with x_0 chosen, say, as the center of the rigid body, smoothly blend it to zero away from the object (choosing R to be the radius of the blend region, so that the blended rotational term drops monotonically to zero),

それを加えて


\vec{ψ} (\vec{x}) = \vec{ψ}_0 + A(\vec{x})\vec{ψ}_{rigid}(\vec{x})

を得る。

例えば、各剛体への逆二乗距離に基づくブレンド関数A(\vec{x})の実験を行った。

We have experimented, for example, with a blending function A(\vec{x}) based on inverse squared distance to each rigid body.

剛体の境界では、速度は剛体モーションと、表面に接するベクトル場の合計となる。
これは構成上、非粘性境界条件を尊重する。

Note that at the boundary of the rigid object, the velocity is the sum of the rigid motion and a vector field tangent to the surface: by construction this respects the inviscid boundary condition.

単一のボディにてついては参照点は任意だが、複数のボディがある場合は同じ参照点を使用してブレンドをよりよくするのに役立つ。
特に、すべてのボディが同じ剛体モーションで動いている場合は、そのポテンシャルを合わせてブレンドを完璧にしたいと考えている。

While for a single body the reference point is arbitrary, if we have multiple bodies it can help to use the same reference point to make the blend better. In particular, if all bodies are moving with the same rigid motion, then of course we want their potentials to match up to make the blend perfect.

変形する物体の周りの流れ場はよりトリッキーである。
閉じた変形表面の場合、もしボディがその体積を保持しなければ不可能である。
したがって、将来の作業のためにそれは残しておく。

Flow fields around deforming bodies are trickier. For a closed deforming surface, it may even be impossible—if the body doesn’t conserve its volume—and thus we leave that for future work.

いくつかのプリミティブな渦(Vortex)は、\vec{x}位置の角速度\vec{ω}、半径R、および平滑化フォールオフ関数を有する単純なパーティクルを含む。

Some vortex primitives include a simple vortex particle at \vec{x_0} with angular velocity \vec{ω} , radius R, and smooth fall-off function f :


\vec{\psi}_{vort}(\vec{x}) = f\Biggl(\frac{||\vec{x} − \vec{x_0}||}{R}\Biggr) \frac{{R}^2 − ||\vec{x} − \vec{x_0}||^2}{2} \vec{ω} ... (7)

そして、スモーク・リングやプルーム(Plumes)に役立つ、単純な渦カーブ:

and a simple vortex curve, useful for smoke rings and plumes:


\vec{\psi}_{curve}(\vec{x}) = f\Biggl(\frac{||\vec{x} − \vec{x_C}||}{R}\Biggr) \frac{{R}^2 − ||\vec{x} − \vec{x_C}||^2}{2} \vec{ω_C} ... (8)

\vec{x_C}\vec{x}までの、曲線上の最も近い点であり、\vec{ω_C}は角速度(曲線に接する)である。

他の興味深いフロー構造も、剛体運動式を念頭に置いて同様に作成することができる。

where \vec{x_C} is the closest point on the curve to \vec{x}, and \vec{ω} C is the angular velocity (tangent to the curve). Other interesting flow structures can be similarly created with the rigid motion formulas in mind.

備考メモ

論文に書かれている備考部分に対するメモです。(あくまで個人的な解釈です)

f:id:edo_m18:20180111134000p:plain
↑論文に添えられていた図

左手側は、剛体の背後に乱流を起こしたもの。右手側は、流体を後ろに押し出したあと渦輪を設定したもの。(あってる?)

どちらのケースも、乱流ノイズの各オクターブは、ジオメトリに対してスケールに適した方法で調整される。


\psi_t(x) = \sum_i a_i ramp\left(\frac{d(x)}{d_i}\right) N\left(\frac{x}{d_i}, \frac{t}{d_i}\right)

そして、障害物の背後にのみ生成した乱流に平滑化された振幅関数を乗算する。
これを、基礎の層流(lamninar(ラミナ))に加える。これも、境界付近でスムーズにゼロにランプされる。

※ 流体の流れは2つに分けられ、流体部分が秩序正しく流れる場合を層流と呼び、不規則に混合しながら流れる場合を乱流と呼ぶ。
出典: https://www.weblio.jp/content/laminar+flow


\psi(x, t) = A(x) \psi_T(x, t) + ramp\left(\frac{d(x)}{d_L}\right) \psi_L(x)

個人的見解

以下は、論文を読んでいくにあたって、記号など「こういう意味かなー」と思ったことのメモです。
記号や単語の意味を類推して読んでいくと意外とすっと頭に入ってくることもあるので。

\psi_T(x)が、乱流(Trubulent)を表す式。(なのでT
それを、ジオメトリに応じてAdjustする変数a
これが「乱流」部分になるので、それを、基礎層流(Laminar Flow)に加える、ということになる。
(という意味かな?)

そして、基礎層流を表すのが


ramp\left(\frac{d(x)}{d_L}\right) \psi_L(x)

の式。

仮にこれをLと置き、さらに乱流をTと置くと、最後の式は


\psi(x, t) = A(x) T + L

と書ける。

このA(x)が、文章で書かれている「We then multiply by a smooth AMPLITUDE function」のことだと思う。

ただ、平滑化された振幅関数ってどう表現するのだろう・・。ひとまず適当にSinとか掛けておけばいいんだろうか。
なんとなく、Sinを使った値を加算してみたら煙が立ち上る感じに見えたので、多分そんな感じなんだと思うw

その他、役立ちそうなリンク

prideout.net

catlikecoding.com

github.com

リンクではありませんが、MacにはGrapherというアプリが標準でついていて、以下のキャプチャのように、数式を入れるとそれを可視化してくれる、というものがあります。

数式だけだと一体なにをしているんだ? となることが多いですが、視覚化されると「ああ、なるほど。こういう形を求めているのだな」と分かることが少なくないので、利用して色々な数式を視覚化してみることをオススメします。

f:id:edo_m18:20171210134609p:plain

TransformクラスのTransform**** / InverseTransform****系メソッドのメモ

以前、Qiitaで書いていた記事の移植です。
Inverse系も追加したのでこちらに移動しました。

概要

TransformPointTransformVectorTransformDirectionがそれぞれなにをやってくれているのか、ピンと来てなかったので、どういう処理をしたら同じ結果が得られるのか書いてみたのでメモ。

まぁやってることはシンプルに、モデル行列掛けているだけだけど、その後の処理で平行移動を無効化したり正規化したり、といったことをしている。方向だけを取りたい、みたいなときに使う。
なにやってるか分からないと気持ち悪かったので書いてみた、完全にメモですw

Transform****系メソッド

// ----------------------------------------------------------------------------
// TransformPoint
// 意味: 通常のモデル行列を掛けたポイントを得る
Vector3 point = Vector3.one;
Vector3 newPoint1 = transform.TransformPoint(point);

Vector4 v = new Vector4(point.x, point.y, point.z, 1f);
Vector3 newPoint2 = transform.localToWorldMatrix * v;

// ↓Vector4作らないで済むのでこちらのほうが便利
// Vector3 newPoint2 = transform.localToWorldMatrix.MultiplyPoint(point);

// ----------------------------------------------------------------------------
// TransformVector
// 意味: 回転を適用したのちに、平行移動を無効化したベクトルを得る
Vector3 vector = new Vector3(0, 1.5f, 0);
Vector3 newVector1 = transform.TransformVector(vector);

Vector4 v = new Vector4(vector.x, vector.y, vector.z, 1f);
Vector3 newVector2 = (transform.localToWorldMatrix * v) - transform.position;

// ↓Vector4作らないで済むのでこちらのほうが便利
// Vector3 newVector2 = transform.localToWorldMatrix.MultiplyVector(vector);

// ----------------------------------------------------------------------------
// TransformDirection
// 意味: 回転を適用したのちに、平行移動を無効化し正規化したベクトルを得る
Vector3 direction = Vector3.up;
Vector3 newDirection1 = transform.TransformDirection(direction);

Vector4 v = new Vector4(direction.x, direction.y, direction.z, 1f);
Vector3 newDirection2 = ((transform.localToWorldMatrix * v) - transform.position).normalized;

// ↓Vector4作らないで済むのでこちらのほうが便利
// Vector3 newDirection2 = transform.localToWorldMatrix.MultiplyVector(direction);

InverseTransform****系メソッド

// ----------------------------------------------------------------------------
// InverseTransformPoint
// 意味: ワールド座標からローカル座標へ変換する

Vector3 newPoint1 = transform.InverseTransformPoint(_v);

Matrix4x4 m = transform.worldToLocalMatrix;
Vector4 p = new Vector4(_v.x, _v.y, _v.z, 1f);
Vector3 newPoint2 = m * p;


// ----------------------------------------------------------------------------
// InverseTransformVector
// 意味: 指定したベクトルの方向のみ、ワールド座標からローカル座標へ変換する

Vector3 newdir1 = transform.InverseTransformVector(_v);

Matrix4x4 m = transform.worldToLocalMatrix;
Vector4 c0 = m.GetColumn(0);
Vector4 c1 = m.GetColumn(1);
Vector4 c2 = m.GetColumn(2);
Vector4 c3 = new Vector4(0, 0, 0, 1f);
Matrix4x4 newM = new Matrix4x4(c0, c1, c2, c3);
newM = newM.inverse.transpose;

Vector4 p = new Vector4(_v.x, _v.y, _v.z, 1f);
Vector3 newdir2 = newM * p;

// ↓内容的には以下でまかなえる
// Vector3 newdir2 = transform.worldToLocalMatrix.MultiplyVector(_v);


// ----------------------------------------------------------------------------
// InverseTransformDirection
//
// ※ InverseTransformVectorの正規化した単位ベクトルを得るのかと思いきや、InverseTransformVectorと同じだったので割愛

解説

Inverse系のVector処理はやや複雑です。抜粋するとコードは以下。

Matrix4x4 m = transform.worldToLocalMatrix;
Vector4 c0 = m.GetColumn(0);
Vector4 c1 = m.GetColumn(1);
Vector4 c2 = m.GetColumn(2);
Vector4 c3 = new Vector4(0, 0, 0, 1f);
Matrix4x4 newM = new Matrix4x4(c0, c1, c2, c3);
newM = newM.inverse.transpose;

取り出した、ワールドからローカルへの変換行列をさらに分解して新しい行列を生成しています。
ここでやっていることは方向のみのベクトルを扱う関係で平行移動部分を除去し、移動なしの行列に変換しています。

そして最後に、逆転置行列にするためにnewM = newM.inverse.transpose;としています。

なぜ逆転置行列なのか。
それには理由があります。詳細については以下の記事がとても詳しいのでそちらを御覧ください。

raytracing.hatenablog.com

言葉で説明している箇所を引用させていただくと、

頂点について、モデル座標系からワールド座標系に移すときには何らかの変換行列を使って変換する。
しかし、頂点に対して作用する行列と同じものを法線に対して作用させてもうまくいかない。 まず、平行移動成分を除去しなければならない。法線は向きのみの量なので平行移動は関係ないからである。
回転移動成分についてはそのまま使うことが出来る。
拡大縮小などのスケーリング成分については、たとえば各頂点をX軸方向に3倍にする場合、法線はX軸方向に1/3倍にする必要がある。(最終的に正規化して長さは1にする)

これらの各要素を考慮しつつ法線を適切に変換するための行列を求めるには、頂点の変換行列の逆転置行列を求めればよい、ということが知られている。

ということ。
実際にベクトルを書いてみると分かりますが、スケールを掛けると方向が異なった方向を向いてしまいます。
しかし今回の求めるべきベクトルは方向を適切に変換することにあります。

なので上記のように「逆転置行列」を使う必要がある、というわけです。

TransformのLookAtをUnity APIを使わずに書く

概要

仕事でLookAtを、Unity APIを使わずに書くことがあったのでメモ。

実行した結果。ちゃんとCubeを注視し続けます。
f:id:edo_m18:20180418015908g:plain

サンプルコード

実装したサンプルコードは以下。

このスクリプトをカメラに貼り付けて、ターゲットを指定すると常にそれを視界に捉えた状態になります。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LookAtSample : MonoBehaviour
{
    [SerializeField]
    private Transform _target;
    
    private void Update()
    {
        LookAt(_target.position);
    }

    private void LookAt(Vector3 target)
    {
        Vector3 z = (target - transform.position).normalized;
        Vector3 x = Vector3.Cross(Vector3.up, z).normalized;
        Vector3 y = Vector3.Cross(z, x).normalized;

        Matrix4x4 m = Matrix4x4.identity;
        m[0, 0] = x.x; m[0, 1] = y.x; m[0, 2] = z.x;
        m[1, 0] = x.y; m[1, 1] = y.y; m[1, 2] = z.y;
        m[2, 0] = x.z; m[2, 1] = y.z; m[2, 2] = z.z;

        Quaternion rot = GetRotation(m);
        transform.rotation = rot;
    }

    private Quaternion GetRotation(Matrix4x4 m)
    {
        float[] elem = new float[4];
        elem[0] = m.m00 - m.m11 - m.m22 + 1.0f;
        elem[1] = -m.m00 + m.m11 - m.m22 + 1.0f;
        elem[2] = -m.m00 - m.m11 + m.m22 + 1.0f;
        elem[3] = m.m00 + m.m11 + m.m22 + 1.0f;

        int biggestIdx = 0;
        for (int i = 0; i < elem.Length; i++)
        {
            if (elem[i] > elem[biggestIdx])
            {
                biggestIdx = i;
            }
        }

        if (elem[biggestIdx] < 0)
        {
            Debug.Log("Wrong matrix.");
            return new Quaternion();
        }

        float[] q = new float[4];
        float v = Mathf.Sqrt(elem[biggestIdx]) * 0.5f;
        q[biggestIdx] = v;
        float mult = 0.25f / v;

        switch (biggestIdx)
        {
            case 0:
                q[1] = (m.m10 + m.m01) * mult;
                q[2] = (m.m02 + m.m20) * mult;
                q[3] = (m.m21 - m.m12) * mult;
                break;
            case 1:
                q[0] = (m.m10 + m.m01) * mult;
                q[2] = (m.m21 + m.m12) * mult;
                q[3] = (m.m02 - m.m20) * mult;
                break;
            case 2:
                q[0] = (m.m02 + m.m20) * mult;
                q[1] = (m.m21 + m.m12) * mult;
                q[3] = (m.m10 - m.m01) * mult;
                break;
            case 3:
                q[0] = (m.m21 - m.m12) * mult;
                q[1] = (m.m02 - m.m20) * mult;
                q[2] = (m.m10 - m.m01) * mult;
                break;
        }

        return new Quaternion(q[0], q[1], q[2], q[3]);
    }
}

LookAtの行列を作る

LookAtは結局のところ、ターゲットに向く「回転」を作ることと同義です。

上のコードでやっていることはシンプルです。
「向かせたい方向のベクトル」と「空方向のベクトル」から、外積を使ってX軸を割り出します。
(空方向と垂直なベクトルはX軸ですよね)

そして向かせたい方向ベクトルがZ軸となるわけなので、結果として、割り出したX軸と向かせたいベクトルとの外積を取ることで最終的な「上方向」のベクトルを得ることができます。
(「上」が空方向とは限らない)

そうやって求めた3軸の各要素を元に行列を作ります。
なぜこれが「LookAt」に相当するのか。

(x, y, z)(1, 1, 1)のときを考えてみましょう。
該当のコードは以下です。

private void LookAt(Vector3 target)
{
    Vector3 z = (target - transform.position).normalized;
    Vector3 x = Vector3.Cross(Vector3.up, z).normalized;
    Vector3 y = Vector3.Cross(z, x).normalized;

    Matrix4x4 m = Matrix4x4.identity;
    m[0, 0] = x.x; m[0, 1] = y.x; m[0, 2] = z.x;
    m[1, 0] = x.y; m[1, 1] = y.y; m[1, 2] = z.y;
    m[2, 0] = x.z; m[2, 1] = y.z; m[2, 2] = z.z;

    Quaternion rot = GetRotation(m);
    transform.rotation = rot;
}

そして今作った行列をそのベクトル(1, 1, 1)に適用してみます。
Unityの行列は列オーダーなので、ベクトルを右から掛けることで計算します。
つまり、

 \begin{vmatrix}
x.x &y.x &z.x \\
x.y &y.y &z.y \\
x.z &y.z &z.z \\
\end{vmatrix}\times
\begin{vmatrix}
1 \\
1 \\
1 \\
\end{vmatrix}=
\begin{vmatrix}
x.x &y.x & z.x
\end{vmatrix}

となって、回転後の値になっているのが分かるかと思います。

回転行列からクォータニオンを計算する

さて、無事に回転行列が作れましたが、Unityでは回転をクォータニオンで扱っています。
そのため、求めた回転行列はそのまま使えません。なので、クォータニオンに変換する必要があります。

回転行列からクォータニオンへの変換は、マルペケさんのこちらの記事(その58 やっぱり欲しい回転行列⇔クォータニオン相互変換)を参考にさせていただきました。

コード部分は以下です。

private Quaternion GetRotation(Matrix4x4 m)
{
    float[] elem = new float[4];
    elem[0] = m.m00 - m.m11 - m.m22 + 1.0f;
    elem[1] = -m.m00 + m.m11 - m.m22 + 1.0f;
    elem[2] = -m.m00 - m.m11 + m.m22 + 1.0f;
    elem[3] = m.m00 + m.m11 + m.m22 + 1.0f;

    int biggestIdx = 0;
    for (int i = 0; i < elem.Length; i++)
    {
        if (elem[i] > elem[biggestIdx])
        {
            biggestIdx = i;
        }
    }

    if (elem[biggestIdx] < 0)
    {
        Debug.Log("Wrong matrix.");
        return new Quaternion();
    }

    float[] q = new float[4];
    float v = Mathf.Sqrt(elem[biggestIdx]) * 0.5f;
    q[biggestIdx] = v;
    float mult = 0.25f / v;

    switch (biggestIdx)
    {
        case 0:
            q[1] = (m.m10 + m.m01) * mult;
            q[2] = (m.m02 + m.m20) * mult;
            q[3] = (m.m21 - m.m12) * mult;
            break;
        case 1:
            q[0] = (m.m10 + m.m01) * mult;
            q[2] = (m.m21 + m.m12) * mult;
            q[3] = (m.m02 - m.m20) * mult;
            break;
        case 2:
            q[0] = (m.m02 + m.m20) * mult;
            q[1] = (m.m21 + m.m12) * mult;
            q[3] = (m.m10 - m.m01) * mult;
            break;
        case 3:
            q[0] = (m.m21 - m.m12) * mult;
            q[1] = (m.m02 - m.m20) * mult;
            q[2] = (m.m10 - m.m01) * mult;
            break;
    }

    return new Quaternion(q[0], q[1], q[2], q[3]);
}

詳細についてはマルペケさんの記事をご覧ください。

ざっくり概要だけを書くと、回転行列の各要素はクォータニオンから回転行列に変換する際に「どの要素を使うか」で求めることができます。
マルペケさんで書かれている行列を引用させていただくと、以下のようになります。


\begin{vmatrix}
1 - 2y^2 - 2z^2 &2xy + 2wz &2xz - 2wy &0 \\
2xy - 2wz &1 - 2x^2 - 2z^2 &2yz + 2wx &0 \\
2xz + 2wy &2yz - 2wx &1 - 2x^2 - 2y^2 &0 \\
0 &0 &0 &1
\end{vmatrix}

ここで使われているx, y, z, wクォータニオンの各成分です。

そして、この値を組み合わせることによって目的のx, y, z, wを求める、というのが目標です。
例えば、以下の回転行列の要素を組み合わせることで値を取り出します。


m_{11} + m_{22} + m_{33} = 3 - 4(x^2 + y^2 + z^2)  \\
= 3 - 4(n_x^2 + n_y^2 + n_z^2) \sin^2 \theta ' \\
= 3 - 4(1 - \cos^2 \theta ' \\
= 4w^2 - 1

となります。
ここで、 \theta ' = \frac{\theta}{2}なので、wの式に直すと以下のようになります。


w = \frac{\sqrt{m_{11} + m_{22} + m_{33} + 1}}{2}

他の値も同様にして計算していきます。
これ以外にも色々考慮しないとならないことがありますが、詳細についてはマルペケさんの記事をご覧ください。

こうして求めたクォータニオンを、該当オブジェクトのrotationに設定することで、冒頭の画像のようにLookAtが完成します。

フリーハンドで描いた図形をポリゴン分割する

概要

フリーハンドで平面に描いた形に図形を形成するのをやりたかったので、頂点群からポリゴンを形成する処理について書きたいと思います。

こんな感じで、適当に打った点からポリゴンを形成します↓

点群からポリゴンを形成する

まず必要になるのが、点群からポリゴンを形成する処理です。

実装についてはこちらの記事(Javaゲーム制作記 任意多角形の三角形分割)を参考にさせていただきました。

ちなみに、記事中に、さらに元となった記事へのリンクが書いてありましたがリンク切れしてました。
おそらく元となった記事はこちらだと思います↓

sonson.jp

任意の点群からポリゴンを形成する

点群からポリゴンを形成するには、当然ですが点群が囲む任意多角形をポリゴンとして三角形に分割していく作業が必要になります。

大まかに流れを書くと以下のようになります。

  1. 点群を得る
  2. 点群の中で、任意の点(※1)から一番遠い点を見つける
  3. (2)で見つかった点とその両隣の点で三角形を作る
  4. このとき、(2)の点と両隣の点から成る線が作る角度が180度を超えていないことを確認する(※2)
  5. (3)で形成した三角形の中に、点郡の他の点が含まれていないことを確認する
  6. (5)で内包している点がなかったら、それを分割された三角形として採用し、(2)で見つかった点を点群リストから除外する
  7. もし(5)の工程で内包する点が見つかった場合は三角形が構成できないので、ひとつ隣の点を採用点に変更し、もう一度(5)の処理を行う。その際、確認した三角形の向きを保持しておく(※3)
  8. (5)の処理を行い、見つかった三角形と、(7)で保持していた三角形の向きをチェックし、異なった方向だった場合はまた隣の点に移動して(5)の処理を繰り返す
  9. 以後、点群が残り3点(三角形が構成できる最後の点群)になるまで繰り返す

※1 ... 任意の点なのでどこでも構いません。原点などが採用しやすいでしょう。
※2 ... もし超えている場合、見つけた点よりも遠い点が存在するため判定がおかしい。
※3 ... 向きをチェックする理由は、多角形の点の構成によっては外側の三角形が見つかる可能性があるため(図解で後述します)

以上の手順を繰り返すことで、点群を複数の三角形に分割し、冒頭の画像のように任意の多角形をポリゴンに分解することができるようになります。

・・・と、文章だとなんのこっちゃ、だと思うのでまずは図解してみます。

f:id:edo_m18:20180324231129p:plain f:id:edo_m18:20180324231123p:plain f:id:edo_m18:20180324231218p:plain f:id:edo_m18:20180324231222p:plain f:id:edo_m18:20180324231321p:plain f:id:edo_m18:20180324231324p:plain f:id:edo_m18:20180324231327p:plain f:id:edo_m18:20180324231330p:plain f:id:edo_m18:20180324231337p:plain f:id:edo_m18:20180324231340p:plain f:id:edo_m18:20180324231346p:plain f:id:edo_m18:20180324231349p:plain
作った三角形の中に別の点が含まれてしまっている f:id:edo_m18:20180324231353p:plain
三角形の向きは採用点から次の点を辺1、前の点を辺2として、その外積を使って求める f:id:edo_m18:20180324231356p:plain f:id:edo_m18:20180324231402p:plain f:id:edo_m18:20180324231405p:plain f:id:edo_m18:20180324231408p:plain

以上のような感じで、順々に三角形に分解していき、最後の3頂点になるまでそれを繰り返す、という方法です。
仕組み自体はとてもシンプルですね。

コードで見てみる

今回実装したコードも合わせて載せておきます。
(図解したことをコードにしているだけなので、詳細は割愛します)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// Draw mesh by clicked points.
/// </summary>
public class DrawMesh : MonoBehaviour
{
    private List<Vector3> _leftVertices = new List<Vector3>();
    private List<Vector3> _triangles = new List<Vector3>();

    private Vector3 _prevDirection = Vector3.zero;

    private bool _isIncluding = false;
    private int _curIndex;
    private int _nextIndex;
    private int _prevIndex;

    private Vector3 CurrentPoint
    {
        get { return _leftVertices[_curIndex]; }
    }
    private Vector3 PreviousPoiont
    {
        get { return _leftVertices[_prevIndex]; }
    }
    private Vector3 NextPoint
    {
        get { return _leftVertices[_nextIndex]; }
    }

    /// <summary>
    /// Clear vertices and triangles.
    /// </summary>
    private void ClearMesh()
    {
        _leftVertices.Clear();
        _triangles.Clear();
    }

    /// <summary>
    /// Create mesh by vertices.
    /// </summary>
    public GameObject CreateMesh(List<Vector3> vertices)
    {
        ClearMesh();

        _leftVertices.AddRange(vertices);

        while (_leftVertices.Count > 3)
        {
            DetecteTriangle();
        }

        _triangles.AddRange(_leftVertices);

        Debug.Log("Done chekcing.");

        Mesh mesh = new Mesh();
        mesh.vertices = _triangles.ToArray();

        int[] indices = new int[_triangles.Count];
        for (int i = 0; i < indices.Length; i ++)
        {
            indices[i] = i;
        }

        mesh.triangles = indices;
        mesh.RecalculateNormals();

        GameObject go = new GameObject("MeshObject", typeof(MeshFilter), typeof(MeshRenderer));

        MeshFilter filter = go.GetComponent<MeshFilter>();
        filter.mesh = mesh;

        return go;
    }

    /// <summary>
    /// Detect triangle from far point.
    /// </summary>
    private void DetecteTriangle()
    {
        if (!_isIncluding)
        {
            FindFarPoint();
        }

        Vector3 a = CurrentPoint;
        Vector3 b = NextPoint;
        Vector3 c = PreviousPoiont;

        Vector3 edge1 = b - a;
        Vector3 edge2 = c - a;

        float angle = Vector3.Angle(edge1, edge2);
        if (angle >= 180)
        {
            Debug.LogError("Something was wrong.");
            return;
        }

        if (IsIncludePoint())
        {
            Debug.Log("Point is including.");

            // try to find other point.
            _isIncluding = true;

            // Store current triangle direction.
            _prevDirection = GetCurrentDirection();

            MoveToNext();

            return;
        }

        _isIncluding = false;

        _triangles.Add(a);
        _triangles.Add(b);
        _triangles.Add(c);

        _leftVertices.RemoveAt(_curIndex);
    }

    /// <summary>
    /// Check to include point in the triangle.
    /// </summary>
    /// <returns></returns>
    private bool IsIncludePoint()
    {
        for (int i = 0; i < _leftVertices.Count; i++)
        {
            // skip if index in detected three points.
            if (i == _curIndex || i == _nextIndex || i == _prevIndex)
            {
                continue;
            }

            if (CheckInPoint(_leftVertices[i]))
            {
                return true;
            }
        }

        return false;
    }

    /// <summary>
    /// Get current triangle direction.
    /// </summary>
    /// <returns>Triagnel direction normal.</returns>
    private Vector3 GetCurrentDirection()
    {
        Vector3 edge1 = (NextPoint - CurrentPoint);
        Vector3 edge2 = (PreviousPoiont - CurrentPoint);

        return Vector3.Cross(edge1, edge2).normalized;
    }

    /// <summary>
    /// Check including point.
    /// </summary>
    /// <param name="target">Target point.</param>
    /// <returns>return true if point is including.</returns>
    private bool CheckInPoint(Vector3 target)
    {
        // Triangle points.
        Vector3[] tp =
        {
            CurrentPoint,
            NextPoint,
            PreviousPoiont,
        };

        Vector3 prevNormal = default(Vector3);
        for (int i = 0; i < tp.Length; i++)
        {
            Vector3 edge1 = (target - tp[i]);
            Vector3 edge2 = (target - tp[(i + 1) % tp.Length]);

            Vector3 normal = Vector3.Cross(edge1, edge2).normalized;

            if (prevNormal == default(Vector3))
            {
                prevNormal = normal;
                continue;
            }

            // If not same direction, the point out of a triangle.
            if (Vector3.Dot(prevNormal, normal) <= 0.99f)
            {
                return false;
            }
        }

        return true;
    }

    /// <summary>
    /// Poition reference move to next.
    /// </summary>
    private void MoveToNext()
    {
        _curIndex = (_curIndex + 1) % _leftVertices.Count;
        _nextIndex = (_curIndex + 1) % _leftVertices.Count;
        _prevIndex = _curIndex - 1 >= 0 ? _curIndex - 1 : _leftVertices.Count - 1;
    }

    /// <summary>
    /// Find far point from origin.
    /// </summary>
    private void FindFarPoint()
    {
        int farIndex = -1;
        float maxDist = float.MinValue;
        for (int i = 0; i < _leftVertices.Count; i++)
        {
            float dist = Vector3.Distance(Vector3.zero, _leftVertices[i]);
            if (dist > maxDist)
            {
                maxDist = dist;
                farIndex = i;
            }
        }

        _curIndex = farIndex;
        _nextIndex = (_curIndex + 1) % _leftVertices.Count;
        _prevIndex = (_curIndex - 1) >= 0 ? _curIndex - 1 : _leftVertices.Count - 1;
    }
}

[2018.05.02追記]

上記コードではポリゴン用の頂点を複製してポリゴン数(x3)と同じだけの頂点を生成していましたが、インデックスを割り当てるように修正したのでそちらのコードも載せておきます。

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

/// <summary>
/// Draw mesh by clicked points.
/// </summary>
public class DrawMesh : MonoBehaviour
{
    private List<int> _triangles = new List<int>();
    private List<Vector3> _vertices = new List<Vector3>();
    private Dictionary<int, bool> _verticesBuffer = new Dictionary<int, bool>();

    private Vector3 _prevDirection = Vector3.zero;

    private bool _isIncluding = false;
    private int _curIndex;
    private int _nextIndex;
    private int _prevIndex;

    private Vector3 CurrentPoint
    {
        get { return _vertices[_curIndex]; }
    }
    private Vector3 PreviousPoiont
    {
        get { return _vertices[_prevIndex]; }
    }
    private Vector3 NextPoint
    {
        get { return _vertices[_nextIndex]; }
    }

    /// <summary>
    /// Clear buffers.
    /// </summary>
    private void Clear()
    {
        _vertices.Clear();
        _verticesBuffer.Clear();
        _triangles.Clear();
    }

    private void Initialize(List<Vector3> vertices)
    {
        Clear();

        // 設定された頂点を保持しておく
        _vertices.AddRange(vertices);

        // 全頂点のインデックスを保持、使用済みフラグをfalseで初期化
        for (int i = 0; i < vertices.Count; i++)
        {
            _verticesBuffer.Add(i, false);
        }
    }

    /// <summary>
    /// Create mesh by vertices.
    /// </summary>
    public GameObject CreateMesh(List<Vector3> vertices)
    {
        Initialize(vertices);

        while (true)
        {
            KeyValuePair<int, bool>[] left = _verticesBuffer.Where(buf => !buf.Value).ToArray();
            if (left.Length <= 3)
            {
                break;
            }
            DetecteTriangle();
        }

        int[] keys = _verticesBuffer.Keys.ToArray();
        foreach (int key in keys)
        {
            if (!_verticesBuffer[key])
            {
                _verticesBuffer[key] = true;
                _triangles.Add(key);
            }
        }

        Debug.Log("Done chekcing.");

        Mesh mesh = new Mesh();
        mesh.vertices = _vertices.ToArray();

        mesh.triangles = _triangles.ToArray();
        mesh.RecalculateNormals();

        GameObject go = new GameObject("MeshObject", typeof(MeshFilter), typeof(MeshRenderer));

        MeshFilter filter = go.GetComponent<MeshFilter>();
        filter.mesh = mesh;

        return go;
    }

    /// <summary>
    /// Detect triangle from far point.
    /// </summary>
    private void DetecteTriangle()
    {
        if (!_isIncluding)
        {
            FindFarPoint();
        }

        Vector3 a = CurrentPoint;
        Vector3 b = NextPoint;
        Vector3 c = PreviousPoiont;

        Vector3 edge1 = b - a;
        Vector3 edge2 = c - a;

        float angle = Vector3.Angle(edge1, edge2);
        if (angle >= 180)
        {
            Debug.LogError("Something was wrong.");
            return;
        }

        if (IsIncludePoint())
        {
            Debug.Log("Point is including.");

            // try to find other point.
            _isIncluding = true;

            // Store current triangle dicretion.
            _prevDirection = GetCurrentDirection();

            MoveToNext();

            return;
        }

        _isIncluding = false;

        _triangles.Add(_curIndex);
        _triangles.Add(_nextIndex);
        _triangles.Add(_prevIndex);

        bool isDtected = true;
        _verticesBuffer[_curIndex] = isDtected; 
    }

    /// <summary>
    /// Check to include point in the triangle.
    /// </summary>
    /// <returns></returns>
    private bool IsIncludePoint()
    {
        foreach (var key in _verticesBuffer.Keys)
        {
            int index = key;

            if (_verticesBuffer[key])
            {
                continue;
            }

            // skip if index in detected three points.
            if (index == _curIndex || index == _nextIndex || index == _prevIndex)
            {
                continue;
            }

            if (CheckInPoint(_vertices[index]))
            {
                return true;
            }
        }

        return false;
    }

    /// <summary>
    /// Get current triangle direction.
    /// </summary>
    /// <returns>Triagnel direction normal.</returns>
    private Vector3 GetCurrentDirection()
    {
        Vector3 edge1 = (NextPoint - CurrentPoint).normalized;
        Vector3 edge2 = (PreviousPoiont - CurrentPoint).normalized;

        return Vector3.Cross(edge1, edge2);
    }

    /// <summary>
    /// Check including point.
    /// </summary>
    /// <param name="target">Target point.</param>
    /// <returns>return true if point is including.</returns>
    private bool CheckInPoint(Vector3 target)
    {
        // Triangle points.
        Vector3[] tp =
        {
            CurrentPoint,
            NextPoint,
            PreviousPoiont,
        };

        Vector3 prevNormal = default(Vector3);
        for (int i = 0; i < tp.Length; i++)
        {
            Vector3 edge1 = (target - tp[i]);
            Vector3 edge2 = (target - tp[(i + 1) % tp.Length]);

            Vector3 normal = Vector3.Cross(edge1, edge2).normalized;

            if (prevNormal == default(Vector3))
            {
                prevNormal = normal;
                continue;
            }

            // If not same direction, the point out of a triangle.
            if (Vector3.Dot(prevNormal, normal) <= 0.99f)
            {
                return false;
            }
        }

        return true;
    }

    /// <summary>
    /// Poition reference move to next.
    /// </summary>
    private void MoveToNext()
    {
        _curIndex = FindNextIndex(_curIndex);
        _nextIndex = FindNextIndex(_curIndex);
        _prevIndex = FindPrevIndex(_curIndex);
    }

    /// <summary>
    /// 原点から最も遠い点を探す
    /// </summary>
    private void FindFarPoint()
    {
        int farIndex = -1;
        float maxDist = float.MinValue;

        foreach (var key in _verticesBuffer.Keys)
        {
            if (_verticesBuffer[key])
            {
                continue;
            }

            float dist = Vector3.Distance(Vector3.zero, _vertices[key]);
            if (dist > maxDist)
            {
                maxDist = dist;
                farIndex = key;
            }
        }

        _curIndex = farIndex;
        _nextIndex = FindNextIndex(_curIndex);
        _prevIndex = FindPrevIndex(_curIndex);
    }

    /// <summary>
    /// 指定インデックスから調べて次の有効頂点インデックスを探す
    /// </summary>
    private int FindNextIndex(int start)
    {
        int i = start;
        while (true)
        {
            i = (i + 1) % _vertices.Count;
            if (!_verticesBuffer[i])
            {
                return i;
            }
        }
    }

    /// <summary>
    /// 指定インデックスから調べて前の有効頂点インデックスを探す
    /// </summary>
    private int FindPrevIndex(int start)
    {
        int i = start;
        while (true)
        {
            i = (i - 1) >= 0 ? i - 1 : _vertices.Count - 1;
            if (!_verticesBuffer[i])
            {
                return i;
            }
        }
    }
}

クリック位置を点群として採用する

さて、任意の多角形(点群)から三角形(ポリゴン)に分割する処理ができました。
あとはドラッグした位置の点を取り、それを点群としてリスト化することで目的のことが達成できます。

今回はシンプルに以下のように点群を得る処理を書きました。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PointDrawer : MonoBehaviour
{
    [SerializeField]
    private DrawMesh _drawMesh;

    [SerializeField]
    private Material _dotMat;

    [SerializeField]
    private float _dotSize = 0.05f;

    [SerializeField]
    private Material _material;

    [SerializeField]
    private float _threshold = 0.1f;

    private float _sqrThreshold = 0;

    private List<Vector3> _samplingVertices = new List<Vector3>();

    private List<GameObject> _dotList = new List<GameObject>();
    private List<Vector3> _vertices = new List<Vector3>();
    private List<GameObject> _meshList = new List<GameObject>();

    /// <summary>
    /// Get average point.
    /// </summary>
    private Vector3 AveragePoint
    {
        get
        {
            Vector3 avg = Vector3.zero;
            for (int i = 0; i < _samplingVertices.Count; i++)
            {
                avg += _samplingVertices[i];
            }
            avg /= _samplingVertices.Count;

            return avg;
        }
    }

    private void Awake()
    {
        _sqrThreshold = _threshold * _threshold;
    }

    private void Update()
    {
        if (Input.GetMouseButton(0))
        {
            TryRaycast();
        }

        if (Input.GetMouseButtonUp(0))
        {
            GameObject go = _drawMesh.CreateMesh(_vertices);
            go.GetComponent<MeshRenderer>().material = _material;
            go.transform.position += go.transform.forward * -0.001f;
            _meshList.Add(go);
        }

        if (Input.GetKeyDown(KeyCode.Q))
        {
            Clear();
        }
    }

    /// <summary>
    /// Try raycast to the plane.
    /// </summary>
    private void TryRaycast()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, float.MaxValue))
        {
            if (_vertices.Count == 0)
            {
                AddVertex(hit.point);
                return;
            }

            if (_samplingVertices.Count == 0)
            {
                _samplingVertices.Add(hit.point);
                return;
            }

            float dist = (AveragePoint - hit.point).sqrMagnitude;
            if (dist >= _sqrThreshold)
            {
                AddVertex(hit.point);
            }
            else
            {
                _samplingVertices.Add(hit.point);
            }
        }
    }

    private void AddVertex(Vector3 point)
    {
        CreateDot(point);
        _vertices.Add(point);
        _samplingVertices.Clear();
    }

    /// <summary>
    /// Create dot for clicked poisition.
    /// </summary>
    /// <returns>Dot GameObject.</returns>
    private GameObject CreateDot(Vector3 position)
    {
        Debug.Log("Create dot.");

        GameObject dot = GameObject.CreatePrimitive(PrimitiveType.Sphere);
        dot.transform.localScale = Vector3.one * _dotSize;
        dot.transform.position = position;
        dot.GetComponent<MeshRenderer>().material = _dotMat;
        Destroy(dot.GetComponent<Collider>());

        _dotList.Add(dot);

        return dot;
    }

    public void Clear()
    {
        for (int i = 0; i < _dotList.Count; i++)
        {
            Destroy(_dotList[i]);
        }
        _dotList.Clear();

        for (int i = 0; i < _meshList.Count; i++)
        {
            Destroy(_meshList[i]);
        }
        _meshList.Clear();
    }
}

実装は大したことしてないですが、ドラッグ中(マウスダウン中)にRaycastを行って点の位置を特定、さらにそれを即座に採用せず、閾値以上動いたらそれを点として採用する、という感じです。

閾値以上移動したかどうかは、現在のマウス位置を毎フレーム取り、それの平均位置から現在のマウス位置との距離で判定しています。

このあたりは、要は点群が得られればいいだけなので如何用にでも実装できるかと思います。

まとめ

点群が用意できれば、それをポリゴンに分解できるので、あとはそれに対してやりたいことを実装すれば完了です。
今回はマスクとして使いたかったので、作ったメッシュにマスク用シェーダ(マテリアル)を割り当てて実際は利用しました。

ちなみに、(たまたまかもしれませんが)多少立体的になってもちゃんとポリゴンが形成されたのでもしかしたら3Dでも応用できるかもしれません。

AndroidのSpeechRecognizerをネイティブプラグイン化してUnityで使う

概要

UnityでAndroidのネイティブな音声認識機能を利用したかったのでプラグインから作成してみました。
今回は作成方法などのまとめです。

なお、ネイティブプラグイン自体の作成方法については以前書いたのでそちらを参照ください。

edom18.hateblo.jp

今回実装したものを実際に動かした動画です↓



音声認識する部分の処理を書く

さて、さっそくプラグイン部分のコードを。
プラグイン自体の作成方法は前回の記事を見ていただくとして、今回はプラグイン部分のみを抜き出しています。

プラグイン用のプロジェクトを作成したら、以下のように音声認識エンジンを起動するクラスを実装します。

package com.edo.speechplugin.recoginizer;

import android.content.Context;
import android.os.Bundle;
import android.speech.RecognitionListener;
import android.speech.RecognizerIntent;
import android.speech.SpeechRecognizer;
import android.content.Intent;

import static com.unity3d.player.UnityPlayer.UnitySendMessage;

public class NativeSpeechRecognizer
{
    static public void StartRecognizer(Context context, final String callbackTarget, final String callbackMethod)
    {
        Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
        intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
        intent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, context.getPackageName());

        SpeechRecognizer recognizer = SpeechRecognizer.createSpeechRecognizer(context);
        recognizer.setRecognitionListener(new RecognitionListener()
        {
            @Override
            public void onReadyForSpeech(Bundle params)
            {
                // On Ready for speech.
                UnitySendMessage(callbackTarget, callbackMethod, "onReadyForSpeech");
            }

            @Override
            public void onBeginningOfSpeech()
            {
                // On begining of speech.
                UnitySendMessage(callbackTarget, callbackMethod, "OnBeginningOfSpheech");
            }

            @Override
            public void onRmsChanged(float rmsdB)
            {
                // On Rms changed.
                UnitySendMessage(callbackTarget, callbackMethod, "onRmsChanged");
            }

            @Override
            public void onBufferReceived(byte[] buffer)
            {
                // On buffer received.
                UnitySendMessage(callbackTarget, callbackMethod, "onBufferReceived");
            }

            @Override
            public void onEndOfSpeech()
            {
                // On end of speech.
                UnitySendMessage(callbackTarget, callbackMethod, "onEndOfSpeech");
            }

            @Override
            public void onError(int error)
            {
                // On error.
                UnitySendMessage(callbackTarget, callbackMethod, "onError");
            }

            @Override
            public void onResults(Bundle results)
            {
                // On results.
                UnitySendMessage(callbackTarget, callbackMethod, "onResults");
            }

            @Override
            public void onPartialResults(Bundle partialResults)
            {
                // On partial results.
                UnitySendMessage(callbackTarget, callbackMethod, "onPartialResults");
            }

            @Override
            public void onEvent(int eventType, Bundle params)
            {
                // On event.
                UnitySendMessage(callbackTarget, callbackMethod, "onEvent");
            }
        });

        recognizer.startListening(intent);
    }
}

上記はリスナーの登録の雛形です。
Unityの機能であるUnitySendMessageを使っていますが詳細は後述します。

SpeechRecognizer#setRecognitionListenerでリスナを登録し、SpeechRecognizer#startListening音声認識エンジンを起動します。

認識した文字列を受け取る

認識した文字列を受け取る箇所については認識した際のコールバックで文字列を取り出します。

@Override
public void onResults(Bundle results)
{
    ArrayList<String> list = results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
    String str = "";
    for (String s : list)
    {
        if (str.length() > 0)
        {
            str += "\n";
        }
        str += s;
    }

    UnitySendMessage(callbackTarget, callbackMethod, "onResults\n" + str);
}

基本的にプラグインとの値の受け渡しは文字列で行うのが通常のようです。
あとはUnity側で文字列を受け取り、適切に分解して利用することで無事、Unity上で音声認識を利用することができるようになります。

Unityの機能を利用できるようにclasses.jarをimportする

音声認識したあと、それをコールバックするためUnity側にメッセージを送信する必要があります。
その際、Unity側の実装を呼び出す必要があるため、それを利用するためにclasses.jarをimportしておく必要があります。

import先はモジュールのlibsフォルダ内です。

なお、該当のファイルは以下の場所にあります。(環境によってパスは読み替えてください)

D:\Program Files\Unity2017.3.1p4\Editor\Data\PlaybackEngines\AndroidPlayer\Variations\mono\Release\Classes\classes.jar

Gradle設定にclasses.jarを含めないよう追記する

classes.jarは、UnitySendMessageを使うのに必要ですが、aarに含まれてしまうとUnityでのビルド時にエラーが出てしまうため、aarに含めないよう設定ファイルに記述する必要があります。

以下を、モジュール用のbuild.gradleに追記します。

android.libraryVariants.all{ variant->
  variant.outputs.each{output->
    output.packageLibrary.exclude('libs/classes.jar')
  }
}

全体としては以下のようになります。

plugins {
    id 'com.android.library'
}

android {
    namespace 'com.edo.speechrecognizer'
    compileSdk 32

    defaultConfig {
        minSdk 23
        targetSdk 32

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles "consumer-rules.pro"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

}

android.libraryVariants.all{ variant->
    variant.outputs.each{output->
        output.packageLibrary.exclude('libs/classes.jar')
    }
}

dependencies {

    implementation 'androidx.appcompat:appcompat:1.5.1'
    implementation 'com.google.android.material:material:1.7.0'
    implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: [])
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

設定ファイルはこれです。

ちなみに設定しないと、

com.android.build.api.transform.TrasnformException: com.android.idle.common.process.ProcessException: java.util.concurrent.ExecutionException: com.android.dex.DexException: Multiple dex files define Lbitter/jnibridge/JNIBridge$a;

みたいなエラーが出ます。

[2023.03.04 追記]

どうやら上の記述だとビルド時に含まれてしまうようになったっぽいです。
ので、依存関係の解決を以下のように変更します。( compileOnly に変更する)

- implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: [])
+ compileOnly fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: [])

AndroidManifestに録音権限を追記する

当然ですが、音声認識を利用するためにはマイクからの音を利用する必要があるため、AndroidManifestに下記の権限を追記する必要があります。

<uses-permission android:name="android.permission.RECORD_AUDIO" />

Unity側から呼び出す処理を実装する

プラグインが作成できたらそれを利用するためのC#側の実装を行います。
以下のようにしてプラグイン側の処理を呼び出します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class AndroidSpeechRecognizer : MonoBehaviour
{
    [SerializeField]
    private Text _text;

    private void Update()
    {
        if (Input.touchCount > 0)
        {
            Touch touch = Input.touches[0];
            if (touch.phase == TouchPhase.Began)
            {
                StartRecognizer();
            }
        }
    }

    private void StartRecognizer()
    {
#if UNITY_ANDROID
        AndroidJavaClass nativeRecognizer = new AndroidJavaClass("com.edo.speechplugin.recoginizer.NativeSpeechRecognizer");
        AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
        AndroidJavaObject context = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");

        context.Call("runOnUiThread", new AndroidJavaRunnable(() =>
        {
            nativeRecognizer.CallStatic(
                "StartRecognizer",
                context,
                gameObject.name,
                "CallbackMethod"
            );
        }));
#endif
    }

    private void CallbackMethod(string message)
    {
        string[] messages = message.Split('\n');
        if (messages[0] == "onResults")
        {
            string msg = "";
            for (int i = 1; i < messages.Length; i++)
            {
                msg += messages[i] + "\n";
            }

            _text.text = msg;
            Debug.Log(msg);
        }
        else
        {
            Debug.Log(message);
        }
    }
}

ネイティブプラグイン側で音声認識の結果をUnitySendMessageによってコールバックするようになっているので、それを受け取るコールバック用メソッドを実装しています。

プラグイン側では文字列でメソッド名を指定してコールバックを実行するので、プラグイン側にメソッド名を適切に渡す必要があります。

あとは認識した文字列を元に、行いたい処理を実装すれば音声認識を利用したアプリを制作することができます。

トラブルシューティング

もし、プラグインは実行できるのにすぐにエラーで停止してしまう場合は、マイクの権限が付与されていない可能性があるのでアプリの設定を見直してみてください。

参考記事

今回のプラグイン作成には以下の記事を参考にさせていただきました。

indie-du.com

fantom1x.blog130.fc2.com

fantom1x.blog130.fc2.com

Unity向けにAndroidのネイティブプラグインを作成する

概要

Unity向けに、Androidのネイティブ機能を呼び出す部分が作りたくて色々調べたのでまとめておきます。

今回は特にこちらの記事を参考にさせていただきました。

indie-du.com

Android Studioで新規プロジェクトを作成する

今回作成するのはaar(Android Archive)なので、新規で作成するプロジェクトは空の、ごくシンプルな状態で作成して問題ありません。(プロジェクト作成後に、モジュール追加してそっちでコードを書くので、通常のapk作成フローとは異なります)

f:id:edo_m18:20180320163214p:plain

次に、Phone and Tabletを選択し、Minimum SDKのバージョンを設定します。
ここは、後々、Unityの設定でも最低バージョンを指定する際に同じバージョンにしないとエラーが出るので、それなりに小さいのを選んでおいたほうがいいでしょう。

f:id:edo_m18:20180320163359p:plain

そしてAdd No Activityを選択してプロジェクトを作成します。

f:id:edo_m18:20180320163520p:plain

ライブラリ用モジュールを作成する

プロジェクトが作成できたら、ライブラリ用のモジュールを新規追加します。

f:id:edo_m18:20180320163800p:plain

Android Libraryを選択して追加します。

f:id:edo_m18:20180320163843p:plain

プラグイン名となるモジュール名を入力します。

f:id:edo_m18:20180320164033p:plain

すると、プロジェクトビューに、先ほど入力した名前でモジュールが追加されます。

f:id:edo_m18:20180320164210p:plain

Unityからアクセスするクラスを実装する

今回はネイティブの機能(ダイアログ)を簡単に呼び出すだけなので、なにも継承しないシンプルなJavaクラスをモジュールに追加します。
追加するには、Androidビュー内のモジュール名上で右クリックしてNew > Java Classを選択します。

f:id:edo_m18:20180320164823p:plain

そして、NativeDialogというクラス名でファイルを作成し、参考にさせていただいた記事同様の記述を行います。

引用させてもらうと、以下のコードになります。ネイティブのダイアログを表示する、というものです。
パッケージ名は自身の環境に合わせてください

package jp.co.test.dialog_plugin;

import android.app.AlertDialog;
import android.content.Context;

public class NativeDialog {

    static public void showMessage(Context context, String title, String message) {

        new AlertDialog.Builder(context)
                .setTitle(title)
                .setMessage(message)
                .setPositiveButton("YES, YES, YES!", null)
                .setNegativeButton("...No?", null)
                .show();
    }
}

Gradleを使ってビルドする

ファイルが追加されたら、Gradleを使ってビルドします。

Android Studioの右側にある「Gradle」タブを開き、:plugin-name(名称は自身で設定したもの)の下にある「Tasks > build > assemble」をダブルクリックして実行します。

f:id:edo_m18:20180320170330p:plain

基本はこれでビルドされるはずですが、自分の環境ではちょっとエラーで躓いてしまったので、同様のエラーが出た場合は、後述の対策を参照してみてください。

エラー内容

$ Error:A problem occurred configuring project ':hoge-plugin'.
> Could not resolve all dependencies for configuration ':hoge-plugin:_debugPublishCopy'.
   > Could not find any version that matches com.android.support:appcompat-v7:27.+.
     Versions that do not match:
         26.0.0-alpha1
         25.3.1
         25.3.0
         25.2.0
         25.1.1
         + 31 more
     Required by:
         project :dialog-plugin

ビルドが成功すると、プロジェクトフォルダ内(※)の「myplugin/build/outputs/aar」に、ビルドされたaarファイルが作成されています。

Android Studioのプロジェクトビューはいくつかのモードがあり、デフォルトはAndroidになっているので、これをProjectに変更するとフォルダ構造が見れるようになるので、そちらのモードにすると表示されるようになります。

f:id:edo_m18:20180320185855p:plain

上記dialog-pluginというモジュール名で作成した時のキャプチャです。
****-debug.aar****-release.aarが作成されます。今回はdebugのほうをimportしました。

これを、Unityのプラグインフォルダにコピーして利用します。

f:id:edo_m18:20180320170912p:plain

Unityから、ネイティブプラグインの機能を呼び出す

プラグイン部分が作成できたので、あとはこれをC#から読み込み、利用するコードを書きます。
以下のように、AndroidJavaClassAndroidJavaObjectを利用して構築します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AndroidDialogTest : MonoBehaviour
{
    private void Update()
    {
        if (Input.touchCount > 0)
        {
            ShowDialog();
        }
    }

    private void ShowDialog()
    {
#if UNITY_ANDROID
        AndroidJavaClass nativeDialog = new AndroidJavaClass("plugintest.edo.com.dialog_plugin.NativeDialog");

        AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
        AndroidJavaObject context = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");

        context.Call("runOnUiThread", new AndroidJavaRunnable(() =>
        {
            nativeDialog.CallStatic(
                "showMessage",
                context,
                "テスト",
                "ほげ"
            );
        }));
#endif
    }
}

Android Studioのビルドでハマった

Android開発をしている人にとっては多分なんてことはないことなんだと思いますが、Android Studioを使ったことがほぼないので、細かなエラーに悩まされました。

こちらの記事を参考に、ごく簡単なプラグイン作成を試してみたところ、

indie-du.com

以下のようなエラーが。

$ Error:A problem occurred configuring project ':hoge-plugin'.
> Could not resolve all dependencies for configuration ':hoge-plugin:_debugPublishCopy'.
   > Could not find any version that matches com.android.support:appcompat-v7:27.+.
     Versions that do not match:
         26.0.0-alpha1
         25.3.1
         25.3.0
         25.2.0
         25.1.1
         + 31 more
     Required by:
         project :dialog-plugin

どうも調べていくと、Android Support Libraryを最新にしろ、ということのよう。
ただ、調べてもSDK Managerでチェック入れてインストール、みたいなのしか出てこないのに、利用しているAndroid Studioだとそもそもそのチェック項目がない。なんでやねん。

と思っていたら、こちらにやり方が書いてありました。

qiita.com

要は、build.gradleに依存関係を書け、ということのよう。

以下のように追記しました。

allprojects {
  repositories {
    jcenter()
    maven { url 'https://maven.google.com' } // 変更点
  }
}

こちらも参考にさせていただきました。

animane.hatenablog.com

gradle

ちなみに余談ですが、最初、gradle自体が分かっていなかったんですが、ビルド自動化ツールでした。

GradleはApache AntやApache Mavenのコンセプトに基づくオープンソースビルド自動化システムであり、プロジェクト設定の宣言にはApache Mavenが利用するXML形式ではなくGroovyベースのドメイン固有言語 (DSL) を採用している[2]。Gradleはタスクの起動順序の決定に有向非巡回グラフ(英: Directed Acyclic Graph、DAG)を利用する。

[出典: wikipedia]

Android Studioではこれを取り入れていて、専用のタブやビューが存在します。
build.gradleはテキストファイルで、自動ビルドツール用の設定ファイルです。
(なので、プロジェクト内のどこかに存在しているので、それを編集します)

まとめ

以上で、ネイティブのダイアログを表示するだけのプラグインが作成できました。
最後に、全体の流れをまとめとして書いておきます。

  1. Android Studioで空プロジェクトを作る
  2. 作成したプロジェクトにモジュールを新規追加する
  3. モジュールに、ネイティブ機能を実行する処理を追加・実装する
  4. Gradleを使ってモジュールをビルドする
  5. 生成されたaarファイルをUnityにimportする
  6. Unityから、aar内の実装を呼び出す処理を書く

大まかにはこんな流れで実装していきます。
ビルドエラー周りが少々厄介ですが、それをクリアしてしまえば、比較的ネイティブの機能を実行するのは容易になるかなと思います。

Unreal Engine C++ 逆引きメモ

目次

概要

まだまだUEに慣れていないので、色々なことを忘れる前に逆引きでメモしておきます。
(なので、随時更新予定)

また、UEでC++を書くにあたって理解しておかなければならない点として、標準のC++とは「異なる」という点です。
どういうことかと言うと、UEではガベージコレクション、通称GCと呼ばれる仕組みを導入しています。

しかし、当然ながらC++にはガベージコレクションはありません。
そこでUEでは、独自のインスンタンス生成の仕組みや、マクロを用いたプリプロセッサ経由で様々な、GCのための準備をしてくれます。

そのため、標準のC++とUEで使うC++、言ってみればUE C++とで作法が違う、という点を覚えておく必要があります。

生成・取得・削除

C++クラスの生成

通常のC++では、newを用いてインスタンスを生成します。
しかし、UE C++ではインスタンスの生成方法に違いがあります。理由は前述のように、GC対象として管理するためです。
最近ではスマートポインタを使ったりしますが、それと似た感じですね。

UMyClass MyClass = NewObject<UMyClass>();

// オーナーを指定する場合は引数に入れる
// UMyClass MyClass = NewObject<UMyClass>(Owner);

当然のことながら、UE管理下に置かないような純粋なC++で書く処理についてはこの作法は適用されません。

コンストラクタ内でNewObjectは使えない

どうやら、コンストラクタ内では上記のNewObjectは使えないようです。(使うとクラッシュする)

ではどうするかというと、FObjectInitializer::CreateDefaultSubobjectを利用します。
FObjectInitializerは、コンストラクタ引数に指定しておくと、UEシステムが適切に渡してくれるようになっています。(詳細は後述)

以下のようにコンストラクタを定義することで利用できるようになります。

UAnyClass::UAnyClass(const class FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
    // オブジェクト生成
    UAthorClass* instance = ObjectInitializer.CreateDefaultSubobject<UAthorClass>(this, TEXT("hoge"));
}

しかし実はこのCreateDefaultSubobjectは、UObject基底クラスでラッパーが実装されているようで、オブジェクトを生成する目的だけであれば、CreateDefaultSubobjectを直に呼ぶことで同様のことを達成することができます。

UAnyClass::UAnyClass()
{
    // オブジェクト生成
    UAthorClass* instance = CreateDefaultSubobject<UAthorClass>(this, TEXT("hoge"));
}

なおこのコンストラクタに引数を指定した場合としない場合の挙動の差ですが、UEシステムが自動的に生成する****.generated.h内にてマクロが生成され、コンストラクタの定義に応じて書き換わるよういなっているようです。

詳細はこちら([UE4] ObjectInitializerでコンポーネント生成を制御する | 株式会社ヒストリア)の記事をご覧ください。

PlayerController / PlayerPawnを取得する

ゲーム開始時に生成されたプレイヤーコントローラ / プレイヤーポーンを取得するには、Kismet/GameplayStatics.hを読み込む必要があります。

PlayerController

#include "Kismet/GameplayStatics.h"

APlayerController* PlayerController = UGameplayStatics::GetPlayerController(this, 0);
if (PlayerController)
{
    // do anything.
}

PlayerPawn

#include "Kismet/GameplayStatics.h"

// UGameplayStatics::GetPlayerPawnを介して取得し、適切にキャストする
AAnyCharacter* MyCharacter = Cast<AAnyCharacter>(UGameplayStatics::GetPlayerPawn(this, 0));
if (MyCharacter)
{
    // do anything.
}

GameModeとの関連

なお、このPlayerControllerPlayerPawnの関係は、PawnをコントロールするのがPlayerControllerの役割です。
これらの設定はGameModeに設定するようになっており、またGameModeはプロジェクト設定にて設定され、これが実行時に起動するポイントとなるようです。

詳細はこちら↓

msyasuda.hatenablog.com

ワールドに存在するアクターをすべて取得する

#include "Kismet/GameplayStatics.h"

// find all AnyActors
TArray<AActor*> FoundActors;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AAnyActor::StaticClass(), FoundActors);

for (auto Actor : FoundActors)
{
    AAnyActor* AnyActor = Cast<AAnyActor>(Actor);
    if (AnyActor )
    {
        // do anything.
    }
}

アクターオブジェクトを生成する

オブジェクトの生成には、UWorldクラスのメソッドを利用します。

// FActorSpawnParametersを使うのに必要
#include "Runtime/Engine/Classes/Engine/World.h"

// AActor::GetWorldから、UWorldを得る
UWorld* const World = GetWorld();

// Nullチェック
if (!World)
{
    return;
}

FVector Location(0.0f, 0.0f, 0.0f);
FRotator Rotator(0.0f, 0.0f, 0.0f);
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = this;
SpawnParams.Instigator = Instigator;

AAnyActor* AnyActor = World->SpawnActor<AAnyActor>(ActorBP, Location, Rotation, SpawnParams);

削除中かを知る

まだ理解が浅いですが、Unityと同様、Destroyを実行しても即座にメモリから消えるわけではなく、ゲームとして破綻しないよう様々な終了処理が存在するはずです。
そのため、削除中、という状態が存在します。
それをチェックするには以下のようにします。

AActor* anyActor = ...;
anyActor->IsPendingKill();

移動

アクターを移動させる

AActorクラスにはGetActorLocationSetActorLocationがあるのでこれを利用する。

FVector location = actor->GetActorLocation();
location.X += 10.0f;
actor->SetActorLocation(location);

ちなみに、SetActorLocationの定義を見ると以下のようになっています。

/** 
 * Move the actor instantly to the specified location. 
 * 
 * @param NewLocation  The new location to teleport the Actor to.
 * @param bSweep       Whether we sweep to the destination location, triggering overlaps along the way and stopping short of the target if blocked by something.
 *                     Only the root component is swept and checked for blocking collision, child components move without sweeping. If collision is off, this has no effect.
 * @param Teleport     How we teleport the physics state (if physics collision is enabled for this object).
 *                     If equal to ETeleportType::TeleportPhysics, physics velocity for this object is unchanged (so ragdoll parts are not affected by change in location).
 *                     If equal to ETeleportType::None, physics velocity is updated based on the change in position (affecting ragdoll parts).
 *                     If CCD is on and not teleporting, this will affect objects along the entire swept volume.
 * @param OutSweepHitResult The hit result from the move if swept.
 * @return Whether the location was successfully set if not swept, or whether movement occurred if swept.
 */
bool SetActorLocation(const FVector& NewLocation, bool bSweep=false, FHitResult* OutSweepHitResult=nullptr, ETeleportType Teleport = ETeleportType::None);

引数を色々変更することによって、移動後の物理干渉などに対する設定や演算結果を受け取ることができるようです。

カメラの向いている方向に移動させる

まず、Worldからカメラマネージャを取得し、そこからカメラの前方を取得、それを元にアクターを移動させる、という手順で行います。

#include "Engine.h" // GEngineを使うのでインクルードしておく

void AMyActor::BeginPlay()
{
    if (GEngine != nullptr)
    {
        // CameraManagerをワールドから取得する
        CameraManager = GEngine->GetFirstLocalPlayerController(GetWorld())->PlayerCameraManager;
    }
}

void AMyActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
    
    FRotator cameraRot = CameraManager->GetCameraRotation();
    FVector dir = cameraRot.Vector();
    dir.Normalize();

    FVector location = GetActorLocation();
    location += dir * 10.0f * DeltaTime;
    
    SetActorLocation(location);
}

コンポーネント

Blueprintに、カスタムコンポーネントを表示させる

UCLASS(Blueprintable, meta = (BlueprintSpawnableComponent))
class PROJECTNAME_API UHogeMovementComponent : public UPawnMovementComponent
{
    // ... 略 ...
}

こんな感じで、UCLASS(Blueprintable, meta = (BlueprintSpawnableComponent))を指定すると、BPのAddComponentのリストに表示されるようになります。

コンポーネントのセットアップ

コンポーネントを適切にセットアップしないと、TickComponentが呼ばれないなど問題が出るため、適切にセットアップする必要があります。

void USampleComponent::AttachSampleComponent(AActor* Actor)
{
    if (!Actor)
    {
        return;
    }

    // Componentを新規生成
    UOtherComponent* Comp = NewObject<UOtherComponent>(Actor);

    // アクタにアタッチする
    Actor->AddInstanceComponent(Comp);

    // UActorComponent::RegisterComponentで、イベントループに登録する
    Comp->RegisterComponent();
}

追加されているコンポーネントを取得する

TArray<AnyComponent*> components;
GetComponents<AnyComponent>(components);

// コンポーネントの数を確認
UE_LOG(LogTemp, Log, TEXT("Count: %d"), components.Num());

Ownerを取得する

ComponentはActorのコンポーネントとして振る舞うため、オーナーを取得して操作することが増えるかと思います。
オーナーの取得は以下のようにします。

AActor *owner = GetOwner();

ComponentからInputを使う

InputまわりはUInputComponentが司ります。
UActorComponentではUInputComponentを保持していないので、オーナーなどから取得して適切にセットアップする必要があります。

PawnクラスのサブクラスなどではSetupPlayerInputComponentのタイミングでUInputComponentが渡ってくるので、そこでセットアップの機会があるようです)

https://docs.unrealengine.com/latest/INT/Programming/Tutorials/PlayerCamera/3/docs.unrealengine.com

AActor *actor = GetOwner();
actor->InputComponent->BindAction("Fire", IE_Pressed, this, &UAnyComponent::FireHandler);

void UAnyComponent::FireHandler()
{
    UE_LOG(LogTemp, Log, TEXT("Fire!!!");
}

※ Input系については、UE4のお作法に則って適切にセットアップする必要があります。

セットアップについては、以下の引越しガイドの「入力イベント」あたりに載っています。

docs.unrealengine.com

Static Meshを生成する

USceneComponent *root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
UStaticMeshComponent *mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("mesh"));
mesh->SetupAttachment(root);

static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("[reference]"));
UStaticMesh *asset = MeshAsset.Object;

mesh->SetStaticMesh(asset);

ちなみに、[reference]の部分は、UEエディタのアセットのコンテキストメニュー内のCopy Referenceから得ることができます。

f:id:edo_m18:20180208102926p:plain

参考:
forums.unrealengine.com

Colliderを設定する

コライダの設定にはUSphereComponentなどを利用します。
詳細パネルではSphere Collisionとか表示されるやつです。

AMyPawn::AMyPawn()
{
    // ... 中略 ...

    // Sphereコライダを生成
    USphereComponent *sphere = CreateDefaultSubobject<USphereComponent>(TEXT("Sphere"));
    sphere->InitSphereRadius(100.0f);
    sphere->SetSimulatePhysics(false);
    sphere->SetCollisionProfileName(TEXT("BlockAll"));

    // Sphereの見た目を生成
    UStaticMeshComponent *sphereVisual = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("SphereVisual"));
    sphereVisual->AttachTo(sphere);

    static ConstructorHelpers::FObjectFinder<UStaticMesh> meshAsset(TEXT("/Game/StarterContent/Shapes/Shape_Sphere.Shape_Sphere"));
    if (meshAsset.Succeeded())
    {
        sphereVisual->SetStaticMesh(meshAsset.Object);
        sphereVisual->SetRelativeLocation(FVector(0.0f, 0.0f, 0.0f));
        sphereVisual->SetWorldScale3D(FVector(0.8f));
    }
}

セットアップ時に行っているのは3点。

  • sphere->InitSphereRadius(100.0f);
  • sphere->SetSimulatePhysics(false);
  • sphere->SetCollisionProfileName(TEXT("BlockAll"));

です。それぞれ上から、

  • 球体の半径の設定
  • 物理挙動させるか(falseの場合は、Unityで言うIsKinematic = trueの状態)
  • コリジョンの仕方

となります。
特に最後の「コリジョンの仕方」は、衝突するのか、イベントのみ(overlap)なのか、というところ設定するので、意図した通りに設定しておかないと「衝突しないじゃん」となるので注意です。(というか、ここを勘違いしていてずっと衝突しなくてちょっとハマった)

コリジョンを処理する

コリジョンを設定したあとは、コリジョン発生時になにかしらの処理をしたい場合があります。
その場合に使えるのが、ColliderのもつGetOverlappingActorsメソッドです。

コライダの範囲にあるAActorオブジェクトを取得してくれます。

※ 以下のコードは、公式のチュートリアルの中で使われているコードの抜粋です。

void ABatteryCollectorCharacter::CollectPickups()
{
    // Get all overlapping Actors and store them in an array
    TArray<AActor*> CollectedActors;
    CollectionSphere->GetOverlappingActors(CollectedActors);

    // For each Actor we collected
    for (int32 iCollected = 0; iCollected < CollectedActors.Num(); ++iCollected)
    {
        // Cast the actor to APickup
        APickup* const TestPickup = Cast<APickup>(CollectedActors[iCollected]);

        // If the cast is successful and the pickup is valid and active
        if (TestPickup && !TestPickup->IsPendingKill() && TestPickup->IsActive())
        {
            // call the pickup's WasCollected function
            TestPickup->WasCollected();

            // Deactivate the pickup 
            TestPickup->SetActive(false);
        }
    }
}

Blueprintで利用する

C++で書いたクラス(コンポーネント)も、ブループリントから利用するように作成することが出来ます。
そのためのマクロがUPROPERTYUFUNCTIONです。
これらを適切に設定することで、ブループリントから設定できたり、あるいはGetter / Setterとして機能したり、あるいはブループリントで実装を促す、なんてこともできるようになります。

以下に、キーワードと意味を、よく見るものを抜粋して記載しておきます。
※ 英語ドキュメントの翻訳(意訳)なので、詳細についてはドキュメントをご覧ください。

Keyword 意味
BlueprintImplementableEvent ブループリント(ノード)でオーバーライドするように促す。そのため、Body(実装)部分は書いてはならない。UEにより、(ブループリントで)オーバーライドされた本体を実行するProcessEventを呼び出すためのコードが自動生成される
BlueprintNativeEvent BlueprintImplementableEvent同様、ブループリントでオーバーライドするようデザインされたものであるが、違いとしてはC++による実装を行う点。実装本体は[FunctionName]_Implementationを実装する必要がある。元の[FunctionName]内には、自動生成コードとして、[FunctionName]_Implementationを呼び出すコードが追加される(※1)
BlueprintPure このキーワードをつけられた関数は、副作用を起こさないものとしてマーク付けられ、さらにBlueprintCallableを意味します。Getメソッドの実装に適したものです。さらに、non-pureになるようにconst functionでfalseとマークすることもできる(※2)
BlueprintCallable Blueprintから呼び出しできるようにマークする
Category カテゴリを定義する。設定すると、Blueprint上でカテゴライズされて表示される

※1 [FunctionName]\_Implementationを実装しないとコンパイルエラーになる。定義すると、BPのEvent Graphで配置して利用できるようになる。以下、サンプル↓

UFUNCTION(BlueprintNativeEvent)
void HogeHoge();

virtual void HogeHoge_Implementation();

f:id:edo_m18:20180225214321p:plain

※2 PureとNon-Pureについて書かれている記事があったので、詳細はこちらをご覧ください→ [UE4] Pure関数とNonPure関数|株式会社ヒストリア

ドキュメント:UFUNCTION - Unreal Engine Wiki

Blueprintからプロパティに値を設定できるようにする

コンポーネントを作成し配置しても、適切にマクロを設定しておかないとBPからプロパティに対して値を設定することができません。

以下のように設定することでそれが可能となります。

UPROPERTY(BlueprintReadWrite, meta = (ExposeOnSpawn)

ドキュメント: UPROPERTY - Epic Wiki


その他

enumを定義する

l参考にしていたVideo Tutorialは若干古いバージョンだったため、その中で説明されていたenumだとエラーが出てコンパイルができませんでした。
ということで、いちおうメモ。

UENUM(BlueprintType)
enum class EHogeEnum : uint8
{
    EFuga,
    EFoo,
    EBar,
};

と、uint8を継承した形で宣言しないとなりません。

タイマーを利用する

タイマーの利用には、FTimerManagerクラスを利用します。
以下の例では、自身を再帰的にタイマーで呼び出します。

// header
FTimeHandle Timer;
void AAnyActor::TimerHandler();

// implementation
void AAnyActor::TimerHandler()
{
    // &AActor::GetWorldTimerManager
    float Delay = FMath::FRandRange(1.0f, 2.0f);
    GetWorldTimerManager().SetTimer(Timer, this, &AAnyActor::TimerHandler, Delay, false);
}

ドキュメントはこちら

docs.unrealengine.com


トラブルシューティング

TickComponentが呼ばれなくなった!

C++を書いていて、突然、少し前まで正常に動いていたTickComponentが動かなくなるケースがありました。

色々調べてみても、必要なフラグの扱いやらメソッドの定義やらは正常に行っている・・でも動かない。

最終的に解決したのは、「該当のComponentを一度消し、追加し直す」ことで解消しました。
そのあたりが書かれていた記事がこちら↓

community.gamedev.tv

まさか入れ直しだけで解決するとは・・。
おそらくですが、(社内のエンジンに詳しい人と話していて聞いたのは)Hot Reloadの機能がUE4には備わっていて、それの関連付けなどがおかしくなってしまったのでは、とのこと。
多分、その関連付け周りの処理が、コンポーネントの追加・削除のタイミングで行われているのでしょう。

なので、追加し直しで直ったのではないかなと。

ちなみに、TickComponentを呼ぶ必要があるかどうか、みたいなフラグ周りについては以下の記事が色々まとめてくれているので参考にしてみてください。

usagi.hatenablog.jp

case内の初期化

これはUEというよりC++の問題ですが、switch文内で初期化を伴う処理を書いている場合、case文を{}で囲まないとコンパイルエラーとなるようです。


その他Tips

色々な値をログ出力

Log Fomatting

  • LogMessage
UE_LOG(LogTemp, Log, TEXT("Hoge"));
  • Log an FString
FString anyString = ...;
UE_LOG(LogTemp, Log, TEXT("Log: %s"), *anyString);
  • Log an Bool
bool anyBool = ...;
UE_LOG(LogTemp, Log, TEXT(Bool value: %s"), anyBool ? TEXT("True") : TEXT("False")); 
  • Log an Int
int anyInt = ...;
UE_LOG(LogTemp, Log, TEXT("Int value: %d"), anyInt);
  • Log a Float
float anyFloat = ...;
UE_LOG(LogTemp, Log, TEXT("Float value: %f"), anyFloat);
  • Log an FVector
FVector anyVector = ...;
UE_LOG(LogTemp, Log, TEXT("FVector value: %s"), *anyVector.ToString());
  • Log an FName
FName anyName = ...;
UE_LOG(LogTemp, Log, TEXT("FName value: %s"), *anyName.ToString());

ドキュメントはこちら → Logs, Printing Messages To Yourself During Runtime

Tickメソッド外で時間を扱う

Tickメソッド内では、引数にDeltaTimeが渡ってくるのでそれを利用すればいいですが、それ以外の場合はワールドから取得する必要があります。

void AnyMethod()
{
    float time = GetWorld()->GetTimeSeconds();
    float deltaTime = GetWorld()->GetDeltaSeconds();

    UE_LOG(LogTemp, Log, TEXT("Time: %f, DeltaTime: %f"), time, deltaTime);
}

番外編

ちょっと昔に書いた記事ですが、Cocos2D-xでアプリを作る際にまとめた、C++関連の記事です。
UEとは関係ない部分もありますが、C++的なところは同じなので紹介。

qiita.com