Unity_3d_practice
https://github.com/skonishi1125/unity_3d_practice
https://scrapbox.io/files/6965b1cd3df7428c4cae454e.png
3Dに慣れるためのプロジェクトを用意して、自由に触れる場所を作る。
三次元ベクトル、クォータニオンなどの数学操作やカメラをぐりぐり調整する操作に慣れよう。
土台作り
Gridbox Prototype Materialsを導入。
UDPを使うとテクスチャ切れ(明るい紫)にならず動く。
New Input System
https://docs.unity3d.com/Packages/com.unity.inputsystem@1.14/manual/Installation.html
アセットストアから入れると右クリックで作れるようになる。
Generate C# Classで用意。ボタンセット。
Player.csで呼び出すようにする。
登録について
code:c#
input.Player.Movement.performed += ctx => moveInput = ctx.ReadValue<Vector2>();
input.Player.Movement.canceled += ctx => moveInput = Vector2.zero;
今見ると、これはイベントリスナーの登録と同じことをしていることが分かる。performedというボタンが押されたという時に、合わせて発火させる内容を指定しているということ。無名関数でVector2の値をmoveInputという変数に入れているという感じ。離したらベクトル0を入れている。
オブジェクト構成
code:txt
Player (空のGameObject / Transform, Script, CharacterController)
└── Model (空のGameObject / 見た目全体をまとめる親)
├── Body (Cube)
└── Head (Sphere)
└── Nose (Cube)
こんな感じが基本
3D空間における、前方forwardの定義
基本的にZ軸のプラス方向が正面とされる。
X軸のプラスが右。Y軸のプラスが上方向。なので、モデルを管理するときもZ軸のプラス方向が顔になるようになどを考慮すること。
Cinemachine
https://docs.unity3d.com/Packages/com.unity.cinemachine@3.1/manual/setup-follow-camera.html
GameObject > Cinemachine > Targeted Cameras > Follow Cameraで作成
Y軸のFollow Offsetを調整した
3次元の動き
Inputで動かす
Vector2で入力を管理しているとき、xがx軸の動き、zがy軸の動きを受け付けることになるのでそれをまず考慮。
Character Controllerコンポーネントを付与(移動に関する物理計算をカプセル化したAPIみたいなもの)。このコンポーネントのメソッドを使って基本的に操作していくこととなる。
Character Controllerについて詳しく知る
Rigidbodyを用いずに、Colliderの機能も内包したcomponent。プレイヤー移動など、きびきびした操作を実現する場合はこちらを用いるのがUnity 3Dアクションの基本。自前でCapsule型のColliderを持つため、Colliderの付与なども必要ない。移動面もスクリプト側でMove()メソッドなどを呼んで値を指定すれば、壁を突き抜けない、斜面を上るといった計算も行うことができる。また、衝突されても吹き飛ばなかったり、勝手に転んだりすることもないという、移動に特化しつつ、多少の物理法則を持たせることができる。便利。2Dだとrb +coの組み合わせをベースに、ドット絵がこけたりしないように回転方向を固定させていたが、3Dはもっと複雑になるため、そのあたりを楽にするためにこのコンポーネントを付ける。
Height, Radius, CenterでColliderの大きさを調整できるので、キャラに合わせて大きさを調整する。
マウスクリックした方向に向けて、振り向く処理を作る
手順は以下の3ステップ
カメラレンズからマウスクリック位置に向かってRayを放つ。
その光線が地面のどこに当たったのかの座標を取得する。
今の自分の位置から、当たった場所へのベクトルを計算して回転させる。
計算処理について
まずはRayがHitしたとき、その位置 - Playerの位置を引き、角度のベクトルを取得する
code:c#
// 座標取得 (A: Player B: クリック地点)
Vector3 posA = transform.position;
Vector3 posB = hit.point;
// ベクトルの引き算
Vector3 direction = posB - posA;
direction.y = 0; // 一旦縦方向は考慮せず、地面と水平な回転を考慮
https://scrapbox.io/files/69647ae70c17b06a83ba2654.jpeg
取得したベクトルを使って、クォータニオンで計算を行い回転を決める。
code:c#
direction.y = 0; // 一旦縦方向は考慮せず、地面と水平な回転を考慮
if (direction != Vector3.zero)
{
Quaternion targetRotation = Quaternion.LookRotation(direction);
transform.rotation = targetRotation;
}
ここで使われているのはクォータニオンを使った処理だが、行列で表すと以下のような感じ。
(上で導いたベクトル(3,0,3)を用いているわけではないが、イメージ)
https://scrapbox.io/files/69647af1cba4e6c5aeef0edc.jpeg
⭐行列計算は小数点部分で懸念があったり処理が重いので、実際はクォータニオンといって別の計算式が使われる。線形代数分かるようになったらまた調べてみよう。
正規化して、精度を高める
こちらで振り向き処理を行っても良いが、出来れば正規化(向きを保ちながらベクトルの長さを1にすること。厳密には単位ベクトルとは異なるが、単位ベクトルとして呼ばれることもある)しよう。
code:c#
Vector3 direction = posB - posA;
if (direction != Vector3.zero)
transform.rotation = Quaternion.LookRotation(direction);
素で出た形でクォータニオン計算に用いても良いが、
code:c#
Vector3 targetDir = (posB - posA).normalized;
if (targetDir != Vector3.zero)
transform.rotation = Quaternion.LookRotation(targetDir);
こちらのほうが計算が楽になり、結果的に処理が軽くなることが多い
内積とかの計算式を考えればこの理屈はわかる。
内積a・b = |a| |b| cosθなので、|a|と|b|を正規化して1にしておけば、実質的にa・b = cosθの値で比較することができるようになる。
視野角について考慮してみる
code:c#
float dot = Vector3.Dot(forwardDir, targetDir); // 内積計算
float viewingAngle = 0.707f; // とりあえず視野角 0.707 = cos45°とする
if (dot > viewingAngle)
Debug.Log($"<color=green>クリック位置は視野内です。</color> (内積: {dot:F2})");
else
Debug.Log($"<color=yellow>クリック位置は視界外です。</color> (内積: {dot:F2})");
内積(dot)と、cosθの値を求める。θ=45°としたとき、視野角は90°。
なぜ視野角がcosθの2倍の値になるのか
cosθ = 45°としたとき、これを満たすのは以下の図のように2つある
cos0°を正面としたとき、結果的に90°の視界になる
https://scrapbox.io/files/6964b62ee7c63a687cf6978f.jpeg
Unity_3dタワーディフェンスでは、このように判定している
code:c#
// Towerから見た正面ベクトル と 敵ポジションを内積で比較
float dot = Vector3.Dot(forwardDir, targetDir);
// GetViewingAngle()は、Degreeで返る。
// また合計の値が返る(90°なら、前方から-45から+45°の範囲となる)。
// cosθとして使うには、Radianにして、引数を渡す必要があるので変換
// ex) 90°なら、radianにして、cos45° (0.707...)を返して、内積と比較させる
float halfAngleRad = (status.GetViewingAngle() * .5f) * Mathf.Deg2Rad;
float cos = Mathf.Cos(halfAngleRad);
//Debug.Log($"内積: {dot} cosθ: {cos}");
if (dot > cos) {
// 視野角内だったときの処理...
外積を使って、左右判定をする
内積は2つのベクトルがどれくらい同じ向きかを表す値だが、外積は2つのベクトルがどれくらいズレているかを示すベクトルのこと
内積はスカラー(floatで表せる数字)を出すが、外積は2つのベクトルに垂直な新しいベクトルを作る。なので、外積の結果はVector3で受け取ることになる
code:txt
外積 C = A × B として書く(単純なかけ算というわけではない)
・AとBの両方に垂直な方向を持つ
・AとBが作る平行四辺形の面積に等しい大きさを持つ
AとB両方に垂直な方向を持つということは2次元空間ではないので、外積は基本的に3次元ベクトルだけの概念。
外積Cのy座標が、ターゲット方向ベクトルの左右ズレの値そのものになる。
yが上向き(プラス)なら右向き、下向き(マイナス)なら左向き。
? 外積周りの基礎などをしっかり理解しないと詳細に理解するのは難しいかも。右ねじとか。とりあえずy軸が+なら右、-なら左ということで覚えておこう。
code:c#
Vector3 cross = Vector3.Cross(forwardDir, targetDir );
string side = cross.y > 0 ? "右" : "左";
Debug.Log($"<color=cyan>{side}</color>振り向き (外積: {cross:F2})");
Slerpで振り向く処理
code:c#
private void Update()
{
if (Input.GetMouseButtonDown(0))
RotateToMouseCursor();
RotateSmoothly();
}
// RotateToMouseCursor()
if (targetDir != Vector3.zero)
targetRotation = Quaternion.LookRotation(targetDir);
private void RotateSmoothly()
{
transform.rotation = Quaternion.Slerp(
transform.rotation,
targetRotation,
Time.deltaTime * rotationSpeed
);
}
LookRotation()
指定された方向をz軸にする(前)。外積を内部で計算してあれこれ決定している
Quaternion.Slerp(A, B, t)
AからBに向けて、tの割合だけ近づける。
常に残り距離と比較したtの割合だけ進むということになるので、アキレスと亀のように無限に分割されることになるが、floatを用いた計算には限度があるので、無限に分割されることはない or 画面上で認識できないほどの微小な振動に収まる。
左右どちらから回るかもSlerpが決定している(外積をこっちで渡さずとも、最短経路を決定してくれる)
座標空間とカメラ
Camera.main.ScreenPointToRay(Input.mousePosition);
下の図の赤い線のイメージで良い。カメラからRayを出してそのポイントを取得している。高さにもちゃんと依存する。
https://scrapbox.io/files/6964ff9f43162984a9e6a23c.png
code:txt
# 段差のある所のクリック例
マウス座標: (471.74, 907.52, 0.00) / hit.point: (-5.29, 3.96, 0.76)
# Playerの立っているところと同じ高さにクリックした時の例
マウス座標: (952.04, 297.20, 0.00) / hit.point: (-0.08, 0.00, -4.36)
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
Debug.Log($"マウス座標: {Input.mousePosition} / hit.point: {hit.point}");
物理エンジンと移動床
弾転がしゲームを作って、理解に努めてみよう。
Sphere
rb
Interpolateを有効化して滑らかな動きを表現しておく
高速に動かしてすり抜けることがあれば、Continuousの設定をしておく。
ジャンプ処理
Update()で入力を受け付ける。実際に飛ばす処理はFixedUpdateでやる。
瞬間的に力を入れたい場合は、rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);というような第二引数を付与する。
動く床の処理
code:Movingfloor.cs
private void Update()
{
Vector3 moveVector = transform.position;
// 前後させるだけの処理
float sinValue = Mathf.Sin(Time.time) * sinMultiplier;
if (sinValue > 0)
moveVector.z = moveVector.z - moveSpeed * Time.deltaTime;
else
moveVector.z = moveVector.z + moveSpeed * Time.deltaTime;
transform.position = moveVector;
}
これは、こうなる
https://www.youtube.com/watch?v=TdXxxtg_yKA
transform.positionは、Unityにおいてはテレポート処理の連続として扱われる。なので、上に乗ったものが連動して動く力というのは働かない。上のものも動かす場合は、床にもRigidbodyを追加し、Is Kinematic等の設定である程度固定する。
動かし方
Rigidbodyを取得。Vector3 startPosition等を用意し、デフォルトの位置を変数に保持しておく(移動する範囲の真ん中などにオブジェクトを配置しておくと楽)。動かすときに、rb.MovePosition()を用いて動かすとよい。それでも滑る場合は、摩擦のMaterialを割り当てておくと緩和できる。なおこちらは、乗る側にもRigidbodyが必要。
? Character Controllerが付与されている場合は?
今回は弾転がしゲーム(SphereにRBが付与)なので動作するが、実際アクションゲームなどでCharacter Controllerが付与されている場合はRigidbodyが存在しないので、床との物理的な摩擦が発生せず、結果床が移動してもPlayerはその場にとどまり続ける。そのため、スクリプトで制御する
Player側で、真下にRaycastする。そのレイヤーが移動床だった場合、移動床のrbを取得。rbから加速度を取り出し動かしてやればよい。
code:c#
// PlayerのUpdateで行う処理 下にRayを出して移動床だったとき、どうするみたいな処理
if (Physics.Raycast(
transform.position, Vector3.down, out hit,
movefloorCheckDistance, whatIsMovingfloor
))
{
Rigidbody floorRb = hit.collider.GetComponent<Rigidbody>();
Vector3 floorVelocity = floorRb.linearVelocity;
controller.Move(floorVelocity * Time.deltaTime);
Sin波を使った往復処理についても、触れておこう
code:c#
private void Move()
{
float offsetZ = Mathf.Sin(Time.time * moveSpeed) * sinMultiplier;
Vector3 targetPosition = startPosition + new Vector3(0, 0, offsetZ);
rb.MovePosition(targetPosition);
}
Time.timeはゲーム開始からの秒数。Mathf.Sin(radian)は、必ず-1 ~ +1の範囲が返る。
https://scrapbox.io/files/6965bf847dac21f0575750d9.jpeg
(縦x 横yって書いたけど誤植)
キャラのずれ解消
Character ControllerやGizmoと3Dモデルがずれている
https://scrapbox.io/files/6965b1cd3df7428c4cae454e.png
Playerの原点から正しく出ており、Character Controller及びGizmoは問題ない。なので、どちらかというと直すのはビジュアル側のほうである。
code:txt
Player
- Visual
- Head
- Body
というように分けて、Visualの座標を下げて調整すると楽。
3Dモデル操作 基礎
キャラクターに骨組みを入れて、動かせる状態にすることをリグ(リギング)と呼ぶ。
骨組みそのものをボーンと呼び、ボーンを動かすことでどのパーツが動くかを設定する作業のことを指すこともある。キャラクターの外見の形を表現するものがメッシュと呼ぶ。ボーンとメッシュの動きを連動させるようなイメージになる。ボーンとメッシュの動きの整合性を取ることをスキニングと呼ぶ。
UnityではHumanoidという仕組みがあり、リグが正しく設定されていれば、別キャラクターで作ったアニメーションを使いまわすことも出来る。
Animation Rigging
Unity上でキャラクターの動きを、後付けで補正したり制御ができるパッケージ。歩行などのアニメーションファイルをベースに、NPCを見つめたりとか、特定のものに視線を流したいときに使う。
Constraint(コンストレイント)
3D開発での制約、拘束を指す単語。これでモデルの動きを制御する。
実践
導入してみよう
Window > Package Manager > Unity RegistryでAnimation Riggingを導入
UnityのタブにAnimation Riggingがあるので、設定したいオブジェクトのビジュアル部分を選択して、Rig Setup。Rig Builderが付与される。Bone Setupで特定のボーンを調整するBone Builderを付与できる。
3Dモデル用意、アニメーションの待機、歩行の割当
3DモデルをObjectとして配置できる。今回はそのモデルをmixamoに入れて、Unityで使えるようにしたファイルを用意(character_mainというモデルがあったとき、入れ子構造でリグ情報のオブジェクトを見ることができるようになっている)
code:txt
Playet (ここにCharacter Controllerを付与)
- character_main (ここにAnimatorを付与)
- mixamorig: ....
- mixamorig: ....
Animatorコンポーネントに割り当てるもの
2Dと同じく、Animation Controllerが必要。そして、Avatarが必要。Avatarは、3Dモデルの骨組み(リグ)をUnityが理解するための規格に変換したもの。mixamoから落としたデータの、Rig > Avatar Definitionから生成できるので、そちらで作る。
https://scrapbox.io/files/696737f2ff68a69fdf080347.png
Humanoid設定は、モデルが人間の形をしていることを示す設定値。Unity側が解釈して、人間特有の動きを最適化する。ロボットとか動物とかなら、Generic等の設定にするとよい。
Avatar Definition: この3Dファイル(.fbx)からAvatarファイルを作るようにするという指定。設定することでAvatarファイルができるので、Animator componentに渡せる。
Controllerにデフォルトアニメーションとして、このidleモーションを割り当てるとこの時点で動作するようになる(三△みたいなアイコン)。
左右移動
Vector2で移動を受け取る前提とすると、WSがz軸の移動、ADがx軸の移動になる。なので度の入力もないときはidle, そうでないときは前後, 左右移動のアニメをつくるといい。
Blend Treeで、Blend Typeを2D Simple Directionalに。モーションを今回は5つ入れる。idleと、上下左右移動。
? キャラのモデルだけ、Character Controllerなどより早く動く
Apply Root Motionが有効になっていた。キャラのアニメに応じて移動もさせるという設定。スクリプトで制御しているので、スクリプトの値 + この値で、アニメだけ早く動いてしまっていた。
xVelocityと、zVelocityの値の計算式
code:c#
private Vector3 moveDirection;
private void Move()
{
// 入力(Vector2)を、3D空間の移動方向(Vector3)に変換する
// 2DのY(上下)を、3DのZ(前後)に入れ替える
moveDirection = new Vector3(moveInput.x, 0, moveInput.y);
controller.Move(moveDirection * speed * Time.deltaTime);
}
private void SetAnimatorParams()
{
float xVelocity = Vector3.Dot(moveDirection.normalized, transform.right);
float zVelocity = Vector3.Dot(moveDirection.normalized, transform.forward);
anim.SetFloat("xVelocity", xVelocity);
anim.SetFloat("zVelocity", zVelocity);
}
真正面(画面で見ると、キャラの背中が見えている状態)
前方入力した時
まずは内積の計算。内積 = 2つのベクトルがどの程度同じ向きなのかを返す。moveDirectionは純粋なinputの値ではなく、speedとかTime.deltatimeが考慮されているので、まずは正規化(向きを保ちながらベクトルの長さを1にする)。transform.rightは単位ベクトルなので加工しなくて良い。すると、
xVelocityは、(0,0,1)と(1,0,0)の内積。= |a||b|cosθ, θは90°。よってcos90° = 0 、
zVelocityは、(0,0,1)と(0,0,1)の内積。 = cosθ, θは0°。よってcos0° = 1
斜め上入力のとき(↗)
moveDirectionは、正規化すると(1/√2, 0, 1/√2)になる。右に1, 上に1入力しているので、その斜めの値は√2(rの値)になる。要するに1:1:√2の形。これを正規化する(r=1にする)と、1/√2 : 1/√2 : 1の形になるので、その結果がxとzに返っている(1/√2 = √2/2 = 0.707)。正規化されているか、絶対値でチェックもできる。
https://scrapbox.io/files/696757eeeb8fba514a97c8b1.png
つまり、xVelocity = cos45° = 1/√2 = 0.707
zVelocity = cos45° = 0.707 が返る
animatorに渡されるのは、上の値。結果 前 + 右のアニメがブレンドされて返る
移動を滑らかに仕上げる
移動のtransfer(前から後ろなど)に遷移したとき、少しかくついた動きになる。anim.SetBoolにdumptime(遷移にかける時間)を設定することで解決する。
そのアニメ中に顔だけ、特定のオブジェクトを向くようにする
Animation Riggingにて、Rig Setupと、Bone Setupで骨組みを付与する
https://scrapbox.io/files/6969fd70c3aba1bc1bf2d04c.png
下記画像のように、Rig 1 というObjectが配置されているので、こちらの中に作って対応していく。
https://scrapbox.io/files/6969fdb2706449cbfcb0f00b.png
code:txt
Rig 1
- Head_Aim (Multi-Aim Constraint コンポーネントの付与)
- Target
Multi-Aim Constraint
今回頭を調整したいので、Constrainted Objectにプレイヤーの頭を指定する。
Aim Axis, Up Axis
頭にとっての前軸、上軸が何かを指定する。ローカル座標として該当のオブジェクトを確認するのが良い。今回はZが前、Yが上。
https://scrapbox.io/files/6969ff6bd81e0a8a2942ca2c.png
Source Objectsで視線誘導したいものを指定すれば、その方向に向く。
https://scrapbox.io/files/696a00742c7ff7cce3eb7a14.png
特定の場所に、特定のオブジェクトをくっつける
たとえば、左手を頭にくっつけるような調整もできる
まずは用語理解から
FKForward Kinematics: 順運動学。肩、肘、手首の順番で角度を決めていくやりかたのこと
IKInverse Kinematics: 逆運動学。最終的な位置を先に決定し、それに合わせて肘や肩の角度を自動で計算すること。今回は、目標地点をターゲットにするということからこちら。慣習的にLeftHand_IKという命名にしよう。
componentとして、Two Bone IK Constraintを使う。Tipに左手を割当。Auto Setup from Tip Transform でその後の設定も自動調整できる。
targetとhint(ヒンジ:折り曲げ位置)が生成される
https://scrapbox.io/files/696a09327eba85b503e154b9.png
このターゲットを、 今回頭の子要素にする。頭が動いた場合でも、ターゲットが追従してくれるようにするため。
範囲内にターゲットがあったとき、振り向くようにする
近くにNPCがいたとき、向くような処理を実現する。まずPlayerのScriptを分割。
Rig, Head_Aim(Multi-Aim Constraint)とTargetの参照を持たせる。見つかったら取得というより、Weightを調整する形が好ましいだろう。
自身の周囲にPhysics.OvarlapSphereでコライダー取得を試みる
https://docs.unity3d.com/jp/2020.3/ScriptReference/Physics.OverlapSphere.html
https://docs.unity3d.com/jp/2018.4/ScriptReference/Gizmos.DrawWireSphere.html
code:c#
private void FindBestTarget()
{
// 指定範囲内、NPCレイヤーを持つコライダーを取得
Collider[] closeTargets = Physics.OverlapSphere(transform.position, searchRadius, npcLayer);
float minAngle = baseAngle; // 設定した視野角より狭い範囲で探す
Transform detectedTarget = null;
foreachで内積計算。
code:c#
foreach (var col in closeTargets)
{
Vector3 toTarget = (col.transform.position - transform.position).normalized;
float dot = Vector3.Dot(transform.forward, toTarget);
float angle = Mathf.Acos(dot) * Mathf.Rad2Deg;
if (angle < minAngle) // 視野角内に収まっているか
{
// TODO: 遮蔽物があるならチェックする
detectedTarget = col.transform;
minAngle = angle; // より正面に近い人を優先
}
}
currentTarget = detectedTarget;
toTarget = (col.transform.position - transform.position).normalized;
反応colliderの位置 - 自分の位置 で、角度だけのベクトルを取得
Unity_3d_practice#69647be800000000008e5045
float dot = Vector3.Dot(transform.forward, toTarget);
内積計算。単位ベクトル同士なので、純粋なcosθの値が返る。
float angle = Mathf.Acos(dot) * Mathf.Rad2Deg;
45°とか、そういった角度の単位に変換している。
Radianで返して、
https://docs.unity3d.com/ja/2020.1/ScriptReference/Mathf.Acos.html
ちなみに1ラジアンは57.3°。なので45°の場合は、0.78...ラジアンくらい
度に変換する
https://docs.unity3d.com/ja/2019.4/ScriptReference/Mathf.Rad2Deg.html
TODO
リグを使ったキャラの操作
Animation Rigging
どこにひっつけるとか、どこを見るようにするとかそういったものを、シーンを切り分けて作ってみよう
シェーダー
質量を決めるための計算式のこと。3dモデルに光が当たったとき、布のように反射させるのか、金属風にするのか、アニメ風にするのかなどを描画するのが役目。光の反射に関する表現
マテリアル
どういった色を使う、どういったテクスチャを張るなどをパラメータで指定するための素材
only upみたいなゲームを作ってみるのも勉強になりそう。移動床、テクスチャ、すり抜け床、3Dモデルの操作。
3Dとは関連無いが、実装してみたい内容
NPCとかのテキスト表示
セーブ, ロード処理