A group project for the Virtual Environments assignment during my
MSc.
Team members:
Eleni Maria Stea,
Domna Banakou, Hayden Lee, Eleni Kokkinara
Screnshots: here.
We used the 3D Studio Max for the modeling part and the XVR scripting language to create a project appropriate for
the CAVE environment.
Students, who are the main end users
of the application, are given the opportunity to attend a virtual class during
which they are able to put together or apart the basic parts of a human skeleton,
using a tracker device. They are able to move around the virtual
room and observe the skeleton from different view points, offering the sense of
looking at a 3D x-ray. The entire background of the class provides them
with useful information on how to complete their task.
Since the application combines the teaching efficiency of a traditional class with
the joy of a game, it could be used as a teaching supplement, promoting an
intuitive understanding of the human anatomy.
Assignment aims and approach:
The primary objective of the application was to make the user interface as
friendly and intuitive as possible. For that reason, the whole virtual environment
was designed to satisfy certain requirements:
- The virtual environment should reminded the students that their task was not
only an entertainment game but an alternative way of learning.
- The use of the system shouldn't have been complex or confusing, otherwise it
wouldn't have been attractive to the end users.
- The users should receive feedback on when they were completing their task
successfully or not. That way they would actively participate to the virtual
course and been able to get a final result by themselves.
- The users should been able to interrupt and restart the application at any
time.
Our approach to satisfy the previous aims was the following:
- The virtual environment was designed to look like a traditional classroom
in order to satisfy the first aim.
- The headtracker was used for navigating around the classroom and only 1
button of the wand device is needed for selecting and moving the skeleton
parts. A second button is used for the scattering of bones. Thus, the
second aim is satisfied.
- We used two different colors (red and green) to indicate the correct and
the wrong placement of a bone in order to satisfy the third aim.
- The user can press a button to scatter the bones everywhere at anytime
so that the fourth aim is satisfied.
Implementation:
3D Studio Max Modelling
The scene and a number of objects of the virtual environment were created
or modified in 3D Studio Max. First we constructed the virtual classroom
where the model of the skeleton was placed. Then, we added some helpful
and decorative tables to make the classroom more realistic.
For the skeleton model a lot of preprocessing was needed. Since
it consisted of many parts, we first had to group them to reduce
their number. We also had to minimize the number of polygons to
optimise the performance of the application. Then, we exported each
grouped skeleton part to separate .aam files, after writing down their
positions in the 3DS model (see XVR coding).
The scene, the blackboards and the skeleton picture were modeled
in 3D Studio and different textures were applied afterwards; we used
only a few polygons in order to achieve a faster execution.
XVR Programming
In the begining, we had to load each object in the correct size and position
relatively to the CAVE and set the camera position which was different
for the CAVE and the non-CAVE interface.
In order to place the bones in the correct position to look like a full
skeleton, we had to use the positions extracted from 3D Studio Max af-
ter inversing the z coordinate with the -y. The size of each object was
calculated comparing it to other CAVE examples; since we wanted to be
able to change it during testing, we set the size of each object relatively to
the scene size and we added two buttons that could increase or decrease a
scaling factor, apply the scaling and print the scaling factor value to the
XVR console as seen in Figure 8.
In order to scatter and place the bones in random positions we used a
random value generator that calculates random distances from the initial
position for each part when a button is pressed. This distance is always
smaller than the room size so that the bones are always inside the virtual
classroom as seen in Figure 1. The only bone that never been moved from
its initial position is the scull.
For the interaction between the user and the environment, a simple vir-
tual cursor was used. The users are able to move the 3d cursor in order
to grab the bones and position them correctly. They are allowed to pick
up a bone when its bounding box (see Figure 6) and that of the cursor
intersect. When this occurs, the bone is highlighted to give feedback to
the users regarding which bone they are about to grab as seen in Figure
4.
The correct placement of a bone is determined by its distance from the
ideal position, which is the initial one when the skeleton is complete as
seen in Figure 5. If the bone is placed near enough to the ideal location,
the program snaps it into that automatically. A threshold is set in order
to specify the maximum accepted distance from the initial position. This
threshold can be changed the same way as the scaling factor. By modify-
ing the threshold we can adjust the difficulty level of the task since a very
low threshold value requires greater accuracy as seen in Figure 7.
When a bone is dropped somewhere in the scene, it is highlighted. Two
different colors are used: red to indicate a wrong placement and green to
indicate a correct one as seen in Figures 2, 3 and 4.
Screenshots:
Figure 1: The skeleton bones are scattered.
Figure 2: The user placed the torso in the correct position.
Figure 3: The user left the bone in a wrong position (red highlight).
Figure 4: The user left the bone close to the correct position.
Figure 5: The task is completed.
Figure 6: Skeleton bounding boxes.
Figure 7: Bone position accepted as correct although not for
very large
threshold values.
Figure 8: The skeleton size is changed when interactively
changing the scaling
factor.
Figure 9: The skeleton inside the CAVE.
Back to top
Code:
#include <Script3d.h>
SET SCENE_FOV = 60;
SET SCENE_NEAR = 0.5;
SET SCENE_FAR = 1000;
var in_cave = true;
var lt0, lt1;
var room_obj;
var room_mesh;
var bboard_obj;
var bboard_mesh;
var bboardskel_obj;
var bboardskel_mesh;
var bboardex_obj;
var bboardex_mesh;
var ptr_obj;
var ptr_mesh;
var ptr_pos = [0, 0, 0];
var show_bbox = false;
var skel_scale = 0.005;
var bboard_scale = 0.006;
var bboardskel_scale = 0.006;
var bboardex_scale = 0.004;
var room_scale = 1;
var snap_thres = 0.05;
var picked_obj = -1;
var hl_obj = -1;
var skel_offs = [0, 0.8, -1.0];
var skel_part_pos = {
[-0.012, 281.258, -7.303],
[-0.012, 210.655, -6.371],
[43.862, 134.749, -4.115],
[-44.692, 134.749, -4.115],
[15.634, 47.491, -12.756],
[-15.658, 47.491, -12.756],
[17.09, 121.581, -7.288],
[-17.115, 121.581, -7.288],
[43.653, 173.607, -13.665],
[-44.488, 173.607, -13.665],
[36.548, 218.049, -13],
[-37.378, 218.049, -13],
[18.845, 7.34, -3.191],
[-18.87, 7.34, -3.191]
};
var skel_part_names = {
"head.aam",
"body.aam",
"lhand.aam",
"rhand.aam",
"llleg.aam",
"rlleg.aam",
"luleg.aam",
"ruleg.aam",
"llarm.aam",
"rlarm.aam",
"luarm.aam",
"ruarm.aam",
"lfoot.aam",
"rfoot.aam"
};
var skel_parts = Array(0);
function find_collision();
function target_pos(idx);
function correct(idx);
function update_skeleton();
function scatter();
function CameraMoveMouse();
function MoveControl();
function DrawGrid(col, size);
function OnDownload()
{
}
function OnInit(params)
{
setLocalDir();
SceneSetParam(VR_HEADTRACKER,1);
lt0 = CVmLight();
lt0.SetPosition([0.0, 3.0, 0.0]);
lt0.SetDiffuse(0.8, 0.8, 0.8);
lt0.SetSpecular(0.8, 0.8, 0.8);
lt0.Enable();
lt1 = CVmLight();
lt1.SetPosition([0.0,3.0,0.4]);
lt1.SetDiffuse(0.5, 0.5, 0.5);
lt1.SetSpecular(0.5, 0.5, 0.5);
lt1.Enable();
var PosC=[0.0,0.0,0.0];
if(!in_cave) {
PosC= [0.0,0.6,3.0];
CameraSetPosition(PosC);
var InititialCameraDirection = [0.0, -0.10, -1.0];
CameraSetDirection(InititialCameraDirection);
}
ptr_mesh = CVmNewMesh("data/ptr.aam");
ptr_obj=CVmObj();
ptr_obj.setScale(0.05);
ptr_obj.LinkToMesh(ptr_mesh);
bboard_mesh = CVmNewMesh("data/bboard.aam");
bboard_obj = CVmObj();
bboard_obj.setScale(room_scale * bboard_scale);
bboard_obj.LinkToMesh(bboard_mesh);
bboard_obj.setPosition(0, 0.4, 0);
bboard_obj.setRotation(90, [1, 0, 0]);
bboardskel_mesh = CVmNewMesh("data/bboardskel.aam");
bboardskel_obj = CVmObj();
bboardskel_obj.setScale(room_scale * bboardskel_scale);
bboardskel_obj.LinkToMesh(bboardskel_mesh);
bboardskel_obj.setPosition(-1.5, 0.4, 1.5);
bboardskel_obj.Rotate(90, 1, 0, 0);
bboardskel_obj.Rotate(-90, 0, 0, 1);
bboardex_mesh = CVmNewMesh("data/bboardex.aam");
bboardex_obj = CVmObj();
bboardex_obj.setScale(room_scale * bboardex_scale);
bboardex_obj.LinkToMesh(bboardex_mesh);
bboardex_obj.setPosition(1.5, 0.3, 1.5);
bboardex_obj.Rotate(90, 0, 0, 1);
room_mesh = CVmNewMesh("data/room3.aam");
room_obj = CVmObj();
room_obj.LinkToMesh(room_mesh);
room_obj.setScale(room_scale);
room_obj.setPosition(0, -1, 1.5);
for(var i=0; i<len(skel_part_names); i++) {
var mesh = CVmNewMesh("data/" + skel_part_names[i]);
var obj = CVmObj();
obj.LinkToMesh(mesh);
obj.setScale(skel_scale);
obj.setPosition(skel_part_pos[i] * skel_scale - skel_offs * room_scale);
aadd(skel_parts, obj);
}
glEnable(GL_NORMALIZE);
}
function OnFrame()
{
SetClearColor(0,0,0);
SceneBegin();
DrawGrid([0.5, 0.5, 0.5], 100);
room_obj.Draw();
bboard_obj.Draw();
bboardskel_obj.Draw();
bboardex_obj.Draw();
ptr_obj.Draw(VR_CALC_MATRIX);
if(show_bbox) {
ptr_obj.DrawBoundingBox();
}
for(var i=0; i<len(skel_parts); i++) {
skel_parts[i].Draw(VR_CALC_MATRIX);
if(hl_obj == i) {
if(correct(i)) {
skel_parts[i].ModulateMaterials(0.3, 1.0, 0.5);
} else {
skel_parts[i].ModulateMaterials(1.0, 0.5, 0.3);
}
} else {
skel_parts[i].ModulateMaterials(1.0, 1.0, 1.0);
}
if(show_bbox) {
skel_parts[i].DrawBoundingBox(VR_NOSHADING);
}
}
SceneEnd();
}
function MoveControl()
{
var bn_press = false;
if(in_cave) {
ptr_pos = getTrackerPos(1);
if(getTrackerButton(1) == 1) {
bn_press = true;
}
if(getTrackerButton(1) == 8) {
scatter();
}
}
else {
var move_dist = 0.02;
var pressedKey = Keyboard();
switch(pressedKey){
case "a":
ptr_pos.x -= move_dist;
break;
case "d":
ptr_pos.x += move_dist;
break;
case "w":
ptr_pos.y += move_dist;
break;
case "s":
ptr_pos.y -= move_dist;
break;
case "e":
ptr_pos.z -= move_dist;
break;
case "c":
ptr_pos.z += move_dist;
break;
case "b":
show_bbox = !show_bbox;
break;
case "-":
skel_scale *= 0.9;
if(skel_scale < 0.0) {
skel_scale = 0.0;
}
update_skeleton();
outputln("skeleton scale: ", skel_scale);
break;
case "=":
skel_scale *= 1.1;
update_skeleton();
outputln("skeleton scale: ", skel_scale);
break;
case "[":
room_scale *= 0.9;
if(room_scale < 0.0) {
room_scale = 0.0;
}
room_obj.setScale(room_scale);
room_obj.setPosition(0, -room_scale, 0);
update_skeleton();
outputln("room scale: ", room_scale);
break;
case "]":
room_scale *= 1.1;
room_obj.setScale(room_scale);
room_obj.setPosition(0, -room_scale, 0);
update_skeleton();
outputln("room scale: ", room_scale);
break;
case ",":
snap_thres *= 0.9;
outputln("snap threshold: ", snap_thres);
break;
case ".":
snap_thres *= 1.1;
outputln("snap threshold: ", snap_thres);
break;
case " ":
bn_press = true;
break;
case "\b":
case "\n":
case "\r":
scatter();
break;
}
}
if(bn_press) {
if(picked_obj == -1) {
picked_obj = find_collision();
} else {
if(correct(picked_obj)) {
skel_parts[picked_obj].setPosition(target_pos(picked_obj));
}
picked_obj = -1;
}
}
ptr_obj.setPosition(ptr_pos);
if(picked_obj != -1) {
skel_parts[picked_obj].setPosition(ptr_pos);
} else {
hl_obj = find_collision();
}
}
function DownloadReady(RequestID)
{
}
function OnTimer()
{
if(!in_cave) {
CameraMoveMouse();
}
MoveControl();
}
function OnExit()
{
}
function find_collision()
{
for(var i=1; i<len(skel_parts); i++) {
if(skel_parts[i].IsCollidingBBox(ptr_obj)) {
return i;
}
}
return -1;
}
function target_pos(idx)
{
return skel_part_pos[idx] * skel_scale - skel_offs * room_scale;
}
function correct(idx)
{
var pos = skel_parts[idx].getPosition();
var targ = target_pos(idx);
var dif = targ - pos;
if(dif.x * dif.x + dif.y * dif.y + dif.z * dif.z < snap_thres * snap_thres) {
return true;
}
return false;
}
function update_skeleton()
{
for(var i=0; i<len(skel_parts); i++) {
skel_parts[i].setScale(skel_scale);
skel_parts[i].setPosition(skel_part_pos[i] * skel_scale - skel_offs * room_scale);
}
}
function randvec()
{
return [tofloat(rand(8192) - 4096) / 4096.0, tofloat(rand(8192) - 4096) / 4096.0, tofloat(rand(8192) - 4096) / 4096.0];
}
function scatter()
{
for(var i=1; i<len(skel_parts); i++) {
skel_parts[i].translate(randvec() * [room_scale * 1.0, room_scale * 0.9, room_scale * 0.8] * 0.5);
}
}
function CameraMoveMouse()
{
static var InMouseR = false, InMouseL = false;
static var PrecX = 0, PrecY = 0;
var TR_SENSITIVITY = 0.0001 ;
var ROT_SENSITIVITY = 0.01;
if(Mouse.ButtonL && !Mouse.ButtonR)
{
if (InMouseL)
{
CameraRotate(( Mouse.X-PrecX)*ROT_SENSITIVITY,0,1,0);
CameraRotateABS((Mouse.Y-PrecY)*ROT_SENSITIVITY,1,0,0);
}
else
{
PrecX = Mouse.X;
PrecY = Mouse.Y;
}
InMouseL = true;
InMouseR = false;
}
else
if(Mouse.ButtonR)
{
if (InMouseR)
{
var CameraMatrix = CameraGetMatrix();
var CameraPos = CameraGetPosition();
if (!Mouse.ButtonL)
CameraPos += CameraGetZAxis() * (Mouse.y-PrecY)*TR_SENSITIVITY + CameraGetXAxis() * (Mouse.X- PrecX)*TR_SENSITIVITY;
else
CameraPos -= CameraGetYAxis() * (Mouse.y-PrecY)*TR_SENSITIVITY;
CameraSetPosition(CameraPos);
}
else
{
PrecX = Mouse.X;
PrecY = Mouse.Y;
}
InMouseR = true;
InMouseL = false;
}
else
{
InMouseR = false;
InMouseL = false;
}
}
function DrawGrid(col, size)
{
glPushAttrib(GL_LIGHTING_BIT | GL_LINE_BIT | GL_CURRENT_BIT);
glLineWidth(1);
glDisable(GL_LIGHTING);
glColor(col);
var max = size / 2.0;
var min = -max;
var step = size / 10.0;
glBegin(GL_LINES);
for (var i = min; i <= max; i += step) {
glVertex(i, 0, max);
glVertex(i, 0, min);
glVertex(max, 0, i);
glVertex(min, 0, i);
}
glEnd();
glPopAttrib();
}
function onError()
{
}
function onEvent()
{
}
The skeleton model was downloaded from here.
|