Частина 18 - SDL і сучасний OpenGL 3.0+

У цьому розділі Ви дізнаєтесь про те, як поєднати бібліотеку SDL із відомою Відкритою Графічною Бібліотекою (OpenGL).

Що таке OpenGL?

OpenGL - це перший вибір, коли мова йде про програмування 2d та 3d графіки, незалежної від платформи. Наголос робиться лише на програмуванні графіки!

Чому і коли поєднувати SDL та OpenGL?

SDL - чудовий вибір, якщо вам потрібно незалежну від платформи 2d графіку. OpenGL також здатна використовувати 2d-графіку, але навіщо використовувати більш складну бібліотеку, якщо Ви можете використовувати просту у використанні бібліотеку SDL? - І до речі, під SDL насправді використовується OpenGL (або подібні бібліотеки залежно від системи) для досягнення своєї апаратно прискореної 2d-графіки.

Однак, якщо для Вашого проекту потрібна тривимірна графіка, яка не охоплюється SDL, Ви можете легко налаштувати свою систему на OpenGL. Налаштування середовища OpenGL за допомогою SDL дуже просте і не залежить від платформи. Без SDL Ви б писали інший код для налаштування OpenGL для кожної операційної системи. Насправді навіть професійні розробники використовують SDL для налаштування своїх програм OpenGL.

Крім того, оскільки OpenGL - це суто графічна бібліотека, будь-яке інше завдання надалі виконується SDL (наприклад, обробка клавіатури, звук…).

На цьому етапі я хотів би процитувати Клауса Фор дер Ландвера (професійного розробника) з Turtle-Games, який дуже чітко описав відношення SDL та OpenGL.

Незважаючи на те, що графіка часто виходить на перший план, для мене, як програміста ігор, це лише один із багатьох аспектів, з яким мені доводиться мати справу. А графіка навіть не вимагає найбільшої роботи. Наприклад, OpenAL коштує набагато більше часу та зусиль, якщо Ви хочете створити тривимірне управління звуковими каналами. І є багато інших інтерфейсів. Ось перелік категорій, у яких SDL був дуже корисним:

  • кілька дисплеїв
  • управління вікнами
  • Обробка подій
  • клавіатура
  • мишка
  • джойстик
  • ігровий контролер
  • силовий зворотний зв'язок
  • потоки
  • таймери

… для Windows, Mac і Linux.

Джерело: Pascal Game Development Community.

Що таке сучасний OpenGL?

З версії 2.0 OpenGL так званий фіксований конвеєр був замінений програмованим конвеєром (сучасний OpenGL). Як правило, конвеєр дозволяє вхідним даним відображатись на екрані апаратно прискореним способом за допомогою графічної карти. Для фіксованого трубопроводу було легко намалювати щось на екрані, але, як випливає з назви, він був досить фіксованим і негнучким. Програмований конвеєр, який контролюється нещодавно введеною мовою шейдерів (сценаріїв), є набагато гнучкішим, проте легкості уже немає: - D.

У будь-якому випадку, деякі люди називають OpenGL версії 3.0 і новіше сучасним OpenGL. Це пов’язано з тим, що станом на цю версію було вимкнено багато типових функціональних можливостей. Оберненої сумісності немає.

У цьому розділі я продемонструю, як використовувати SDL 2.0 та новіші для створення сучасного середовища OpenGL, використовуючи деякі основні шейдери. Я базувався на описі на чудовому підручнику з C ++ на opengl-tutorial.org та їх другому розділі. Ви можете шукати там інші матеріали OpenGL або ознайомитись із цим введенням у WikiBook OpenGL Introduction (C ++). Мені невідомі підручники OpenGL для Free Pascal чи Delphi щодо сучасних OpenGL (повідомте мене, якщо знаєте).

Модулі OpenGL для Pascal (заголовки)

Подібно до SDL 2.0, Вам потрібні певні модулі, які перекладають і підключають ваш код до бібліотеки OpenGL. Існує власний модуль OpenGL GL, який охоплює основні функції OpenGL. Крім того, для сучасного OpenGL вам потрібен пристрій GLext, який охоплює функціональні можливості до OpenGL версії 4.0. Ці модулі поставляються разом із компілятором Free Pascal.

Якщо ви зацікавлені в підтримці OpenGL версії 4.4, вам слід заглянути в dglOpenGL.pas. Цей модуль не постачається безпосередньо разом із компілятором Free Pascal.

Нехай почнуться веселощі

Давайте подивимось на код:


program chap10_SDL2;
uses Classes, SysUtils, SDL2, GL, GLext;

const
  vertexShaderFile = 'VertexShader.txt';
  fragmentShaderFile = 'FragmentShader.txt';
  triangleData: array[0..8] of GLfloat = ( -1.0, -1.0, 0.0,
                                            1.0, -1.0, 0.0,
                                            0.0,  1.0, 0.0  );

var
sdlWindow1: PSDL_Window;
sdlGLContext1: TSDL_GLContext;
i: Word;
VertexArrayID: GLuint;
triangleVBO: GLuint;

VertexShaderID: GLuint;
VertexShaderCode: PGLchar;
FragmentShaderID: GLuint;
FragmentShaderCode: PGLchar;
ShaderCode: TStringList;
ProgramID: GLuint;
compilationResult: GLint = GL_FALSE;
InfoLogLength: GLint;
ErrorMessageArray: array of GLChar;

begin
  if SDL_Init( SDL_INIT_VIDEO ) < 0 then HALT;

  //отримати вікно OpenGL і створити контекст OpenGL
  sdlWindow1 := SDL_CreateWindow( 'OpenGL window', 50, 50, 500, 500, SDL_WINDOW_OPENGL );
  if sdlWindow1 = nil then HALT;

  sdlGLContext1 := SDL_GL_CreateContext( sdlWindow1 );
  if @sdlGLContext1 = nil then HALT;

  //ініціалізувати OpenGL і завантажити розширення
  if Load_GL_VERSION_4_0 = false then
    if Load_GL_VERSION_3_3 = false then
      if Load_GL_VERSION_3_2 = false then
        if Load_GL_VERSION_3_0 = false then
        begin
          writeln(' ERROR: OpenGL 3.0 or higher needed. '); readln;
          HALT;
        end;

  //вивести постачальника, версію та версію шейдера OpenGL
  writeln( 'Vendor: ' + glGetString( GL_VENDOR ) );
  writeln( 'OpenGL Version: ' + glGetString( GL_VERSION ) );
  writeln( 'Shader Version: ' + glGetString( GL_SHADING_LANGUAGE_VERSION ) );

  //створити об'єкт масиву вершин (VAO)
  glGenVertexArrays( 1, @VertexArrayID );
  glBindVertexArray( VertexArrayID );

  //створити об'єкт буфера вершин (VBO)
  glGenBuffers( 1, @triangleVBO );
  glBindBuffer( GL_ARRAY_BUFFER, triangleVBO );
  glBufferData( GL_ARRAY_BUFFER, SizeOf( triangleData ), @triangleData, GL_STATIC_DRAW );

  //створити шейдери
  VertexShaderID := glCreateShader( GL_VERTEX_SHADER );
  FragmentShaderID := glCreateShader( GL_FRAGMENT_SHADER );

  //завантажити код шейдерів і отримати PChars
  ShaderCode := TStringList.Create;
  ShaderCode.LoadFromFile( VertexShaderFile );
  VertexShaderCode := ShaderCode.GetText;
  if VertexShaderCode = nil then HALT;
  ShaderCode.LoadFromFile( FragmentShaderFile );
  FragmentShaderCode := ShaderCode.GetText;
  if FragmentShaderCode = nil then HALT;
  ShaderCode.Free;

  //компіляція і перевірка помилок вершин шейдерів
  write('Compiling and error checking Vertex Shader... ' );
  glShaderSource( VertexShaderID, 1, @VertexShaderCode, nil );
  glCompileShader( VertexShaderID );

  glGetShaderiv( VertexShaderID, GL_COMPILE_STATUS, @compilationResult );
  glGetShaderiv( VertexShaderID, GL_INFO_LOG_LENGTH, @InfoLogLength );
  if compilationResult = GL_FALSE then
  begin
    writeln( 'failure' );
    SetLength( ErrorMessageArray, InfoLogLength+1 );
    glGetShaderInfoLog( VertexShaderID, InfoLogLength, nil, @ErrorMessageArray[0] );
    for i := 0 to InfoLogLength do write( String( ErrorMessageArray[i] ) );
    writeln;
  end else writeln( 'success' );

  //компіляція і перевірка помилок шейдерів фрагментів
  write('Compiling and error checking Fragment Shader... ' );
  glShaderSource( FragmentShaderID, 1, @FragmentShaderCode, nil );
  glCompileShader( FragmentShaderID );

  glGetShaderiv( FragmentShaderID, GL_COMPILE_STATUS, @compilationResult );
  glGetShaderiv( FragmentShaderID, GL_INFO_LOG_LENGTH, @InfoLogLength );
  if compilationResult = GL_FALSE then
  begin
    writeln( 'failure' );
    SetLength( ErrorMessageArray, InfoLogLength+1 );
    glGetShaderInfoLog( VertexShaderID, InfoLogLength, nil, @ErrorMessageArray[0] );
    for i := 0 to InfoLogLength do write( String( ErrorMessageArray[i] ) );
    writeln;
  end else writeln( 'success' );

  //створення і зв'язуваня програми
  write('Creating and linking program... ' );
  ProgramID := glCreateProgram();
  glAttachShader( ProgramID, VertexShaderID );
  glAttachShader( ProgramID, FragmentShaderID );
  glLinkProgram( ProgramID );

  glGetShaderiv( ProgramID, GL_LINK_STATUS, @compilationResult );
  glGetShaderiv( ProgramID, GL_INFO_LOG_LENGTH, @InfoLogLength );
  if compilationResult = GL_FALSE then
  begin
    writeln( 'failure' );
    SetLength( ErrorMessageArray, InfoLogLength+1 );
    glGetShaderInfoLog( VertexShaderID, InfoLogLength, nil, @ErrorMessageArray[0] );
    for i := 0 to InfoLogLength do write( String( ErrorMessageArray[i] ) );
    writeln;
  end else writeln( 'success' );

  for i := 0 to 400 do
  begin
    glClearColor( 0.0, 1.0-i/400, 0.0+i/400, 1.0 );
    glClear( GL_COLOR_BUFFER_BIT );
    glUseProgram( ProgramID );
    glEnableVertexAttribArray( 0 );
    glBindBuffer( GL_ARRAY_BUFFER, triangleVBO );
    glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 0, nil );
    glDrawArrays( GL_TRIANGLES, 0, 3 );
    glDisableVertexAttribArray( 0 );
    SDL_Delay( 20 );
    SDL_GL_SwapWindow( sdlWindow1 );
  end;

  //очистка
  glDetachShader( ProgramID, VertexShaderID );
  glDetachShader( ProgramID, FragmentShaderID );

  glDeleteShader( VertexShaderID );
  glDeleteShader( FragmentShaderID );
  glDeleteProgram( ProgramID );

  StrDispose( VertexShaderCode );
  StrDispose( FragmentShaderCode );

  glDeleteBuffers( 1, @triangleVBO );
  glDeleteVertexArrays( 1, @VertexArrayID );

  SDL_GL_DeleteContext( sdlGLContext1 );
  SDL_DestroyWindow( sdlWindow1 );

  SDL_Quit;
end.

Ого, це багато коду. Ви отримаєте ось що:

Result for chapter 10

Фон буде повільно змінюватися із зеленого на синій. І ви отримаєте це:

Command result for chapter 10

Інформація про постачальника, версію OpenGL та версія шейдера залежить від вашої системи. Крім того, якщо ваша система не підтримує потрібну версію OpenGL, ви не матимете “success”, а швидше “failure” після компіляції та зв'язування. Тоді може бути показана додаткова інформація.

program chap10_SDL2;
uses Classes, SysUtils, SDL2, GL, GLext;

const
  vertexShaderFile = 'VertexShader.txt';
  fragmentShaderFile = 'FragmentShader.txt';
  triangleData: array[0..8] of GLfloat = ( -1.0, -1.0, 0.0,
                                            1.0, -1.0, 0.0,
                                            0.0,  1.0, 0.0  );

var
sdlWindow1: PSDL_Window;
sdlGLContext1: TSDL_GLContext;
i: Word;
VertexArrayID: GLuint;
triangleVBO: GLuint;

VertexShaderID: GLuint;
VertexShaderCode: PGLchar;
FragmentShaderID: GLuint;
FragmentShaderCode: PGLchar;
ShaderCode: TStringList;
ProgramID: GLuint;
compilationResult: GLint = GL_FALSE;
InfoLogLength: GLint;
ErrorMessageArray: array of GLChar;

Програма називається “chap10_SDL2” зі зрозумілих причин.

Додатково до модуля SDL2 ми завантажуємо рідні модулі FPC Classes (для підтримки TStringList), SysUtils (для функцій PChar) і GL та GLext для підтримки OpenGL.

Оголошено три константи. Перші дві визначаються як імена файлів так званих вихідних файлів шейдерів. В основному це прості текстові файли, які містять сценарій. Детальніше про шейдери та сценарій пізніше. Третій - це масив із дев'яти значень GLfloat. GLfloat - тип змінної числа з плаваючою крапкою системи OpenGL, який насправді перекладається як тип Single Паскаля. Коротше, ці дев'ять значень описують три точки в тривимірному просторі, які, якщо вони з'єднані, утворюють трикутник. Детальніше про це пізніше.

Перша змінна “sdlWindow1” добре відома з попередніх частин. Будь-яка змінна, які слідують далі, є нові. Більшість з них пов'язані з OpenGL.

“sdlGLContext1” типу TSDL_GLContext потрібна для створення так званого контексту OpenGL. Насправді, цей тип змінної надається SDL і ключовий тип для встановлення контексту OpenGL простим і крос-платформенним способом.

Змінна “i” проста змінна типу Word для підрахунку.

Цілі числа та рядки OpenGL

Більшість наведених нижче змінних є або типом GLuint, або типом PGLchar. Остання змінна - це динамічний масив GLchars. Їх конкретне значення буде обговорено пізніше, але GLuint - це цілий тип без знаку OpenGL (без від’ємних значень), що перекладається на тип Cardinal/Longword Паскаля. Обробка тексту в OpenGL працює за допомогою рядків із нульовим закінченням типу PGLchar, які перекладаються на PChar Паскаля. Тоді очевидно GLchar перекладається на Char.

На цьому етапі Ви можете задатись питанням, чому для SDL замість простих рядків використовуються рядки з нульовим закінченням (див. Частина 9 для змінних типу PAnsiChar). Відповідь знову полягає в тому, що OpenGL базується на C, який обробляє рядки таким чином. PChar, до речі, дорівнює PAnsiChar.

Решта змінних “ShaderCode” типу TStringList будуть використовуватися для обробки текстових файлів шейдера. “CompilationResult” та “InfoLogLength” мають тип GLint. На відміну від GLuint вони допускають негативні значення.

begin
  if SDL_Init( SDL_INIT_VIDEO ) < 0 then HALT;

  //отримати вікно OpenGL і створити контекст OpenGL
  sdlWindow1 := SDL_CreateWindow( 'OpenGL window', 50, 50, 500, 500, SDL_WINDOW_OPENGL );
  if sdlWindow1 = nil then HALT;

  sdlGLContext1 := SDL_GL_CreateContext( sdlWindow1 );
  if @sdlGLContext1 = nil then HALT;

  //ініціалізувати OpenGL і завантажити розширення
  if Load_GL_VERSION_4_0 = false then
    if Load_GL_VERSION_3_3 = false then
      if Load_GL_VERSION_3_2 = false then
        if Load_GL_VERSION_3_0 = false then
        begin
          writeln(' ERROR: OpenGL 3.0 or higher needed. '); readln;
          HALT;
        end;

  //вивести постачальника, версію та версію шейдера OpenGL
  writeln( 'Vendor: ' + glGetString( GL_VENDOR ) );
  writeln( 'OpenGL Version: ' + glGetString( GL_VERSION ) );
  writeln( 'Shader Version: ' + glGetString( GL_SHADING_LANGUAGE_VERSION ) );

Спочатку, як вже відомо, ініціалізується SDL2. “sdlWindow1” створюється як відомо з SDL_CreateWindow. Однак будьте обережні, для роботи з OpenGL потрібно встановити прапорець SDL_WINDOW_OPENGL!

SDL 2.0 і контекст OpenGL

Контекст OpenGL - це свого роду абстрактна назва. Він не представляє просто вікно, хоча воно і створене з вікна SDL2, а навпаки, містить усе (включаючи інформацію про вікно), що пов’язано з цим контекстом OpenGL. Отже, контекст OpenGL є свого роду «ширшим», ніж просто вікно, тому його називають контекстом, а не просто вікном OpenGL.

Функцією створення контексту OpenGL із вікна SDL2 є:

function SDL_GL_CreateContext(window: PSDL_Window): TSDL_GLContext

Отже, це просто, просто використовуйте вікно SDL2 як аргумент, і вуаля - ви отримаєте контекст OpenGL, незалежний від платформи. Ось чому всі люблять SDL2 для роботи з OpenGL. Зверніть увагу, що повернутий контекст є не вказівником, а фактичним екземпляром. Тож для перевірки помилок на nil вам потрібно звернутися до адреси екземпляра оператором @.

Перевірка версії та ініціалізація OpenGL

Вкладені оператори if-then-check перевіряють, чи встановлено OpenGL принаймні версії 3.0 . Якщо так, завантажується найвища доступна версія. Якщо ні, програма зупиняється і повертає текстове повідомлення.

Якщо Ваше обладнання не підтримує OpenGL 3.0 або новішої версії, спробуйте оновити графічний драйвер. Існує велика ймовірність того, що Ви зможете використовувати OpenGL 3.0 або новішої версії. У будь-якому випадку, якщо оновлення не виходить або Ви не хочете оновлюватись, можливо, Ви заглянете в главу JEDI-SDL про OpenGL, там обробляється старий OpenGL (хоча ця глава стосується SDL 1.2, це не повинно не занадто важко змусити його працювати з SDL 2.0 з незначними змінами).

Далі виводяться три повідомлення. Вони представляють постачальника, версію OpenGL та версію мови шейдерів. Щоб отримати їх у читабельній формі, Вам потрібно перетворити константи у рядки за допомогою функції glGetString. Давайте ще раз подивимося у вікно команд:

Command result for chapter 10

Погляньте на перші три рядки, і ви побачите, як це може виглядати.

Об'єкт масиву вершин та об'єкт буфера вершин


  //створити об'єкт масиву вершин (VAO)
  glGenVertexArrays( 1, @VertexArrayID );
  glBindVertexArray( VertexArrayID );

  //створити об'єкт буфера вершин (VBO)
  glGenBuffers( 1, @triangleVBO );
  glBindBuffer( GL_ARRAY_BUFFER, triangleVBO );
  glBufferData( GL_ARRAY_BUFFER, SizeOf( triangleData ), @triangleData, GL_STATIC_DRAW );

Якщо ви новачок у OpenGL, OpenGL - це машина, що має безліч перемикачів. Кожен перемикач, коротко кажучи, об'єкт масиву вершин (Vertex Array Object - VAO) - це специфічний об'єкт OpenGL, який містить важливі параметри (наприклад, формат даних вершин) та посилання на інші об'єкти, включаючи об'єкти буфера вершин (Vertex Buffer Objects - VBO). Зверніть увагу, він сам не зберігає дані (вміст) об’єкта, він просто зберігає посилання на ці об’єкти.

Об'єкт буфера вершин (Vertex Buffer Object - VBO) містить фактичні дані (вміст). У прикладі це три вершини, кожна з яких описується трьома значеннями з плаваючою комою в декартовому просторі.

Ім'я або ідентифікатор об'єкта OpenGL

Оскільки важливо розуміти, на відміну від SDL, де об'єкти, як правило, безпосередньо подаються функції за допомогою посилання на вказівник, у OpenGL у Вас є так зване ім'я об'єкта OpenGL, яке насправді є цілим числом типу GLuint. Тому ідентифікатор - це також відповідна назва. Подивимось, як це працює:

VAO створюється функцією glGenVertexArrays(кількість імен VAO, вказівник на масив імен VAO). Перший параметр визначає, скільки імен VAO я хотів би створити. Нам просто потрібно 1. Другий параметр запитує вказівник на масив імен VAO. Оскільки імена VAO - це лише прості GLuints, це простий масив GLuints. У будь-якому випадку, оскільки нам нам просто потрібен один, підходить і вказівник на просту змінну GLuint. У нашому випадку це “VertexArrayID”. Для прив’язки («активації») відповідного VAO до контексту OpenGL використовується функція glBindVertexArray( назва VAO ). Аргументом є назва VAO, яку ми щойно створили у “VertexArrayID”.

Подібно до VAO, VBO створюється функцією glGenBuffers (кількість імен VBO, вказівник на масив імен VBO). Знову ж таки, нам потрібен лише 1 VBO, ім’я якого повинно бути повернуто до “triangleVBO”. Ця змінна просто зберігає ідентифікатор (ім’я об’єкта) типу GLuint.

З іменування “triangleVBO” нам стає зрозуміло, що ми маємо намір тут (представляючи трикутник трьома вершинами), як би OpenGL знав? - Ми пояснюємо значення цього буферного об'єкта OpenGL, використовуючи glBindBuffer (ціль, ім'я VBO). Існує безліч варіантів як цільовий, але тут правильний вибір - GL_VERTEX_BUFFER.

Фактичний VBO створюється glBufferData (ціль, розмір сховища даних об'єкта в байтах, вказівник на дані, які потрібно скопіювати у VBO, очікуване використання). Ця функція приймає чотири аргументи. Ціль знову GL_VERTEX_BUFFER. Розмір сховища даних VBO у байтах визначається функцією SizeOf Паскаля, застосованою до “triangleData”. Константа “triangleData” також містить дані для копіювання у VBO, тому її вказівник підходить як третій аргумент. Оскільки ми не збираємося багато змінювати дані, ми повинні використовувати GL_STATIC_DRAW як четвертий аргумент.

Якщо Ви новачок у OpenGL, не хвилюйтеся, якщо Ви вперше розгублені . Більшість людей теж. А тепер все може навіть погіршитися :-(. If you are a newcomer to OpenGL, don’t worry if you are confused the first time. Most people are. And now it may even get worse :-(.

Шейдери і мова шейдерів OpenGL

Починаючи з сучасного OpenGL, багато говорять про так звані шейдери. Шейдери - це сценарії, написані мовою сценарію, схожою на С, що називається мова шейдерів OpenGL (OpenGL Shading Language - GLSL). Ці сценарії компілюються під час виконання та впливають на спосіб обробки графічних даних на певних етапах у так званому конвеєрі рендеринга OpenGL. Насправді Ви можете створювати досить складні та спеціальні ефекти за допомогою шейдерів, навіть не змінюючи жодного рядка свого вихідного коду програми.

Вершинний шейдер і фрагментний шейдер

Є два шейдери, які мають вирішальне значення, і їх потрібно налаштувати для роботи з сучасними OpenGL. Вони називаються Vertex Shader і Fragment Shader. Хоча є ще шейдери, про які тут не йдеться. Кожен тип шейдерів впливає на різні аспекти візуалізації.

Vertex Shader - це перша програма шейдера, виконана в конвеєрі рендеринга. Кожна вершина "проходить через" програму Vertex Shader і відповідно обробляється, звідси і назва. Часто цей шейдер використовується для виконання операцій перетворення. Скрипт шейдера, використаний у цьому посібнику, показано далі:

#version 330 core

layout(location = 0) in vec3 vertexPosition_modelspace;

void main(void) {
	gl_Position.xyz = vertexPosition_modelspace;
        gl_Position.w = 1.0;
}

Цей вихідний код GLSL зберігається у файлі VertexShader.txt і знаходиться в тому ж каталозі, що і вихідний код прикладу для цієї частини. Я не збираюся тут детально пояснювати цей код GLSL, але детальне пояснення наведено у opengl-tutorial.org Chapter 2, звідки я, до речі, отримав цей код шейдера.

Frgament Shader - остання програма Shader, виконана в конвеєрі рендерингу. Так званий процес растеризації утворює фрагменти. Кожен фрагмент "пропускається" через програму Fragment Shader і обробляється відповідно. Сценарій шейдера, який використовується для шейдера фрагментів, наступний:

#version 330 core

out vec3 color;

void main(){
   color = vec3(1,0,0);
 }

Цей код знаходиться в файлі FragmentShader.txt і розміщується в тому ж каталозі, що й VertexShader.txt. Знову ж таки, детальне пояснення наведено у opengl-tutorial.org Chapter 2. У будь-якому випадку ви помітите, що існує змінна “color” (трикомпонентний вектор). Як бачите, він встановлює значення (червоний, зелений, синій) для фрагментів на (1,0,0), що означає, що результат повинен бути червоний, червоний = 100%, зелений і синій = 0%. Ви можете погратись з цими значеннями.

  //створити шейдери
  VertexShaderID := glCreateShader( GL_VERTEX_SHADER );
  FragmentShaderID := glCreateShader( GL_FRAGMENT_SHADER );

  //завантажити код шейдерів і отримати PChars
  ShaderCode := TStringList.Create;
  ShaderCode.LoadFromFile( VertexShaderFile );
  VertexShaderCode := ShaderCode.GetText;
  if VertexShaderCode = nil then HALT;
  ShaderCode.LoadFromFile( FragmentShaderFile );
  FragmentShaderCode := ShaderCode.GetText;
  if FragmentShaderCode = nil then HALT;
  ShaderCode.Free;

Обидва шейдери створюються функцією glCreateShader( Shader type ). Вона повертає посилання (або ім'я) як GLuint, як це було раніше для VAO та VBO. Ми зберігаємо їх у VertexShaderID та FragmenShaderID, відповідно.

Наступна частина стосується завантаження вихідного коду з двох файлів шейдерів (VertexShader.txt, FragmentShader.txt) та перетворення їх для використання з OpenGL. Спочатку створюється змінна “ShaderCode” типу TStringList. Його метод LoadFromFile дозволяє зручно завантажувати вміст файлу у змінну. Спочатку для Vertex Shader, ім’я файлу якого зберігається у константі “VertexShaderFile”. Змінна “VertexShaderCode” має тип PGLchar, тобто спосіб, яким OpenGL обробляє рядки. Оскільки PGLchar у будь-якому випадку має тип PChar, метод GetText тут цілком підходить для перетворення рядка вихідного коду в масив символів із нульовим закінченням. Нарешті, є проста перевірка, чи є PGLchars порожніми (нуль), чого не повинно бути, якщо на вихідний код вказують, як очікувалося.

Точно те ж саме виконується для FragmentShader і the вихідний код асоціюється з “FragmentShaderCode”.

Нарешті, фіктивна змінна “ShaderCode” звільняється.


  //компіляція і перевірка помилок вершин шейдерів
  write('Compiling and error checking Vertex Shader... ' );
  glShaderSource( VertexShaderID, 1, @VertexShaderCode, nil );
  glCompileShader( VertexShaderID );

  glGetShaderiv( VertexShaderID, GL_COMPILE_STATUS, @compilationResult );
  glGetShaderiv( VertexShaderID, GL_INFO_LOG_LENGTH, @InfoLogLength );
  if compilationResult = GL_FALSE then
  begin
    writeln( 'failure' );
    SetLength( ErrorMessageArray, InfoLogLength+1 );
    glGetShaderInfoLog( VertexShaderID, InfoLogLength, nil, @ErrorMessageArray[0] );
    for i := 0 to InfoLogLength do write( String( ErrorMessageArray[i] ) );
    writeln;
  end else writeln( 'success' );

  //компіляція і перевірка помилок шейдерів фрагментів
  write('Compiling and error checking Fragment Shader... ' );
  glShaderSource( FragmentShaderID, 1, @FragmentShaderCode, nil );
  glCompileShader( FragmentShaderID );

  glGetShaderiv( FragmentShaderID, GL_COMPILE_STATUS, @compilationResult );
  glGetShaderiv( FragmentShaderID, GL_INFO_LOG_LENGTH, @InfoLogLength );
  if compilationResult = GL_FALSE then
  begin
    writeln( 'failure' );
    SetLength( ErrorMessageArray, InfoLogLength+1 );
    glGetShaderInfoLog( VertexShaderID, InfoLogLength, nil, @ErrorMessageArray[0] );
    for i := 0 to InfoLogLength do write( String( ErrorMessageArray[i] ) );
    writeln;
  end else writeln( 'success' );
  

Для асоціювання вихідного коду, який ми зберігаємо у “VertexSourceCode”, до “VertexShaderID” типу GLuint, функція glShaderSource (посилання на шейдер, кількість елементів масиву, вказівник на масив вихідного коду PGLchars, вказівник на масив довжин). Посилання на шейдер вершин зберігається у “VertexShaderID”, що є першим аргументом. У нас є лише один вихідний код, тому другий аргумент - 1. Вихідний код зберігається VertexShaderCode, а його вказівник адресується @VertexShaderCode як третій аргумент. Як видно раніше, оскільки у нас тут лише один елемент, не обов’язково мати справді масив. Четвертий параметр дозволяє вказати певну довжину, але якщо встановити значення nil, він очікує нульові закінчення масивів символів.

Компіляція зроблена прямо за допомогою glCompileShader (посилання на шейдери). Насправді радимо тут перевірити помилки, тому показано, як це зробити. Функція glGetShaderiv (посилання на шейдер, параметр об'єкта, покажчик правильного типу для поверненого значення) використовується для запиту інформації про об'єкти. Спочатку ми хотіли б знати, чи була компіляція успішною. Посилання на шейдер зберігається у “VertexShaderID”, параметром об’єкта є GL_COMPILE_STATUS. Це поверне значення GLint, яке можна інтерпретувати як GL_FALSE або GL_TRUE. Результат зберігається в “compilationResult”, використовуючи його вказівник (@compilationResult) як аргумент.

Одразу після цього ми запитуємо довжину інформаційного журналу за допомогою GL_INFO_LOG_LENGTH. Це буде більше ніж 0, якщо якась інформація була зареєстрована (можливо, тоді під час компіляції сталася помилка). Результат повертається до “InfoLogLength” за допомогою його покажчика @InfoLogLength.

Якщо виникає помилка, “compilationResult” - GL_FALSE. У цьому випадку виводиться “failure” разом із більш конкретною інформацією. Я тут не буду вдаватися в подробиці, оскільки цього не повинно статися. В іншому випадку (і це має бути так) виводиться “success”.

Точно так само компілюється і перевіряється Fragment Shader.


  //створення і зв'язуваня програми
  write('Creating and linking program... ' );
  ProgramID := glCreateProgram();
  glAttachShader( ProgramID, VertexShaderID );
  glAttachShader( ProgramID, FragmentShaderID );
  glLinkProgram( ProgramID );

  glGetShaderiv( ProgramID, GL_LINK_STATUS, @compilationResult );
  glGetShaderiv( ProgramID, GL_INFO_LOG_LENGTH, @InfoLogLength );
  if compilationResult = GL_FALSE then
  begin
    writeln( 'failure' );
    SetLength( ErrorMessageArray, InfoLogLength+1 );
    glGetShaderInfoLog( VertexShaderID, InfoLogLength, nil, @ErrorMessageArray[0] );
    for i := 0 to InfoLogLength do write( String( ErrorMessageArray[i] ) );
    writeln;
  end else writeln( 'success' );

Шейдери повинні бути прикріплені та пов'язані за допомогою програми Shader. Програма Shader створюється за допомогою glCreateProgram(). Тут важливі дужки. Функція повертає посилання типу GLuint, яке зберігається в ProgramID.

Шейдери додаються до цієї програми шейдерів за допомогою glAttacheShader (посилання на програму, посилання на шейдери). Програма лінкується за допомогою glLinkProgram (посилання на програму). Посиланням на програму Shader є “ProgramID”. Посилання на шейдери - “VertexShaderID” та “FragmentShaderID”, відповідно.

За повною аналогією з перевіркою помилок для компіляції Shader перевіряється зв'язок програми Shader. У будь-якому разі замість GL_COMPILE_STATUS, використовується GL_LINK_STATUS.


  for i := 0 to 400 do
  begin
    glClearColor( 0.0, 1.0-i/400, 0.0+i/400, 1.0 );
    glClear( GL_COLOR_BUFFER_BIT );
    glUseProgram( ProgramID );
    glEnableVertexAttribArray( 0 );
    glBindBuffer( GL_ARRAY_BUFFER, triangleVBO );
    glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 0, nil );
    glDrawArrays( GL_TRIANGLES, 0, 3 );
    glDisableVertexAttribArray( 0 );
    SDL_Delay( 20 );
    SDL_GL_SwapWindow( sdlWindow1 );
  end;

Цикл for рахує від 0 до 400. Протягом кожного проходу він спочатку змінює колір фону за допомогою glClearColor (червоний, зелений, синій, альфа). Червоний та альфа постійні, зелений та синій змінюються кожен прохід в залежності від змінної i. Це призводить до того, що фон повільно змінюється із зеленого на синій, не соромтеся погратись зі значеннями rgba. Для фактичного очищення кольорового буфера використовується glClear (буфер) з GL_COLOR_BUFFER_BIT як аргумент.

glUseProgram (посилання на програму Shader) використовується для застосування програми Shader до стану візуалізації. “ProgramID” - посилання на програму Shader у прикладі коду.

glEnableVertexAttribArray (індекс масиву) використовується для того, щоб зробити масив атрибутів доступним для візуалізації за допомогою glDrawArrays. Тут індекс дорівнює 0. “triangleVBO” прив’язаний за допомогою glBindBuffer (ціль, буфер) до цілі GL_BUFFER_ARRAY для зміни даних атрибутів зазначеного VBO. Останні виконуються функцією glVertexAttribPointer (індекс, розмір, тип, нормалізований, крок, зміщення першого компонента) із заданими аргументами. Отже, індекс становить 0, 3 компонентів на загальний атрибут вершини, кожен із плаваючою комою (таким чином, GL_FLOAT), не нормалізований (таким чином, GL_FALSE), відсутність кроків між атрибутами вершин, відсутність зміщення для першого компонента.

Візуалізація здійснюється за допомогою glDrawArrays (тип примітиву, початковий індекс, кількість елементів). Тип примітиву - трикутник, отже GL_TRIANGLES є першим аргументом. Ми починаємо з самого початку, тому індекс дорівнює 0. У нас є 3 послідовні елементи (вершини).

glDisableVertexAttribArray (індекс масиву), очевидно, є функцією лічильника до glEnableVertexAttribArray (індекс масиву). Це вимикає масив атрибутів вершин.

SDL_Delay затримує цикл на 20 мілісекунд.

Процедура

procedure SDL_GL_SwapWindow(window: PSDL_Window)

використовується для фактичного відображення результату візуалізації у вікні “sdlWindow1”. Майте на увазі, що це вікно має бути ініціалізоване як вікно OpenGL. Цю процедуру можна порівняти з функцією SDL SDL_RenderPresent.

Після досягнення змінною i значення 400, цикл for завершується.


  //очистка
  glDetachShader( ProgramID, VertexShaderID );
  glDetachShader( ProgramID, FragmentShaderID );

  glDeleteShader( VertexShaderID );
  glDeleteShader( FragmentShaderID );
  glDeleteProgram( ProgramID );

  StrDispose( VertexShaderCode );
  StrDispose( FragmentShaderCode );

  glDeleteBuffers( 1, @triangleVBO );
  glDeleteVertexArrays( 1, @VertexArrayID );

  SDL_GL_DeleteContext( sdlGLContext1 );
  SDL_DestroyWindow( sdlWindow1 );

  SDL_Quit;
end.

Для очищення шейдери слід від'єднати від програми шейдерів за допомогою glDetachShader (програма, шейдер). Після цього їх можна видалити функцією glDeleteShader (шейдер), а програму - glDeleteProgram (програма).

Скрипт шейдера PChars звільняється за допомогою StrDispose( PChar ).

VBO і масив вершин мають бути звільнені за допомогою glDeleteBuffers (кількість об'єктів буфера, вказівник на масив буферів) і glDeleteVertexArrays (кількість VAO, вказівник на масив VAO) відповідно. Перший параметр - це кількість об’єктів, які потрібно видалити, що в обох випадках 1.

Щоб вирішити контекст OpenGL використано

procedure SDL_GL_DeleteContext(context: TSDL_GLContext)

Вікно SDL знищується як вже відомо. Нарешті, завершується SDL як вже відомо функцією SDL_Quit.

← попередня частина | наступна частина? →

Немає коментарів:

Дописати коментар