仮想空間を自由に歩き回りたい~リダイレクテッド・ウォーキング~
皆さんこんにちは学部4年の森口です。
私ももう卒業でA-PxLのブログで記事を書くのもこれが最後になるのかなと思います。
学部1年の頃から活動してきて色々思い出のできた部だったと思いますし私のキャリア形成のきっかけを作ってくれた部でもあります。A-PxLのメンバーで良かったとほんとに思います。
さて、今日の本題ですがこれまで私が担当した記事を確認したところVR関連のものが殆どなかったのでVR関連の技術である「リダイレクテッド・ウォーキング」というものについて話していきたいと思います。
リダイレクテッド・ウォーキングとは
リダイレクテッド・ウォーキングとは被験者に対し現実と少し異なる映像を見せることで仮想的に物理空間を拡張する技術のことです。
ここで言う「現実と少し異なる映像」は被験者の移動量や回転量に対して操作を加えた映像のことで、例えば実空間での1メートルの移動に合わせて2メートル移動しているような映像を見せるといった感じです。
この映像は極端に現実と違うものではそのことを被験者が知覚できてしまいVR酔いなどに繋がります。知覚不可能なほどの変化を与えることで現実では限られたガーディアン内をウロウロしているだけなのにそれよりも遥かに広大なVR空間を移動可能にする技術です。
リダイレクテッド・ウォーキングにおける操作
リダイレクテッド・ウォーキングには3つの基本的な操作があります。並進移動量操作、回転量操作 曲率操作です。
物体の動きというのは並進移動と回転の2つに分けられるため、これらの拡張や縮小を行う操作に加え曲率操作というカーブした動きを直線的な動きに変換する操作も含んでいます。それぞれについて説明していきたいと思います。
並進移動量操作
並進移動量操作は実空間の並進移動量をもとに仮想空間内の並進移動量を拡張・縮小を行うことです。
一応操作なので縮小することも含まれると思いますがリダイレクテッド・ウォーキングの目的の一つにVRの体験空間の拡張があるのでこの操作では主に並進移動量を増幅することが多いと思います。
一番簡単に思いつく実装方法はHMDの位置を毎フレーム取得して移動方向のベクトルを何倍かした結果得られるポイントにHMDを無理やり移動させることでしょうか。
この際に使われる係数のようなものは並進ゲインと呼ばれます。
回転量操作
回転量操作は並進移動量操作同様に実空間の回転量をもとに仮想空間内の回転量を拡張・縮小します。
これも毎フレームHMDの向いてる向きを取得して実空間で回転した量を用いて操作を加えた分仮想空間のHMDを回転してあげるのが一番簡単ですかね。回転量操作では回転ゲインという数値を使います。
曲率操作
曲率操作は実空間における曲がった動きを直線的な動きへと変換する操作です。極端な話、適切な曲率操作を用いたコンテンツでは同じ空間を円形にぐるぐる回っているだけで仮想空間では永遠に直進することができます。
このときに用いられる曲率ゲインは歩いている経路の半径の逆数(半径をr とすると曲率ゲインは1/r)で表現されます。
この半径については約11.6mの半径では被験者が曲がっていることに気が付かなかったという研究結果もあるようです。
言葉だけでは伝わりづらいと思いますのでかんたんな図を載せておきます。(曲線が不格好なのはご容赦ください)
このような操作を駆使することでガーディアンの拡張を行うことができます。
リダイレクテッド・ウォーキングの例
リダイレクテッド・ウォーキングを用いたコンテンツはいくつか公開されているので紹介していきたいと思います。
Unlimited Corridor
こちらは東京大学の廣瀬・谷川・鳴海研究室とユニティ・テクノロジーズ・ジャパンが共同で研究・出展を行ったコンテンツでDCEXPO2016で展示されました。
このコンテンツのユーザーは湾曲した壁を触りながらVR空間を移動します。このとき曲率操作によりVR内の視点では直線状の壁伝いに歩行している映像が映されています。
このコンテンツでは曲率操作に加えて壁を触るというアクションから触覚を用いたアプローチでリダイレクテッド・ウォーキングを実現しています。
Change Blindness Redirection in Virtual Reality
こちらは南カルフォルニア大学のMxR Labが研究開発を行ったものでユーザーの位置に対してVR空間のオブジェクト配置を変更することでガーディアン内でおなじを動きをし続けることができます。
Change Blindness Redirection in Virtual Reality
やってみた
実はこのリダイレクテッド・ウォーキングは私の卒業論文のテーマなのです。
なので私もこの技術を使って一つコンテンツを作っています。
コンテンツは溶岩を海を移動するというコンテンツになっています。動画内のガーディアンの大きさは5m四方ですがVR空間内はおおよそ10m四方になっているので単純計算で4倍の大きさのスペースを移動できるようになています(道を用意しているのでその部分しか移動できませんが...)原理としては実空間の移動に合わせて地面を移動・回転させることで見かけ上の移動量(相対的な移動量)を変化させています。
この手法の問題点は現状事前に移動ルートを決めガーディアンから出ないようにさせる必要があります。そのため山道を探検したりだとか迷路のような一定のルートを進むようなコンテンツでは効力を発揮しますが昨今のRPGゲームみたいな自由に歩き回るようなコンテンツには向かないと思います。しかし映像からわかるようにUnlimitedCorriderのような機材を使わずOculusQuest一つでできるようにして、なるべく一般の人たちの自宅でのVR体験にスポットを当てて研究開発を行いました。
どこまで公開していい情報なのかわからないのでこのあたりで私の話は終わりますが個人的にVR空間の無限歩行には昔から興味があったので非常に研究していて面白いテーマでした。
まとめ
それでは今回の記事のまとめに入りたいと思います。
- リダイレクテッド・ウォーキングは人が認識できないほどの違いを含んだ映像をVR空間で見せることで仮想的に物理空間を拡張する技術である
- リダイレクテッド・ウォーキングには並進移動量操作、回転量操作、曲率操作の基本操作がある
とりあえずこの2つは覚えてもらえると嬉しいです。
VRはまだまだこれから発展していくホットな技術だと思うのでゲーム開発のようにインタラクティブな視点に目を向けるのも大事ですが、今回のようなどちらかというとアカデミックな視点に目を向けると違った面白さに出会えるんじゃないでしょうか。また、それらを前者のコンテンツに組み込んでも面白いと思います。
それでは私の最後の記事はこのへんで終わりにしたいと思います。
ご覧いただきありがとうございます。
参考記事など
シミュレーションゲームを作りたかった・・・
はじめに
こんにちは、学部4年の杉山です。
今年卒業になるのでこれが最後の私の記事となります。
もっとそれっぽい記事を書ければよかったのですが、卒論(記事に書けるほど内容が濃くない)もあり、思い付きネタをここで消化できればと思います。
本題
今回挑戦したのは、今、私界隈で最もアツいシミュレーションゲームというジャンルです、シミュゲーと言っても小ジャンルはさらにバラバラに分かれていて今回やるのはEquilinoxのような、箱庭のなかで生態系を作り上げるようなゲームです。
さっそく作っていきたいと思います。
制作
今回作るにあたって短期間での開発ということもあり、CivilizationシリーズのようなHexGridを利用して作っていこうと考えました。
まずはおなじみBlenderでグリッドを作ります。
Circleの頂点の数を減らして、solidifyモディファイアを適用するだけです。
これをUnity上に並べて
それぞれにパラメータを割り振っていきます。今回は植物、草食動物、肉食動物をそれぞれランダムに配置しました。グリッドは植物の量によってタイルの色を黒と緑の間で変化させます。草食動物は植物を食べ食糧が足りなくなると周りのマスに分散したり減ったりします。肉食動物は草食動物を食べ足りなくなれば減ります。実際に動いている動画がこちらです。
要素が少ないためか単純な変遷をたどっています。数値によっては獣が絶滅したり植物が消えたりするので値を調整するのが大変です。
まとめ
今回はなんとなく展望が見えるような見えないような中途半端な出来ではございますが、時間があれば要素を追加したり細かいバグを潰していったりしてより複雑な自分だけの箱庭を作っていきたいですね。
GPUインスタンシングとComputeShaderを使った簡単なサンプル
始めに
今回ブログを担当するなおしです。
GPUインスタンシング(DrawMeshInstancedIndirect)とComputeShaderを組み合わせた簡単なサンプルを今回作成していこうと思います。
これらを使うとたくさんのメッシュに動きを加えることが出来るのでとても楽しいです。
GPUインスタンシング
詳しくは解説しないので知りたいという方は以下の記事を参考にしてみてください。
ComputeShader
こちらもここでは解説しませんので、詳しく知りたい方はこちらの記事を参考にすると良いと思います。
今回作るもの
範囲を決めてその中を反射し続けるパーティクルを作成します。
プログラム
プログラムはC#プログラム、ComputeShader、Shaderの3つからなります。
C#プログラム
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using System.Runtime.InteropServices;
public class BallRenderer : MonoBehaviour
{
[Header("DrawMeshInstancedIndirectのパラメータ")]
[SerializeField]
private Mesh m_mesh;
[SerializeField]
private Material m_instanceMaterial;
[SerializeField]
private Bounds m_bounds;
[SerializeField]
private ShadowCastingMode m_shadowCastingMode;
[SerializeField]
private bool m_receiveShadows;
[Space(20)]
[SerializeField]
private int m_instanceCount;
[SerializeField]
private ComputeShader m_particleCalclator;
[Header("パーティクル設定")]
[Space(20)]
[SerializeField]
private float m_range;
[SerializeField]
private float m_scale;
[SerializeField]
private Color m_color;
[SerializeField]
private float m_particleVelocity;
private int m_calcParticlePositionKernel;
private Vector3Int m_calcParticlePositionGroupSize;
private ComputeBuffer m_argsBuffer;
private ComputeBuffer m_particleBuffer;
private ComputeBuffer m_particleVelocityBuffer;
private readonly int m_DeltaTimePropId = Shader.PropertyToID("_DeltaTime");
struct Particle {
public Vector3 position;
public Vector4 color;
public float scale;
}
// Start is called before the first frame update
void Start()
{
InitializeArgsBuffer();
InitializeParticleBuffer();
InitializeVelocityBuffer();
SetUpParticleCalclator();
}
void Update() {
m_particleCalclator.SetFloat(m_DeltaTimePropId, Time.deltaTime);
m_particleCalclator.Dispatch(m_calcParticlePositionKernel,
m_calcParticlePositionGroupSize.x,
m_calcParticlePositionGroupSize.y,
m_calcParticlePositionGroupSize.z);
}
// Update is called once per frame
void LateUpdate()
{
Graphics.DrawMeshInstancedIndirect(
m_mesh,
0,
m_instanceMaterial,
m_bounds,
m_argsBuffer,
0,
null,
m_shadowCastingMode,
m_receiveShadows
);
}
private void InitializeArgsBuffer() {
uint[] args = new uint[5] { 0, 0, 0, 0, 0 };
uint numIndices = (m_mesh != null) ? (uint) m_mesh.GetIndexCount(0) : 0;
args[0] = numIndices;
args[1] = (uint)m_instanceCount;
m_argsBuffer = new ComputeBuffer(1, args.Length * sizeof(uint), ComputeBufferType.IndirectArguments);
m_argsBuffer.SetData(args);
}
private void InitializeParticleBuffer() {
Particle[] particles = new Particle[m_instanceCount];
for(int i = 0; i < m_instanceCount; ++i) {
particles[i].position = RandomVector(-m_range, m_range);
particles[i].color = m_color;
particles[i].scale = m_scale;
}
m_particleBuffer = new ComputeBuffer(m_instanceCount, Marshal.SizeOf(typeof(Particle)));
m_particleBuffer.SetData(particles);
m_instanceMaterial.SetBuffer("_ParticleBuffer", m_particleBuffer);
}
private void InitializeVelocityBuffer() {
Vector3[] velocities = new Vector3[m_instanceCount];
for(int i = 0; i < m_instanceCount; ++i) {
// Random.onUnitySphere:半径1の球面上のランダムな点を返す
// つまり、大きさm_particleVelocityのランダムなベクトルを計算
velocities[i] = Random.onUnitSphere * m_particleVelocity;
}
m_particleVelocityBuffer = new ComputeBuffer(m_instanceCount, Marshal.SizeOf(typeof(Vector3)));
m_particleVelocityBuffer.SetData(velocities);
}
private void SetUpParticleCalclator() {
m_calcParticlePositionKernel = m_particleCalclator.FindKernel("CalcParticlePosition");
m_particleCalclator.GetKernelThreadGroupSizes(m_calcParticlePositionKernel,
out uint x,
out uint y,
out uint z);
m_calcParticlePositionGroupSize = new Vector3Int(m_instanceCount/(int)x, (int)y, (int)z);
m_particleCalclator.SetFloat("_Range", m_range);
m_particleCalclator.SetBuffer(m_calcParticlePositionKernel, "_Particle", m_particleBuffer);
m_particleCalclator.SetBuffer(m_calcParticlePositionKernel, "_ParticleVelocity", m_particleVelocityBuffer);
}
private Vector3 RandomVector(float min, float max) {
return new Vector3(
Random.Range(min, max),
Random.Range(min, max),
Random.Range(min, max)
);
}
// 領域の解放
private void OnDisable() {
m_particleBuffer?.Release();
m_particleVelocityBuffer?.Release();
m_argsBuffer?.Release();
}
}
ComputeShader
#pragma kernel CalcParticlePosition
struct Particle {
float3 position;
float4 color;
float scale;
};
RWStructuredBuffer<Particle> _Particle;
RWStructuredBuffer<float3> _ParticleVelocity;
float _Range;
float _DeltaTime;
[numthreads(64, 1, 1)]
void CalcParticlePosition(uint id : SV_DISPATCHTHREADID) {
float3 pos = _Particle[id].position + _ParticleVelocity[id] * _DeltaTime;
if(abs(pos.x) > _Range) {
_ParticleVelocity[id].x *= -1;
pos.x = _Particle[id].position.x + _ParticleVelocity[id].x * _DeltaTime;
}
if(abs(pos.y) > _Range) {
_ParticleVelocity[id].y *= -1;
pos.y = _Particle[id].position.y + _ParticleVelocity[id].y * _DeltaTime;
}
if(abs(pos.z) > _Range) {
_ParticleVelocity[id].z *= -1;
pos.z = _Particle[id].position.z + _ParticleVelocity[id].z * _DeltaTime;
}
_Particle[id].position = pos;
}
Shader
Shader "Unlit/Particle"
{
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct Particle
{
float3 position;
float4 color;
float scale;
};
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
float4 color : COLOR;
};
StructuredBuffer<Particle> _ParticleBuffer;
v2f vert (appdata v, uint instanceId : SV_InstanceID)
{
Particle p = _ParticleBuffer[instanceId];
v2f o;
float3 pos = (v.vertex.xyz * p.scale) + p.position;
o.vertex = mul(UNITY_MATRIX_VP, float4(pos, 1.0));
o.color = p.color;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return i.color;
}
ENDCG
}
}
}
簡単な解説
始めにStart関数内でDrawMeshInstancedIndirectで使用するComputeBuffer、パーティクル情報のComputeBuffer、パーティクルの速度ベクトルのComputeBuffer、ComputeShaderの初期化を行います。
ComputeShaderではパーティクル情報と速度ベクトルのComputeBufferが必要となるのでそれらの初期化が完了してから初期化を行います。
// Start is called before the first frame update void Start() { InitializeArgsBuffer(); InitializeParticleBuffer(); InitializeVelocityBuffer(); SetUpParticleCalclator(); }
Update関数では毎フレームのDeltaTimeをComputeShaderに設定してからそれぞれのパーティクルの座標をGPUで計算します。
void Update() { m_particleCalclator.SetFloat(m_DeltaTimePropId, Time.deltaTime); m_particleCalclator.Dispatch(m_calcParticlePositionKernel, m_calcParticlePositionGroupSize.x, m_calcParticlePositionGroupSize.y, m_calcParticlePositionGroupSize.z); }
パーティクルの位置の更新ですが、一度速度ベクトル*DeltaTimeを座標に足し合わせます。
そのとき、範囲外に出ようとしていた場合は速度速度ベクトルをマイナスにしてからもう一度計算を行っています。
[numthreads(64, 1, 1)] void CalcParticlePosition(uint id : SV_DISPATCHTHREADID) { float3 pos = _Particle[id].position + _ParticleVelocity[id] * _DeltaTime; if(abs(pos.x) > _Range) { _ParticleVelocity[id].x *= -1; pos.x = _Particle[id].position.x + _ParticleVelocity[id].x * _DeltaTime; } if(abs(pos.y) > _Range) { _ParticleVelocity[id].y *= -1; pos.y = _Particle[id].position.y + _ParticleVelocity[id].y * _DeltaTime; } if(abs(pos.z) > _Range) { _ParticleVelocity[id].z *= -1; pos.z = _Particle[id].position.z + _ParticleVelocity[id].z * _DeltaTime; } _Particle[id].position = pos; }
あとはLateUpdateでパーティクルの描画を行います。このサンプルではDrawMeshInstancedIndirectを使って描画を行います。
// Update is called once per frame void LateUpdate() { Graphics.DrawMeshInstancedIndirect( m_mesh, 0, m_instanceMaterial, m_bounds, m_argsBuffer, 0, null, m_shadowCastingMode, m_receiveShadows ); }
最後に
これらを使うことで沢山のオブジェクトを描画して様々な動きを付加することが出来ます。
まだ私も勉強中ですが、これからも何か面白いものが出来るか試していきたいです。
unityでよく使うC#のクラス、メソッド、などの話
はじめまして、学部一年の渡辺です。 初めてのブログということで、自分のような初心者の方に役立ちそうなテーマについて書こうと思います。 と、言ってもまだunityを触り始めて半年弱(モデリングにも挑戦したりしていたので実質3か月くらい)なので、間違いがあるかもしれません。その場合、指摘してくださるとうれしいです。(のちのち画像等追加します)
よく使うもの
はじめに
私は大学でC、サークルでC#を同時にやったため、オブジェクト指向型についての理解をちゃんとせずに勉強してしまったため、かなり学ぶ上で混乱が生じました。 そのためまずオブジェクト指向型の理解をすべきだと思います。
クラス
Input
まずゲームといえばプレイヤーをコントロールしなければなりません。 コントローラーなりキーボードなりの入力を受け付ける。
メソッド | 説明 |
---|---|
Input.GetKey | 押している間 |
Input.GetKeyDown | 押した瞬間 |
Input.GetKeyUp | 離した瞬間 |
Input.GetMouseButton(0) | 左ボタン |
Input.GetMouseButton(1) | 右ボタン |
Input.GetMouseButton(2) | 中ボタン |
使用例
if (Input.GetKey(KeyCode.W))
Input.GetAxis() float型で返す
使用例
void Update() { float h = horizontalSpeed * Input.GetAxis("Mouse X"); float v = verticalSpeed * Input.GetAxis("Mouse Y"); transform.Rotate(v, h, 0); }
Edit -> Project Settings -> InputでInputManagerが見れます。
GameObject
ゲーム内のオブジェクトを扱うクラス
変数 | 説明 |
---|---|
transform | ゲームオブジェクトの位置 |
layer | ゲームオブジェクトのレイヤー |
tag | ゲームオブジェクトのタグ |
name | 名前 |
Transform
オブジェクトの位置、回転、スケールを扱うクラス
変数 | 説明 |
---|---|
position | ワールド座標における位置(Vector3) |
rotation | ワールド座標における方向(Quaternion) |
localScale | 大きさ |
forward | 物体が向いている方向 |
transform.LookAt(target((Transform target)));
targetに向かせる
SceneManager
シーンの切り替え等に利用できます。 タイトル画面やリザルト画面を作るときに必要になるかと。
使用する際は
using UnityEngine.SceneManagement
と宣言する必要があります。またFile->BuildSettingsでシーンの登録をする必要があります。
使用例
SceneManager.LoadScene("シーン名");
Rayの使い方
Rayは光線という意味ですが、unityでもよく使います。 ある点から光線を出し、当たったオブジェクトの情報を取得できます。 いろいろとややこしいですが、覚えるとかなり便利かと思います。
Ray
Rayの宣言
Ray(origin(原点), direction(方向))
使用例
Ray ray = new Ray(transform.position, transform.forward);
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit
Rayの当たったものの情報を得る。
変数 | 説明 |
---|---|
transform | 衝突したコライダーまたは Rigidbody の Transform |
collider | ヒットしたコライダー |
distance | レイの原点から衝突点までの距離 |
Physics.Raycast
Physics.Raycastには二つ方法があります。
- Physics.Raycast(原点, 方向, out hit*1, Rayの長さ, 当たるレイヤー);
- Physics.Raycast(ray*2, out hit, Rayの長さ, 当たるレイヤー);
Rayの長さは指定しない場合無限になります。 レイヤーは指定しない場合すべてのレイヤーが対象です。
当たり判定
OnCollisionEnter関数
衝突したかどうかを判断する関数です。
void OnCollisionEnter(Collision 当たった物体) { Debug.Log(当たった物体.gameObject.name); }
OnTriggerEnter関数
物体のColliderのIsTriggerにチェックを入れた場合、衝突しなくなりすり抜けます。 その場合の当たり判定はOnTriggerEnter関数を使います。
void OnTriggerEnter(Collider 入った物体) { Debug.Log(入った物体.gameObject.name); }
最後に
今までコピペしてそれっぽいスクリプトを書いていましたが、今後は自力で書けるようになりたいと思い、この記事を書かせていただきました(というかほぼメモ)。同じような方のお力になれていたら幸いです。 学んだことがあったらこれからも追加していきたいと思います。
参考にさせていただいたサイト
開発者視点のVR酔い対策
学部2年の星野です。自分が大体のもので酔いやすいこともあり、VRコンテンツを制作する際にVR酔いについてはかなり注意しているつもりです。今回は開発者視点におけるVR酔いの対策について書いていきたいと思います。
VR酔いとは
VRを体験している途中や後で、吐き気や頭痛などの症状がでること。
症状としてはほぼ車酔いと同じ
原因としてはいくつか説があるが、視覚と感覚のずれによるものという説が有力(車酔いと同じ)
自分自身も何回かなったことがありますが、その時はかなりキツかったです。(もちろん状況や人によって症状は変わります。)
VR酔いの対策
fpsとリフレッシュシートを確保する
fpsはアプリ側、リフレッシュシートはヘッドセット側の映像更新の頻度を表す
ちゃんと確保しないと映像が遅延するため酔いやすくなる
ときには解析度を犠牲にしてでもfpsを確保
VRを違和感なく進行するためには90fps以上必要
次の動きを予想させる
- 視点や画面が大きく切り替わる前に予告を出したりフェードインやフェードアウトを使うことで、脳に準備をさせる
移動の実装には注意する
テレポート
コックピットなどの表示(周辺視野を隠す)
移動させない(ルームスケールの範囲内)
実践
自分の参加しているチーム開発のカメラや移動のシステムを担当しました。 下の動画は現在の進捗です。
次の動きを予想させる→フェードイン、アウトを使う。 移動の実装→テレポート
そのおかげか、気持ち悪さや酔いを感じたことはなかったです。(慣れもあるかもしれません)
まとめ
今後もVRコンテンツを制作する時には上であげたことに注意していきたいと思います。