今回は、EXODUSの解析結果をUC-win/Roadで可視化するために開発されたプラグインの基本的な活用方法について、サンプルコードを用いて解説します。あらかじめ保存された解析結果を再生する形で、UC-win/Roadの人間キャラクタオブジェクトを制御する仕組みとなっています。
|
■UC-win/Roadのキャラクタオブジェクトの仕様
UC-win/Roadのキャラクタオブジェクトの機能は、人、動物などの移動しながら複雑な動きをするものに適用されます。こういった動きを保存するために、md3というファイル形式を採用しています。1つのキャラクタモデルには、さまざまな動きを再現するために複数のアニメーションデータが保存されており、任意の動きを表現できます。しかし、たとえば歩くアニメーションの場合に移動速度を時速30キロに設定すると、人間の動きとして不自然になってしまうため、時速1キロから10キロの範囲で設定してやるのが適切です。
このため、UC-win/Roadではアニメーション毎に適切な移動速度を設定できるよう設定画面が用意されており、移動時にモデル移動速度に比例した速さでアニメーションを再生することができます。
UC-win/RoadのSDKを使用すれば、キャラクタモデルの移動を制御することで、上記のような処理を実装できます。
■キャラクタに関連するオブジェクト
UC-win/Roadでキャラクタを動かして描画するには2つのオブジェクトを使用します。1つはキャラクタの描画に必要なポリゴン情報、すなわち、アニメーション情報を持つオブジェクトです。これはQuakeIII呼ばれるオブジェクトでで、当初はQuakeIIIというゲームのために開発されたキャラクタデータ形式です。
もう1つは、CharacterInstanceというオブジェクトで、UC-win/RoadのVR空間の中にキャラクタの配置・移動を行うために使用されます。このオブジェクトは位置情報、移動の計算、アニメーションの状態を管理するオブジェクトです。1つのCharacterInstanceは1つのQuakeIIIオブジェクトを参照します。また、複数のCharacterInstanceが同じQuakeIIIオブジェクトを参照することも可能です。車でいえば、複数の人が同じ型の車を運転できますが、この車の型は1つの共通のデータということになります。
データ構造では、キャラクタの形状に関する情報は1カ所で保存し(QuakeIII)、複数のインスタンス(CharacterInstance)に適用します。そのためUC-win/Roadでキャラクタの動きを制御する時は、QuakeIIIオブジェクトを事前に読み込む必要があります。MD3キャラクタのクラス構造は図1の通りです。
■サンプルプログラムのソースコード
次のソースコードでは、UC-win/Roadのキャラクタのインスタンスを生成し、初期位置を設定しています。この例ではキャラクタを一時的なオブジェクトとして生成します。
function CreateNewInstance(project: IF8ProjectForRoad):
IF8CharacterInstance;
var
startPosition : GLPointType;
begin
// プロジェクトが存在するかどうかを確認し、
// MD3キャラクタモデルが1つ以上かどうかをチェックします。
if Assigned(project) and (project.numberOfCharacters > 0) then
begin
//1つ目のmd3キャラクタを使用し、キャラクタの
// インスタンスを生成します。
result := project.CreateCharacterInstance
(project.character[1], true);
//初期位置を一時的な変数に設定し、
// キャラクタインスタンスのプロパティに代入します。
startPosition[_x] := 5000;
startPosition[_y] := 0;
startPosition[_z] := 5000;
startPosition[_z] := 0;
newInstance.instancePosition := startPosition;
end;
end; |
■UC-win/Roadのシミュレーションとの同期
通常、UC-win/Roadの交通シミュレーションの一部としてキャラクタの動きを計算します。プラグインでキャラクタのインスタンスを制御する場合は、本体の計算と同期して制御する必要があります。交通シミュレーションの計算では各車両の位置とキャラクタの位置を画面上に表示する前に計算します。表示するフレーム毎に1回計算を行いますが、VR空間の3次元データの複雑さによって、フレームの更新レートは変動します。そのため、可変の時間差に対応した計算を行う必要があります。
移動の計算自体は移動するものや数学モデルによってさまざまな方法がありますが、バーチャルリアリティのシミュレーションでは、ある物の姿勢から次の姿勢を計算します。その際、姿勢と姿勢の時間差を使用して移動量を計算します。下記は一定の速度で直線にそった動きの例です。この例は、時間差が可変の場合でも使用可能な計算方法です。
procedure CalculatePosition(dtInSeconds : double; const CurrentX,
CurrentY : double; out X, Y : double);
const
SPEED = 10; // メートル毎秒
DirectionX = 1.41421;
// 移動の方向ベクトル、X座標
DirectionY = 1.41421;
// 移動の方向ベクトル、Y座標
//方向ベクトルを単位ベクトルとします。
begin
//dtInSeconds後の新しいX座標を計算します。
X := CurrentX + SPEED * dtInSeconds * DirectionX;
//dtInSeconds後の新しいY座標を計算します。
Y := CurrentY + SPEED * dtInSeconds * DirectionY;
end; |
■キャラクタ位置の制御
ここではあらかじめ保存された移動の設定を、実際の時間で再現させる処理を説明します。まず、情報の保存形式を決める必要があります。汎用性を考慮し、姿勢(位置と方向)情報を時間によって保存するものとします。変数の型は以下のように宣言します。
type
//位置情報、方向、時刻の保存レコード
//timeで指定された時刻にキャラクタあるいは移動物体が
//positionの位置、orientationの方向で表示されると考えます。
RoutePointType = record
time : double; //時刻
position : array [1..3] of double; //3D座標
orientation : array [1..3] of double; //3Dベクトル
end;
//キャラクタあるいは移動物体が移動するルートを
//RoutePointTypeの配列で定義します。
RouteType = array of RoutePointType; |
|
■図1 MD3キャラクタのクラス構造 |
この型により決まった時刻での姿勢情報が保存されます。この動きを表現するには、保存されている姿勢でキャラクタを表示させ、実際に経過した時間によって適切な位置を計算します。このように、時間によって表示する姿勢を更新していくことで動きの表現が可能です。
ただし、UC-win/Roadで描画を行う時刻に合わせて姿勢情報を計算する必要があります。その場合は、UC-win/Roadの描画時刻とルートに保存されている姿勢の時刻が異なる可能性があるため、UC-win/Roadの描画時刻に最も近い姿勢情報(直前と直後)を補間することで、滑らかな動きの表現が可能になります。
■道路の平面線形
Roadオブジェクトの下にある各TurningPointは、平面線形のIP点(Intersection
Point)を定義します。これは道路直線空間の延長線が交差する点になります。平面線形はTurningPointの配列で定義され、その配列はRoadオブジェクトのturningPointプロパティからアクセスします。
|
■図2 キャラクタの移動の再現の仕組み |
コールバック関数を使用してキャラクタの姿勢を更新するタイミングを、UC-win/Road本体によって教えてもらうようにします。生成したキャラクタのOnMoveイベントプロパティに関数を割り当てると、キャラクタの姿勢を更新する際に、割り当てた関数が呼ばれます。この関数でキャラクタの動きを計算し、姿勢情報を更新します。
ここまでで、生成したキャラクタの姿勢を更新するところまではできましたが、UC-win/Roadのキャラクタインスタンスをあらかじめ保存されているルートに対応させる必要があります。複数の方法が考えられますが、ここでは新しいクラスでルートとキャラクタインスタンスを対応させます。具体的には、このクラスでルートの情報を保存し、キャラクタのOnMoveイベントにこのクラスのメソッドを割り当てます。
キャラクタインスタンスがUC-win/Road本体から削除される時、OnDetroyイベントを使えば、プラグイン側で関係する情報を解放することができます。また、ルート情報をテキストファイルから読み込む機能なども、このクラスで実装すると便利です。最終的に下記のようなクラス宣言になります。
unit Move;
interface
uses
Classes,
F8OpenGL,
PluginCore;
type
// ルートで1つのノードを保存するレコード
// ここではF8OpenGLで宣言されている
// ポイントタイプを使用します。
RoutePointType = record
time: double; // 時刻
position: GLPointType; // 3D座標
orientation: GLPointType; // 3Dベクトル
end;
// キャラクタインスタンスとルート情報を対応させるクラス
MoveClass = class
private
// 現在管理するキャラクタインスタンスのインタフェース
p_Instance: IF8MovingObjectInstance;
// ルートの中で現在の姿勢情報インデックスを保持する変数
currentPointIndex: integer;
// ルートの中で現在の時刻を保持する変数
time: double;
// ルートの中身を保持する変数
route: array of RoutePointType;
// キャラクタインスタンスを割り当てる際に呼び出す関数。
// この関数は以下宣言されているMyInstanceプロパティ
// 代入時に自動的に呼ばれます。
procedure SetInstance
(const Value: IF8MovingObjectInstance);
public
// オブジェクトの生成
constructor Create(source: TStrings);
// オブジェクトの開放
destructor Destroy; override;
// UC-win/Road本体が動きの計算を要求する際に
// 呼び出す関数。
procedure Move(dTimeInSeconds: double; Instance:
IF8MovingObjectInstance);
// UC-win/Road本体側でキャラクタインスタンスが
// 削除される際に呼び出す関数。
procedure InstanceDestroy(Instance: IF8DBObject);
// 初期化に用いる関数。最初の位置に戻します。
function GetFirstPosition: GLPointType;
// 現在対応するキャラクタインスタンスにアクセスするための
// プロパティ。
property MyInstance
: IF8MovingObjectInstance read p_Instance
write SetInstance;
|
このクラスのメソッドの実装は以下の通りになります。
implementation
uses
SysUtils;
{ MoveClass }
// ===================================================
// オブジェクトの生成と初期化
// 初期化ではCSVファイルから時刻と姿勢情報を読み込んで
// ルート変数にデータを保持します。
// CSVファイルの形式は下記の通り。
// 時刻,位置のX座標,位置のY座標,位置のZ座標,ヨー角,
// ピッチ角,ロール角
// 例:
// 0,5000,2,5000,1.5707963267948966192313216916398,0,0
// 5,5005,3,5000,0,0,0
// 10,5005,4,5005,0.78539816339744830961566084581988,0,0
// 15,5010,5,5010,-2.3561944901923449288469825374596,0,0
//20,5000,2,5000,1.5707963267948966192313216916398,0,0
// ==================================================
constructor MoveClass.Create(source: TStrings);
var
i: integer;
node: RoutePointType;
begin
// メンバーの初期化
time := 0;
currentPointIndex := 0;
p_Instance := nil;
// CSVファイルの中身の読み込み
source.CommaText := source.Text;
SetLength(route, source.Count div 7);
i := 0;
while i < source.Count do
begin
node.time := StrToFloat(source[i]);
Inc(i);
node.position[_x] := StrToFloat(source[i]);
Inc(i);
node.position[_y] := StrToFloat(source[i]);
Inc(i);
node.position[_z] := StrToFloat(source[i]);
Inc(i);
node.orientation[_x] := StrToFloat(source[i]);
Inc(i);
node.orientation[_y] := StrToFloat(source[i]);
Inc(i);
node.orientation[_z] := StrToFloat(source[i]);
Inc(i);
route[i div 7 - 1] := node;
end;
end;
// ==================================================
// オブジェクトの解放
// ==================================================
destructor MoveClass.Destroy;
begin
MyInstance := nil;
end;
// ==================================================
// 初期化に使用する関数。最初の位置を戻します。
// ==================================================
function MoveClass.GetFirstPosition: GLPointType;
begin
if High(route) >= 0 then
result := route[0].position;
end;
// ==================================================
// UC-win/Road本体側でキャラクタインスタンスが
// 削除される時に呼ぶ関数です。
// キャラクタインスタンスのインタフェースを解放します。
// ==================================================
procedure MoveClass.InstanceDestroy
(Instance: IF8DBObject);
begin
p_Instance := nil;
end;
// ==================================================
// UC-win/Road本体が動きの計算を要求する時に呼ぶ関数。
// あらかじめ読み込んだルートデータから位置を計算し
// キャラクタインスタンスに反映させます。
// ==================================================
procedure MoveClass.Move(dTimeInSeconds: double;
Instance: IF8MovingObjectInstance);
function LookForNode: boolean;
var
i: integer;
begin
result := false;
// 現在の位置からルートの中で該当する位置を検索。
// 見つかったら、現在の位置を更新します。
for i := currentPointIndex to High(route) - 1 do
begin
if (route[i].time <= time) and (route[i + 1].time >
time) then
begin
currentPointIndex := i;
result := true;
Break;
end;
end;
end;
var
// 位置の計算に使う変数。
position: GLPointType;
p1: GLPointType;
p2: GLPointType;
(以下、省略) |
以上のような記述になります。
最終的に開発するプラグイン側では、キャラクタインスタンスの生成、削除などを管理するプログラムが必要となりますが、ここでは省略します。
体験セミナーのお知らせ
|
|
(Up&Coming '12 夏の号掲載) |
|
|
>> 製品総合カタログ
>> プレミアム会員サービス
>> ファイナンシャルサポート
|