Разработка мобильных приложений. Программирование мобильных устройств на языке Kotlin. Учебное пособие для СПО
.pdf(например, треугольника) во время рендеринга. Обычно используются для передачи матриц преобразования, цветов, положения источников света и других глобальных параметров.
4.Varying-переменные (Varyings): Переменные, которые передаются из вершинного шейдера во фрагментный шейдер. Значения varyingпеременных интерполируются между вершинами примитива. Обычно используются для передачи текстурных координат, цветов и других данных, которые должны изменяться плавно по поверхности примитива.
5.Буферы вершин (Vertex Buffers): Области памяти, в которых хранятся
данные вершин (положение, нормали, текстурные координаты и т.д.).
6. Объекты вершинных массивов (Vertex Array Objects - VAOs): (Необязательно, но рекомендуются) Объекты, которые содержат информацию о конфигурации вершинных буферов (какой буфер использовать для какого атрибута, как интерпретировать данные). VAO упрощают переключение между различными конфигурациями вершин.
7.Текстуры: Изображения, которые используются для добавления деталей
иреализма в сцену.
8.Буфер кадра (Framebuffer): Область памяти, в которой хранится изображение, которое будет отображаться на экране.
9.Программы (Programs): Объекты, которые содержат скомпилированные
исвязанные вместе вершинный и фрагментный шейдеры.
Модель Фонга. Ч. 2
В OpenGL ES 2.0 модель освещения Фонга реализуется непосредственно в шейдерах, что дает полный контроль над расчетами освещения.
Опишем алгоритм: 1. Передача данных:
o Позиция вершины (Vertex Position): Координаты вершин передаются как атрибут (a_Position).
oНормаль вершины (Vertex Normal): Нормали к поверхности в каждой вершине передаются как атрибут (a_Normal). Очень важно:
нормали должны быть нормализованы!
o Матрица модели-вида (Model-View Matrix): Матрица,
преобразующая координаты вершин из пространства объекта в пространство камеры, передается как uniform-переменная
(u_MVMatrix).
oМатрица проекции (Projection Matrix): Матрица, преобразующая координаты из пространства камеры в пространство отсечения, передается как uniform-переменная (u_PMatrix).
42
41
oПозиция источника света (Light Position): Положение источника света в пространстве камеры передается как uniform-переменная
(u_LightPosition).
o Цвет источника света (Light Color): Цвет источника света
(ambient, diffuse, specular) передается как uniform-переменные
(u_LightAmbient, u_LightDiffuse, u_LightSpecular).
oСвойства материала (Material Properties): Свойства материала
(ambient, diffuse, specular, shininess) передаются как uniform-
переменные
(u_MaterialAmbient, u_MaterialDiffuse, u_MaterialSpecular, u_Materia lShininess).
2.Вершинный шейдер:
oПреобразует координаты вершин и нормали из пространства объекта
в пространство камеры.
oВычисляет вектор нормали в пространстве камеры и передает его во фрагментный шейдер как varying-переменную.
oВычисляет положение вершины в пространстве камеры и передает его во фрагментный шейдер как varying-переменную (опционально, но полезно для некоторых расчетов).
3.Фрагментный шейдер:
oВычисляет компоненты ambient, diffuse и specular освещения на
основе формул, описанных выше.
oКомбинирует компоненты освещения для получения итогового цвета фрагмента.
class MyGLRenderer : GLSurfaceView.Renderer {
// Шейдерный код
private val vertexShaderCode = """
uniform mat4 u_MVMatrix; // Model-View matrix uniform mat4 u_PMatrix; // Projection matrix
attribute vec4 a_Position; attribute vec3 a_Normal;
varying vec3 v_Normal;
varying vec3 v_Position; // Position in camera space (optional)
void main() {
v_Position = (u_MVMatrix * a_Position).xyz; // Calculate position in camera space
v_Normal = mat3(u_MVMatrix) * a_Normal; // Transform normal to camera space
gl_Position = u_PMatrix * u_MVMatrix * a_Position;
}
43
42
private val fragmentShaderCode = """
precision mediump float; |
|
uniform vec3 u_LightPosition; |
// Light position in camera space |
uniform vec3 u_LightAmbient; |
// Ambient light color |
uniform vec3 u_LightDiffuse; |
// Diffuse light color |
uniform vec3 u_LightSpecular; |
// Specular light color |
uniform vec3 u_MaterialAmbient; // Ambient material color uniform vec3 u_MaterialDiffuse; // Diffuse material color uniform vec3 u_MaterialSpecular;// Specular material color uniform float u_MaterialShininess; // Shininess exponent
varying vec3 v_Normal; |
// Interpolated normal (in camera space) |
varying vec3 v_Position; |
// Interpolated position (in camera space) |
void main() {
vec3 normal = normalize(v_Normal); // Normalize the interpolated
normal
// Vector from fragment to light
vec3 lightVector = normalize(u_LightPosition - v_Position);
// Ambient
vec3 ambient = u_LightAmbient * u_MaterialAmbient;
// Diffuse
float diffuseIntensity = max(0.0, dot(normal, lightVector));
vec3 diffuse = u_LightDiffuse * u_MaterialDiffuse * diffuseIntensity;
// Specular
vec3 viewVector = normalize(-v_Position); // Vector from fragment to
camera
vec3 reflectionVector = reflect(-lightVector, normal);
float specularIntensity = pow(max(0.0, dot(viewVector, reflectionVector)), u_MaterialShininess);
vec3 specular = u_LightSpecular * u_MaterialSpecular * specularIntensity;
gl_FragColor = vec4(ambient + diffuse + specular, 1.0);
}
44
43
• Шейдеры:
oВершинный шейдер:
▪u_MVMatrix, u_PMatrix: Uniform-переменные для матриц Model-View и Projection.
▪a_Position, a_Normal: Атрибуты для координат вершин и нормалей.
▪v_Normal: Varying-переменная для передачи нормали во фрагментный шейдер (в пространстве камеры).
▪v_Position: Varying-переменная для передачи положения вершины во фрагментный шейдер (в пространстве камеры). (Можно убрать, если позиция во фрагментном шейдере не нужна).
▪mat3(u_MVMatrix): Извлекает верхнюю левую 3x3 часть матрицы ModelView для преобразования нормалей. Это важно, чтобы сохранить правильное направление нормалей при масштабировании. Важно: Это работает корректно, только если масштабирование является равномерным (isotropic). Если масштабирование неравномерное, потребуется более сложный расчет с использованием обратной транспонированной матрицы.
o Фрагментный шейдер:
▪u_LightPosition: Uniform-переменная для положения источника света.
▪ u_LightAmbient, u_LightDiffuse, u_LightSpecular: |
Uniform- |
переменные для компонентов цвета источника света. |
|
▪u_MaterialAmbient, u_MaterialDiffuse, u_MaterialSpecular, u_M aterialShininess: Uniform-переменные для свойств материала.
▪v_Normal, v_Position: Varying-переменные, полученные из вершинного шейдера.
▪Расчет освещения:
▪normalize(v_Normal): Нормализует интерполированную нормаль. Обязательно нормализуйте нормаль, поскольку интерполяция может изменить ее длину.
▪normalize(u_LightPosition - v_Position): Вычисляет и нормализует вектор направления света. (Источник света находится в пространстве камеры)
▪dot(normal, lightVector): Вычисляет скалярное произведение между нормалью и вектором направления света. Это дает косинус угла между ними.
▪max(0.0, ...): Гарантирует, что diffuse и specular
компоненты не будут отрицательными.
▪reflect(-lightVector, normal): Вычисляет вектор отражения света.
▪pow(max(0.0, dot(viewVector, reflectionVector)), u_MaterialShininess): Вычисляет specular intensity на
45
44
основе угла между вектором отражения и вектором обзора, а также коэффициента shininess.
• Код Kotlin:
oloadShader() и createProgram(): Функции для загрузки, компиляции и связывания шейдеров (из предыдущих примеров).
oglGetAttribLocation(), glGetUniformLocation(): Получают handles для атрибутов и uniform-переменных.
o |
glEnableVertexAttribArray(), glVertexAttribPointer(): |
Настраивают |
|
атрибуты вершин. |
|
o |
glUniformMatrix4fv(), glUniform3fv(), glUniform1f(): |
Передают |
|
значения uniform-переменных в шейдеры. |
|
o |
glDrawArrays(): Рисует куб. |
|
oПреобразование координат источника света: Перед передачей в шейдер, положение источника света, заданное в мировом
пространстве, преобразуется в пространство камеры. Это необходимо, так как все расчеты освещения производятся во фрагментном шейдере в пространстве камеры.
Важные замечания:
•Нормализация нормалей: Нормализация нормалей абсолютно необходима для правильного освещения. Если вы используете неравномерное масштабирование, нормализацию необходимо делать в каждом кадре.
•Пространство координат: Убедитесь, что все векторы (позиция вершины, нормаль, позиция источника света) находятся в одном и том же пространстве координат (обычно это пространство камеры).
•Производительность: Модель Фонга может быть ресурсоемкой, особенно если используется высокое значение u_MaterialShininess. Для оптимизации производительности можно использовать более простые модели освещения, такие как модель Гуро (Gouraud shading), где освещение вычисляется для каждой вершины, а затем интерполируется между фрагментами.
•Типы данных GLSL: Убедитесь, что типы данных, передаваемые из Kotlin в шейдеры, соответствуют типам данных в шейдерах.
•Матрица для нормалей: Для правильного преобразования нормалей используйте обратную транспонированную матрицу (inverse transpose matrix) матрицы ModelView, особенно при неравномерном масштабировании. В этом примере используется
упрощенный mat3(u_MVMatrix), который работает только при равномерном масштабировании.
46
45
ПРАКТИЧЕСКАЯ РАБОТА 5
Нажимая кнопки влево и вправо, куб перемещается по планетам/спутникам. По нажатию кнопки информация и выбранном элементе Луна на новом экране выводится изображение Луны, освещенное по модели Фонга.
47
46
Карта теней
Тень – это отсутствие света. Если лучи от источника света не попадают на объект, так как поглощаются другим объектом, то первый объект находится в тени. Тени добавляют реализма к изображению и дают увидеть взаимное расположение объектов. Благодаря ним сцена приобретает "глубину".
Существуют несколько хороших способов для приблизительного расчёта теней, но они все имеют свои особенности, которые надо принимать во внимание.
Один из методов – карты теней (shadow maps) – относительно простой в реализации, используется в большинстве видеоигр и даёт достойные результаты. Карты теней не так уж и трудно понять, они довольно дёшевы с точки зрения производительности и их легко улучшить до более продвинутых алгоритмов.
Сначала создадим кадровый буфер для рисования карты глубины:
unsigned int depthMapFBO; glGenFramebuffers(1, &depthMapFBO);
После создадим 2д текстуру, чтобы использовать её качестве буфера глубины для кадрового буфера.
const unsigned int SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024; unsigned int depthMap;
glGenTextures(1, &depthMap); glBindTexture(GL_TEXTURE_2D, depthMap);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, SHADOW_WIDTH, SHADOW_HEIGHT, 0,
GL_DEPTH_COMPONENT, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
Создание карты глубины не выглядит сложным. Так как нас интересуют только значения глубины (а не цвета r,g,b,a), мы указываем формат текстуры GL_DEPTH_COMPONENT. Устанавливаем высоту и ширну текстуры 1024*1024 – это будет размер карты глубины.
Теперь присоединим текстуру глубины к кадровому буферу в качестве буфера глубины:
48
47
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMap, 0);
glDrawBuffer(GL_NONE); glReadBuffer(GL_NONE); glBindFramebuffer(GL_FRAMEBUFFER, 0);
Когда мы рисуем сцену с точки зрения источника света, нас интересует только глубина, буфер цвета не нужен. Кадровый буфер будет неполным без буфера цвета, поэтому мы должны явно указать, что не собираемся рендерить цвет. Для этого мы устанавливаем GL_NONE для glDrawBuffer и glReadBuffer.
Теперь у нас есть правильно настроенный кадровый буфер, которые записывает значения глубины в текстуру, и мы можем рендерить карту глубины. Полная реализация для обоих проходов рендеринга выглядит примерно так:
// 1. сначала рисуем карту глубины
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT); glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO); glClear(GL_DEPTH_BUFFER_BIT); ConfigureShaderAndMatrices();
RenderScene();
// 2. рисуем сцену как обычно с тенями (используя карту глубины) glBindFramebuffer(GL_FRAMEBUFFER, 0);
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); ConfigureShaderAndMatrices(); glBindTexture(GL_TEXTURE_2D, depthMap);
RenderScene();
Этот код не содержит некоторых подробностей, но он даёт общую идею карт теней. Обратите внимание на вызовы glViewport: обычно размер карты глубины отличается от размера экрана (или текстуры, в которую рендерится итоговое изображение). Если вы забудете его поменять, то текстуре глубины будет обновлён только квадратный кусочек с размером экрана или (если текстура меньше) часть информации на ней не будет отображена (останется за краями).
Мы моделируем направленный источник света, поэтому все лучи света параллельны. По этой причине мы будет использовать ортографическую матрицу проекции для источника света (в ней нет перспективных искажений).
float near_plane = 1.0f, far_plane = 7.5f;
glm::mat4 lightProjection = glm::ortho(-10.0f, 10.0f, -10.0f, 10.0f, near_plane, far_plane);
Когда мы рендерим сцену для источника света, нам нужны только координаты вершин и ничего больше. Для такого простого шейдера (назовём его simpleDepthShader) напишем вершинный шейдер:
49
48
layout (location = 0) in vec3 aPos; uniform mat4 lightSpaceMatrix; uniform mat4 model;
void main()
{
gl_Position = lightSpaceMatrix * model * vec4(aPos, 1.0);
}
Для каждой модели этот шейдер отображает вершины модели в пространство источника света с помощью lightSpaceMatrix.
Так как у нас нет цветового буфера в буфере кадров для теней, фрагментный шейдер не требует никаких вычислений, и мы может оставить его пустым.
Пустой фрагментный шейдер ничего не делает, и в конце работы шейдера мы получим обновлённый буфер глубины. Мы можем раскомментировать эту линию кода, реально глубина будет вычислена в любом случае.
Рисование в буфер глубины превращается в следующий код:
simpleDepthShader.use();
glUniformMatrix4fv(lightSpaceMatrixLocation, 1, GL_FALSE, glm::value_ptr(lightSpaceMatrix));
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT); glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO); glClear(GL_DEPTH_BUFFER_BIT); RenderScene(simpleDepthShader); glBindFramebuffer(GL_FRAMEBUFFER, 0);
Функция RenderScene принимает шейдер, вызывает необходимые для рисования функции и устанавливает матрицы модели при необходимости.
В результате у нас заполненный буфер глубины, для каждого пикселя содержащий глубину ближайшего фрагмента с точки зрения света. Можно спроецировать эту текстуру на прямоугольник размером с экран и показать её.
50
49
Шум Перлина
Шум Перлина (Perlin noise) – это процедурный метод генерации псевдослучайного шума, созданного Кеном Перлином в 1983 году. Он широко используется в компьютерной графике для создания реалистичных текстур, ландшафтов, облаков, огня и других визуальных эффектов. Шум Перлина имеет несколько важных свойств, которые делают его полезным:
•Детерминированность: Для заданного набора входных параметров всегда генерируется один и тот же шум.
•Масштабируемость: Шум может быть масштабирован для создания различных уровней детализации.
•Непрерывность: Шум изменяется плавно, без резких скачков.
•Изотропность: Шум выглядит одинаково во всех направлениях.
Принцип работы шума Перлина можно описать следующим образом:
1. Решетка (Grid): Пространство (1D, 2D, 3D и т.д.) делится на решетку с целочисленными координатами.
2. Градиенты (Gradients): Каждому узлу решетки назначается случайный градиентный вектор единичной длины. В 2D пространстве это вектор (x, y)
такой, что x² + y² = 1.
3. Точечное произведение (Dot Product): Для каждой точки в пространстве, для которой необходимо вычислить шум, находятся ближайшие узлы решетки. Для каждого из этих узлов вычисляется вектор, соединяющий узел с точкой, и берется точечное произведение этого вектора с градиентным вектором узла. Это значение показывает, насколько сильно градиент узла “смотрит” в направлении точки.
4.Интерполяция (Interpolation): Значения точечных произведений, полученные для ближайших узлов, интерполируются для получения окончательного значения шума для точки. Для интерполяции используются гладкие функции, такие как кривая Hermite или функция плавного шага (Smoothstep).
5.Октавы (Octaves): Для добавления большего уровня детализации шум Перлина обычно генерируется несколько раз с разными частотами и амплитудами, а затем результаты суммируются. Каждая такая генерация называется октавой. Частота удваивается, а амплитуда уменьшается вдвое с каждой октавой. Это создает иерархию деталей, где крупные формы определяются низкими частотами, а мелкие детали – высокими частотами.
Математическое описание:
Пусть x – координата точки, для которой нужно вычислить шум.
1. Находим координаты узлов решетки:
ox0 = floor(x) – координата левого узла решетки.
ox1 = x0 + 1 – координата правого узла решетки.
2.Вычисляем градиентные векторы в узлах решетки:
og0 = gradient(x0) – градиентный вектор в левом узле.
og1 = gradient(x1) – градиентный вектор в правом узле.
3.Вычисляем векторы от узлов решетки до точки:
51
50
