Environment: VC6
Introduction
This sample uses OpenGl to create a 3-D Scattergraph that the user can view from any angle by rotating with the mouse. The data can be shown with either orthographic or perspective projection. The data, and optionally the color of each point, are loaded from a text file. The application can be re-sized in the usual manner, and this re-sizes the graph. The slightly unusual feature is that the user can draw around points in the graph to select them, and then zoom the graph so as to magnify the region containing the selected points.
The project is an MFC dialog-based application. The dialog shows all the user-accessed controls, and it contains a CWnd-derived class that encapsulates all the graphing functionality.
In the figure above, the user has selected the cyan-coloured cluster of points by drawing around them after clicking Make in the Selection box. If the Zoom button were to be clicked, the graph would rescale so as to show only those points within the selected cluster. The whole dataset can be shown by selecting the Autoscale options for each dimension.
PLEASE NOTE: I do not claim to be an expert on OpenGL. I have coded this project using the OpenGL Programming Guide (3rd ed), other projects on the CodeGuru site as examples, and a whole load of trial and error. Feedback is welcomed!
Graphing Classes
class COpenGLWnd : public CWnd
This class is a generic base class for projects needing a CWnd-derived class to display OpenGL graphics. It draws heavily on the code architecture for the class GLEnabledView described in the Code Guru article "GL enabled view for MDI environment" by Alessandro Falappa, whose work I gratefully acknowledge. For simplicity I have removed some of the functionality (tessellator and quadric code) contained in that class because it was not needed for this project, but you can easily restore it by consulting that article.
The main additions/changes are as follows:
- COpenGLWnd derives from CWnd, rather than CView, and so can easily be incorporated into non-mdi/sdi projects.
- Optional text support has been added to the class, using the windows-specific wglUseFontBitmaps function. This is a lot easier than mapping out one’s own font bitmaps, which seems to be the standard OpenGL method. To activate text support call COpenGLWnd::MakeFont() at some point in the initialisation process. At present this just makes the sysem font available for use, but it would not be hard to extend this to allow user-selection of any available font. To print text call glRasterPos3f to set the printing position, then call COpenGLWnd::PrintString(const char* str) to print the string str.
- GDI drawing support has been added. There is a virtual function COpenGLWnd::OnDrawGDI(CPaintDC *pDC) that gets called from OnPaint, and in which the user can write their own GDI code. I am not sure how robust this is, since the only information that I have been able to find on mixing OpenGL and GDI calls has been that it’s not a good idea. However, the method I have used seems to work, at least on my computer…
- The rendered image can be copied to the clipboard as a bitmap.
class CGLMouseRotate : public COpenGLWnd
This class adds the button and mouse code that allows the user to rotate the image by dragging on the screen. A flag determines whether mouse dragging causes rotation or is ignored.
class CGLScatterGraph : public CGLMouseRotate
This class adds the main functionality for drawing the scatter graph.
Data are passed to the class using CGLScatterGraph::SetData(int count,COLORREF
col,float *pCoords, COLORREF *pColList), where count is the number of
points in the graph, col is the colour used to draw the points (only
used if pColList==NULL), pCoords points to an array of floats
containing the x, y and z coordinates of each point (so array length = count*3),
and pColList is either NULL, or points to an array containing the colours
for each point separately.
The array pointers pCoords and pColList have to be maintained by the calling
class, and must not be deleted or go out of scope while the Scattergraph exists,
unless a new call is made to SetData with count = 0.
Only data points whose coordinates fall within the maximum and minimum values
for each of the axes are displayed. The axis scales can either be set explicitly
by the user, or the graph can autoscale to the maximum and minimum values of
any dimension.The graph can be set to display either an orthogonal or a perspective projection.
CGLSelectableScatterGraph : public CGLScatterGraph
This class adds the functionality that allows the user to select points in
the graph by drawing around them. Selection mode is entered with a call to CGLSelectableScatterGraph::StartMakeSel(). This disables the mouse rotation facility, and enables a standard drawing routine rather like the original Scribble demo. The class maintains a list of CPoints that describe the shape drawn by the user. The shape can be erased and mouse rotation enabled by a call to CGLSelectableScatterGraph::CancelSel().
At the moment the only use that is made of the selection shape is in the CGLSelectableScatterGraph::ZoomSel() function.
The mapping of the screen shape to the data points is done using the OpenGL feedback mode. The following code shows how this is done:
BOOL CGLSelectableScatterGraph::ZoomSel()
{
int i,j,count;
int id=0;
GLint rv;
GLfloat token;
GLfloat *pBuf;
// m_SelPts is a CArray of CPoints holding the coordinates
// drawn previously by the user
if (m_SelPts.GetSize()>2)
{
// make a region from selPts, need a standard array
CPoint *pt=new CPoint[m_SelPts.GetSize()];
for (i=0; i<m_SelPts.GetSize(); i++)
pt[i]=m_SelPts.GetAt(i);
CRgn rgn;
VERIFY(rgn.CreatePolygonRgn(pt,
m_SelPts.GetSize(),
ALTERNATE));
delete [] pt;
// get a list of coordinates using OpenGL feedback mode
// don’t know proper way to calculate how much memory needed
// for feedback buffer, but experiment shows the drawing has
// 96 floats overhead for drawing the axes, + 4 floats per
// point (makes sense as token + 3 coords) give some spare,
// in case miscalculated !!
pBuf=new GLfloat[200+m_Count*4]; // m_Count = number of points
// in the graph
BeginGLCommands(); // a base-class call that just enables the
// OpenGL context
glFeedbackBuffer(200+m_Count*4,GL_3D,pBuf);
glRenderMode(GL_FEEDBACK);
EndGLCommands();
// cause the window to be redrawn, thus calling the GL drawing
// code in feedback mode this puts a list of window coords for
// each item drawn into pBuf, along with a token specifying type
Invalidate();
UpdateWindow();
BeginGLCommands();
rv=glRenderMode(GL_RENDER); // restore standard GL drawing modeint *pInSel=new int[m_Count]; // for each pt, will be 1 if
// inside, 0 if outside shape need to know the viewport, so can
// invert Y values (since OpenGL Y coords increase upwards, Win
// coords increase downwards)GLint viewport[4];
glGetIntegerv(GL_VIEWPORT,viewport);
// now find which data points have which screen coords
// code for parsing feedback buffer comes from OpenGL Programming Guide
count=rv;
while (count)
{
token=pBuf[rv-count];
count–;
if (token==GL_POINT_TOKEN) // ignore the graph axis drawing code
{
GLdouble coords[3];
for (j=0; j<3; j++)
{
coords[j]=pBuf[rv-count];
count–;
}
CPoint pt;
pt.x=int(coords[0]);
pt.y=int(viewport[3]-coords[1]-1);
pInSel[id++]=rgn.PtInRegion(pt);
}
}
EndGLCommands();
// now we set the axis scales to draw the selected points
// (plus any others within the axes limits)
m_bAutoScaleX=m_bAutoScaleY=m_bAutoScaleZ=FALSE; // turn off
// autoscaling
// find max & min of pts within selection shape
float xMax,xMin,yMax,yMin,zMax,zMin;
xMax=yMax=zMax=-FLT_MAX;
xMin=yMin=zMin=FLT_MAX;
// drawCount is index of points which were drawn within old axes,
// and which therefore appear in pSelList
int drawCount=-1;
for (i=0; i<m_Count; i++)
{
// determine whether point was being drawn (is within old axes)
// and so will be in selection list (as either 1 if within shape
// or 0 if outside it)
if (!PtWithinAxes(m_pDat[i*3],m_pDat[i*3+1],m_pDat[i*3+2]))
continue; // ignore points outside axes
drawCount++;
if (pInSel[drawCount]==0) // ignore this point, outside
// user selection
continue;
xMax=max(m_pDat[i*3],xMax);
xMin=min(m_pDat[i*3],xMin);
yMax=max(m_pDat[i*3+1],yMax);
yMin=min(m_pDat[i*3+1],yMin);
zMax=max(m_pDat[i*3+2],zMax);
zMin=min(m_pDat[i*3+2],zMin);
}
// prettify axis scales
m_MaxX=NextAbove(xMax,5);
m_MinX=NextBelow(xMin,5);
m_MaxY=NextAbove(yMax,5);
m_MinY=NextBelow(yMin,5);
m_MaxZ=NextAbove(zMax,5);
m_MinZ=NextBelow(zMin,5);
Invalidate();delete [] pBuf;
delete [] pInSel;
rgn.DeleteObject();
}m_bAllowMouseRotate=m_bOldAllowRotate; // restore ability to
// rotate using the mouse
CancelSel(); // remove all points from m_SelPts, clear selection
// shape from screen
return TRUE;
}
Data Format
You can load data into the main program either using the Load button, or by dropping a file on the main window. The programme reads text files in any of 3 formats.
In all formats each point occupies a row of the text file.
Format 1: There are 3 tab- or space-separated columns, giving the x, y and z coordinates of the point. With this format the points are all drawn in one colour,
as selected by the user from within the programme.
Format 2: There are 4 columns. The first three give the coordinates, the fourth
is an integer value in the COLORREF range that gives the colour for that point.
Format 3: There are 6 columns. The first three give the coordinates, the next
three give RGB values (range 0-255) for the colour for that point.
The files test1.txt, test2.txt and test3.txt give example data in the three
formats.
Some problems
There is sometimes a nasty flicker when resizing the app. If anyone knows how to get rid of it, please let me know!
I tried putting some fog into the graph, which from the book looked like it should be pretty simple to do. But I got weird results – both the back and the front of the scene were fogged, while the mid-distance was clear. I then decided that life was short and that I didn’t really want to fog my graph anyway. But it still irritates me, so if anyone actually knows how to get it to work properly, please let me know.