takumifukasawa’s blog

WebGL, Shader, Unity, UE4

【Unity】AzureKinectの環境構築・接続

AzureKinectを使う機会があったので、セットアップとミニマムな疎通確認までをまとめておきたいと思います。

azure.microsoft.com

3月18日現在、日本での発売はまだされていません。しかし、マイクロソフトの公式ブログによると3月中には発売可能になる予定とのことです。

Azure Kinect Development Kit を日本、ドイツ、イギリスにて2020年3月より発売! - Windows Blog for Japan

環境

  • Windows10
  • azure kinect sdk version 1.3.0
  • Unity 2019.2.18f

ハードウェア要件はこちらに記載されていました。

docs.microsoft.com

手順

1. Azure Kinect SDK をダウンロード

2. SDKのインストール

ダウンロードしたインストーラーを開いてインストール手順に従います

3. Unityプロジェクトを作成

Unity 2019.2.18f で作成しました。パイプラインはビルトインパイプラインにしました。

4. NuGet をUnityPackageからインストール

5. Nuget UnityPackage を作成したプロジェクトにインストール

UnityPackageをドラッグ・ドロップでUnityにインストールします。

6. NuGetでSDKのモジュールをインストール

  • menuの NuGet -> Manage Nuget Packages を開く

  • Nuget のインストール直後だと menuにNuGetが表示されない場合があったので、そのときはUnityを再起動してみます

  • Microsoft.Azure.Kinect.Sensor SDKをインストール

    • 検索バーで AzureKinect を入れると上の方に出てきます

f:id:takumifukasawa:20200310005344p:plain

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

f:id:takumifukasawa:20200310005416p:plain

f:id:takumifukasawa:20200310005435p:plain

8. AzureKinectをPCにつなぐ

AzureKinectの使用には、ACアダプターと、PCとつなぐUSBの、2つを最低限つなぐ必要がなります。

複数台接続の際はデイジーチェーンでのタイムスタンプの同期のためオーディオケーブルをつなぐことになります。

9. 接続確認用のスクリプトを作成

  • Playするとkinectを接続し、Playを止めるとkinectの接続を解除するスクリプトです

  • こちらを任意の名前で作成しGameObjectにアタッチします

gist.github.com

10. Playした時の挙動確認

エラーなどなくログが表示されている(シリアル番号Open)かつ、LEDが赤く点灯することを確認します。

11. 止めたときの挙動確認

Closeというログが出るようになっています。

AzureKinectの接続を解除する際は、状態破棄のため Dispose 処理を忘れずに呼ぶ必要があるようです。


以上まで問題なく動作すれば、AzureKinectとUnityの接続はひとまず無事に上手くいっていることが分かります。

注意点

Playした際に missing assembly reference のエラーが出ることがあります。

その場合は AzureKinect SDK が依存している?モジュールが入っていないためなので、 エラーの出ているモジュールを Nuget でインストールします。

【Unity】CSVを読み込むC#クラス

CSVファイルを読み込んでパースするクラスを、MonoBehaviourではなくC#クラスとして欲しい場面があったので作成してみました。

まず、コードの全文はこちらです。

gist.github.com

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が提供しているモジュールを通してシェーダーの用な計算をスクリプトで書いていく必要があるためです。

リムライト風シェーダーもその一つだったので、メモとして残しておきたいと思います。

f:id:takumifukasawa:20200312211030p:plain

プロジェクトはこちらに置きました。SparkARのバージョンはv83です。

github.com

ソースの抜粋はこちらになります。

フレネルの計算や考え方はシェーダーで書くときと同じなのですが、ライトの向きをスクリプトで取得することができなさそうだったので、法線ベクトルと視線ベクトルの内積を使う形にしています。

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でも実現そのものは出来るはず・・・と思っていたのですが、どのぐらい負荷に差が出るのかまでは予測できなかったので、実際にやってみました。

作ったもの

  1. CPU版: CanvasのContext2Dでレイマーチングしたデモ
  2. GPU版: シェーダーでレイマーチングしたデモ

比較のためにこの2つを作ってみました。見た目は同じです。球にディレクショナルライトが当たり、ライトの位置が回転するように動いています。

f:id:takumifukasawa:20200304003623p:plain

環境

  • Mac 15-inch
  • プロセッサ: 3.1Ghz Intel Core i7
  • グラフィックス: Intel HD Graphics 630 1536MB

CPU版

デモ

Typescriptで書いてみました。Context2Dを使っています。

ベクトルや行列周りの処理は、今回のデモで必要になる最低限の計算を自分で作ってみました。

See the Pen Canvas2D Raymarching by takumifukasawa (@takumifukasawa) on CodePen.

コード

HTML

<canvas id="canvas" width="200" height="200"></canvas>

CSS

canvas {
  display: block;
}

Javascript

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でのデモになります。

デモ

www.shadertoy.com

コード

#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前後

f:id:takumifukasawa:20200305000449p:plain

  • GPU
    • レイを進める最大回数: 64回
    • 解像度[px]: 640 x 360
    • FPS: ほぼ60FPS

f:id:takumifukasawa:20200305000505p:plain

結果

やはりレイマーチングそのものはCPUでも実現はできましたが、処理速度はGPUには到底及ばないぐらいの差が開きました。

参考

www.iquilezles.org

【CSS】filterやcanvasを使わずにテキストにぼかし処理をかけたように見せる

動的にテキストにぼかし(ブラー)をかける場合、cssのfilterプロパティを使ったり、Context2DやWebGLで画像処理的にブラーをかける方法が考えられます。

developer.mozilla.org

今回は、それ以外の方法であたかもブラー処理を施しているかのように見せる手法を考えてみました。

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);
}

developer.mozilla.org

余談

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向けのコントローラーとして使われることもあるみたいです。

developer.leapmotion.com

これまで LeapMotion を使ったことがなかったので、いろいろ遊んでみることにしました。UE4Niagaraとの連携をやってみます。今回は、1秒ごとに手の形に応じてパーティクルを表示する機能を作ってみます。

f:id:takumifukasawa:20200224161821g:plain

サンプルはこちらにアップしました。

github.com

環境

  • Windows10
  • UE4.23.1

LeapMotionの導入

1. PC

ツールのインストール

まず、LeapMotionのsetupを行う必要があります。こちらからセットアップツールをダウンロード → 中に入っているセットアップ用のファイルを開き、インストールを進めます。

developer.leapmotion.com

f:id:takumifukasawa:20200224164520p:plain

起動確認

インストールしたら、LeapMotionが問題なく起動するかを確認してみます。

インストールされたLeapControlPanelをダブルクリックすると、LeapMotionのコントロールパネルが有効になります。 インストールの場所などを特にカスタマイズしなかった場合はおそらく C:\Program Files\Leap Motion\Core Services に入っているはずです。

f:id:takumifukasawa:20200224164547p:plain

有効になると、タスクバーにこのようなアイコンが表示されます。右クリックしてビジュアライザーを開くと、手を検出したボーンのビジュアライズアプリが立ち上がります。

f:id:takumifukasawa:20200224183210p:plain

f:id:takumifukasawa:20200224220837p:plain

これで、LeapMotionが無事にPCで扱える状態になりました。 余談ですが、ビジュアライズアプリのアイコンを見るどうやらUnityで作られているようですね。

2. UE4

LeapMotion のプラグインを有効化

UE4でLeapMotionを使う場合、Pluginsから有効にするだけでLeapMotionを使う準備ができます。有効にしたら再起動が必要です。

f:id:takumifukasawa:20200224161936p:plain

Niagaraプラグインを有効化

NiagaraUE4の新しいVFXツールで、立ち位置的にはこれまでのCascadeパーティクルに変わるとされています。

docs.unrealengine.com

Niagaraを使うには、 NiagaraNiagara Extra を有効化にし、こちらもエディターを再起動する必要があります。Niagaraは扱いとしてはまだβ版のようですね。

f:id:takumifukasawa:20200224162009p:plain

Niagaraの作業

任意のSkeletalMeshからパーティクルを発生させるアセットを作成していきます。両手から発生させたいので、パーティクルのアセットは1つにし、SkeletalMeshを差し替えることのできるような構造になっているのがよさそうです。Niagara の実装はこちらを参考にさせていただきました。

www.youtube.com

Niagara System, Niagara Emitter を作成

Niagaraアセットの構成はこちらの記事がとてもわかりやすく解説してくださっています。

crabpunch.hatenablog.com

今回は、Niagara System と Niagara Emitter の2つのアセットを使っていきます。それぞれ FX_HandParticleSystem, FX_SurfaceParticle と名付けました。

Niagara Emitter で SkeletalMesh からパーティクルを発生させる機能をつくり、Niagara System から Niagara Emitter の SkeletalMesh を指定できるように設定を変更していきます。

f:id:takumifukasawa:20200224184922j:plain

Niagara のアセットを作る際にテンプレートを選ぶことができるのですが、Niagara System の方はテンプレートなしで、Niagara Emitter は Simple Sprite Burst テンプレートを使用します。

Niagara System f:id:takumifukasawa:20200224185817p:plain

Niagara Emitter f:id:takumifukasawa:20200224185833p:plain

Niagara Emitter: SkeletalMesh からパーティクルを発生させる

先ほど作った Niagara Emitter は、モジュールを足したり消したりして以下のような構成にしました。重要なものをピックアップしていきたいと思います。

ポイントは Spawn Burst InstanteneousInitialize Mesh Reproduction Sprite です。

f:id:takumifukasawa:20200224190249j:plain

Spawn Burst Instanteneous でパーティクル発生時に一定個数のパーティクルを発生させます。今回は Spawn Count を3000とし、Emitter Life CycleNextLoopDuration を 1としました。これで、1秒間隔で3000個発生させるパーティクルとなります。また、 Initialize ParticleLifeTime も1秒以内に収まるようにします。

f:id:takumifukasawa:20200224190902j:plain

いよいよメッシュの表面からパーティクルを発生させる部分に入ります。Initialize Mesh Reproduction Sprite モジュールを追加すると、SkeletalMesh の表面からパーティクルを発生させることができます。ここに手のメッシュを指定するのですが、これは LeapMotion のプラグインを有効化すると、そのフォルダの中に入っています。

f:id:takumifukasawa:20200224191231p:plain

f:id:takumifukasawa:20200224191444p:plain

ここまでの作業で、手の形からパーティクルが発生されるようになりました。

f:id:takumifukasawa:20200224231153p:plain

Niagara System: SkeletalMesh の指定

つづいて Niagara System で SkeletalMesh を自由に変更できるようにしていきます。変更可能にしておくことにより NiagaraSystem 1つでいくつもの SkeletalMesh に対応させることができます。

始めに作った Niagara System を開き、Track から先ほどの Niagara Emitter を追加します。

f:id:takumifukasawa:20200224192134p:plain

Userの中にSkeletalMesh型の変数を追加します。ここではUser.SkeletalMesh命名しました。次に、追加した変数を Initialize Mesh Reproduction SpriteMesh にドラッグします。すると、紫の鎖のようなマークとともに SkeletalMesh から User.SkeletalMesh に切り替わったかと思います。これで、発生源の SkeletalMesh を 変数から参照できるようになりました。

f:id:takumifukasawa:20200224192651p:plain

f:id:takumifukasawa:20200224192925p:plain

LeapMotion を扱う Actor を作成

ここでは、手の動きと Niagara を連携させる処理を作っていきます。具体的な処理は実際にダウンロードして中身を見ていただければと思います。この Actor はBP_LeapDesktopActor命名しました。

そもそも UE4 上で LeapMotion による手の動きと SkeletalMesh の連動は、手の SkeletalMesh に設定した Animation のインスタンスに対して 毎フレームごとに手の動きを更新する構造になっているようです。

Actor を作るにあたって、LeapMotion と手の動きを連携しつつ、手の表示もし、エフェクトと連携も行う Actor を Level 上に配置するだけで使えるようになることが再利用のしやすさ的にも理想なのですが、一つ問題があります。

それは、手のアニメーションと Niagara を連携させるためには、Level に配置した Niagara System の SkeletalMesh の Source に Actor を指定する 必要があること。具体的にはここです。

f:id:takumifukasawa:20200224220426p:plain

その理由から SkeletalMesh Actor を Level 上に配置することが必須となったため、このような中身にしました。大きなポイントは3つです。

  1. 手の動きを反映させる SkeletalMesh Component は Actor に追加したままで、非表示にしておく
  2. 代わりに、手を表示する SkeletalMesh Actor を Level 上で配置する
  3. 変数で SkeletalMesh を受け取るようにし、Blueprint内で その SkeletalMesh の Animation を更新する

これで、Level 上に SkeletalMesh Actor を置きつつ手の動きを Niagara に連携させることを実現しました。

Event Graph f:id:takumifukasawa:20200224193703p:plain

アニメーション更新メソッド(右手) f:id:takumifukasawa:20200224193744p:plain

Level に配置

LeapMotion を取り扱う Actor を配置し、子に Niagara System のアセットと SkeletalMesh を配置します。エフェクトと手の位置を合わせるため、position や rotation をゼロにしておきます。

注意点は親の Actor の rotation で、LeapMotion のデバイスの位置や向きに応じて回転を合わせます。

また、NiagaraOverride Parameters で手ごとの Default MeshSource を指定します。

f:id:takumifukasawa:20200224230551p:plain

完成

あとは好きなように色やサイズなどを調整します。

f:id:takumifukasawa:20200224161601p:plain

注意点

GPUパーティクルを有効にするとなぜかパーティクルが表示されなくなります。そのため現状CPUパーティクルのみ有効です。 他のSkeletalMeshでは問題なく動いたので、Niagaraの設定などを深堀りする必要がありそうです。こちらは調査中です。

参考

Niagaraの入門(2) Niagaraの構成 - カニミソの備忘録

https://www.youtube.com/watch?v=Ad6ABt_Vd5Y

【UE4】LeapMotionを導入するとエディターが落ちやすくなるときの対策

環境

  • Windows10
  • UE4.23.1

起こったこと

UE4 で LeapMotion を使うには、プラグインを有効にし、LeapMotion Content / ExamplesLeapDesktopActorをLevelに配置するだけで手の動きに連動したメッシュの表示を行うことができます。

f:id:takumifukasawa:20200224173437p:plain 

しかし、Actorを配置したあとに何回か実行を繰り返すとエディターが落ちるようになりました。再起動しても現象は変わらずでした。

解決策

LeapDesktopActor の Event Graph を開くと、Event End Play イベントの Set Leap Event のModeがLEAP MODE VRでした。

今回はデスクトップ向けで使っていたので、これをLeap Mode DESKTOPにします。自分の場合はこうすると落ちなくなりました。

f:id:takumifukasawa:20200224173454p:plain

f:id:takumifukasawa:20200224173505p:plain

備考

github の readme で Set Leap Event の項目を見ると、Set basic global leap tracking options. Useful for switching tracking fidelity or desktop/vr tracking mode. と書いてありました。つまりこのメソッドはトラッキングの設定を変えるための用途のようです。

ラッキングの設定がEvent BeginPlayEvent EndPlayで違っていた理由は調べてもよくわからなかったので、正しい解決法かは分かりません。ただ、開発するプラットフォーム向けに揃えておいた方がよさそうだと思ったので、設定を変更しました。

github.com