15 мая 2023 года "Исходники.РУ" отмечают своё 23-летие!
Поздравляем всех причастных и неравнодушных с этим событием!
И огромное спасибо всем, кто был и остаётся с нами все эти годы!

Главная Форум Журнал Wiki DRKB Discuz!ML Помощь проекту


3-D Рассеивание при помощи OpenGL


This article was contributed by William Heitler.

Компилятор: Visual C++ 6.0

Описание

Пример использует OpenGL для создания 3-D рассеивания, которое можно посмотреть под любым углом при помощи мышки. Данные так же можно просматривать в номальной проекции так и в перспективной. Данные и цвет каждой точки загружаются из текстового файла. При изменении размера окна приложения, график тоже пропорционально меняет размер. Присутствует возможность выделить определённый регион точек и изменить его масштаб.

Проект представляет из себя диалоговое приложение MFC. В диалоге содержатся все необходимые для управления элементы, а так же наследованный от CWnd класс, который включает в себя все возможности для рисования графики.

На рисунке, пользователь выделил голубые точки путём нажатия в секции кнопки Make. Если после этого нажать Zoom, то график будет перемасштабирован, чтобы показать только выбранную группу точек. Чтобы облегчить масштабирование по другим измерениям, можно воспользоваться опцией Autoscale.

Графические классы

class COpenGLWnd : public CWnd

Это универсальный класс, основанный на CWnd, в котором присутствует всё необходимое для отображения OpenGL графики.

Основные, отличительные особенности этого класса:

  1. К классу добавлена дополнительная поддержка текста, при помощи функции wglUseFontBitmaps. Такой способ намного проще, чем использование собственных изображений шрифта в стандартных методах OpenGL. Активировать поддержку текста можно вызовом COpenGLWnd::MakeFont() в том же месте, где происходит процесс инициализации. В данный момент доступен только шрифт sysem, однако не составляет большого труда позволить пользователю выбирать любой другой шрифт. Для печати текста сперва вызываем glRasterPos3f (для установки координат вывода текста), а затем COpenGLWnd::PrintString(const char* str) (для печати строки str).
  2. Добавлена поддержка рисования GDI. Это виртуальная функция COpenGLWnd::OnDrawGDI(CPaintDC *pDC) которая вызывается из OnPaint, и в которую можно добавить свой собственный код GDI. Я не уверен в стабильности данного решения, так как единственное, что я нашёл относящееся к смешиванию OpenGL и GDI - это то, что это нехорошая идея. Однако, этот метод работает.
  3. Картинку можно скопировать в буфер обмена (clipboard) как bitmap.
class CGLMouseRotate : public COpenGLWnd

Класс содержит код для кнопок и мышки, позволяющий вращать изображение. Флаг определяет - разрешено ли вращение мышкой или нет.

class CGLScatterGraph : public CGLMouseRotate

Этот класс содержит необходимые функции для рисования графика рассеивания.

Данные передаются в класс при помощи CGLScatterGraph::SetData(int count,COLORREF col,float *pCoords, COLORREF *pColList), где count это количество точек в графике, col - цвет, используемый для рисования точек (используется только если pColList==NULL), pCoords - указатель на массив, содержащий x, y и z координаты каждой точки (соответственно длина массива = count*3), и pColList - это либо NULL, либо указатель на массив, содержащий цвета для каждой точки отдельно.
Указатели массва pCoords и pColList должны быть поддержаны вызывающим классом, и не должны удаляться либо выходить за пределы диапазона, пока рассеивание существует, до тех пор, пока не будет сделан новый вызов в SetData с count = 0.

CGLSelectableScatterGraph : public CGLScatterGraph

Этот класс позволяет выделять точки в графике путём рисования кривой вокруг них. Вход в режим выделения осуществляется при помощи CGLSelectableScatterGraph::StartMakeSel(). При этом запрещается возможность вращения мышкой и разрешает обычное рисование мышкой. Класс содержит список CPoints который описывает нарисованную пользователем поверхность. Вызов функции CGLSelectableScatterGraph::CancelSel() убирает выделение и разрешает вращение мышкой.

Выделение поверхности реализовано в функции CGLSelectableScatterGraph::ZoomSel().

Отображение экранной поверхности в точки сделано при помощи режима обратной связи в OpenGL. Приведённый ниже код, демонстрирует эту реализацию:

 

BOOL CGLSelectableScatterGraph::ZoomSel()
{
   int i,j,count;
   int id=0;
   GLint rv;
   GLfloat token;
   GLfloat *pBuf;

// m_SelPts это CArray указателей CPoints содержащий координаты
// предыдущего ресунка, сделанного пользователем
  if (m_SelPts.GetSize()>2)
  {
  // делаем регион из selPts, требуется стандартный массив
     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;

// получаем список координат при помощи режима обратной связи OpenGL
// Верного способа для вычисления необходимого кол-ва памяти
// для буфера обратной связи не известно, однако эксперименты показали,
// что для рисования осей необходимо 96 значений float, + 4 float на
// точку даёт некоторый запас, вслучае неправильного рассчёта !!
    pBuf=new GLfloat[200+m_Count*4]; // m_Count = количество точек
                                     // в графе
    BeginGLCommands(); // разрешаем контекст OpenGL
    glFeedbackBuffer(200+m_Count*4,GL_3D,pBuf);
    glRenderMode(GL_FEEDBACK);
    EndGLCommands();
// заставляет окно перерисоваться, таким образом вызывая код рисования
// GL и в режиме обратной связи помещает список оконных координат для
// каждого перерисовываемого элемента в pBuf

    Invalidate();
    UpdateWindow();
    BeginGLCommands();
    rv=glRenderMode(GL_RENDER); // восстанавливаем стандартный режим
                                // рисования GL

    int *pInSel=new int[m_Count]; // для каждой точки будет 1 если
// внутри и 0 если за пределами поверхности

GLint viewport[4];
    glGetIntegerv(GL_VIEWPORT,viewport);

    count=rv;
    while (count)
    {
      token=pBuf[rv-count];
      count--;
      if (token==GL_POINT_TOKEN) // игнорируем код рисования осей
      {
         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();

// теперь устанавливаем масштаб по осям для рисования выделенных точек
// (плюс все остальные в пределах осей)
    m_bAutoScaleX=m_bAutoScaleY=m_bAutoScaleZ=FALSE; // выключаем
                                                 // автомасштабирование
// находим максимальные и минимальные значения точек в пределах
// выделенной поверхности
    float xMax,xMin,yMax,yMin,zMax,zMin;
    xMax=yMax=zMax=-FLT_MAX;
    xMin=yMin=zMin=FLT_MAX;
    
// drawCount это счётчик точек, которые были нарисованы в пределах,
// старых осей и поэтому появились в pSelList
    int drawCount=-1; 
    for (i=0; i<m_Count; i++)
    {
// определяем - где была нарисована точка (в старых ли осях)
// и поэтому была помещена в список выделения (если в пределах, то 1
// либо 0 если за пределами)
      if (!PtWithinAxes(m_pDat[i*3],m_pDat[i*3+1],m_pDat[i*3+2]))
         continue;  // игнорируем точки за пределами осей
      drawCount++;
      if (pInSel[drawCount]==0)  // игнорируем точку, так как она за
                                 // пределами выделения
          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);
     }
// подгоняем масштаб по осям
    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; // восстанавливаем способность
                                         // вращения мышкой
  CancelSel();  // удаляем все точки из m_SelPts, очищаем выделение
  return TRUE;
}

Формат данных

Данные можно загружать из основной программы, используя кнопку Load, либо перетаскивая файл на основное окно. Программа умеет считывать файлы трёх форматов.

Во всех форматах каждая точка занимает отдельную строку в текстовом файле.
Формат 1: Имеет три колонки, разделённые табуляцией или пробелами, содержащие координаты точки x, y и z. В таком формате все точки рисуются одним цветом, который установлен в программе.
Формат 2: Имеет четыре колонки. Первые три содержат координаты точки, а четвёртая имеет целое значение (integer value) в диапазоне COLORREF, которая является цветом точки.
Формат 3: Имеет шесть колонок. Первые три содержат координаты, а другие три содержат значения RGB (в диапазоне 0-255), которые в совокупности дают цвет точки.

Файлы test1.txt, test2.txt и test3.txt иллюстрирую все три формата.

Downloads

Скачать демонстрационный проект - 59 Kb