Category Archives: BY

BY-1

скриншот

Введение

Результатом BY-0 стала простенькая программа-симулятор движения твёрдых сфер на плоскости. Очевидная следующая задача — сделать движение в трёхмерном пространстве. Сделаем это в два шага (1а и 1б).

Шаг 1а

Цель — реализовать движение в 3D. Будем сравнивать старый и новый варианты.

Первым делом расширим двумерный вектор Vec2 до трёхмерного вектора Vec3, добавив координату z. Ключевое слово explicit в определении конструктора предотвращает неявное преобразование скаляра к вектору. Теперь вызов

glTranslated(p.x, p.y, p.z);

вместо

glTranslated(p.x, p.y, 0.0);

в Ball::paint() позиционирует шар в трёхмерном пространстве.

Вместо действия одной центральной силы притяжения будем рассчитывать взаимное притяжение шаров друг к другу по формуле закона всемирного тяготения (естественно, значение константы G у нас своё). Сила, действующая на пару шаров вычисляется функцией gravity(Ball&, Ball&).

Кроме того, на шары будет действовать сила «вязкого» трения (не зависящая, впрочем, от взаимно-относительного положения тел) равная -Fr^2 v, где F — коэффициент трения, r — радиус шара, v — скорость шара. Сила трения вычисляется функцией friction(Ball&). Размеры окна в процессе вычисления сил нам более не нужны, поэтому переменные width и height убраны.

Рисование в 3D требует применения техник отсечения невидимых поверхностей. Стандартной техникой, работающей на попиксельном уровне и реализованной аппаратно в GPU, является тест глубины с применением буфера глубины (также называемого z-буфером). В OpenGL тест глубины включается с помощью команды (см. main())

glEnable(GL_DEPTH_TEST);

при рисовании каждого кадра этот буфер обычно требуется сбрасывать (аналогично значениям цветов пикселей), что и делается вызовом

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

Кроме того, для повышения реалистичности картинки неплохо было бы включить освещение. Простейший вариант, доступный в OpenGL 1.0 состоит в использовании повершинного расчёта освещённости с закрашиванием треугольников линейной интерполяцией цветов, полученных на вершинах (затенение Гуро). Для его включения с использованием одного источника света достаточно трёх вызовов

  glEnable(GL_NORMALIZE);
  glEnable(GL_LIGHTING);
  glEnable(GL_LIGHT0);

Параметры источника света LIGHT0 задаются по умолчанию. Это направленный (бесконечно удалённый, заданный направлением лучей) источник белого цвета. Для изменения его параметров можно использовать функции glLight*. При расчёте освещения полагается, что нормали имеют единичную длину. Однако, например, при масштабировании объекта (мы задаём радиус шара), происходит масштабирование и нормалей в том числе, что изменяет их длину. Вызов glEnable(GL_NORMALIZE) предлагает OpenGL позаботиться об этом: включает автоматическую нормализацию нормалей.

Обработчик события изменения сторон окна теперь выставляет (пока всё ещё ортогональную) проекцию примерно фиксированного размера, отображая вдоль наибольшего измерения (ширины или высоты окна) координаты сцены от -500 до 500 (вдоль оси 0x или оси 0y, соответственно).

Что до прочих мелочей, то изменилась «политика выхода» из программы: вместо «жёсткого» exit(0) вызываем glutExit(). Кроме того, чтобы распределить вычислительную нагрузку на все доступные в системе аппаратно поддерживаемые потоки ЦП внутри функции frameMove() используется директива OpenMP:

# pragma omp parallel for

благодаря чему многоядерный процессор загружается на 100%. Впрочем, реальная эффективность такого распараллеливания не проверялась.

Шаг 1б

Следующая цель — управляемая проекция с имитацией перспективы, попросту говоря, «камера». Будем сравнивать вариант, полученный на шаге 1а, и окончательный вариант BY-1.

Камера (класс SphericalCoords) у нас будет довольно простая: основанная на сферических координатах. Положение её в пространстве задаётся двумя углами phi (долгота) и theta (широта), а также расстоянием до начала координат r. Пользователь может поворачивать камеру вокруг начала координат влево (A) и вправо (D) на угол dphi, вверх (Z) и вниз (X) на угол dtheta или придвигать (W) и отодвигать (S) её от начала координат домножением-делением r на заданный коэффициент rf.

Кроме клавиш, генерирующих при нажатии ASCII-код, можно использовать «специальные клавиши» (управление курсором, функциональные). Для этого требуется определить специальный обработчик (в нашей программе он назван specialKeyPressed) и подключить его к GLUT вызовом glutSpecialFunc. Теперь камерой можно управлять клавишами-стрелками.

Матрица проекции строится как произведение двух матриц.

1. Матрицы перспективной проекции, задающей углы поля зрения камеры по горизонтали и вертикали и расстояния от камеры до ближней и дальней отсекающих плоскостей (таким образом, объём, видимый камерой и проецируемый на экран, представляет собой усечённую пирамиду). Эта матрица вычисляется при изменении размеров окна с помощью вызова

gluPerspective(75, double(w) / double(h), 16.0, 4096.0);

который выставляет угол зрения по вертикали в 75 градусов, соотношение сторон (коэффициент, на который нужно домножить угол зрения по вертикали, чтобы получить угол зрения по горизонтали), расстояние до ближней отсекающей плоскости — 16 единиц (всё, что ближе к камере — не рисуется), расстояние до дальней отсекающей плоскости — 4096 единиц (всё, что дальше от камеры — не рисуется. Таким образом задаются размеры объёма отсечения, но не его положение и ориентация в пространстве. Изменяя угол зрения можно добиться эффекта оптического зума.

2. Матрицы камеры, задающей положение и ориентацию видимой области в пространстве. Эта матрица вычисляется в функции SphericalCoords::apply() при помощи функции gluLookAt, которая позволяет задать координаты положения камеры, координаты точки, на которую направлена (смотрит) камера и направление «вверх» (с помощью выбора которого можно вращать камеру вокруг оси зрения). Как нетрудно догадаться, фактически эта функция вычисляет матрицу поворота и сдвига.

Матрицу перспективной проекции нужно вычислять при изменении соотношения сторон окна рендеринга, поэтому это делается внутри соответствующего обработчика. Матрицу камеры нужно вычислять при каждом движении камеры, поэтому это действие выделено в отдельную функцию. Ввиду такого разделения было принято решение сохранять матрицу перспективной проекции в стеке матриц проекции под текущей матрицей проекции, представляющей собой произведение описанных выше двух матриц. При вызове apply() матрица, сохранённая под вершиной стека, извлекается (дублируется) и домножается на матрицу камеры.

Вернёмся к изменениям в main(). Можно имитировать полупрозрачность шаров используя смешивание цветов пикселя, уже записанного в буфер кадра, и пикселя, принадлежащего полигону, рисуемому «поверх». При этом значение альфа-канала цвета используется как коэффициент линейной интерполяции. Включение смешивания выполняется вызовом

glEnable(GL_BLEND);

Формула смешивания задаётся с помощью функции glBlendFunc:

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

Первый параметр определяет, на что будет домножен цвет «источника» (нового пикселя), в данном случае — на собственное значение альфа-канала. Второй параметр определяет, на что будет домножен цвет «назначения» (пикселя в буфере кадра), в данном случае — на (1 — a), где a — значение альфа-канала нового пикселя. Цвет результирующего пикселя получается как сумма этих двух произведений. Например, вызов

glBlendFunc(GL_ONE, GL_ONE);

приведёт к простому сложению цветов пикселей.

К сожалению, представленный выше метод имитации полупрозрачности при использовании теста глубины работает правильно только тогда, когда полигоны, отправляемые на растеризацию, не пересекаются и отрисовываются в порядке убывания глубины. Поэтому при отрисовке шаров, сгенерированных GLUT, будут видны артефакты: в одном месте раньше будут нарисованы полигоны обратной стороны, в другом месте — полигоны видимой, ближней стороны. От этих артефактов можно избавиться, если включить отбрасывание полигонов по направлениям нормалей (куллинг), что делается вызовом

glEnable(GL_CULL_FACE);

Таким образом, полигоны, нормали к которым «смотрят» от зрителя (эти полигоны повёрнуты задней стороной), не будут отрисовываться, что уберёт артефакты при рисовании одиночных шаров (однако не решит проблем с наложением нескольких шаров).

Реклама

BY-0

Введение

Когда-то давно я написал небольшой примерчик использования библиотеки GLUT, в котором демонстрировалось физически корректное столкновение двух твёрдых шариков. В феврале 2010г он был размещён на моей странице на codepad.org. Теперь же я решил сделать из него что-то более интересное и использовать в качестве учебного пособия.

Шарики

Изначально код рассчитан на N шариков. При этом почему-то было сделано так, что столкновения обрабатываются только между первыми двумя. Впрочем, это несложно исправить.

Вначале генерируется случайная «целевая» точка столкновения внутри окна (функция new_circles). N шариков со случайными массами и радиусами расставляются в случайных позициях со скоростями, направленными в точку столкновения, по величине равными четверти расстояния от центра шарика до точки столкновения. Таким образом, шарики должны столкнуться примерно через 4 секунды.

За рисование (рендеринг) отвечает функция repaint. Собственно же движения и столкновения шариков рассчитываются в функции frame_move. Время на момент расчёта последнего кадра хранится в глобальной переменной t. В случае, если на момент вызова frame_move() прошло больше 0 микросекунд, происходит изменение положения шариков соответственно их скоростям и прошедшему времени. Если центры шариков 0 и 1 находятся друг от друга не дальше суммы радиусов этих шариков, то регистрируется столкновение и скорости шариков меняются соответственно. Код под комментарием «improve distance» производит «разведение» шариков, чтобы они перестали касаться. Работа этого кода может приводить к «туннелированию» шариков, если результирующие скорости оказались близки к параллельным.

Функция handle_key реализует реакцию на нажатие пользователем клавиш, когда активно окно, где происходит рисование шариков (под MS Visual Studio данный файл следует компилировать как консольное приложение, соответственно будет два окна: консоль и окно с графикой, правда, чтобы его скомпилировать и успешно запустить, нужно раздобыть библиотеку GLUT в составе glut.h, glut32.lib и glut32.dll). Нажатие на enter или пробел создаёт новый набор шариков, нажатие на escape (код 27 в ASCII) или q приводит к выходу из программы.

Функция main производит инициализацию GLUT, состояния контекста OpenGL, создаёт аппроксимацию сферы (единичного радиуса, 12 вершин на «круг» по долготе, 4 таких круга по широте), регистрирует (передаёт библиотеке указатели на эти функции) функции-обработчики событий (callbacks; под комментарием «register callbacks» ). Вызов glutMainLoop() запускает цикл-обработчик событий, из которого, собственно, и вызываются зарегистрированные нами функции.

Окна

Зачем нам нужен GLUT? Что до OpenGL, то это низкоуровневый графический API, обращающийся непосредственно к драйверу GPU. В его «компетенции» не входит создание окон GUI или, тем более, организация взаимодействия с пользователем (реакция на события клавиатуры и мыши). Всё это, а также инициализация виртуального устройства OpenGL (создание контекста OpenGL) реализуется операционной системой. Если же хочется обеспечить независимость от ОС и/или простоту кода (что важно для маленьких приложений), имеет смысл воспользоваться сторонними библиотеками-обёртками, предоставляющими указанный функционал. Например, библиотекой GLUT.

Библиотека GLUT имеет давно устоявшийся (в настоящее время не развивается), достаточно простой в использовании (самый простой?) интерфейс на языке C (можно использовать в программах на разных языках программирования). Конечно, есть другие библиотеки, с помощью которых можно сделать то же самое и что-нибудь ещё, как маленькие и простые (например, SFML), так и огромные, предоставляющие широкий функционал для создания серьёзных GUI приложений (например, Qt).

Новый вариант

Итак, старый вариант был переправлен.

Для сравнения старого и нового вариантов удобно использовать какой-нибудь вариант утилиты diff (у Microsoft есть своя реализация — WinDiff, которая может присутствовать в составе Visual Studio/Windows SDK; мне же больше нравится WinMerge).

В качестве реализации GLUT было решено взять более современную (чем оригинальная) open-source библиотеку freeglut (тем более, что есть сборка под Visual C++). Вместо микросекундного времени boost::date_time, будем использовать обычный миллисекундный std::clock.

Макрос GLUTCALLBACK нам теперь без надобности (был на случай особого соглашения вызова для функций-обработчиков, например, __stdcall).

Посмотрим Ball::paint vs circle::paint. Стек матриц не используется, поэтому перед установкой матрицы для шара, просто заменяем текущую матрицу на единичную. Далее, было:

glScaled(r, r, r);
glTranslated(p.x / r, p.y / r, 0.0);

стало

glTranslated(p.x, p.y, 0.0);
glScaled(r, r, r);

Указанные функции домножают текущую матрицу на матрицу соответствующего преобразования слева. Поэтому (новый вариант), сначала промасштабируем единичную сферу, сделав из неё сферу радиуса r, потом переместим её из начала координат в позицию (p.x, p.y), а не наоборот, как было в старом варианте (поэтому там деление на r).

Появилась новая функция Ball::move, которая осуществляет расчёт сдвига шара в соответствии с заданным ускорением a. Ускорение считается постоянным в течении указанного промежутка времени time_step. Метод первого порядка, его точность низкая, но вполне соответствует обычной «игровой физике». Итак, сначала нужно записать в a сумму ускорений, порождаемых силами, действующими на шар, потом вызвать move.

Сила здесь будет одна — похожее на гравитацию (прямо пропорциональное массе шара и константе G, обратно пропорциональное квадрату расстояния) притяжение к центру экрана. Вычислением ускорения, порождаемого этой силой, занимается функция applyCentralForce.

Функция frame_move переименована в frameMove и теперь рассчитывает столкновения для всех пар шариков. Кроме того, вместо того, чтобы «разводить» шарики, «зашедшие» внутрь друг друга, фиксация столкновения производится только в случае сближения центров шариков. Шарики могут оказаться внутри друг друга (что особенно заметно из-за того, что при случайном выборе позиции факт «попадания» внутрь не проверяется), зато нет «туннелирования».

Добавлен обработчик события изменения размеров окна рендеринга reshape, именно там устанавливается матрица проекции, которая в старом варианте просто не использовалась. Текущие размеры окна хранятся в глобальных переменных width и height.

С фактом наличия глобальных переменных пока решено смириться.