Изучаем OpenGL ES2 для Android Урок №2. Создание треугольников

Изучаем OpenGL ES2 для Android Урок №2. Создание треугольников

Основу кода и идеи я черпал отсюда: 1. Сатия Коматинени, Дэйв Маклин, Саид Хашими. Android 3 для профессионалов. Создание приложений для планшетных компьютеров и смартфонов.: Пер. с англ. – М.: ООО «И.Д.Вильямс». 2012 – 1024 с. 2. http://www.learnopengles.com/android-lesson-one-getting-started/

На первом уроке (можно посмотреть здесь https://habrahabr.ru/post/278895/ или здесь albatrossgames.blogspot.com/2016/03/opengl-es-2-android-1-opengl.html#more ) мы с вами научились заливать экран одним цветом с помощью OpenGL ES. Пришла пора рисовать треугольники, а точнее, с помощью треугольников мы нарисуем парусник, который будет циклично двигаться слева направо.

Почему треугольники? Дело в том, что в OpenGL есть только три графических примитива: точка, линия и треугольник. Корпус яхты (трапеция) и море (прямоугольник) нарисованы тоже с помощью треугольников. Как известно, точку в нашем пространстве определяют три координаты (x, y, z). Так как наш рисунок плоский, то у всех точек рисунка одна координата по оси 0z (она перпендикулярна плоскости экрана и идет на нас) будет равна нулю. Для примера я указал координаты по оси 0х и 0у двух крайних точек большого паруса (грота).

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

Как вы видите, создается массив данных с плавающей запятой. Для каждой точки указывается её координата и цветовая гамма. Первая точка у нас белая, поэтому весовые коэффициенты у красного, зеленого и синего одинаковы и равны 1, а вот две остальные вершины я подсинил для красоты. Обход точек делается против часовой стрелки Размерность координат в OpenGL условная и будет фактически определяться количеством пикселей экрана устройства по ширине и высоте. Теперь нужно создать буфер, куда мы перекачаем данные о точках для OpenGL. Связано это с тем, что OpenGL написан на С-подобном языке. Поэтому мы переводим наши данные в другой вид, понятный для OpenGL, выделяя память.

Давайте рассмотрим каждую часть. Во-первых, мы выделили блок машинной памяти, используя ByteBuffer.allocateDirect (); эта память не будет управляться Сборщиком мусора (что важно). Необходимо сказать методу, насколько большой блок памяти должен быть в байтах. Так как наши вершины хранятся в массиве в виде переменных float и занимают 4 байта на каждый float, мы передаем triangle1VerticesData.length * mBytesPerFloat. (Напомню, что private final int mBytesPerFloat = 4;)

Следующая строка говорит байтовому буферу, как он должен организовывать свои байты в машинном коде. Когда дело доходит до значений, которые охватывают несколько байтов, таких как 32-разрядные целые числа, байты можно записывать в разном порядке, например, от наиболее значимого значения до наименее значимого. Это похоже на написание большого числа либо слева направо или справа налево. Нам это всё равно, но важно то, что мы используем один и тот же порядок, что и система. Мы организуем это, вызывая order(ByteOrder.nativeOrder()). Наконец, лучше не иметь дело с отдельными байтами напрямую. Мы хотим работать с floats, поэтому вызываем FloatBuffer (), чтобы получить FloatBuffer, который содержит основные байты. Затем копируем, начиная с нулевой позиции, данные из памяти Dalvik в машинную память, вызывая mTriangle1Vertices.put(triangle1VerticesData).position(0);

Память будет освобождена, когда процесс прекращается, поэтому нам не нужно беспокоиться об этом.

Понимание матриц Хороший урок для понимания матриц www.songho.ca/opengl/gl_transform.html

Чтобы понять матрицы, нужно для начала разобраться, как мы «видим» объект в OpenGL. Представьте, что вы держите в руках фотоаппарат и хотите сфотографировать наш парусник.

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

В начале, мы установили цвет фона сине-серый, аналогично, как делали в первом занятии.

Потом разместили камеру, фактически указали координаты т.К. Как вы видите, камера у нас сдвинута по оси 0z на 1,5 единицы (расстояние ОК).

В следующих строчках кода мы указываем координату точки, в которую смотрит камера.

Следующие строчки кода определяют, как ориентирована камера или положение up vector. Из-за неудачных переводов, в разных статьях допущены неточности в этом вопросе. В нашем коде сейчас

Это значит, что камера размещена обычно, так, если бы вы положили её на горизонтальный стол и смотрит на парусник в т.0.

Теперь представьте, что из камеры выходят три вектора, как из т.О. Поставив весовой коэффициент final float upY = 1.0f, мы говорим OpenGL, что вверх будет направлена ось 0У и видим картинку, как в начале статьи. Но стоит нам поставить вот такие коэффициенты

мы увидим на эмуляторе следующее. Наш парусник будет карабкаться по наклонной вверх.

Камера повернулась на 45 градусов против часовой стрелки, если смотреть на ось 0z. Понятно, что если сделать такую комбинацию

вверх будет смотреть ось 0х камеры и кораблик будет плыть вертикально вверх. Все наши данные мы передаем методу setLookAtM. Matrix.setLookAtM(mViewMatrix, 0, eyeX, eyeY, eyeZ, lookX, lookY, lookZ, upX, upY, upZ);

Видимый объем камеры

Рассмотрим следующий кусок кода.

Метод onSurfaceChanged позволяет отработать изменение ориентации самого устройства. Повернем на эмуляторе наш гаджет и видим такую картину

Не очень красиво, но в принципе то, что мы нарисовали. Следующая строчка кода устанавливает размер экрана. Сначала устанавливаем координаты нижней точки левого угла экрана (0,0), а потом ширину и высоту экрана. GLES20.glViewport(0, 0, width, height); Давайте еще раз рассмотрим наш рисунок:

Объем, который видит наша камера, заключен в объеме усеченной пирамиды (A1,B1,C1,D1, A2,B2,C2,D2). Сначала мы находим отношение ширины к высоте устройства (ratio). final float ratio = (float) width / height; Потом задаем координату по 0х левой и правой стороны видимого параллелепипеда (A1,B1,C1,D1). final float left = -ratio; final float right = ratio; Задаем координату по 0у нижней и верхней стороны параллелепипеда (A1,B1,C1,D1). final float bottom = -1.0f; final float top = 1.0f; Расстояние от камеры до передней стороны (КО1) final float near = 1.0f; Расстояние от камеры до задней стороны (КО2) final float far = 10.0f; Применяем матричное преобразование Matrix.frustumM(mProjectionMatrix, 0, left, right, bottom, top, near, far); Есть несколько различных видов матриц, которые мы используем: 1. Матрица модели. Эта матрица используется для размещения модели где-то в «мире». Например, если у вас есть модель автомобиля, и вы захотите расположить её на расстоянии 1000 м на восток, вы будете использовать матрицу модели. 2. Матрица вида. Эта матрица представляет собой камеру. Если мы хотим, посмотреть на нашу машину, которая находится в 1000 м на восток, мы должны передвинуть себя на 1000 м на восток. Или можно остаться неподвижными, а весь остальной мир передвинуть на 1000 м на запад. Чтобы сделать это, мы будем использовать матрицу вида. 3. Матрица проекции. Так как наши экраны являются плоскими, то нам нужно сделать окончательное преобразование в «проект» нашего вида на экране и получить 3D-перспективу. Для этого используют матрицу проекции.

Определение вершинных и фрагментных шейдеров Даже самые простые рисунки в OpenGL ES 2.0 требуют создания программных сегментов, которые называются шейдерами. Шейдеры формируют ядро OpenGL ES 2.0. Вершины обрабатываются вершинными шейдерами и имеют дело только с точками вершин. Пространства между вершинами обрабатывают с помощью фрагментных шейдеров, они имею дело с каждым пикселем на экране.

Для написания шейдеров используют язык программирования OpenGL Shading Language (GLSL). Каждый шейдер в основном состоит из ввода, вывода и программы. Сначала мы определяем форму, которая представляет собой комбинированную матрицу, содержащую все наши преобразования. Она постоянна для всех вершин и используется для проецирования их на экран. Затем мы определяем два атрибута для позиции и цвета. Эти атрибуты будут прочитаны из буфера, которое мы определили ранее, они задают положение и цвет каждой вершины. Затем мы определим варьирование (изменение цвета), который интерполирует значения для всего треугольника и передаст его во фрагментный шейдер. Когда дело доходит до фрагментного шейдера, то он будет содержать интерполированное значение для каждого пикселя.

Скажем, мы определили треугольник, у которого вершины будут красная, зеленая и синяя, а его размер будет занимать 10 пикселей на экране. При выполнении фрагментного шейдера, он будет содержать различный изменяющийся цвет для каждого пикселя. В какой-то точке, что изменение будет красным, но на полпути между красным и синим, это может быть пурпурный цвет.

Рассмотрим наш вершинный шейдер

Через униформы (uniform) в шейдеры передаются внешние данные, которые могут быть использованы для расчетов. Униформы могут быть использованы только для чтения. Униформы могут быть переданы как в вершинный, так и в фрагментный шейдеры. В нашем случае униформа одна — это матрица модели-вида-проекции u_MVPMatrix и передается она в вершинный шейдер. Ключевое слово mat4 означает, что это матрица размером 4х4 состоящая из чисел с плавающей точкой. Униформы никак не связаны с конкретной вершиной и являются глобальными константами. Для названия униформ обычно используют префикс u_. Атрибуты (attribute) — это свойство вершины. У вершины могут быть различные атрибуты. Например, координаты положения в пространстве, координаты вектора нормали, цвет. Кроме того, вы можете передавать в вершинный шейдер какие-либо свои атрибуты. Важно понять, что атрибут-это свойство вершины и поэтому он должен быть задан для каждой вершины. Атрибуты передаются только в вершинный шейдер. Атрибуты доступны вершинному шейдеру только для чтения. Нельзя определять атрибуты во фрагментном шейдере. В дальнейшем для удобства будем обозначать атрибуты с префиксом a_.

Определим в вершинном шейдере три атрибута: attribute vec4 a_Position; Переменная a_Position – атрибут вершины, который имеет дело с положением вершины (координатами), это четырехкомпанентный вектор (vec4). Атрибут цвета вершины attribute vec4 a_Color; Атрибут интерполяции цвета varying vec4 v_Color; Рассмотрим код функции main подробнее: v_Color = a_Color; Передаем информацию о цвете вершин во фрагментный шейдер. gl_Position = u_MVPMatrix * a_Position; Трансформируем положение вершин с помощью матрицы и записываем в новую переменную gl_Position. Системная переменная gl_Position — это четырех-компонентный вектор, определяющий координаты вершины, спроецированные на плоскость экрана. Переменная gl_Position обязательно должна быть определена в вершинном шейдере, иначе на экране мы ничего не увидим.

Приступим к рассмотрению фрагментного шейдера.

Точность по умолчанию устанавливаем среднюю, так как она не нужна нам высокой в случае фрагментного шейдера. В вершинном шейдере точность по умолчанию высокая. precision mediump float; Конечная цель фрагментного шейдера — это получение цвета пикселя. Рассчитанный цвет пикселя должен быть обязательно записан в системную переменную gl_FragColor. В нашем простейшем примере мы не вычисляем цвет пикселя во фрагментном шейдере, а просто присваиваем значение цвета v_color, полученного путем интерполяции из цветов вершин: gl_FragColor = v_color;

Загрузка шейдеров в OpenGL

Связывание вершинного и фрагментного шейдеров вместе в программе

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

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

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

Установка перспективной проекции

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

Вывод объектов на экран Вывод производим в методе onDrawFrame(GL10 glUnused) Чтобы треугольники двигались по оси 0х, применяем матрицу перемещения и задаем увеличение смещения по х на 0,001 за каждое обновление поверхности. Как только х достигает 1 или правого края экрана, обнуляем его.

📎📎📎📎📎📎📎📎📎📎