В этом уроке:
- рисуем графические примитивы
Исходники уроков доступны на гитхабе. Скачивайте проект, в нем будем использовать модуль lesson170_primitives.
На прошлом уроке мы разбирались, как передать в шейдеры данные о вершинах и получить в итоге треугольник. Чтобы этот механизм стал более понятен, попробуем развить тему, и создадим несколько примеров по передаче данных о вершинах и построения разных графических примитивов (точка, линия и треугольник) из этих вершин.
Треугольник
Сейчас наше приложение рисует один треугольник. Если мы посмотрим на класс OpenGLRenderer, то в методе prepareData увидим в нем список вершин:
float[] vertices = { -0.5f, -0.2f, 0.0f, 0.2f, 0.5f, -0.2f, };
Каждая пара значений – это координаты (x,y) одной вершины. Три пары = три вершины = треугольник.
Далее, в методе onDrawFrame мы используем метод glDrawArrays чтобы нарисовать треугольник.
@Override public void onDrawFrame(GL10 arg0) { glClear(GL_COLOR_BUFFER_BIT); glDrawArrays(GL_TRIANGLES, 0, 3); }
GL_TRIANGLES - тип примитива, который необходимо нарисовать, треугольник в нашем случае
0 - вершины надо брать из массива начиная с позиции 0, т.е. с самой первой
3 - означает, что для рисования необходимо использовать три вершины
Запускаем приложение
Теперь попробуем нарисовать 4 треугольника. Для этого нам понадобится больше вершин. 4 треугольника, в каждом по три вершины, значит нам нужно 3*4=12 вершин.
Перепишем массив vertices в методе prepareData
float[] vertices = { // треугольник 1 -0.9f, 0.8f, -0.9f, 0.2f, -0.5f, 0.8f, // треугольник 2 -0.6f, 0.2f, -0.2f, 0.2f, -0.2f, 0.8f, // треугольник 3 0.1f, 0.8f, 0.1f, 0.2f, 0.5f, 0.8f, // треугольник 4 0.1f, 0.2f, 0.5f, 0.2f, 0.5f, 0.8f, };
Теперь у нас есть 12 вершин из которых можно построить 4 треугольника.
Запускаем
Но видим только один треугольник вместо 4-х. Мы забыли сказать системе, что надо рисовать треугольники используя 12 вершин. Т.е. в метод glDrawArrays мы до сих пор передаем значение 3, а это значит что система возьмет из массива значения, чтобы нарисовать только три вершины.
Перепишем onDrawFrame
@Override public void onDrawFrame(GL10 arg0) { glClear(GL_COLOR_BUFFER_BIT); glDrawArrays(GL_TRIANGLES, 0, 12); }
В методе glDrawArrays укажем 12 вместо 3. Теперь система будет знать, что из массива ей необходимо использовать данные, чтобы сформировать 12 вершин для рисования треугольников, и мы получим 4 треугольника.
Запускаем
Видим два треугольника и прямоугольник. Этот прямоугольник на самом деле состоит из двух треугольников, расположенных впритык друг к другу.
Посмотрите на массив вершин и обратите внимание, что у треугольников 3 и 4 есть общие вершины (0.1f, 0.2f) и (0.5f, 0.8f). Вот по этой стороне они и соединились образовав прямоугольник.
В итоге мы получили 4 треугольника, два из которых выглядят, как один прямоугольник.
Типы треугольников
Чтобы нарисовать треугольник, мы передаем тип GL_TRIANGLES в метод glDrawArrays. Существует еще два типа отрисовки треугольников: GL_TRIANGLE_STRIP и GL_TRIANGLE_FAN.
В чем разница между ними? Смотрим рисунок
GL_TRIANGLES – каждые три переданные вершины образуют треугольник. Т.е.
v0, v1, v2 – первый треугольник
v3, v4, v5 – второй треугольник
GL_TRIANGLE_STRIP – каждый следующий треугольник использует две последние вершины предыдущего
v0, v1, v2 – первый треугольник
v1, v2, v3 – второй треугольник
v2, v3, v4 – третий треугольник
v3, v4, v5 – четвертый треугольник
GL_TRIANGLE_FAN – каждый следующий треугольник использует последнюю вершину предыдущего и самую первую вершину
v0, v1, v2 – первый треугольник
v0, v2, v3 – второй треугольник
v0, v3, v4 – третий треугольник
Рассмотрим эти типы на примерах
Задаем 6 вершин в prepareData:
float[] vertices = { 0.1f, 0.8f, 0.1f, 0.2f, 0.5f, 0.8f, 0.1f, 0.2f, 0.5f, 0.2f, 0.5f, 0.8f, };
тип GL_TRIANGLES и 6 вершин в glDrawArrays:
glDrawArrays(GL_TRIANGLES, 0, 6);
Запускаем
Получаем прямоугольник, составленный из двух треугольников.
Теперь нарисуем тот же прямоугольник, но чуть по-другому
Задаем 4 вершины
float[] vertices = { 0.1f, 0.8f, 0.1f, 0.2f, 0.5f, 0.8f, 0.5f, 0.2f, };
тип GL_TRIANGLE_STRIP и 4 вершины
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
Если у вас среда разработки ругается на константу GL_TRIANGLE_STRIP, то добавьте ее в импорт в начале класса:
import static android.opengl.GLES20.GL_TRIANGLE_STRIP;
И для всех последующих констант делайте аналогично.
Запускаем
Результат тот же, но в этот раз мы использовали 4 вершины, а не 6. Тип треугольника GL_TRIANGLE_STRIP помог немного сэкономить. В данном примере это конечно не особо критично, но, в целом, чем меньше вершин нам приходится передавать, тем выше скорость работы приложения.
Рассмотрим последний тип. Задаем 8 вершин
float[] vertices = { 0.0f, 0.0f, -0.4f, 0.4f, 0.4f, 0.4f, 0.8f, 0.0f, 0.4f, -0.4f, -0.4f, -0.4f, -0.8f, 0.0f, -0.4f, 0.4f, };
тип GL_TRIANGLE_FAN и 8 вершин
glDrawArrays(GL_TRIANGLE_FAN, 0, 8);
Запускаем
Получили шестиугольник. Для этого мы указали центральную вершину и вершины-углы, и в режиме GL_TRIANGLE_FAN система нарисовала шестиугольник.
Линия
Переходим к линии. Чтобы нарисовать линию нам необходимо указать две вершины.
Задаем их в массиве:
float[] vertices = { -0.9f, -0.9f, 0.9f, 0.9f, };
И перепишем onDrawFrame:
@Override public void onDrawFrame(GL10 arg0) { glClear(GL_COLOR_BUFFER_BIT); glDrawArrays(GL_LINES, 0, 2); }
Используем константу GL_LINES и указываем, что надо использовать две вершины.
Запускаем
С одной линией все понятно, попробуем нарисовать три линии.
Указываем вершины
float[] vertices = { // линия 1 -0.9f, -0.9f, 0.9f, 0.9f, // линия 2 -0.5f, 0.0f, 0.5f, 0.0f, // линия 3 0.0f, 0.7f, 0.0f, -0.7f, };
Не забываем указать в glDrawArrays, что необходимо использовать 6 вершин, и зададим толщину линии = 5.
@Override public void onDrawFrame(GL10 arg0) { glClear(GL_COLOR_BUFFER_BIT); glLineWidth(5); glDrawArrays(GL_LINES, 0, 6); }
Запускаем
Нарисованы три линии
Типы линий
Существует три типа отрисовки линий. Разберем их на примерах.
Тип GL_LINES мы уже использовали, он просто берет попарно вершины и рисует линии между ними. Т.е. если у нас есть вершины (v0, v1, v2, v3, v4, v5), то мы получим три линии (v0,v1), (v2,v3) и (v4,v5).
Задаем вершины
float[] vertices = { -0.4f, 0.6f, 0.4f, 0.6f, 0.6f, 0.4f, 0.6f, -0.4f, 0.4f, -0.6f, -0.4f, -0.6f, };
Указываем тип GL_LINES, 6 вершин
glDrawArrays(GL_LINES, 0, 6);
Запускаем
Каждая пара вершин образовала линию.
Тип GL_LINE_STRIP рисует линии не попарно, а последовательно между всеми вершинами. Т.е. если у нас есть вершины (v0, v1, v2, v3, v4, v5), то мы получим пять линий (v0,v1), (v1,v2), (v2,v3), (v3,v4) и (v4,v5).
Вершины мы будем использовать те же, а тип поменяем на GL_LINE_STRIP
glDrawArrays(GL_LINE_STRIP, 0, 6);
Запускаем
Линии нарисованы последовательно между всеми вершинами
Тип GL_LINE_LOOP аналогичен GL_LINE_STRIP, только он вдобавок еще рисует линию между первой и последней точкой.
Меняем тип на GL_LINE_LOOP
glDrawArrays(GL_LINE_LOOP, 0, 6);
Запускаем
Результат тот же, что и при GL_LINE_STRIP, плюс есть линия между первой и последней вершинами.
Точка
Осталось рассмотреть точку. Здесь уже нет никаких разных типов, только GL_POINTS.
Укажем его в glDrawArrays:
@Override public void onDrawFrame(GL10 arg0) { glClear(GL_COLOR_BUFFER_BIT); glDrawArrays(GL_POINTS, 0, 6); }
Вершины оставим те же.
Толщину точки можно задать в вершинном шейдере, используя переменную gl_PointSize.
vertex_shader.glsl
attribute vec4 a_Position; void main() { gl_Position = a_Position; gl_PointSize = 5.0; }
Запускаем
Нарисованы 6 точек
Ну и напоследок давайте нарисуем сразу несколько разных примитивов, например: 4 треугольника, 2 линии, 3 точки
4 треугольника – это 4 * 3 = 12 вершин
2 линии – это 2 * 2 = 4 вершины
3 точки – это 3 вершины
Итого нам нужно задать 12 + 4 + 3 = 19 вершин
float[] vertices = { // треугольник 1 -0.9f, 0.8f, -0.9f, 0.2f, -0.5f, 0.8f, // треугольник 2 -0.6f, 0.2f, -0.2f, 0.2f, -0.2f, 0.8f, // треугольник 3 0.1f, 0.8f, 0.1f, 0.2f, 0.5f, 0.8f, // треугольник 4 0.1f, 0.2f, 0.5f, 0.2f, 0.5f, 0.8f, // линия 1 -0.7f, -0.1f, 0.7f, -0.1f, // линия 2 -0.6f, -0.2f, 0.6f, -0.2f, // точка 1 -0.5f, -0.3f, // точка 2 0.0f, -0.3f, // точка 3 0.5f, -0.3f, };
Перепишем onDrawFrame
@Override public void onDrawFrame(GL10 arg0) { glClear(GL_COLOR_BUFFER_BIT); glLineWidth(5); glDrawArrays(GL_TRIANGLES, 0, 12); glDrawArrays(GL_LINES, 12, 4); glDrawArrays(GL_POINTS, 16, 3); }
Мы три раза вызываем метод glDrawArrays.
Первый вызов говорит системе о том, что надо нарисовать треугольники и использовать при этом 12 вершин, начиная с первой в массиве (индекс 0).
Второй вызов говорит системе о том, что надо нарисовать линии и использовать при этом 4 вершины, начиная с тринадцатой в массиве (индекс 12). Начинаем с тринадцатой потому, что первые 12 вершин в массиве мы использовали, чтобы задать треугольники, а вершины линий у нас идут начиная с тринадцатой.
Третий вызов говорит системе о том, что надо нарисовать точки и использовать при этом 3 вершины, начиная с семнадцатой в массиве (индекс 16). Начинаем с семнадцатой потому, что первые 12 вершин в массиве мы использовали, чтобы задать треугольники, следующие 4 вершины мы использовали, чтобы задать линии, а вершины точек у нас идут начиная с семнадцатой.
Запускаем
Система нарисовала 4 треугольника (2 их из них образуют прямоугольник), 2 линии и три точки.
Надеюсь, стала понятнее связь между массивом вершин и методом glDrawArrays. Т.е. в glDrawArrays мы указываем какие фигуры рисовать и сколько вершин для этого использовать, а данные по вершинам система берет из массива vertices. Возможно осталось непонятным, например, как система определяет, что для вершины надо брать именно 2 точки из массива vertices. Это мы подробно разберем на следующем уроке и алгоритм передачи данных в шейдеры станет полностью понятным.
Присоединяйтесь к нам в Telegram:
- в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
- в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance
- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня