カテゴリ別アーカイブ

31 Articles

セカンドライフでタッチしたプリムの情報を取得する

はじめに

セカンドライフでプリムをクリックしたときにどこの位置を触ったのか、
といった情報をずらーっと出してみるサンプルです。

サンプル

とにかくいろいろ情報を取得してみます。
全部表示すると訳が分からなくなるのでちょっと表示しています。
ちなみに、テクスチャのUUIDに関しては権限を持っていないと取得できません。

integer	detected_link		= 0;
integer	detected_face		= 0;
vector	detected_touch_pos	= ZERO_VECTOR;
vector	detected_touch_uv	= ZERO_VECTOR;

integer	selected_link		= 0;
integer	selected_face		= 0;
vector	selected_touch_pos	= ZERO_VECTOR;
vector	selected_touch_uv	= ZERO_VECTOR;
string	selected_name;
string	selected_description;
vector	selected_position;
rotation selected_rotation;
vector	selected_size;
string	selected_texture_name;
vector	selected_texture_repeats;
vector	selected_texture_offsets;
float	selected_texture_rotation_in_radians;
vector	selected_color;
float	selected_alpha;
integer	selected_isfullbright;
float	selected_intensity;
string	selected_normal_name;
vector	selected_normal_repeats;
vector	selected_normal_offsets;
float	selected_normal_rotation_in_radians;
string	selected_specular_name;
vector	selected_specular_repeats;
vector	selected_specular_offsets;
float	selected_specular_rotation_in_radians;
vector	selected_specular_color;
integer	selected_specular_glossiness;
integer	selected_specular_environment;
integer	selected_alpha_mode;
integer	selected_mask_cutoff;

setSelected(integer target_link, integer target_face, vector target_touch_pos, vector target_touch_uv) {
	integer num = 0;
	list x = llGetLinkPrimitiveParams(target_link,
		[
			PRIM_NAME,						// string name
			PRIM_DESC,						// string description
			PRIM_POS_LOCAL,					// vector position
			PRIM_ROT_LOCAL,					// rotation rot
			PRIM_SIZE,						// vector size
			PRIM_TEXTURE, target_face,		// string texture, vector repeats, vector offsets, float rotation_in_radians
			PRIM_COLOR, target_face,		// vector color, float alpha
			PRIM_FULLBRIGHT, target_face,	// integer boolean
			PRIM_GLOW, target_face,			// float intensity
			PRIM_NORMAL, target_face,		// string texture, vector repeats, vector offsets, float rotation_in_radians
			PRIM_SPECULAR, target_face,		// string texture, vector repeats, vector offsets, float rotation_in_radians, vector color, integer glossiness integer environment
			PRIM_ALPHA_MODE, target_face	// integer alpha_mode, integer mask_cutoff 
		]
	);
	selected_link			= target_link;
	selected_face			= target_face;
	selected_touch_pos		= target_touch_pos;
	selected_touch_uv		= target_touch_uv;
	selected_name			= llList2String(x, num++);
	selected_description	= llList2String(x, num++);
	selected_position		= llList2Vector(x, num++);
	selected_rotation		= llList2Rot(x, num++);
	selected_size			= llList2Vector(x, num++);
	selected_texture_name					= llList2String(x, num++);
	selected_texture_repeats				= llList2Vector(x, num++);
	selected_texture_offsets				= llList2Vector(x, num++);
	selected_texture_rotation_in_radians	= llList2Float(x, num++);
	selected_color							= llList2Vector(x, num++);
	selected_alpha							= llList2Float(x, num++);
	selected_isfullbright					= llList2Integer(x, num++);
	selected_intensity						= llList2Float(x, num++);
	selected_normal_name					= llList2String(x, num++);
	selected_normal_repeats					= llList2Vector(x, num++);
	selected_normal_offsets					= llList2Vector(x, num++);
	selected_normal_rotation_in_radians		= llList2Float(x, num++);
	selected_specular_name					= llList2String(x, num++);
	selected_specular_repeats				= llList2Vector(x, num++);
	selected_specular_offsets				= llList2Vector(x, num++);
	selected_specular_rotation_in_radians	= llList2Float(x, num++);
	selected_specular_color					= llList2Vector(x, num++);
	selected_specular_glossiness			= llList2Integer(x, num++);
	selected_specular_environment			= llList2Integer(x, num++);
	selected_alpha_mode						= llList2Integer(x, num++);
	selected_mask_cutoff					= llList2Integer(x, num++);
}

showSelectedData() {
	llOwnerSay(
		"linknum: "		+ (string)selected_link + ", " + 
		"facenum: "		+ (string)selected_face + "\n" +
		"linkpos: "		+ (string)selected_position + "\n" +
		"linksize: "	+ (string)selected_size + "\n" +
		"color: "		+ (string)selected_color + "\n" +
		"alpha: "		+ (string)selected_alpha + "\n" +
		"texture: "		+ (string)selected_texture_name + "\n" +
		"normal: "		+ (string)selected_normal_name + "\n" +
		"specular: "	+ (string)selected_specular_name + "\n" + 
		"touchuv: "		+ (string)selected_touch_uv + "\n" +
		"touchpos: "	+ (string)selected_touch_pos + "\n" +
		""
	);
}

touchStart() {
	// 押した場所を確認
	setSelected(detected_link, detected_face, detected_touch_pos, detected_touch_uv);
	showSelectedData();
}

default {
	
	touch_start(integer num_detected){
		detected_link		= llDetectedLinkNumber(0);
		detected_face		= llDetectedTouchFace(0);
		detected_touch_pos	= llDetectedTouchPos(0);
		detected_touch_uv	= llDetectedTouchUV(0);
		touchStart();
	}
	
}

 

セカンドライフでタッチしている時間によって動作を変える

はじめに

セカンドライフでタッチしている時間によって動作をかえます。

タイマーを測って動作を切り替える

フラグとllResetTimeとllGetTimeを組み合わせるサンプルです。
作り方によっては、細かく時間を区切ることができます。

なお、ユーザーが一人の場合を想定しています。
複数の場合は、作りを変えた方がいいかもしれません。

integer	is_touching			= FALSE;

touchStart() {
	llOwnerSay("touchStart");
}

shortTouchEnd() {
	llOwnerSay("shortTouchEnd");
}

longTouchEnd() {
	llOwnerSay("longTouchEnd");
}

longlongTouch() {
	llOwnerSay("longlongTouch");
}

default {
	
	touch_start(integer num_detected){
		is_touching		= TRUE;
		touchStart();
		llResetTime();
	}
	
	// 押し続け
	touch(integer num_detected) {
		float timeSec = llGetTime();
		// 1秒以上の押し続け
		if(is_touching && (timeSec >= 3.0)) {
			longlongTouch();
			is_touching = FALSE;
		}
	}
	 
	touch_end(integer num_detected){
		// 1秒未満の押し終わり
		if(is_touching) {
			float timeSec = llGetTime();
			if(timeSec < 1.0) {
				shortTouchEnd();
			}
			else if(timeSec < 3.0) {
				longTouchEnd();
			}
			is_touching = FALSE;
		}
	}
}

タイマーイベントで押した時間を測る

細かいことはできませんが、タイマーイベントを使用しても作ることができます。

ちなみに、これもまた複数ユーザーのことは考えていません・・・

touchStart() {
	llOwnerSay("touchStart");
}

longlongTouch() {
	llOwnerSay("longlongTouch");
}

default {
	
	touch_start(integer num_detected){
		touchStart();
		llSetTimerEvent(0);
		llSetTimerEvent(3);
	}
	
	// 3秒以上押し続けた場合
	timer() {
		llSetTimerEvent(0);
		longlongTouch();
	}
	
	touch_end(integer num_detected){
		llSetTimerEvent(0);
	}
}

複数ユーザーの対応方法

まずどんな場合でも初期状態に戻す初期化関数を作っておきましょう。

クリックされると引数に値が入ってきます。
1つであれば1人かクリックした場合です。
1人がクリックした場合にllDetectedKeyでユーザーのUUIDを内部メモリに保存しましょう。
2人以上がもしクリックした場合は初期化関数を呼びましょう。

クリックが始まったらtouch_startのイベントを締め切りましょう(誰かがクリックするの防止)
あとは、touch関数の引数は1人のときのみのUUIDを確認して、
最初に保存したUUIDと一致するか調べて処理を行います。

タッチが終了したら、内部メモリのUUIDを解放しましょう。

なお、タイミングにより内部メモリにUUIDが残り続ける場合があるので、
タイムアウトできれいにするようにしておくとよいでしょう。

セカンドライフのテクスチャの秘密(ノーマルマップ編)

はじめに

こんにちは!

久しぶりのブログです。

今日は、セカンドライフのテクスチャ、ノーマルマップ  の秘密について話します。
続かないかもしれませんが、とりあえず第1弾です。


ノーマルマップの色成分の基礎

ノーマルマップといえば、平面の凹凸を現した画像です。
通常グレースケールで高さを表した画像から、このノーマルマップを作成します。
おそらく、少し詳しzい方はこの辺までは、ご存知かと思います。

しかし、具体的に
赤色成分は、何をセカンドライフ上の何を示すのか、
青色成分は……?
といったことまで知っている方はそんなにいないのでは?

ということで、この色成分が何を表しているかの解説となります。

ノーマルマップの英語の解説を確認してみましょう。
これを見ると、次のような情報が分かります。
赤成分R = 方向成分X
緑成分G = 方向成分Y
青成分B = 方向成分Z

ただし、方向とは言ってもX+がR+に
対応しているのかといったことはテキストには書いてありません。
なぜ、それが重要かといいますと、ソフトによって色成分との対応が異なるためです。

>・ZBrush:(-X,+Y,+Z)
>・CrazyBump:(+X,+Y,+Z)
>・Blender 2.4x:(-X,-Y,+Z)
>・Blender 2.57以降:(+X,+Y,+Z)
Sandy Virtual City様のブログから引用
※XYZ軸がどちらに対応しているかも重要となるため、ブログ内の軸の解説も参照しましょう。
ブログでは、X軸のプラスをUV座標系のU方向、Y軸のプラスをUV座標系のV方向、Z軸のプラスを凸方向としています。

では、セカンドライフを調べてみましょう。
公式ページにあるノーマルマップの画像から、
凸に対して、画素の色が次のようになっています。
上が RGB(126,178,212)
右が RGB(169,124,210)
下が RGB(109, 75,206)
左が RGB( 69,124,212)

つまり、セカンドライフ(+X, -Y, +Z)
となることが分かります。

これは、NormalMap-Onlineでは、
Invert の R にチェックが入った場合と同様になります。

これらをおさらないすると、たとえば下記のようなバンプマップ/ハイトマップを用意したとすると、

次のような画像がセカンドライフ上でのノーマルマップとなります。

本日は、ここまで!

セカンドライフで自分の行動の色々な情報を取得する

次のような自分の行動の情報を取得してみます。
・自分が喋った回数
・人から聞いた回数(アバターが話したものに限定)
・空を飛んでいる時間(ホバーは含めない)
・海の中を泳いでいる時間(ホバーは含めない)
・歩いている時間
・走った時間
・ジャンプ中の時間
・テレポートした回数
・SIMをまたいだ回数
・人や当たり判定がある物体と衝突した回数

以下、サンプルです。
このコードを適当なオブジェクトに入れた後、HUDとして装備してください。
drawStatusと発言すると、自分の情報が表示されます。

ログインした回数とかも取れるかなと思ったのですが、
装備した回数と見分けがつかないので諦めました。

integer count_attack		= 0;
integer count_speak			= 0;
integer count_listen		= 0;
integer count_flying		= 0;
integer count_swimming		= 0;
integer count_running		= 0;
integer count_walking		= 0;
integer count_teleport		= 0;
integer count_simtraversal	= 0;
integer count_jumping		= 0;

integer TYPE_ATTACK			= 0;
integer TYPE_SPEAK			= 1;
integer TYPE_LISTEN			= 2;
integer TYPE_FLYING			= 3;
integer TYPE_SWIMMING 		= 4;
integer TYPE_RUNNING		= 5;
integer TYPE_WALKING		= 6;
integer TYPE_TELEPORT		= 7;
integer TYPE_SIMTRAVERSAL	= 8;
integer TYPE_JUMPING		= 9;

integer show_dialog		= FALSE;
integer listen_handle	= -1;

addAction(integer action) {
//	llOwnerSay("action : "+ (string)action);
	if(action == TYPE_ATTACK) {
		count_attack	= count_attack + 1;
	}
	else if(action == TYPE_SPEAK) {
		count_speak		= count_speak + 1;
	}
	else if(action == TYPE_LISTEN) {
		count_listen	= count_listen + 1;
	}
	else if(action == TYPE_FLYING) {
		count_flying	= count_flying + 1;
	}
	else if(action == TYPE_SWIMMING) {
		count_swimming	= count_swimming + 1;
	}
	else if(action == TYPE_RUNNING) {
		count_running	= count_running + 1;
	}
	else if(action == TYPE_WALKING) {
		count_walking	= count_walking + 1;
	}
	else if(action == TYPE_TELEPORT) {
		count_teleport	= count_teleport + 1;
	}
	else if(action == TYPE_SIMTRAVERSAL) {
		count_simtraversal	= count_simtraversal + 1;
	}
	else if(action == TYPE_JUMPING) {
		count_jumping	= count_jumping + 1;
	}
}

showStatus() {
	llWhisper(0,
		"attack\t:"			+ (string) count_attack			+ " times\n" +
		"speak\t:"			+ (string) count_speak			+ " times\n" +
		"listen\t:"			+ (string) count_listen			+ " times\n" +
		"teleport\t:"		+ (string) count_teleport		+ " times\n" +
		"simtraversal\t:"	+ (string) count_simtraversal	+ " times\n" +
		"flying\t\t:"		+ (string) count_flying			+ " sec\n" +
		"swimming\t\t:"		+ (string) count_swimming		+ " sec\n" +
		"running\t:"		+ (string) count_running		+ " sec\n" +
		"walking\t:"		+ (string) count_walking		+ " sec\n" +
		"jumping\t:"		+ (string) count_jumping		+ " sec"
	);
}

openDialog() {
	if(listen_handle != -1) {
		llListenRemove(listen_handle);
	}
	listen_handle = llListen(-100, "", llGetOwner(), "");
	llDialog( llGetOwner(), "Which operation?\nYou say \"drawStatus\"... or \"resetStatus\"", ["status", "reset", "cancel"], -100 );
	llSetTimerEvent(0.0);
	llSetTimerEvent(30.0);
	show_dialog = TRUE;
}

closeDialog() {
	if(listen_handle != -1) {
		llListenRemove(listen_handle);
	}
	show_dialog = FALSE;
	listen_handle = llListen(0, "", "", "");
	llSetTimerEvent(0.0);
	llSetTimerEvent(1.0);
}

default {
	state_entry() {
		llOwnerSay("Start Script !\nYou say \"drawStatus\"... or \"resetStatus\"");
		closeDialog();
	}
	
	touch_start(integer total_number) {
		openDialog();
	}
	
	timer() {
		if(show_dialog) {
			closeDialog();
		}
		else {
			string my_animatin = llGetAnimation(llGetOwner());
			if(my_animatin == "Flying") {
				vector v = llGetPos();
				if(llWater(v) > v.z) {
					addAction(TYPE_SWIMMING);
				}
				else {
					addAction(TYPE_FLYING);
				}
			}
			else if(my_animatin == "Running") {
				addAction(TYPE_RUNNING);
			}
			else if(my_animatin == "Walking") {
				addAction(TYPE_WALKING);
			}
			else if(my_animatin == "Jumping") {
				addAction(TYPE_JUMPING);
			}
		}
	}
	
	listen( integer channel, string name, key id, string message ) {
//		llOwnerSay(""+ (string)channel +" " + name + " " + (string)id + " " + message);
		if(show_dialog) {
			if(message == "status") {
				showStatus();
			}
			else if(message == "reset") {
				llResetScript();
			}
			else if(message == "cancel") {
			}
			closeDialog();
		}
		else {
			if(id == llGetOwner()) {
				if(message == "drawStatus") {
					showStatus();
				}
				else if(message == "resetStatus") {
					llResetScript();
				}
				else {
					addAction(TYPE_SPEAK);
				}
			}
			else if(llGetAgentSize(id)){
				addAction(TYPE_LISTEN);
			}
		}
	}
	
	changed(integer mask) {
		if(mask & CHANGED_OWNER) {
			llResetScript();
		}
		if((mask & CHANGED_TELEPORT) && (mask & CHANGED_REGION)) {
			addAction(TYPE_TELEPORT);
		}
		else if(mask & CHANGED_TELEPORT) {
			addAction(TYPE_TELEPORT);
		}
		else if(mask & CHANGED_REGION) {
			addAction(TYPE_SIMTRAVERSAL);
		}
	}
	
	collision_start(integer num) {
		if(llDetectedType(0) & ACTIVE) {
			vector my_velocity = llGetVel();
			vector velocity = llDetectedVel(0);
			float power = llVecMag(my_velocity - velocity);
			if(power >= 1.0) {
				addAction(TYPE_ATTACK);
			}
		}
	}
	
	on_rez(integer param) {
		// 1 ログイン、装備、rez
	}
	
	attach(key attached) {
		if(attached) {
			// 2 ログイン、装備
		}
	}
}

セカンドライフのプリムの当たり判定の検証

こんにちは!

昨日から引き続き、「セカンドライフ技術系 Advent Calendar 2016」です!

すみません。
昨日の記事は、役に立つ情報もあったかもしれませんが、
今回は、テクニック系と違い、前々から気になっていたことを検証する……といった内容となります。
なんかすごいの来る!と期待していた方はスミマセン。(いないと思われますが)

というわけで、

あんまり面白いものではないかもしれませんが、
調査結果の報告というわけで、話していきます……。


はじめに

昨年に、当たり判定の記事を書きました。
セカンドライフでメッシュに物理形状を設定しよう1

今回は、その記事のさらに「実像の種類」について気になったことを確認していきたいと思います。
最初に少し解説しますが、より理解を深めたい方は前回の記事から読むことをお勧めします。

「実像の種類」というのはオブジェクトの設定から選べるものです。
実像というのは、当たり判定用に用いるオブジェクトです。

gui_jituzo_1 → gui_jituzo_2

ここで実像を「なし」「プリム」「凸状の外殻構造」の3種類選べます。
「なし」にした場合は、当たり判定用のオブジェクトを用意しないということになります。

ここで「プリム」「凸状の外殻構造」の違いについて説明するために、
3次元だと書きにくいので、2次元で考えたいと思います。

atari_hazimeni_1
2枚の壁が90度の垂直であることとします。

atari_hazimeni_2
実像の種類を「凸状の外殻構造」にすると、
黄色の囲んだ部分のように当たり判定が生まれます。
※中身すべてにすべて当たり判定を持つことになります。

atari_hazimeni_3
実像の種類を「プリム」にすると、
そのプリムそのものが1枚の壁のように当たり判定を持ちます。
※中身というものは存在しません。


実像の種類「プリム」の疑問

ここで、私の中では素朴な疑問が生まれました。
当たり判定の計算方法により、アバターが透けることがないのかということです。

たまーに、なぜかよくわからないですが、壁の中に入って閉じ込められることありませんか。
それが、このこの設定によるものなのかなーと、疑問に思ったわけです。

セカンドライフの実際の当たり判定のアルゴリズムについては知らないので少し、推測した話となります。
まず、当たり判定といえば、レイと3角ポリゴンとの衝突の検出※が有名です。
そこで、これをまず使用していると仮定します。
(※アルゴリズム、計算方法については、「はじめての3Dゲーム開発」)

そして、もしこのようなアルゴリズムを使って単純に実装している場合、
つまり、1本のレイを飛ばすような当たり判定を使っているのであれば、
次のような現象が起きるのでは……と思いつきます。

atari_nazo_1
壁1と壁2が90度で交わっているとします。

atari_nazo_2
ここで Ray1 を飛ばします。
つまりアバターが壁1に向かって斜めに突進した感じです。

atari_nazo_3
Ray1は、壁1と壁2の2種類に当たり判定チェックを行います。
その結果、壁1との衝突を検知することができます。
そして、めり込まないように、斜めの成分だけを抜き出します。

atari_nazo_4
次に、壁1との衝突点からRay2を飛ばします。
ここで、問題なのですが、Ray2が壁2との当たり判定を正しく行えるかという謎です。

普通は壁1と壁2は接続されているはずなので、
Ray2は壁2と衝突するはずですが、
実際は壁1と壁2は、別のポリゴンとして管理しており、
これらのポリゴンの位置は、実数による誤差の関係上、微妙に隙間が空いているはずなのです。

1本のレイを使用した単純な当たり判定ならば、上記のような問題は起きるのですが、
もちろん、レイを複数とばす。あるいは、そもそも球とポリゴンとの当たり判定を使用するなど
いろいろな工夫を行っていれば、このようなことになりません。
なので、有名なセカンドライフであれば、すり抜けることはおそらくないはずですが。

というわけで、前置きがすごく長くはありましたが、
当たり判定用のプリム=壁のプリムを作成して、
衝突した場合どうなるか検証するのが、この記事の目的となります!


すり抜けの検証

次のような壁(1辺が10m)を作成して、
atari_test_1

下記のように10分間壁に向かって当たり続けて、すりぬけるか確認します。
atari_test2

ビューアは2種類で確認しました。
公式ビューア(v4.1.2.321518)
Catznipビューア(R10.0)の両方で確認しました。

元となるメッシュデータについては、頂点を共有しないメッシュと、
共有するメッシュの2種類について確認。

また、当たり判定のデータは、dae情報で直接設定する方法と、
表示用メッシュから自動生成する方法の2種類を試しました。

それで早速、結果なのですが、

.
..

なんと

.
..

なんとッ!

.
..

すり抜けませんでした!

まあ、そうですよねー。
期待していた方?はスミマセンでした。

ちなみにさらに検証していったのですが、
隙間がわずかにあってもすり抜けませんでした。

どの程度の隙間からすり抜けるようになるのか気になる点ではありますが、
まずは、接続された面であれば、当たり判定を「プリム」にしていても
問題ないということは確実ということが分かり、安心しました。

たまーにすり抜けるのは、
たぶん、幽霊の仕業か本当に実像的にスキマがあるのでしょう。きっと……


一応、これで2日間にわたる記事は終わりです。
ですが!まだまだ12月が始まったばかりです!

ではみなさま!
セカンドライフ技術系 Advent Calendar 2016」を楽しんでいきましょう!

.
..

.
..

.
..

オマケ

私は、Advent Calendar以外にも普段気が向いたときに、セカンドライフ系の記事をかいております。
その中で、私としてヨイデキな記事をいくつか紹介します。(宣伝デス)

2016年4月21日 セカンドライフのスカイボックスに太陽の光を!
シンプルながらも、知っておくと良いテクニック!

2014年12月29日 セカンドライフの関数逆引きメモ
セカンドライフにはどういった機能があるのか、逆引きをみて一通り覚えておこう!

2014年12月12日 セカンドライフでアニメーション
セカンドライフでアニメーションを作る方法!

%d人のブロガーが「いいね」をつけました。