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);

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

Реклама

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: