Blenderでキャラクターモデリング
学部1年の石本です。
Blenderを使ってモデリングをしてみました。
必要なもの
- blender
- 作りたいキャラの画像
制作過程で思ったこと
参考動画
必要なもの
blender
まずはblenderをダウンロードします。
【Blender2.8入門】ダウンロード方法/基本操作/モデリングを始める①
作りたいキャラの画像
作りたいキャラの
- 正面顔
- 横顔
- 正面全身
の画像を用意します。
これらをblenderで読み込んでキャラの姿形を作っていきます。
真顔、棒立ちの画像があれば作りやすいです。
制作過程で思ったこと
- 動画の通りに作ったほうがいい!
手順一個少なくてもいい感じの形になるからいいや、って手順省くと後でつじつまが合わなくなります。
初めてモデリングするときは、動画の通りに作ったほうがいいです。
- 男性キャラは胸もまつげも必要ないし、体の凹凸も少なくしたほうがいい
参考にした動画は女性キャラクターを作っていましたが、私が作りたいキャラクターは男性だったので、男性の体形になるようにいい感じに変えました。
- 完璧を求めすぎない
ずっと見てると、自分の作っているモデルの形が不格好に見えてきます。
初めてのモデリングだからと割り切って、先を進めたほうがいいです。
私も1か月くらい放置していたので、まだ完成できていません。
- 参考動画
【Blender 2.91 Tutorial】Low Poly キャラクターモデリング解説 intro - Character Modeling
この動画では、Gameエンジン, VRChat, MMDなどを想定したLowPolyのキャラクターモデリング方法が解説されています。
この動画の通りにすると、出来ます。
私も、この動画を片手に、今、顔、体、手、眉毛まで作りました。
初心者でも、いい感じに作れています。
キャラクターモデリングに興味のある人は、参考にしてみてください。
Arduinoと加速度センサーを使って自作コントローラーを作りたい
学部二年の土鍋です。
最近、ハードウェアに興味があるのでArduinoとUnityを利用して遊べないかなと思い、今回自作コントローラーを作ってみました。
準備
作りたいもの
「VR刀バトルゲーム」
Arduinoと9軸センサーを用いて制作した刀型コントローラーを振るとUnity内の刀も連動して動く。
ぶっちゃけ、既存のVRコントローラーでできるし、Wiiのコントローラーも似たような感じ…
でも自分で作ることに意味がある!
使用したもの
- Arduino UNO
- Fabo #501、#202
- Unity 2020
Arduinoとは?
知ってる人も多そうですが…
イタリアで生まれたオープンソースのマイコンで、初心者でもプログラムを書いてかんたんにハードウェアで遊べます。
Faboとは?
会津大学のOBが起業した会社、及び製品。
簡単にセンサー等をセットアップでき、サンプルコードなどが充実しているため、初心者でもすぐに様々なセンサーを扱えます。
製作過程
- センサーで加速度とジャイロ(角加速度)を取得
- 値をシリアル通信でArduinoからUnityへ
- 取得した値を利用して物体を移動させたり回転させたりさせる
センサーの値を取得
9軸センサー(Fabo#202)は加速度、ジャイロ、コンパスの情報を獲得できます。
今回はFaboが提供するライブラリを使用し、コードはサンプルコードをもとに書きました。
取得した値↓
ArduinoとUnityをシリアル通信する
取得した加速度、ジャイロの値をシリアル通信で受け取ります。 受け取った値を物体にローカル座標(センサ座標)に加速度を加えるメソッドと角加速度を加えるメソッド(RigidBodyのAddRelactiveForceとAddRelactiveTorque)に入れます。
取得した値を利用して物体を移動させたり回転させたりさせる
ここで問題発生
取得した値には重力の加速度も含まれるため、単純に値を使うことが出来ません。
うーん重力うざいですね…
地球から重力が無くなればいいのに...
そうもいかないので
重力を打ち消したい
単純にZ軸の加速度から重力を引くと傾けたときに値がおかしくなります。
つまり、センサーの座標軸ではなく重力に関してはワールド座標で考える必要があります。
ではワールド座標Z軸正の向きに重力と同じ加速度を与えてみましょう。
ただUnityではY軸です(ややこしい)
動いた!!!
しかし、なぜかめちゃくちゃ重かった。
苦労したこと
- シリアル通信を行うととんでもなくUnityが重くなった
どうやらReadTo()の部分が原因のよう (結局改善出来なかったので、どなたか教えて下さい…) - 重力を考慮すること
- センサ座標とワールド座標を考慮すること
- もともと三軸でどうにか実装しようとしていたがうまく行かなかった
まとめと今後の目標
そんな重い処理じゃないはずなのになぜか重すぎるのでどうにか修正したいと思います。その影響か挙動のずれが大きいのでそこも修正が必要。 また、物理と数学はゲーム作る上で重要だと改めて感じました。
VRの中でも五感再現等に興味があるので、刀で切られた感覚を再現できたら面白いですよね(ただの妄想)
今後、もっと勉強を進めてIVRC等に挑戦出来たらなと考えています。
ご覧いただきありがとうございました。
参考資料
202 9Axis I2C Brick - FaBo Arduino Docs
【Unity/Arduino】UnityとArduinoの加速度センサを連携してみた - ソースに絡まるエスカルゴ
【Unity】 Rigidbodyの回転方法 - エフアンダーバー
Rigidbody-AddRelativeForce - Unity スクリプトリファレンス
仮想空間を自由に歩き回りたい~リダイレクテッド・ウォーキング~
皆さんこんにちは学部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 ); }
最後に
これらを使うことで沢山のオブジェクトを描画して様々な動きを付加することが出来ます。
まだ私も勉強中ですが、これからも何か面白いものが出来るか試していきたいです。