Предисловие


В прошлом уроке Вы научились создавать окно и собирать примеры. В этом уроке Вы научитесь рисовать объекты! Что же, прошу под кат.

Содержание


Базовые уроки:
  • Урок 1. Создание окна
  • Урок 2. Первый треугольник
  • Урок 3. Матрицы
  • Урок 4. Цветной куб
  • Урок 5. Текстурированный куб
  • Урок 6. Клавиатура и мышь
  • Урок 7. Загрузка моделей
  • Урок 8. Базовый шейдинг

Продвинутые уроки:
  • Урок 9. VBO индексация
  • Урок 10. Прозрачность
  • Урок 11. 2D текст
  • Урок 12. OpenGL расширения
  • Урок 13. Normal Mapping
  • Урок 14. Отрисовка на текстуру
  • Урок 15. Lightmaps
  • Урок 16. Shadow mapping
  • Урок 17. Вращение
  • Урок 18.1. «Билборды»
  • Урок 18.2. Частицы

Всякое:
  • Урок 19. FPS счетчик
  • Урок 20.1. Нажатие на объекты с помощью OpenGL хака
  • Урок 20.2. Нажатие на объекты с помощью физического движка
  • Урок 20.3. Нажатие на объекты с помощью собственного raycastingа


Статья


Предисловие


Это будет еще один длинный урок.

OpenGL 3 позволяет с легкостью делать довольно сложные вещи, но в противовес этому заставляет совершать слишком много действий для отрисовки простого треугольника.

Не забывайте тестировать код из статьи на регулярной основе.

Если ваша программу падает сразу после запуска, возможно вы запускаете ее из неправильной директории. Прочтите еще раз раздел про настройке Visual Studio из первого урока.

VAO


Не будем сейчас вдаваться в детали. Вам сейчас надо создать Vertex Array Object и установить его как активный:
GLuint VertexArrayID;
glGenVertexArrays(1, &VertexArrayID);
glBindVertexArray(VertexArrayID);

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

Если хотите узнать побольше об VAO — то добро пожаловать в английскую википедию под
спойлер
Vertex Array Object (VAO) — это объект OpenGL, который хранит в себе состояния, необходимые для поддержания информации о вершинах. Он хранит формат вершин также как и Buffer Objects, которые предоставляют массивы вершин.


Экранные коодинаты


Треугольник характеризуется тремя точками. Когда в трехмерной графике идет разговор о «точках» имеются ввиду вершины (вертексы, vertex).
Вершина имеет 3 координаты: X, Y и Z. Вы можете представлять эти 3 координаты так:
  • X — направо.
  • Y — вверх.
  • Z — на себя (да, имеено на себя, а не от себя).

Так же еще есть другой метод, называемый Правилом Правой Руки. Если вы сожмете руку в кулак, направите оттопыренный большой палец направо, а указательный наверх.
  • X — это большой палец.
  • Y — это указательный палец.
  • Z — это средний палец.

Представлять координату Z в таком ключе довольно странно. Почему же это так? Короткий ответ: потому что сотни лет существования Математики Правой Руки, дадут Вам кучу полезных инструментов. И единственным минусом будет неинтуитивный Z.

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

Так что нам понадобятся три 3D точки для того, что бы описать треугольник:
// Массив из 3 векторов, которые описывают 3 вершины
static const GLfloat g_vertex_buffer_data[] = {
   -1.0f, -1.0f, 0.0f,
   1.0f, -1.0f, 0.0f,
   0.0f,  1.0f, 0.0f,
};

Первая вершина (-1, -1, 0). Это означает, что до тех пор, пока мы ее не трансформировали, она будет отображаться в координатах (-1, -1) на экране. Что же это значит? Ось экрана находится по центру, X — направо, как всегда, Y — вверх. Пример:


Вы не можете изменить это правило. Оно записано в вашей видеокарте. Так что (-1, -1) это нижний левый угол экрана. (1, -1) это нижний правый и (0, 1) это центр сверху. Так что наш треугольник займет большую часть экрана.

Отрисовка треугольника
Следующим шагом будет передача треугольника в OpenGL. Мы сделаем это с помощью создания буффера:
// Эта переменная будет описывать наш вершинный буффер
GLuint vertexbuffer;
// Генерируем 1 буффер и размещаем его идентификатор в vertexBuffer
glGenBuffers(1, &vertexbuffer);
// Устанавливаем сгенерированный буффер, как активный
glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer);
// Передаем наши вершины в OpenGL
glBufferData(GL_ARRAY_BUFFER, sizeof(g_vertex_buffer_data), g_vertex_buffer_data, GL_STATIC_DRAW);

Это надо сделать только 1 раз. Теперь в нашем основном цикле, где раньше мы ничего не отрисовывали, мы можем отрисовать треугольник:
// Первый буффер-аттрибут : вершины
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer);
glVertexAttribPointer(
   0,                  // Аттрибут №0. Нет особой причины указывать именно 0, но этот номер должен совпадать с номером из шейдера.
   3,                  // Количество
   GL_FLOAT,           // Тип
   GL_FALSE,           // Нормализован?
   0,                  // Шаг
   (void*)0            // Смещение
);
// Отрисовываем треугольник!
glDrawArrays(GL_TRIANGLES, 0, 3); // Начинаем с 0 вершины; всего 3 вершины -> 1 треугольник
glDisableVertexAttribArray(0);

Если вам повезет — то при запуске вы получите белый треугольник.


Но если у вас все также черный экран — то значит у вас отрисовывается черный треугольник на черном фоне. Что бы это исправить можно вызвать glClearColor и glClear перед каждой отрисовкой. Что изменит цвет фона. Либо задать цвет треугольнику. Чем мы сейчас и займемся.

Шейдеры


Компиляция шейдеров


В самой простой конфигурации нам понадобится 2 шейдера: один называется «Вершинным шейдером», а другой «Фрагментным шейдером». Вершинный шейдер вызывается для каждой вершины, в то время, когда Фрагментный шейдер вызывается для каждого сэмпла. У нас используется 4х кратный antialising, а значит у нас по 4 сэмпла на пиксель.

Шейдеры программируются на языке GLSL: Graphics Library Shader Language, который является частью OpenGL. В отличии от C или Java, GLSL компилируется во время исполнения программы, что означает, что вы должны компилировать шейдеры при каждом запуске программы.

Обычно для каждого шейдера отводится отдельный файл. К примеру у нас есть SimpleFragmentShader.fragmentshader и SimpleVertexShader.vertexshader. Расширение может быть любым. Хоть .txt или .glsl.

Вот код. Не обязательно полностью понимать код, поскольку он вызывается лишь один раз в программе, так что комментариев должно быть достаточно. Так как эта функция будет использоваться во всем уроках, она будет помещена в отдельный файл common/loadShader.cpp. Заметьте, что также как и к буфферам, к шейдерам доступ осуществляется по их индексу. Реализация спрятана в драйвере.
GLuint LoadShaders(const char * vertex_file_path,const char * fragment_file_path){

	// Создаем шейдеры
	GLuint VertexShaderID = glCreateShader(GL_VERTEX_SHADER);
	GLuint FragmentShaderID = glCreateShader(GL_FRAGMENT_SHADER);

	// Считываем код вершинного шейдера из файла
	std::string VertexShaderCode;
	std::ifstream VertexShaderStream(vertex_file_path, std::ios::in);
	if(VertexShaderStream.is_open()){
		std::string Line = "";
		while(getline(VertexShaderStream, Line))
			VertexShaderCode += "\n" + Line;
		VertexShaderStream.close();
	}else{
		printf("Impossible to open %s. Are you in the right directory ? Don't forget to read the FAQ !\n", vertex_file_path);
		getchar();
		return 0;
	}

	// Считываем код фрагментного шейдера из файла
	std::string FragmentShaderCode;
	std::ifstream FragmentShaderStream(fragment_file_path, std::ios::in);
	if(FragmentShaderStream.is_open()){
		std::string Line = "";
		while(getline(FragmentShaderStream, Line))
			FragmentShaderCode += "\n" + Line;
		FragmentShaderStream.close();
	}

	GLint Result = GL_FALSE;
	int InfoLogLength;


	// Компилируем вершинный шейдер
	printf("Compiling shader : %s\n", vertex_file_path);
	char const * VertexSourcePointer = VertexShaderCode.c_str();
	glShaderSource(VertexShaderID, 1, &VertexSourcePointer , NULL);
	glCompileShader(VertexShaderID);

	// Проверяем вершинный шейдер
	glGetShaderiv(VertexShaderID, GL_COMPILE_STATUS, &Result);
	glGetShaderiv(VertexShaderID, GL_INFO_LOG_LENGTH, &InfoLogLength);
	if ( InfoLogLength > 0 ){
		std::vector<char> VertexShaderErrorMessage(InfoLogLength+1);
		glGetShaderInfoLog(VertexShaderID, InfoLogLength, NULL, &VertexShaderErrorMessage[0]);
		printf("%s\n", &VertexShaderErrorMessage[0]);
	}



	// Компилируем фрагментный шейдер
	printf("Compiling shader : %s\n", fragment_file_path);
	char const * FragmentSourcePointer = FragmentShaderCode.c_str();
	glShaderSource(FragmentShaderID, 1, &FragmentSourcePointer , NULL);
	glCompileShader(FragmentShaderID);

	// Проверяем фрагментный шейдер
	glGetShaderiv(FragmentShaderID, GL_COMPILE_STATUS, &Result);
	glGetShaderiv(FragmentShaderID, GL_INFO_LOG_LENGTH, &InfoLogLength);
	if ( InfoLogLength > 0 ){
		std::vector<char> FragmentShaderErrorMessage(InfoLogLength+1);
		glGetShaderInfoLog(FragmentShaderID, InfoLogLength, NULL, &FragmentShaderErrorMessage[0]);
		printf("%s\n", &FragmentShaderErrorMessage[0]);
	}



	// Соединяем шейдеры в программу
	printf("Linking program\n");
	GLuint ProgramID = glCreateProgram();
	glAttachShader(ProgramID, VertexShaderID);
	glAttachShader(ProgramID, FragmentShaderID);
	glLinkProgram(ProgramID);

	// Проверяем программу
	glGetProgramiv(ProgramID, GL_LINK_STATUS, &Result);
	glGetProgramiv(ProgramID, GL_INFO_LOG_LENGTH, &InfoLogLength);
	if ( InfoLogLength > 0 ){
		std::vector<char> ProgramErrorMessage(InfoLogLength+1);
		glGetProgramInfoLog(ProgramID, InfoLogLength, NULL, &ProgramErrorMessage[0]);
		printf("%s\n", &ProgramErrorMessage[0]);
	}

	
	glDetachShader(ProgramID, VertexShaderID);
	glDetachShader(ProgramID, FragmentShaderID);
	
	glDeleteShader(VertexShaderID);
	glDeleteShader(FragmentShaderID);

	return ProgramID;
}


Наш вершинный шейдер


Давайте начнем с вершинного шейдера. Первая строка скажет компилятору, что мы используем синтаксис OpenGL 3.
#version 330 core

Вторая строка описывает входные данные:
layout(location = 0) in vec3 vertexPosition_modelspace;

Давайте опишем эту строку по подробнее:
  • «vec3» это трехкомпонентный вектор в GLSL. Он похож (но отличен) на glm::vec3, который мы использовали для описания треугольника. Важно помнить, что если мы используем 3 компонентный вектор — то мы должны использовать 3 компонентный вектор в GLSL.
  • «layout(location = 0)» ссылается на буффер, который будет использоваться для заполнения vertexPosition_modelspace. Каждая вершина имеет множество аттрибутов: позиция, один или несколько цветов, один или несколько текстурных координат (UV) и т.д. OpenGL не знает, что определенный атрибут — это цвет. Он просто видит трехкомпонентный вектор. Поэтому мы должны сообщить какой буффер за что отвечает. Сообщаем мы при помощи ключевого слова layout и указании индекса, который должен быть таким же, как и первый аргумент glVertexAttribPointer. Значение 0 не важно, оно может быть любым. (Но не больше, чем glGetIntegerv(GL_MAX_VERTEX_ATTRIBX, &v) ).
  • «vertexPosition_modelSpace» — название аргумента.
  • «in» — означает, что это входной аргумент. Вскоре мы познакомимся в «out».

Функция, вызываемая для каждой вершины, называется main, прямо как в C:
void main(){

Наша main функция будет просто устанавливать позицию вершины на координаты, указанные в буфере. Так что если мы передает (1, 1) одна из вершин треугольника будет в верхнем правом углу экрана. В следующих уроках мы познакомимся с более интересными вычислениями, которые можно производить над входными данными.
  gl_Position.xyz = vertexPosition_modelspace;
  gl_Position.w = 1.0;
}

«gl_Position» одна из нескольких встроенных переменных. Вы должны передать ей какое-то значение. Все остальное — не обязательно. (Про «все остальное» мы поговорим в 4 уроке)

Наш фрагментный шейдер


Для нашего первого фрагментного шейдера мы реализуем нечто очень простое: раскраску каждого фрагмента в красный цвет. (Помните, что на каждый пиксель 4 фрагмента, поскольку мы используем 4х AA)
#version 330 core
out vec3 color;
void main(){
  color = vec3(1,0,0);
}

Да, vec3(1, 0, 0) означает красный. Дело в том, что компьютерный экраны представляют цвет, как комбинацию из Красного, Зеленого и Синего (RGB). Так что (1, 0, 0) означает самый яркий красный, нет зеленого и нет синего.

Соединение всего этого


Перед главным циклом вызываем функцию LoadShaders.
// Создаем и компилируем нашу GLSL программу из шейдеров
GLuint programID = LoadShaders( "SimpleVertexShader.vertexshader", "SimpleFragmentShader.fragmentshader" );

Теперь внутри главного цикла в начале очищаем экран. Функция glClearColor(0.0f, 0.0f, 0.4f, 0.0f) установит цвета фона на синий. Для очистки экрана вызывается:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

А затем говорим OpenGL, какой шейдер мы используем:
// Используем наш шейдер
glUseProgram(programID);
// Отрисовка треугольника...

Вот и все. Вот наш красный треугольник на синем фоне.

В следующем уроке мы поговорим о трансформациях: как настроить камеру, двигать объекты и т.д.
Поделиться с друзьями
-->

Комментарии (18)


  1. pfemidi
    29.07.2016 10:44
    +2

    Ось экрана находится по центру, X — направо, как всегда, Y — налево.

    Может быть всё-таки «Y — вверх»?


    1. Megaxela
      29.07.2016 10:49

      Конечно! Опечатался. Сейчас поправим.


  1. motpfofs
    29.07.2016 10:49

    Ссылка на первый урок «Урок 1. Создание окна» битая — на редактирование поста ссылается(


    1. Megaxela
      29.07.2016 10:49

      Да, и вправду. Исправил, спасибо.


      1. Darthman
        29.07.2016 16:19

        А еще в содержании дубриуется «всякое» и «Продвинутые уроки»


  1. gbg
    29.07.2016 10:57
    +1

    Для работы с файлами используются iostreams, для вывода на консоль — printf. Каша из головы автора плавно перетекает в головы учеников.


    1. Megaxela
      29.07.2016 11:06

      Да, соглашусь, что лучше было бы использовать постоянно что-то одно.
      Как думаете имеет смысл рефакторить вывод в консоль в iostream? Там дальше тоже будет работа с файлами, она вообще осуществляется через fopen. С другой стороны — это урок по OpenGL, а не по C++.


      1. gbg
        29.07.2016 11:13

        Чем больше материал похож на, как любит выражаться Кармак, «solid rock», тем больше к нему доверия.

        С другой стороны, тяжбы на тему «потоки в C++ медленные!11111» длятся достаточно давно с попеременным успехом. Для загрузки файлов я бы и вовсе рекомендовал использовать технологию отображения в память везде, где это только возможно.


        1. Megaxela
          29.07.2016 11:19

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


          1. gbg
            29.07.2016 11:24

            Разумеется. Отображение файлов прекрасно подходит для несжатых текстур, например.


          1. tbl
            29.07.2016 12:38
            +1

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


  1. Darthman
    29.07.2016 12:51
    +4

    Не хочу показаться занудой, но таких уроков в сети десятки и сотни. OpenGL3 появился, если мне не изменяет память в 2008 году.
    Может лучше цикл сменить сразу на Vulkan, по нему информации очень мало еще и это было бы действительно очень интересно.


    1. valera5505
      29.07.2016 14:28

      Мне кажется, на вулкане пелена исходников была бы ещё больше


      1. Darthman
        29.07.2016 16:18
        +3

        Я говорю не об обилии кода в статье, а об обилии подобного кода в интернете. Статью про то, как вывести треугольник, модельку, шейдер и прочее на АПИ восьмилетней давности завались. На любом языке, даже на Delphi их десяток можно найти циклов статей по OpenGL.
        Чего не скажешь про новое API — Vulkan.


  1. asd111
    29.07.2016 15:26
    +2

    Добрый день. Посмотрел содержание и думаю что было бы полезно добавить уроки по Physically based rendering PBR ( BRDF microfacets, Cook-torrance и т.п.), т.е. физически точное освещение, т.к. это очень важно уметь в современных реалиях.

    Ещё я думаю что было бы полезно добавить уроки по Nvidia gameworks или по аналогичной библиотеке от AMD Radeon GPUOpen, т.к. в данных библиотеках реализовано большое количество современных графических эффектов в частности у Nvidia:

    • Volumetric Lighting Implements a physical model of light scattering through DirectX shaders.
    • VXAO Voxel Accelerated Ambient Occlusion
    • HFTS Hybrid Frustum Traced Shadows
    • FleX Particle based simulation technique for real-time visual effects
    • VXGI Voxel Cone Tracing
    • HBAO+ Enhanced Horizon Based Ambient Occlusion
    • TXAA Temporal Anti-aliasing
    • Soft Shadows Improves on PCSS to reach new levels of quality and performance, with the ability to render cascaded shadow maps
    • Depth of Field Combination of diffusion based DOF and a fixed cost constant size bokeh effect
    • FaceWorks Library for implementing high-quality skin and eye shading
    • WaveWorks Cinematic-quality ocean simulation for interactive applications
    • HairWorks Enabling simulation and rendering of fur, hair and anything with fibers
    • GI Works Adding Global Illumination greatly improves the realism of the rendered image
    • Turbulence High definition smoke and fog with physical interaction as well as supernatural effects
    • FlameWorks A grid based system for generating fire, smoke and explosion effects for games


    1. Megaxela
      29.07.2016 16:43

      По окончанию перевода этих статей посмотрю в сторону Physically based rendering PBR, поскольку это и вправду может стать хорошим дополнением к статьям по OpenGL. А уроки по NVidia gameworks и GPUOpen как-нибудь в другой раз. Уж слишком это конкретизированные библиотеки (ну а еще потому что я по ним вообще ничего не знаю, надо будет пару уроков почитать и тогда уж возьмусь либо за перевод, либо за собственную статью).


  1. riv_shiell
    30.07.2016 01:21

    Очень надеюсь что цикл статей не закончится на базовых вещах, так как в большинстве курсов всё ограничивается в лучшем случае наложением текстурки :/


    1. Megaxela
      30.07.2016 01:23

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