【Unity】AzureKinectの環境構築・接続
AzureKinectを使う機会があったので、セットアップとミニマムな疎通確認までをまとめておきたいと思います。
3月18日現在、日本での発売はまだされていません。しかし、マイクロソフトの公式ブログによると3月中には発売可能になる予定とのことです。
Azure Kinect Development Kit を日本、ドイツ、イギリスにて2020年3月より発売! - Windows Blog for Japan
環境
ハードウェア要件はこちらに記載されていました。
手順
1. Azure Kinect SDK をダウンロード
- https://docs.microsoft.com/ja-jp/azure/Kinect-dk/sensor-sdk-download
- Microsoft installer をクリックしてダウンロード
2. SDKのインストール
ダウンロードしたインストーラーを開いてインストール手順に従います
3. Unityプロジェクトを作成
Unity 2019.2.18f で作成しました。パイプラインはビルトインパイプラインにしました。
4. NuGet をUnityPackageからインストール
- ここから version 2.0 の UnityPackage を選択
- UnityPackage直リンクはこちら
5. Nuget UnityPackage を作成したプロジェクトにインストール
UnityPackageをドラッグ・ドロップでUnityにインストールします。
6. NuGetでSDKのモジュールをインストール
menuの
NuGet -> Manage Nuget Packages
を開くNuget のインストール直後だと menuにNuGetが表示されない場合があったので、そのときはUnityを再起動してみます
Microsoft.Azure.Kinect.Sensor
SDKをインストール- 検索バーで
AzureKinect
を入れると上の方に出てきます
- 検索バーで
7. dllのインポート
1でインストールしたAzureKinectSDKからdllを2つ(depthengine_2.0.dll, k4a.dll
)、unityのAssets/Plugins
にドラッグ&ドロップしてインポート
Assets 以下に Pluginsフォルダがなければ作成しておきます
dllのpathは、SDKのインストールのパスを特に変えていない場合はこちらになるはずです
C:\Program Files\Azure Kinect SDK v1.3.0\sdk\windows-desktop\amd64\release\bin
8. AzureKinectをPCにつなぐ
AzureKinectの使用には、ACアダプターと、PCとつなぐUSBの、2つを最低限つなぐ必要がなります。
複数台接続の際はデイジーチェーンでのタイムスタンプの同期のためオーディオケーブルをつなぐことになります。
9. 接続確認用のスクリプトを作成
10. Playした時の挙動確認
エラーなどなくログが表示されている(シリアル番号
とOpen
)かつ、LEDが赤く点灯することを確認します。
11. 止めたときの挙動確認
Close
というログが出るようになっています。
AzureKinectの接続を解除する際は、状態破棄のため Dispose 処理を忘れずに呼ぶ必要があるようです。
以上まで問題なく動作すれば、AzureKinectとUnityの接続はひとまず無事に上手くいっていることが分かります。
注意点
Playした際に missing assembly reference
のエラーが出ることがあります。
その場合は AzureKinect SDK が依存している?モジュールが入っていないためなので、 エラーの出ているモジュールを Nuget でインストールします。
【Unity】CSVを読み込むC#クラス
CSVファイルを読み込んでパースするクラスを、MonoBehaviourではなくC#クラスとして欲しい場面があったので作成してみました。
まず、コードの全文はこちらです。
Assets/Resources/
以下に入っているCSVファイルを読み込みます。
例えば Assets/Resources/test.csv
が
a, b, c, d e, f, g, h
というデータになっている場合、 CSVReader.getPath("test")
を呼ぶことで読み込むことができます。結果はこのようになります。
// 文字列の配列のリスト [ ["a", "b", "c", "d"], ["e", "f", "g", "h"] ]
string.Split 関数を呼ぶ時に System.StringSplitOptions.RemoveEmptyEntries
を指定することで空白の文字列を削除していますが、こちらは扱いたいデータに応じて変えたり、引数で分割方法を指定できるようにするなどの使用方法も有りうると思います。
【SparkAR】リムライト風シェーダー
SparkARは、簡単なシェーディングであればパッチエディターでノードを組み合わせることで実現できるのですが、それだけでは難しい表現の場合はJSでスクリプトを書く必要があります。
なぜスクリプトかと言うと、SparkARではシェーダーを直接書くことができず、SparkARが提供しているモジュールを通してシェーダーの用な計算をスクリプトで書いていく必要があるためです。
リムライト風シェーダーもその一つだったので、メモとして残しておきたいと思います。
プロジェクトはこちらに置きました。SparkARのバージョンはv83です。
ソースの抜粋はこちらになります。
フレネルの計算や考え方はシェーダーで書くときと同じなのですが、ライトの向きをスクリプトで取得することができなさそうだったので、法線ベクトルと視線ベクトルの内積を使う形にしています。
const Shaders = require('Shaders'); const Reactive = require('Reactive'); const Materials = require('Materials'); // vertexのattributeを取得 const localPosition = Shaders.vertexAttribute({ variableName: Shaders.VertexAttribute.POSITION }); const localNormal = Shaders.vertexAttribute({ variableName: Shaders.VertexAttribute.NORMAL }); // 座標変換用の行列を取得 const mvMatrix = Shaders.vertexTransform({ variableName: Shaders.BuiltinUniform.MV_MATRIX }); const normalMatrix = Shaders.vertexTransform({ variableName: Shaders.BuiltinUniform.NORMAL_MATRIX }); const normal = normalMatrix.mul(localNormal).normalize(); const position = Reactive.pack4( localPosition.x, localPosition.y, localPosition.z, 1. ); const mvPosition = mvMatrix.mul(position); const viewPosition = mvPosition.mul(-1); const viewDir = Reactive.normalize(Reactive.pack3(viewPosition.x, viewPosition.y, viewPosition.z)); const dotNV = Reactive.dot(viewDir, normal); const fresnel = Reactive.pow(Reactive.sub(1, Reactive.clamp(dotNV, 0, 1)), 2); const emissiveColor = Reactive.pack4( fresnel, fresnel, fresnel, 1. ); // Emissiveスロットに色を割り当てる const material = Materials.get('RimLight'); material.setTextureSlot(Shaders.DefaultMaterialTextures.EMISSIVE, emissiveColor);
【Javascript】ブラウザでシェーダーを使わずにレイマーチングをしてみる
はじめに
ブラウザでレイマーチングを行うには、WebGLを使ってシェーダーで書くのが一般的なやり方かと思います。むしろ、高いFPSを実現するにはそれ以外の方法は実質難しいはずです。具体的には、描画したい範囲に板ポリを貼りそのマテリアルにレイマーチングを行うシェーダーを割り当てる方法です。
ピクセルごとに複数回レイを飛ばしてシーンを構築するような処理そのものは、負荷は大きいですがGPUの得意とするところで、CPUで1ピクセルずつ順番に計算するよりも圧倒的に高速だからです。
ただ、処理速度は遅くなりますがCPUでは実現できないという訳ではありません。負荷はともかくとして、レイマーチングそのものはあくまでも手法で、処理自体はGPUでもCPUでも実現そのものは出来るはず・・・と思っていたのですが、どのぐらい負荷に差が出るのかまでは予測できなかったので、実際にやってみました。
作ったもの
比較のためにこの2つを作ってみました。見た目は同じです。球にディレクショナルライトが当たり、ライトの位置が回転するように動いています。
環境
CPU版
デモ
Typescriptで書いてみました。Context2Dを使っています。
ベクトルや行列周りの処理は、今回のデモで必要になる最低限の計算を自分で作ってみました。
See the Pen Canvas2D Raymarching by takumifukasawa (@takumifukasawa) on CodePen.
コード
HTML
<canvas id="canvas" width="200" height="200"></canvas>
canvas { display: block; }
const ITERATION_NUM: number = 16; const NORMAL_EPS: number = 0.0001; const STOP_THRESHOLD: number = 0.0001; //--------------------------------------------------------------- // utils //--------------------------------------------------------------- function clamp(x: number, a: number, b: number): number { return Math.min(Math.max(x, a), b); } //--------------------------------------------------------------- // vector //--------------------------------------------------------------- class Vector2 { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } } class Vector3 { x: number; y: number; z: number; constructor(x: number, y: number, z: number): Vector3 { this.x = x; this.y = y; this.z = z; return this; } get magnitude(): number { return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); } static magnitude(Vector3: v): number { return Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z); } get normalized(): Vector3 { const mag = this.magnitude; return new Vector3( this.x / mag, this.y / mag, this.z / mag ); } static normalize(v: Vector3): Vector3 { const mag = v.magnitude; return new Vector3( v.x / mag, v.y / mag, v.z / mag ); } normalize(): Vector3 { const mag = this.magnitude; return new Vector3( this.x / mag, this.y / mag, this.z / mag ); } static sub(a: Vector3, b: Vector3): Vector3 { return new Vector3( a.x - b.x, a.y - b.y, a.z - b.z ); } static dot(a: Vector3, b: Vector3): number { return a.x * b.x + a.y * b.y + a.z * b.z; } static cross(a: Vector3, b: Vector3): Vector3 { return new Vector3( a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x ); } static add(v1: Vector3, v2: Vector3): Vector3 { return new Vector3( v1.x + v2.x, v1.y + v2.y, v1.z + v2.z, ); } static mul(v: Vector3, m: number): Vector3 { return new Vector3( v.x * m, v.y * m, v.z * m ); } } //--------------------------------------------------------------- // mat3 //--------------------------------------------------------------- class Mat3 { elem: number[3][3]; constructor(row1: Vector3, row2: Vector3, row3: Vector3) { this.elem = [ [ row1.x, row1.y, row1.z ], [ row2.x, row2.y, row2.z ], [ row3.x, row3.y, row3.z ], ]; } mul(v3: Vector3): Vector3 { return new Vector3( this.elem[0][0] * v3.x + this.elem[0][1] * v3.y + this.elem[0][2] * v3.z, this.elem[1][0] * v3.x + this.elem[1][1] * v3.y + this.elem[1][2] * v3.z, this.elem[2][0] * v3.x + this.elem[2][1] * v3.y + this.elem[2][2] * v3.z ); } } //--------------------------------------------------------------- // color //--------------------------------------------------------------- class Color { r: number; g: number; b: number; a: number; constructor(r: number, g: number, b: number, a: number = 255) { this.r = r; this.g = g; this.b = b; this.a = a; } add(color: Color): void { this.r = clamp(this.r + color.r, 0, 255); this.g = clamp(this.g + color.g, 0, 255); this.b = clamp(this.b + color.b, 0, 255); this.a = clamp(this.a + color.a, 0, 255); } } //--------------------------------------------------------------- // raymarch block //--------------------------------------------------------------- function scene(p: Vector3): number { return p.magnitude - 1; } function getCamera(ro: Vector3, rd: Vector3): Mat3 { const dir = Vector3.sub(rd, ro); const forward: Vector3 = dir.normalized; const right: Vector3 = Vector3.cross(forward, new Vector3(0, 1, 0)); const up: Vector3 = Vector3.cross(right, forward); return new Mat3(right, up, forward); } function getNormal(p: Vector3): Vector3 { const e: Vector2 = new Vector2(NORMAL_EPS, 0); const n: Vector3 = new Vector3( scene(Vector3.add(p, new Vector3(e.x, e.y, e.y))) - scene(Vector3.sub(p, new Vector3(e.x, e.y, e.y))), scene(Vector3.add(p, new Vector3(e.y, e.x, e.y))) - scene(Vector3.sub(p, new Vector3(e.y, e.x, e.y))), scene(Vector3.add(p, new Vector3(e.y, e.y, e.x))) - scene(Vector3.sub(p, new Vector3(e.y, e.y, e.x))) ); return n.normalized; } function raymarch(u: number, v: number): Color { const time = performance.now() / 1000, // [s] const uv: Vector2 = new Vector2(u, v); const ro: Vector3 = new Vector3( Math.cos(time) * 5, 0, Math.sin(time) * 5 ); const target: Vector3 = new Vector3(0, 0, 0); const fov: number = 1.5; const camera: Mat3 = getCamera(ro, target); const rd: Vector3 = camera.mul(new Vector3(uv.x, uv.y, fov).normalized); let depth: number = 0; let dist: number = 0; // raymarch for(let i: number = 0; i < ITERATION_NUM; i++) { dist = scene(Vector3.add(ro, Vector3.mul(rd, depth))); if(dist < STOP_THRESHOLD) { break; } depth += dist; } // no hit if(dist >= STOP_THRESHOLD) { return new Color(0, 0, 0, 255); } // hit const color: Color = new Color(0, 0, 0, 255); const position: Vector3 = Vector3.add(ro, Vector3.mul(rd, depth)); const normal: Vector3 = getNormal(position); const lightPos: Vector3 = new Vector3(-1, 1, 1); const lambert: float = Math.max(0, Vector3.dot(normal, lightPos.normalized)); const surfaceColor: Vector3 = Vector3.mul(new Vector3(255, 255, 255), lambert); color.add(new Color( surfaceColor.x, surfaceColor.y, surfaceColor.z )); return color; } //--------------------------------------------------------------- // main block //--------------------------------------------------------------- const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const width: number = canvas.offsetWidth; const height: number = canvas.offsetHeight; const stats = new Stats(); stats.domElement.style.position = 'fixed'; stats.domElement.style.top = '0px'; stats.domElement.style.left = '0px'; document.body.appendChild(stats.domElement); function draw() { stats.begin(); const imageData = ctx.createImageData(width, height); const data = imageData.data; for(let y = 0; y < height; y++) { for(let x = 0; x < width; x++) { const w = x; // flip y const h = height - y; const i = (x + y * width) * 4; const coord: Vector2 = new Vector2(w, h); const resolution: Vector2 = new Vector2(width, height); const color: Color = raymarch( (coord.x - width * .5) / Math.min(width, height), (coord.y - height * .5) / Math.min(width, height) ); data[i ] = color.r; data[i + 1] = color.g; data[i + 2] = color.b; data[i + 3] = color.a; } } ctx.putImageData(imageData, 0, 0); stats.end(); requestAnimationFrame(draw); } requestAnimationFrame(draw);
GPU版
せっかくなのでシェーダーで動作するコードも書いてみました。
こちらはShaertoyでのデモになります。
デモ
コード
#define EPS .0001 #define NORMAL_EPS .0001 const float stopThreshold = .0001; float scene(vec3 p) { return length(p) - 1.; } mat3 camera(vec3 o, vec3 t) { vec3 forward = normalize(t - o); vec3 right = cross(forward, vec3(0., 1., 0.)); vec3 up = cross(right, forward); return mat3(right, up, forward); } vec3 getNormal(vec3 p) { vec2 e = vec2(NORMAL_EPS, 0); return normalize( vec3( scene(p + e.xyy) - scene(p - e.xyy), scene(p + e.yxy) - scene(p - e.yxy), scene(p + e.yyx) - scene(p - e.yyx) ) ); } void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = (fragCoord.xy - iResolution.xy * .5) / min(iResolution.x, iResolution.y); vec3 ro = vec3( cos(iTime) * 5., 0., sin(iTime) * 5. ); vec3 target = vec3(0.); float fov = 1.5; vec3 rd = camera(ro, target) * normalize(vec3(uv, fov)); // raymarching float depth = 0.; float dist = 0.; for(int i = 0; i < 64; i++) { dist = scene(ro + rd * depth); if(dist < stopThreshold) { break; } depth += dist; } // no hit if(dist >= stopThreshold) { fragColor = vec4(vec3(0.), 1.); return; } vec3 color = vec3(0.); vec3 position = ro + rd * depth; vec3 normal = getNormal(position); // directional lighting vec3 lightPos = vec3(-1., 1., 1.); float lambert = max(0., dot(normal, normalize(lightPos))); color += lambert * vec3(1.); fragColor = vec4(color, 1.); }
比較
CPU版をGPU版と同じ条件にするとFPSが1ぐらいしか出なかったので解像度などを下げました。
- CPU版
- レイを進める最大回数: 16回
- 解像度[px]: 200 x 200
- FPS: 20FPS前後
結果
やはりレイマーチングそのものはCPUでも実現はできましたが、処理速度はGPUには到底及ばないぐらいの差が開きました。
参考
【CSS】filterやcanvasを使わずにテキストにぼかし処理をかけたように見せる
動的にテキストにぼかし(ブラー)をかける場合、cssのfilterプロパティを使ったり、Context2DやWebGLで画像処理的にブラーをかける方法が考えられます。
今回は、それ以外の方法であたかもブラー処理を施しているかのように見せる手法を考えてみました。
text-shadowとcolorのみを操作します。
こちらがデモになります。
See the Pen Easy Text Blur by takumifukasawa (@takumifukasawa) on CodePen.
text-shadowは横のオフセット
,縦のオフセット
,ぼかしのサイズ
,色
の4つの値を指定することができます。
colorをtransparentにしてベタ塗りをなくし、text-shadowで縦横のオフセットを0にし中心を基準に影を広げることがポイントです。
p { font-size: 18px; line-height: 1.6em; letter-spacing: 0.07em; color: transparent; text-shadow: 0 0 6px rgba(0, 0, 0, 0.8); }
余談
text-shadowのブラー処理のロジックが気になったので調べてみました。
W3Cのドラフトにはこう書かれていました。
https://drafts.csswg.org/css-text-decor-3/#propdef-text-shadow
This property accepts a comma-separated list of shadow effects to be applied to the text of the element. Values are interpreted as for box-shadow [CSS-BACKGROUNDS-3]. (But note that spread values and the inset keyword are not allowed.)
つまり、text-shadowの要素はbox-shadowとして解釈されるようです。
続いてbox-shadowの項目を見てみます。
https://www.w3.org/TR/css-backgrounds-3/#propdef-box-shadow
box-shadowのblurについて説明しているshadow-blurの項目にはこう書かれていました。
https://www.w3.org/TR/css-backgrounds-3/#shadow-blur
Note this means for a long, straight shadow edge, the blur radius will create a visibly apparent color transition approximately the twice length of the blur radius that is perpendicular to and centered on the shadow’s edge, and that ranges from almost the full shadow color at the endpoint inside the shadow to almost fully transparent at the endpoint outside it.
ロジックを理解できたわけではないのですが、どうやらいわゆるカーネルを用いた画像処理的な方法ではなく、独自の複雑な処理を施しているのでしょうか。
もし詳しい方がいらしたらぜひご教授いただきたいです。
【UE4】LeapMotion と Niagara を連携させる
先日、LeapMotionを入手しました。LeapMotionはざっくり言うと手を検出するデバイスです。VR向けのコントローラーとして使われることもあるみたいです。
これまで LeapMotion を使ったことがなかったので、いろいろ遊んでみることにしました。UE4とNiagaraとの連携をやってみます。今回は、1秒ごとに手の形に応じてパーティクルを表示する機能を作ってみます。
UE4のNiagaraとLeapMotionを連携させてみた。手の形に応じてパーティクルを発生させる。#UE4 #leapmotion pic.twitter.com/E6mr6RdS9F
— takumifukasawa (@takumifukasawa) 2020年2月24日
サンプルはこちらにアップしました。
環境
- Windows10
- UE4.23.1
LeapMotionの導入
1. PC
ツールのインストール
まず、LeapMotionのsetupを行う必要があります。こちらからセットアップツールをダウンロード → 中に入っているセットアップ用のファイルを開き、インストールを進めます。
起動確認
インストールしたら、LeapMotionが問題なく起動するかを確認してみます。
インストールされたLeapControlPanel
をダブルクリックすると、LeapMotionのコントロールパネルが有効になります。
インストールの場所などを特にカスタマイズしなかった場合はおそらく C:\Program Files\Leap Motion\Core Services
に入っているはずです。
有効になると、タスクバーにこのようなアイコンが表示されます。右クリックしてビジュアライザー
を開くと、手を検出したボーンのビジュアライズアプリが立ち上がります。
これで、LeapMotionが無事にPCで扱える状態になりました。 余談ですが、ビジュアライズアプリのアイコンを見るどうやらUnityで作られているようですね。
2. UE4
LeapMotion のプラグインを有効化
UE4でLeapMotionを使う場合、Pluginsから有効にするだけでLeapMotionを使う準備ができます。有効にしたら再起動が必要です。
Niagara のプラグインを有効化
NiagaraはUE4の新しいVFXツールで、立ち位置的にはこれまでのCascadeパーティクルに変わるとされています。
Niagaraを使うには、 Niagara
と Niagara Extra
を有効化にし、こちらもエディターを再起動する必要があります。Niagaraは扱いとしてはまだβ版のようですね。
Niagaraの作業
任意のSkeletalMeshからパーティクルを発生させるアセットを作成していきます。両手から発生させたいので、パーティクルのアセットは1つにし、SkeletalMeshを差し替えることのできるような構造になっているのがよさそうです。Niagara の実装はこちらを参考にさせていただきました。
Niagara System, Niagara Emitter を作成
Niagaraアセットの構成はこちらの記事がとてもわかりやすく解説してくださっています。
今回は、Niagara System と Niagara Emitter の2つのアセットを使っていきます。それぞれ FX_HandParticleSystem
, FX_SurfaceParticle
と名付けました。
Niagara Emitter で SkeletalMesh からパーティクルを発生させる機能をつくり、Niagara System から Niagara Emitter の SkeletalMesh を指定できるように設定を変更していきます。
Niagara のアセットを作る際にテンプレートを選ぶことができるのですが、Niagara System の方はテンプレートなしで、Niagara Emitter は Simple Sprite Burst テンプレートを使用します。
↓ Niagara System
↓ Niagara Emitter
Niagara Emitter: SkeletalMesh からパーティクルを発生させる
先ほど作った Niagara Emitter は、モジュールを足したり消したりして以下のような構成にしました。重要なものをピックアップしていきたいと思います。
ポイントは Spawn Burst Instanteneous と Initialize Mesh Reproduction Sprite です。
Spawn Burst Instanteneous
でパーティクル発生時に一定個数のパーティクルを発生させます。今回は Spawn Count
を3000とし、Emitter Life Cycle
の NextLoopDuration
を 1としました。これで、1秒間隔で3000個発生させるパーティクルとなります。また、 Initialize Particle
の LifeTime
も1秒以内に収まるようにします。
いよいよメッシュの表面からパーティクルを発生させる部分に入ります。Initialize Mesh Reproduction Sprite
モジュールを追加すると、SkeletalMesh の表面からパーティクルを発生させることができます。ここに手のメッシュを指定するのですが、これは LeapMotion のプラグインを有効化すると、そのフォルダの中に入っています。
ここまでの作業で、手の形からパーティクルが発生されるようになりました。
Niagara System: SkeletalMesh の指定
つづいて Niagara System で SkeletalMesh を自由に変更できるようにしていきます。変更可能にしておくことにより NiagaraSystem 1つでいくつもの SkeletalMesh に対応させることができます。
始めに作った Niagara System を開き、Track から先ほどの Niagara Emitter を追加します。
User
の中にSkeletalMesh
型の変数を追加します。ここではUser.SkeletalMesh
と命名しました。次に、追加した変数を Initialize Mesh Reproduction Sprite
の Mesh
にドラッグします。すると、紫の鎖のようなマークとともに SkeletalMesh
から User.SkeletalMesh
に切り替わったかと思います。これで、発生源の SkeletalMesh を 変数から参照できるようになりました。
LeapMotion を扱う Actor を作成
ここでは、手の動きと Niagara を連携させる処理を作っていきます。具体的な処理は実際にダウンロードして中身を見ていただければと思います。この Actor はBP_LeapDesktopActor
と命名しました。
そもそも UE4 上で LeapMotion による手の動きと SkeletalMesh の連動は、手の SkeletalMesh に設定した Animation のインスタンスに対して 毎フレームごとに手の動きを更新する構造になっているようです。
Actor を作るにあたって、LeapMotion と手の動きを連携しつつ、手の表示もし、エフェクトと連携も行う Actor を Level 上に配置するだけで使えるようになることが再利用のしやすさ的にも理想なのですが、一つ問題があります。
それは、手のアニメーションと Niagara を連携させるためには、Level に配置した Niagara System の SkeletalMesh の Source に Actor を指定する 必要があること。具体的にはここです。
その理由から SkeletalMesh Actor を Level 上に配置することが必須となったため、このような中身にしました。大きなポイントは3つです。
- 手の動きを反映させる SkeletalMesh Component は Actor に追加したままで、非表示にしておく
- 代わりに、手を表示する SkeletalMesh Actor を Level 上で配置する
- 変数で SkeletalMesh を受け取るようにし、Blueprint内で その SkeletalMesh の Animation を更新する
これで、Level 上に SkeletalMesh Actor を置きつつ手の動きを Niagara に連携させることを実現しました。
Event Graph
アニメーション更新メソッド(右手)
Level に配置
LeapMotion を取り扱う Actor を配置し、子に Niagara System のアセットと SkeletalMesh を配置します。エフェクトと手の位置を合わせるため、position や rotation をゼロにしておきます。
注意点は親の Actor の rotation で、LeapMotion のデバイスの位置や向きに応じて回転を合わせます。
また、Niagara の Override Parameters
で手ごとの Default Mesh
と Source
を指定します。
完成
あとは好きなように色やサイズなどを調整します。
注意点
GPUパーティクルを有効にするとなぜかパーティクルが表示されなくなります。そのため現状CPUパーティクルのみ有効です。 他のSkeletalMeshでは問題なく動いたので、Niagaraの設定などを深堀りする必要がありそうです。こちらは調査中です。
参考
【UE4】LeapMotionを導入するとエディターが落ちやすくなるときの対策
環境
- Windows10
- UE4.23.1
起こったこと
UE4 で LeapMotion を使うには、プラグインを有効にし、LeapMotion Content / Examples
のLeapDesktopActor
をLevelに配置するだけで手の動きに連動したメッシュの表示を行うことができます。
しかし、Actorを配置したあとに何回か実行を繰り返すとエディターが落ちるようになりました。再起動しても現象は変わらずでした。
解決策
LeapDesktopActor の Event Graph を開くと、Event End Play イベントの Set Leap Event のModeがLEAP MODE VR
でした。
今回はデスクトップ向けで使っていたので、これをLeap Mode DESKTOP
にします。自分の場合はこうすると落ちなくなりました。
↓
備考
github の readme で Set Leap Event
の項目を見ると、Set basic global leap tracking options. Useful for switching tracking fidelity or desktop/vr tracking mode.
と書いてありました。つまりこのメソッドはトラッキングの設定を変えるための用途のようです。
トラッキングの設定がEvent BeginPlay
とEvent EndPlay
で違っていた理由は調べてもよくわからなかったので、正しい解決法かは分かりません。ただ、開発するプラットフォーム向けに揃えておいた方がよさそうだと思ったので、設定を変更しました。