Pull to refresh

Краткий курс компьютерной графики, аддендум: GLSL

ProgrammingGame development
Tutorial

Official translation (with a bit of polishing) is available here.




Очередная вводная статья для начинающих программировать графику реального времени


У меня когда-то возникла задача (быстро) визуализировать молекулы. Например, молекула может быть представлена просто как набор сфер навроде вот этого:



Конкретно этот вирус состоит из примерно трёх миллионов атомов. Вы можете скачать его модель на замечательном сайте rcsb.org.

Это отличный топик для обучения шейдерам.

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

OpenGL helloworld


Как обычно, я создал репозиторий для сопутствующего кода. В самом OpenGL нет нормального кроссплатформенного способа создать контекст для рендера, поэтому здесь я пользуюсь библиотекой GLUT, чтобы создать окно, хотя никакого взаимодействия с пользователем я толком не делаю. Заодно помимо GLUT для этого туториала нам понадобятся библиотеки GLU и GLEW.

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

Скрытый текст
#include <GL/glu.h>
#include <GL/glut.h>
#include <vector>
#include <cmath>

const int SCREEN_WIDTH  = 1024;
const int SCREEN_HEIGHT = 1024;
const float camera[]           = {.6,0,1};
const float light0_position[4] = {1,1,1,0};

void render_scene(void) {
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();
	gluLookAt(camera[0], camera[1], camera[2], 0,  0, 0, 0, 1, 0);
	glColor3f(.8, 0., 0.);
	glutSolidTeapot(.7);
	glutSwapBuffers();
}

void process_keys(unsigned char key, int x, int y) {
	if (27==key) {
		exit(0);
	}
}

void change_size(int w, int h) {
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glViewport(0, 0, w, h);
	glOrtho(-1,1,-1,1,-1,8);
	glMatrixMode(GL_MODELVIEW);
}

int main(int argc, char **argv) {
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
	glutInitWindowPosition(100,100);
	glutInitWindowSize(SCREEN_WIDTH, SCREEN_HEIGHT);
	glutCreateWindow("GLSL tutorial");
	glClearColor(0.0,0.0,1.0,1.0);

	glutDisplayFunc(render_scene);
	glutReshapeFunc(change_size);
	glutKeyboardFunc(process_keys);

	glEnable(GL_COLOR_MATERIAL);
	glEnable(GL_DEPTH_TEST);
	glEnable(GL_LIGHTING);
	glEnable(GL_LIGHT0);
	glLightfv(GL_LIGHT0, GL_POSITION, light0_position);

	glutMainLoop();
	return 0;
}



Давайте разбираться, причём начнём сразу с main().

  • Первая строчка просто инициализирует библиотеку, вторая говорит, что мы будем использовать двойной фреймбуффер, цвета и z-буфер.
  • Затем мы даём размеры, местоположение и заголовок окна и фоновый цвет, в данном случае синий.
  • Дальше начинаются интересные вещи: glutDisplayFunc, glutReshapeFunc и glutKeyboardFunc устанавливают коллбэки на наш код, который будет вызываться при событиях перерисовки экрана, изменения геометрии окна а также обработка клавиатуры.
  • Затем мы включаем некий набор чекбоксов, которые просто говорят, что да, у нас будет один источник освещения, что да, z-буфер надо использовать и тд.
  • Ну и финальный аккорд — вызов основного цикла обработки окна, покуда glutMainLoop работает, операционная система показывает наше окно.

Обработка клавиатуры у нас простейшая, я (несколько брутально) выхожу из программы при нажатии клавиши ESC. При изменении геометрии окна я говорю OpenGL, что проекция у нас по-прежнему ортогональная, и что он должен отобразить в полный размер окна квадрат с координатами [-1,1]x[-1,1].

Самое интересное у нас в функции render_scene().
  • Сначала стираем экран и соответствующий z-буфер
  • затем обнуляем матрицу ModelView и грузим в неё текущее положение камеры (в данном случае оно неизменно, поэтому можно было бы его определить в main())
  • Устанавливаем красный цвет
  • Рисуем чайник
  • переключаем экранные буферы


В итоге у нас должна получиться вот такая картинка:



GLSL helloworld


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

Картинка должна получиться вот такой:



Что именно добавилось в коде? Для начала добавились два новых файла: frag_shader.glsl и vert_shader.glsl, написанные не на C++, а на GLSL. Это код шейдеров, который будет скормлен непосредственно графической карте. А в main.cpp добавилась обвязка, которая говорит OpenGL, что нужно использовать эти шейдеры.

А именно, создаётся хэндлер prog_hdlr, и к нему линкуются предварительно прочитанные из текстовых файлов, а затем откомпилированные вершинный и фрагментный шейдеры.

Рисуем «молекулу» средствами стандартного OpenGL


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

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

Не забудьте посмотреть на изменения. Я добавил массив atoms, который содержит массивы из семи элементов: первые три это координаты центра текущего атома, затем его радиус, а затем ещё три его цвет.

Вот такая картинка должна получиться:



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

Могут ли нам помочь шейдеры?


Могут! Шейдеры — это не только изменение освещения, хотя изначально они задумывались именно для этого. Я хочу минимизировать перенос данных между CPU и GPU, поэтому буду отправлять только одну вершину на каждую сферу, которую нужно отрисовать.

Я пишу код под старый GLSL #120, т.к. мне необходимо, чтобы он исполнялся на очень древних машинах, новый GLSL имеет чуточку другой синтаксис, но общие идеи строго те же самые.

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

Итак, в чём состоит идея?

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

Скрытый текст


Далее в вершинном шейдере мы можем изменять gl_PointSize, это даст в итоге набор квадратов:

Скрытый текст

Обратите внимание, что фрагментный шейдер будет выполняться для каждого пикселя квадрата!

То есть, теперь всё совсем просто, мы считаем, насколько данный пиксель квадрата далёк от центра, если
он превышает радиус сферы, то мы вызываем discard:

Скрытый текст

Получили набор плоских конфетти, которые проходят через центр каждой сферы.

Что интересно, мы имеем право изменять глубину каждого фрагмента:

Скрытый текст


Осталось только посчитать освещение, вот такая картинка получится в итоге:



Сравните её с картинкой, где мы рисовали триангулированные сферы. Изображение существенно более аккуратное и существенно быстрее отрисовывается.

Теперь осталось добавить чтение .pdb файла, screen-space ambient occlusion и вы получите заглавную картинку этой статьи.




Для тех, кто хочет понять, как рисовать сферы при помощи шейдеров, но с перспективой, а не с glOrtho, есть прекрасная статья.
Tags:GLSLгеометрия для пятого класса
Hubs: Programming Game development
Total votes 52: ↑49 and ↓3 +46
Views43K
Python для работы с данными
May 26, 202131,500 ₽Нетология
Аналитика для руководителей
May 27, 202154,000 ₽Нетология
SEO для всех
July 12, 202121,000 ₽Loftschool
Game Design
July 15, 202160,500 ₽XYZ School