Домой / Кровля / Opengl освещение. Уроки и примеры программирования Array (). Как работает освещение

Opengl освещение. Уроки и примеры программирования Array (). Как работает освещение

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

Стоит отметить, что при использовании FFP OpenGL освещение рассчитывалось не в мировой системе координат, а в видовой. На мой взгляд это не очень удобно, т.к. видовая система координат связана с камерой, а источники освещения удобно задавать в мировой системе координат и именно там и производить весь расчет.

Расчет освещения

Для расчетов освещения в этом уроке используется модель освещения Фонга (Phong shading). Основной смысл модели в том, что итоговое освещение объекта складывается из трех компонентов:

  • Фоновой свет (ambient)
  • Рассеянный свет (diffuse)
  • Отраженный свет (specular)

Дополнительно к этим параметрам мы добавим собственное свечение материала (emission), этот параметр позволяет подсветить объект даже если он не освещен ни одним источником освещения.

Соответственно каждый из компонентов рассчитывается с учетом параметров источника освещения и материала объекта. Боле подробную информацию по этой модели освещения вы можете получить в этой заметке .

Расчет освещения может быть как повершинным (per-vertex lighting) так и попиксельным (per-pixel lighting). В данном уроке мы рассмотрим именно поиксельное освещение, оно позволяет сгладить недостаточную детализацию полигональных моделей и более точно рассчитать освещенность в каждой точке объекта. Основной расчет попиксельного освещения происходит во фрагментном шейдере.

Прежде чем приступать к расчету освещения необходимо рассчитать и передать некоторые параметры вершин из вершинного шейдера во фрагментный:

  • Нормаль к поверхности объекта в вершине (normal)
  • Направление падающего света, от вершины к источнику освещения (light direction)
  • Направление взгляда, от вершины к наблюдателю (view direction)
  • Расстояние от точечного источника освещения до вершины (distance)

Нормаль к поверхности объекта и направление падающего света используются для расчета рассеянного (diffuse) и отраженного (specular) света, однако для расчета отраженного света необходимо еще дополнительно знать направление взгляда наблюдателя. Расстояние от вершины до источника освещения необходимо для расчета общего коэффициента затухания (attenuation). Вершинный шейдер при этом будет таким:

#version 330 core #define VERT_POSITION 0 #define VERT_TEXCOORD 1 #define VERT_NORMAL 2 layout(location = VERT_POSITION) in vec3 position; layout(location = VERT_TEXCOORD) in vec2 texcoord; layout(location = VERT_NORMAL) in vec3 normal; // параметры преобразований uniform struct Transform { mat4 model; mat4 viewProjection; mat3 normal; vec3 viewPosition; } transform; // параметры точеченого источника освещения uniform struct PointLight { vec4 position; vec4 ambient; vec4 diffuse; vec4 specular; vec3 attenuation; } light; // параметры для фрагментного шейдера out Vertex { vec2 texcoord; vec3 normal; vec3 lightDir; vec3 viewDir; float distance; } Vert; void main(void ) { // переведем координаты вершины в мировую систему координат vec4 vertex = transform.model * vec4(position, 1 .0 ) ; // направление от вершины на источник освещения в мировой системе координат vec4 lightDir = light.position - vertex; // передадим во фрагментный шейдер некоторые параметры // передаем текстурные координаты Vert.texcoord = texcoord; // передаем нормаль в мировой системе координат Vert.normal = transform.normal * normal; // передаем направление на источник освещения Vert.lightDir = vec3(lightDir) ; // передаем направление от вершины к наблюдателю в мировой системе координат Vert.viewDir = transform.viewPosition - vec3(vertex) ; // передаем рассятоние от вершины до источника освещения Vert.distance = length(lightDir) ; // переводим координаты вершины в однородные gl_Position = transform.viewProjection * vertex; }

Без источника света изображения не видно. Что бы инициализировать источник, и включить обработчик расчёта воздействия источника на объекты достаточно выполнить команды: glEnable(gl_lighting);// включить режима анализа освещения

GlEnable(gl_light0); // включить в сцену конкретный (нулевой) источник, с его характеристиками

Для отключения источника используется функция Disable(). Источник света по умолчанию располагается в пространстве с координатами (0,0,∞). Можно создавать источник света в любой точке пространства изображений.

В библиотеке OpenGl поддерживаются источники света четырех типов:

  • фонового освещения (ambient lighting),
  • точечные источники (point sources),
  • прожекторы (spotlights),
  • удаленные источники света (distant light).
Каждый источник света имеет свой набор характеристик.
Характеристики источника света, соответствуют параметрам модели Фонга.
Для установки векторных параметров используется функция glLightfv(), которая имеет следующий формат:

glLightfv(source, parameter, pointer_to_array) ;

Существует четыре векторных параметра, которые определяют положение и направление лучей источника и цветовой состав его составляющих - фоновой, диффузионной и зеркальной.
Для установки скалярных параметров в OpenGL служит функция glLightf():

glLightf(source, parameter, value) ;

Пусть, например, требуется включить в сцену источник GL_LIGHT0, который должен находиться в точке (1.0, 2.0, 3.0). Положение источника сохраняется в программе в виде точки в однородных координатах:

GLfloat light0_pos={1.0, 2.0, 3.0, 1.0};

Если четвертый компонент этой точки равен нулю, то точечный источник превращается в удаленный, для которого существенно только направление лучей:

GLfloat light0_dir={1.0, 2.0, 3.0, 0.0};

Далее определяется цветовой состав фоновой, диффузионной и зеркальной составляющих источника. Если в рассматриваемом примере источник имеет зеркальную составляющую белого цвета, а фоновая и диффузионная составляющие должны быть красными, то фрагмент программы, формирующий источник, выглядит следующим образом:

GLfloat diffise0= {1.0, 0.0, 0.0, 1.0};

GLfloat ambient0={1.0, 0.0, 0.0, 1.0};

GLfloat specular0={1.0, 1.0, 1.0, 1.0};

GlEnable(GL_LIGHTING);

GlEnable(GL_LIGHT0);

GlLightfv(GL_LIGHT0, GL_POSITION, light0_pos);

GlLightfv(GL_LIGHT0, GL_AMBIENT, ambient0);

GlLightfv(GL_LIGHT0, GL_DIFFUSE, diffuse0);

GlLightfv(GL_LIGHT0, GL_SPECULAR, specular0);

В сцену можно включить и глобальное фоновое освещение, которое не связано ни с каким отдельным источником освещения. Если, например, требуется слабо подсветить все объекты сцены белым цветом, в программу следует включит такой фрагмент кода:

GLfloat global_ambient={0.1, 0.1, 0.1, 1.0};

GlLightModelfv(GL_LIGHT_MODEL_AMBIENT, global_ambient);

В модели освещения член, учитывающий расстояние до источника, имеет вид:

K= 1/(a+ b*d+ c*d^2)

И постоянную, линейную и квадратичную составляющие. Соответствующие коэффициенты для каждого источника задаются индивидуально с помощью функции установки скалярных параметров, например:

GlLightf(GL_LIGHT0, GL_CONSTANT_ATTENATION, a);

Для преобразование точечного источника в прожектор нужно задать направление луча прожектора (GL_SPOT_DIRECTION), показатель функции распределения интенсивности (GL_SPOT_EXPONENT) и угол рассеяния луча (GL_SPOT_CUTTOF). Эти параметры устанавливаются с помощью функций glLightf() и glLightfv().

Параметры, устанавливаемые для источников света по умолчанию приведены в таблице 3.

Параметры, устанавливаемые для источников света по умолчанию

Таблица 3

Имя параметра Значение по умолчанию Содержание
GL_AMBIENT (0.0, 0.0, 0.0, 1.0) ambient RGBA intensity of light
GL_DIFFUSE (1.0, 1.0, 1.0, 1.0) diffuse RGBA intensity of light
GL_SPECULAR (1.0, 1.0, 1.0, 1.0) specular RGBA intensity of light
GL_POSITION (0.0, 0.0, 1.0, 0.0) (x, y, z, w) position of light
GL_SPOT_DIRECTION (0.0, 0.0, -1.0) (x, y, z) direction of spotlight
GL_SPOT_EXPONENT 0.0 spotlight exponent
GL_SPOT_CUTOFF 180.0 spotlight cutoff angle
GL_CONSTANT_ATTENUATION 1.0 constant attenuation factor
GL_LINEAR_ATTENUATION 0.0 linear attenuation factor
GL_QUADRATIC_ATTENUATION 0.0 quadratic attenuation factor

Освещение в OpenGL ES – это полезная особенность, которая может придать 3D -играм приятный оттенок. Чтобы использовать подобную функциональность, сначала нам необходимо понять модель освещения OpenGL ES.

Как работает освещение

Задумаемся о том, как работает освещение. Для начала нам потребуется источник света, испускающий свет. Понадобится также освещаемый объект. Наконец, нам также понадобится сенсор вроде глаз или камеры, принимающий фотоны, которые посылаются источником света и отражаемые объектом. Освещение меняет воспринимаемый цвет объекта в зависимости от: типа источника освещения; цвета или интенсивности источника света; позиции источника света и его направления относительно освещаемого объекта; материала и текстуры объекта.

Интенсивность, с которой свет отражается объектом, зависит от множества факторов. Самый главный фактор, на который мы обращаем внимание, – это угол, с которым световой луч падает на поверхность. Чем ближе этот угол к прямому, тем больше интенсивность, с которой свет отразится от объекта. Это проиллюстрировано на рис. 11.1.

Как только световой луч упадет на поверхность, он отразится в двух различных направлениях. Большая часть света отразится рассеянно. Это означает, что отраженные световые лучи неравномерно рассыпаны в случайном порядке по поверхности объекта. Некоторые лучи отражаются зеркально. Это означает, что световые лучи отразятся назад так, будто они падают на идеальное зеркало. На рис. 11.2 показана разница между рассеянным и зеркальным отражениями.

Рис. 11.1. Чем ближе угол к прямому, тем больше интенсивность отраженного света

Рис. 11.2. Рассеянное и зеркальное отражения

Зеркальное отражение будет проявляться как блики на объектах. Будет ли свет отражаться от объекта зеркально, зависит от материала, из которого он сделан. Объекты с неровной или шероховатой, как кожа, поверхностью, скорее всего, не будут иметь зеркальных бликов. Объекты, имеющие гладкую поверхность наподобие стекла или мрамора, будут демонстрировать эти световые артефакты. Конечно, стекло или мрамор не являются идеально гладкими, но по сравнению с деревом или человеческой кожей они являются таковыми.

Когда свет падает на поверхность, его отражение также меняет свой цвет в зависимости от химического состава освещаемого объекта. Объекты, которые кажутся нам красными, отражают только красные порции света и поглощают все другие частоты. Черный объект – это такой объект, который поглощает практически весь свет, который падает на него.

OpenGL ES позволяет имитировать реальное поведение, определяя источники света и материалы объектов.

Источники освещения

Нас окружает множество разнообразных источников освещения. Солнце постоянно посылает свои фотоны. Мониторы излучают свет, окружающий нас приятным свечением по ночам. Лампочки и фары помогают нам избегать столкновений с различными предметами в темноте. OpenGL ES позволяет создавать четыре типа источников света.

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

Точечные источники света. Имеют позицию в пространстве и испускают свет во всех направлениях. Например, точечным источником света является лампочка.

Направленные источники освещения. Выражаются как направления в OpenGL ES. Предполагается, что они находятся бесконечно далеко. В идеале Солнце может являться таким источником. Мы можем предположить, что все световые лучи, исходящие от Солнца, попадают на Землю под одинаковым углом из-за расстояния между Землей и Солнцем.

О Светильники. Эти источники похожи на точечные источники освещения тем, что имеют заданную позицию в пространстве. Кроме того, у них есть направление, в котором они излучают световые лучи. Они создают световой конус, ограниченный некоторым радиусом. Примером такого источника света является уличный фонарь.

Мы будем рассматривать только подсветку, а также точечные и направленные источники света. Светильники часто сложно использовать на ограниченных GPU Android-устройств из-за способа расчета освещения в OpenGL ES. Скоро вы поймете, почему это так.

Помимо позиции и направления источника света OpenGL ES позволяет определять цвет или интенсивность света. Эти характеристики выражаются с помощью цвета RGBA. Однако OpenGL ES требует определять четыре различных цвета для одного источника вместо одного.

Подсветка – интенсивность/цвет, вносящий вклад в создание затенения объекта. Объект будет освещен одинаково со всех сторон, независимо от его позиции или ориентации относительно источника света.

Рассеянный – интенсивность/цвет света, которым будет освещен объект после расчета рассеянного отражения. Грани объекта, которые не смотрят на источник света, не будут освещены, как и в реальной жизни.

Зеркальный – интенсивность/цвет, похожий на рассеянный цвет. Однако он влияет только на те точки объекта, которые имеют определенную ориентацию по отношению к источнику света и сенсору.

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

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

Материалы

Все объекты в нашем мире состоят из какого-либо материала. Каждый материал определяет, как свет, падающий на объект, будет отражаться и изменять цвет отраженного света. OpenGL ES позволяет определять те же четыре цвета RGBA для материала, что и для источника света.

Подсветка – цвет, который объединяется с фоновым цветом любого источника света на сцене.

Рассеянный – цвет, который объединяется с рассеянным цветом любого источника света.

Зеркальный – цвет, который объединяется с зеркальным цветом любого источника света. Он используется для создания бликов на поверхности объекта.

Эмиссивный – продолжаем игнорировать этот тип цвета, поскольку он практически не применяется в нашем контексте.

Рисунок 11.3 иллюстрирует первые три типа свойств материала/источника света: подсветка, рассеянный и зеркальный.

Рис. 11.3. Различные типы материалов/источников света: только подсветка (слева), только рассеянный (посередине), подсветка и рассеянный цвет с зеркальными бликами (справа)

На рис. 11.3 показано влияние различных свойств материалов и источников света на цвет. Подсветка освещает размер равномерно. Рассеянный свет отразится в зависимости от угла, под которым на объект падают световые лучи; площади, которые непосредственно повернуты к источнику света, будут освещены ярче, площади, до которых свет не может добраться, будут темными. На правом изображении вы можете увидеть комбинацию подсветки, рассеянного и зеркального света. Зеркальный свет проявляет себя как белые блики на сфере.

Как OpenGL ES рассчитывает освещение: нормали вершин

Вы знаете, что интенсивность света, отраженного от объекта, зависит от его угла падения на объект. OpenGL ES использует этот факт для расчета освещения. Он применяет для этого нормали вершин, которые необходимо определять в коде так же, как и координаты текстур или цвета вершин. На рис. 11.4 показана сфера и ее нормали вершин.

Рис. 11.4. Сфера и ее нормали вершин

Нормали – это единичные векторы, указывающие направление, к которому повернута поверхность. В нашем случае поверхность – это треугольник. Вместо определения нормали поверхности мы определяем нормаль вершины. Разница между этими нормалями заключается в том, что нормаль вершины может не указывать в ту же сторону, что и нормаль поверхности. Это четко видно на рис. 11.4, где каждая нормаль вершины является усредненной нормалью всех треугольников, к которым принадлежит вершина. Такое усреднение производится для создания гладкой затененности объекта.

При отрисовке объекта с использованием освещения и нормалей вершин OpenGL ES определит угол между каждой вершиной и источником света. Если он знает этот угол, то может рассчитать цвет вершины, основываясь на свойствах материала. Конечным результатом является цвет каждой вершины, который далее применяется к каждому треугольнику в комбинации с рассчитанными цветами других вершин. Этот использованный цвет будет объединен с любыми текстурными преобразованиями, которые мы применим к объекту.

Это звучит довольно пугающе, но на самом деле не все так плохо. Нам нужно разрешить использование освещения и определить источники освещения, материал отрисовываемого объекта и нормали вершин (в дополнение к параметрам вершин, которые мы обычно определяем, например позицию или координаты текстур). Рассмотрим, как это можно реализовать с помощью OpenGL ES.

На практике

Теперь выполним все действия, необходимые для того, чтобы работать с освещением с помощью OpenGL ES. Создадим несколько небольших вспомогательных классов, которые немного упростят работу с источниками света, и поместим их в пакет com.badlogi с.androi dgames.framework.gl.

Разрешение и запрещение освещения

Как и для прочих состояний OpenGL ES, сначала следует подключить названную функциональность. Это можно сделать следующим образом:

После этого освещение будет применено ко всем отрисовываемым объектам. Чтобы получить результат, необходимо определить источники света и материалы, а также нормали вершин. Как только мы закончим отрисовывать все необходимые объекты, освещение можно отключить:

Определение источников освещения

OpenGL ES предоставляет 4 типа источников освещения: подсветка, точечный, направленный и светильник. Рассмотрим, как определить первые три. Чтобы светильники были эффективными и хорошо выглядели, каждая модель должна состоять из огромного количества треугольников. Для множества теперешних мобильных устройств это невозможно.

OpenGL ES позволяет определять максимум 8 источников освещения одновременно, а также один глобальный источник подсветки. Каждый из 8 источников освещения имеет идентификатор, от GL10.GL LIGHT0 до GL10.GL LIGHT7. Если нужно изменить свойства одного из этих источников освещения, это можно сделать, определив соответствующий ему ID.

Разрешить использование источников освещения можно с помощью следующего синтаксиса:

Далее OpenGL ES получит свойства этого источника освещения и применит их ко всем отрисовываемым объектам. Если нам нужно запретить использование источника освещения, мы можем сделать это с помощью следующего утверждения:

Подсветка – это особый случай, поскольку у нее нет идентификатора. На сцене OpenGL ES может существовать только одна подсветка. Рассмотрим этот источник освещения подробнее.

Подсветка

Подсветка – это особый тип освещения. У него нет позиции или направления, только цвет, который применяется ко всем освещаемым объектам одинаково. OpenGL ES позволяет определять глобальную подсветку следующим образом:

Массив ambi entCol or содержит значения RGBA цвета подсветки, представленные как числа с плавающей точкой в диапазоне от 0 до 1. Метод gl LightModel fv принимает в качестве первого параметра константу, определяющую, что мы хотим установить цвет источника фонового освещения, массив чисел с плавающей точкой, который содержит цвет источника, и смещение для массива чисел с плавающей точкой, из которого метод начнет считывать значения RGBA. Поместим код, решающий эту задачу, в небольшой класс. Его код приведен в листинге 11.2.

Листинг 11.2. Класс AmbientLight.java. простая абстракция глобальной подсветки ODenGL ES

Все, что мы делаем, – сохраняем цвет подсветки в массиве чисел с плавающей точкой и предоставляем два метода: один из них используется для установки цвета, а другой, чтобы указать OpenGL ES, что использовать следует именно этот цвет. По умолчанию применяется серый цвет.

Точечные источники освещения

Точечные источники освещения имеют позицию, а также фоновые, рассеянные и зеркальные цвет/интенсивность (мы не рассматриваем эмиссивные цвет/интенсивность). Определить разные типы цветов можно следующим образом:

Первый параметр – это идентификатор источника света. В этом случае мы используем четвертый источник. Следующий параметр определяет атрибут, который мы хотим изменить. Третий параметр – это массив чисел с плавающей точкой, содержащий значения RGBA, а последний – это смещение в данном массиве. Определить позицию источника так же просто:

Мы снова определяем атрибут, который хотим изменить (в данном случае позицию), массив из четырех элементов содержит х-, у- и z-координату источника света в создаваемом мире. Обратите внимание, четвертый элемент массива должен быть равен единице, если источник света имеет позицию. Поместим это во вспомогательный класс. Его код содержится в листинге 11.3.

Листинг 11.3. Класс Point.Light.java, простая абстракция точечных источников света OpenGL ES

Наш вспомогательный класс содержит фоновые, рассеянные и зеркальные цветовые компоненты света, а также позицию (четвертый элемент равен единице). В дополнение мы храним последний идентификатор, используемый для данного источника, поэтому становится возможно создать метод disableO, который отключит свет при необходимости. Также у нас есть метод enableO, который принимает экземпляр класса GL10 и идентификатор источника света (например GL10.GL LIGHT6). Он разрешает использование освещения, устанавливает его атрибуты и сохраняет использованный идентификатор. Метод disableO просто запрещает использование освещения, используя член класса 1ast.Ligh.tId, установленный в методе enablе.

Мы используем разумные значения по умолчанию для фонового, рассеянного и зеркального цветов при инициализации массивов-членов класса. Свет будет белым и не будет создавать никаких бликов, поскольку его зеркальная составляющая черная.

Направленные источники света

Направленные источники света практически идентичны точечным. Единственное различие заключается в том, что они имеют направление вместо позиции. Способ выражения направления несколько запутан. Вместо использования вектора, указывающего направление, OpenGL ES ожидает, что мы определим одну точку. Затем направление будет определено с помощью вектора, соединяющего эту точку и начало координат. Следующий сниппет позволяет создать направленный источник света, исходящий с правой стороны мира:

Мы можем преобразовать его в вектор:

Остальные атрибуты, вроде фонового или рассеянного цвета, идентичны аналогичным атрибутам точечного источника света. В листинге 11.4 показан код небольшого вспомогательного класса, использующегося для создания направленных источников света.

Листинг 11.4. Класс Directi onLi ght.java, простая абстракция направленных источников света в OpenGL ES

Этот вспомогательный класс практически идентичен классу PointLight. Единственное различие заключается в том, что в массиве directi on четвертый элемент равен единице. Кроме того, вместо метода setPosition появился метод setDirecti on . Он позволяет определять направление, например так: (-1; 0; 0), в этом случае свет будет исходить с правой стороны. Внутри метода все компоненты вектора меняют свой знак, таким образом мы преобразовываем направление к формату, ожидаемому OpenGL ES.

Определяем материалы

Материал определяется несколькими атрибутами. Как и в случае с любыми другими объектами OpenGL ES, материал – это состояние, которое будет активно до тех пор, пока мы не изменим его снова или пока не потеряется контекст OpenGL ES. Чтобы установить текущие атрибуты материалов, мы можем сделать следующее:

Как и обычно, нам необходимо определить фоновый, рассеянный и зеркальный RGBA-цвета. Это можно сделать так же, как и ранее, – с помощью массивов чисел с плавающей точкой, состоящих из четырех элементов.

Объединить эти действия в один вспомогательный класс очень просто. Результат вы можете увидеть в листинге 11.5.

Листинг 11.5. Класс Material Java, простая абстракция материалов OpenGL ES

Здесь тоже нет ничего удивительного. Мы просто сохраняем три компонента, описывающих материал, а также предоставляем функции для установки их значений и метод enabl е, которые передают их OpenGL ES.

У OpenGL ES есть еще один козырь в рукаве, когда речь идет о материалах. Обычно он вместо метода glMaterialfvO использует нечто, называемое цветом материала. Это означает, что вместо фонового и рассеянного цветов, определяемых методом glMateri al fv, OpenGL ES примет цвет вершин наших моделей в качестве фонового и рассеянного цветов материала. Чтобы разрешить использование этой особенности, необходимо просто вызвать ее:

Обычно я именно так и поступаю, потому что фоновый и рассеянный цвета часто одинаковы. Поскольку я не использую зеркальные блики в большинстве моих игр и демонстраций, я вполне могу применять такой способ и совсем не вызывать метод glMaterial fv. Какой способ задействовать вам – решаете только вы.

Определяем нормали

Чтобы в OpenGL ES работало освещение, необходимо определить нормали вершин для каждой вершины модели. Нормаль вершины должна представлять собой единичный вектор, указывающий (обычно) в ту сторону, в которую повернута поверхность, к которой принадлежит вершина. На рис. 11.5 проиллюстрированы нормали вершин для нашего куба.

Рис. 11.5. Нормали вершин для каждой вершины нашего куба

Нормаль вершины – это еще один атрибут вершины, такой же, как позиция или цвет. Чтобы воспользоваться нормалями вершин, нам необходимо еще раз изменить класс Verti ces3. Для того чтобы указать OpenGL ES, где он может найти нормали для каждой вершины, мы будем использовать метод gl Normal PointerO, точно так же, как мы ранее применяли методы gl VertexPointer или gl Col or Pointer . В листинге 11.6 показана финальная версия класса Verti ces3.

Листинг 11.6. Класс Vertices3.Java, финальная версия, поддерживающая нормали

В классе появился новый член hasNormal.s, отслеживающий, имеют ли вершины нормали.

Конструктор теперь принимает также параметр hasNormals. Нам еще необходимо модифицировать расчет члена vertexSize, добавив три числа с плавающей точкой на каждую вершину там, где это возможно.

Как вы можете видеть, методы setVertices и setlndices остаются без изменений.

В только что продемонстрированном методе bind О используем те же приемы с буфером ByteBuffer, что и ранее, но в этот раз добавляем нормали с помощью метода gl Normal Pointer . Для вычисления смещения указателя нормали необходимо принять в расчет то, заданы ли координаты текстур и цвета.

Как вы можете видеть, метод draw также не изменился; все действо происходит в методе bind О.

Наконец, мы несколько изменяем метод unbindO. Запрещаем использование указателей нормали, если таковые имелись, соответственно очищая состояние OpenGL ES.

Применить измененный класс Verti ces3 так же просто, как и ранее. Рассмотрим небольшой пример:

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

Все, что нам остается, – создать экземпляр класса Verti ces3 и установить значения вершин. Довольно легко, не правда ли?

Вся работа по привязке, рисованию и отвязке выполняется точно так же, как и в предыдущей версии класса. Как и ранее, мы можем добавить цвета вершин и координаты текстур.

Собираем все воедино

Соберем все вместе. Нам необходимо нарисовать сцену, имеющую глобальную подсветку, точечные и направленные источники света. Они будут освещать куб, расположенный в начале координат. Нам также нужно вызвать метод gl uLookAt, . чтобы расположить камеру. На рис. 11.6 показан внешний вид нашего мира.

Как и для всех прочих примеров, создадим класс, который будет называться LightTest, как обычно расширяющий класс GLGame. Он будет возвращать экземпляр класса LightScreen с помощью метода getStartScreenO. Класс LightScreen наследует от класса GLScreen (листинг 11.7).

Рис. 11.6. Наша первая освещенная сцена

Листинг 11.7. Фрагменты класса LightTest.java. создание освещения с помощью OpenGL ES

Начнем с описания нескольких членов класса. Член angle хранит информацию о текущем угле поворота куба вокруг оси у. Член Verti ces3 хранит вершины модели куба, которые мы скоро определим. В дополнение у нас есть экземпляры классов AmbientLight, PointLight и Di rectional Light, а также экземпляр класса Material.

Далее следует конструктор. Здесь создаются вершины модели куба, а также загружается текстура ящика. Мы также инициализируем источники освещения и материалы. Цвет подсветки – светло-зеленый. Направленный источник – красного цвета и располагается в точке (3; 3; 0) нашего мира. Направленный источник света имеет синий рассеянный цвет, свет падает слева. Для материала используем значения по умолчанию (несколько фоновый, белый для рассеянной составляющей и черный для зеркальной).

В методе resume убеждаемся, что наша текстура (пере)загружается, если контекст будет потерян.

Метод createCube практически не изменился с предыдущих примеров. Однако в этот раз мы добавляем нормали для каждой вершины, что показано на рис. 11.5. Помимо этого все остается прежним.

В методе update просто увеличиваем угол поворота куба.

Здесь уже интереснее. Первые несколько строк являются шаблонным кодом, предназначенным для очистки буфера цветов и глубины, разрешения тестирования глубины и установки области видимости.

Далее мы устанавливаем матрицу проекций равной перспективной матрице проекций с помощью метода gl uPerspecti ve, а также используем метод gl uLookAt для модельно-видовой матрицы, благодаря чему камера работает так же, как на рис. 11.6.

Затем разрешаем использование освещения. К этому моменту еще не определен ни один источник света, поэтому мы задаем их в следующих нескольких строках с помощью вызова метода для источников света и материалов.

Как обычно, также разрешаем текстурирование и привязываем текстуру ящика. Наконец, вызываем метод gl RotatefC) для поворота куба и затем отрисовываем его вершины с помощью удачно размещенных вызовов экземпляра класса Verti ces3.

В конце метода мы отключаем точечные и направленные источники освещения (помните, подсветка – это глобальное состояние), а также текстурирование и тестирование глубины. Это все, что касается освещения в OpenGL ES.

Остальная часть класса пуста; нам не нужно производить какие-либо действия в случае паузы. На рис. 11.7 показан результат работы программы.

Рис. 11.7. Сцена, изображенная на рис. 11.6, отрисованная с помощью OpenGL ES

Несколько примечаний к освещению в OpenGL ES

Хотя использование освещения может добавить изюминку вашей игре, у него есть свои ограничения и ловушки. Есть несколько моментов, о которых вы должны знать.

Использование освещения потребляет слишком много ресурсов, особенно это заметно на медленных устройствах. Применяйте освещение осторожно. Чем больше источников освещения вы опишете, тем больше вычислений потребуется, чтобы отрисовать сцену.

Определять позицию/направление точечных/направленных источников света следует после того, как будут загружены матрицы камеры и до того, как модельно-видовая матрица будет умножена на какие-либо другие матрицы для перемещения и поворота объектов. Это критично. Если не следовать этим указаниям, возможно появление необъяснимых световых артефактов.

При использовании метода gl Seal ef для изменения размера модели ее нормали также будут масштабированы. Это плохо, поскольку OpenGL ES ожидает, что нормали будут иметь параметры в заданных единицах измерения. Чтобы обойти эту проблему, вы можете использовать команду glEnable(GL10.GL NORMALIZE) или при некоторых обстоятельствах gl Enable(GL10 .GL RESCALE N0RMAL). Полагаю, следует использовать первую команду, поскольку применение второй имеет ограничения и подводные камни. Проблема заключается в том, что нормализация или повторное масштабирование нормалей требует большой вычислительной мощности. Лучшее решение с точки зрения производительности – не масштабировать освещенные объекты.

Вэтомурокемыбудемучитьсяосвещатьизатенятьнаши3дмодели.Вотсписоктого,чтомыизучим:

  • Каксделатьтак,чтобыобъектбылярчекогданаходитсяближекисточникусвета.
  • Каксделатьотблескикогдамывидимотраженныйсветнапредмете(specular lighting )
  • Как сделать, чтобы объект был немного затененный, когда свет падает не прямо на объект(diffuse lighting)
  • Подсветка сцены(ambient lighting)
  • Тени. Эта тема заслуживает отдельного урока(или уроков, если даже не книг).
  • Зеркальное отражение(например, вода)
  • Подповерхностное рассеивание(например, как у воска)
  • Анизотропные материалы(окрашенный металл, например)
  • Затенение основанное на физических процессах, чтобы имитировать реальность еще лучше.
  • Преграждениесвета(Ambient Occlusion есличто-топреграждаетсвет,тостановитсятемнее)
  • Отражение цвета(красный ковер будет делать белый потолок слегка слегка красноватым)
  • Прозрачность
  • Глобальное освещение(в принципе все что мы указали выше можно назвать этим термином)

Другими словами, самое простое освещение и затенение.

Нормали

В прошлом уроке мы работали с нормалями, но без особого понимания, зачем они вообще нужны.

Нормали Треугольников

Нормаль к плоскости — это единичный вектор который направлен перпендикулярно к этой плоскости.

Нормаль к треугольнику — это единичный вектор направленный перпендикулярно к треугольнику. Нормаль очень просто рассчитывается с помощью векторного произведения двух сторон треугольника(если вы помните, векторное произведение двух векторов дает нам перпендикулярный вектор к обоим) и нормализованный: его длина устанавливается в единицу.

Вот псевдокод вычисления нормали:

треугольник(v1, v2, v3)
сторона1 = v2-v1
сторона2 = v3-v1
треугольник.нормаль = вектПроизведение(сторона1, сторона2).нормализировать()

Вершинная Нормаль

Это нормаль введенная для удобства вычислений. Это комбинированная нормаль от нормалей окружающих данную вершину треугольников. Это очень удобно, так как в вершинных шейдерах мы имеем дело с вершинами, а не с треугольниками. В любом случае в OpenGL у мы почти никогда и не имеем дела с треугольниками.

вершина v1, v2, v3, ....
треугольник tr1, tr2, tr3 // они все используют вершину v1
v1.нормаль = нормализовать(tr1.нормаль + tr2.нормаль + tr3.нормаль)

Использование нормалей вершин в OpenGL

Использовать нормали в OpenGL очень просто. Нормаль — это просто атрибут вершины, точно так же, как и позиция, цвет или UV координаты...Тоесть ничего нового учить не придется...даже наша простенькая функция loadOBJ уже загружает нормали.

GLuint normalbuffer;
glGenBuffers(1, &normalbuffer);

glBufferData(GL_ARRAY_BUFFER, normals.size() * sizeof(glm::vec3), &normals, GL_STATIC_DRAW);

// Третий атрибутный буфер: нормали
glEnableVertexAttribArray(2);

glBindBuffer(GL_ARRAY_BUFFER, normalbuffer);
glVertexAttribPointer(
2, // атрибут
3, // размер
GL_FLOAT, // тип
GL_FALSE, // нормализованный ли?
0, // шаг
(void*)0 // смещение в буфере
);

И этого достаточно чтобы начать:


Диффузное освещение

Важность нормали к поверхности

Когда световой луч попадает на поверхность, большая его часть отражается во все стороны. Это называется «диффузная компонента». Остальные компоненты мы рассмотрим чуть позже.

После падения луча, поверхность отражает свет по разному, в зависимости от угла под которым падает этот луч к поверхности. Если луч падает перпендикулярно к поверхности, то он концентрируется на маленьком участке, если по касательной, то рассеивается на гораздо большей поверхности:


С точки зрения компьютерной графики, цвет пикселя очень зависит от разности углов направления света и нормали поверхности.


//
//
float cosTheta = dot(n,l);

В этом коде «n» - это нормаль, а «l» - единичный вектор который идет от поверхности к источнику света(а не наоборот, хотя это может показатьсянепонятным)

Будьте внимательны со знаком

Иногда наша формула будет не работать. Например, когда свет будет находиться за треугольником, n и l будут противоположны, поэтому n.l будет отрицательным. И в итоге у нас будет какой-то отрицательный цвет, и в итоге какой-то бред. Поэтому мы приведем все отрицательный числа к 0 с помощью функции clamp.

// Косинус угла между нормалью и направлением света
// 1 — если свет перпендикулярен к треугольнику
// 0 — если свет параллелен к треугольнику
// 0 — если свет позади треугольника
float cosTheta = clamp(dot(n,l), 0,1);
color = LightColor * cosTheta;

Цвет материала

Конечно цвет предмета должен очень сильно зависеть от цвета материала. Белый свет состоит из трех компонент — красного, синего и зеленого. Когда свет падает на красную поверхность, то зеленая и синяя компоненты поглощаются, а красная отражается.



Мы можем промоделировать это простым умножением:

color = MaterialDiffuseColor * LightColor * cosTheta;

Моделирование света

Давайте предположим, что у нас есть точечный источник света, который излучает свет во все направления, как, например, свечка.

С таким источником света, уровень освещения поверхности будет зависеть от расстояния до источника света: чем дальше, тем темнее. Эта зависимости рассчитывается так:

color = MaterialDiffuseColor * LightColor * cosTheta / (distance*distance);

Вскоре нам понадобится еще один параметр чтобы управлять уровнем силы света — цвет света, но пока, давайте предположим, что у нас есть лампочка белого света с определенной мощностью(например, 60 ватт).

color = MaterialDiffuseColor * LightColor * LightPower * cosTheta / (distance*distance);

Объединяем все вместе

Чтобы этот код работал нам нужен определенный набор параметров(цвета и мощности) и немного дополнительного кода.

MaterialDiffuseColor — мы можем взять прямо из текстуры.

LightColor и LightPower нужно будет выставить в шейдере с помощью GLSL uniform.

CosTheta будет зависеть от векторов n и l. Его можно вычислять для любого из пространств, угол будет одним и тем же. Мы будем использовать пространство камеры, так как тут очень просто посчитать положение светового источника:

// Нормаль фрагмента в пространстве камеры
vec3 n = normalize(Normal_cameraspace);
// Направление света(от фрагмента к источнику света
vec3 l = normalize(LightDirection_cameraspace);

Normal _cameraspace и LightDirection _ cameraspace подсчитываются в вершинном шейдере и передаются во фрагментный для дальнейшей обработки:

// Позиция вершины в пространстве камеры:МВП * положение
gl_Position = MVP * vec4(vertexPosition_modelspace,1);
// Положение вершины в мировом пространстве: M * положение
Position_worldspace = (M * vec4(vertexPosition_modelspace,1)).xyz;
// Вектор который идет от вершины камере в пространстве камеры
// В пространстве камеры, камера находится по положению (0,0,0)
vec 3 vertexPosition _ cameraspace = ( V * M * vec 4( vertexPosition _ modelspace ,1)). xyz ;
EyeDirection_cameraspace = vec3(0,0,0) - vertexPosition_cameraspace;
// Вектор который идет от вершины к источнику света в пространстве камеры.
//Матрица M пропущена, так как она в в этом пространстве единичная.
vec3 LightPosition_cameraspace = (V * vec4(LightPosition_worldspace,1)).xyz;
LightDirection_cameraspace = LightPosition_cameraspace +
EyeDirection_cameraspace;
// Нормаль вершины в пространстве камеры
Normal_cameraspace = (V * M * vec4(vertexNormal_modelspace,0)).xyz; // Будет работать лишь в том случае , когда матрица модели не изменяет её размер .

На первый взгляд код может показаться довольно сложным и запутанным, но на самом деле, тут нет ничего нового чего не было в уроке 3: Матрицы. Я старался давать каждой переменной осмысленные имена, чтобы вам было легко понять что и как тут происходит.

Обязательно попробуйте!!!

M и V – это матрицы Модели и Вида, которые передаются в шейдер точно так же, как и наша старая добрая MVP.

Время испытаний

Я рассказал вам все что нужно, чтобы сделать диффузное освещение. Вперед, попробуйте.

Результат

Только лишь с одной диффузной компонентой у нас получается вот такая вот картинка(простите меня за некрасивые текстуры).



Вроде бы как получше, чем было раньше, но многого еще не хватает. Особенно заметна проблема с неосвещенными частями. Затылок нашей дорогой мартышки Сюзанны полностью черный(мы ведь использовали clamp()).

Окружающее освещение(ambient lighting)

Окружающее освещение – это чистой воды читерство.

Затылок Сюзанны не должен быть полностью черным, так как в реальной жизни свет от лампы должен упасть на стену, пол, потолок, частично отразиться от него, и осветить теневую часть объекта.

Однако это слишком вычислительно затратно делать в реальном времени. И именно поэтому мы будем добавлять некую постоянную составляющую. Как будто сам объект излучает немного света, чтобы не быть полностью черным.

vec3 MaterialAmbientColor = vec3(0.1,0.1,0.1) * MaterialDiffuseColor;
color =
// Окружающее освещение : симулируем непрямое освещение
MaterialAmbientColor +
// Диффузное : " цвет " самого объекта
MaterialDiffuseColor * LightColor * LightPower * cosTheta /
(distance*distance);

Результат

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



Отраженный свет(Specular light)

Часть света которая отражается, в основном отражается в сторону отраженного луча к поверхности.



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

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


// вектор взгляда(в сторону камеры)
vec3 E = normalize(EyeDirection_cameraspace);
//Направление в котором треугольник отражает свет
vec 3 R = reflect (- l , n );
// Косинус угла между вектором взгляда и вектором отражения обрезанный до нуля если нужно
// - Смотрим прям на отражение -> 1
// -Смотрим куда-то в другую сторону -> < 1
float cosAlpha = clamp(dot(E,R), 0,1);
color =
// Окружающее освещение:симулируем непрямое освещение
MaterialAmbientColor +
// Диффузное : " цвет " самого объекта
MaterialDiffuseColor * LightColor * LightPower * cosTheta /
(distance*distance) ;
// Отраженное: отраженные отблески, как зеркало
MaterialSpecularColor * LightColor * LightPower * pow(cosAlpha,5) /

В следующем уроке мы будем разбирать, как можно ускорить рендеринг нашего VBO.