Skip to content
第01回 3Dダイスポーカー

3 / 4 ダイスの出目の判定 ​

このステップの目的

ダイスの面の法線とワールド上方向のベクトルの内積から、最も上を向いている面を選ぶ

出目の確認 ​

出目の判定結果出目の判定結果

まずはダイスの出目を判定する機能を実装します。

ダイスの各面から伸びる矢印はその面の法線、白い矢印はワールド上方向です。

法線とは

法線は、面に垂直な方向を示すベクトルです。
ここでダイスの各面から伸びる矢印は、その面の法線を表しています。

どの面が上を向いているかの判定は、各面の法線とワールド上方向の内積を計算して行います。

ベクトルの内積 ​

ベクトルの内積は、2つのベクトルの方向の一致度を示す値です。

長さが1の単位ベクトル同士の内積は、方向の一致度を直接表します。

  • 値が1に近いほど、2つのベクトルは同じ方向を向いていることになります。
  • -1に近い場合は逆方向を向いています。
  • 0に近い場合は直角に交わっています。

つまりダイスの各面の法線とワールド上方向の内積を計算し、最も大きなベクトルの面を選ぶことで、ダイスのどの面が上を向いているかを判定できます。

ベクトルの正規化

ベクトルの長さでベクトルを割ることで単位ベクトルを得ることができます。
これをベクトルの正規化と呼びます。

内積はベクトル同士の積で、互いのベクトルの長さの影響を受けます。
法線は主に方向を示すベクトルであり、通常長さを1に正規化したものを使用します。

面と軸の対応 ​

法線(normal)は、面に対して垂直な方向です。
今回は、ダイスの6面がそれぞれどちらを向いているかを表す、長さ1の外向きのベクトルを使います。

物理挙動は立方体の6面を基準に考えます。
3Dモデルの目のくぼみや丸めた角の細かい面は扱いません。

まず、Assets/UNSLOW/DicePoker/Scenes/DiceFaceTest.unityを開いてPlayしてください。

DiceFaceTestの初期状態DiceFaceTestの初期状態

投げる前のダイスの各面から伸びる短い矢印が、その面の法線を表します。
赤はX方向の面、緑はY方向の面、青はZ方向の面に対応します。

1は+X、2は+Y、3は+Zの面です。
初期姿勢では2が上を向いています。

出目ダイスのローカル座標での法線配布済みのベクトル
1+XVector3.right
2+YVector3.up
3+ZVector3.forward
4−ZVector3.back
5−YVector3.down
6−XVector3.left

DiceFace.Normalsには、この順番で6個のベクトルが用意されています。
配列の添字0が出目1、添字5が出目6に対応します。

csharp
public sealed class DiceFace : MonoBehaviour
{
    /// <summary>
    ///     ダイスの各面の法線方向
    /// </summary>
    public static readonly Vector3[] Normals =
    {
        Vector3.right,      // 1(x+)
        Vector3.up,         // 2(y+)
        Vector3.forward,    // 3(z+)
        Vector3.back,       // 4(z-)
        Vector3.down,       // 5(y-)
        Vector3.left        // 6(x-)
    };

TIP

一般に、ダイスは反対にある面の目を足すと7になるよう作られています。
ここでもそのルールに従って目を設定しています。

ローカル座標系とワールド座標系 ​

ローカル座標系は、個々のオブジェクトが自分自身を基準にした座標系です。
ワールド座標系は、Scene全体で共通の座標です。

ダイスの各面の法線方向を向いた6本の矢印はダイスの子になっているため、ダイスと一緒に回転します。
例えば1の面の法線は、ダイスから見れば常に+Xですが、ワールドから見た向きは回転に応じて変わります。

一方でダイスの外に置かれた白い矢印は、回転とは関係なくワールドの真上を向き続けます。

Spaceキーまたは「ダイスを振る」ボタンで投げ、ダイスと矢印が回る様子を確認しましょう。
最終的に、上面から伸びる矢印と白い矢印がおおむね同じ方向を向いて停止するはずです。

つまり、各面の法線とワールド上方向の内積が最も大きい面を求めることができれば、ダイスの上面を判定することができます。

内積による上面判定 ​

長さ1の単位ベクトル同士では、内積は向きの一致度として使えます。

内積上方向との関係
1同じ向き。真上を向いている
0直角。横を向いている
−1逆向き。真下を向いている

6面の内積一覧は常に表示され、ダイスが回っている間も更新されます。
各面の矢印と白い上方向の矢印の関係を、数値と見比べてください。

例えば面2が真上を向く姿勢では、面2の内積は1、反対の面5は−1、残りの面は0になります。

  1. ダイスを投げ、止まったときの上面を模様から読み取る。
  2. その面と反対側の面について、内積が1・0・−1のどれに近いかを予想する。
  3. 一覧の値を読み、予想と比べる。最も大きい値の面と、見た目の上面が一致するか確認する。

DiceFace.Read()の実装 ​

Playを止め、Assets/UNSLOW/DicePoker/Scripts/DiceFace.csを開いてDiceFace.Read()を実装します。
最終的には以下のようになるはずです。

csharp
    /// <summary>
    ///     ダイスの上面の出目を読み取る
    /// </summary>
    /// <returns>ダイスの上面の出目(1〜6)</returns>
    public int Read()
    {
        var value = 1;
        var best = -1f;

        for (var i = 0; i < Normals.Length; i++)
        {
            var direction = transform.TransformDirection(Normals[i]);
            var dot = Vector3.Dot(direction, Vector3.up);

            if (dot <= best)
                continue;

            best = dot;
            value = i + 1;
        }

        return value;
    }

Normalsにはダイス自身を基準にした法線方向が入っています。
この各法線とワールド上方向とを比較するためには、両方を同じ座標系で表す必要があります。
ここでは法線をローカル座標からワールド座標に変換して、ワールド上方向との内積を計算します。

DiceFace.cscsharp
var direction = transform.TransformDirection(Normals[i]);
var dot = Vector3.Dot(direction, Vector3.up);

1行目のtransform.TransformDirectionは、ダイスのローカル座標系の法線方向をワールド座標の方向に変換します。
transformは、このDiceFaceが接続されているダイスのTransformです。

2行目では、変換した法線directionとワールド上方向Vector3.upの内積をdotに保存します。

var について

varは、式の右辺から変数の型を決める書き方です。
ここではdirectionはVector3型、dotはfloat型になります。

自動での型決定は都度変数の型を手書きするよりもコードが簡潔になり、可読性も向上します。
是非活用してください。

最大値の面を選ぶ ​

ループで各面に対して内積の計算を行い最も内積が大きい面を選び、valueとして返せば出目の判定が完成します。

DiceFace.cscsharp
if (dot <= best)
    continue;

best = dot;
value = i + 1;

continueは、今回の繰り返しの残りを飛ばして次の繰り返しへ進む命令です。
最大値が更新されていない場合は処理をスキップします。
更新された場合はbestとvalueを更新します。

iは、調べている面の添字0〜5を表しています。
配列の添字は0から、出目は1からなので、i + 1で対応させます。

結果を照合する ​

コードが完成したら保存してUnityへ戻り、Consoleのエラーがないのを確認してからPlayします。
ダイスを投げて、「実際の上面の模様」「一覧で内積が最大の面」「Readの結果」の3つが一致するかを確認してください。
何回か投げ直し、都度上の3つが一致するかを確かめましょう。

内積の活用 ​

今回はベクトルの内積をダイスの上面の判定に使いましたが、内積は3Dゲーム制作では極めて重要な役割を果たします。
例えば以下のような場面で使われます。

  • 敵の視線がプレイヤーをとらえているかの判定
  • カメラが対象を正面に捉えているかの判定
  • 誘導ミサイルの回転量を求めるための計算

是非、内積を活用して自身の3Dゲーム制作に役立ててください。

うまくいかない場合 ​

コードの該当箇所を確認してください。

  • transform.TransformDirectionを通して法線をワールド座標に変換しているか
  • 内積の最大値を更新できているか
  • 添字に1を足して出目としての値に変換できているか

確認後はUnity EditorのPlayを停止して次のステップに進みましょう。

チェックリスト
  • 転がるダイスの矢印と、変化する6面の内積を確認
  • 面ごとの法線をワールド空間へ変換している
  • bestとvalueが、どの条件で更新されるか説明できる
  • Vector3.Dotで上方向との一致度を求め、最大の面を出目にしている
  • 投擲後に、上面・内積が最大の面・Readの結果が一致している
  • 投げ直して違う面が上を向いても、判定結果が一致している