openFrameworksでマウス入力からある程度滑らかな線を引く

お絵描き機能の実現のためにタイトルのことをしたいときのソリューションをまとめておく。本気で滑らかにしたいならよりよいアルゴリズムがあるはずなので、他の場所で記事を探して欲しい。

基本

一番単純な実装は、マウスが押されている時の各フレームごとのマウス位置で点を打つ方法だ。しかし、これ少し早くマウスを動かすだけで点線になってしまう。そこで現在のマウス位置と直前のフレームでのマウス位置の間でofDrawLineで線を引く。こうすると必ず線はつながる。線を引くだけだとつなぎ目が見えてしまうことがあるが、そんなときは線の終端に線の太さと同じ円を描くと滑らかになる。

shaderを使うとき

上記のやり方でやっていくとshaderを使ったときに、ofDrawLineで描く線がofSetLineWidthの値に関わらず細くなる現象に遭遇する。shader側に渡すことで解決出来るらしいのだが、それもちょっとなと思って描き方を変えた。線の角度と長さを計算し、ofVertexを利用して四角形を描くのだ。メソッドにまとめたのが以下のコードになる。

void Test::drawLine(ofPoint startPos, ofPoint stopPos, float lineWidth){
    ofPoint vec = (stopPos - startPos).normalize() * lineWidth/2;
    vec.rotate(90, ofPoint(0, 0, 1));
    
    ofPoint first = startPos + vec;
    ofPoint second = stopPos + vec;
    ofPoint third = stopPos - vec;
    ofPoint fourth = startPos - vec;
    ofBeginShape();
    ofVertex(first);
    ofVertex(second);
    ofVertex(third);
    ofVertex(fourth);
    ofEndShape();
    
    ofDrawCircle(startPos, lineWidth/2);
    ofDrawCircle(stopPos, lineWidth/2);
}

UnityのClothをスクリプトから操る

最近仕事でめっちゃCloth使ったのだが、Clothはスクリプトからcoefficientsを利用することで操れて楽しい。以下、簡単にClothについて箇条書き。

  • Clothがついてると拡大縮小が出来ないが、ClothをDestroyすると可能になる
    • Cloth、実は実行中でもDestroyとAddComponetが可能だったりする
    • パラメータをどこかに保存しておけば布の状態を復元できる
  • Clothの制約、coefficientsはコードから変更できる
    • 通常GUIから変更すると書いてあるが実際はコードから変更可能
    • 普通に左上が原点のマトリクスだった(下にPlaneのcofficientsを変更するコードを貼っておく)
    • これにより動的に布の動き方を制御することが可能
      • 固定点を上部にした状態と、下部にした状態を使い分けたり、布に片方がだけはためくなどの状態を作り出せる
const int meshWidth = 11;
const int meshHeight = 11;
const int meshNum = meshWidth * meshHeight;	

ClothSkinningCoefficient[] frontSkin = new ClothSkinningCoefficient[meshNum];
ClothSkinningCoefficient[] backSkin = new ClothSkinningCoefficient[meshNum];

void Start(){
	for (int i = 0; i < meshNum; i++) {
		frontSkin [i].maxDistance = float.MaxValue;
		frontSkin [i].collisionSphereDistance = float.MaxValue;
		backSkin [i].maxDistance = float.MaxValue;
		backSkin [i].collisionSphereDistance = float.MaxValue;
	}
	for (int i = 0; i < 11; i++) {
		frontSkin [i ].maxDistance = 1.5f;
		frontSkin [i + 11*10].maxDistance = 0;

		backSkin [i + 110].maxDistance = 1.5f;
		backSkin [i].maxDistance = 0;
	}
}

public void ChangeFront(){
	GetComponent<Cloth> ().coefficients = frontSkin;
}

macOSのUnityでマルチディスプレイ時の注意点 InputModuleをハックする

Unity、少し前からPCでのマルチディスプレイに対応している。
https://docs.unity3d.com/ja/540/Manual/MultiDisplay.html

対応しているのだが、macではマルチディスプレイ時に主ディスプレイ以外のマウスイベントを拾ってくれなくなる問題(少なくとも5.6.1f1では)があった。これのソリューションをまとめておく。

概要

簡単に箇条書きすると以下のようになる。EventSystemを利用するのはUnityのGUIパーツに反応させるため。もし使わないなら不要。

  1. node.jsの'osx-mouse’でマウスイベントを取得
  2. ‘node-osc’を利用しOSCでUnityに送信
  3. UnityOSCで受け取り
  4. EventSystemのInputModuleが持つBaseInputを継承したクラスに値を渡し、擬似的なマウス入力を実現

Node.jsのサンプルコード

var mouse = require('osx-mouse')();
var osc = require('node-osc');

var client = new osc.Client('127.0.0.1', 12000);

mouse.on('move', function(x, y) {
    console.log("move "+x, y);
    var m = new osc.Message("/mousePos");
    m.append(x);
    m.append(y);
    client.send(m);
});

mouse.on('left-down', function(x, y) {
    console.log("left-down "+x, y);
    var m = new osc.Message("/mouseDown");
    m.append(x);
    m.append(y);
    client.send(m);
});

mouse.on('left-up', function(x, y) {
    console.log("left-up "+x, y);
    var m = new osc.Message("/mouseUp");
    m.append(x);
    m.append(y);
    client.send(m);
});

mouse.on('left-drag', function(x, y) {
    console.log("left-drag "+x, y);
    var m = new osc.Message("/mouseDragged");
    m.append(x);
    m.append(y);
    client.send(m);
});

すんなり書けて楽だった。

Unity側

OSC自体の扱いは省略。

まず標準のStandAloneInputModuleを継承したクラス、今回はマルチディスプレイのタッチパネルだったのでTouchPanelInputModuleという名前のクラスを作り、BaseInputを入れ替えられるようにする。BaseInputはPCにおいては各フレームごとのマウス入力の状態が入ってるクラスだ。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class TouchPanelInputModule : StandaloneInputModule {

	public void SetInput(BaseInput input){
		m_InputOverride = input;
	}

}

次に、そのBaseInputを継承したToucPanelInputを作る。OSCで送られてくるマウスイベントを元にこのTouchPanelInputにSetMouseEventしてやることでマウス入力が行われるようになる。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class TouchPanelInput : BaseInput {
	private Vector2 mousePos;
	private bool mouseButton = false;
	private bool mouseButtonDown = false;
	private bool mouseButtonUp = false;
		
	public void SetMouseEvent(Vector2 pos, bool mouseOn, bool mouseDown, bool mouseUp){
		this.mousePos = pos;
		mouseButton = mouseOn;
		mouseButtonDown = mouseDown;
		mouseButtonUp = mouseUp;
	}

	public override Vector2 mousePosition {
		get {
			return mousePos;
		}
	}

	public override Vector2 mouseScrollDelta {
		get {
			return base.mouseScrollDelta;
		}
	}

	public override bool GetMouseButton (int button)
	{
		if (button == 0) {
			return mouseButton;
		}
		return false;
	}

	public override bool GetMouseButtonDown (int button)
	{
		if (button == 0) {
			return mouseButtonDown;
		}
		return false;
	}

	public override bool GetMouseButtonUp (int button)
	{
		if (button == 0) {
			return mouseButtonUp;
		}
		return false;
	}

	public override bool GetButtonDown (string buttonName)
	{
		return false;
	}
}

OSCで送られてくるマウスイベントからいい感じにBaseInputみたいにするのに下記クラスを作成したが、名前OSCMouseInputManagerとかAdapterくらいのほうが良かった気がする。あとこいつの機能、上記のTouchPanelInputに持たせたほうがいいと思う。

class OSCMouseEvent {
	public bool down, up, drag;
	public Vector3 rawPos;
	private int noCount;

	public bool On {
		get{
			return drag || down || up;
		}
	}

	public void SetValues(bool down, bool up, bool drag, Vector3 rawPos){
		this.down = down;
		this.up = up;
		this.drag = drag;
		this.rawPos = rawPos;
		noCount = 0;
	}

	public void Update(){
		down = false;
		up = false;
		noCount++;
		if (noCount > 20) {
			drag = false;
		}
	}
}

注意点としてはEventSystemは同時にひとつのInputModuleしか持てないみたいなので、うまいことModuleのenabledを切り替えてくれ!って感じ。

おわりに

https://bitbucket.org/Unity-Technologies/ui
標準のInputModuleたちはコードが公開されているので大変参考になった。

最初は自分自身でEventSystemの真似事を実装してて、RaycastAllしてヒットしたらInvokeでonClickを呼び出しみたいなことやったのだけど、動くっちゃ動くけど、なぜか最初のクリックには反応せず、2回目以降は反応するなどの中途半端な機能性だったので、がんばってInputModuleとBaseInputをゴリゴリと変えてみたらちゃんと動くようになった。

こんな面倒くさいことせずとも動くようになって頂きたい……。