COVISE and OpenCOVER support
Lab hardware
We are using Suse Linux 10.0 on most of our lab machines. The names of the lab machines you have an account on are:
- coutsound.ucsd.edu: AMD Opteron based file server and computer connected to dual-head setup next to projectors. Use this machine to compile and to change your password. Let me know when you change your password so that I can update it on the rest of the lab machines.
- visualtest01.ucsd.edu: AMD Opteron based, drives lower half of c-wall
- visualtest02.ucsd.edu: AMD Opteron based, drives upper half of c-wall
- flint.ucsd.edu: Intel Pentium D based Dell in terminal room
- chert.ucsd.edu: Intel Pentium D based Dell in terminal room
- basalt.ucsd.edu: Intel Pentium D based Dell in terminal room
- rubble.ucsd.edu: Intel Pentium D based Dell in terminal room
- slate.ucsd.edu: Intel Pentium D based Dell in terminal room
Due to the different computer hardware we use in the terminal room and the cave room, you should always compile your plugins on coutsound. All these machines can be accessed from the public internet. You can log in with your username by using ssh. Example for user 'jschulze' logging into 'flint': ssh jschulze@flint.ucsd.edu
Account information
In order to cross login between lab machines without a password you will need to create a DSA key pair. To generate this you run the command 'ssh-keygen -t dsa' on any lab machine and use the default file names, and do not enter a pass phrase. This should generate two files in your ~/.ssh/ directory: id_dsa and id_dsa.pub. The last step is to copy the file id_dsa.pub to new file authorized_keys, or append the contents of id_dsa.pub to authorized_keys if it already exists.
General information about Covise modules
The entire Covise installation, including all plugins, is located at ~jschulze/covise/. Each user should have a link in their home directory named 'covise' which points to this directory. There should also be a link 'plugins' which points to the plugins directory. You should put all the plugins you write in this course into the directory: plugins/cse291/.
Other directories you might need throughout the project are:
- covise/src/renderer/OpenCOVER/kernel/: core OpenCOVER functionality, especially coVRModuleSupport.cpp
- covise/src/kernel/OpenVRUI/: OpenCOVER's user interface elements. useful documentation in doc/html subdirectory; browse by running Firefox on index.html
- covise/src/renderer/OpenCOVER/osgcaveui/: special CaveUI functions, not required in class but useful
You compile your plugin with the 'make' command. This creates a library file in covise/amd64/lib/OpenCOVER/plugins/. Covise uses qmake, so the make file is being generated by the .pro file. The name of the plugin is determined by the project name in the .pro file in your plugin directory (first line, keyword TARGET). I defaulted TARGET to be p1<username> for project #1. It is important that the TARGET name be unique, or else you will overwrite somebody else's plugin. You can change the name of your source files or add additional source files (.cpp,.h) by listing them after the SOURCES tag in the .pro file.
You run OpenCOVER by typing 'opencover' anywhere at the command line. You quit opencover by hitting the 'q' key on the keyboard or ctrl-c in the shell window you started it from.
Good examples for plugins are plugins/Volume and plugins/PDBPlugin. Look at the code in these plugins to find out how to add menu items and how to do interaction. Note that there are two ways to do interaction: with pure OpenCOVER routines, or with OSGCaveUI. In this course we will try to use only OpenCOVER's own routines. Plugins do not get loaded by opencover before they are configured in the configuration file.
Important Directories
- ~/covise/config: configuration files
- ~/plugins: plugin code
- /home/jschulze/svn/extern_libs/amd64/OpenSceneGraph-svn/OpenSceneGraph: OpenSceneGraph installation directory
- http://www.openscenegraph.org: main OSG web site
- http://openscenegraph.org/archiver/osg-users: OSG email archive. If you have an OSG problem, this is a good place to start.
Covise configuration files
The configuration files are in the directory $HOME/covise/config/. In class, the only files you need to look at are:
- config.ucsd.xml: general configuration information for all lab machines, except the c-wall machines
- config.calitcwall.xml: C-wall specific configuration information (visualtest02.ucsd.edu)
- config.calitvarrier.xml: Varrier specific configuration information (varrier.ucsd.edu)
The configuration files are XML files which can be edited with any ASCII text editor (vi, emacs, nedit, gedit, ...). There are sections specific for certain machines. To load your plugin (e.g., p1jschulze) on one or more machines (e.g., chert and basalt), you need to add or modify a section to contain:
<LOCAL host="chert,basalt"> <COVERConfig> <Module value="p1jschulze" name="p1jschulze"/> </COVERConfig> </LOCAL>
Debugging OpenCover Plugins
OpenCover code can be debugged with gdb. If it throws a 'Segmentation Fault' make sure the core is getting dumped with 'unlimit coredumpsize'. Then you should find a file named 'core' or 'core.<pid>' in the directory you are running opencover from. Let's assume your latest core file is called core.4567 then you can run gdb with:
- gdb ~/covise/amd64/bin/Renderer/OpenCOVER core.4567
RETURN through the startup screens until you get a command prompt. The two most important commands are:
- bt: to display the stack trace. The topmost call is the one which caused the segmentation fault.
- quit: to quit gdb
Documentation for gdb is at: http://sourceware.org/gdb/documentation/
Intersection testing
If you have wondered how you can find out if the wand pointer intersects your objects, here is a template routine for it. You need to pass it the beginning and end of a line you're intersecting with, in world coordinates. The line will be starting from the hand position and extend along the Y axis.
#include <osgUtil/IntersectVisitor> class IsectInfo // this is an optional class to illustrate the return values of the accept() function { public: bool found; ///< false: no intersection found osg::Vec3 point; ///< intersection point osg::Vec3 normal; ///< intersection normal osg::Geode *geode; ///< intersected Geode }; void getObjectIntersection(osg::Node *root, osg::Vec3& wPointerStart, osg::Vec3& wPointerEnd, IsectInfo& isect) { // Compute intersections of viewing ray with objects: osgUtil::IntersectVisitor iv; osg::ref_ptr<osg::LineSegment> testSegment = new osg::LineSegment(); testSegment->set(wPointerStart, wPointerEnd); iv.addLineSegment(testSegment.get()); iv.setTraversalMask(2); // Traverse the whole scenegraph. // Non-Interactive objects must have been marked with setNodeMask(~2): root->accept(iv); isect.found = false; if (iv.hits()) { osgUtil::IntersectVisitor::HitList& hitList = iv.getHitList(testSegment.get()); if(!hitList.empty()) { isect.point = hitList.front().getWorldIntersectPoint(); isect.normal = hitList.front().getWorldIntersectNormal(); isect.geode = hitList.front()._geode.get(); isect.found = true; } } }
Tracker Data
Here is a piece of code to get the pointer (=wand) position (pos1) and a point 1000 millimeters from it (pos2) along the pointer line:
osg::Vec3 pointerPos1Wld = cover->getPointerMat().getTrans(); osg::Vec3 pointerPos2Wld = osg::Vec3(0.0, 1000.0, 0.0); pointerPos2Wld = pointerPos2Wld * cover->getPointerMat();
This is the way to get the head position:
Vec3 viewerPosWld = cover->getViewerMat().getTrans();
Interaction handling
To register an interaction so that only your plugin uses the mouse pointer while a button on the wand is pressed, you want to use the TrackerButtonInteraction class. For sample usage see plugins/Volume. Here are the main calls you will need. Most of these functions go in the preFrame routine, unless otherwise noted:
- Make sure you include at the top of your code:
#include <OpenVRUI/coTrackerButtonInteraction.h>
- In the constructor you want to create your interaction for button A, which is the left wand button:
interaction = new coTrackerButtonInteraction(coInteraction::ButtonA,"MoveObject",coInteraction::Menu);
- In the destructor you want to call:
delete interaction;
- To register your interaction and thus disable button A interaction in all other plugins call the following function. Make sure that to call this function before other modules can register the interaction. In particular, this might mean that you need to register the interaction before a mouse button is pressed, for instance by registering it when intersecting with the object to interact with.
if(!interaction->registered) { coInteractionManager::the()->registerInteraction(interaction); }
- To do something just once, after the interaction has just started:
if(interaction->wasStarted()) { }
- To do something every frame while the interaction is running:
if(interaction->isRunning()) { }
- To do something once at the end of the interaction:
if(interaction->wasStopped()) { }
- To unregister the interaction and free button A for other plugins:
if(interaction->registered && (interaction->getState()!=coInteraction::Active)) { coInteractionManager::the()->unregisterInteraction(interaction); }
OSG Text
Make sure you #include <osgText/Text>. There is a good example for how osgText can be used in ~/covise/src/renderer/OpenCOVER/osgcaveui/Card.cpp. _highlight is the osg::Geode the text gets created for, createLabel() returns the Drawable with the text, _labelText is the text string, and osgText::readFontFile() reads the font.
How to Create a Rectangle with a Texture
The following code sample from osgcaveui/Card.cpp demonstrates how to create a rectangular geometry with a texture.
Geometry* Card::createIcon() { Geometry* geom = new Geometry(); Vec3Array* vertices = new Vec3Array(4); float marginX = (DEFAULT_CARD_WIDTH - ICON_SIZE * DEFAULT_CARD_WIDTH) / 2.0; float marginY = marginX; // bottom left (*vertices)[0].set(-DEFAULT_CARD_WIDTH / 2.0 + marginX, DEFAULT_CARD_HEIGHT / 2.0 - marginY - ICON_SIZE * DEFAULT_CARD_WIDTH, EPSILON_Z); // bottom right (*vertices)[1].set( DEFAULT_CARD_WIDTH / 2.0 - marginX, DEFAULT_CARD_HEIGHT / 2.0 - marginY - ICON_SIZE * DEFAULT_CARD_WIDTH, EPSILON_Z); // top right (*vertices)[2].set( DEFAULT_CARD_WIDTH / 2.0 - marginX, DEFAULT_CARD_HEIGHT / 2.0 - marginY, EPSILON_Z); // top left (*vertices)[3].set(-DEFAULT_CARD_WIDTH / 2.0 + marginX, DEFAULT_CARD_HEIGHT / 2.0 - marginY, EPSILON_Z); geom->setVertexArray(vertices); Vec2Array* texcoords = new Vec2Array(4); (*texcoords)[0].set(0.0, 0.0); (*texcoords)[1].set(1.0, 0.0); (*texcoords)[2].set(1.0, 1.0); (*texcoords)[3].set(0.0, 1.0); geom->setTexCoordArray(0,texcoords); Vec3Array* normals = new Vec3Array(1); (*normals)[0].set(0.0f, 0.0f, 1.0f); geom->setNormalArray(normals); geom->setNormalBinding(Geometry::BIND_OVERALL); Vec4Array* colors = new Vec4Array(1); (*colors)[0].set(1.0, 1.0, 1.0, 1.0); geom->setColorArray(colors); geom->setColorBinding(Geometry::BIND_OVERALL); geom->addPrimitiveSet(new DrawArrays(PrimitiveSet::QUADS, 0, 4)); // Texture: StateSet* stateset = geom->getOrCreateStateSet(); stateset->setMode(GL_LIGHTING, StateAttribute::OFF); stateset->setRenderingHint(StateSet::TRANSPARENT_BIN); stateset->setTextureAttributeAndModes(0, _icon, StateAttribute::ON); return geom; }
Load and display an image from disk
Here is sample code to create a geode (imageGeode) with an image which gets loaded from disk. OpenGL's size limitation for textures applies (usually 4096x4096 pixels). The image might have to have powers of two edges, but that limitation should not hold anymore on newer graphics cards.
Geode* createImageGeode() { // Create OSG image: Image* image = new Image(); image = osgDB::readImageFile(filename); // Create OSG texture: imageTexture = new Texture2D(); if (image) imageTexture->setImage(image); else std::cerr << "Cannot load image file " << filename << std::endl; // Create OSG geode: imageGeode = new Geode(); imageGeode->addDrawable(createImageGeometry()); return imageGeode; } Geometry* createImageGeometry() { const float WIDTH = 3.0f; const float HEIGHT = 2.0f; Geometry* geom = new Geometry(); // Create vertices: Vec3Array* vertices = new Vec3Array(4); (*vertices)[0].set(-WIDTH / 2.0, HEIGHT / 2.0, 0); // bottom left (*vertices)[1].set( WIDTH / 2.0, HEIGHT / 2.0, 0); // bottom right (*vertices)[2].set( WIDTH / 2.0, HEIGHT / 2.0, 0); // top right (*vertices)[3].set(-WIDTH / 2.0, HEIGHT / 2.0, 0); // top left geom->setVertexArray(vertices); // Create texture coordinates for image texture: Vec2Array* texcoords = new Vec2Array(4); (*texcoords)[0].set(0.0, 0.0); (*texcoords)[1].set(1.0, 0.0); (*texcoords)[2].set(1.0, 1.0); (*texcoords)[3].set(0.0, 1.0); geom->setTexCoordArray(0,texcoords); // Create normals: Vec3Array* normals = new Vec3Array(1); (*normals)[0].set(0.0f, 0.0f, 1.0f); geom->setNormalArray(normals); geom->setNormalBinding(Geometry::BIND_OVERALL); // Create colors: Vec4Array* colors = new Vec4Array(1); (*colors)[0].set(1.0, 1.0, 1.0, 1.0); geom->setColorArray(colors); geom->setColorBinding(Geometry::BIND_OVERALL); geom->addPrimitiveSet(new DrawArrays(PrimitiveSet::QUADS, 0, 4)); // Set texture parameters: StateSet* stateset = geom->getOrCreateStateSet(); stateset->setMode(GL_LIGHTING, StateAttribute::OFF); // make texture visible independent of lighting stateset->setRenderingHint(StateSet::TRANSPARENT_BIN); // only required for translucent images stateset->setTextureAttributeAndModes(0, imageTexture, StateAttribute::ON); return geom; }
Wall Clock Time
If you are going to animate anything, keep in mind that the rendering system runs anywhere between 1 and 100 frames per second, so you can't rely on the time between frames being anything you assume. Instead, you will want to know exactly how much time has passed since you last rendered something, i.e., you last preFrame() call. You should use cover->frameTime(), or better cover->frameDuration(); these return a double value with the number of seconds (at an accuracy of milli- or even microseconds) passed since the start of the program, or since the last preFrame(), respectively.
Backtrack all nodes up to top of scene graph
osg::NodePath path = getNodePath(); ref_ptr<osg::StateSet> state = new osg::StateSet; for (osg::NodePath::iterator it = path.begin(); it != path.end(); ++it) { if ((*it)->getStateSet()) { state->merge((*it)->getStateSet()); } }
Taking a screenshot from the command line
- run application on visualtest02 and bring up desired image, freeze head tracking
- log on to coutsound
- Make sure screenshot is taken of visualtest02: setenv DISPLAY :0.0
- Take screenshot: xwd -root -out <screenshot_filename.xwd>
- Convert image file to TIFF: convert <screenshot_filename.xwd> <screenshot_filename.tif>
Taking a Screenshot from within OpenCOVER
osg::camera allows you to take a screenshot at higher than physical display resolution. Here is an example from the email thread at http://openscenegraph.org/archiver/osg-users/2007-April/0054.html using wxWindows.
//save the old viewport: osg::ref_ptr<osg::Viewport> AlterViewport = sceneView->getViewport(); osg::ref_ptr<osg::Image> shot = new osg::Image(); //shot->setPixelFormat(GL_RGB); int w = 0; int h = 0; GetClientSize(&w, &h); float ratio = (float)w/(float)h; w = 2500; h = (int)((float)w/ratio); //shot->scaleImage(w, h, 24); shot->allocateImage(w, h, 24, GL_RGB, GL_UNSIGNED_BYTE); osg::Node* subgraph = TheDocument->RootGroup.get(); osg::ref_ptr<osg::Camera> camera = new osg::Camera(*(sceneView->getCamera()) ); // set view camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); // set viewport camera->setViewport(0,0,w,h); // set the camera to render before the main camera. camera->setRenderOrder(osg::Camera::PRE_RENDER); // tell the camera to use OpenGL frame buffer object where supported. camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); camera->attach(osg::Camera::COLOR_BUFFER, shot.get()); // add subgraph to render camera->addChild(subgraph); //Need to mage it part of the scene : sceneView->setSceneData(camera); //Make it frame: Update(); Refresh(); wxImage img; img.Create(w, h); img.SetData(shot->data()); shot.release(); wxImage i2 = img.Mirror(false); i2.SaveFile(filename); sceneView->setSceneData(subgraph); sceneView->setViewport(AlterViewport.get() );
Return a ref_ptr from a function
Here is a safe way to return a ref_ptr type from a function.
osg::ref_ptr<osg::Group> makeGroup(...Some arguments..) { osg::ref_ptr<osg::MatrixTransform> mt=new MatrixTransform(); // ...some operations... return mt.get(); }
Also check out this link to learn more about how to use ref_ptr: http://donburns.net/OSG/Articles/RefPointers/RefPointers.html
Occlusion Culling in OpenSceneGraph
Occlusion culling removes objects which are hidden behind other objects in the culling stage so they never get rendered, thus resulting in a higher rendering rate. In covise/src/renderer/OpenCOVER/kernel/VRViewer.cpp, the SceneView is being created. By default CullingMode gets set like this:
osg::CullStack::CullingMode cullingMode = cover->screens[i].sceneView->getCullingMode(); cullingMode &= ~(osg::CullStack::SMALL_FEATURE_CULLING); cover->screens[i].sv->setCullingMode(cullingMode);
There are several types of culling options available. However, the easiest way to test your culling code would be to set the cullingMode to ENABLE_ALL_CULLING.
I will be posting my editable occluder code shortly which also fixes some of the 3ds osgExp problems listed below.
There isn't any way to automatically add occlusion culling to a scene, you'll need to insert convex planar occluders into your scene. See the osgoccluder example for inspiration.
If you have access to 3D Studio Max, you can find instructions on how to install and use the OSG exporter which gives you access to culling and LOD helpers for your 3D models. However, 3ds 9 is not stable with osgExp and will not allow you to have access to these occluderHelpers. I am unaware of any progress to improve osgExp for the newer versions of 3ds. If you choose this option, use it with the stable 3ds 8 or 7 with osgExp version 9.3.
Check out the osgoccluder example located in: svn/extern_libs/amd64/OpenSceneGraph-svn/OpenSceneGraph/examples
An alternative to occlusion culling is to use LOD (level of detail) nodes in the scene graph. This means that when you are farther away, less polygons get rendered. See the osglod example for inspiration.
Message Passing in OpenCOVER
This is how you can send a message from the master to all nodes in the rendering cluster. These functions are defined in covise/src/renderer/OpenCOVER/kernel/coVRMSController.h.
if(coVRMSController::instance()->isMaster()) { coVRMSController::instance()->sendSlaves((char*)&appReceiveBuffer,sizeof(receiveBuffer)); } else { coVRMSController::instance()->readMaster((char*)&appReceiveBuffer,sizeof(receiveBuffer)); }
The above functions make heavy use of the class coVRSlave (covise/src/renderer/OpenCOVER/kernel/coVRSlave.h). This class uses the Socket class to implement the communication between nodes. The Socket class can be used, using a different port, to implement communication between rendering nodes.
Notice that the above functions are for communication WITHIN a rendering cluster. In order to send a message to a remote OpenCOVER (running on another rendering cluster connected via a WAN) you would use cover->sendMessage. The source code for this function is at covise/src/renderer/OpenCOVER/kernel/coVRPluginSupport.h.
Moving an object with the pointer
Here is some sample code to move an object. object2w is the object's transformation matrix in world space. lastWand2w and wand2w are the wand matrices from the previous and current frames, respectively, from cover->getPointer().
void move(Matrix& lastWand2w, Matrix& wand2w) { // Compute difference matrix between last and current wand: Matrix invLastWand2w = Matrix::inverse(lastWand2w); Matrix wDiff = invLastWand2w * wand2w; // Perform move: _node->setMatrix(object2w * wDiff); }
A great tutorial page is at: http://www.ecst.csuchico.edu/~beej/guide/ipc/shmem.html
Find out which node a plugin is running on
The following routine works on our Varrier system to find out the host number, starting with 0. The node names are vellum1-10, vellum2-10, etc.
int getNodeIndex() { char hostname[33]; gethostname(hostname, 32); int node; sscanf(hostname, "vellum%d-10", &node); // this needs to be adjusted to naming convention return (node-1); // names start with 1 }
Hide or select different pointer
The pointer is by default a line coming out of the users's hand held device. Icons like an airplane, a steering wheel, slippers, or a magnifying glass indicate the current navigation mode. This is the mode the left mouse button will use. To hide the pointer, remove all geodes which are part of the pointer:
while(VRSceneGraph::instance()->getHandTransform()->getNumChildren()) VRSceneGraph::instance()->getHandTransform()->removeChild(m_handScaledTransform->getChild(0));
To select a different pre-defined pointer:
VRSceneGraph::instance()->setPointerType(<new_type>);