Skip to content

Latest commit

 

History

History
478 lines (367 loc) · 32.2 KB

Lesson03.md

File metadata and controls

478 lines (367 loc) · 32.2 KB

Урок 3 - Вращающийся куб с текстурой

http://i.imgur.com/rGatTpUl.png

Введение

Этот урок будет немного сложнее предыдущего, но зато интересней, в нем будет рассмотрен следующий материал:

  1. Системы координат OpenGL
  2. Работа с матрицами, подготовка и передача их в шейдерную программу
  3. Создание простой текстуры и передача ее в шейдер
  4. Создание вершинного и индексного буфера для геометрии куба
  5. Вывод вращающегося куба на экран

Системы координат OpenGL

Координаты объекта, в режиме FFP (Fixed Function Pipeline) в предыдущих версиях OpenGL, проходили следующие преобразования системы координат:

  1. Локальные координаты преобразуются матрицей ModelView в видовые (eye space)
  2. Видовые преобразуются матрицей Projection в однородные (clip space)
  3. Однородные (X,Y,Z,W) преобразуются в нормализованные (X/W, Y/W, Z/W, 1)
  4. Нормализованные преобразуются параметрами glViewport и glDepthRange в экранные (screen space)

Разработчикам также была облегчена работа с матрицами преобразований. В частности разработчик мог использовать стек матриц в различных режимах работы с матрицами. Режим работы задавался с помощью функции glMatrixMode, например можно было задать режим GL_MODELVIEW или GL_PROJECTION, который давал доступ к модельно-видовой (ModelView) и проекционной (Projection) матрицам, соответственно. Для работы со стеком матриц предназначались функции glPushMatrix и glPopMatrix. Однако в OpenGL версии 3 и выше все эти функции были объявлены устаревшими и исключены из API.

В OpenGL 3.3 за первые два пункта преобразований координат теперь отвечает разработчик: используя шейдерную программу он должен перевести локальные координаты объекта в однородные. Каким образом он это сделает - неважно, он может эмулировать старую схему или придумать что-то свое, главное получить однородные координаты.

Однородные координаты называются так неспроста, они переводят все имеющиеся координаты в единое пространство, ограниченное по всем осям системы координат параметром W. В итоге, после перевода однородных координат в нормализованные, любые координаты вершин, которые необходимо отобразить, находятся в пределах [-1, 1] по всем трем осям нормализованной системы координат. Если координаты вершины не попадают в этот интервал - вершина отбрасывается.

Зачастую разработчики графических приложений разбивают первый этап преобразования из локальных координат в видовые на два:

  1. Трансформация координат матрицей объекта (ModelMatrix)
  2. Трансформация координат матрицей наблюдателя (ViewMatrix)

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

Терминология обозначения матриц и систем координат одна из многих причин путаниц при работе с разными графическими API, например в DirectX матрица перевода локальной системы координат объекта в мировые называется не ModelMatrix, а WorldMatrix.

Кстати, в OpenGL правая система координат, т.е. построенная по правилу правой руки. В начальном положении ось Z направлена на нас, ось X направлена вправо и ось Y направлена вверх, относительно экрана монитора.

Работа с матрицами

В этом и последующих уроках будет использоваться подход к матрицам описанный выше, для описания объекта на сцене необходимо будет задать три матрицы: ModelMatrix, ViewMatrix и ProjectionMatrix.

Матрицы ViewMatrix и ProjectionMatrix обычно привязаны к специальному объекту - камере. Матрица ViewMatrix меняется если наблюдатель изменил свое положение или направление взгляда. Матрица ProjectionMatrix меняется гораздо реже, например при переключении из меню приложения к сцене и т.п.

Матрица ModelMatrix закреплена за объектом, она меняется при движении объекта или его вращении.

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

  1. Указать в шейдере тип принимаемой матрицы - uniform matN matrixName
  2. После сборки шейдерной программ (link) получить индекс юниформа (location)
  3. Передать матрицу в шейдерную программу используя одну из функций glUniformMatrix

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

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

// построение матрицы вращения
void Matrix4Rotation(Matrix4 M, float x, float y, float z)
{
	const float A = cosf(x), B = sinf(x), C = cosf(y),
	            D = sinf(y), E = cosf(z), F = sinf(z);
	const float AD = A * D, BD = B * D;

	M[ 0] = C * E;           M[ 1] = -C * F;          M[ 2] = D;      M[ 3] = 0;
	M[ 4] = BD * E + A * F;  M[ 5] = -BD * F + A * E; M[ 6] = -B * C; M[ 7] = 0;
	M[ 8] = -AD * E + B * F; M[ 9] = AD * F + B * E;  M[10] = A * C;  M[11] = 0;
	M[12] = 0;               M[13] = 0;               M[14] = 0;      M[15] = 1;
}

Функция Matrix4Rotation строит матрицу вращения для углов поворота по трем осям координат (x, y, z). Если вас интересует подробное описание построение матрицы поворота, то рекомендую ознакомиться с детальным The Matrix and Quaternions FAQ. Отдельно на этой теме мы в уроке останавливаться не будем.

Помимо этого мы отодвинем точку наблюдения от куба, чтобы видеть его полностью, для этого нам понадобиться построить матрицу переноса ViewMatrix:

// построение матрицы переноса
void Matrix4Translation(Matrix4 M, float x, float y, float z)
{
	M[ 0] = 1; M[ 1] = 0; M[ 2] = 0; M[ 3] = x;
	M[ 4] = 0; M[ 5] = 1; M[ 6] = 0; M[ 7] = y;
	M[ 8] = 0; M[ 9] = 0; M[10] = 1; M[11] = z;
	M[12] = 0; M[13] = 0; M[14] = 0; M[15] = 1;
}

Функция Matrix4Translation строит матрицу переноса по трем координатным осям (x, y, z). Опять же детальную информацию вы можете получить в The Matrix and Quaternions FAQ.

Также нам понадобиться матрица проекции ProjectionMatrix, о которой было рассказано в предыдущем уроке. Также как и там мы будем использовать перспективную матрицу проекции, построенную при помощи функции Matrix4Perspective.

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

// перемножение двух матриц
void Matrix4Mul(Matrix4 M, Matrix4 A, Matrix4 B)
{
	M[ 0] = A[ 0] * B[ 0] + A[ 1] * B[ 4] + A[ 2] * B[ 8] + A[ 3] * B[12];
	M[ 1] = A[ 0] * B[ 1] + A[ 1] * B[ 5] + A[ 2] * B[ 9] + A[ 3] * B[13];
	M[ 2] = A[ 0] * B[ 2] + A[ 1] * B[ 6] + A[ 2] * B[10] + A[ 3] * B[14];
	M[ 3] = A[ 0] * B[ 3] + A[ 1] * B[ 7] + A[ 2] * B[11] + A[ 3] * B[15];
	M[ 4] = A[ 4] * B[ 0] + A[ 5] * B[ 4] + A[ 6] * B[ 8] + A[ 7] * B[12];
	M[ 5] = A[ 4] * B[ 1] + A[ 5] * B[ 5] + A[ 6] * B[ 9] + A[ 7] * B[13];
	M[ 6] = A[ 4] * B[ 2] + A[ 5] * B[ 6] + A[ 6] * B[10] + A[ 7] * B[14];
	M[ 7] = A[ 4] * B[ 3] + A[ 5] * B[ 7] + A[ 6] * B[11] + A[ 7] * B[15];
	M[ 8] = A[ 8] * B[ 0] + A[ 9] * B[ 4] + A[10] * B[ 8] + A[11] * B[12];
	M[ 9] = A[ 8] * B[ 1] + A[ 9] * B[ 5] + A[10] * B[ 9] + A[11] * B[13];
	M[10] = A[ 8] * B[ 2] + A[ 9] * B[ 6] + A[10] * B[10] + A[11] * B[14];
	M[11] = A[ 8] * B[ 3] + A[ 9] * B[ 7] + A[10] * B[11] + A[11] * B[15];
	M[12] = A[12] * B[ 0] + A[13] * B[ 4] + A[14] * B[ 8] + A[15] * B[12];
	M[13] = A[12] * B[ 1] + A[13] * B[ 5] + A[14] * B[ 9] + A[15] * B[13];
	M[14] = A[12] * B[ 2] + A[13] * B[ 6] + A[14] * B[10] + A[15] * B[14];
	M[15] = A[12] * B[ 3] + A[13] * B[ 7] + A[14] * B[11] + A[15] * B[15];
}

Функция Matrix4Mul перемножает матрицы A и B и помещает результат в матрицу M. Надеюсь как выполняется матричное умножение знает каждый из вас и подробно объяснить смысл производимых в функции действий вам не надо :)

Стоит также вспомнить, что умножение матриц операция ассоциативная, т.е. A*(B*C) = (A*B)*C, поэтому мы можем вычислить часть выражения заранее, а часть в случае необходимости.

В данном уроке мы не будем менять матрицу проекции и матрицу наблюдателя, поэтому их мы можем объединить заранее и не вычислять постоянно:

// переменные для хранения матриц
Matrix4 viewMatrix = {0.0f}, projectionMatrix = {0.0f}, viewProjectionMatrix = {0.0f};

// создадим перспективную матрицу
const float aspectRatio = (float)window->width / (float)window->height;
Matrix4Perspective(projectionMatrix, 45.0f, aspectRatio, 1.0f, 10.0f);

// с помощью матрицы наблюдателя отодвинем сцену назад
Matrix4Translation(viewMatrix, 0.0f, 0.0f, -4.0f);

// совместим матрицу проекции и матрицу наблюдателя
Matrix4Mul(viewProjectionMatrix, projectionMatrix, viewMatrix);

Для вращения куба нам необходимо построить матрицу вращения:

// матрица вращения куба и итоговая матрица трансформации
Matrix4 modelMatrix = {0.0f}, modelViewProjectionMatrix = {0.0f};

// зададим углы поворота куба с учетом времени
// просто произвольные значения
if ((cubeRotation[0] += 3.0f * (float)deltaTime) > 360.0f)
	cubeRotation[0] -= 360.0f;

if ((cubeRotation[1] += 15.0f * (float)deltaTime) > 360.0f)
	cubeRotation[1] -= 360.0f;

if ((cubeRotation[2] += 7.0f * (float)deltaTime) > 360.0f)
	cubeRotation[2] -= 360.0f;

// рассчитаем матрицу преобразования координат вершин куба
Matrix4Rotation(modelMatrix, cubeRotation[0], cubeRotation[1], cubeRotation[2]);
Matrix4Mul(modelViewProjectionMatrix, viewProjectionMatrix, modelMatrix);

Для передачи матрицы в шейдерную программу нам необходим узнать ее индекс:

// переменная для хранения индекса матрицы в шейдерной программе
GLint matrixLocation;

// получим индекс матрицы
matrixLocation = glGetUniformLocation(shaderProgram, "modelViewProjectionMatrix");

После получения итоговой матрицы и ее индекса мы можем передать ее в шейдерную программу:

// предаем матрицу трансформации в формате row-major
glUniformMatrix4fv(matrixLocation, 1, GL_TRUE, modelViewProjectionMatrix);

Очень частая проблема, которая приводит к путанице в среде разработчиков графических приложений - формат самих матриц и формат расположения их в памяти.

Формат матрицы можно определить по формату векторов, это либо row-vector матрица (используется вектор-строка), либо column-vector матрица (используется вектор-столбец). Соответственно при использовании row-vector необходимо вектор умножать на матрицу, а при использовании column-vector умножать матрицу на вектор.

По формату расположения в памяти матрицы также делятся на два типа: row-major матрица (матрица в памяти записана по строкам) и column-major матрица (матрица в памяти записана по столбцам). Стоит отметить, что переход между форматами осуществляется путем транспонирования матрицы.

По историческим причинам OpenGL использует column-major матрицы, соответственно ожидая на входе в свои функции именно этот формат, однако такой формат неудобен, поэтому в этих уроках используется row-major формат расположения в памяти, а при передачи в шейдерную программу функцией glUniformMatrix устанавливается флаг transpose, которые переводит матрицу в column-major формат.

Загрузка и создание текстуры

Тема текстур в OpenGL очень объемная, существуют различные типа текстур для разных целей. В этом уроке мы создадим самую простую и наиболее часто используемую текстуру - двумерную текстуру, в OpenGL такая текстура обозначается как GL_TEXTURE_2D.

Изображение для текстуры в этом уроке хранится в формате TGA. Этот очень простой формат, местами даже проще чем BMP. Для загрузки изображения из этого формата и создания текстуры используется функция TextureCreateFromTGA:

// формат заголовка TGA
struct TGAHeader
{
	uint8_t  idlength;
	uint8_t  colormap;
	uint8_t  datatype;
	uint8_t  colormapinfo[5];
	uint16_t xorigin;
	uint16_t yorigin;
	uint16_t width;
	uint16_t height;
	uint8_t  bitperpel;
	uint8_t  description;
};

// функция загрузки изображения из файла TGA и сздания текстуры
GLuint TextureCreateFromTGA(const char *fileName)
{
	ASSERT(fileName);

	TGAHeader *header;
	uint8_t   *buffer;
	uint32_t  size;
	GLint     format, internalFormat;
	GLuint    texture;

	// попытаемся загрузить изображение из файла
	if (!LoadFile(fileName, true, &buffer, &size))
		return 0;

	// если размер файла заведомо меньше заголовка TGA
	if (size <= sizeof(TGAHeader))
	{
		LOG_ERROR("Too small file '%s'\n", fileName);
		delete[] buffer;
		return 0;
	}

	header = (TGAHeader*)buffer;

	// проверим формат TGA-файла - несжатое RGB или RGBA изображение
	if (header->datatype != 2 || (header->bitperpel != 24 && header->bitperpel != 32))
	{
		LOG_ERROR("Wrong TGA format '%s'\n", fileName);
		delete[] buffer;
		return 0;
	}

	// получим формат текстуры
	format = (header->bitperpel == 24 ? GL_BGR : GL_BGRA);
	internalFormat = (format == GL_BGR ? GL_RGB8 : GL_RGBA8);

	// запросим у OpenGL свободный индекс текстуры
	glGenTextures(1, &texture);

	// сделаем текстуру активной
	glBindTexture(GL_TEXTURE_2D, texture);

	// установим параметры фильтрации текстуры - линейная фильтрация
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

	// установим параметры "оборачивания" текстуры - отсутствие оборачивания
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

	// загрузим данные о цвете в текущую автивную текстуру
	glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, header->width, header->height, 0, format,
		GL_UNSIGNED_BYTE, (const GLvoid*)(buffer + sizeof(TGAHeader) + header->idlength));

	// после загрузки в текстуру данные о цвете в памяти нам больше не нужны
	delete[] buffer;

	// проверим на наличие ошибок
	OPENGL_CHECK_FOR_ERRORS();

	return texture;
}

Добавить к кооментариям исходного кода приведенного выше практически нечего, используя функцию TextureCreateFromTGA полчаем готовую для исопльзования текстуру:

// переменная для хранения индекса текстуры
GLuint colorTexture = 0;

// создадим и загрузим текстуру
colorTexture = TextureCreateFromTGA("data/texture.tga");

// если не получилось загрузить текстуру
if (!colorTexture)
	return false;

// делаем активным текстурный юнит 0
glActiveTexture(GL_TEXTURE0);

// назначаем текстуру на активный текстурный юнит
glBindTexture(GL_TEXTURE_2D, colorTexture);

Функция glActiveTexture задает активный текстурный юнит видеокарты, который мы будем использовать. Привязка текстуры к активному текстурному юниту осуществляется функцией glBindTexture.

Теперь необходимо сообщить шейдерной программе в каком текстурном юните распологается наша текстура:

// переменная для хранения индекса текстуры
GLint textureLocation = -1;

// получим индекс текстуры из шейдерной программы
textureLocation = glGetUniformLocation(shaderProgram, "colorTexture");

// укажем, что текстура привязана к текстурному юниту 0
if (textureLocation != -1)
	glUniform1i(textureLocation , 0);

Теперь шейдерная программа знает какой текстурный юнит использовать. Максимальное количество доступных текстурных юнитов можно узнать используя функцию glGetIntegerv с параметром GL_MAX_TEXTURE_IMAGE_UNITS.

Геометрия куба и буферы для ее хранения

Также ккак и в прошлом уроке для тругольника мы вручную зададим геометрию куба, однако в этот раз не будем задавать цвет вершин, а назначим им текстурные координаты:

// количество вершин куба
static const uint32_t cubeVerticesCount = 24;

// описание геометрии куба для всех его сторон
// координаты вершин куба
const float s = 1.0f; // половина размера куба
const float cubePositions[cubeVerticesCount][3] = {
	{-s, s, s}, { s, s, s}, { s,-s, s}, {-s,-s, s}, // front
	{ s, s,-s}, {-s, s,-s}, {-s,-s,-s}, { s,-s,-s}, // back
	{-s, s,-s}, { s, s,-s}, { s, s, s}, {-s, s, s}, // top
	{ s,-s,-s}, {-s,-s,-s}, {-s,-s, s}, { s,-s, s}, // bottom
	{-s, s,-s}, {-s, s, s}, {-s,-s, s}, {-s,-s,-s}, // left
	{ s, s, s}, { s, s,-s}, { s,-s,-s}, { s,-s, s}  // right
};

// текстурные координаты куба
const float cubeTexcoords[cubeVerticesCount][2] = {
	{0.0f,1.0f}, {1.0f,1.0f}, {1.0f,0.0f}, {0.0f,0.0f}, // front
	{0.0f,1.0f}, {1.0f,1.0f}, {1.0f,0.0f}, {0.0f,0.0f}, // back
	{0.0f,1.0f}, {1.0f,1.0f}, {1.0f,0.0f}, {0.0f,0.0f}, // top
	{0.0f,1.0f}, {1.0f,1.0f}, {1.0f,0.0f}, {0.0f,0.0f}, // bottom
	{0.0f,1.0f}, {1.0f,1.0f}, {1.0f,0.0f}, {0.0f,0.0f}, // left
	{0.0f,1.0f}, {1.0f,1.0f}, {1.0f,0.0f}, {0.0f,0.0f}  // right
};

Конечно мы можем рисовать куб используя прямоугольники, однак обычно геометрия моделей представлена в виде граней (face), которые представляют собой треугольники. Для того чтобы нарисовать куб треугольниками не дублируя вершин на понадобиться индексный буфер, в котором последовательно идут по три индекса для каждого из треугольников, котоыре мы будем рисовать:

// количество индексов куба
const uint32_t cubeIndicesCount = 36;

// индексы вершин куба в порядке поротив часовой стрелки
const uint32_t cubeIndices[cubeIndicesCount] = {
	 0, 3, 1,  1, 3, 2, // front
	 4, 7, 5,  5, 7, 6, // back
	 8,11, 9,  9,11,10, // top
	12,15,13, 13,15,14, // bottom
	16,19,17, 17,19,18, // left
	20,23,21, 21,23,22  // right
};

Индексы определяют номера вершин в вершинном буфере, которые будет использоваться при выводе объекта на экран.

Теперь необходимо создать VBO для хранения вершин куба и индексного буфера, также не забываем про VAO, в котором будут хранится все созданные связи между VBO и вершинными атрибутами в шейдерной программе:

// переменные для хранения VAO и VBO связанных с кубом
GLuint cubeVBO[3], cubeVAO;

// переменные для хранения индексов вершинных атрибутов
GLint positionLocation = -1, texcoordLocation = -1;

// создаем VAO
glGenVertexArrays(1, &cubeVAO);

// сделаем VAO активным
glBindVertexArray(cubeVAO);

// создадим 3 VBO для данных куба - координаты вершин, текстурные координат и индексный буфер
glGenBuffers(3, cubeVBO);

// получим индекс вершинного атрибута 'position' из шейдерной программы
positionLocation = glGetAttribLocation(shaderProgram, "position");
if (positionLocation != -1)
{
	// начинаем работу с буфером для координат вершин куба
	glBindBuffer(GL_ARRAY_BUFFER, cubeVBO[0]);
	// поместим в буфер координаты вершин куба
	glBufferData(GL_ARRAY_BUFFER, cubeVerticesCount * (3 * sizeof(float)),
		cubePositions, GL_STATIC_DRAW);

	// укажем параметры вершинного атрибута для текущего активного VBO
	glVertexAttribPointer(positionLocation, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), 0);
	// разрешим использование вершинного атрибута
	glEnableVertexAttribArray(positionLocation);
}

// получим индекс вершинного атрибута 'texcoord' из шейдерной программы
texcoordLocation = glGetAttribLocation(shaderProgram, "texcoord");
if (texcoordLocation != -1)
{
	// начинаем работу с буфером для текстурных координат куба
	glBindBuffer(GL_ARRAY_BUFFER, cubeVBO[1]);
	// поместим в буфер текстурные координаты куба
	glBufferData(GL_ARRAY_BUFFER, cubeVerticesCount * (2 * sizeof(float)),
		cubeTexcoords, GL_STATIC_DRAW);

	// укажем параметры вершинного атрибута для текущего активного VBO
	glVertexAttribPointer(texcoordLocation, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), 0);
	// разрешим использование вершинного атрибута
	glEnableVertexAttribArray(texcoordLocation);
}

// начинаем работу с индексным буфером
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, cubeVBO[2]);
// поместим в буфер индексы вершин куба
glBufferData(GL_ELEMENT_ARRAY_BUFFER, cubeIndicesCount * sizeof(uint32_t),
	cubeIndices, GL_STATIC_DRAW);

Обратите внмание, что вершинный буфер задается параметром GL_ARRAY_BUFFER, а индексный буфер задается параметром GL_ELEMENT_ARRAY_BUFFER.

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

Вывод вращающегося куба на экран

Совместив все предыдущие действия мы можем вывести на экран куб:

// делаем шейдерную программу активной
glUseProgram(shaderProgram);

// передаем в шейдер матрицу преобразования координат вершин куба
glUniformMatrix4fv(matrixLocation, 1, GL_TRUE, modelViewProjectionMatrix);

// сделаем VAO куба активным
glBindVertexArray(cubeVAO);

// вывдоим куб на экран
glDrawElements(GL_TRIANGLES, cubeIndicesCount, GL_UNSIGNED_INT, NULL);

Шейдеры к уроку

Код вершинного шейдера:

#version 330 core

// матрица преобразования координат, получаемая из программы
uniform mat4 modelViewProjectionMatrix;

// входные вершинные атрибуты
in vec3 position;
in vec2 texcoord;

// исходящие параметры, которые будут переданы в фрагментный шейдер
out vec2 fragTexcoord;

void main(void)
{
	// перевод позиции вершины из локальных координат в однородные
	gl_Position = modelViewProjectionMatrix * vec4(position, 1.0);

	// передадим текстурные координаты в фрагментный шейдер
	fragTexcoord = texcoord;
}

Код фрагментного шедера:

#version 330 core

// текстура
uniform sampler2D colorTexture;

// параметры, полученные из вершинного шейдера
in vec2 fragTexcoord;

// результирующий цвет пикселя на экране
out vec4 color;

void main(void)
{
	// получим цвет пикселя из текстуры по текстурным координатам
	color = texture(colorTexture, fragTexcoord);
}

Полезные ссылки

  1. OpenGL Transformations
  2. The Matrix and Quaternions FAQ
  3. Описание функций glUniform

Исходный код

Доступ к исходному коду уроку с проектом для MSVC можно получить двумя способами: