Aizu-Progressive xr Lab blog

会津大学のVR部であるA-PxLの部員が持ち回りで投稿していくブログです。部員がそれぞれVRに関する出来事やVRにちなんだことについて学んだことを書いていきます。

「面白法人カヤックVR分室」としても活動しています。詳細はこちら

GPUインスタンシングとComputeShaderを使った簡単なサンプル

始めに

今回ブログを担当するなおしです。

GPUインスタンシング(DrawMeshInstancedIndirect)とComputeShaderを組み合わせた簡単なサンプルを今回作成していこうと思います。

これらを使うとたくさんのメッシュに動きを加えることが出来るのでとても楽しいです。



GPUインスタンシング

詳しくは解説しないので知りたいという方は以下の記事を参考にしてみてください。

shitakami.hatenablog.com

qiita.com

gottaniprogramming.seesaa.net



ComputeShader

こちらもここでは解説しませんので、詳しく知りたい方はこちらの記事を参考にすると良いと思います。

neareal.com

neareal.com



今回作るもの

範囲を決めてその中を反射し続けるパーティクルを作成します。

f:id:aizu-vr:20210104152157g:plain



プログラム

プログラムは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か月くらい)なので、間違いがあるかもしれません。その場合、指摘してくださるとうれしいです。(のちのち画像等追加します)

よく使うもの


はじめに

私もついこないだまで理解していなかったことが構造体とクラスの違いです。Transformクラスの中にVector3という構造体があり、それがx,y,zなどの情報を保持しています。

クラス


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には二つ方法があります。

  1. Physics.Raycast(原点, 方向, out hit*1, Rayの長さ, 当たるレイヤー);
  2. 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);
}

最後に


今までコピペしてそれっぽいスクリプトを書いていましたが、今後は自力で書けるようになりたいと思い、この記事を書かせていただきました(というかほぼメモ)。同じような方のお力になれていたら幸いです。 学んだことがあったらこれからも追加していきたいと思います。

参考にさせていただいたサイト


*1:RaycastHit hit

*2:Ray ray

Live2dモデルをVRで動かしてみた

こんにちは、学部2年の髙橋です。 今回はLive2dに関するお話です。
Live2dモデルが活用されている場としてよく見るのはゲームやVtuberといったコンテンツですが、VRでも同様にlive2dを利用することができるか気になったので試してみました。

使ったモデル

以前自作したモデルがあったのでそのまま使います。

f:id:aizu-vr:20201115164716p:plain

Unityでインポートする

ここ からCubism SDK for Unity をダウンロードしてUnityにインポート。 次に組み込み用ファイルとして書き出したLive2dモデルをUnityにインポートすると、モデルのプレハブができます。
シーンに設置した際は一度実行しないとモデルが表示されないことに注意。

細かい部分は以下の公式チュートリアルを見た方がわかりやすいと思います。

SDKをインポート | Live2D Manuals & Tutorials

ちなみにこのときよくモデルの描画順がおかしくなるのですが、モデルプレハブのコンポーネントにある Cubism Render Controller > Sorting > Mode を Back To Front Orderにすることで解決します。

とりあえずVRにする

Oculus IntegrationとOculus LinkでつないだOculus Questを使用しました。

f:id:aizu-vr:20201115172839j:plain

実際にHMDから見た画像。 VRで見る限りは平面ですがキャプチャするとわからなくなりますね。

動かす

HMDやコントローラーから頭等の動きが取れるので、Live2dに反映してみました。

頭部の動き

【初心者向け】UnityとLive2Dで拡張しやすいVTuber配信システムを作る方法 - Qiita

上の記事を参考に実装しました。

以下のようなスクリプトをOVRCameraRig以下のCenterEyeAnchorにアタッチして、インスペクターからモデルのパラメータを設定すると動きます。

public class RotateHead : MonoBehaviour
{
    private Quaternion trackingDir;
    [SerializeField] private bool mirroring = false;
    void LateUpdate()
    {
        trackingDir = this.gameObject.GetComponent<Transform>().rotation;
        UpdateHeadRot(trackingDir);
    }

    [SerializeField] CubismParameter headRotX = null, headRotY = null, headRotZ = null;
    private float intensity = 100;
    void UpdateHeadRot(Quaternion angle)
    {
        int mirror = mirroring == true ? 1: -1;
        setParameter(headRotX, angle.y*intensity*mirror);
        setParameter(headRotY, angle.x*intensity*-1);
        setParameter(headRotZ, angle.z*intensity*mirror*-1);
    }
    void setParameter(CubismParameter parameter, float angle)
    {
        if(parameter != null)
        {
            parameter.Value = Mathf.Clamp(angle, parameter.MinimumValue, parameter.MaximumValue);
        }
    }
}

Live2Dの一般的なパラメーターの設定では、xの回転で頭が左右に回り、yの回転で上下、zで左右に振れます。 上のスクリプトではUnityのrotationの値をもとに回転させているのですが、unityの x, y, zは回転軸を指しているのでずれがあります。

また回転のスタート地点が最初の正面に固定されています。

口パク

せっかくなので喋った音声に合わせて口が動くようにしました。

github.com

OVRLipSyncをもとに自力で実装しなきゃだめかなーと思ったら似たようなことをしている先人がいたので、ありがたく使わせていただきました。

結果

実際にやってみた。※声は抜けてます。

youtu.be

瞬きや呼吸はLive2d SDKに付属しているコンポーネントで設定してあります。

自動まばたきの設定 | Live2D Manuals & Tutorials

パラメータを周期的に動作させる方法 | Live2D Manuals & Tutorials

思ったよりそれっぽく動きました。

まとめ

  • あくまで平面なのでVRのコンテンツとして落とし込むのは特殊な状況に限りそう。

  • WebカメラもFacerigも使わずLive2dを動かせたので配信用途には使えるかもしれない。

今回は軽くVR上に持ってきたLive2dモデルを触るとこまでやったので、もう少し何か形になるとこまで触っていきたいです。

開発者視点のVR酔い対策

f:id:aizu-vr:20201026124223p:plain 学部2年の星野です。自分が大体のもので酔いやすいこともあり、VRコンテンツを制作する際にVR酔いについてはかなり注意しているつもりです。今回は開発者視点におけるVR酔いの対策について書いていきたいと思います。

VR酔いとは

  • VRを体験している途中や後で、吐き気や頭痛などの症状がでること。

  • 症状としてはほぼ車酔いと同じ

  • 原因としてはいくつか説があるが、視覚と感覚のずれによるものという説が有力(車酔いと同じ)

自分自身も何回かなったことがありますが、その時はかなりキツかったです。(もちろん状況や人によって症状は変わります。)

VR酔いの対策

fpsとリフレッシュシートを確保する
  • fpsはアプリ側、リフレッシュシートはヘッドセット側の映像更新の頻度を表す

  • ちゃんと確保しないと映像が遅延するため酔いやすくなる

  • ときには解析度を犠牲にしてでもfpsを確保

  • VRを違和感なく進行するためには90fps以上必要

次の動きを予想させる
  • 視点や画面が大きく切り替わる前に予告を出したりフェードインやフェードアウトを使うことで、脳に準備をさせる
移動の実装には注意する
  • テレポート

  • コックピットなどの表示(周辺視野を隠す)

  • 移動させない(ルームスケールの範囲内)

実践

自分の参加しているチーム開発のカメラや移動のシステムを担当しました。 下の動画は現在の進捗です。

youtu.be

次の動きを予想させる→フェードイン、アウトを使う。 移動の実装→テレポート

そのおかげか、気持ち悪さや酔いを感じたことはなかったです。(慣れもあるかもしれません)

まとめ

今後もVRコンテンツを制作する時には上であげたことに注意していきたいと思います。

【Odin Inspector】NPCの振る舞いをインスペクターで設定する

はじめに

こんにちは、学部3年の木村です。

タイトルの通り、ゲームに登場するNPCの行動をインスペクターで設定できるようにします。

想定するゲームにはNPCとしてMonster(獅子舞や仏像)が存在し、 各NPCは現在の状態に応じて探索や追跡などの行動をとることができます。
この場合、Dictionaryを使って状態と行動を対応付けたいのですがUnityのInspectorではGenericsを扱えないため、アクセス修飾子をpublicにしたり属性にSerializeFieledを付けたりしてもInspectorには表示されません。抽象クラスやインターフェースも同様です。

そこで、Odin - Inspector and Serializerというアセットを使用しました。

Odinとは

OdinはUnityのInspectorを使いやすくするプラグインです。

導入するだけでList等の表示がいい感じになったり、 (多分)何でもシリアライズできるようになったり、便利な属性が80以上使えるようになります。odininspector.comではどんなことができるようになるかがアニメーション付きで紹介されています。

$55の有料アセットですが、ゲーム制作中のパラメータ調整などが圧倒的に快適になるとても便利なアセットです。

本編

一部省略していますが、 大体以下のような構成です。

Monsterが継承しているSerializedMonoBehaviourは、Dictionary等もシリアライズできる(Inspectorで設定できる)MonoBehaviourです。元のMonoBehaviourの機能はそのまま使えます。

Inspectorでは次のように表示されます。(クラス図ではMonsterに定義しているActionsTableMonsterActionRepositoryで定義されています。)

DictionaryがInspectorに表示されており、Keyに状態(MonsterState)、Valueにその状態のときにとる行動(抽象クラスNpcActionの実装)を指定することでNPCの振る舞いを設定できます。

ついでに

OdinのButton属性を使って、InspectorのボタンからMonsterの状態を変更できるようにしています。

f:id:aizu-vr:20200921050826g:plain

public bool _enableDebug;

[Button, ShowIf(nameof(_enableDebug)), EnableIf(nameof(_enableDebug))]
private void ChangeState(MonsterState state)
{
    // 状態をstateに変更
}

終わり

Odinの紹介と活用例でした。
ゲーム制作中にUnityのインスペクターを使っていると微妙に手の届かないかゆいところがあり、自分でエディタ拡張を書いていたら沼にハマり肝心のゲーム制作が進まないということがあるかもしれません。そんなときにOdinはかゆいところをいい感じにかいてくれます。
ちょっと高めのアセットですがUnityがとても快適になるので、よかったら導入してみてはいかがでしょうか。

会津大学VR部の部員が持ち回りで投稿していくブログです。特にテーマに縛りを設けずに書いていきます!