From 32eacaf015e95f06d279be5650b24fc426bbb7a9 Mon Sep 17 00:00:00 2001 From: Vadim Date: Sat, 17 Feb 2024 18:24:26 +0200 Subject: [PATCH] translated to Ukrainian --- _config.yml | 6 +- _includes/header_uk.html | 161 + _layouts/default.html | 2 + uk/assets/faq_quaternions/index.html | 2749 +++++++++++++++++ uk/beginners-tutorials/index.markdown | 25 + .../index.markdown | 242 ++ .../index.markdown | 309 ++ .../tutorial-3-matrices/index.markdown | 417 +++ .../tutorial-4-a-colored-cube/index.markdown | 262 ++ .../tutorial-5-a-textured-cube/index.markdown | 490 +++ .../index.markdown | 234 ++ .../tutorial-7-model-loading/index.markdown | 268 ++ .../tutorial-8-basic-shading/index.markdown | 307 ++ uk/download/index.markdown | 74 + uk/index.markdown | 44 + .../billboards/index.markdown | 123 + .../billboards-particles/index.markdown | 16 + .../particles-instancing/index.markdown | 412 +++ uk/intermediate-tutorials/index.markdown | 45 + .../tutorial-10-transparency/index.markdown | 123 + .../tutorial-11-2d-text/index.markdown | 141 + .../index.markdown | 131 + .../tutorial-13-normal-mapping/index.markdown | 544 ++++ .../index.markdown | 225 ++ .../tutorial-15-lightmaps/index.markdown | 60 + .../tutorial-16-shadow-mapping/index.markdown | 466 +++ .../tutorial-17-quaternions/index.markdown | 345 +++ .../tutorial-9-vbo-indexing/index.markdown | 96 + .../an-fps-counter/index.markdown | 46 + .../index.markdown | 598 ++++ .../clicking-on-objects/index.markdown | 18 + .../index.markdown | 201 ++ .../index.markdown | 210 ++ .../index.markdown | 170 + uk/miscellaneous/contribute/index.markdown | 22 + .../contribute/translation/index.markdown | 43 + uk/miscellaneous/faq/index.markdown | 146 + uk/miscellaneous/index.markdown | 26 + .../math-cheatsheet/index.markdown | 130 + .../useful-tools-links/index.markdown | 112 + 40 files changed, 10036 insertions(+), 3 deletions(-) create mode 100644 _includes/header_uk.html create mode 100644 uk/assets/faq_quaternions/index.html create mode 100644 uk/beginners-tutorials/index.markdown create mode 100644 uk/beginners-tutorials/tutorial-1-opening-a-window/index.markdown create mode 100644 uk/beginners-tutorials/tutorial-2-the-first-triangle/index.markdown create mode 100644 uk/beginners-tutorials/tutorial-3-matrices/index.markdown create mode 100644 uk/beginners-tutorials/tutorial-4-a-colored-cube/index.markdown create mode 100644 uk/beginners-tutorials/tutorial-5-a-textured-cube/index.markdown create mode 100644 uk/beginners-tutorials/tutorial-6-keyboard-and-mouse/index.markdown create mode 100644 uk/beginners-tutorials/tutorial-7-model-loading/index.markdown create mode 100644 uk/beginners-tutorials/tutorial-8-basic-shading/index.markdown create mode 100644 uk/download/index.markdown create mode 100644 uk/index.markdown create mode 100644 uk/intermediate-tutorials/billboards-particles/billboards/index.markdown create mode 100644 uk/intermediate-tutorials/billboards-particles/index.markdown create mode 100644 uk/intermediate-tutorials/billboards-particles/particles-instancing/index.markdown create mode 100644 uk/intermediate-tutorials/index.markdown create mode 100644 uk/intermediate-tutorials/tutorial-10-transparency/index.markdown create mode 100644 uk/intermediate-tutorials/tutorial-11-2d-text/index.markdown create mode 100644 uk/intermediate-tutorials/tutorial-12-opengl-extensions/index.markdown create mode 100644 uk/intermediate-tutorials/tutorial-13-normal-mapping/index.markdown create mode 100644 uk/intermediate-tutorials/tutorial-14-render-to-texture/index.markdown create mode 100644 uk/intermediate-tutorials/tutorial-15-lightmaps/index.markdown create mode 100644 uk/intermediate-tutorials/tutorial-16-shadow-mapping/index.markdown create mode 100644 uk/intermediate-tutorials/tutorial-17-quaternions/index.markdown create mode 100644 uk/intermediate-tutorials/tutorial-9-vbo-indexing/index.markdown create mode 100644 uk/miscellaneous/an-fps-counter/index.markdown create mode 100644 uk/miscellaneous/building-your-own-c-application/index.markdown create mode 100644 uk/miscellaneous/clicking-on-objects/index.markdown create mode 100644 uk/miscellaneous/clicking-on-objects/picking-with-a-physics-library/index.markdown create mode 100644 uk/miscellaneous/clicking-on-objects/picking-with-an-opengl-hack/index.markdown create mode 100644 uk/miscellaneous/clicking-on-objects/picking-with-custom-ray-obb-function/index.markdown create mode 100644 uk/miscellaneous/contribute/index.markdown create mode 100644 uk/miscellaneous/contribute/translation/index.markdown create mode 100644 uk/miscellaneous/faq/index.markdown create mode 100644 uk/miscellaneous/index.markdown create mode 100644 uk/miscellaneous/math-cheatsheet/index.markdown create mode 100644 uk/miscellaneous/useful-tools-links/index.markdown diff --git a/_config.yml b/_config.yml index 19e14e07b..39b265d9d 100644 --- a/_config.yml +++ b/_config.yml @@ -8,9 +8,9 @@ highlighter: rouge safe: true lsi: false source: . -languages_iso: [en, zh, ja, ru, fr, kr, hu] -languages: [en, cn, jp, ru, fr, kr, hu] -languages_names: [english, chinese, japanese, russian, français, korean, hungarian] +languages_iso: [en, zh, ja, ru, fr, kr, hu, uk] +languages: [en, cn, jp, ru, fr, kr, hu, uk] +languages_names: [english, chinese, japanese, russian, français, korean, hungarian, ukrainian] # Build settings markdown: kramdown kramdown: diff --git a/_includes/header_uk.html b/_includes/header_uk.html new file mode 100644 index 000000000..3f672cf35 --- /dev/null +++ b/_includes/header_uk.html @@ -0,0 +1,161 @@ + diff --git a/_layouts/default.html b/_layouts/default.html index 3457b4795..068771349 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -9,6 +9,8 @@ {% include header_fr.html %} {% elsif page.language == "hu"%} {% include header_hu.html %} + {% elsif page.language == "uk"%} + {% include header_uk.html %} {% else %} {% include header.html %} {% endif%} diff --git a/uk/assets/faq_quaternions/index.html b/uk/assets/faq_quaternions/index.html new file mode 100644 index 000000000..a0d8290e8 --- /dev/null +++ b/uk/assets/faq_quaternions/index.html @@ -0,0 +1,2749 @@ + + + + + + +
+ЧаПи по Матрицям та Кватерніонам
+==============================
+
+Версія 1.19 від 20 березня 2002
+-------------------------------
+
+Ці ЧаПи підтримуються "andreas@(no-spam)cs.ualberta.ca".
+Будь-які пропозиції та пов'язані питання вітаються.
+Просто надішліть електронного листа.
+
+Остання копія цієї ЧаПи (англійською) може бути знайдена за наступною адресою: 
+
+    
+http://www.cs.ualberta.ca/~andreas/math/matrfaq_latest.html
+
+Не соромтеся розповсюджувати або копіювати цей FAQ на свій розсуд.
+
+Внески
+-------------
+
+  Введення I1: Steve ?????
+
+  Поправки від Q55 до Q59: Andreas Junghanns
+
+  Поправки до Q50: Morten Ofstad
+
+  Примітки до Q39: Tom Nuydens
+
+  Поправки до Q29 і Q37: Eric Reiss
+
+  Уточнення до Q56: Duncan Murdoch
+
+  Уточнення до Q37: Ron Avitzur
+
+  Поправки до Q1: Mona Wong
+
+  Виправлення до Q36 і Q37: Eric Reiss
+
+  Покращення до Q34 і Q38: Jon Watte
+
+ Попередження та альтернатива до Q58 і Q59: Paul Pedriana
+
+  Уточнення (та оптимізація [Lee]) до Q53: Eleanor Groundwater і Lee Morgan
+
+  Покращення до Q39: jhunpingco
+
+  Виправлення до Q11 і optimization to Q12: Gordon
+
+  Виправлення до Q54 to Q60: Eleanor Groundwater
+
+  Виправлення та покращення до Q23 і Q24: Ben Houston
+
+  Доповнення до  Q39: Jon Watte
+
+  Поправки до Q61: Adam D. Moss
+
+  Доповнення до  Q63: Mike Cline
+
+
+Введення
+------------
+
+I1.  Важлива примітка щодо OpenGL і цього документа
+
+Історія
+-------
+
+Я (Andreas) намагався знайти "hexapod@(no-spam)netcom.com" хто намагався підтримувати це
+протягом якось часу, але сайт netcom.com більше не існує,
+електронна пошта відкидається. Так як я (і колеги) витратили певний час, намагаючись зрозуміти
+що не так з одним з алгоритмів, який був даний в попередній версії цього
+документу, я вирішив скорегувати його і викласти назад в інтернет.
+
+Попередні сайти які зберігали цей документ, більше не існують:
+
+  ftp://ftp.netcom.com/pub/he/hexapod/index.html
+  http://www.glue.umd.edu/~rsrodger
+
+
+Версії, дати і посилання на локальні копії (можете порівняти):
+matrfaq_1.02.html: Версія 1.2  2 жовтня 1997
+matrfaq_1.04.html: Версія 1.4  26 грудня 1998
+matrfaq_1.06.html: Версія 1.6  30 вересня 2000
+matrfaq_1.07.html: Версія 1.7  20 грудня 2000
+matrfaq_1.08.html: Версія 1.8  21 грудня 2000
+matrfaq_1.09.html: Версія 1.9  16 січня 2001
+matrfaq_1.10.html: Версія 1.10  30 січня 2001
+matrfaq_1.11.html: Версія 1.11  9 лютого 2001
+matrfaq_1.12.html: Версія 1.12  26 березня 2001
+matrfaq_1.13.html: Версія 1.13  20 липня 2001
+matrfaq_1.14.html: Версія 1.14  17 серпня 2001
+matrfaq_1.15.html: Версія 1.15  20 серпня 2001
+matrfaq_1.16.html: Версія 1.16  2 жовтня 2001
+matrfaq_1.17.html: Версія 1.17  30 листопада 2001
+matrfaq_1.18.html: Версія 1.18  27 січня 2002
+matrfaq_1.19.html: Версія 1.19  20 березня 2002
+ 
+Будь ласка, не ставте мені математичні запитання. Я лише підтримую ці ЧаПи
+і маю досить невеликі знання в цій темі. Але якщо Ви маєте
+питання, на яке тут немає відповіді і знаєте на нього відповідь, будь-ласка,
+надішліть мені всю відповідну інформацію, бажано в підготовленому форматі,
+що б я міг включити його. Дякую!
+Якщо Ви бажаєте залишитись анонімним, просто повідомне про це, 
+в іншому випадку, я включу Вас в список.
+
+Питання
+---------
+
+
+Основи
+======
+
+ Q1. Що таке матриця?
+ Q2. What is the order of a matrix?
+ Q3. How do I represent a matrix using the C/C++ programming languages?
+ Q4. What are the advantages of using matrices?
+ Q5. How do matrices relate to coordinate systems?
+
+
+Арифметика
+==========
+
+ Q6. What is the identity matrix?
+ Q7. What is the major diagonal matrix of a matrix?
+ Q8. What is the transpose of a matrix?
+ Q9. How do I add two matrices together?
+Q10. How do I subtract two matrices?
+Q11. How do I multiply two matrices together?
+Q12. How do I square or raise a matrix to a power?
+Q13. How do I multiply one or more vectors by a matrix?
+
+
+Детермінант та обернена матриця
+=========================
+
+Q14. What is the determinant of a matrix?
+Q15. How do I calculate the determinant of a matrix?
+Q16. What are Isotropic and Anisotropic matrices?
+Q17. What is the inverse of a matrix?
+Q18. How do I calculate the inverse of an arbitary matrix?
+Q19. How do I calculate the inverse of an identity matrix?
+Q20. How do I calculate the inverse of a rotation matrix?
+Q21. How do I calculate the inverse of a matrix using Kramer's rule?
+Q22. How do I calculate the inverse of a 2x2 matrix?
+Q23. How do I calculate the inverse of a 3x3 matrix?
+Q24. How do I calculate the inverse of a 4x4 matrix?
+Q25. How do I calculate the inverse of a matrix using linear equations?
+
+
+Трансформації
+==========
+
+Q26. What is a rotation matrix?
+Q27. How do rotation matrices relate to coordinate systems?
+Q28. How do I generate a rotation matrix in the X-axis?
+Q29. How do I generate a rotation matrix in the Y-axis?
+Q30. How do I generate a rotation matrix in the Z-axis?
+Q31. What are Euler angles?
+Q32. What are yaw, roll and pitch?
+Q33. How do I combine rotation matrices?
+Q34. What is Gimbal Lock?
+Q35. What is the correct way to combine rotation matrices?
+Q36. How do I generate a rotation matrix from Euler angles?
+Q37. How do I generate Euler angles from a rotation matrix?
+Q38. How do I generate a rotation matrix for a selected axis and angle?
+Q39. How do I generate a rotation matrix to map one vector onto another?
+Q40. How do I use matrices to convert between two coordinate systems?
+Q41. What is a translation matrix?
+Q42. What is a scaling matrix?
+Q43. What is a shearing matrix?
+Q44. How do I perform linear interpolation between two matrices?
+Q45. How do I perform cubic interpolation between four matrices?
+Q46. How can I render a matrix?
+
+
+Кватерніони
+===========
+
+Q47.  What are quaternions?
+Q48.  How do quaternions relate to 3D animation?
+Q49.  How do I calculate the conjugate of a quaternion?
+Q50.  How do I calculate the inverse of a quaternion?
+Q51.  How do I calculate the magnitude of a quaternion?
+Q52.  How do I normalise a quaternion?
+Q53.  How do I multiply two quaternions together?
+Q54.  How do I convert a quaternion to a rotation matrix?
+Q55.  How do I convert a rotation matrix to a quaternion?
+Q56.  How do I convert a rotation axis and angle to a quaternion?
+Q57.  How do I convert a quaternion to a rotation axis and angle?
+Q58.  How do I convert spherical rotation angles to a quaternion?
+Q59.  How do I convert a quaternion to spherical rotation angles?
+Q60.  How do I convert Euler rotation angles to a quaternion?
+Q61.  How do I use quaternions to perform linear interpolation between matrices?
+Q62.  How do I use quaternions to perform cubic interpolation between matrices?
+Q63.  How do I use quaternions to rotate a vector?
+
+
+Введення
+------------ 
+
+I1. Важливі замітки, що відносяться до OpenGL та цього документу
+-------------------------------------------------------
+
+  В цьому документу (як і в багатьох математичних книгах), всі матриці зображаються
+  в стандартний математичний спосіб. На жаль, всі графічні бібліотеки, такі як
+  IrisGL, OpenGL та SGI Performer представляють їх так, що стовпчики і рядки
+  поміняні місцями.
+
+  В цьому документі Ви будете бачити матрицю перенесення 4х4 в наступному вигляді:
+
+
+          | 1  0  0  X |
+          |            |
+          | 0  1  0  Y |
+      M = |            |
+          | 0  0  1  Z |
+          |            |
+          | 0  0  0  1 |
+
+  В Performer (до прикладу) воно буде заповнюватись наступним чином:
+
+    M[0][1] = M[0][2] = M[0][3] =
+    M[1][0] = M[1][2] = M[1][3] =
+    M[2][0] = M[2][1] = M[2][3] = 0 ;
+    M[0][0] = M[1][1] = M[2][2] = m[3][3] = 1 ;
+    M[3][0] = X ;
+    M[3][1] = Y ;
+    M[3][2] = Z ;
+
+  тобто, матриця зберігається так:
+
+          | M[0][0]  M[1][0]  M[2][0]  M[3][0] |
+          |                                    |
+          | M[0][1]  M[1][1]  M[2][1]  M[3][1] |
+      M = |                                    |
+          | M[0][2]  M[1][2]  M[2][2]  M[3][2] |
+          |                                    |
+          | M[0][3]  M[1][3]  M[2][3]  M[3][3] |
+
+  OpenGL використовує одновимірний масив для збереження матриць,
+  та, на щастя, в пам'яті рядки розташовані послідовно отже, якщо взяти 
+  адресу pfMatrix і привести її до float*, то можна буде передати її
+  безпосередньо в функцію, наприклад glLoadMatrixf.
+  
+  В прикладах коду, які розкидані по цьому документу, одновимірний масив
+  використовується для зберігання матриці. Порядок елементів в цій матриці
+  транспоновано відповідно до OpenGL.
+
+
+
+         цей документ                   OpenGL
+
+        | 0  1  2  3  |            | 0  4  8  12 |
+        |             |            |             |
+        | 4  5  6  7  |            | 1  5  9  13 |
+    M = |             |        M = |             |
+        | 8  9  10 11 |            | 2  6  10 14 |
+        |             |            |             |
+        | 12 13 14 15 |            | 3  7  11 15 |
+
+
+Відповіді
+-------
+
+Основи
+======
+
+Q1.  Що таке матриця?
+----------------------
+
+  Матриця це двовимірний масив чисел, де кожний рядок чи стовпчик
+  містить числові значення.
+
+  Арифметичні операції, які можна здійснити з матрицями, включають
+  в себе додавання, віднімання, множення і ділення.
+
+  Розмір матриці - це кількість рядків і стовпчиків.
+
+  Матриця з M рядків і N стовпчиків називається матриця MxN.
+
+  Посилатись на окремі елементи матриці можна за допомогою двох індексів. 
+  В математиці часто використовують для цього змінні i та j. Спочатку 
+  йдуть рядки, потім стовпчики.
+
+  До прикладу, якщо є матриця M з розміром 4x4, то елементи цієї матриці
+  мають наступні індекси в форматі рядок:стовпчик:
+
+        | 00 01 02 03 |
+    M = | 10 11 12 13 |
+        | 20 21 22 23 |
+        | 30 31 32 33 |
+
+  
+  Самий правий верхній елемент матриці має i=0 та j=3. Це позначається так:
+
+    M    = M
+     i,j    0,3
+
+  В комп'ютерній анімації найбільш популярні матриці мають розміри 2x2,
+  3x3 і 4x4.
+
+  Матриці 2x2 використовуються для здійснення поворотів, зсувів та інших
+  видів обробки зображень. Матриці розміру NxN можуть використовуватись
+  в обробці зображень та згортки.
+  
+  Матриці 3x3 використовуються для низькобюджетної 3D анімації. Такі 
+  операції, як повороти і множення можуть бути здійсненними за допомогою
+  матричних операцій, але перспективна проекція глибини виконується за допомогою
+  оптимізованих операцій ділення.
+
+  Матриці 4x4  використовуються для високоякісної 3D анімації. Множення та
+  перспективна проекція глибини може бути здійснено за допомогою матричної
+  математики.
+
+  
+Q2. Що таке "порядок" матриці?
+-------------------------------------
+
+  "Порядок" матриці це просто інше ім'я для розміру матриці.
+  Матриця, що має M рядків та N стовпчиків має порядок MxN.
+
+
+Q3.  Як представити матрицю, використовуючи мови C/C++?
+-----------------------------------------------------------------------
+
+  Найпростіший спосіб представити матрицю в C/C++ є використання "typedef".
+  Матриці 3x3 і 4x4 можуть бути об'явлені наступним чином:
+
+
+    typedef float MATRIX3[9];
+    typedef float MATRIX4[16];
+
+  Так як розмір матриць 3x3 і 4x4, це вимагає 9 і 16 елементів відповідно.
+
+  На перший погляд, використання одновимірного масиву може виглядати не інтуітивно
+  і використання двовимірного масиву здається більш природним:
+
+    typedef float MATRIX3[3][3];
+    typedef float MATRIX4[4][4];
+
+  Проте, використання двовимірних масивів дуже часто веде до непорозумінь. 
+  Для математиків, спочатку йдуть рядки (i), потім стовпчики (j).
+
+     Mij
+
+  Використовуючи C/C++, це перетворюється в
+
+     matrix[j][i]
+
+
+  Використання двовимірних масивів часто призводить до зниження продуктивності процесору,
+  тому що С компілятор може використовувати множення для знаходження
+  правильного індексу.
+
+  Отже, використання лінійних масивів більш ефективно. Проте ще залишається
+  одна проблема. Як співставити двовимірний масив та одновимірний? І тут є
+  два способи - спочатку рядки, потім стовпчики чи навпаки.
+  
+  Різниця в продуктивності між ними незначна. Є невелика різниця в 
+  продуктивності для таких операцій, як множення матриць.
+
+  Використовуючи C/C++, лінійний порядок для матриць наступний:
+
+
+  mat[0]  = M00        mat[3]  = M03
+
+  mat[12] = M30        mat[15] = M33
+
+
+        |  0  1  2  3 |
+        |             |              | 0 1 2 |
+        |  4  5  6  7 |              |       |
+    M = |             |          M = | 3 4 5 |
+        |  8  9 10 11 |              |       |
+        |             |              | 6 7 8 |
+        | 12 13 14 15 |
+
+
+Q4.  Які переваги в використанні матриць?
+-----------------------------------------------
+
+  One of the first questions asked about the use of matrices in computer
+  animation is why they should be used at all in the first place.
+  Intuitively, it would appear that the overhead of for-next loops and
+  matrix multiplication would slow down an application.
+
+  Arguments that resolve these objections can be pointed out. These include
+  the use of CPU registers to handle loop counters on-board data caches
+  to optimise memory accesses.
+
+  Advantages can also be pointed out. By following a mathematical approach
+  to defining 3D algorithms, it is possible to predict and plan the
+  design of a 3D animation system. Such mathematical approaches allow
+  for the implementation of character animation, spline curves and inverse
+  kinematics.
+
+  However, one objection that frequently comes up is that it would be
+  quicker to just multiply each pair of coordinates by the rotation
+  coefficients for that axis, rather than perform a full vector-matrix
+  multiplication.
+
+  ie. Rotation in X transforms Y and Z
+      Rotation in Y transforms X and Z
+      Rotation in Z transforms X and Y
+
+  The argument to this goes as follows:
+
+  Given a vertex V = (x,y,z), rotation angles (A,B and C) and translation
+  (D,E,F). A  the algorithm
+  is defined as follows:
+
+    ---------------------------
+
+    sx = sin(A)             // Setup - only done once
+    cx = cos(A)
+    sy = sin(B)
+    cy = cos(B)
+    sz = sin(C)
+    cz = cos(C)
+
+    x1 =  x * cz +  y * sz  // Rotation of each vertex
+    y1 =  y * cz -  x * sz
+    z1 =  z
+
+    x2 = x1 * cy + z1 * sy
+    y2 = z1
+    z2 = z1 * cy - x1 * sy
+
+    x3 = x2
+    y3 = y2 * cx + z1 * sx
+    z3 = z2 * cx - x1 * sx
+
+    xr = x3 + D             // Translation of each vertex
+    yr = y3 + E
+    zr = z3 + F
+
+    ---------------------------
+
+  Altogether, this algorithm will use the following amounts of processing
+  time:
+
+    Set-up                                 Per-vertex
+    -------------------------              ------------------------
+    6 trigonometric functions
+    6 assignment operations.               12 assignment
+                                           12 multiplication
+                                            9 addition
+    -------------------------              ------------------------
+
+  Assume that the same operations is being performed using matrix
+  multiplication.
+
+  With a 4x4 matrix, the procesing time is used as follows:
+
+    Set-up                       Change    Per-vertex               Change
+    --------------------------   ------    ------------------------ ------
+    6  trigonometric functions    0                                  0
+    18 assignment operation      -12        3  assignment           -9
+    12 multiplication            +12        9  multiplication       -3
+    6  subtraction               +6         6  addition             -3
+    --------------------------   ------    ------------------------ ------
+
+  Comparing the two tables, it can be seen that setting up a rotation
+  matrix costs at least 12 multiplication calculations and an extra
+  18 assignment calls.
+
+  However, while this may seem extravagant, the savings come from
+  processing each vertex. Using matrix multiplication, the savings made
+  from processing just 4 vertices, will outweigh the additional set-up
+  cost.
+
+
+Q5.  How do matrices relate to coordinate systems?
+--------------------------------------------------
+
+  With either 3x3 or 4x4 rotation, translation or shearing matrices, there
+  is a simple relationship between each matrix and the resulting coordinate
+  system.
+
+  The first three columns of the matrix define the direction vector of the
+  X, Y and Z axii respectively.
+
+  If a 4x4 matrix is defined as:
+
+        | A B C D |
+    M = | E F G H |
+        | I J K L |
+        | M N O P |
+
+  Then the direction vector for each axis is as follows:
+
+     X-axis = [ A E I ]
+
+     Y-axis = [ B F J ]
+
+     Z-axis = [ C G K ]
+
+
+Арифметика
+==========
+
+
+Q6.  Що таке матриця ідентичності?
+---------------------------------
+
+  The identity matrix is matrix in which has an identical number of rows
+  and columns. Also, all the elements in which i=j are set one. All others
+  are set to zero. For example a 4x4 identity matrix is as follows:
+
+        | 1 0 0 0 |
+    M = | 0 1 0 0 |
+        | 0 0 1 0 |
+        | 0 0 0 1 |
+
+
+Q7. Що таке головна діагональ матриці?
+--------------------------------------------
+
+  The major diagonal of a matrix is the set of elements where the
+  row number is equal to the column number ie.
+
+    M   where i=j
+     ij
+
+  In the case of the identity matrix, only the elements on the major
+  diagonal are set to 1, while all others are set to 0.
+
+
+Q8.  Що таке транспонування матриці?
+---------------------------------------
+
+  The transpose of matrix is the matrix generated when every element in
+  the matrix is swapped with the opposite relative to the major diagonal
+
+  This can be expressed as the mathematical operation:
+
+    M'   = M
+      ij    ji
+
+  However, this can only be performed if a matrix has an equal number
+  of rows and columns.
+
+  If the matrix M is defined as:
+
+        |  0.707 -0.866 |
+    M = |               |
+        |  0.866  0.707 |
+
+
+  Then the transpose is equal to:
+
+        |  0.707  0.866 |
+    T = |               |
+        | -0.866  0.707 |
+
+  If the matrix is a rotation matrix, then the transpose is guaranteed
+  to be the inverse of the matrix.
+
+
+Q9.  Як додати дві матриці?
+----------------------------------------
+
+  The rule of thumb with adding two matrices together is:
+
+    "add row and column to row and column"
+
+  This can be expressed mathematically as:
+
+    R   = M   + L
+     ij    ij    ij
+
+  However, both matrices must be identical in size.
+
+  For example, if the 2x2 matrix M is added with the 2x2 matrix L then
+  the result is as follow:
+
+    R = M + L
+
+        | A B C |   | J K L |
+        |       |   |       |
+      = | D E F | + | M N O |
+        |       |   |       |
+        | G H I |   | P Q R |
+
+
+        | A+J B+K C+L |
+        |             |
+      = | D+M E+N F+O |
+        |             |
+        | G+P H+Q I+R |
+
+
+Q10. Я відняти дві матриці?
+-------------------------------------
+
+  The rule of thumb with subtracting two matrices is:
+
+    "subtract row and column from row and column"
+
+  This can be expressed mathematically as:
+
+    R   = M   - L
+     ij    ij    ij
+
+  However, both matrices must be identical in size.
+
+  For example, if the 2x2 matrix L is subtracted from the 2x2 matrix M then
+  the result is as follows:
+
+    R = M - L
+
+        | A B C |   | J K L |
+        |       |   |       |
+      = | D E F | - | M N O |
+        |       |   |       |
+        | G H I |   | P Q R |
+
+
+        | A-J B-K C-L |
+        |             |
+      = | D-M E-N F-O |
+        |             |
+        | G-P H-Q I-R |
+
+
+Q11. Як помножити дві матриці?
+---------------------------------------------
+
+  The rule of thumb with multiplying two matrices together is:
+
+    "multiply row into column and sum the result".
+
+  This can be expressed mathematically as:
+
+          n
+          --
+    R   = \   M   x L
+     ij   /    ik    kj
+          --
+          k=1
+
+  If the two matrices to be multiplied together have orders:
+
+    M = AxB and L = CxD
+
+  then the two values B and C must be identical.
+
+  Also, the resulting matrix has an order of AxD
+
+  Thus, it is possible to multiply a Nx4 matrix with a 4x4 matrix
+  but not the other way around.
+
+  For example, if the 4x4 matrix M is defined as:
+
+        | A B C D |
+    M = | E F G H |
+        | I J K L |
+        | M N O P |
+
+  and a 4x2 matrix L is defined as:
+
+    L = | Q R |
+        | S T |
+        | U V |
+        | W X |
+
+  then the size of the resulting matrix is 4x2. The resulting matrix
+  is defined as:
+
+    R = M x L
+
+        | A B C D |   | Q R |
+      = | E F G H | x | S T |
+        | I J K L |   | U V |
+        | M N O P |   | W X |
+
+        | AQ+BS+CU+DW  AR+BT+CV+DX |
+      = | EQ+FS+GU+HW  ER+FT+GV+HX |
+        | IQ+JS+KU+LW  IR+JT+KV+LX |
+        | MQ+NS+OU+PW  MR+NT+OV+PX |
+
+
+Q12. Як піднести матрицю до степеня?
+--------------------------------------------------
+
+  A matrix may be squared or even raised to an integer power. However
+  there are several restrictions. For all powers, the matrix must be
+  orthogonal ie. have the same width and height
+
+  For example,
+
+     -1
+    M   is the inverse of the matrix
+
+     0
+    M   generates the identity matrix
+
+     1
+    M   leaves the matrix undamaged.
+
+     2
+    M   squares the matrix and
+
+     3
+    M   generates the cube of the matrix
+
+
+  Raising a matrix to a power greater than one involves multiplying a matrix
+  by itself a specific number of times.
+
+  For example,
+
+     2
+    M  = M . M
+
+     3
+    M  = M . M . M
+
+  and so on.
+
+
+  Raising the identity matrix to any power always generates the identity
+  matrix ie.
+
+     n
+    I  = I
+
+    -----------------------------------------
+
+  One can be bit faster using the following piece of code, note that m and i
+  are both getting changed:
+
+    m -> matrix to be raised to a power
+    i -> power to raise matrix to
+    a -> matrix that will contain the result
+    I -> identity matrix
+    / -> integer division (ie round down to nearest whole number)
+    % -> integer remainder operation
+    * -> matrix multiplication
+    
+    a=I
+    while(i>0){
+       if(i%2) a=a*m
+       i=i/2
+       m=m*m
+    }
+
+Q13. Як помножити один чи декілька векторів на матрицю?
+-------------------------------------------------------
+
+  The best way to perform this task is to treat the list of vectors as
+  a single matrix, with each vector represented as a column vector.
+
+  If N vectors are to be multiplied by a 4x4 matrix, then they can be
+  treated as a single 4xN matrix:
+
+  If the matrix is defined as:
+
+        | A B C D |
+    M = | E F G H |
+        | I J K L |
+        | M N O P |
+
+  and the list of vectors is defined as:
+
+        | x1 x2 x3 x4 x5|
+    V = | y1 y2 y3 y4 y5|
+        | z1 z2 z3 z4 z5|
+        | 1  1  1  1   1|
+
+  Note that an additional row of constant terms is added to the vector
+  list, all of which are set to 1.0. In real life, this row does not
+  exist. It is simply used to make the orders of the matrix M and the
+  vector list V match.
+
+  Then the multiplication is performed as follows:
+
+            M . V = V'
+
+  | A B C D |   | x1 x2 x3 x4 x5 |   | A.x1+B.y1+C.z1+D A.x2+B.y2+C.z2+D ... |
+  | E F G H | . | y1 y2 y3 y4 y5 | = | E.x1+F.y1+G.z1+H E.x2+F.y2+G.z2+H ... |
+  | I J K L |   | z1 z2 y3 y4 z5 |   | I.x1+J.y1+K.z1+L I.x2+J.y2+K.z2+L ... |
+  | M N O P |   | 1  1  1  1  1  |   | M.x1+N.y1+O.z1+P M.x2+N.y2+O.z2+P ... |
+
+  For each vector in the list there will be a total of 12 multiplication
+  16 addition and 1 division operation (for perspective).
+
+  If the matrix is known not to be a rotation or translation matrix then the
+  division operation can be skipped.
+
+
+Детермінант та обернення
+=========================
+
+
+Q14. What is the determinant of a matrix?
+-----------------------------------------
+
+  The determinant of a matrix is a floating point value which is used to
+  indicate whether the matrix has an inverse or not. If zero, then no
+  inverse exists. If non-zero, then a determinant exists.
+
+  As an example, consider a matrix consisting of a single element:
+
+    M = [ 1 ].
+
+  For a matrix of this size, the determinant is simply the value of the 
+  single element.
+
+  Also, the inverse is simply the reciprocal of this single element: 
+
+   -1
+  M   = [ 1 / M[0][0] ] 
+
+  If this single value is non-zero, then an inverse exists. In the case   
+  of the identity matrix, this happens to be 1 / 1 or 1.0 
+
+  However, if the value of this single element is zero, then the determinant
+  is also zero.
+  
+  Attempting to calculate the reciprocal of zero, generates a value of
+  infinity. This isn't permitted as far a matrices are concerned, so no
+  inverse of the matrix exists.
+
+  For an identity matrix, the determinant is always equal to one.
+  Any matrix with a determinant of 1.0 is said to be isotropic.
+
+  Thus all rotation matrices are said to be isotropic, since the
+  determinant is always equal to 1.0.
+
+  This can be proved as follows:
+
+        | A B |   | cos X  -sin X |
+    M = |     | = |               |
+        | C D |   | sin X   cos X |
+
+    D = AD - BC
+
+    D = (cos X . cos X) -  (-sin X . sin X)
+
+            2          2
+    D = (cos X ) + (sin X)
+
+            2       2
+    But, cos X + sin X = 1
+
+    Therefore,
+
+    D = 1
+
+
+Q15. Як розрахувати детермінант матриці?
+----------------------------------------------------
+
+  The determinant of a matrix is calculated using Kramer's rule, where
+  the value can be calculated by breaking the matrix into smaller
+  matrices.
+
+  For a 2x2 matrix M, the determinant D is calculated as follows:
+
+        | A B |
+    M = |     |
+        | C D |
+
+    D = AD - BC
+
+  For 3x3 and 4x4 matrices, this is more complicated, but can be solved
+  by methods such as Kramer's Rule.
+
+
+Q16. What are Isotropic and Anisotropic matrices?
+-------------------------------------------------
+
+  An Isotropic matrix is one in which the sum of the squares of all
+  three rows or columns add up to one.
+
+  A matrix in which this is not the case, is said to be Anisotropic.
+
+  When 3x3 or 4x4 matrices are used to rotate and scale an object, it
+  is sometimes necessary to enlarge or shrink one axis more than the
+  others.
+
+  For example, with seismic surveys, it is convenient to enlarge the
+  Z-axis by a factor or 50 or more, while letting the X and Y axii
+  remain the same.
+
+  Another example is the implementation of "squash" and "stretch"
+  with character animation. When a character is hit by a heavy object
+  eg. an anvil, the desired effect is to character stretched out
+  sideways and squashed vertically:
+
+  A suitable matrix would be as follows:
+
+        |  2   0   0    0  |
+    M = |  0   2   0    0  |
+        |  0   0   0.5  0  |
+        |  0   0   0    1  |
+
+  However, there is problem looming ahead. While this matrix will cause
+  no problems with the transformation of vertex data, it will cause
+  problems with gouraud shading using outward normals.
+
+  Because the transformation stage is implemented using matrix
+  multiplication, both vertex data and outward normal data will be
+  multiplied with this matrix.
+
+  While this is not a problem with vertex data (it is the desired effect)
+  it causes a major headache with the outward normal data.
+
+  After raw multiplication, each outward normal will no longer be
+  normalised and consequently will affect other calculations such as
+  shading and back-face culling.
+
+
+Q17. Що таке інверсія матриці (обернена матриця)?
+-------------------------------------
+                                                                 -1
+  Given a matrix M, then the inverse of that matrix, denoted as M  , is
+  the matrix which satisfies the following expression:
+
+         -1
+    M . M   = I
+
+  where I is the identity matrix.
+
+  Thus, multiplying a matrix with its inverse will generate the identity
+  matrix. However, several requirements must be satisfied before the
+  inverse of a matrix can be calculated.
+
+  These include that the width and height of the matrix are identical and
+  that the determinant of the matrix is non-zero.
+
+  Calculating the inverse of a matrix is a task often performed in order
+  to implement inverse kinematics using spline curves.
+
+
+Q18. Як розрахувати обернену матрицю для довільної матриці?
+----------------------------------------------------------
+
+  Depending upon the size of the matrix, the calculation of the inverse
+  can be trivial or extremely complicated.
+
+  For example, the inverse of a 1x1 matrix is simply the reciprocal of
+  the single element:
+
+  ie. M = | x |
+
+  Then the inverse is defined as:
+
+     -1   | 1 |
+    M   = | - |
+          | x |
+
+  Solving 2x2 matrices and larger can be achieved by using Kramer's Rule
+  or by solving as a set of simultaneous equations.
+
+  However, in certain cases, such as identity or rotation matrices, the
+  inverse is already known or can be determined from taking the transpose
+  of the matrix.
+
+
+Q19. Як розрахувати обернену матрицю для одиничної матриці?
+----------------------------------------------------------
+
+  Don't even bother. The inverse of an identity matrix is the identity
+  matrix. ie.
+
+         -1
+    I . I   = I
+
+  Any identity matrix will always have a determinant of +1.
+
+
+Q20. Як розрахувати обернену матрицю для матриці повороту?
+---------------------------------------------------------
+
+  Since a rotation matrix always generates a determinant of +1,
+  calculating the inverse is equivalent of calculating the transpose.
+
+  Alternatively, if the rotation angle is known, then the rotation
+  angle can be negated and used to calculate a new rotation matrix.
+
+
+Q21. Як розрахувати обернену матрицю, використовуючи правило Крамера?
+--------------------------------------------------------------------
+
+  Given a 3x3 matrix M:
+
+        | A B C |
+        |       |
+    M = | D E F |
+        |       |
+        | G H I |
+
+  Then the determinant is calculated as follows:
+
+             n
+            ---
+            \                           i
+    det M = /   M    * submat    M  * -1
+            ---  0,i         0,i
+            i=1
+
+  where
+
+    submat   M defines the matrix composed of all rows and columns of M
+          ij
+
+  excluding row i and column j. submat   may be called recursively.
+                                      ij
+
+  If the determinant is non-zero then the inverse of the matrix exists.
+  In this case, the value of each matrix element is defined by:
+
+     -1      1                            i+j
+    M    = -----  *  det submat     M * -1
+     j,i   det M               i,j
+
+
+Q22. How do I calculate the inverse of a 2x2 matrix?
+----------------------------------------------------
+
+  For a 2x2 matrix, the calculation is slightly harder. If the matrix is
+  defined as follows:
+
+        | A B |
+    M = |     |
+        | C D |
+
+  Then the determinant is defined as:
+
+    det = AD - BC
+
+  And the inverse is defined as:
+
+     -1     1   |  D  -B  |
+    M   =  ---  |         |
+           det  | -C   A  |
+
+  This can be proved using Kramer's rule. Given the matrix M:
+
+        | A B |
+    M = |     |
+        | C D |
+
+  Then the determinant is:
+
+                           0                            1
+    det =  M    * submat M    * -1 +  M    * submat M    * -1
+            0,0           0,0          0,1           0,1
+
+    <=>    M    * M    * 1         +  M    * M    * -1
+            0,0    1,1                 0,1    1,0
+
+    <=>    A  * D                  +  B    * C    * -1
+
+    <=>    AD                      +  BC . -1
+
+    <=>    AD - BC
+
+    ==============
+
+
+  And the inverse is derived from:
+
+     -1                      0+0      -1
+    M    = det submat    * -1    <=> M    = M    *  1 <=> D
+     0,0             0,0              0,0    1,1
+
+     -1                      1+0      -1
+    M    = det submat    * -1    <=> M    = M    * -1 <=> C * -1
+     0,1             1,0              0,1    1,0
+
+     -1                      0+1      -1
+    M    = det submat    * -1    <=> M    = M    * -1 <=> B * -1
+     1,0             0,1              1,0    0,1
+
+     -1                      1+1      -1
+    M    = det submat    * -1    <=> M    = M    *  1 <=> A
+     1,1             1,1              1,1    0,0
+
+
+  Then the inverse matrix is equal to:
+
+     -1    1  |  D  -C |
+    M   = --- |        |
+          det | -B   A |
+
+  Providing that the determinant is not zero.
+
+
+Q23. How do I calculate the inverse of a 3x3 matrix?
+----------------------------------------------------
+
+  For 3x3 matrices and larger, the inverse can be calculated by
+  either applying Kramer's rule or by solving as a set of linear
+  equations.
+
+  If Kramer's rule is applied to a matrix M:
+
+        | A B C |
+    M = | D E F |
+        | G H I |
+
+  then the determinant is calculated as follows:
+
+    det M = A * (EI - HF) - B * (DI - GF) + C * (DH - GE)
+
+
+  Providing that the determinant is non-zero, then the inverse is
+  calculated as:
+
+     -1     1     |   EI-FH  -(BI-HC)   BF-EC  |
+    M   = ----- . | -(DI-FG)   AI-GC  -(AF-DC) |
+          det M   |   DH-GE  -(AH-GB)   AE-BD  |
+
+
+  This can be implemented using a pair of 'C' functions:
+
+    ---------------------------------
+
+    VFLOAT m3_det( MATRIX3 mat )
+      {
+      VFLOAT det;
+
+      det = mat[0] * ( mat[4]*mat[8] - mat[7]*mat[5] )
+          - mat[1] * ( mat[3]*mat[8] - mat[6]*mat[5] )
+          + mat[2] * ( mat[3]*mat[7] - mat[6]*mat[4] );
+
+      return( det );
+      }
+
+   ----------------------------------
+
+   int m3_inverse( MATRIX3 mr, MATRIX3 ma )
+     {
+     VFLOAT det = m3_det( ma );
+
+     if ( fabs( det ) < 0.0005 )
+       {
+       m3_identity( mr );
+       return(0);
+       }
+
+     mr[0] =    ma[4]*ma[8] - ma[5]*ma[7]   / det;
+     mr[1] = -( ma[1]*ma[8] - ma[7]*ma[2] ) / det;
+     mr[2] =    ma[1]*ma[5] - ma[4]*ma[2]   / det;
+
+     mr[3] = -( ma[3]*ma[8] - ma[5]*ma[6] ) / det;
+     mr[4] =    ma[0]*ma[8] - ma[6]*ma[2]   / det;
+     mr[5] = -( ma[0]*ma[5] - ma[3]*ma[2] ) / det;
+
+     mr[6] =    ma[3]*ma[7] - ma[6]*ma[4]   / det;
+     mr[7] = -( ma[0]*ma[7] - ma[6]*ma[1] ) / det;
+     mr[8] =    ma[0]*ma[4] - ma[1]*ma[3]   / det;
+     return(1);
+     }
+
+    ---------------------------------
+
+
+Q24. How do I calculate the inverse of a 4x4 matrix?
+----------------------------------------------------
+
+  As with 3x3 matrices, either Kramer's rule can be applied or the
+  matrix can be solved as a set of linear equations.
+
+  An efficient way is to make use of the existing 'C' functions defined
+  to calculate the determinant and inverse of a 3x3 matrix.
+
+  In order to implement Kramer's rule with 4x4 matrices, it is necessary
+  to determine individual sub-matrices. This is achieved by the following
+  routine:
+
+    --------------------------
+	void	m4_submat( MATRIX4 mr, MATRIX3 mb, int i, int j ) {
+	  int di, dj, si, sj;
+
+	  // loop through 3x3 submatrix
+	  for( di = 0; di < 3; di ++ ) {
+	    for( dj = 0; dj < 3; dj ++ ) {
+
+	      // map 3x3 element (destination) to 4x4 element (source)
+	      si = di + ( ( di >= i ) ? 1 : 0 );
+	      sj = dj + ( ( dj >= j ) ? 1 : 0 );
+
+	      // copy element
+	      mb[di * 3 + dj] = mr[si * 4 + sj];
+	    }
+	  }
+	}
+    --------------------------
+
+  The determinant of a 4x4 matrix can be calculated as follows:
+
+    --------------------------
+
+    VFLOAT m4_det( MATRIX4 mr )
+      {
+      VFLOAT  det, result = 0, i = 1;
+      MATRIX3 msub3;
+      int     n;
+
+      for ( n = 0; n < 4; n++, i *= -1 )
+        {
+        m4_submat( mr, msub3, 0, n );
+
+        det     = m3_det( msub3 );
+        result += mr[n] * det * i;
+        }
+
+      return( result );
+      }
+
+    --------------------------
+
+  And the inverse can be calculated as follows:
+
+    --------------------------
+
+    int m4_inverse( MATRIX4 mr, MATRIX4 ma )
+      {
+      VFLOAT  mdet = m4_det( ma );
+      MATRIX3 mtemp;
+      int     i, j, sign;
+
+      if ( fabs( mdet ) < 0.0005 )
+	m4_identity( mr );
+        return( 0 );
+
+      for ( i = 0; i < 4; i++ )
+        for ( j = 0; j < 4; j++ )
+          {
+          sign = 1 - ( (i +j) % 2 ) * 2;
+
+          m4_submat( ma, mtemp, i, j );
+
+          mr[i+j*4] = ( m3_det( mtemp ) * sign ) / mdet;
+          }
+
+      return( 1 );
+      }
+
+    --------------------------
+
+  Having a function that can calculate the inverse of any 4x4 matrix is
+  an incredibly useful tool. Application include being able to calculate
+  the base matrix for splines, inverse rotations and rearranging matrix
+  equations.
+
+
+Q25. How do I calculate the inverse of a matrix using linear equations?
+-----------------------------------------------------------------------
+
+  If a matrix M exists, such that:
+
+         | A B C |
+    M  = | D E F |
+         | G H I |
+
+  then the inverse exists:
+
+         | P Q R |
+    M' = | S T U |
+         | V W X |
+
+  and the following expression is valid:
+
+                     -1
+        M     .     M     = I
+
+    | A B C |   | P Q R |   | 1 0 0 |
+    | D E F | . | S T U | = | 0 1 0 |
+    | G H I |   | V W X |   | 0 0 1 |
+
+
+  The inverse can then be calculated through the solution as a set of
+  linear equations ie.:
+
+    | AP + BS + CV |   | 1 |   Column 0 (X)
+    | DP + ES + FV | = | 0 |
+    | GP + HS + IV |   | 0 |
+
+    | AQ + BT + CW |   | 0 |   Column 1 (Y)
+    | DQ + ET + FW | = | 1 |
+    | GQ + HT + IW |   | 0 |
+
+    | AR + BU + CX |   | 0 |   Column 2 (Z)
+    | DR + EU + FX | = | 0 |
+    | GR + HU + IX |   | 1 |
+
+
+TRANSFORMS
+==========
+
+Q26. What is a rotation matrix?
+-------------------------------
+
+  A rotation matrix is used to rotate a set of points within a
+  coordinate system. While the individual points are assigned new
+  coordinates, their relative distances do not change.
+
+  All rotations are defined using the trigonometric "sine" and "cosine"
+  functions.
+
+  For a two-dimensional coordinate system, the rotation matrix is as
+  follows:
+
+    | cos(A)  -sin(A) |
+    |                 |
+    | sin(A)   cos(A) |
+
+
+  With the rotation angle A set to zero, this generates the identity
+  matrix:
+
+        |  1  0 |
+    I = |       |
+        |  0  1 |
+
+
+  If the rotation is set to +90 degrees, then the matrix is as follows:
+
+        |  0 -1 |
+    M = |       |
+        |  1  0 |
+
+  If the rotation is set to -90 degrees, then the matrix is as follows:
+
+        |  0  1 |
+    M = |       |
+        | -1  0 |
+
+
+  Negating the rotation angle is equivalent to generating the transpose
+  of the matrix.
+
+  If a rotation matrix is multiplied with its transpose, the result is
+  the identity matrix.
+
+Q27. How do rotation matrices relate to coordinate systems?
+------------------------------------------------------
+
+  Rotation matrices relate to coordinate systems in the following way.
+  Mathematical convention requires that a positive rotation angle
+  generates a clockwise rotation when looking from the origin towards
+  the positive end of the rotation axis.
+
+  Applying this rule, allows for the derivation of three Cartesian
+  rotation matrices. Consider a right-handed coordinate system. For each
+  rotation axis, look from the origin towards the positive end of the
+  selected axis. This generates the following three views:
+
+
+     +----------------------------------------+
+     |                                        |
+     |   X-axis       Y-axis        Z-axis    |
+     |                                        |
+     |                                        |
+     |  ^ Y          ^ Z                Y ^   |
+     |  |            |                    |   |
+     |  |            |                    |   |
+     |  |            |                    |   |
+     |  |            |                    |   |
+     |  O----> Z     O----> X      X <----O   |
+     |                                        |
+     +----------------------------------------+
+
+
+  Since a positive rotation angle generates a clockwise rotation, it is
+  possible to generate a set of coordinate mappings for each rotation.
+  For simplicity, a rotation of +90 will be considered:
+
+  Starting with the X-axis:
+
+   ( 0, 1, 0 ) -> ( 0, 0, 1 )
+   ( 0, 0, 1 ) -> ( 0,-1, 0 )
+   ( 0,-1, 0 ) -> ( 0, 0,-1 )
+   ( 0, 0,-1 ) -> ( 0, 1, 0 )
+
+  These can be simplified to:
+
+    X' =  X
+    Y' = -Z
+    Z' =  Y
+
+  These can then be placed into a matrix:
+
+         | 1  0       0     |
+    Rx = | 0  cos A  -sin A |
+         | 0  sin A   cos A |
+
+
+  Doing the same for the Y-axis:
+
+    ( 0, 0, 1) -> ( 1,0, 0)
+    ( 1, 0, 0) -> ( 0,0,-1)
+    ( 0, 0,-1) -> (-1,0, 0)
+    (-1, 0, 0) -> ( 0,0, 1)
+
+  These can be simplified to:
+
+    X' = Z
+    Y' = Y
+    Z' = -X
+
+  These can then be placed into a matrix:
+
+         |  cos A   0   sin A  |
+    Ry = |  0       1   0      |
+         | -sin A   0   cos A  |
+
+  And finally for the Z-axis:
+
+    ( 0, 1, 0 ) -> ( -1,  0, 0 )
+    (-1, 0, 0 ) -> (  0, -1, 0 )
+    ( 0,-1, 0 ) -> (  1,  0, 0 )
+    ( 1, 0, 0 ) -> (  0,  1, 0 )
+
+  These can be simplified to:
+
+    X' = -Y
+    Y' =  X
+    Z' =  Z
+
+  Placing these into a matrix:
+
+         |  cos A   -sin A    0  |
+    Rz = |  sin A    cos A    0  |
+         |  0        0        1  |
+
+  These are the three basic rotation matrices used by OpenGL.
+
+
+Q28. How do I generate a rotation matrix in the X-axis?
+-------------------------------------------------------
+
+  Use the 4x4 matrix:
+
+         |  1  0       0       0 |
+     M = |  0  cos(A) -sin(A)  0 |
+         |  0  sin(A)  cos(A)  0 |
+         |  0  0       0       1 |
+
+
+Q29. How do I generate a rotation matrix in the Y-axis?
+-------------------------------------------------------
+
+  Use the 4x4 matrix:
+
+         |  cos(A)  0   sin(A)  0 |
+     M = |  0       1   0       0 |
+         | -sin(A)  0   cos(A)  0 |
+         |  0       0   0       1 |
+
+
+Q30. How do I generate a rotation matrix in the Z-axis?
+-------------------------------------------------------
+
+  Use the 4x4 matrix:
+
+         |  cos(A)  -sin(A)   0   0 |
+     M = |  sin(A)   cos(A)   0   0 |
+         |  0        0        1   0 |
+         |  0        0        0   1 |
+
+
+Q31. What are Euler angles?
+---------------------------
+
+  Euler angles are the name given to the set of rotation angles which
+  specify the rotation in each of the X, Y and Z rotation axii.
+
+  These are specfied in vector format eg. |x y z| and can be stored
+  as a VECTOR data structure.
+
+  For example, the set
+
+  |  0 0  0 | will always generate the identity matrix.
+
+  Other angles are represented as follows:
+
+  | 90 0  0 | is a rotation of +90 degrees in the X-axis.
+
+  | 0 90  0 | is a rotation of +90 degrees in the Y-axis and
+
+  | 0  0 90 | is a rotation of +90 degrees in the Z-axis.
+
+  Euler angles can be represented using a single vector data structure.
+
+
+Q32. What are Yaw, Roll and Pitch?
+----------------------------------
+
+  Yaw, Roll and Pitch are aeronautical terms for rotation using the
+  Euclidean coordinate system (Euler angles), relative to the local
+  coordinate system of an aeroplane.
+
+  Imagine you are viewing an aeroplane from above and from directly behind.
+
+  The Z-axis is lined up with the tail and nose of the aeroplane.
+  The X-axis runs from the tip of the left wing to the tip of the right
+  wing.
+
+  The Y axis points straight up from the ground.
+
+  Pitch then becomes rotation in the X-axis, Yaw becomes rotation in the
+  Y-axis and Roll becomes rotation in the Z-axis.
+
+
+Q33. How do I combine rotation matrices?
+----------------------------------------
+
+  Rotation matrices are combined together using matrix multiplication.
+  As a result, the order of multiplication is very important.
+
+
+Q34. What is Gimbal lock?
+-------------------------
+
+  Gimbal lock is the name given to a problem that occurs with the use of
+  Euler angles. Because the final rotation matrix depends on the order
+  of multiplication, it is sometimes the case that the rotation in one
+  axis will be mapped onto another rotation axis.
+
+  Even worse, it may become impossible to rotate an object in a desired
+  axis. This is called Gimbal lock.
+
+  For example, assume that an object is being rotated in the order Z,Y,X
+  and that the rotation in the Y-axis is 90 degrees.
+
+  In this case, rotation in the Z-axis is performed first and therefore
+  correctly. The Y-axis is also rotated correctly. However, after
+  rotation in the Y axis, the X-axis is rotated onto the Z-axis.
+
+  Thus, any rotation in the X-axis actually rotates the object in the
+  Z-axis. Even worse, it becomes to rotate the object in the X-axis.
+
+  A convinient solution to this problem is to make use of Quaternions.
+
+
+Q35. What is the correct way to combine rotation matrices?
+----------------------------------------------------------
+
+  Really, there is no "correct way" of combining rotation matrices.
+  However, in order to be able to predict the result of combining
+  matrices together, some organisation is required. This is also
+  necessary if a full 3D matrix library is to be built.
+
+  The simplest way to rotate an object is to multiply the matrices
+  using the order:
+
+    M = X.Y.Z
+
+  where M is the final rotation matrix, and X,Y,Z are the individual
+  rotation matrices. This defines a rotation in the X-axis (pitch) first,
+  followed by the Y-axis (yaw) and a final rotation in the Z-axis (roll).
+
+  However, whenever the view from the camera viewpoint is being
+  evaluated, then the order and signs of the rotation is reversed.
+
+  For example, if you are standing up, and turn to your left, everything
+  in your field of view appears to move towards the right.
+
+  However, someone else facing you will say that you turned towards their
+  right.
+
+  Thus the view from the camera is modelled using the order:
+
+    M = -Z.-Y.-X
+
+  This is the inverse (or transpose) of the rotation matrix generated
+  if the camera were being rendered as another object.
+
+
+Q36. How do I generate a rotation matrix from Euler angles?
+-----------------------------------------------------------
+
+  At first glance, the most obvious method to generate a rotation matrix
+  from a set of Euler angles is to generate each matrix individually and
+  multiply all three together ie.
+
+    m3_rotx( mat_x,     vec -> angle_x );
+    m3_roty( mat_y,     vec -> angle_y );
+    m3_rotz( mat_z,     vec -> angle_z );
+    m3_mult( mat_tmp,   mat_z, mat_y   );
+    m3_mult( mat_final, mat_tmp, mat_x );
+
+  This set of calls could be placed in a separate routine eg.
+
+    m3_fromeuler( MATRIX *mat_final, VECTOR3 *euler )
+
+  However, to perform this sequence of calls is very wasteful in terms
+  of processing time. Given that each 4x4 rotation matrix is guaranteed
+  to have 10 elements with value zero (0), 2 elements with value one (1)
+  and four others of arbitary value, over 75% of every matrix operation
+  is wasted. This does not include the set up and initialisation of each
+  matrix.
+
+  Altogether, over 75% of all matrix operations are spent processing
+  arithmetic expressions which lead to either zero or one.
+
+  A more efficient way must be found. Fortunately, there is another way
+  of determining the final resulting matrix.
+
+  If all three matrices are combined in algebraic format, the following
+  expression is defined:
+
+    M = X.Y.Z
+
+  where M is the final matrix,
+        X is the rotation matrix for the X-axis,
+        Y is the rotation matrix for the Y-axis,
+        Z is the rotation matrix for the Z-axis.
+
+  Expanding into rotation matrices in algebraic format gives:
+
+         |  1  0  0 |
+    X  = |  0  A -B |
+         |  0  B  A |
+
+         |  C  0  D |
+    Y  = |  0  1  0 |
+         | -D  0  C |
+
+         |  E -F  0 |
+    Z  = |  F  E  0 |
+         |  0  0  1 |
+
+  where A,B are the cosine and sine of the X-axis rotation axis,
+        C,D are the cosine and sine of the Y-axis rotation axis,
+        E,F are the cosine and sine of the Z-axis rotation axis.
+
+  Then the expression:
+
+    M  = X.Y.Z
+
+  can be split into two matrix multiplications:
+
+    M' = X.Y
+
+    M  = M'.Z
+
+  Evaluating M' first:
+
+
+    M' = X.Y
+
+         | 1  0  0 |   | C  0  D |
+    M' = | 0  A -B | . | 0  1  0 |
+         | 0  B  A |   |-D  0  C |
+
+         | 1.C + 0.0 +  0.-D   1.0 + 0.1 +  0.0   1.D + 0.0 +  0.C |
+    M' = | 0.C + A.0 + -B.-D   0.0 + A.1 + -B.0   0.D + A.0 + -B.C |
+         | 0.C + B.0 +  A.-D   0.0 + B.1 +  A.0   0.D + B.0 +  A.C |
+
+  Simplifying M' gives:
+
+         |  C     0   D   |
+    M' = |  B.D   A  -B.C |
+         | -A.D   B   A.C |
+
+
+  Evaluating M gives:
+
+    M  = M'.Z
+
+         |  C   0   D  |   | E -F  0 |
+    M  = |  BD  A  -BC | . | F  E  0 |
+         | -AD  B   AC |   | 0  0  1 |
+
+         |   C.E + 0.F +   D.0    C.-F + 0.E +  D.0     C.0 + 0.0 +   D.1 |
+    M  = |  BD.E + A.F + -BC.0   BD.-F + A.E + -BC.0   BD.0 + A.0 + -BC.1 |
+         | -AD.E + B.F +  AC.0  -AD.-F + B.E +  AC.0  -AD.0 + 0.0 +  AC.1 |
+
+  Simplifying M gives a 3x3 matrix:
+
+         |  CE      -CF       D  |
+    M  = |  BDE+AF  -BDF+AE  -BC |
+         | -ADE+BF   ADF+BE   AC |
+
+  This is the final rotation matrix. As a 4x4 matrix this is:
+
+         |  CE      -CF       D   0 |
+    M  = |  BDE+AF  -BDF+AE  -BC  0 |
+         | -ADE+BF   ADF+BE   AC  0 |
+         |  0        0        0   1 |
+
+  The individual values of A,B,C,D,E and F are evaluated first. Also, the
+  values of BD and AD are also evaluated since they occur more than once.
+
+  Thus, the final algorithm is as follows:
+
+    -----------------------
+
+    A       = cos(angle_x);
+    B       = sin(angle_x);
+    C       = cos(angle_y);
+    D       = sin(angle_y);
+    E       = cos(angle_z);
+    F       = sin(angle_z);
+
+    AD      =   A * D;
+    BD      =   B * D;
+
+    mat[0]  =   C * E;
+    mat[1]  =  -C * F;
+    mat[2]  =   D;
+    mat[4]  =  BD * E + A * F;
+    mat[5]  = -BD * F + A * E;
+    mat[6]  =  -B * C;
+    mat[8]  = -AD * E + B * F;
+    mat[9]  =  AD * F + B * E;
+    mat[10] =   A * C;
+
+    mat[3]  =  mat[7] = mat[11] = mat[12] = mat[13] = mat[14] = 0;
+    mat[15] =  1;
+
+    -----------------------
+
+  Using basic matrix calculations, the operation count would reach
+  128 multiplications, 96 additions and  80 assignments operations.
+
+  Using the optimised algorithm, only 12 multiplications, 6 subtractions
+  and 18 assignment operations are required.
+
+  So, it is obvious that by using the optimised algorithm, a performance
+  achievement of 1000% is achieved!
+
+
+Q37. How do I convert a rotation matrix to Euler angles?
+--------------------------------------------------------
+
+  This operation is the exact opposite to the one answered in the question
+  above. Given that the rotation matrix is:
+
+         |  CE      -CF       D   0 |
+    M  = |  BDE+AF  -BDF+AE  -BC  0 |
+         | -ADE+BF   ADF+BE   AC  0 |
+         |  0        0        0   1 |
+
+  where A,B are the cosine and sine of the X-axis rotation axis,
+        C,D are the cosine and sine of the Y-axis rotation axis,
+        E,F are the cosine and sine of the Z-axis rotation axis.
+
+  Using the C data structure for a 4x4 matrix, the index values are
+  as follows:
+
+         |  0  1  2  3 |
+    M =  |  4  5  6  7 |
+         |  8  9 10 11 |
+         | 12 13 14 15 |
+
+  From looking at these two tables, it can be see that array element
+  [2] has the value of  D or  sin(Y). Then the rotation angle in the
+  Y-axis can be calculated from a call to to the inverse-sine function.
+  Passing this value to the cosine function then gives the value of
+  C.
+
+  If C is not zero, then the rotation angles in each of the X and Z
+  axii, can be derived from the terms on the third column and first row
+  respectively. These are as follows:
+
+    X-axis:  M[6]  = -BC
+             M[10] =  AC
+
+    Z-axis:  M[0]  =  CE
+             M[1]  = -CF
+
+  The actual rotation angles can be derived by taking each pair of values
+  dividing by C and passing the results to the inverse tangent function.
+
+  If C is zero, then these calculations are not possible. In this case
+  the rotation angle in the Y-axis will be either -90 or +90. Thus D
+  will either have the value of 1 or -1.
+
+  In this case, Gimbal Lock will have occurred. Rotations in both the
+  X and Z axii will appear to be in the same axis. This can be seen
+  through the evaluation of the rotation axis.
+
+         |  0.E       -0.F        1    0 |
+    M  = |  B.1.E+AF  -B.1.F+AE  -B.0  0 |
+         | -A.1.E+BF   A.1.F+BE   A.0  0 |
+         |  0          0          0    1 |
+
+  Multiplying out each term gives:
+
+         |  0          0          1    0 |
+    M  = |  BE+AF     -BF+AE      0    0 |
+         | -AE+BF      AF+BE      0    0 |
+         |  0          0          0    1 |
+
+  Rearranging these terms gives:
+
+         |   0          0          1    0 |
+    M  = |   BE+AF      AE-BF      0    0 |
+         | -(AE-BF)     BE+AF      0    0 |
+         |   0          0          0    1 |
+
+  Then it can be seen that the matrix is really of the form:
+
+         |  0   0   1   0 |
+    M  = |  V   W   0   0 |
+         | -W   V   0   0 |
+         |  0   0   0   1 |
+
+  Where V has the value of BE+AF and
+        W has the value of AE-BF
+
+  These two values can be considered to be the sin and cosine of a
+  single rotation axis.
+
+  The final algorithm is then as follows:
+
+    -----------------------------------
+
+    angle_y = D =  asin( mat[2]);        /* Calculate Y-axis angle */
+    C           =  cos( angle_y );
+    angle_y    *=  RADIANS;
+
+    if ( fabs( C ) > 0.005 )             /* Gimball lock? */
+      {
+      trx      =  mat[10] / C;           /* No, so get X-axis angle */
+      try      = -mat[6]  / C;
+
+      angle_x  = atan2( try, trx ) * RADIANS;
+
+      trx      =  mat[0] / C;            /* Get Z-axis angle */
+      try      = -mat[1] / C;
+
+      angle_z  = atan2( try, trx ) * RADIANS;
+      }
+    else                                 /* Gimball lock has occurred */
+      {
+      angle_x  = 0;                      /* Set X-axis angle to zero */
+
+      trx      =  mat[5];                 /* And calculate Z-axis angle */
+      try      =  mat[4];
+
+      angle_z  = atan2( try, trx ) * RADIANS;
+      }
+
+    /* return only positive angles in [0,360] */
+    if (angle_x < 0) angle_x += 360;
+    if (angle_y < 0) angle_y += 360;
+    if (angle_z < 0) angle_z += 360;
+
+    -----------------------------------
+
+
+Q38. How do I generate a rotation matrix for a selected axis and angle?
+-----------------------------------------------------------------------
+
+  The simplest way to generate this type of rotation matrix is through the
+  use of quaternion mathematics.
+
+  See question [Q53. How do I convert a quaternion to a rotation matrix?]
+  for further details.
+
+  The following code snipped does most of the work (phi being the rotation
+  angle and (u,v,w) the rotation vector):
+
+	rcos = cos(phi);
+	rsin = sin(phi);
+	matrix[0][0] =      rcos + u*u*(1-rcos);
+	matrix[1][0] =  w * rsin + v*u*(1-rcos);
+	matrix[2][0] = -v * rsin + w*u*(1-rcos);
+	matrix[0][1] = -w * rsin + u*v*(1-rcos);
+	matrix[1][1] =      rcos + v*v*(1-rcos);
+	matrix[2][1] =  u * rsin + w*v*(1-rcos);
+	matrix[0][2] =  v * rsin + u*w*(1-rcos);
+	matrix[1][2] = -u * rsin + v*w*(1-rcos);
+	matrix[2][2] =      rcos + w*w*(1-rcos);
+
+  Don't forget to set the rest of the matrix to 0 (1 at [3][3]) if
+  you are using 4x4 matrices!
+
+
+Q39. How do I generate a rotation matrix to map one vector onto another?
+------------------------------------------------------------------------
+
+  When developing animation software, a common requirement is to find
+  a rotation matrix that will map one direction vector onto another.
+
+  This problem may be visualised by considering the two direction
+  vectors to be attached at their starting points. Then the entire
+  rotation space forms a unit sphere.
+
+  In theory, there are an infinite number of rotation axii and angles
+  that will map one vector onto the other. All of these axii lie on the
+  plane where all of the points are the exact same distance from both
+  vectors.
+
+  However, only one solution is of practical interest. This is the path
+  which covers the shortest angular distance between the two vectors.
+
+  The rotation axis to this path is calculated by taking the cross
+  product between the two vectors:
+
+    Vaxis = Vs x Vf
+
+  The rotation angle is calculated by taking the dot product between the
+  two vectors:
+
+                -1
+    Vangle = cos   ( Vs . Vf )
+
+  One practical application of the solution to this problem is finding
+  the shortest flight path between two cities. In this case, each city
+  is represented as a direction vector generated from spherical
+  coordinates. Since planet Earth is spherical, the desired flight path
+  is the shortest angular rotation between the two cities.
+
+  Note: If Vs and Vf are colinear, the cross product returns (0,0,0).
+  You should test for that case and use any of the 90 degree rotations
+  of either Vs or Vf as a rotation axis, e.g. (y,z,x). If x==y==z, then
+  using one of x,y, or z from the second vector might help.
+   --> Is there a better way?
+
+  Having the rotation angle and vector, generating the corresponding matrix
+  is easy, see [Q38. How do I generate a rotation matrix for
+  a selected axis and angle?] for details.
+
+  Don't let the spectre of Gimbal Lock fool you: Euler angles are still
+  a complete representation of any rotation in 3D space; it's just that
+  the actual Euler angles needed to achieve some particular desired
+  rotation may be rather unintuitive.
+
+
+Q40. How do I use matrices to convert one coordinate system to another?
+-----------------------------------------------------------------------
+
+  Similar to the previous problem, the requirement is to map one
+  coordinate system onto another. However, instead of just trying to
+  map one coordinate axis onto another, all three axii have to be matched.
+  Both coordinate systems are therefore represented as either 3x3 or
+  4x4 matrices.
+
+  The problem is therefore to find the rotation matrix that will map one
+  matrix onto another. This can be expressed mathematically:
+
+    Mfinal = Mrot . Morig
+
+  where Mfinal is the final coordinate system matrix,
+        Morig  is the original coordinate system and
+        Mrot   is the desired rotation matrix.
+
+  The goal is then to find the matrix Mrot. This can be achieved by
+  rearranging the equation to give:
+
+                  -1
+    Mfinal . Morig   = Mrot
+
+                           -1
+    Mrot   = Mfinal . Morig
+
+  Thus, the desired rotation matrix can be by calculatng the inverse of
+  the original coordinate system and multiplying it with the final
+  rotation matrix.
+
+  As a check, consider the cases when either the original or final
+  rotation matrices are the identity matrix. In each case, the rotation
+  matrix should match the final matrix and the inverse of the final
+  matrix respectively.
+
+  Once calculated, the rotation matrix may be converted into a
+  quaternion.
+
+Q41. What is a translation matrix?
+----------------------------------
+
+  A translation matrix is used to position an object within 3D space
+  without rotating in any way. Translation operations using matrix
+  multiplication can only be performed using 4x4 matrices.
+
+
+  If the translation is defined by the vector [X Y Z ], then the 4x4
+  matrix to implement translation is as follows:
+
+        | 1  0  0  X |
+        |            |
+        | 0  1  0  Y |
+    M = |            |
+        | 0  0  1  Z |
+        |            |
+        | 0  0  0  1 |
+
+  If the vector is [0 0 0] then the vertex list will remain as before.
+
+
+Q42. What is a scaling matrix?
+------------------------------
+
+  A scaling matrix is used to enlarge or shrink the size of a 3D
+  model.
+
+  If the scaling vector is [X Y Z] then the matrix to perform this is
+  as follows:
+
+        | X  0  0  0 |
+        |            |
+        | 0  Y  0  0 |
+    M = |            |
+        | 0  0  Z  0 |
+        |            |
+        | 0  0  0  1 |
+
+  If the scaling vector is [1 1 1], then this generates the identity
+  matrix and vertex geometry will remain unchanged.
+
+
+Q43. What is a shearing matrix?
+-------------------------------
+
+  A shearing matrix is used to make a 3D model appear to slant sideways.
+  For example, "italic" text requires each character to slant towards the
+  right.
+
+  In three dimensions six possible shearing directions exist:
+
+    o shear X by Y
+    o shear X by Z
+    o shear Y by X
+    o shear Y by Z
+    o shear Z by X
+    o shear Z by Y
+
+  All six shearing directions may be combined into a single matrix:
+
+        | 1    Syx  Szx  0 |
+        |                  |
+        | Sxy  1    Szy  0 |
+    M = |                  |
+        | Sxz  Syz  1    0 |
+        |                  |
+        | 0    0    0    1 |
+        |                  |
+
+    Where Sij implements a shear of I by J
+
+    Thus, Sxy shears X by Y
+
+  In theory, rotation in three dimensions may be considered a combination
+  of six shearing directions.
+
+
+Q44. How do I perform linear interpolation between two matrices?
+----------------------------------------------------------------
+
+  Given two rotation matrices, the problem is to find a way of
+  determining intermediate positions specified by a parametric
+  variable t, where t ranges from 0.0 to 1.0
+
+  This can be achieved by converting the two matrices into either
+  Euler angles or Spherical rotation angles (via quaternions) and
+  a translation vector.
+  In either case, each matrix is converted into a pair of 3D vectors.
+
+  Interpolation between these two vectors can then be performed
+  through the use of the standard linear interpolation equation:
+
+      Vr = Va + t .(Vb - Va )
+
+  where Vr is the resulting vector
+        Va is the start position vector
+        Vb is the final position vector
+
+  This equation may be applied to both translation and rotation
+  vectors.
+
+  Once determined, the resulting translation and rotation are then
+  converted back into the desired intermediate matrix.
+
+
+Q45. How do I perform cubic interpolation between four matrices?
+----------------------------------------------------------------
+
+  Given four rotation or translation matrices, the problem is to
+  find a way  of determining intermediate positions specified by a
+  parametric variable t.
+
+  This can be achieved by making use of cubic interpolation. As with
+  linear interpolation, the four matrices are converted into their
+  corresponding translation and rotation vectors (Again, either Euler
+  angles or spherical rotation angles).
+
+  Each set of four vectors is then converted into a single geometry
+  vector G. Through the use of spline mathematics, this geometry vector
+  is converted into an interpolation matrix M.
+
+  If the geometry vector is defined as:
+
+         | x1 x2 x3 x4 |
+     G = | y1 y2 y3 y4 |
+         | z1 z2 z3 z4 |
+
+  Then multiplication by the base matrix:
+
+         |  -4.5    9.0  -5.5  1.0  |
+    Mb = |  13.5  -22.5   9.0  0.0  |
+         | -13.5   18.0  -4.5  0.0  |
+         |   4.5   -4.5   1.0  0.0  |
+
+  will generate the 3x4 interpolation matrix Mi:
+
+    Mi = G .Mb
+
+  This can be implemented through a standard matrix-vector multiplication.
+
+  Interpolation can then be performed by the use of the parametric
+  variable t:
+
+        R  = Mi . t
+
+                           |t^3|
+    | xr |   | A B C D |   |t^2|
+    | yr | = | E F G H | . |t  |
+    | zr |   | I J K L |   |1  |
+
+  The result vector can then be converted back into a rotation or
+  translation matrix.
+
+  It should be noted that the rotation paths that are generated may
+  occasionally become rather loopy. This is normal, as the algorithm
+  is trying to find the path with the least amount of rotation between
+  all four vectors.
+
+  Of the two methods, spherical rotation angles will usually be seen to
+  provide the cleanest interpolation paths for rotation.
+
+
+Q46. How can I render a matrix?
+-------------------------------
+
+  When using a graphics window for 3D animation, it is convenient to be
+  able to view a rotation matrix concurrently with the animation.
+
+  However, displaying a rotation matrix as an array of numeric values
+  does not provide a very meaningful context.
+
+  An alternative to rendering numeric data is to make use of graphical
+  display methods such as bar-graphs.
+
+  Much like a graphic equalizer on a stereo, a rotation matrix may be
+  displayed in a bar graph format. Each element of the rotation matrix
+  is rendered as an individual bar-graph in the range -1 to +1.
+
+  A 3x3 matrix would look like the following:
+
+    +--+ +--+ +--+
+    |##| |  | |  |
+    +--+ +--+ +--+
+    |  | |  | |  |
+    +--+ +--+ +--+
+
+    +--+ +--+ +--+
+    |  | |##| |  |
+    +--+ +--+ +--+
+    |  | |  | |  |
+    +--+ +--+ +--+
+
+    +--+ +--+ +--+
+    |  | |  | |##|
+    +--+ +--+ +--+
+    |  | |  | |  |
+    +--+ +--+ +--+
+
+  In this case, the rotation matrix is the identity matrix, since each
+  element in the major diagonal is +1, and all others are zero.
+
+  For added visual clarity, parameters which are negative may shaded
+  in a different colour than those which are positive.
+
+
+QUATERNIONS
+===========
+
+Q47. What are quaternions?
+--------------------------
+
+  Quaternions extend the concept of rotation in three dimensions to
+  rotation in four dimensions. This avoids the problem of "gimbal-lock"
+  and allows for the implementation of smooth and continuous rotation.
+
+  In effect, they may be considered to add a additional rotation angle
+  to spherical coordinates ie. Longitude, Latitude and Rotation angles
+
+  A Quaternion is defined using four floating point values |x y z w|.
+
+  These are calculated from the combination of the three coordinates
+  of the rotation axis and the rotation angle.
+
+
+Q48. How do quaternions relate to 3D animation?
+-----------------------------------------------
+
+  As mentioned before, Euler angles have the disadvantage of being
+  susceptible to "Gimbal lock" where attempts to rotate an
+  object fail to appear as expected, due to the order in which the 
+  rotations are performed.
+
+  Quaternions are a solution to this problem. Instead of rotating an
+  object through a series of successive rotations, quaternions allow
+  the programmer to rotate an object through an arbitary rotation axis
+  and angle. 
+
+  The rotation is still performed using matrix mathematics. However,
+  instead of multiplying matrices together, quaternions representing
+  the axii of rotation are multiplied together. The final resulting 
+  quaternion is then converted to the desired rotation matrix.
+
+  Because the rotation axis is specifed as a unit direction vector,
+  it may also be calculated through vector mathematics or from 
+  spherical coordinates ie (longitude/latitude).
+
+  Quaternions offer another advantage in that they be interpolated.
+  This allows for smooth and predictable rotation effects.
+
+
+Q49. How do I calculate the conjugate of a quaternion?
+------------------------------------------------------
+
+  This can be achieved by reversing the polarity (or negating) the
+  vector part of the quaternion, ie:
+    --
+    Qr =  ( Qr.scalar, -Qr.vector )
+
+  ----------------------------------------------------------------
+
+  quaternion_conjugate( QUAT *qr, QUAT *qa )
+    {
+    qr -> qw =  qa -> qw;
+    qr -> qx = -qa -> qx;
+    qr -> qy = -qa -> qy;
+    qr -> qz = -qa -> qz;
+    }
+
+  --------------------------------------
+
+Q50. How do I calculate the inverse of a quaternion?
+----------------------------------------------------
+
+  This is equivalent to calculating the conjugate of the quaternion,
+  if the quaternion is normalized (or a unit quaternion).
+  In all other cases, the magnitude of the inverse is 1/|q|.
+  See Q49. How do I calculate the conjugate of a quaternion?
+
+
+Q51. How do I calculate the magnitude of a quaternion?
+------------------------------------------------------
+
+  The magnitude of a quaternion is calculated by multiplying the
+  quaternion with its conjugate ie:
+
+              ------------
+             /      --
+  |Qr| =  \/     Qr.Qr
+
+  This can be implemented as the following code sequence:
+
+  -------------------------------------------------------------------
+
+  QFLOAT quaternion_magnitude( QUAT *qa )
+    {
+    return( sqrt(qa->qw*qa->qw+
+                 qa->qx*qa->qx+ qa->qy*qa->qy+qa->qz*qa->qz) )
+    }
+
+  -------------------------------------------------------------------
+
+
+Q52. How do I normalise a quaternion?
+-------------------------------------
+
+  A quaternion can be normalised in a way similar to vectors. The
+  magnitude of the quaternion is calculated first. Then both the
+  scalar and vector part of the quaternion are divided by this value.
+
+  A unit quaternion will always have a magnitude of 1.0
+
+
+Q53. How do I multiply two quaternions together?
+------------------------------------------------
+
+  Given two quaternions Q1 and Q2, the goal is to calculate the
+  combined rotation Qr:
+
+    Qr = Q1.Q2
+
+  This is achieved through the expression:
+
+    Qr = Q1.Q2 = ( w1.w2 - v1.v2, w1.v2 + w2.v1 + v1 x v2 )
+
+  where v1 = (x,y,z) of Q1
+        w1 = (w)     of Q1
+        v2 = (x,y,z) of Q2
+        w2 = (w)     of Q2
+
+  and both . and x are the standard vector dot and cross products.
+
+  This can be implemented using the following code segment:
+
+  ---------------------------------------------------
+
+  quaternion_multiply( QUAT *qr, QUAT *qa, QUAT *qb )
+    {
+    qr.scalar = qa->scalar * qb->scalar - v3_dot( &qa->vector, &qb->vector );
+
+    v3_cross(  &va, &qa->vector, &qb->vector );
+    v3_scalef( &vb, &qa->vector, &qb->scalar );
+    v3_scalef( &vc, &qb->vector, &qa->scalar );
+    v3_add(    &va,         &va, &vb );
+    v3_add(    &qr->vector, &va, &vc );
+
+    quaternion_normalise( qr );
+    }
+
+  ---------------------------------------------------
+
+  An optimization can also be made by rearranging to
+
+	w = w1w2 - x1x2 - y1y2 - z1z2
+	x = w1x2 + x1w2 + y1z2 - z1y2
+	y = w1y2 + y1w2 + z1x2 - x1z2
+	z = w1z2 + z1w2 + x1y2 - y1x2
+
+
+Q54. How do I convert a quaternion to a rotation matrix?
+--------------------------------------------------------
+
+  Assuming that a quaternion has been created in the form:
+
+    Q = |X Y Z W|
+
+  Then the quaternion can then be converted into a 4x4 rotation
+  matrix using the following expression (Warning: you might have to
+  transpose this matrix if you (do not) follow the OpenGL order!):
+
+
+         �        2     2                                      �
+         � 1 - (2Y  + 2Z )   2XY + 2ZW         2XZ - 2YW       �
+         �                                                     �
+         �                          2     2                    �
+     M = � 2XY - 2ZW         1 - (2X  + 2Z )   2YZ + 2XW       �
+         �                                                     �
+         �                                            2     2  �
+         � 2XZ + 2YW         2YZ - 2XW         1 - (2X  + 2Y ) �
+         �                                                     �
+
+
+  If a 4x4 matrix is required, then the bottom row and right-most column
+  may be added.
+
+  The matrix may be generated using the following expression:
+
+    ----------------
+
+    xx      = X * X;
+    xy      = X * Y;
+    xz      = X * Z;
+    xw      = X * W;
+
+    yy      = Y * Y;
+    yz      = Y * Z;
+    yw      = Y * W;
+
+    zz      = Z * Z;
+    zw      = Z * W;
+
+    mat[0]  = 1 - 2 * ( yy + zz );
+    mat[1]  =     2 * ( xy - zw );
+    mat[2]  =     2 * ( xz + yw );
+
+    mat[4]  =     2 * ( xy + zw );
+    mat[5]  = 1 - 2 * ( xx + zz );
+    mat[6]  =     2 * ( yz - xw );
+
+    mat[8]  =     2 * ( xz - yw );
+    mat[9]  =     2 * ( yz + xw );
+    mat[10] = 1 - 2 * ( xx + yy );
+
+    mat[3]  = mat[7] = mat[11 = mat[12] = mat[13] = mat[14] = 0;
+    mat[15] = 1;
+
+  The resulting matrix uses the following positions:
+
+      � mat[0]  mat[4] mat[ 8] mat[12] �
+  M = � mat[1]  mat[5] mat[ 9] mat[13] �
+      � mat[2]  mat[6] mat[10] mat[14] �
+      � mat[3]  mat[7] mat[11] mat[15] �
+ 
+    ----------------
+
+
+Q55. How do I convert a rotation matrix to a quaternion?
+--------------------------------------------------------
+
+  A rotation may be converted back to a quaternion through the use of
+  the following algorithm:
+
+  The process is performed in the following stages, which are as follows:
+
+    Calculate the trace of the matrix T from the equation:
+
+                2     2     2
+      T = 4 - 4x  - 4y  - 4z
+
+                 2    2    2
+        = 4( 1 -x  - y  - z )
+
+        = 1 + mat[0] + mat[5] + mat[10]
+
+
+    If the trace of the matrix is greater than zero, then
+    perform an "instant" calculation.
+    Important note wrt. rouning errors:
+    Test if ( T > 0.00000001 ) to avoid large distortions!
+
+      S = sqrt(T) * 2;
+      X = ( mat[9] - mat[6] ) / S;
+      Y = ( mat[2] - mat[8] ) / S;
+      Z = ( mat[4] - mat[1] ) / S;
+      W = 0.25 * S;
+
+    If the trace of the matrix is equal to zero then identify
+    which major diagonal element has the greatest value.
+
+    Depending on this, calculate the following:
+
+    if ( mat[0] > mat[5] && mat[0] > mat[10] )  {	// Column 0: 
+        S  = sqrt( 1.0 + mat[0] - mat[5] - mat[10] ) * 2;
+        X = 0.25 * S;
+        Y = (mat[4] + mat[1] ) / S;
+        Z = (mat[2] + mat[8] ) / S;
+        W = (mat[9] - mat[6] ) / S;
+	
+    } else if ( mat[5] > mat[10] ) {			// Column 1: 
+        S  = sqrt( 1.0 + mat[5] - mat[0] - mat[10] ) * 2;
+        X = (mat[4] + mat[1] ) / S;
+        Y = 0.25 * S;
+        Z = (mat[9] + mat[6] ) / S;
+        W = (mat[2] - mat[8] ) / S;
+
+    } else {						// Column 2:
+        S  = sqrt( 1.0 + mat[10] - mat[0] - mat[5] ) * 2;
+        X = (mat[2] + mat[8] ) / S;
+        Y = (mat[9] + mat[6] ) / S;
+        Z = 0.25 * S;
+        W = (mat[4] - mat[1] ) / S;
+    }
+
+     The quaternion is then defined as:
+
+       Q = | X Y Z W |
+
+
+Q56. How do I convert a rotation axis and angle to a quaternion?
+----------------------------------------------------------------
+
+  Given a rotation axis and angle, the following
+  algorithm may be used to generate a quaternion:
+
+    ------------------------------------------------
+
+    vector_normalize(axis);
+    sin_a = sin( angle / 2 );
+    cos_a = cos( angle / 2 );
+
+    X    = axis -> x * sin_a;
+    Y    = axis -> y * sin_a;
+    Z    = axis -> z * sin_a;
+    W    = cos_a;
+
+    ------------------------------------------------
+
+  It is necessary to normalise the quaternion in case any values are 
+  very close to zero.
+
+
+Q57. How do I convert a quaternion to a rotation axis and angle?
+----------------------------------------------------------------
+
+  A quaternion can be converted back to a rotation axis and angle
+  using the following algorithm:
+ 
+    ---------------------------------------------------
+    quaternion_normalise( |X,Y,Z,W| );
+
+    cos_a = W;
+    angle = acos( cos_a ) * 2;
+    sin_a = sqrt( 1.0 - cos_a * cos_a );
+
+
+    if ( fabs( sin_a ) < 0.0005 ) sin_a = 1;
+
+    axis -> x = X / sin_a;
+    axis -> y = Y / sin_a;
+    axis -> z = Z / sin_a;
+    --------------------------------------------------- 
+
+
+Q58. How do I convert spherical rotation angles to a quaternion?
+----------------------------------------------------------------
+
+  A rotation axis itself may be defined using spherical coordinates
+  (latitude and longitude) and a rotation angle
+
+  In this case, the quaternion can be calculated as follows:
+
+    -----------------------
+    sin_a    = sin( angle / 2 )
+    cos_a    = cos( angle / 2 )
+
+    sin_lat  = sin( latitude )
+    cos_lat  = cos( latitude )
+
+    sin_long = sin( longitude )
+    cos_long = cos( longitude )
+
+    X       = sin_a * cos_lat * sin_long
+    Y       = sin_a * sin_lat
+    Z       = sin_a * sin_lat * cos_long
+    W       = cos_a
+    -----------------------
+
+  WARNING: There might be a problem in this code.
+  An alternative is the code snipped given in [Q60. How
+  do I convert Euler rotation angles to a quaternion?"].
+
+
+Q59. How do I convert a quaternion to spherical rotation angles?
+----------------------------------------------------------------
+
+  A quaternion can be converted to spherical coordinates by extending
+  the conversion process:
+
+    -----------------------
+    cos_a  = W;
+    sin_a  = sqrt( 1.0 - cos_a * cos_a );
+    angle  = acos( cos_a ) * 2;
+
+    if ( fabs( sin_angle ) < 0.0005 ) sin_a = 1;
+
+    tx = X / sin_a;
+    ty = Y / sin_a;
+    tz = Z / sin_a;
+
+    latitude = -asin( ty );
+
+    if ( tx * tx + tz * tz < 0.0005 )
+      longitude   = 0;
+    else
+       longitude  = atan2( tx, tz );
+
+    if ( longitude < 0 )
+      longitude += 360.0;
+    -----------------------
+
+  WARNING: In this code might still be a problem.
+  Please let me know what it is and how to fix this.
+
+
+Q60. How do I convert Euler rotation angles to a quaternion?
+-------------------------------------------------------------------
+
+  Converting Euler rotation angles to quaterions can be achieved through
+  the use of quaternion multiplication. Each rotation angle is converted 
+  to an axis-angle pair, with the axis corresponding to one of the 
+  Euclidean axii. The axis-angle pairs are converted to quaternions and 
+  multiplied together. The final quaternion is the desired result.
+  
+  The following code segment demonstrates this:
+  ---------------------------------------------
+
+  quaternion_from_euler( QUATERNION *q, VFLOAT ax, VFLOAT ay, VFLOAT az )
+  {
+    VECTOR3 vx = { 1, 0, 0 }, vy = { 0, 1, 0 }, vz = { 0, 0, 1 };
+    QUATERNION qx, qy, qz, qt;
+
+    quaternion_from_axisangle( qx, &vx, rx );
+    quaternion_from_axisangle( qy, &vy, ry );
+    quaternion_from_axisangle( qz, &vz, rz );
+
+    quaternion_multiply( &qt, &qx, &qy );
+    quaternion_multiply( &q,  &qt, &qz );
+  }
+
+  ---------------------------------------------
+
+  The following more or less comes from:
+  http://vered.rose.utoronto.ca/people/david_dir/GEMS/GEMS.html
+  
+   //Pitch->X axis, Yaw->Y axis, Roll->Z axis
+    Quaternion::Quaternion(float fPitch, float fYaw, float fRoll)
+    {
+       const float fSinPitch(sin(fPitch*0.5F));
+       const float fCosPitch(cos(fPitch*0.5F));
+       const float fSinYaw(sin(fYaw*0.5F));
+       const float fCosYaw(cos(fYaw*0.5F));
+       const float fSinRoll(sin(fRoll*0.5F));
+       const float fCosRoll(cos(fRoll*0.5F));
+       const float fCosPitchCosYaw(fCosPitch*fCosYaw);
+       const float fSinPitchSinYaw(fSinPitch*fSinYaw);
+
+       X = fSinRoll * fCosPitchCosYaw     - fCosRoll * fSinPitchSinYaw;
+       Y = fCosRoll * fSinPitch * fCosYaw + fSinRoll * fCosPitch * fSinYaw;
+       Z = fCosRoll * fCosPitch * fSinYaw - fSinRoll * fSinPitch * fCosYaw;
+       W = fCosRoll * fCosPitchCosYaw     + fSinRoll * fSinPitchSinYaw;
+    }
+
+  ---------------------------------------------
+   
+
+Q61. How do I use quaternions to perform linear interpolation between matrices?
+-------------------------------------------------------------------------------
+
+  For many animation applications, it is necessary to interpolate
+  between two rotation positions of a given object. These positions may
+  have been specified using keyframe animation or inverse kinematics.
+
+  Using either method, at least two rotation matrices must be known, and
+  the desired goal is to interpolate between them. The two matrices are
+  referred to as the starting and finish matrices( MS and MF).
+
+  Using linear interpolation, the interpolated rotation matrix is
+  generated using a blending equation with the parameter T, which
+  ranges from 0.0 to 1.0.
+
+  At T=0, the interpolated matrix is equal to the starting matrix.
+  At T=1, the interpolated matrix is equal to the finishing matrix.
+
+  Then the interpolated rotation matrix (MI) is specified as:
+
+    MI = F( MS, MF, T )
+
+  where F is a blending function.
+
+  The first stage in interpolating between the two matrices is to
+  determine the rotation matrix that will convert MS to MF.
+  This is achieved using the following expression:
+
+          -1
+    T = Ms  . Mf
+
+  where Ms is the start matrix,
+        Mf is the finish matrix,
+    and T is the intermediate matrix.
+
+  The next stage is to convert this matrix into a rotation axis and
+  angle. This is achieved by converting the matrix into a quaternion
+  and finally into the required rotation axis and angle.
+
+  In order to generate the interpolated rotation matrix, it is only
+  necessary to scale the rotation angle and convert this angle and
+  the rotation axis back into a rotation matrix.
+
+  Using a 4x4 matrix library, this is as follows:
+
+    m4_transpose(    mt, ms );                /* Inverse             */
+    m4_mult(         ms, mt, mb );            /* Rotation matrix     */
+    m4_to_axisangle( ms, axis, angle );       /* Rotation axis/angle */
+
+
+    for ( t = 0; t < 1.0; t += 0.05 )
+      {
+      m4_from_axisangle( mi, axis, angle * t ); /* Final interpolation */
+
+      ... whatever ...
+
+      }
+
+  where t is the interpolation factor ranging from 0.0 to 1.0
+
+
+Q62. How do I use quaternions to perform cubic interpolation between matrices?
+------------------------------------------------------------------------------
+
+  For some applications, it may not be convenient or possible to use linear
+  interpolation for animation purposes. In this case, cubic interpolation
+  is another alternative.
+
+  In order to use cubic interpolation, at least four rotation matrices must
+  be known.
+
+  Each of these is then converted into a set of spherical rotations
+  via quaternions and spherical rotation angles (ie. longitude, latitude
+  and rotation angle).
+
+  These are then multiplied with the base matrix for a Cardinal spline
+  curve. This interpolation matrix can then be used to determine the
+  intermediate spherical rotation angles.
+
+  Once the interpolated coordinates are known (latitude, longitude and
+  rotation angle), the interpolated rotation matrix can then be generated
+  through the conversion to quaternions.
+
+  Using a 4x4 matrix library, the algorithm is as follows:
+
+    ----------------------------------------------------------------------
+
+    for ( n = 0; n < 4; n++ )
+      m4_to_spherical( mat[n], &v_sph[n] );      /* Spherical coordinates */
+
+    m4_multspline( m_cardinal, v_sph, v_interp ); /* Interpolation vector */
+
+    ...
+
+    v3_cubic( v_pos, v_interp, t );            /* Interpolation */
+
+    m4_from_spherical( m_rot, v_pos );        /* Back to a matrix */
+
+    ----------------------------------------------------------------------
+
+
+Q63. How do I use quaternions to rotate a vector?
+------------------------------------------------------------------------------
+
+  A rather elegant way to rotate a vector using a quaternion directly
+  is the following (qr being the rotation quaternion):
+                       -1
+       v' = qr * v * qr
+
+  This can easily be realised and is most likely faster then the transformation
+  using a rotation matrix.
+  
+
+ + + + + + + + + diff --git a/uk/beginners-tutorials/index.markdown b/uk/beginners-tutorials/index.markdown new file mode 100644 index 000000000..96492bd46 --- /dev/null +++ b/uk/beginners-tutorials/index.markdown @@ -0,0 +1,25 @@ +--- +layout: page +status: publish +published: true +title: Початковий OpenGL +date: '2011-05-07 10:45:02 +0200' +date_gmt: '2011-05-07 10:45:02 +0200' +categories: [section] +tags: [] +language: uk +--- +Виконуйте їх в правильному порядку ! + +{% assign sorted_pages = site.pages | sort:"order" %} +{% for p in sorted_pages %} + {% assign splt = p.url | split: page.url %} + {% if splt.size == 2 and splt[0] == '' %} + {% assign slash = splt[1] | split: '/' %} +{% if slash.size == 1 %} +- {{p.title}} +{% else %} + - {{p.title}} +{% endif %} + {% endif %} +{% endfor %} diff --git a/uk/beginners-tutorials/tutorial-1-opening-a-window/index.markdown b/uk/beginners-tutorials/tutorial-1-opening-a-window/index.markdown new file mode 100644 index 000000000..5856b9900 --- /dev/null +++ b/uk/beginners-tutorials/tutorial-1-opening-a-window/index.markdown @@ -0,0 +1,242 @@ +--- +layout: tutorial +status: publish +published: true +title: 'Туторіал 1 : Відкриваємо вікно' +date: '2011-04-07 17:54:16 +0200' +date_gmt: '2011-04-07 17:54:16 +0200' +categories: [tuto] +order: 10 +tags: [] +language: uk +--- + +* TOC +{:toc} + +# Вступ + +Ласкаво просимо до першого туторіала! + +Перед зануренням в OpenGL, ми спочатку вивчимо як побудувати код що йде з кожним туторіалом і як його запустити, і, що дуже важливо, як гратись з цим кодом самостійно. + +# Передумови + +Ніяких спеціальних знань не потрібно, що б йти цим туторіалом. Досвід в будь якій мові програмування ( C, Java, Lisp, Javascript, чи щось інше ) буде допомагати зрозуміти цей код, але не є потрібним, це навіть буде дещо складно вивчати дві речі одночасно. + +Всі туторіали написанні з використанням "простого с++" : багато зусиль було направлено на те, що б зробити код якомога простіше. Ніяких шаблонів, класів та вказівників. Тому, ви зможете все зрозуміти навіть якщо ви знаєте тільки Java. + +# Забудьте все + +Ви нічого не знали, та забудьте все, що ви знали про OpenGL. +Якщо ви знаєте щось схоже на `glBegin()` - забудьте. Тут ми будемо вивчати сучасний OpenGL (OpenGL 3 та 4) і багато туторіалів в інтернеті вчать "старий" OpenGL (OpenGL 1 та 2). Отже, забудьте все, що ви могли знати до цього, інакше ваші мізки вибухнуть від суміші. + +# Збирання (компіляція та лінковка) туторіала + +Всі туторіали можуть бути побудовані на Windows, Linux та Mac. Для всіх них процедура в основному однакова : + +* **Оновіть Ваші драйвера** !! ЗРОБІТЬ ЦЕ. Ви попередженні. +* Завантажте компілятор, якщо у вас не має ще його. +* Встановіть CMake +* Завантажте код цього туторіала +* Згенеруйте проект, використовуючи CMake +* Побудуйте проект, використовуючи ваш компілятор +* Пограйтесь з прикладами ! + +Нижче ви знайдете детальні інструкції для різних платформ. Можливо, потрібно буде їх трішки адаптувати. Якщо не впевнені, читайте інструкцію для Windows та адаптуйте її. + +## Збираємо в Windows + + * Оновлення ваших драйверів достатньо проста процедура. Просто відвідайте сайт NVIDIA чи AMD та завантажте їх. Якщо ви не впевнені що до моделі GPU, просто відкрийте Панель керування - Система та безпека - Система - Менеджер пристроїв - Адаптер дисплея. Якщо у вас інтегроване відео від Intel, драйвер буде від виробника комп'ютера/ноутбука. + + * Ми пропонуємо використовувати Visual Studio 2017 Express for Desktop як компілятор. Ви маєте змогу завантажити його безкоштовно [тут](https://www.visualstudio.com/en-US/products/visual-studio-express-vs). Переконайтесь, що ви обрали "вибіркове встановлення" і відмітили С++. Якщо вам більше до вподоби MinGW, ми рекомендуємо використовувати [Qt Creator](http://qt-project.org/). Встановіть те, що ви хочете. Послідовність кроків буде пояснюватись для Visual Studio, але вони будуть подібні для більшості середовищ для програмування (IDE). +* Завантажте [CMake ](http://www.cmake.org/cmake/resources/software.html) та встановіть його. +* [Завантажте код](http://www.opengl-tutorial.org/download/) та розпакуйте його, наприклад в C:\Users\XYZ\Projects\OpenGLTutorials\ +* Запустіть CMake. В першому рядку вкажіть каталог, куди ви розпакували код. Якщо не впевнені, оберіть каталог, що містить файл CMakeLists.txt. В наступному рядку введіть ім'я каталогу, де компілятор збереже всі свої файли. Наприклад C:\Users\XYZ\Projects\OpenGLTutorials-build-Visual2017-64bits\, чи C:\Users\XYZ\Projects\OpenGLTutorials\build\Visual2017-32bits\. Зверніть увагу, що ви можете обрати будь який каталог, не обов'язково в цьому. +![]({{site.baseurl}}/assets/images/tuto-1-window/CMake.png) + +* Натисніть на кнопку Configure. Так як ви робите це вперше, CMake запитає вас про компілятор, який ви хочете використовувати. Обирайте згідно першого пункту. Якщо у вас 64бітна Windows, ви можете обрати 64бітний компілятор. Якщо є сумніви - обирайте 32бітний. +* Натискайте кнопку Configure до тих пір, поки всі червоні поля не зникнуть. Натисніть Generate (Згенерувати). Ваш проект в Visual Studio буде створено. Тепер можна забути про CMake. +* Відкрийте C:\Users\XYZ\Projects\OpenGLTutorials-build-Visual2010-32bits\. Знайдіть там Tutorials.sln file і відкрийте в Visual Studio. +![]({{site.baseurl}}/assets/images/tuto-1-window/directories.png) + +В меню *Build*, натисніть *Build All*. Кожний туторіал і залежності будуть скомпільовані. Кожний виконуваний файл буде скопійовано в C:\Users\XYZ\Projects\OpenGLTutorials\ . Сподіваюсь, помилок не буде. + +![]({{site.baseurl}}/assets/images/tuto-1-window/visual_2010.png) + +* Відкрийте C:\Users\XYZ\Projects\OpenGLTutorials\playground, запустіть playground.exe. З'явиться вікно з чорним вмістом. + +![]({{site.baseurl}}/assets/images/tuto-1-window/empty_window.png) + +Ви можете запустити будь який туторіал з Visual Studio. Натисніть правою кнопкою мишки на Playground один раз та оберіть "Choose as startup project". Тепер ви маєте змогу відлагоджувати код після того, як натиснете F5. + +![]({{site.baseurl}}/assets/images/tuto-1-window/StartupProject.png) + + +## Збираємо в Linux + +В світі дуже багато різних Linux-дистрибутивів, так що неможливо дати одну єдину інструкцію. Адаптація може бути потрібною, так що не вагаючись, читайте документацію по вашому дистрибутиву. + +* Встановіть останні драйвера. Ми наполегливо рекомендуємо використовувати "пропрієтарні драйвера". Так, це не GNU, але вони працюють. Якщо Ваш дистрибутив не вміє автоматично встановлювати їх, подивіться на [документацію Ubuntu](http://help.ubuntu.com/community/BinaryDriverHowto). +* Встановіть всі потрібні компілятори, інструменти та бібліотеки. Повний список `cmake make g++ libx11-dev libxi-dev libgl1-mesa-dev libglu1-mesa-dev libxrandr-dev libxext-dev libxcursor-dev libxinerama-dev libxi-dev`. Для встановлення використовуйте `sudo apt-get install *****` чи `su && yum install ******` +* +* [Завантажте вихідний код](http://www.opengl-tutorial.org/download/) та розпакуйте його, наприклад сюди ~/Projects/OpenGLTutorials/ +* перейдіть в цей каталог (куди було розпаковано код) ~/Projects/OpenGLTutorials/ , і введіть наступне: + + * mkdir build + * cd build + * cmake .. + + +* makefile було створено в каталозі build/. +* Введіть "make all". Всі туторіали і залежності будуть скомпільовані. Всі файли, які можна запустити, будуть скопійовані в каталог ~/Projects/OpenGLTutorials/. Сподіваємось, що помилок не буде. +* Відкрийте ~/Projects/OpenGLTutorials/playground, і запустіть ./playground. Повинно з'явитись "чорне вікно". + +Також рекомендується використовувати IDE, наприклад, [Qt Creator](http://qt-project.org/). До речі, Qt Creator має вбудовану підтримку CMake і забезпечує комфортні умови для налагодження програми. Ось інструкції для нього: + +* В QtCreator, перейдіть по меню File->Tools->Options->Compile&Execute->CMake +* Встановіть шлях до CMake. Скоріше за все це буде щось виду /usr/bin/cmake +* В File->Open Project; виберіть tutorials/CMakeLists.txt +* Виберіть build каталог, краще за межами каталогу tutorials +* За бажанням додайте `-DCMAKE_BUILD_TYPE=Debug`` в параметрах. Перевірте. +* Натисніть на "молоток" внизу. Туторіал буде запущено з каталогу tutorials/. +* Що б запустити туторіал з QtCreator, натисніть Projects->Execution parameters->Working Directory, і оберіть каталог з шейдерами, текстурами і моделями. Наприклад, для туторіалу номер 2 це ~/opengl-tutorial/tutorial02_red_triangle/ + + +## Збираємо на Mac + +Ця процедура дуже подібна до Windows (можна використовувати і Makefile, але про них тут не буде пояснень): + +* Встановіть XCode з Mac App Store +* [Завантажте CMake](http://www.cmake.org/cmake/resources/software.html), і встановіть .dmg . Вам не потрібно встановлювати інструменти командного рядка. +* [Завантажте вихідний код](http://www.opengl-tutorial.org/download/) і розпакуйте його, наприклад, сюди ~/Projects/OpenGLTutorials/ . +* Запустіть CMake (Applications->CMake). В першому рядку перейдіть в каталог з розпакованими файлами (там повинен бути файл CMakeLists.txt). В другому рядку вкажіть, де буде розміщено весь вивід, потрібний компілятору. Наприклад, можна взяти каталог ~/Projects/OpenGLTutorials_bin_XCode/. Зауважте, це може бути будь-який каталог, не обов'язково в цім самім. +* Натисніть кнопку Configure. Так як це перший раз конфігурується проект, CMake запитає, який компілятор використовувати. Оберіть XCode. +* Продовжуйте натискати Configure до тих пір, коли всі червоні рядки не зникнуть. Потім натисніть Generate. Ващ Xcode проект було створено. Можна забути про CMake. +* Відкрийте ~/Projects/OpenGLTutorials_bin_XCode/ . Ви знайдете там файл Tutorials.xcodeproj - відкривайте його. +* Оберіть бажаний туторіал для запуску в панелі Xcode's Scheme і натисніть кнопку Run для компіляції та запуску: + +![]({{site.baseurl}}/assets/images/tuto-1-window/Xcode-projectselection.png) + + +## Замітки Code::Blocks + +Через те, що є дві бажини (один в C::B, один в CMake), Вам потрібно відредагувати командний рядок в Project->Build Options->Make commands, наступним чином : + +![]({{site.baseurl}}/assets/images/tuto-1-window/CodeBlocksFix.png) + + +Також потрібно встановити робочий каталог: Project->Properties -> Build targets -> tutorial N -> execution working dir ( це src_dir/tutorial_N/ ). + +# Як запустити цей туторіал + +Ви повинні запускати туторіали безпосередньо з правильного каталогу - просто подвійним кліком на файлі. Якщо Вам подобається командний рядок, перейдіть в потрібний каталог. + +Якщо Ви хочете запускати туторіал з IDE, не забудьте почитати інструкції вище і встановити правильний робочий каталог. + +# Як користуватись цим туторіалом + +Кожний туторіал містить код і дані, які можуть бути знайденими в каталозі tutorialXX/. Проте, Вам не слід модифікувати ці проекти - вони для довідки. Відкрийте playground/playground.cpp і модифікуйте його. Знущайтесь з нього так, як Вам заманеться. Якщо з ним щось трапиться, просто скопіюйте будь-який туторіал в нього і все повернеться до норми. + +Ми будемо надавати шматки коду по ходу туторіалів. Не бійтесь копіювати їх напряму в "ігровий майданчик" (playground) під час читання - експерименти це добре. Уникайте простого читання кінцевого коду - так Ви багато не вивчите. Навіть просто копіюючи собі код ви отримаєте купу проблем. + +# Відкриваємо вікно + +На кінець! Код на OpenGL! +Ну, добре, не зовсім. Багато туторіалів показують Вам "низькорівневі деталі", що б Ви могли побачити, що магії немає. Але "створити вікно" - це дійсно нудна і непотрібна робота, тому ми будемо використовувати GLFW - зовнішню бібліотеку для цього. Якщо Вам дійсно хочеться, ви можете використовувати Win32 API для Windows, X11 для Linux чи Cocoa API дл Mac, чи якусь іншу високорівневу бібліотеку - SFML, FreeGLUT, SDL ... подивіться цю [сторінку]({{site.baseurl}}/uk/miscellaneous/useful-tools-links/). + +Отже, поїхали. Спочатку розберемося з залежностями: нам потрібні деякі базові речі для друку повідомлень в консолі: + +``` cpp +// Стандартні заголовки +#include +#include +``` + +По перше, GLEW. Цей рядочок - трішки маленької магії, обговоримо пізніше. + +``` cpp +// Підключимо GLEW. Завжди робіть це перед gl.h та glfw3.h, тому що тут трішки магії. +#include +``` + +Ми вирішили дозволити GLFW керувати вікном і клавіатурою, тому включимо і це: + +``` cpp +// Підключимо GLFW +#include +``` + +Нам поки це не потрібно, але це бібліотека для роботи з 3D математикою. Але дуже швидко нам це знадобиться. В бібліотеці GLM немає магії, Ви можете написати свою, просто ця бібліотека зручна. Ми використовуємо `using namespace` що б замість `glm::vec3` писати просто `vec3`. + +``` cpp +// Підключимо GLM +#include +using namespace glm; +``` + +Якщо просто скопіювати і вставити ці всі `#include` в файл `playground.cpp`, компілятор буде жалітись на відсутність функції `main()`. Отже, створимо її: + +``` cpp +int main(){ +``` + +І спочатку ініціалізуємо GLFW: + +``` cpp +// Ініціалізуємо GLFW +glewExperimental = true; // Потрібно для core профілю +if( !glfwInit() ) +{ + fprintf( stderr, "Failed to initialize GLFW\n" ); + return -1; +} +``` + +Тепер ми можемо створити наше перше OpenGL вікно! + +``` cpp +glfwWindowHint(GLFW_SAMPLES, 4); // 4x antialiasing +glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); // Ми хочемо OpenGL 3.3 +glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); +glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // Що б MacOS була щаслива +glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // Нам не потрібен старий OpenGL + +// Відкриємо вікно та створимо OpenGL контекст +GLFWwindow* window; // (In the accompanying source code, this variable is global for simplicity) +window = glfwCreateWindow( 1024, 768, "Tutorial 01", NULL, NULL); +if( window == NULL ){ + fprintf( stderr, "Failed to open GLFW window. If you have an Intel GPU, they are not 3.3 compatible. Try the 2.1 version of the tutorials.\n" ); + glfwTerminate(); + return -1; +} +glfwMakeContextCurrent(window); // Ініціалізуємо GLEW +glewExperimental=true; // Потрібно для core профілю +if (glewInit() != GLEW_OK) { + fprintf(stderr, "Failed to initialize GLEW\n"); + return -1; +} +``` + +Зберемо і запустимо. Вікно повинно з'явитись і відразу закритись. Звичайно! Зробімо так, що б програма чекала поки користувач не натисне Escape. + +``` cpp +// Переконаємося, що ми можемо зафіксувати факт натиснення кнопки нижче +glfwSetInputMode(window, GLFW_STICKY_KEYS, GL_TRUE); + +do{ + // Очистимо екран. Це не згадувалось до туторіла 2, проте це може спричинити мерехтіння, тим не менш воно є. + glClear( GL_COLOR_BUFFER_BIT ); + + // Нічого малювати, побачимось в туторіалі 2 ! + + // обміняємо буфери + glfwSwapBuffers(window); + glfwPollEvents(); + +} // Перевіримо, чи натиснули ESC що б закрити вікно +while( glfwGetKey(window, GLFW_KEY_ESCAPE ) != GLFW_PRESS && + glfwWindowShouldClose(window) == 0 ); +``` + +І зробіть висновки з нашого першого туторіла. В туторіалі 2 ви навчитесь малювати свій перший трикутник. diff --git a/uk/beginners-tutorials/tutorial-2-the-first-triangle/index.markdown b/uk/beginners-tutorials/tutorial-2-the-first-triangle/index.markdown new file mode 100644 index 000000000..d177b7905 --- /dev/null +++ b/uk/beginners-tutorials/tutorial-2-the-first-triangle/index.markdown @@ -0,0 +1,309 @@ +--- +layout: tutorial +status: publish +published: true +title: 'Туторіал 2 : Перший трикутник' +date: '2011-04-07 18:54:11 +0200' +date_gmt: '2011-04-07 18:54:11 +0200' +categories: [tuto] +order: 20 +tags: [] +language: uk +--- + +* TOC +{:toc} + +Це буде ще один довгий туторіал. + +OpenGL 3 робить простим написання складних штук, але малювання одного трикутника достатньо складне. + +Не забувайте копіювати та вставляти собі код регулярно. + +**Якщо програма падає, то можливо її запускають з неправильного каталогу. Прочитайте уважно [перший туторіал]({{ site.baseurl }}/beginners-tutorials/tutorial-1-opening-a-window/) і [FAQ]({{ site.baseurl }}/miscellaneous/faq/), що б налаштувати Visual Studio!** + +# VAO + +Я не хочу сильно заглиблюватись зараз в деталі , але для початку потрібно створити об'єкт "масив вершин" (Vertex Array Object): + +``` cpp +GLuint VertexArrayID; +glGenVertexArrays(1, &VertexArrayID); +glBindVertexArray(VertexArrayID); +``` + +Зробіть це після того, як створено вікно (після того, як створено контекст OpenGL) та перед викликом будь-якої функції OpenGL. + +Якщо ви дійсно хочете знати більше про VAO, тут є декілька інших туторіалів, але це не дуже важливо. + +# Екранні координати + +Трикутник визначається трьома точками. Коли говорять про "точки" в 3D графіці, ми зазвичай використовуємо слово "вершина" (вертекс). Вершина має три координати - X, Y та Z. Ви можете думати про ці три координати наступним чином: + +- X вправо від вас +- Y вгору +- Z до вас (так, назад, а не вперед) + +Та є гарний спосіб уявити собі це - використовуйте правило правої руки + +- X - великий палець +- Y вказівний +- Z середній палець. Якщо великий палець направити вправо, вказівний вгору (в небо), то середній палець буде показувати назад. + +Напрям для осі Z виглядає трішки дивно, чому воно так? Коротка відповідь - тому що сотні років використання правила "правої руки" дало багато корисних інструментів. Єдиний недолік цього - не інтуїтивний напрямок осі Z. + +Зверніть увагу, що ви можете вільно переміщувати вашу руку - ваші X, Y та Z теж будуть переміщуватись. Детальніше про це пізніше. + +Отже нам потрібно три 3D точки для того, що б зробити трикутник, отже почнемо: + + +``` cpp +// вектор з трьох векторів, який являє собою 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 направлена вгору. Ось як це виглядає на широкому екрані: + +![screenCoordinates]({{site.baseurl}}/assets/images/tuto-2-first-triangle/screenCoordinates.png){: height="165px" width="300px"} + +Це те, що ви не можете змінити, це вбудовано в вашу відеокарту. Отже (-1, -1) є нижньою лівою межею вашого екрана. (1, -1) це нижня права межа, (0, 1) - середина зверху. Отже, цей трикутник повинен займати більшу частину екрана. + +# Малюємо наш трикутник + +Наступний крок потрібен для передачі трикутника до OpenGL. Ми зробимо це, створивши буфер: + +```cpp +// Це ідентифікатор нашого вершинного буфера +GLuint vertexbuffer; +// Згенеруємо буфер і збережемо його ідентифікатор в vertexbuffer +glGenBuffers(1, &vertexbuffer); +// Наступна команда зробить наш буфер 'vertexbuffer' активним (поточним) +glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer); +// Передамо наші данні в активний буфер OpenGL. +glBufferData(GL_ARRAY_BUFFER, sizeof(g_vertex_buffer_data), g_vertex_buffer_data, GL_STATIC_DRAW); +``` + +Це потрібно зробити тільки раз. + +Тепер, в нашому головному циклі, де ми малювали "нічого", можемо намалювати наш чудовий трикутник: + +``` cpp +// перший атрибут в буфері : вершини +glEnableVertexAttribArray(0); +glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer); +glVertexAttribPointer( + 0,                  // атрибут номер 0. Нема ніякої причини, чому 0, але він повинен збігатись з номером в layout в шейдері. + 3,                  // розмір + GL_FLOAT,           // тип + GL_FALSE,           // нормалізувати? + 0,                  // крок/stride + (void*)0            // початок в масиву в буфері +); +// Малюємо трикутник ! +glDrawArrays(GL_TRIANGLES, 0, 3); //Починаючи з вершини 0; малюємо 3 вершини -> 1 трикутник +glDisableVertexAttribArray(0); +``` + +Яко ви щасливчик, то ви можете побачити білий трикутник. (**Не панікуйте, якщо не бачите** деякі системи вимагають шейдер для малювання) : + +![triangle_no_shader]({{site.baseurl}}/assets/images/tuto-2-first-triangle/triangle_no_shader1.png){: height="232px" width="300px"} + +Тепер воно сумно-біле. Давайте покращимо це, намалювавши його в червоному кольорі. Це можна зробити використавши "шейдери". + +# Шейдер + +## Компіляція шейдера + +В найпростішому можливому варіанті вам потрібно два шейдери - один називається вершинний шейдер, який буде виконуватись для кожної вершини і другий, який називається фрагментний шейдер, який буде виконуватись для кожної вибірки (sample). І якщо ми використовуємо 4 кратне згладжування, у нас буде 4 вибірки на кожний піксель. + +Шейдери пишуться на мові, яка називається GLSL - GL Shader Language, який є частиною OpenGL. На відміну від С чи Java, GLSL компілюється під час виконання, що значить, что кожний шейдер компілюється кожний раз, коли ваша програма запускається. + +Ці два шейдери зазвичай знаходяться в різних файлах. В нашому прикладі, ми маємо SimpleFragmentShader.fragmentshader та SimpleVertexShader.vertexshader. Розширення файлу не має значення, це може бути .txt чи .glsl. + +Отже ось код. Не обов'язково повністю зрозуміти їх, ми робимо це один раз в програмі, коментарів буде достатньо. Так як ця функція буде використовуватись в всіх наших прикладах, код розташований в окремому файлі - common/loadShader.cpp . Зауважте, що шейдери це просто буфери, вони не доступні напряму - у нас просто є ідентифікатор. Справжня реалізація схована в драйвері. + +``` 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::stringstream sstr; + sstr << VertexShaderStream.rdbuf(); + VertexShaderCode = sstr.str(); + 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::stringstream sstr; + sstr << FragmentShaderStream.rdbuf(); + FragmentShaderCode = sstr.str(); + 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 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 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 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; +} +``` + +## Наш вершинний шейдер + +Давайте спочатку напишемо наш вершинний шейдер. +Перший рядок повідомляє компілятору, що ми хочемо синтаксис третьої версії. + +``` glsl +#version 330 core +``` +{: .highlightglslvs } + +Другий рядок оголошує вхідні дані: + +``` glsl +layout(location = 0) in vec3 vertexPosition_modelspace; +``` +{: .highlightglslvs } + +Давайте розберемо цей рядок детальніше: + +- `vec3` - це вектор з 3 елементів GLSL. Це подібно (але відрізняється) до `glm::vec3`, який ми використовували для оголошення нашого трикутника. Важливо те, що якщо ми використовуємо вектор з трьох елементів в с++, то ми повинні використовувати такий же в GLSL. +- `layout(location = 0)` відноситься до буфера, який поставляє дані для атрибуту `vertexPosition_modelspace` . Кожна вершина може містити багато атрибутів - позицію, один чи декілька кольорів, текстурні координати (і не одні), багато інших даних. OpenGL не знає, що це колір, для нього це просто `vec3`. Тому ми повинні прив'язати буфери до вхідних даних. Ми робимо це, вказавши налаштування <> таким же, як і перший параметр `glVertexAttribPointer`. Значення "0" не є чимось особливим, можна взяти, наприклад, 12 (але не більше, ніж поверне виклик функції `glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &v)`), важливо тільки що б ці числа були однакові. +- `vertexPosition_modelspace` може мати будь-яке ім'я. Воно буде містити позицію поточної вершини для кожного запуску шейдера. +- `in` значить, що це вхідні дані. Скоро ми побачимо ключове слово `out`. + +Функція, яка викликається для кожної вершини має ім'я `main`, так само як і в C: + +``` glsl +void main(){ +``` +{: .highlightglslvs } + +Наша функція `main` просто встановить позицію вершини з того, що було в буфері. Отже, якщо ми маємо (1,1), трикутник буде мати одну з його вершин на краю екрана зверху справа. В наступному тутоіалі ми побачимо, як зробити дещо більш цікавіше з вхідною позицією. + +``` glsl + gl_Position.xyz = vertexPosition_modelspace; + gl_Position.w = 1.0; +} +``` +{: .highlightglslvs } + +`gl_Position` - одна з декількох вбудованих змінних - ви *повинні* присвоїти їй якесь значення. Все інше не є обов'язковим, ми дізнаємось, що таке "все інше" в туторіалі 4. + +## Наш фрагментний шейдер + + +Для нашого першого фрагментного шейдера ми зробимо одну просту річ - виставимо колір для кожного фрагмента в червоне. (Пам'ятаєте, що є чотири фрагменти на один піксель, тому що ми використовуємо 4х згладжування). + +``` glsl +#version 330 core +out vec3 color; +void main(){ + color = vec3(1,0,0); +} +``` +{: .highlightglslfs } + +Так, `vec3(1,0,0)` - це червоний колір. Це тому, що на екрані колір представлений трійкою чисел для червоного, зеленого та синього кольору. Отже (1,0,0) значить чистий червоний, відсутній зелений і синій. + +# Складаємо все докупи + +Імпортуємо нашу функцію `LoadShaders`, додавши include: + +```cpp +#include +``` + +Перед головним циклом викличемо нашу функцію `LoadShaders`: + +```cpp +// Створимо та скомпілюємо нашу GLSL програму з шейдерів +GLuint programID = LoadShaders( "SimpleVertexShader.vertexshader", "SimpleFragmentShader.fragmentshader" ); +``` + +Тепер в основному циклі, спочатку очистимо екран. Це змінить колір тла на темно-синій, тому що перед цим був виклик `glClearColor(0.0f, 0.0f, 0.4f, 0.0f)`: + +``` cpp +glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); +``` + +і скажемо OpenGL, що ми хочемо використовувати наш шейдер: + +``` cpp +// Використовуємо наш шейдер +glUseProgram(programID); +// Малюємо трикутник... +``` + +... і ось ваш червоний трикутник ! + +![red_triangle]({{site.baseurl}}/assets/images/tuto-2-first-triangle/red_triangle.png){: height="231px" width="300px"} + +В наступному туторіалі ми вивчимо трансформації - як налаштувати камеру, переміщувати об'єкти, то що. \ No newline at end of file diff --git a/uk/beginners-tutorials/tutorial-3-matrices/index.markdown b/uk/beginners-tutorials/tutorial-3-matrices/index.markdown new file mode 100644 index 000000000..b052dec40 --- /dev/null +++ b/uk/beginners-tutorials/tutorial-3-matrices/index.markdown @@ -0,0 +1,417 @@ +--- +layout: tutorial +status: publish +published: true +title: 'Туторіал 3 : Матриці' +date: '2011-04-09 20:59:15 +0200' +date_gmt: '2011-04-09 20:59:15 +0200' +categories: [tuto] +order: 30 +tags: [] +language: uk +--- + +* TOC +{:toc} + +> _Двигун не рухає корабель зовсім. Корабель стоїть на місці, а двигуні переміщую всесвіт навколо._ +> +> Futurama + +**Це напевне самий важливий туторіал в цієї серії. Переконайтесь, що Ви прочитали його хоча б разів вісім.** + +# Однорідні координати + +[wiki](https://uk.wikipedia.org/wiki/%D0%9E%D0%B4%D0%BD%D0%BE%D1%80%D1%96%D0%B4%D0%BD%D1%96_%D0%BA%D0%BE%D0%BE%D1%80%D0%B4%D0%B8%D0%BD%D0%B0%D1%82%D0%B8) + +До тепер ми думали про точки в 3-х вимірному просторі як про (x,y,z). Давайте додамо w. Тепер у нас буде вектор (x,y,z,w). + +Це буде зрозуміліше скоро, але зараз запам'ятайте наступне: + +- Якщо w == 1, тоді вектор (x,y,z,1) позиція в просторі. +- Якщо w == 0, тоді вектор (x,y,z,0) напрямок. + +(А ще краще запам'ятати це назавжди.) + +Що це змінює? Гаразд, для поворотів це не змінює нічого. Коли ми крутимо точку чи напрямок ми отримуємо те ж сами. Але для переміщення (коли точка переміщується в певному напрямку), дещо змінюється. А що може означати "перемістити напрямок"? Не багато. + +Однорідні координати дозволять нам використовувати однакові математичні формули для цих двох випадків. + +# Матриця переходу + +[wiki](https://uk.wikipedia.org/wiki/%D0%9C%D0%B0%D1%82%D1%80%D0%B8%D1%86%D1%8F_%D0%BF%D0%B5%D1%80%D0%B5%D1%85%D0%BE%D0%B4%D1%83) + +## An introduction to matrices + +Матриця це просто масив з певною кількістю стовпчиків та рядків. Наприклад, матриця 2х3 може виглядати так: + +![]({{site.baseurl}}/assets/images/tuto-3-matrix/2X3.png) + +В 3D графіці ми в основному використовуємо матриці 4х4. Вони дозволяють нам трансформувати наші вершини (x,y,z,w). Це досягається множенням вершини на матрицю : + +**Matrix x Vertex (in this order !!) = TransformedVertex** + +![]({{site.baseurl}}/assets/images/tuto-3-matrix/MatrixXVect.gif) + +Це не так страшно, як це виглядає. Поставте свій лівий палець на a, ваш правий палець на x. Це _ax_. Перемістіть ваш лівий палець до наступного числа (b) і ваш правий палець до наступного числа (y). Тепер ви маєте _by_. І ще раз : _cz_. Ще раз : _dw_. ax + by + cz + dw. Ви отримали ваш новий x!. Зробіть це для кожного рядка і ви отримаєте новий вектор (x,y,z,w). + +Зараз це трішки нудно обчислювати, ми будемо робити це часто, то ж попросимо комп'ютер робити це за нас. + +**На C++, з GLM:** + +``` cpp +glm::mat4 myMatrix; +glm::vec4 myVector; +// заповнимо myMatrix і myVector чимось +glm::vec4 transformedVector = myMatrix * myVector; // Знову, в цьому порядку ! Це дуже важливо. +``` + +**В GLSL :** + +``` glsl +mat4 myMatrix; +vec4 myVector; +// заповнимо myMatrix і myVector чимось +vec4 transformedVector = myMatrix * myVector; // Так, це достатньо схоже на GLM +``` +{: .highlightglslvs } + +( чи спробували ви скопіювати це в ваш код? давайте, спробуйте ) + +## Матриця перенесення + +Це сама проста матрична трансформація для розуміння. Матриця перенесення виглядає так : + +![]({{site.baseurl}}/assets/images/tuto-3-matrix/translationMatrix.png) + +де X,Y,Z є значеннями, які ви хочете додати до вашої позиції + +То ж, якщо ви хочете перенести вектор (10,10,10,1) на 10 одиниць по осі X, отримуємо : + +![]({{site.baseurl}}/assets/images/tuto-3-matrix/translationExamplePosition1.png) + +(зробіть це! *ЗРОБІТЬ*) + +... і ми отримуємо однорідний вектор (20,10,10,1) ! Пам'ятайте, 1 означає, що є позиція, не напрямок. Наша трансформація не змінила того факту, о ми мали справу з позицією і це дуже добре. + +Давайте поглянемо, що трапиться з вектором, який показує напрямок по осі -z : (0,0,-1,0) + +![]({{site.baseurl}}/assets/images/tuto-3-matrix/translationExampleDirection1.png) + +... отримали оригінальний напрямок (0,0,-1,0), що є добре, тому що як я говорив раніше, переміщення напрямку позбавлено жодного сенсу. + +А як же це перевести в код? + +**В C++, з GLM:** + +``` cpp +#include // після + +glm::mat4 myMatrix = glm::translate(glm::mat4(), glm::vec3(10.0f, 0.0f, 0.0f)); +glm::vec4 myVector(10.0f, 10.0f, 10.0f, 0.0f); +glm::vec4 transformedVector = myMatrix * myVector; // вгадайте результат +``` + +**В GLSL :** + +``` glsl +vec4 transformedVector = myMatrix * myVector; +``` +{: .highlightglslvs } + + +Добре, насправді, ви практично ніколи не будете робити це в GLSL. В більшості випадків, ви будете використовувати функцію glm::translate() в C++ що б розрахувати матрицю і надіслати її в GLSL і зробити тільки множення : + +> TODO: тут повинен бути малюнок множення + +## Одинична матриця + +[wiki](https://uk.wikipedia.org/wiki/%D0%9E%D0%B4%D0%B8%D0%BD%D0%B8%D1%87%D0%BD%D0%B0_%D0%BC%D0%B0%D1%82%D1%80%D0%B8%D1%86%D1%8F) + +Ця матриця особлива. Вона нічого не робить. Та я згадую її тому що це так важливо знати, як і те що множення A на 1.0 дає A. + +![]({{site.baseurl}}/assets/images/tuto-3-matrix/identityExample.png) + +**В C++ :** + +``` cpp +glm::mat4 myIdentityMatrix = glm::mat4(1.0f); +``` + +## Матриці масштабування + +Матриці масштабування теж не складні : + +![]({{site.baseurl}}/assets/images/tuto-3-matrix/scalingMatrix.png) + +Отже, якщо ви хочете збільшити вектор (положення чи напрямок, не має значення) на 2.0 в всіх напрямках : + +![]({{site.baseurl}}/assets/images/tuto-3-matrix/scalingExample.png) + +і w знову не змінюється. Ви можете запитати - в чому суть "масштабування напрямку"?. Добре, зазвичай це позбавлено сенсу і ви не будете використовувати подібне, проте в деяких (дуже рідкісних) випадках це може бути зручним. + +(чи помітили ви, що одинична матриця це просто спеціальний випадок матриці масштабування де (X,Y,Z) = (1,1,1). Це також особливий випадок матриці перенесення де (X,Y,Z)=(0,0,0)) + +**In C++ :** + +``` cpp +// Use #include and #include +glm::mat4 myScalingMatrix = glm::scale(2.0f, 2.0f ,2.0f); +``` + +## Матриці повороту + +Це трішки складнувато. Я не буду заглиблюватись в деталі, так як це не так важливо. Якщо вам цікаво, почитайте [ЧАПИ: матриці і кватерніони]({{site.baseurl}}/assets/faq_quaternions/index.html) (достатньо популярний ресурс, можливо доступний вашою мовою). Також можна подивитись [туторіал по поворотам]({{site.baseurl}}/uk/intermediate-tutorials/tutorial-17-quaternions) + + +**В C++ :** + +``` cpp +// Використовуємо #include і #include +glm::vec3 myRotationAxis( ??, ??, ??); +glm::rotate( angle_in_degrees, myRotationAxis ); +``` + +## Поєднання перетворень + +Тепер ви знаєте, як робити повороти, перенесення та масштабування векторів. Було б добре вміти поєднувати ці перетворення. Цього можна досягти, перемноживши матриці разом, наприклад : + +``` cpp +TransformedVector = TranslationMatrix * RotationMatrix * ScaleMatrix * OriginalVector; +``` + +**!!! ОБЕРЕЖНО !!!** Ці рядки коду насправді спочатку масштабують, потім повертають і в кінці вже роблять перенесення.Так працюють матричні перетворення. + +Якщо написати ці операції в іншому порядку, то швидше за все результат буде іншим. Спробуйте : + + +- зробіть крок вперед (обережно, там ваш комп'ютер) і поверніть наліво ; + +- поверніть наліво і зробіть крок вперед; + +Зрозуміло, що порядок вище це те, що вам зазвичай потрібно для ігрових героїв та об'єктів : спочатку масштабуйте, потім задайте напрямок і робіть перенесення. Наприклад, у нас є модель корабля (повороти не використовуємо для спрощення) : + +* Невірний варіант : + - Ви переносите корабель на (10,0,0). Тепер його центр на 10 одиниць зміщено відносно оригінального положення + - Ви масштабуєте ваш корабель в 2 рази\. Всі координати тепер помножено на 2 _відносно оригінального положення_, яке зараз далеко звідси... І тепер ви маєте великий корабель, і його центр знаходиться на відстані 2*10 = 20 одиниць. Це не те, що ви хочете. + +* Правильний варіант. + - Ви збільшуєте ваш корабель в 2 рази\. Ви отримуєте великий корабель, а його центр залишається на попередньому місці. + - Ви переносите його. Його розмір не змінюється і він на правильній дистанції. + +Перемноження матриць дуже схоже до перемноження матриці на вектор. Тому ми можемо пропустити це знову і якщо вам цікаво, завітайте на сторінку [ЧАПИ: матриці і кватерніони]({{site.baseurl}}/assets/faq_quaternions/index.html#Q11). Зараз ми просто попросимо комп'ютер зробити це за нас : + +**В C++, за допомогою GLM :** + +``` cpp +glm::mat4 myModelMatrix = myTranslationMatrix * myRotationMatrix * myScaleMatrix; +glm::vec4 myTransformedVector = myModelMatrix * myOriginalVector; +``` + +**В GLSL :** + +``` glsl +mat4 transform = mat2 * mat1; +vec4 out_vec = transform * in_vec; +``` +{: .highlightglslvs } + +# Матриці моделі, виду та проекції + +_Далі в цьому туторіалі ми припускаємо, що ви знаєте, як малювати в Blender популярну 3d модель - мавпу Сюзанну (Suzanne)._ + +Матриці моделі, вита та проекції зручні інструменти для розділення перетворень на частини. Ми можете не використовувати їх (в кінці кінців, ми робили це в туторіалі 1 та 2). Але ви повинні. Це те, як всі це роблять, тому що це найпростіший спосіб. + +## Матриця моделі + +Ця модель, як і наш улюблений червоний трикутник, визначена набором вершин. Координати X,Y,Z цих вершин визначені відносно центра об'єкта, тобто це так, начебто (0,0,0) є центром моделі. + +![]({{site.baseurl}}/assets/images/tuto-3-matrix/model.png) + +Нам би хотілось мати змогу переміщувати цю модель, тому що гравець контролює її за допомогою клавіатури та мишки. Це легко, так як ви вже навчились робити це `перенесення*поворот*масштаб`. Ви просто застосовуєте цю матрицю до кожної вершини в кожному кадрі (в GLSL, не в C++!) і все рухається. Те що не переміщується буде знаходитись в _центрі світу_. + +![]({{site.baseurl}}/assets/images/tuto-3-matrix/world.png) + +Your vertices are now in _World Space_. This is the meaning of the black arrow in the image below : _We went from Model Space (all vertices defined relatively to the center of the model) to World Space (all vertices defined relatively to the center of the world)._ + +![]({{site.baseurl}}/assets/images/tuto-3-matrix/model_to_world.png) + +Ми можемо підсумувати це наступною діаграмою : + +![]({{site.baseurl}}/assets/images/tuto-3-matrix/M.png) + +## Матриця виду + +Ще одна цитата з Футурами : + +> _Двигун не переміщую корабель зовсім. Корабель стоїть на місці, а двигун переміщую всесвіт навколо._ + +![]({{site.baseurl}}/assets/images/tuto-3-matrix/camera.png) + +Якщо трішки поміркувати, то те саме можна застосувати і до камер. Якщо ви хочете побачити гору під іншим кутом, ви можете перемістити камеру... чи перемістити гору. Це не зовсім практично в реальному житті, але в комп'ютерній графіці це може бути простіше. + +Отже, спочатку ваша камера знаходиться на початку Світових координат. Для того, щоб перемістити світ, ми просто додамо ще одну матрицю. Нехай ви хочете перемістити вашу камеру на 3 одиниці вправо (+X). Це еквівалентно переміщенню всього світу (з сіткою) на 3 одиниці вліво (-X). Доки ваші мізки горять, зробимо наступне : + +``` cpp +// використовуємо #include і #include +glm::mat4 ViewMatrix = glm::translate(glm::mat4(), glm::vec3(-3.0f, 0.0f ,0.0f)); +``` + +Ще раз, зображення вище ілюструє наступне : _ми перемістились з світового простору (всі вершини визначені відносно центру світу, як ми це зробили в попередній секції) в простір камери (всі вершини визначені відносно камери)._ + +![]({{site.baseurl}}/assets/images/tuto-3-matrix/model_to_world_to_camera.png) + +А поки ваша голова не вибухнула від цього, повеселімося з чудовою функцією `glm::lookAt` з GLM : + +``` cpp +glm::mat4 CameraMatrix = glm::lookAt( + cameraPosition, // позиція камери в світових координатах + cameraTarget, // куди ви хочете дивитись, в світових координатах + upVector // швидше за все просто glm::vec3(0,1,0), але (0,-1,0) якщо бажаєте подивитись, як воно буде догори дриґом +); +``` + +Ось обов'язкова схема + +![]({{site.baseurl}}/assets/images/tuto-3-matrix/MV.png) + +Але це ще далеко не все. + +## Матриця проекції + +Тепер ми в просторі камери. Це значить, що після всіх трансформацій, вершина яка має координати x==0 та y==0 буде намальована в центрі екрана. Але двох координат недостатньо для цього. Нам ще потрібна відстань до камери (z). Для двох вершин з однаковими координатами x та y, вершина з більшою координатою z буде більше в центрі екрана (буде більшою), ніж інша. + +Це називається перспективною проекцією : + +![]({{site.baseurl}}/assets/images/tuto-3-matrix/model_to_world_to_camera_to_homogeneous.png) + +І на щастя для нас, матриця 4х4 може відобразити цю проекцію[^projection]: + +``` cpp +// Генерація складної для прочитання матриці, але це звичайна 4х4 матриця +glm::mat4 projectionMatrix = glm::perspective( + glm::radians(FoV), // Вертикальне поле зору (FoV), в радіанах. Міра збільшення. Це як лінзи камери. Зазвичай лежить в межах 90° (дуже широке) і 30° (трішки збільшене) + 4.0f / 3.0f, //Співвідношення сторін. Залежить від розміру вашого вікна. Зауважте, що 4/3 == 800/600 == 1280/960 - знайомо? + 0.1f, // Ближня площина відсікання. Намагайтесь використовувати якомога більше значення, інакше будуть проблеми с точністю. + 100.0f // Дальня площина відсікання. Намагайтесь використовувати якомога менше значення. +); +``` + +І в останнє : + +_Ми перейшли з простору камери (всі вершини визначені відносно камери) в Однорідний простір (всі вершини визначені в маленькому кубі. Все що в середині куба - видно на екрані)._ + + +І фінальна діаграма : + +![]({{site.baseurl}}/assets/images/tuto-3-matrix/MVP.png) + +Ось ще одна діаграма, що допоможе краще зрозуміти суть перспективи. До перспективного перетворення, ми отримали наші сині об'єкти в просторі камери, і червоні фігури показують зрізану піраміду камери : це частина екрана, що камера може відобразити. + +![]({{site.baseurl}}/assets/images/tuto-3-matrix/nondeforme.png) + +Множення всього на матрицю проекції має наступний ефект : + +![]({{site.baseurl}}/assets/images/tuto-3-matrix/homogeneous.png) + +В цьому зображенні піраміда камери стала ідеальним кубом (з координатами від -1 до 1 по всім осям, це трішки складно, щоб побачити), і всі сині об'єкти були деформовані однаковим чином. Таким чином, об'єкти, що знаходяться ближче до камери (ближче передньої грані куба) будуть великими, інші будуть меншими. Прямо як в реальному житті ! + +Давайте поглянемо, як це виглядає з сторони камери : + +![]({{site.baseurl}}/assets/images/tuto-3-matrix/projected1.png) + +Ось ваше зображення ! Це зображення трішки квадратне, тому що були застосовані інші перетворення (вони автоматичні, вам не потрібно нічого робити для цього в шейдерах), що б зображення гарно виглядало в реальному вікні : : + +![]({{site.baseurl}}/assets/images/tuto-3-matrix/final1.png) + +І це зображення, яке насправді буде намальовано ! + + + +## Комулятивні перетворення : Матриця МодельВидПроекція + +... Звичайні стандартні матричні множення, так як ви їх любите ! + +``` cpp +// C++ : розраховуємо матрицю +glm::mat4 MVPmatrix = projection * view * model; // Пам'ятайте - зворотній порядок ! +``` + +``` glsl +// GLSL : застосуємо це +transformed_vertex = MVP * in_vertex; +``` +{: .highlightglslfs } + +# Зберемо все разом + +* Крок перший : додамо функцію трансформації з GLM GTC : + +``` cpp +#include +``` +* Другий крок : згенеруємо нашу MVP матрицю. Це потрібно зробити для кожної моделі, яку ви хочете намалювати. + + ``` cpp + // Матриця проекції : поле зору : 45°, співвідношення 4:3, діапазон : 0.1 одиниць <-> 100 одиниць + glm::mat4 Projection = glm::perspective(glm::radians(45.0f), (float) width / (float)height, 0.1f, 100.0f); + + // Для ортокамери : + //glm::mat4 Projection = glm::ortho(-10.0f,10.0f,-10.0f,10.0f,0.0f,100.0f); // В світових координатах + + // Camera matrix + glm::mat4 View = glm::lookAt( + glm::vec3(4,3,3), // Камера знаходиться в координатах (4,3,3), в світових координатах + glm::vec3(0,0,0), // І направлена на початок координат + glm::vec3(0,1,0) // "голова зверху" (можна використати 0,-1,0 для догори дриґом) + ); + + // Матриця моделі : одинична матриця (модель буде в центрі координат) + glm::mat4 Model = glm::mat4(1.0f); + // Наша матриця МодельВидПроекція : перемножимо наші три матриці + glm::mat4 mvp = Projection * View * Model; // Пам'ятайте, матриці потрібно множити в зворотному порядку + ``` + +* Третій крок : передамо це в GLSL + + ``` cpp + // Отримаємо об'єкт "MVP" uniform + // Тільки під час ініціалізації + GLuint MatrixID = glGetUniformLocation(programID, "MVP"); + + // Відправимо нашу трансформацію до поточного обраного шейдера, в "MVP" uniform + // Це потрібно робити в головному циклі, так як кожна модель буде мати свою MVP матрицю (як мінімум для частини M - модель) + glUniformMatrix4fv(MatrixID, 1, GL_FALSE, &mvp[0][0]); + ``` +* Четвертий крок : використовуємо в GLSL для перетворення наших вершин в `SimpleVertexShader.vertexshader` + + ``` glsl + // Вхідні вершини, різні для всіх виконань цього шейдера. + layout(location = 0) in vec3 vertexPosition_modelspace; + + // Значення, що залишається незмінним для всієї сітки (меша) + uniform mat4 MVP; + + void main(){ + // вивід позиції вершини, в обрізаному просторі : MVP * position + gl_Position = MVP * vec4(vertexPosition_modelspace,1); + } + ``` + {: .highlightglslvs } + +* Готово ! У нас такий же трикутник, як і в туторіалі номер 2, все ще в початку координат (0,0,0), але ми бачимо його з точки (4,3,3), голова камери направлена догори і поле зору 45° + +![]({{site.baseurl}}/assets/images/tuto-3-matrix/perspective_red_triangle.png) + +В туторіалі номер 6, ви вивчите як модифікувати ці значення динамічно, використовуючи клавіатуру/мишку, що б створити камеру як в іграх, але спочатку ми вивчимо як розфарбувати нашу модель певним кольором (туторіал 4) і накладемо зображення/текстуру (туторіал 5). + +# Вправи + +* Спробуйте змінити glm::perspective +* Спробуйте замість перспективної проекції використати ортографічну проекцію (glm::ortho) +* Модифікуйте матрицю моделі для перенесення, повороту та масштабування. +* Зробіть це саме, тільки в іншому порядку. Що ви помітили? Який порядок буде найкращим для переміщень "героя"? + +_Додаток_ + +[^projection]: [...] на щастя для нас, матриця 4x4 може відобразити цю проекцію : насправді це не так. Трансформація перспективи не є афінною, і як наслідок, не може повністю бути реалізована матрицями. Після множення на матрицю проекції, однорідні координати є поділеними на їх компоненту W. Ця компонента W буває рівною -Z (тому що матриця проекції так зроблена). І тому, точки, що далеко від центру координат будуть поділені на велике Z, їх X та Y координати будуть маленькими, точки будуть більш близькими одна до другої, об'єкти більш маленькими це все дає перспективу. Ця трансформація виконується апаратно (залізом) і не видна в шейдерах. \ No newline at end of file diff --git a/uk/beginners-tutorials/tutorial-4-a-colored-cube/index.markdown b/uk/beginners-tutorials/tutorial-4-a-colored-cube/index.markdown new file mode 100644 index 000000000..c80ecbfed --- /dev/null +++ b/uk/beginners-tutorials/tutorial-4-a-colored-cube/index.markdown @@ -0,0 +1,262 @@ +--- +layout: tutorial +status: publish +published: true +title: 'Туторіал 4 : Кольоровий куб' +date: '2011-04-26 07:55:37 +0200' +date_gmt: '2011-04-26 07:55:37 +0200' +categories: [tuto] +order: 40 +tags: [] +language: uk +--- + +* TOC +{:toc} + +Ласкаво просимо до 4 туторіалу ! Ви зробите наступне : + +* Намалюєте куб, а не ці нудні трикутники +* Додасте трішки кольорів +* Вивчите, що таке Z-буфер + + +# Малюємо куб + +У куба шість квадратних граней. Так як OpenGL знає тільки про трикутники, вам потрібно намалювати 12 трикутників - по два для кожної грані. Ми визначимо наші вершини так само, як ми це робили для трикутників. + +``` cpp +// Наші вершини. Три послідовні числа дають 3D вершину; Три послідовні вершини дають трикутник. +// Куб має 6 граней по два трикутника кожна, отже ми маємо 6*2=12 трикутників, та 12*3 вершин +static const GLfloat g_vertex_buffer_data[] = { + -1.0f,-1.0f,-1.0f, // трикутник 1 : початок + -1.0f,-1.0f, 1.0f, + -1.0f, 1.0f, 1.0f, // трикутник 1 : кінець + 1.0f, 1.0f,-1.0f, // трикутник 2 : початок + -1.0f,-1.0f,-1.0f, + -1.0f, 1.0f,-1.0f, // трикутник 2 : кінець + 1.0f,-1.0f, 1.0f, + -1.0f,-1.0f,-1.0f, + 1.0f,-1.0f,-1.0f, + 1.0f, 1.0f,-1.0f, + 1.0f,-1.0f,-1.0f, + -1.0f,-1.0f,-1.0f, + -1.0f,-1.0f,-1.0f, + -1.0f, 1.0f, 1.0f, + -1.0f, 1.0f,-1.0f, + 1.0f,-1.0f, 1.0f, + -1.0f,-1.0f, 1.0f, + -1.0f,-1.0f,-1.0f, + -1.0f, 1.0f, 1.0f, + -1.0f,-1.0f, 1.0f, + 1.0f,-1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, + 1.0f,-1.0f,-1.0f, + 1.0f, 1.0f,-1.0f, + 1.0f,-1.0f,-1.0f, + 1.0f, 1.0f, 1.0f, + 1.0f,-1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f,-1.0f, + -1.0f, 1.0f,-1.0f, + 1.0f, 1.0f, 1.0f, + -1.0f, 1.0f,-1.0f, + -1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, + -1.0f, 1.0f, 1.0f, + 1.0f,-1.0f, 1.0f +}; +``` +Буфер для OpenGL створено, прив'язаний, заповнений та сконфігурований стандартними функціями (`glGenBuffers`, `glBindBuffer`, `glBufferData`, `glVertexAttribPointer`), деталі в туторіалі номер 2. Код, який це малює не змінився. Вам потрібно тільки встановити правильну кількість вершин, які потрібно намалювати : + +``` cpp +// Намалюємо трикутник ! +glDrawArrays(GL_TRIANGLES, 0, 12*3); // 12*3 індексів, починаючи з 0 -> 12 трикутників -> 6 квадратів +``` + +Декілька зауважень до цього коду : + +* Зараз наша 3D модель є фіксованою - якщо ми хочемо її змінити, нам потрібно модифікувати початковий код програми і надіятись на краще. Ми навчимося завантажувати динамічні моделі в туторіалі 7. +* Кожна вершина записана що найменше тричі (просто пошукайте "-1.0f,-1.0f,-1.0f" в коді). Це даремна витрата пам'яті. Ми вирішимо цю проблему в туторіалі 9. + +Тепер ви маєте всі частинки пазлу, що б намалювати білий куб. Тепер справа за шейдерами. Продовжуйте, як мінімум спробуйте :) + +# Додаємо кольорів + +Колір як і координати це просто дані. В OpenGL вони називаються "атрибутами". Власне кажучи, ми вже використовували це з `glEnableVertexAttribArray()` та `glVertexAttribPointer()`. Давайте додамо ще один атрибут. Код буде дуже схожим. + +По перше, додамо ваші кольори: одна RGB трійка на одну вершину. Я тут їх згенерував випадковим чином, тому вони не будуть виглядати гарно, ви можете придумати щось краще, наприклад, скопіювати координати вершин і використовувати як кольори. + +``` cpp +// один колір для кожної вершини. Кольори випадкові. +static const GLfloat g_color_buffer_data[] = { + 0.583f, 0.771f, 0.014f, + 0.609f, 0.115f, 0.436f, + 0.327f, 0.483f, 0.844f, + 0.822f, 0.569f, 0.201f, + 0.435f, 0.602f, 0.223f, + 0.310f, 0.747f, 0.185f, + 0.597f, 0.770f, 0.761f, + 0.559f, 0.436f, 0.730f, + 0.359f, 0.583f, 0.152f, + 0.483f, 0.596f, 0.789f, + 0.559f, 0.861f, 0.639f, + 0.195f, 0.548f, 0.859f, + 0.014f, 0.184f, 0.576f, + 0.771f, 0.328f, 0.970f, + 0.406f, 0.615f, 0.116f, + 0.676f, 0.977f, 0.133f, + 0.971f, 0.572f, 0.833f, + 0.140f, 0.616f, 0.489f, + 0.997f, 0.513f, 0.064f, + 0.945f, 0.719f, 0.592f, + 0.543f, 0.021f, 0.978f, + 0.279f, 0.317f, 0.505f, + 0.167f, 0.620f, 0.077f, + 0.347f, 0.857f, 0.137f, + 0.055f, 0.953f, 0.042f, + 0.714f, 0.505f, 0.345f, + 0.783f, 0.290f, 0.734f, + 0.722f, 0.645f, 0.174f, + 0.302f, 0.455f, 0.848f, + 0.225f, 0.587f, 0.040f, + 0.517f, 0.713f, 0.338f, + 0.053f, 0.959f, 0.120f, + 0.393f, 0.621f, 0.362f, + 0.673f, 0.211f, 0.457f, + 0.820f, 0.883f, 0.371f, + 0.982f, 0.099f, 0.879f +}; +``` + +Буфер створено, прив'язано і заповнено точнісінько так само як і попередній: + +``` cpp +GLuint colorbuffer; +glGenBuffers(1, &colorbuffer); +glBindBuffer(GL_ARRAY_BUFFER, colorbuffer); +glBufferData(GL_ARRAY_BUFFER, sizeof(g_color_buffer_data), g_color_buffer_data, GL_STATIC_DRAW); +``` + +Конфігурування ідентично : + +``` cpp +// другий атрибут буферу - колір +glEnableVertexAttribArray(1); +glBindBuffer(GL_ARRAY_BUFFER, colorbuffer); +glVertexAttribPointer( + 1, // атрибут. Нема жодної причини, чому тут 1, але в шейдері в полі layout повинно бути таке ж число. + 3, // розмір + GL_FLOAT, // тип + GL_FALSE, // нормалізовано? + 0, // крок + (void*)0 // зміщення в буфері +); +``` + +Тепер, в вершинному шейдері, ми маємо доступ до цього додаткового буферу: + +``` glsl +// Пам'ятайте, що ця "1" потрібна бути рівною 1 в glVertexAttribPointer +layout(location = 1) in vec3 vertexColor; +``` + +{: .highlightglslvs } + +В нашому випадку ми не хочемо робити нічого дивного з вершинним шейдером. Ми просто передамо далі в фрагментний шейдер: + +``` glsl +// Вихідні дані. Будуть інтерпольовані для кожного фрагменту. +out vec3 fragmentColor; + +void main(){ + + [...] + + // Колір кожної вершини буде інтерпольовано + // що б розрахувати колір кожного фрагменту + fragmentColor = vertexColor; +} +``` +{: .highlightglslvs } + +В фрагментному шейдері ми декларуємо `fragmentColor` знову: + +``` glsl +// Інтерпольоване значення з вершинних шейдерів +in vec3 fragmentColor; +``` +{: .highlightglslfs } + +... і скопіюємо в результуючий колір: + +``` glsl +// вихідні дані +out vec3 color; + +void main(){ + // вихідний колір = колір, що вказаний в вершинному шейдері, + // інтерпольований з трьох суміжних вершин + color = fragmentColor; +} +``` +{: .highlightglslfs } + +І ось що ми маємо : + +![]({{site.baseurl}}/assets/images/tuto-4-colored-cube/missing_z_buffer.png) + + +Ой-ой. Потворно. Що б зрозуміти, що коїться, ось що буде, якщо ми намалюємо "далекий трикутник" і "близький": +![]({{site.baseurl}}/assets/images/tuto-4-colored-cube/FarNear.png) + + +Виглядає нормально. Тепер ми намалюємо "далекий" трикутник останнім: + +![]({{site.baseurl}}/assets/images/tuto-4-colored-cube/NearFar.png) + +Він перемалював "близький" трикутник, незважаючи на те, що він має бути за ним! Це те, що трапилось з нашим кубом - деякі грані повинні бути прихованими, але так як вони малювались останніми, то вони видимі. Давайте врятуємо ситуацію за допомогою Z-буфера! + +*Нотатка 1*: якщо ви не бачите проблеми, змініть позицію камери на (4,3,-3) + +*Нотатка 2*: Чому, якщо "колір подібний до позиції, колір це атрибут", нам потрібно декларувати `out vec3 fragmentColor` і `in vec3 fragmentColor` для кольору і не потрібно для позиції? Тому що позиція є трішки особливою - це єдина річ, яка є обов'язковою (інакше OpenGL не буде знати, де потрібно намалювати трикутник!). Тому в вершинному шейдері `gl_Position` є вбудованою змінною. + +# Z-буфер + +Рішення проблеми полягає в тому, що б зберігати глибину (тобто координату "Z") кожного фрагменту в спеціальному буфері і кожного разу, коли ви хочете записати фрагмент, спочатку перевірити чи потрібно це робити (тобто, чи знаходиться новий фрагмент ближче, чим попередній). + +Ви можете зробити це власноруч, але є значно простіший спосіб - просто попросити "залізо" зробити це для вас: + +``` cpp +// Дозволити тест глибини +glEnable(GL_DEPTH_TEST); +// Дозволити малювати фрагмент, якщо він ближче до камери ніж попередній +glDepthFunc(GL_LESS); +``` + +Нам потрібно очищати буфер глибини кожного разу, а не тільки колір: + +``` cpp +// Очистити екран +glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); +``` +І цього достатньо для рішення всіх ваших проблем. + +![]({{site.baseurl}}/assets/images/tuto-4-colored-cube/one_color_per_vertex.png) + +# Вправи + +* Намалюйте куб і трикутник в різних позиціях. Вам потрібно буде згенерувати 2 матриці MVP що б зробити два виклики для малювання в головному циклі, але буде достатньо одного шейдеру. + +* Згенеруйте значення кольору самостійно. Деякі ідеї: Випадкові (ваші кольори будуть змінюватись кожний запуск), залежні від позиції вершин, комбінація цих способів. Придумайте щось своє. Якщо ви не знаєте С, ось синтаксис: + +``` cpp +static GLfloat g_color_buffer_data[12*3*3]; +for (int v = 0; v < 12*3 ; v++){ + g_color_buffer_data[3*v+0] = ваш червоний колір тут; + g_color_buffer_data[3*v+1] = ваш зелений колір тут; + g_color_buffer_data[3*v+2] = ваш синій колір тут; +} +``` + +* Як тільки ви закінчите з цим, зробіть, що б колір змінювався кожний кард. Вам потрібно буде викликати `glBufferData` на кожний кадр. Переконайтесь, що потрібний буфер обрано (прив'язано) (потрібен виклик `glBindBuffer`). diff --git a/uk/beginners-tutorials/tutorial-5-a-textured-cube/index.markdown b/uk/beginners-tutorials/tutorial-5-a-textured-cube/index.markdown new file mode 100644 index 000000000..47422597d --- /dev/null +++ b/uk/beginners-tutorials/tutorial-5-a-textured-cube/index.markdown @@ -0,0 +1,490 @@ +--- +layout: tutorial +status: publish +published: true +title: 'Туторіал 5 : Текстурований куб' +date: '2011-04-26 07:55:58 +0200' +date_gmt: '2011-04-26 07:55:58 +0200' +categories: [tuto] +order: 50 +tags: [] +language: uk +--- + +* TOC +{:toc} + + +В цьому туторіалі ви навчитесь: + +* Що таке UV координати +* Як самостійно завантажити текстури +* Як їх використовувати в OpenGL +* Що таке фільтрація і mip текстури, і як їх використовувати +* Як завантажувати текстури надійніше за допомогою GLFW +* Що таке альфаканал + +# Про UV координати + +Коли текстура накладається на меш, Вам потрібно повідомити OpenGL яка частина зображення використовується для кожного трикутника. Це робиться за допомогою UV координат. + +Кожний вертекс крім положення може мати ще пару дійсних чисел, U та V. Ці координати використовуються для доступу до текстури наступним чином: + +![]({{site.baseurl}}/assets/images/tuto-5-textured-cube/UVintro.png) + +Зверніть увагу, як текстура спотворена на трикутнику. + +# Самостійне завантаження .BMP зображення + +Знання формату BMP файлу не є принциповим - багато бібліотек можуть завантажувати BMP файли для Вас. Та насправді це дуже просто і може допомогти Вам краще зрозуміти, як все працює всередині. Отже, ми напишемо завантажувач BMP файлів з нуля і Ви зрозумієте, як воно працює, але не використовуйте його в Ваших проектах. + +Ось об'явлення функції завантаження: + +``` cpp +GLuint loadBMP_custom(const char * imagepath); +``` + +і використовувати будемо десь так: + +``` cpp +GLuint image = loadBMP_custom("./my_texture.bmp"); +``` +Отже, подивимось, як читати BMP файли. + +Для початку нам потрібні певні данні. Ці змінні будуть встановлені при читанні файлу. + +``` cpp +// Дані, що читаються з заголовку BMP файлу +unsigned char header[54]; // Кожний BMP файл починається з заголовку розміром 54 байти +unsigned int dataPos; // Позиція в файлі, де розташовані фактичні дані +unsigned int width, height; +unsigned int imageSize; // = width*height*3 +// Фактичні RGB дані +unsigned char * data; +``` + +Для початку відкриємо файл + +``` cpp +// Відкриваємо файл +FILE * file = fopen(imagepath,"rb"); +if (!file){printf("Неможливо відкрити зображення\n"); return 0;} +``` +Перша річ в файлі - це 54байтовий заголовок. Він містить інформацію про те, чи це дійсно BMP файл, його розмір, кількість бітів на піксель і інше. Прочитаємо його: + +``` cpp +if ( fread(header, 1, 54, file)!=54 ){ // Якщо не можемо прочитати перші 54 байти, то це проблема + printf("Не коректний BMP файл\n"); + return false; +} +``` +Заголовок завжди починається з двох літер BM. Ось що Ви побачите, якщо відкриєте такий файл в шістнадцятковому редакторі: + +![]({{site.baseurl}}/assets/images/tuto-5-textured-cube/hexbmp.png) + +Отже, перевіримо, що перші два байти це дійсно 'B' та 'M' : + +``` cpp +if ( header[0]!='B' || header[1]!='M' ){ + printf("Not a correct BMP file\n"); + return 0; +} +``` + +Тепер ми можемо прочитати розмір файлу і позицію даних в файлі: + +``` cpp +// Читаємо цілі числа з масиву байтів +dataPos = *(int*)&(header[0x0A]); +imageSize = *(int*)&(header[0x22]); +width = *(int*)&(header[0x12]); +height = *(int*)&(header[0x16]); +``` + +Нам потрібно розрахувати деякі дані, якщо вони відсутні : + +``` cpp +// Деякі BMP файли погано сформовані, вгадаємо відсутню інформацію +if (imageSize==0) imageSize=width*height*3; // 3 : червона, зелена і синя компонента займають по одному байту +if (dataPos==0) dataPos=54; // Дані розташовані відразу за заголовком +``` + +Тепер, коли ми знаємо розмір зображення, ми можемо виділити потрібний розмір пам'яті і прочитати дані: + +``` cpp +// Створюємо буфер +data = new unsigned char [imageSize]; + +// Читаємо дані зображення з файлу в буфер +fread(data,1,imageSize,file); + +//Тепер все потрібне в пам'яті, файл можна закрити +fclose(file); +``` +Тепер прийшов час реального OpenGL. Створення текстури дуже схоже на створення вершинного буфера - створити текстуру, прив'язати, заповнити і сконфігурувати. + +В `glTexImage2D` параметр `GL_RGB` показує, що ми використовуємо трьохкомпонентний колір і `GL_BGR` показує, як саме компоненти розташовані в файлі. Власне кажучи, BMP не зберігає дані в вигляді червоний-зелений-синій, а не синій-зелений-червоний, отже ми підкажемо OpenGL дійсний порядок. + +``` cpp +// Створимо одну текстуру OpenGL +GLuint textureID; +glGenTextures(1, &textureID); + +// "Прив'яжемо" новостворену текстуру - всі подальші функції роботи з текстурою будуть використовувати цю текстуру +glBindTexture(GL_TEXTURE_2D, textureID); + +// Передамо це зображення OpenGL +glTexImage2D(GL_TEXTURE_2D, 0,GL_RGB, width, height, 0, GL_BGR, GL_UNSIGNED_BYTE, data); + +glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); +glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); +``` + +Ми пояснимо ці два рядки коду пізніше. Тепер в с++ коді Ви маєте змогу використовувати цю нову функцію для завантаження текстури: + +``` cpp +GLuint Texture = loadBMP_custom("uvtemplate.bmp"); +``` + +> Важлива деталь :** використовуйте текстури, розміри яких - ступінь двійки !** +> +> * добре : 128\*128, 256\*256, 1024\*1024, 2\*2... +> * погано : 127\*128, 3\*5, ... +> * прийнятно, але незвично : 128\*256 + +# Використання текстур в OpenGL + +Давайте спочатку поглянемо на фрагментний шейдер. Більшість коду достатньо прямолінійна: + +``` glsl +#version 330 core + +// Інтерполюємо дані з вершинного шейдеру +in vec2 UV; + +// Вихідні дані +out vec3 color; + +// Значення, що залишаються незмінними для всього мешу. +uniform sampler2D myTextureSampler; + +void main(){ + + // вихідний колір = колір текстури в координатах UV + color = texture( myTextureSampler, UV ).rgb; +} +``` +{: .highlightglslfs } + +Три речі: + +* Фрагментний шейдер потребує UV координати. Здається очевидно. +* Також він потребує `sampler2D` для того, що б мати доступ до текстури (шейдер може мати доступ до декількох текстур одночасно) +* Нарешті, доступ до текстури виконується за допомогою `texture()`, який повертає (R,G,B,A) vec4. Ми побачимо A незабаром. + +Вершинний шейдер також достатньо простий, потрібно просто передати UV координати в фрагметний шейдер: + +``` glsl +#version 330 core + +// Вхідні вершинні дані, різні для всіх виконань цього шейдера. +layout(location = 0) in vec3 vertexPosition_modelspace; +layout(location = 1) in vec2 vertexUV; + +// Вихідні дані, будуть інтерполюватись для кожного фрагменту. +out vec2 UV; + +// Дані, які будуть незмінні (постійні) для кожного меша. +uniform mat4 MVP; + +void main(){ + + // Вихідна позиція для вершини, в обрізаному просторі (clip space) : MVP * position. + gl_Position = MVP * vec4(vertexPosition_modelspace,1); + + // UV координати для цієї вершини. Немає ніяких спеціальний просторів для цього. + UV = vertexUV; +} +``` +{: .highlightglslvs } + +Пам'ятаєте "layout(location = 1) in vec2 vertexUV" з туторіалу 4 ? Ми зробимо точнісінько так само і тут, але замість буферу триплетів (R,G,B), ми будемо використовувати буфер пар (U,V). + +``` cpp +// Дві UV координати для кожної вершини. Вони були створені за допомогою Blender. Ми скоро навчимося як їх створити самостійно. +static const GLfloat g_uv_buffer_data[] = { + 0.000059f, 1.0f-0.000004f, + 0.000103f, 1.0f-0.336048f, + 0.335973f, 1.0f-0.335903f, + 1.000023f, 1.0f-0.000013f, + 0.667979f, 1.0f-0.335851f, + 0.999958f, 1.0f-0.336064f, + 0.667979f, 1.0f-0.335851f, + 0.336024f, 1.0f-0.671877f, + 0.667969f, 1.0f-0.671889f, + 1.000023f, 1.0f-0.000013f, + 0.668104f, 1.0f-0.000013f, + 0.667979f, 1.0f-0.335851f, + 0.000059f, 1.0f-0.000004f, + 0.335973f, 1.0f-0.335903f, + 0.336098f, 1.0f-0.000071f, + 0.667979f, 1.0f-0.335851f, + 0.335973f, 1.0f-0.335903f, + 0.336024f, 1.0f-0.671877f, + 1.000004f, 1.0f-0.671847f, + 0.999958f, 1.0f-0.336064f, + 0.667979f, 1.0f-0.335851f, + 0.668104f, 1.0f-0.000013f, + 0.335973f, 1.0f-0.335903f, + 0.667979f, 1.0f-0.335851f, + 0.335973f, 1.0f-0.335903f, + 0.668104f, 1.0f-0.000013f, + 0.336098f, 1.0f-0.000071f, + 0.000103f, 1.0f-0.336048f, + 0.000004f, 1.0f-0.671870f, + 0.336024f, 1.0f-0.671877f, + 0.000103f, 1.0f-0.336048f, + 0.336024f, 1.0f-0.671877f, + 0.335973f, 1.0f-0.335903f, + 0.667969f, 1.0f-0.671889f, + 1.000004f, 1.0f-0.671847f, + 0.667979f, 1.0f-0.335851f +}; +``` +UV координати вище відповідають наступній моделі: + +![]({{site.baseurl}}/assets/images/tuto-5-textured-cube/uv_mapping_blender.png) + +Все інше очевидно. Сгенерували (створили) буфер, прив'язали, заповнили, сконфігурували і малюємо вершинний буфер як зазвичай. Будьте обережні і використовуйте число 2 в якості другого параметра функції `glVertexAttribPointer`, а не 3. + +Ось результат : + +![]({{site.baseurl}}/assets/images/tuto-5-textured-cube/nearfiltering.png) + +і збільшена версія : + +![]({{site.baseurl}}/assets/images/tuto-5-textured-cube/nearfiltering_zoom.png) + +# Що таке фільтація та mipmapping, і як їх використовувати + +Як Ви маєте змогу побачити на знімку екрана вище, якість текстури не сама краща. Це тому що в `loadBMP_custom` ми написали наступне: + +``` cpp +glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); +glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); +``` + +Це значить, що в нашому фрагментному шейдері `texture()` бере тексель який знаходиться в координатах (U,V) і щаслива. + +![]({{site.baseurl}}/assets/images/tuto-5-textured-cube/nearest.png) + +Є декілька речей, які ми можемо покращити. + +## Лінійна фільтрація + +З використанням лінійної фільтрації, `texture()` буде дивитись і на сусідні текселі і змішувати кольори відповідно до їхньої відстані від центру. Це допомагає уникнути різких меж, які Ви бачили на малюнку вище. + +![]({{site.baseurl}}/assets/images/tuto-5-textured-cube/linear1.png) + +Це виглядає значно краще, і цей спосіб широко використовується, але якщо Ви хочете кращої якості, ви можете використовувати анізотропну фільтрацію, але вона дещо повільніша. + +## Анізотропна фільтрація + +Ця фільтрація апроксимує частину зображення, яка насправді видима через фрагмент. Наприклад, якщо наступна текстура видима тільки збоку і трішки повернута, анізотропна фільтрація розрахує колір, який знаходиться в синьому прямокутнику беручи фіксовану кількість зразків (це називається рівень анізотропної фільтрації - "anisotropic level") уздовж основного напрямку. + +![]({{site.baseurl}}/assets/images/tuto-5-textured-cube/aniso.png) + +## Mip текстури (Mipmaps) + +Лінійна і анізотропна фільтрація мають проблеми. Якщо текстуру видно з великої відстані, змішування всього 4 текселів буде недостатньо. Дійсно, якщо Ваша 3Д модель знаходиться так далеко, що займає на екрані один фрагмент, ВСІ текселі зображення повинні бути усереднені для отримання кінцевого кольору. Тому, замість цього ми вводимо Mip текстури: + +![](http://upload.wikimedia.org/wikipedia/commons/5/5c/MipMap_Example_STS101.jpg) + +* В процесі ініціалізації, Ви зменшуєте своє зображенні в 2 рази, і повторюєте, поки не отримаєте зображення розміром 1х1 (що по факту і є усередненим кольором всіх текселів на зображенні) +* Коли Ви малюєте меш, Ви обраєте, яку mip текстуру використовувати відповідно до того, наскільки великий тексель повинен бути +* Ви робите вибірку з цієї mip текстури, використовуючи фільтрацію по найближчому, лінійну чи анізотропну. +* Для кращої якості Ви маєте змогу обрати текселі з двох mip текстур і змішати результат. + +На щастя, це дуже легко зробити, OpenGL зробить все для нас, якщо ми гарненько попросимо: + +``` cpp +// Коли зображення збільшується і немає відповідної mip текстури, використовуємо лінійну фільтрацію +glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); +// Коли зображення зменшується, то використовуємо лінійне змішування між двома відповідними mip текстурами, кожна з них фільтрується лінійно +glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); +// Згенеруємо mip текстуру +glGenerateMipmap(GL_TEXTURE_2D); +``` + +# Як завантажити текстуру за допомогою GLFW + +Наша функція `loadBMP_custom` чудова, тому що ми її написали самі, але використання спеціалізованих бібліотек є кращим рішенням. GLFW2 також може справитись з цією задачею (але тільки для TGA файлів і ця можливість була видалена в GLFW3, який ми наразі використовуємо): + +``` cpp +GLuint loadTGA_glfw(const char * imagepath){ + + // Створимо одну OpenGL текстуру + GLuint textureID; + glGenTextures(1, &textureID); + + // Прив'яжемо новостворену текстуру - всі подальші функції роботи з текстурами будуть модифікувати саме цю текстуру + glBindTexture(GL_TEXTURE_2D, textureID); + + // Прочитаємо файл, викличемо glTexImage2D з відповідними параметрами + glfwLoadTexture2D(imagepath, 0); + + // Гарна трилінійна фільтрація. + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glGenerateMipmap(GL_TEXTURE_2D); + + // Повернемо ідентифікатор новоствореної текстури + return textureID; +} +``` + +# Стисненні текстури + +А тепер Вам напевне цікаво, як завантажувати JPEG файли замість TGA? + +Коротка відповідь - ніяк. GPU не розуміє JPEG. Отже, якщо Ваші зображення збережено як JPEG, то Ви повинні розпакувати їх, що б GPU розумів їх. Ви повернулися до сирих зображень, але ще й втратили якість, коли стискали в JPEG. + +Є краще рішення. + +## Створення стиснених текстур + +* Завантажте [Compressonator](http://gpuopen.com/gaming-product/compressonator/), інструмент від AMD +* Завантажте текстуру правильного розміру (степінь 2) в нього +* Згенеруйте mip текстуру, Вам не потрібно це буде робити в процесі виконання програми +* Стисніть його в DXT1, DXT3 чи DXT5 (більше про різницю між цими форматами на [Wikipedia](http://en.wikipedia.org/wiki/S3_Texture_Compression)) + +![]({{site.baseurl}}/assets/images/tuto-5-textured-cube/TheCompressonator.png) + +* Експорт як .DDS файлу. + +Тепер Ваше зображення стиснено в форматі, який безпосередньо підтримується GPU. Коли викликається функція `texture()` в шейдері, текстура буде розпакована нальоту (автоматично). Це здається повільним, але так як дані займають менше місця, воно буде передаватись швидше. Копіювання пам'яті - це дорого. А ось розпакування текстури - безкоштовне (відеокарта має спеціальне "залізо" для цього). Зазвичай, використання стиснення текстур додає 20% покращення продуктивності програми. Отже Ви виграєте в пам'яті і продуктивності, але втрачаєте в якості. + +## Використання стиснених текстур + +Давайте подивимось, як завантажити зображення. Це дуже схоже до завантаження BMP файлу, за виключенням того, що заголовок організований трішки по іншому: + +``` cpp +GLuint loadDDS(const char * imagepath){ + + unsigned char header[124]; + + FILE *fp; + + /* спробуємо відкрити файл */ + fp = fopen(imagepath, "rb"); + if (fp == NULL) + return 0; + + /* перевіримо тип файлу */ + char filecode[4]; + fread(filecode, 1, 4, fp); + if (strncmp(filecode, "DDS ", 4) != 0) { + fclose(fp); + return 0; + } + + /* отримаємо опис поверхні (surface) */ + fread(&header, 124, 1, fp); + + unsigned int height = *(unsigned int*)&(header[8 ]); + unsigned int width = *(unsigned int*)&(header[12]); + unsigned int linearSize = *(unsigned int*)&(header[16]); + unsigned int mipMapCount = *(unsigned int*)&(header[24]); + unsigned int fourCC = *(unsigned int*)&(header[80]); +``` + +Після заголовка знаходяться дані - всі рівні mip текстури, одна за іншою. Ми маємо змогу завантажити їх одним викликом: + +``` cpp + unsigned char * buffer; + unsigned int bufsize; + /* Як забагато пам'яті потрібно для завантаження всих mip текстур? */ + bufsize = mipMapCount > 1 ? linearSize * 2 : linearSize; + buffer = (unsigned char*)malloc(bufsize * sizeof(unsigned char)); + fread(buffer, 1, bufsize, fp); + /* закриємо файл */ + fclose(fp); +``` + +Тепер ми будемо працювати з 3 різними форматами: DXT1, DXT3 і DXT5. Нам потрібно конвертувати "fourCC" прапорець в значення, яке розуміє OpenGL. + +``` cpp + unsigned int components = (fourCC == FOURCC_DXT1) ? 3 : 4; + unsigned int format; + switch(fourCC) + { + case FOURCC_DXT1: + format = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT; + break; + case FOURCC_DXT3: + format = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT; + break; + case FOURCC_DXT5: + format = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; + break; + default: + free(buffer); + return 0; + } +``` + +Текстура створюється як зазвичай: + +``` cpp + // Створимо одну текстуру OpenGL + GLuint textureID; + glGenTextures(1, &textureID); + + // "Прив'яжемо" новостворену текстуру - всі подальші функції роботи з текстурами будуть працювати з цією + glBindTexture(GL_TEXTURE_2D, textureID); +``` + +І тепер, ми просто повинні заповнити кожну mip текстуру по черзі: + +``` cpp + unsigned int blockSize = (format == GL_COMPRESSED_RGBA_S3TC_DXT1_EXT) ? 8 : 16; + unsigned int offset = 0; + + /* завантажуємо mip текстури */ + for (unsigned int level = 0; level < mipMapCount && (width || height); ++level) + { + unsigned int size = ((width+3)/4)*((height+3)/4)*blockSize; + glCompressedTexImage2D(GL_TEXTURE_2D, level, format, width, height, + 0, size, buffer + offset); + + offset += size; + width /= 2; + height /= 2; + } + free(buffer); + + return textureID; +``` + +## Інвертування UV координат + +DXT компресія прийшла з DirectX, де V координата інвертована порівняно з OpenGL. Отже, якщо Ви будете їх використовувати, то Ви повинні робити так `( coord.u, 1.0-coord.v)` для отримання правильного текселя. Ви маєте змогу це зробити там, де хочеться - при експорті, в завантажувачі зображення, в шейдері... + +# Висновки + +Ви щойно вивчили як створити, завантажити і використовувати текстури в OpenGL. + +В цілому, краще використовувати тільки стиснені текстури, так як вони займають менше місця, практично моментально завантажуються і швидкі при використанні. Основний мінус - це використання конверторів типу Compressonator (чи чогось подібного) + +# Вправи + +* Завантажувач DDS реалізований в коді, але без модифікації координат. Змініть код так, що б куб відображався правильно. +* Поекспериментуйте з різними DDS форматами. Чи дають вони різний результат? Різний коефіцієнт стиснення? +* Спробуйте не генерувати mip текстури в Compressonator. Який результат? Придумайте три різних способи виправити це. + +# Посилання + +* [Використання стиснених текстур в OpenGL](http://www.oldunreal.com/editing/s3tc/ARB_texture_compression.pdf) , Sébastien Domine, NVIDIA + diff --git a/uk/beginners-tutorials/tutorial-6-keyboard-and-mouse/index.markdown b/uk/beginners-tutorials/tutorial-6-keyboard-and-mouse/index.markdown new file mode 100644 index 000000000..0ae941c97 --- /dev/null +++ b/uk/beginners-tutorials/tutorial-6-keyboard-and-mouse/index.markdown @@ -0,0 +1,234 @@ +--- +layout: tutorial +status: publish +published: true +title: 'Туторіал 6 : Клавіатура і миша' +date: '2011-05-08 08:26:13 +0200' +date_gmt: '2011-05-08 08:26:13 +0200' +categories: [tuto] +order: 60 +tags: [] +language: uk +--- + +* TOC +{:toc} + +Ласкаво просимо до нашого 6 туторіалу ! + +Тепер ми вивчимо як використовувати мишку та клавіатуру для переміщення камери як шутерах. + +# Інтерфейс + +так як цей код скоріше за все буде використовуватись в подальших туторіалах, ми розмістимо цей код в окремому файлі - common/controls.cpp і об'явимо функції в `common/controls.hpp` так що б `tutorial06.cpp` знав про них. + +Код `tutorial06.cpp` не сильно змінився з попереднього туторіалу. Основна модифікація в тому, що замість однократного обчислення MVP матриці, ми будемо обчислювати її кожний фрейм. Отже перемістимо цей код в головний цикл: + +``` cpp +do{ + + // ... + + // Обчислимо матрицю MVP використовуючи ввід з клавіатури та миші + computeMatricesFromInputs(); + glm::mat4 ProjectionMatrix = getProjectionMatrix(); + glm::mat4 ViewMatrix = getViewMatrix(); + glm::mat4 ModelMatrix = glm::mat4(1.0); + glm::mat4 MVP = ProjectionMatrix * ViewMatrix * ModelMatrix; + + // ... +} +``` +Цей код містить 3 нові функції: + +* `computeMatricesFromInputs()` читає дані з клавіатури та миші і обчислює матриці `Projection` та `View`. Це місце, де трапляється вся магія. +* `getProjectionMatrix()` просто повертає обчислену матрицю `Projection`. +* `getViewMatrix()` просто повертає матрицю `View`. + +Це один з способів це зробити. Якщо Вам не подобаються ці функції - просто змініть їх. + +Давайте поглянемо, що всередині `controls.cpp`. + +# Код в середині + +Нам потрібно декілька змінних. + +``` cpp +// позиція +glm::vec3 position = glm::vec3( 0, 0, 5 ); +// кут по горизонталі : в напрямку -Z +float horizontalAngle = 3.14f; +// кут по вертикалі : 0, якщо дивитись на горизонт +float verticalAngle = 0.0f; +// Початкове поле зору +float initialFoV = 45.0f; + +float speed = 3.0f; // 3 одиниці / секунду +float mouseSpeed = 0.005f; +``` + +FoV - це рівень збільшення. 80° - дуже широкий кут огляду, великі деформації. 60° - 45° - стандартне значення. 20° - велике збільшення. + +Спочатку ми розрахуємо позицію, `horizontalAngle` (кут по горизонталі), `verticalAngle` (кут по вертикалі) та `FoV` (кут огляду), а потім розрахуємо матриці виду і проекції, враховуючи ці дані. + +## Орієнтація + +Прочитати координати миші дуже просто: + +``` cpp +// Отримання позиції миші +int xpos, ypos; +glfwGetMousePos(&xpos, &ypos); +``` + +та нам потрібно потурбуватись, що б курсор був в центрі екрану або він дуже швидко буде за межами вікна і Ви не зможете йоно рухати. + +``` cpp +// Скинемо курсор миші в наступному кадрі +glfwSetMousePos(1024/2, 768/2); +``` + +Зверніть увагу, що цей код розраховує на те, що розмір вікна - 1024х768, але у Вас може бути по другому. Ви можете використати `glfwGetWindowSize` для розрахунку правильного положення. + +Тепер ми можемо розрахувати кути огляду: + +``` cpp +// Розраховуємо нову орієнтацію +horizontalAngle += mouseSpeed * deltaTime * float(1024/2 - xpos ); +verticalAngle += mouseSpeed * deltaTime * float( 768/2 - ypos ); +``` + +Давайте прочитаємо це з права наліво: + +* `1024/2 - xpos` - як далеко курсор миші від центру вікна.Чим більше це значення, тим більше ми хочемо повернути. +* `float(...)` - конвертує в дійсне число, що б множення відбулося добре. +* `mouseSpeed` - це просто число, яке регулює швидкість повороту. Налаштуйте за смаком чи дайте користувачу це зробити. +* `+=` - якщо Ви не переміщуєте мишку, `1024/2-xpos` буде рівним нулю. І `horizontalAngle+=0` не змінить `horizontalAngle`. Якщо Ви будете використовувати `=`, Ви будете постійно повертатись на початкове положення кожний кадр, що не є добре. + +Тепер ми можемо розрахувати вектор, який показує напрямок погляду в світових координатах. + +``` cpp +// Напрямок - сферичні координати перетворюємо в декартові +glm::vec3 direction( + cos(verticalAngle) * sin(horizontalAngle), + sin(verticalAngle), + cos(verticalAngle) * cos(horizontalAngle) +); +``` + +Це стандартні розрахунки, але якщо Ви нічого не знаєте про синуси і косинуси, ось невелике пояснення: + + + +ФОрмули вище є просто узагальненням для 3Д + +Тепер ми хочемо обчислити вектор "вгору" правильно. Зверніть увагу, що "вгору" не завжди має напрямок паралельний `+Y` - якщо, до прикладу, Ви дивитесь вниз, то вектор "вгору" буде направлений горизонтально. Ось приклад камер, що знаходяться в одній і тій же позиції, в тому же напрямку, але з різними "вгору": + +![]({{site.baseurl}}/assets/images/tuto-6-mouse-keyboard/CameraUp.png) + +В нашому випадку, єдина константа це вектор, що йде праворуч від камери і він завжди горизонтальний. Ви можете перевірити це поставивши Вашу руку горизонтально і подивитись вгору/вниз/будь-куди. Давайте дамо визначення вектору "вправо" - його Y координата дорівнює нулю, так як він горизонтальний, і його X та Z координати такі як і у фігури вище, але повернуті на 90° або Pi/2 радіан. + + +``` cpp +// вектор "вправо" +glm::vec3 right = glm::vec3( + sin(horizontalAngle - 3.14f/2.0f), + 0, + cos(horizontalAngle - 3.14f/2.0f) +); +``` + +Ми маємо вектор "вправо" і напрямок (тобто вектор "вперед"). Вектор "вгору" перпендикулярний до цих двох. Зручний математичний інструмент - векторний добуток - робить все досить простим. + +``` cpp +// вектор "вгору" перпендикулярний до вектору напрямку та вектора "вправо" +glm::vec3 up = glm::cross( right, direction ); +``` + +Дуже легко запам'ятати, що таке векторний добуток. Просто згадайте правило правої руки з Туторіалу 3. Перший вектор - це великий палець, другий вектор - вказівний і результат - середній палець. Дуже зручно. + +## Позиція + +Код достатньо прямолінійний. Зауважте, що я використовую стрілочки вгору/вниз/праворуч/ліворуч, не awsd, тому що у мне azerty клавіатура (німецька) і awsd у мене zqsd. Також, це буде по своєму на qwerZ клавіатурах, давайте залишимо корейську клавіатуру в стороні - я не знаю, як у них розташовані клавіші, але думаю там все по своєму. + +``` cpp +// рухаємося вперед +if (glfwGetKey( GLFW_KEY_UP ) == GLFW_PRESS){ + position += direction * deltaTime * speed; +} +// рухаємося назад +if (glfwGetKey( GLFW_KEY_DOWN ) == GLFW_PRESS){ + position -= direction * deltaTime * speed; +} +// праворуч +if (glfwGetKey( GLFW_KEY_RIGHT ) == GLFW_PRESS){ + position += right * deltaTime * speed; +} +// ліворуч +if (glfwGetKey( GLFW_KEY_LEFT ) == GLFW_PRESS){ + position -= right * deltaTime * speed; +} +``` + +Тут є тільки одна особливість `deltaTime`. Ви не захочеться переміщуватись на "одну одиницю" кожний кадр по простій причині: + +* Якщо у Вас швидкий комп'ютер і у Вас 60 кадрів на секунду, Ви будете переміщуватись `60*speed` одиниць за секунду +* Якщо у Вас повільний комп'ютер і у Вас 20 кадрів на секунду, Ви будете переміщуватись `20*speed` одиниць за секунду + +Ті хто мають швидший комп'ютер не завжди хочуть рухатись швидше, потрібно масштабувати відстань на основі "час відносно останнього кадра", або "дельта часу", що є значно краще. дельта часу (`deltaTime`) дуже легко порахувати: + +``` cpp +double currentTime = glfwGetTime(); +float deltaTime = float(currentTime - lastTime); +``` + +## Поле зору + +Заради розваги, ми можемо прив'язати колесо миші до поля зору - у нас буде "дешеве збільшення/зменшення": + +``` cpp +float FoV = initialFoV - 5 * glfwGetMouseWheel(); +``` + +## Розрахунки матриць + +Обчислення матриць дуже просте. Ми використовуємо ті самі функції, що і раніше, але з новими параметрами. + +``` cpp +// Матриця проекції - поле зору 45°, відношення сторін - 4:3, область відображення 0.1 одиниці <-> 100 одиниць +ProjectionMatrix = glm::perspective(glm::radians(FoV), 4.0f / 3.0f, 0.1f, 100.0f); +// Матриця камери +ViewMatrix = glm::lookAt( + position, // Позиція камери + position+direction, // і дивиться вона сюди - позиція камери плюс вектор напрямку + up // "голова зверху" (використовуйте значення 0,-1,0 що б дивитись догори дриґом) +); +``` + +# Результат + +![]({{site.baseurl}}/assets/images/tuto-6-mouse-keyboard/moveanim.gif) + + +## Відсікання задньої поверхні (Backface Culling) + +Тепер ми можемо вільно переміщуватись навколо і якщо ми заходимо всередину куба, полігони все ще відображаються. Це здається очевидним, але це можливість до оптимізацій. В реальному додатку ми ніколи не будемо _в середині_ куба. + +Ідея оптимізації в тому, що б дозволити відеокарті перевіряти, камера перед чи за трикутником. Якщо камера перед трикутником - відображаємо його, якщо камера за трикутником *і* меш "закритий" *i* ми не в середині меша, *тоді* повинен бути інший трикутник перед ним і ніхто не побачить нічого, проте все буде швидше - в двічі менше трикутників для малювання! + +І найкраще те, що це дуже легко перевірити. Відеокарта розраховує нормаль трикутника (пам'ятаєте векторний добуток?) і перевіряє, чи орієнтована ця нормаль в напрямку камери. + +Це звичайно має певну ціну - орієнтація трикутника неявна. Це значить, що якщо Ви обміняєте дві вершини місцями в буфері, то це все закінчиться діркою. Але це вартує невеликої додаткової роботи. Зазвичай Ви просто можете клікнути мишкою по "інвертувати нормалі" в редакторі 3д моделі (що інвертує вершини (обміняє місцями) і як наслідок, нормалі) і все буде виглядати добре. + +Ввімкнення відсікання - це дуже просто: + +``` cpp +// Відсікання трикутників, нормалі яких не направлені на камеру +glEnable(GL_CULL_FACE); +``` + +# Вправи + +* Обмежте `verticalAngle`, так що б було неможливо рухатись вгору-вниз +* Створіть камеру, яка буде рухатись навколо об'єкта (`position = ObjectCenter + ( radius * cos(time), height, radius * sin(time) )`) прив'яжіть радіус/висоту/час до клавіатури/миші чи чого забажаєте. +* Розважайтесь! diff --git a/uk/beginners-tutorials/tutorial-7-model-loading/index.markdown b/uk/beginners-tutorials/tutorial-7-model-loading/index.markdown new file mode 100644 index 000000000..27f06df26 --- /dev/null +++ b/uk/beginners-tutorials/tutorial-7-model-loading/index.markdown @@ -0,0 +1,268 @@ +--- +layout: tutorial +status: publish +published: true +title: 'Туторіал 7: Завантаження моделей' +date: '2011-05-08 17:48:12 +0200' +date_gmt: '2011-05-08 17:48:12 +0200' +categories: [tuto] +order: 70 +tags: [] +language: uk +--- + +* TOC +{:toc} + +До цього часу ми прописували координати куба безпосередньо в в коді. Я думаю, Ви погодитесь, що це трішки громіздко та незручно. + +В цьому туторіалі ми навчимось завантажувати 3д моделі (меши) прямо з файлу. Ми це зробимо аналогічно текстурам - спочатку напишемо маленький, обмежений завантажувач, а потім я покажу Вам деякі бібліотеки, які можуть це робити краще нас. + +Що б цей туторіал був якомога простіше, ми будемо використовувати файл формату OBJ, який достатньо простий та широковживаний. І, що б все було дійсно просто, ми будемо мати справу тільки з OBJ файлами, що містять одну UV координату та одну нормаль на вершину (ми ще не говорили про те, що таке нормаль). + +# Завантаження OBJ + +Наша функція розташована в common/objloader.cpp і об'явлена в common/objloader.hpp з наступною сигнатурою: + +``` cpp +bool loadOBJ( + const char * path, + std::vector < glm::vec3 > & out_vertices, + std::vector < glm::vec2 > & out_uvs, + std::vector < glm::vec3 > & out_normals +) +``` +Ми хочемо, що б `loadOBJ` читала файл `path`, записувала дані в `out_vertices`/`out_uvs`/`out_normals` і повертає `false`, якщо щось пішло не так. `std::vector` це тип, який в С++ використовується для роботи з масивами змінної довжини. Він не має жодного відношення до математичних векторів. Але працює як масив. І накінець, символ `&` означає, що функція буде мати змогу модифікувати вміст масиву. + +## Приклад OBJ файлу + +Зазвичай OBJ файл виглядає десь так: + +``` +# Blender3D v249 OBJ File: untitled.blend +# www.blender3d.org +mtllib cube.mtl +v 1.000000 -1.000000 -1.000000 +v 1.000000 -1.000000 1.000000 +v -1.000000 -1.000000 1.000000 +v -1.000000 -1.000000 -1.000000 +v 1.000000 1.000000 -1.000000 +v 0.999999 1.000000 1.000001 +v -1.000000 1.000000 1.000000 +v -1.000000 1.000000 -1.000000 +vt 0.748573 0.750412 +vt 0.749279 0.501284 +vt 0.999110 0.501077 +vt 0.999455 0.750380 +vt 0.250471 0.500702 +vt 0.249682 0.749677 +vt 0.001085 0.750380 +vt 0.001517 0.499994 +vt 0.499422 0.500239 +vt 0.500149 0.750166 +vt 0.748355 0.998230 +vt 0.500193 0.998728 +vt 0.498993 0.250415 +vt 0.748953 0.250920 +vn 0.000000 0.000000 -1.000000 +vn -1.000000 -0.000000 -0.000000 +vn -0.000000 -0.000000 1.000000 +vn -0.000001 0.000000 1.000000 +vn 1.000000 -0.000000 0.000000 +vn 1.000000 0.000000 0.000001 +vn 0.000000 1.000000 -0.000000 +vn -0.000000 -1.000000 0.000000 +usemtl Material_ray.png +s off +f 5/1/1 1/2/1 4/3/1 +f 5/1/1 4/3/1 8/4/1 +f 3/5/2 7/6/2 8/7/2 +f 3/5/2 8/7/2 4/8/2 +f 2/9/3 6/10/3 3/5/3 +f 6/10/4 7/6/4 3/5/4 +f 1/2/5 5/1/5 2/9/5 +f 5/1/6 6/10/6 2/9/6 +f 5/1/7 8/11/7 6/10/7 +f 8/11/7 7/12/7 6/10/7 +f 1/2/8 2/9/8 3/13/8 +f 1/2/8 3/13/8 4/14/8 +``` + +Отже: + +* `#` - коментар, як // в C++ +* `usemtl` та `mtllib` описує вигляд моделі. Ми не будемо використовувати їх в цьому туторіалі. +* `v` - вершина +* `vt` - текстурна координата вершини +* `vn` - нормаль вершини +* `f` - грань (face) + + + +`v`, `vt` та `vn` легко зрозуміти. `f` є трішки складнішим. Отже, такий запис `f 8/11/7 7/12/7 6/10/7` означає: + +* 8/11/7 описує першу вершину трикутника +* 7/12/7 описує другу вершину трикутника +* 6/10/7 описує третю вершину трикутника (ох) +* Для першої вершини, 8 - це яку саме вершину використовувати, в нашому випадку це -1.000000 1.000000 -1.000000 (увага - індекси починаються з 1, а не з нуля, як в С++) +* 11 - це яку текстурну координату використовувати. В нашому випадку 0.748355 0.998230 +* 7 - це яку нормаль використовувати, тобто 0.000000 1.000000 -0.000000 + +Ці числа називають індексами. Це зручно, тому що якщо декілька вершин мають одну й ту ж позицію, то потрібно зберегти її лише один раз в записі `v` і можна використати декілька разів. Це дозволяє економити пам'ять. + +Погана новина в тому, що OpenGL не може використовувати один індекс для позиції, один для текстури і один для нормалі. Тому зараз я вирішив використовувати неіндексований меш, а індекси відкласти на потім. В туторіалі 9 ми поговоримо про них. + + +## Створення OBJ файлу в Blender + +Так як наш іграшковий завантажувач OBJ файлів дещо обмежений, то потрібно дотримуватись певних правил при створенні такого файлу. Ось як його потрібно експортувати з Blender: + +![]({{site.baseurl}}/assets/images/tuto-7-model-loading/Blender.png) + + +## Читання файлу + +Тепер давайте подивимось на реальний код. Нам потрібні деякі тимчасові змінні, в яких ми будемо зберігати вміст obj файлу: + +``` cpp +std::vector< unsigned int > vertexIndices, uvIndices, normalIndices; +std::vector< glm::vec3 > temp_vertices; +std::vector< glm::vec2 > temp_uvs; +std::vector< glm::vec3 > temp_normals; +``` +З туторіалу 5 "текстурований куб" Ви знаєте, як відкрити файл: + +``` cpp +FILE * file = fopen(path, "r"); +if( file == NULL ){ + printf("Неможливо відкрити файл!\n"); + return false; +} +``` + +Давайте будемо читати файл до кінця : + +``` cpp +while( 1 ){ + + char lineHeader[128]; + // читаємо перше слово першого рядка + int res = fscanf(file, "%s", lineHeader); + if (res == EOF) + break; // EOF = End Of File - кінець файлу. Виходимо з циклу + + // else : розбираємо lineHeader +``` + +(майте на увазі, що ми припустили, що перше слово першого рядку менше 128 символів, що є не дуже гарним припущенням. Але для нашого парсера підійде) + +Давайте спочатку попрацюємо з вершинами: + +``` cpp +if ( strcmp( lineHeader, "v" ) == 0 ){ + glm::vec3 vertex; + fscanf(file, "%f %f %f\n", &vertex.x, &vertex.y, &vertex.z ); + temp_vertices.push_back(vertex); +``` + +Тобто, якщо перше слово в рядку є `v`, то далі буде 3 дійсних числа. Ми створимо `glm::vec3` з них і додамо в вектор (масив) вершин. + +``` cpp +}else if ( strcmp( lineHeader, "vt" ) == 0 ){ + glm::vec2 uv; + fscanf(file, "%f %f\n", &uv.x, &uv.y ); + temp_uvs.push_back(uv); +``` +якщо ж перше слово це `vt`, то далі буде 2 дійсних числа. З них створимо `glm::vec2` і додамо до вектора текстурних координат. + +і дуже схоже для нормаль: + +``` cpp +}else if ( strcmp( lineHeader, "vn" ) == 0 ){ + glm::vec3 normal; + fscanf(file, "%f %f %f\n", &normal.x, &normal.y, &normal.z ); + temp_normals.push_back(normal); +``` + +А тепер трішки складніше - грані + +``` cpp +}else if ( strcmp( lineHeader, "f" ) == 0 ){ + std::string vertex1, vertex2, vertex3; + unsigned int vertexIndex[3], uvIndex[3], normalIndex[3]; + int matches = fscanf(file, "%d/%d/%d %d/%d/%d %d/%d/%d\n", &vertexIndex[0], &uvIndex[0], &normalIndex[0], &vertexIndex[1], &uvIndex[1], &normalIndex[1], &vertexIndex[2], &uvIndex[2], &normalIndex[2] ); + if (matches != 9){ + printf("Наш простий файл не може прочитати цей файл : ( Спробуйте зберегти з іншими опціями\n"); + return false; + } + vertexIndices.push_back(vertexIndex[0]); + vertexIndices.push_back(vertexIndex[1]); + vertexIndices.push_back(vertexIndex[2]); + uvIndices .push_back(uvIndex[0]); + uvIndices .push_back(uvIndex[1]); + uvIndices .push_back(uvIndex[2]); + normalIndices.push_back(normalIndex[0]); + normalIndices.push_back(normalIndex[1]); + normalIndices.push_back(normalIndex[2]); +``` + +Цей код подібний до попереднього, тільки потрібно трішки більше читати. + +## Обробка даних + +Все що ми робимо, це змінюємо "форму" даних. У нас був рядок символів, тепер декілька `std::vector`. Але цього не достатньо, нам потрібно перетворити дані в форму, яка підходить OpenGL. Ми видаляємо індекси і робимо масив `glm::vec3`. Ця операція називається індексуванням. + +Пройдемося по кожній вершині (кожному v/vt/vn) кожного трикутника (кожний рядочок з f): + +``` cpp + // Для кожної вершини кожного трикутника + for( unsigned int i=0; i vertices; +std::vector< glm::vec2 > uvs; +std::vector< glm::vec3 > normals; // Поки не використовуємо +bool res = loadOBJ("cube.obj", vertices, uvs, normals); +``` + +І передамо ці вектори до OpenGL замість масивів: + +``` cpp +glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(glm::vec3), &vertices[0], GL_STATIC_DRAW); +``` + +І це все! + +# Результати + +Вибачте за цю дивну текстуру - я не дуже гарних художник :( Будь-який внесок вітається! + +![]({{site.baseurl}}/assets/images/tuto-7-model-loading/ModelLoading.png) + + +# Інші формати/завантажувачі + +Цей маленький завантажувач повинен дати достатньо для старту, але не варто використовувати його в реальному житті. Подивіться на сторінку [Корисні посилання та інструменти]({{site.baseurl}}/uk/miscellaneous/useful-tools-links/) і пошукайте щось цікаве. Проте, краще почекайте до туторіалу 9, де ми спробуємо їх використати. diff --git a/uk/beginners-tutorials/tutorial-8-basic-shading/index.markdown b/uk/beginners-tutorials/tutorial-8-basic-shading/index.markdown new file mode 100644 index 000000000..334492d46 --- /dev/null +++ b/uk/beginners-tutorials/tutorial-8-basic-shading/index.markdown @@ -0,0 +1,307 @@ +--- +layout: tutorial +status: publish +published: true +title: 'Туторіал 8 : Базове затінення' +date: '2011-05-08 19:12:46 +0200' +date_gmt: '2011-05-08 19:12:46 +0200' +categories: [tuto] +order: 80 +tags: [] +language: uk +--- + +* TOC +{:toc} + +В цьому 8 туторіалі ми вивчимо як зробити деяке базове (найпростіше) затінення. Це включає: + +* Робити об'єкти трішки яскравішими, коли вони ближче до джерела світла +* Наявність відблисків під час відбиття світлу (specular lighting - дзеркальне освітлення) +* Робити об'єкти темнішими, коли світло не направлено безпосередньо на модель (дифузне освітлення) +* Багато обманювати (навколишнє освітлення) + +А наступного не включає: + +* Тіні. Це велика тема, що заслуговує окремого туторіалу. +* Дзеркально-подібне освітлення (включаючи воду) +* Будь-яке складне освітлення, таке як "підповерхневе освітлення" (наприклад, віск) +* Анізотропний матеріал (наприклад, фарбований метал) +* Затінення, що базується на фізиці та намагається мімікрувати під реальність +* Ambient Occlusion (темнота в печері) +* Розтікання кольору (червоні шкарпетки роблять білу підлогу трішки червоною) +* Прозорість +* Будь-яке глобальне освітлення (це включає все попереднє) + +Одним слово - базове. + +# Нормалі + +Декілька останніх туторіалів ми говорили про нормалі але не повністю розуміли, що це таке. + +## Нормалі трикутника + +Нормаль до площини - це вектор довжини 1, який перпендикулярний до цієї площини. + +Нормаль до трикутника - це вектор довжини 1, який перпендикулярний до цього трикутника. Його легко порахувати, якщо взяти векторний добуток двох ребер цього трикутника (векторний добуток a та b дає вектор, яки перпендикулярний до a та b, пам'ятаєте?) і нормалізувати - це зробить його довжину рівною 1. В псевдокоді це буде так: + +``` +triangle ( v1, v2, v3 ) +edge1 = v2-v1 +edge2 = v3-v1 +triangle.normal = cross(edge1, edge2).normalize() +``` + +Не плутайте нормаль (normal) і функцію `normalize()`. `Normalize()` ділить вектор (не обов'язково нормаль) на його довжину, таким чином довжина вектора буде 1. Нормаль - це про назва вектора, яки представляє собою нормаль. + +## Нормалі вершин + +Ми будемо називати нормаллю вершини комбінацію нормалей трикутників навколо неї. Це зручно, тому що в вершинному шейдері ми маємо справу з вершинами, а не з трикутниками, отже краще мати інформацію про вершини. І в будь-якому випадку, ми не маємо змоги мати інформацію про трикутники в OpenGL. В псевдокоді: + +``` +vertex v1, v2, v3, .... +triangle tr1, tr2, tr3 // Всі трикутники, що мають вершиною v1 +v1.normal = normalize( tr1.normal + tr2.normal + tr3.normal ) +``` + +## Використання нормалей вершин в OpenGL + +Використання нормалей в OpenGL дуже просте. Нормаль це просто атрибут вершини, так само як позиція, колір, UV координати. Тому це просто звичайна робота. Наша функція loadOBJ з туторіалу 7 вже читає їх з OBJ файлу. + +``` cpp +GLuint normalbuffer; + glGenBuffers(1, &normalbuffer); + glBindBuffer(GL_ARRAY_BUFFER, normalbuffer); + glBufferData(GL_ARRAY_BUFFER, normals.size() * sizeof(glm::vec3), &normals[0], GL_STATIC_DRAW); +``` + +та + +``` cpp + // 3 буфер атрибутів - нормалі + glEnableVertexAttribArray(2); + glBindBuffer(GL_ARRAY_BUFFER, normalbuffer); + glVertexAttribPointer( + 2, // атрибут + 3, // розмір + GL_FLOAT, // тип + GL_FALSE, // нормалізувати? + 0, // stride + (void*)0 // зміщення в буфері (де початок даних) + ); +``` + +І цього достатньо для початку. + +# Дифузна частина + +## Важливість нормалей поверхні + +Коли світло падає на об'єкт, велика частина відбивається у всіх напрямках. Це називається "дифузний компонент" (Дуже скоро ми побачимо що трапляється з іншою частиною). + +![]({{site.baseurl}}/assets/images/tuto-8-basic-shading/diffuseWhite1.png) + +Коли певний потік світла попадає на поверхню, вона світиться по різному в залежності від кута, під яким світло попало на неї. + +Якщо світло перпендикулярне до поверхні, воно концентрується на невеликій частині поверхні. Якщо світло попадає під певним кутом до поверхні, то світло поширюється на більшу площу: + +![]({{site.baseurl}}/assets/images/tuto-8-basic-shading/diffuseAngle.png) + +Це означає, що кожна точка поверхні буде виглядати темнішою якщо світло не перпендикулярне (але, пам'ятайте, що більша площа буде освітлена, тому загальна кількість світла буде така ж). + +Це значить, що коли ми розраховуємо колір пікселя, кут між світлом, що приходить і нормаллю поверхні має значення. Отже маємо: + +``` glsl +// косинус кута між нормаллю і напрямком світла, зафіксований вище 0 +// (примітка перекладача - в наступних двох рядках була помилка, говорили про трикутник, а не нормаль) +// - світло паралельно нормалі -> 1 +// - світло перпендикулярно нормалі -> 0 +float cosTheta = dot( n,l ); + +color = LightColor * cosTheta; +``` +{: .highlightglslfs } + +В цьому коді `n` це нормаль до поверхні і `l`- одиничний вектор який направлений від поверхні до джерела світла (і це не помилка, хоча це і контрінтуітивно. Просто це робить математику простіше). + +## Звертайте увагу на знак + +Дещо відсутнє в нашій формулі cosTheta. Якщо світло знаходиться за трикутником, `n` і `l` протилежні, тобто `n.l < 0`. Це означатиме, що `colour = якесь число, менше нуля`, що не має сенсу. Отже, нам потрібно використати функцію clamp, якою ми прирівняємо всі значення менше нуля до нуля: + +``` glsl +// Косинус кута між нормаллю і напрямком світла та обмежене знизу значення нуль. +// - світло паралельно нормалі -> 1 +// - світло перпендикулярно до нормалі -> 0 +// - світло за трикутником -> 0 +float cosTheta = clamp( dot( n,l ), 0,1 ); + +color = LightColor * cosTheta; +``` +{: .highlightglslfs } + +## Колір матеріалу + +Звичайно, вихідний колір також залежить від кольору матеріалу. В цьому зображенні білий колір складається з зеленого, червоного та синього кольору. Коли він зіштовхується з червоним матеріалом, зелений і синій колір поглинається і тільки червоний залишається. + +![]({{site.baseurl}}/assets/images/tuto-8-basic-shading/diffuseRed.png) + +Ми можемо змоделювати це звичайним множенням: + +``` glsl +color = MaterialDiffuseColor * LightColor * cosTheta; +``` +{: .highlightglslfs } + +## Моделювання світла + +Спочатку ми припустили, що у нас є точкове освітлення, що випромінює в всіх напрямках, щось схоже на свічку. + +З таким освітленням світловий потік, який наша поверхня буде отримувати залежить від відстані до джерела світла - чим далі, тим менше світла. Насправді, кількість світла буде зменшуватись пропорційно квадрату відстані: + +``` glsl +color = MaterialDiffuseColor * LightColor * cosTheta / (distance*distance); +``` +{: .highlightglslfs } + +І нарешті нам потрібен ще один параметр для контролю потужності світла. Він буде закодований в LightColor (і буде в наступному туторіалі), але нехай поки буде просто колір (тобто білий) і потужність (тобто 60 ватт). + +``` glsl +color = MaterialDiffuseColor * LightColor * LightPower * cosTheta / (distance*distance); +``` +{: .highlightglslfs } + +## Складаємо все в одну купу + +Що б цей код працював, нам потрібна жменька параметрів (різноманітні кольори і потужність) і трішки коду. + +`MaterialDiffuseColor` отримується з текстури. + +`LightColor` та `LightPower` отримується через uniform змінні в GLSL. + +`cosTheta` залежить від `n` та `l`. Ми можемо виразити їх в будь-якому просторі, головне в одному й тому же. Ми обрали простір камери, тому що це в ньому простіше розраховувати положення світла: + +``` glsl +// Нормаль до розрахованого фрагменту, в просторі камери + vec3 n = normalize( Normal_cameraspace ); + // Напрям світла (від фрагмента до світла) + vec3 l = normalize( LightDirection_cameraspace ); +``` +{: .highlightglslfs } + +з `Normal_cameraspace` та `LightDirection_cameraspace` розрахованих в вершинному шейдері і переданих в фрагментний шейдер : + +``` glsl +// Вихідна позиція в вершинному шейдері, в просторі обрізання : MVP * position +gl_Position = MVP * vec4(vertexPosition_modelspace,1); + +// Позиція вершини в світовому просторі : M * position +Position_worldspace = (M * vec4(vertexPosition_modelspace,1)).xyz; + +// Вектор, який направлений з вершини до камери, в просторі камери. +// В просторі камери, камера знаходиться в початку координат (0,0,0). +vec3 vertexPosition_cameraspace = ( V * M * vec4(vertexPosition_modelspace,1)).xyz; +EyeDirection_cameraspace = vec3(0,0,0) - vertexPosition_cameraspace; + +// Вектор, який направлений з вершини до світла, в просторі камери. M не враховується, тому що це одиничний вектор. +vec3 LightPosition_cameraspace = ( V * vec4(LightPosition_worldspace,1)).xyz; +LightDirection_cameraspace = LightPosition_cameraspace + EyeDirection_cameraspace; + +// Нормаль до вершини, в просторі камери +Normal_cameraspace = ( V * M * vec4(vertexNormal_modelspace,0)).xyz; // Коректно тільки якщо `ModelMatrix` не маштабовано на моделі. Використовуйте зворотнє перенесення, якщо це не так. +``` +{: .highlightglslvs } + +Цей код може виглядати неймовірно, але тут нема нічого такого, чого би ми не бачили в туторіалі 3 - матриці. Я приділив багато уваги тому, що підписати прості для кожного вектора, що б було легше слідкувати за тим, що відбувається **Рекомендую Вам теж так робити.** + +`M` та `V` це матриці моделі та виду, що передаються в шейдери аналогічно MVP. + +## Час попрацювати + +Ви маєте все, що потрібно для кодування дифузійного освітлення. Продовжуйте вчитись :) + +## Результат + +Використовуючи лише дифузійну складову, ми маємо наступний результат (вибачте за ці текстури): + +![]({{site.baseurl}}/assets/images/tuto-8-basic-shading/diffuse_only.png) + +Це краще, ніж було до цього, але все ще багато що відсутнє. Наприклад, спина Сюзанни повністю чорна, так як ми використовуємо `clamp()`. + +# Навколишнє освітлення (Ambient) + +Навколишнє освітлення в графіці це велике шахрайство:) + +Ми очікуємо, що спина Сюзани буде отримувати більше світла в реальному житті, тому що лампа буде освітлювати стіну за нею, яка трішки освітить задню частину об'єкта. + +Це потребує дуже багато обчислень. + +Тому зазвичай просто додають трішки несправжнього освітлення. Тобто, ми робимо так, що 3д модель випромінює трішки світла, що б вона не виглядало повністю чорною. + +Це може бути реалізовано наступним чином: + +``` glsl +vec3 MaterialAmbientColor = vec3(0.1,0.1,0.1) * MaterialDiffuseColor; +``` +{: .highlightglslfs } + +``` glsl +color = + // навколишнє освітлення - симулюємо непряме освітлення + MaterialAmbientColor + + // дифузія : "колір" об'єкту + MaterialDiffuseColor * LightColor * LightPower * cosTheta / (distance*distance) ; +``` +{: .highlightglslfs } + +Давайте подивимось на результат. + +## Результати + +Тепер воно трішки краще. Ви можете відрегулювати (0.1, 0.1, 0.1) для кращого результату. + +![]({{site.baseurl}}/assets/images/tuto-8-basic-shading/diffuse_ambiant.png) + + +# Дзеркальне відбиття + +Інша частина відбитого світла відбивається в основному в напряму "відбиття" від поверхні. Це називається дзеркальним компонентом. + +![]({{site.baseurl}}/assets/images/tuto-8-basic-shading/specular.png) + +Як видно з зображення, воно має форму "пелюстки" (???). В крайніх випадках, дифузна компонента може бути відсутню, пелюстка відбиття може бути дуже-дуже вузькою (все світло буде відбиватись в одному напрямку) і Ви отримаєте дзеркало. +(*Ми можемо маніпулювати параметрами для отримання дзеркала, але в нашому випадку, але єдине, що ми побачимо зараз - це наша лампа. Це буде дивне дзеркало*) + +``` glsl +// Вектор очей (в напрямку камери) +vec3 E = normalize(EyeDirection_cameraspace); +// Напрямок, в якому трикутник відбиває світло +vec3 R = reflect(-l,n); +// косинус кута між вектором очей і вектором відбиття +// обмежений 0 знизу +// - погляд в відображення -> 1 +// - погляд в сторону -> < 1 +float cosAlpha = clamp( dot( E,R ), 0,1 ); + +color = + // Навколишнє освітлення + MaterialAmbientColor + + // Дифузний колір об'єкту + MaterialDiffuseColor * LightColor * LightPower * cosTheta / (distance*distance) ; + // дзеркальне відображення, як дзеркало + MaterialSpecularColor * LightColor * LightPower * pow(cosAlpha,5) / (distance*distance); +``` +{: .highlightglslfs } + +`R` це напрямок в якому відбивається світло. `E` - інвертований напрямок очей (погляду) (так само, як ми зробили для `l`). Якщо цей кут невеликий, це значить, що ми дивимось прямо в відображення. + +`pow(cosAlpha,5)` використовується для контролю ширини "пелюстки" відображення. Збільшуйте 5 для вужчої області (пелюстки). + +## Фінальний результат + +![]({{site.baseurl}}/assets/images/tuto-8-basic-shading/diffuse_ambiant_specular.png) + +Зверніть увагу на дзеркальні відблиски на носі та бровах. + +Ця модель затінення використовувалась роками тому що вона достатньо проста. Вона має безліч недоліків, тому вона була замінена на моделях, що базуються на фізиці, наприклад "microfacet BRDF", але ми повернемося до цього пізніше. + +В наступному туторіалі ми вивчимо як покращити продуктивність Ваших VBO. Це буде перший туторіал середнього рівня! diff --git a/uk/download/index.markdown b/uk/download/index.markdown new file mode 100644 index 000000000..8c95f32c7 --- /dev/null +++ b/uk/download/index.markdown @@ -0,0 +1,74 @@ +--- +layout: page +type: section +status: publish +published: true +title: Завантаження +author: + display_name: Calvin1602 + email: arnaud1602@gmail.com + url: '' +date: '2011-05-11 17:38:54 +0200' +date_gmt: '2011-05-11 17:38:54 +0200' +categories: [] +tags: [] +language: uk +--- + +[Туторіал 1]({{site.baseurl}}/beginners-tutorials/tutorial-1-opening-a-window/) пояснює, як зібрати цей код. Будь-ласка, прочитайте його! + +Ви можете завантажити ZIP архів, який містить повний набір туторіалів тут: + +* [Джерельний код туторіалів на GitHub, .zip](https://github.com/opengl-tutorials/ogl/archive/master.zip) + +Для старих комп'ютерів, спробуйте версію 2.1 (не часто оновлюється) : + +* [Джерельний код туторіалів на GitHub, Opengl 2.1, .zip](https://github.com/opengl-tutorials/ogl/archive/2.1_branch.zip) + +Як варіант, можете спробувати завантажити Git репозиторій: + +* [Завантажте джерельний код на GitHub](https://github.com/opengl-tutorials/ogl) + +Якщо у Вас виникли проблеми з побудовою чи запуском цього коду, будь-ласка, прочитайте [FAQ]({{ site.baseurl }}/uk/miscellaneous/faq/). + +Весь джерельний код на цьому сайті (виключаючи зовнішні бібліотеки) розповсюджується під ліцензією WTFPL Public Licence: + +``` + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + +Copyright (C) 2004 Sam Hocevar + +Everyone is permitted to copy and distribute verbatim or modified +copies of this license document, and changing it is allowed as long +as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. +``` + +переклад ліцензії: + +``` + + Публічна ліцензія «Робіть до біса все що завгодно» + Версія 2, Грудень 2004 + +Copyright (C) 2004 Sam Hocevar + +Будь-хто має право копіювати та розповсюджувати цю копію дослівно +чи модифіковану версію і зміни дозволені якщо ім'я змінено. + + + Публічна ліцензія «Робіть до біса все що завгодно» + ПОЛОЖЕННЯ ТА УМОВИ КОПІЮВАННЯ, РОЗПОВСЮДЖЕННЯ ТА МОДИФІКАЦІЇ + + 0. Робіть що вам заманеться. +``` + +(від перекладача - весь переклад має десь таку саму ліцензію - робіть з ним що хочете, поки не забуваєте про тих, хто його створив) + +Всі матеріали туторіалів доступні під ліцензією Creative-Common license: CC-BY-NC-ND з якою можна ознайомитись [тут](http://creativecommons.org/licenses/by-nc-nd/3.0/fr/deed.en) \ No newline at end of file diff --git a/uk/index.markdown b/uk/index.markdown new file mode 100644 index 000000000..cbf85701c --- /dev/null +++ b/uk/index.markdown @@ -0,0 +1,44 @@ +--- +layout: page +status: publish +published: true +title: Домашня сторінка +date: '2011-05-07 11:00:04 +0200' +date_gmt: '2011-05-07 11:00:04 +0200' +categories: [] +tags: [] +language: uk +--- +Цей сайт присвячений **туторіалу** по **OpenGL 3.3** та більш пізніших! + +Вихідні коди доступні за [посиланням]({{ site.baseurl }}/uk/download). + +Якщо у Вас виникнуть будь-які питання, коментарі, баги чи ще щось, пишіть нам contact@opengl-tutorial.org, та не забувайте спочатку заглянути до [FAQ]({{ site.baseurl }}/miscellaneous/faq/)! + + +Ми раді доповненням до нашого проекту, особливо [перекладам]({{site.baseurl}}/uk/miscellaneous/contribute/translation/) ! + +Якщо Вам подобається наша робота, не бійтесь її розповсюджувати світом! + +Слідуйте за нами! + + + + + +
+ +

News

+ +
    + {% for post in site.posts %} +
  • + + {{ post.title }} +
  • + {% endfor %} +
+ +

підписатись via RSS

+ +
diff --git a/uk/intermediate-tutorials/billboards-particles/billboards/index.markdown b/uk/intermediate-tutorials/billboards-particles/billboards/index.markdown new file mode 100644 index 000000000..55e1d173c --- /dev/null +++ b/uk/intermediate-tutorials/billboards-particles/billboards/index.markdown @@ -0,0 +1,123 @@ +--- +layout: tutorial +status: publish +published: true +title: Білборд +date: '2013-10-15 17:15:15 +0200' +date_gmt: '2013-10-15 17:15:15 +0200' +categories: [] +order: 584 +tags: [] +language: uk +--- + +* TOC +{:toc} + +Білборд це 2д річ в трьохвимірному світі. Не 2д меню поверх всього, не 3д площина, яку Ви можете повертати - це щось проміжне, як "смужка життя" в більшості ігор. + +Їх відрізняє те, що вони позиціонуються в визначених місцях, але їх орієнтація розраховується так, що б вони були завжди повернуті лицем до камери. + +# Рішення №1 : 2д спосіб + +Це спосіб дуже простий. + +Просто розрахуйте положення на екрані і показуйте 2д текст (дивіться туторіал 11) в цій позиції. + +``` cpp +// Все це вже було пояснено в 3 туторіалі. Нічого нового. +glm::vec4 BillboardPos_worldspace(x,y,z, 1.0f); +glm::vec4 BillboardPos_screenspace = ProjectionMatrix * ViewMatrix * BillboardPos_worldspace; +BillboardPos_screenspace /= BillboardPos_screenspace.w; + +if (BillboardPos_screenspace.z < 0.0f){ + // Об'єкт за камерою, не показуємо. +} +``` + +Та-да! + +З позитивного - цей метод дуже простий і білборд буде мати одні й тіж розміри не залежно від відстані до камери. Але 2д текст завжди відображається поверх всього іншого і це може/буде спотворювати процес малювання. + +# Рішення №2 : 3д спосіб + +Цей спосіб зазвичай значно кращий і не сильно складніший. + +Ціль в тому, що б тримати меш вирівняним по відношенню до камери, навіть коли камера переміщується: + +![]({{site.baseurl}}/assets/images/tuto-billboard/2a.gif) + +Вми можете бачити проблему в розрахунку правильної матриці моделі, але це все рівно дуже просто. + +Ідея в тому, що кожний кут білборда знаходиться в центрі, зміщеного векторами камери вгору на вниз: + +![]({{site.baseurl}}/assets/images/tuto-billboard/principle.png) + +Звичайно, ми знаємо тільки позицію центра білборда в світових координатах, тому ми також потребуємо векторів "вгору" та "праворуч" для камери в цих координатах. + +В просторі камери вектор вгору це (0,1,0), Для отримання світових координат, просто домножте на матрицю, що перетворює простір камери в світові координати, що насправді є інвертованою матрицею виду. + +Ось простий спосіб це зробити: + +``` +CameraRight_worldspace = {ViewMatrix[0][0], ViewMatrix[1][0], ViewMatrix[2][0]} +CameraUp_worldspace = {ViewMatrix[0][1], ViewMatrix[1][1], ViewMatrix[2][1]} +``` + +Як тільки у нас це є, дуже легко розрахувати кінцеву позицію вершини: + +``` glsl +vec3 vertexPosition_worldspace = + particleCenter_wordspace + + CameraRight_worldspace * squareVertices.x * BillboardSize.x + + CameraUp_worldspace * squareVertices.y * BillboardSize.y; +``` +{: .highlightglslvs } + +* `particleCenter_worldspace` як видно з імені, позиція центру білборду. Вона вказана в uniform vec3. +* `squareVertices` це оригінальний меш. `squareVertices.x` is -0.5 для лівих вершин, які в результаті переміщуються вліво відносно камери (через `*CameraRight_worldspace`) +* `BillboardSize` - це розмір в світових координатах білборда, отримується через іншу uniform змінну. + +
І ось результат. Це ж дійсно просто ?
+![]({{site.baseurl}}/assets/images/tuto-billboard/2.gif) + +Для запису, ось як `squareVertices` отримано: + +``` cpp +// VBO містить 4 вершини частинок. + static const GLfloat g_vertex_buffer_data[] = { + -0.5f, -0.5f, 0.0f, + 0.5f, -0.5f, 0.0f, + -0.5f, 0.5f, 0.0f, + 0.5f, 0.5f, 0.0f, + }; +``` + +# Рішення №3 : Фіксований 3д розмір + +Як Ви можете побачити вище, розмір білборда змінюється відповідно до відстані від камери. Це очікуваний результат в деяких випадках, але в інших (таких як смужка життя), Ви можете хотіти зафіксувати розмір. + +Так як зміщення між центром і межами білборда повинні бути фіксованими в просторі екрану, ось що ми будемо робити - розрахуємо позицію центра в координатах екрану і змістимо їх. + +``` cpp +vertexPosition_worldspace = particleCenter_wordspace; +// Отримаємо центр екрана для центру частинки +gl_Position = VP * vec4(vertexPosition_worldspace, 1.0f); +// Тут ми повинні самостійно зробити перспективний поділ. +gl_Position /= gl_Position.w; + +// Перемістимо вершину в екранних координатах. Там не потрібні вектори камери. +gl_Position.xy += squareVertices.xy * vec2(0.2, 0.05); +``` + +Пам'ятайте, що на цьому етапі малювання, Ви знаходитись в нормалізованих координатах пристрою, тобто в межах -1..1 по обох осях, це не пікселі! + +Якщо Вам потрібен розмір в пікселях, це просто - використовуйте `(ScreenSizeInPixels / BillboardSizeInPixels)`, а не `BillboardSizeInScreenPercentage`. + +![]({{site.baseurl}}/assets/images/tuto-billboard/3.gif) + +# Рішення №4 : Тільки вертикальний поворот + +Деякі системи моделюють далекі дерева і лампи як білборди. Але Ви точно не хочете, що б Ваші дерева були скрученими - вони повинні бути вертикальними. Отже потрібна гібридна система, яка буде повертати тільки навколо однієї вісі. + +Але це вже вправа для читача! diff --git a/uk/intermediate-tutorials/billboards-particles/index.markdown b/uk/intermediate-tutorials/billboards-particles/index.markdown new file mode 100644 index 000000000..775b0c2c1 --- /dev/null +++ b/uk/intermediate-tutorials/billboards-particles/index.markdown @@ -0,0 +1,16 @@ +--- +layout: page +status: publish +published: true +title: 'Туторіал 18 : Білборди & Частинки' +date: '2013-09-07 11:17:18 +0200' +date_gmt: '2013-09-07 11:17:18 +0200' +categories: [tuto] +order: 580 +tags: [] +language: uk +--- +Цей туторіал розділено на дві частини: + +- [Частина 1: Білборди](./billboards) +- [Частина 2: Частинки і інстанціювання об'єктів](./particles-instancing) diff --git a/uk/intermediate-tutorials/billboards-particles/particles-instancing/index.markdown b/uk/intermediate-tutorials/billboards-particles/particles-instancing/index.markdown new file mode 100644 index 000000000..9041f92a4 --- /dev/null +++ b/uk/intermediate-tutorials/billboards-particles/particles-instancing/index.markdown @@ -0,0 +1,412 @@ +--- +layout: tutorial +status: publish +published: true +title: Частинки/інстанціювання +date: '2013-10-19 10:52:04 +0200' +date_gmt: '2013-10-19 10:52:04 +0200' +categories: [] +order: 588 +tags: [] +language: uk +--- + +* TOC +{:toc} + +Частинки дуже схожі на 3д білборди. Але є суттєві відмінності: + +* зазвичай їх дуже багато +* вони рухаються +* вони з'являються і помирають +* вони напівпрозорі + +Всі ці відмінності приносять певні проблеми. Цей туторіал покаже, один з шляхів вирішення їх. Але існує багато інших. + +# Частинки, їх багато! + +Спершу може здатись, що для того, що б намалювати безліч частинок, можна просто використати код з попереднього туторіалу і викликати `glDrawArrays` для кожної частинки. Це дуже погана ідея, тому що це значить, що весь Ваш блискучий GTX з 512+ мікропроцесорів буде повністю зайнятий малюванням quad (квада???) (точніше, буде тільки один процесор працювати, що значить втрату 99% ефективності). Тоді Ви будете малювати наступний білборд і це буде те саме. + +Чесно кажучи, нам потрібен спосіб намалювати всі частинки одночасно. + +Є багато способів зробити це, ось три з них: + +* Згенерувати один VBO, який містить всі ці частинки разом. Просто, ефективно, працює кругом. +* Використовувати геометричний шейдер. Та не в цьому туторіалі, в основному тому, що 50% комп'ютерів не підтримують це. +* Використовувати інстанціювання (instancing, інший переклад - дублювання). Не на всіх комп'ютерах доступно, але доступно на більшості. + +В цьому туторіалі ми розглянемо 3 варіант, тому що це гарний баланс між продуктивністю і доступністю. Також, якщо цей спосіб не спрацює, то дуже легко додати реалізацію першого. + +## Інстанціювання + +"Інстанціювання" означає, що ми маємо один базовий меш (в нашому випадку - простий чотирикутник з двох трикутників), але багато копій (інстансів) його на екрані. + +Технічно, це робиться за допомогою декількох буферів: +* деякі з них описують базовий меш +* деякі описують властивості кожного інстанса (дубліката) базового меша. + +У Вас є багато-багато варіантів того, що можна додати в кожний буфер. В нашому простому випадку, буде наступне: + +* Один буфер для вершин меша. Без індексного буферу, це 6 елементів `vec3`, що утворюють два трикутники, що в свою чергу утворюють чотирикутник. +* Один буфер центрів частинок. +* Один буфер кольорів частинок. + +Це дуже стандартні буфери. Вони створюються наступним чином: + +``` cpp +// Цей VBO містить 4 вершини для однієї частинки +// Завдяки інстанціюванню, вони будуть спільними для всіх частинок. +static const GLfloat g_vertex_buffer_data[] = { + -0.5f, -0.5f, 0.0f, + 0.5f, -0.5f, 0.0f, + -0.5f, 0.5f, 0.0f, + 0.5f, 0.5f, 0.0f, +}; +GLuint billboard_vertex_buffer; +glGenBuffers(1, &billboard_vertex_buffer); +glBindBuffer(GL_ARRAY_BUFFER, billboard_vertex_buffer); +glBufferData(GL_ARRAY_BUFFER, sizeof(g_vertex_buffer_data), g_vertex_buffer_data, GL_STATIC_DRAW); + +// Цей VBO містить позиції і розміри кожної частинки +GLuint particles_position_buffer; +glGenBuffers(1, &particles_position_buffer); +glBindBuffer(GL_ARRAY_BUFFER, particles_position_buffer); +// Починаємо з пустого (NULL) буферу - він буде оновлюватись кожний кадр. +glBufferData(GL_ARRAY_BUFFER, MaxParticles * 4 * sizeof(GLfloat), NULL, GL_STREAM_DRAW); + +// Цей VBO містить кольори кожної частинки +GLuint particles_color_buffer; +glGenBuffers(1, &particles_color_buffer); +glBindBuffer(GL_ARRAY_BUFFER, particles_color_buffer); + +// Починаємо з пустого (NULL) буферу - він буде оновлюватись кожний кадр. +glBufferData(GL_ARRAY_BUFFER, MaxParticles * 4 * sizeof(GLubyte), NULL, GL_STREAM_DRAW); +``` + +Це самий звичайний спосіб створення буферів. Тепер код оновлення на кожний кадр: + +``` cpp +// Оновлення буферів, які OpenGL буде використовувати для малювання. +// Існують досить складні способи для потокової передачі даних з CPU до GPU, +// але це за межами даного туторіалу. +// http://www.opengl.org/wiki/Buffer_Object_Streaming + +glBindBuffer(GL_ARRAY_BUFFER, particles_position_buffer); +glBufferData(GL_ARRAY_BUFFER, MaxParticles * 4 * sizeof(GLfloat), NULL, GL_STREAM_DRAW); // Буфер ні до чого не прив'язаний, типовий спосіб покращити потокову передачу. Деталі нижче +glBufferSubData(GL_ARRAY_BUFFER, 0, ParticlesCount * sizeof(GLfloat) * 4, g_particule_position_size_data); + +glBindBuffer(GL_ARRAY_BUFFER, particles_color_buffer); +glBufferData(GL_ARRAY_BUFFER, MaxParticles * 4 * sizeof(GLubyte), NULL, GL_STREAM_DRAW); // Буфер ні до чого не прив'язаний, типовий спосіб покращити потокову передачу. Деталі нижче +glBufferSubData(GL_ARRAY_BUFFER, 0, ParticlesCount * sizeof(GLubyte) * 4, g_particule_color_data); +``` + +Цей код теж стандартний. Перед малювання ми прив'язуємо наступним чином: + +``` cpp +// перший буфер атрибутів: вершини +glEnableVertexAttribArray(0); +glBindBuffer(GL_ARRAY_BUFFER, billboard_vertex_buffer); +glVertexAttribPointer( + 0, // атрибут. Немає причини, чому тут саме нуль, але повинно бути таке ж як і в шейдері, layout. + 3, // розмір + GL_FLOAT, // тим + GL_FALSE, // нормалізовано? + 0, // stride + (void*)0 // зміщення в буфері +); + +// другий буфер атрибутів: позиція центрів частинок +glEnableVertexAttribArray(1); +glBindBuffer(GL_ARRAY_BUFFER, particles_position_buffer); +glVertexAttribPointer( + 1, // атрибут. Немає причини, чому тут саме одиниця, але повинно бути таке ж як і в шейдері, layout. + 4, // розмір : x + y + z + size => 4 + GL_FLOAT, // тип + GL_FALSE, // нормалізовано? + 0, // stride + (void*)0 // зміщення в буфері +); + +// 3 буфер атрибутів: колір частинок +glEnableVertexAttribArray(2); +glBindBuffer(GL_ARRAY_BUFFER, particles_color_buffer); +glVertexAttribPointer( + 2, // атрибут. Немає причини, чому тут саме двійка, але повинно бути таке ж як і в шейдері, layout. + 4, // size : r + g + b + a => 4 + GL_UNSIGNED_BYTE, // тип + GL_TRUE, // нормалізовано? *** ТАК, це значит, що unsigned char[4] буде доступне як vec4 (floats) в шейдері + 0, // stride + (void*)0 // зміщення в буфері +); +``` + +І цей код теж самий звичайний. Та різниця з'являється при малюванні. Тепер замість `glDrawArrays` (чи `glDrawElements`, якщо використовується індексний буфер), Ви будете використовувати `glDrawArrraysInstanced` / `glDrawElementsInstanced`, який еквівалентний виклику `glDrawArrays` `N` раз (`N` - це останній параметр, в нашому випадку `ParticlesCount`): + +``` cpp +glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, ParticlesCount); +``` + +Але дещо відсутнє тут. Ми не повідомили OpenGL, який буфер буде базовим мешем, а який для інших копій. Це можна зробити за допомогою `glVertexAttribDivisor`. Ось повний код з коментарями: + +``` cpp +// Ця функція використовується в парі з glDrawArrays*Instanced*. +// Перший параметр буфер атрибутів, про який ми говоримо. +// Другий параметр визначає "швидкість, з якою атрибути просуваються, коли частинки обробляються групами" +// http://www.opengl.org/sdk/docs/man/xhtml/glVertexAttribDivisor.xml +glVertexAttribDivisor(0, 0); // вершини частинок: завжди використовуйте ті самі 4 вершини -> 0 +glVertexAttribDivisor(1, 1); // позиція: одна на чотирикутник (центр) -> 1 +glVertexAttribDivisor(2, 1); // колір: один на чотирикутник -> 1 + +// Малюємо частинки ! +// Це малює багато разів маленький triangle_strip (який виглядає як чотирикутник) +// Це все еквівалентно наступному: +// for(i in ParticlesCount) : glDrawArrays(GL_TRIANGLE_STRIP, 0, 4), +// але швидше +glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, ParticlesCount); +``` +Як Ви маєте змогу помітити, інстанціювання дуже універсальне, тому що Ви можливо передати ціле число через `AttribDivisor`. Наприклад, `glVertexAttribDivisor(2, 10)` означає, що 10 послідовних елемента (частинки) будуть мати однаковий колір. + +## Так в чому справа ? + +Справа в тому, що тепер потрібно оновлювати невеликий буфер кожний фрейм (центри частинок) і не потрібно оновлювати великий меш. В нашому випадку це в 4 рази менше даних для передачі! + + + +# Життя та смерть + +В протилежність більшості об'єктів на сцені, частинки народжуються і помирають з дуже великою швидкістю. Нам потрібен швидкий спосіб для отримання нових частинок і їх знищення, щось краще, ніж `new Particle()`. + + +## Створення нових частинок + +Для цього у нас буде великий контейнер з частинками: + +``` cpp +// структура для збереження частинок на CPU +struct Particle{ + glm::vec3 pos, speed; + unsigned char r,g,b,a; // Колір + float size, angle, weight; + float life; // Час, який залишився жити частинці. якщо менше нуля - частинка мертва і не використовується + +}; + +const int MaxParticles = 100000; +Particle ParticlesContainer[MaxParticles]; +``` + +Тепер потрібно навчитись створювати нову частинку. Ця функція буквально виконує лінійний пошук в `ParticlesContainer`, що виглядає поганою ідеєю, але пошук починається з останнього відомого місця, отже функція часто повертає результат миттєво: + +``` cpp +int LastUsedParticle = 0; + +// Знайти частинку в ParticlesContainer яка ще не використовувалась +// (тобто у неї життя (life) < 0); +int FindUnusedParticle(){ + + for(int i=LastUsedParticle; i (int)(0.016f*10000.0)) + newparticles = (int)(0.016f*10000.0); +``` + +## Видалення старих частинок + +Тут є хитрість, дивіться далі =) + +# Основний цикл симуляції + +`ParticlesContainer` містить активні і "мертві" частинки, а ось буфер, який відправляється до GPU повинен містити тільки живі частинки. + +Тому ми будемо ітеруватись по списку частинок, перевіряти, чи вони живі чи мають вмерти, і якщо все добре, то додамо трішки гравітації і скопіюємо до GPU буферу. + +``` cpp +// Симулюємо всі частинки +int ParticlesCount = 0; +for(int i=0; i 0.0f){ + + // Зменшуємо час життя + p.life -= delta; + if (p.life > 0.0f){ + + // Симуляція простої фізики - тільки гравітація, ніяких колізій + p.speed += glm::vec3(0.0f,-9.81f, 0.0f) * (float)delta * 0.5f; + p.pos += p.speed * (float)delta; + p.cameradistance = glm::length2( p.pos - CameraPosition ); + //ParticlesContainer[i].pos += glm::vec3(0.0f,10.0f, 0.0f) * (float)delta; + + // Заповнюємо GPU буфер + g_particule_position_size_data[4*ParticlesCount+0] = p.pos.x; + g_particule_position_size_data[4*ParticlesCount+1] = p.pos.y; + g_particule_position_size_data[4*ParticlesCount+2] = p.pos.z; + + g_particule_position_size_data[4*ParticlesCount+3] = p.size; + + g_particule_color_data[4*ParticlesCount+0] = p.r; + g_particule_color_data[4*ParticlesCount+1] = p.g; + g_particule_color_data[4*ParticlesCount+2] = p.b; + g_particule_color_data[4*ParticlesCount+3] = p.a; + + }else{ + // Частинки, які тільки закінчили своє життя, будуть розташовані в кінці буферу завдяки SortParticles(); + p.cameradistance = -1.0f; + } + + ParticlesCount++; + + } +} +``` + +Ось що у нас тепер є. Практично готово, але є проблема... + +![]({{site.baseurl}}/assets/images/tuto-particules/particles_unsorted.png) + +## Сортування + +Як було пояснено в [Туторіалі 10]({{site.baseurl}}/uk/intermediate-tutorials/tutorial-10-transparency/), Вам потрібно сортувати напівпрозорі об'єкти від найдальших до найближчих, що б вони правильно "змішались". + + +``` cpp +void SortParticles(){ + std::sort(&ParticlesContainer[0], &ParticlesContainer[MaxParticles]); +} +``` + +Тепер, `std::sort` потребує фукнції, яка зможе порівняти дві частинки і визначити, в якому порядку вони будуть в контейнері. Це можна зробити за допомогою `Particle::operator<`: + +``` + +// представлення частинки в "процесорі" +struct Particle{ + + ... + + bool operator<(Particle& that){ + // Сортуємо в зворотньому порядку : частинки, що розташовані далі малюються в першу чергу + return this->cameradistance > that.cameradistance; + } +}; +``` + +Це призведе до того, що `ParticleContainer` буде відсортовано і частинки будуть відображатись правильно*: + + +![]({{site.baseurl}}/assets/images/tuto-particules/particles_final.gif) + + + + +# Йдемо далі + + +## Анімовані частинки + +Ви можете анімувати текстури Ваших частинок, використовуючи атлас текстур. Додамо "вік частинки" до даних про її позицію і в шейдері розрахуємо UV координати як ми це робили в [туторіалі про 2D шрифти]({{site.baseurl}}/uk/intermediate-tutorials/tutorial-11-2d-text/). Атлас текстур виглядає наступним чином: + +![]({{site.baseurl}}/assets/images/tuto-particules/ParticleAtlas.png) + + +## Обробка декількох систем частинок + +Якщо Вам потрібно більше ніж одна система частинок, у Вас є два варіанти - використовувати один `ParticleContainer` чи по одному контейнеру частинок на кожну систему. + +Якщо у Вас один контейнер для всіх частинок, то це дасть змогу відсортувати їх ідеально. Головна проблема полягає в тому, що Вам потрібно буде використовувати одну й ту ж текстуру для всіх частинок, що є великою проблемою. Це може бути вирішено використанням атласу текстур (одна велика текстура з всіма потрібними текстурами і правильне використання UV координат), але це не дуже зручно редагувати і використовувати. + +Якщо ж у Вас один контейнер на одну систему частинок, частинки будуть сортуватись тільки в всередині контейнеру - якщо дві системи перетинаються, будуть різноманітні артефакти. Але, можливо, для Вашого випадку це не буде проблемою. + +Звичайно, можна використовувати гібридні рішення - декілька систем частинок, кожна з яким містить маленький і керований атлас. + +## Гладкі частинки + +Дуже швидко Ви побачите один спільний артифакт - коли Ваші частинки перетинають "певну геометрію", то з'являється неприємний "ліміт": + +![]({{site.baseurl}}/assets/images/tuto-particules/ParticleHardSmooth.jpg) + + +(картинка взята тут http://www.gamerendering.com/2009/09/16/soft-particles/ ) + +Звичайне рішення подібної проблеми - перевіряти, чи не знаходиться поточний фрагмент неподалік від Z-буферу. Якщо так - то фрагмент зникає. + +Однак, Вам потрібно буде взяти зразки з Z-буфера, що неможливо для "нормального" Z-буферу. Вам потрібно намалювати Вашу сцену в [render target]({{site.baseurl}}/uk/intermediate-tutorials/tutorial-14-render-to-texture/). Інший варіант - можна скопіювати Z-буфер з одного фреймбуферу в інший за допомогою `glBlitFramebuffer`. + + +http://developer.download.nvidia.com/whitepapers/2007/SDK10/SoftParticles_hi.pdf + +## Покращення швидкості заповнення (fillrate) +Є один фактор, який максимально обмежує можливості на сучасних графічних процесора - швидкість заповнення - максимальна кількість фрагментів (пікселів), яку можна записати за 16.6 мс що б отримати 60 кадрів на секунду. + +Це зазвичай є проблемою, тому що частинки зазвичай вимагають велику швидкість заповнення - Ви можете перемальовувати один фрагмент десять раз, кожний раз з іншими частинками і Ви не хочете артефактів, згаданих раніше. + +Посеред всіх фрагментів, які записуються, є багато таких, які повністю не потрібні - вони на межі. Текстури Ваших частинок дуже часто повністю прозорі на краях, але меш частинки все рівно буде малюватись - і оновлювати буфер кольору з таким самим значенням як і до цього. + +Ця невелика утиліта розраховує меш (який Ви будете малювати за допомогою `glDrawArraysInstanced`), який щільно прилягає до текстури: + +![](http://www.humus.name/Cool/ParticleTrimmer.jpg) + + +[http://www.humus.name/index.php?page=Cool&ID=8](http://www.humus.name/index.php?page=Cool&ID=8) . На сайті Еміля Персона є багато інших захоплюючих статей. + +## Фізика частинок + +У якийсь момент, Вам захочеться що б Ваші частинки взаємодіяли з Вашим світом. Наприклад, Ви хочете, що б частинки відскакували від поверхні землі. + +Ви можете просто запустити raycast для кожної частинки з поточної позиції до наступної, ми вивчили як це зробити в [Picking tutorials](http://www.opengl-tutorial.org/miscellaneous/clicking-on-objects/picking-with-a-physics-library/). Але це дуже "дорого", це дуже складно робити для кожної частинки кожний фрейм. + +В залежності від Вашої програми, Ви можете реалізувати апроксимацію геометрії за допомогою площин і raycast на цих площинах тільки. Або Ви можете використовувати справжній raycast, але зробити кеш і апроксимацію можливих колізій. + +А є абсолютно інша техніка - використання вже готового Z-буферу як дуже грубу апроксимацію (видимої) геометрії і частинок, що стикаються. Це "цілком достатньо" і швидко, але буде потрібно це все зробити на GPU, так як неможливо отримати доступ до Z-буферу з CPU (як мінімум швидкий доступ), і як наслідок, цей спосіб достатньо складний. + + +Ось декілька посилань про цю технологію: + +[http://www.altdevblogaday.com/2012/06/19/hack-day-report/](http://www.altdevblogaday.com/2012/06/19/hack-day-report/) + +[http://www.gdcvault.com/search.php#&category=free&firstfocus=&keyword=Chris+Tchou's%2BHalo%2BReach%2BEffects&conference_id=](http://www.gdcvault.com/search.php#&category=free&firstfocus=&keyword=Chris+Tchou's%2BHalo%2BReach%2BEffects&conference_id=) + +## Симуляція на GPU + +Як було сказано вище, Ви можете симулювати рух частинок повністю на GPU. Ви будете все ще керувати життям своїх частинок на CPU, як мінімум, створювати їх. + +У Вас є багато варіантів зробити це і жодний з них не є ціллю цього туторіалу. Я дам лишень декілька порад. + +* Використовуйте "Transform Feedback" (перетворення зворотного зв'язку). Це дозволить Вам зберегти результат вершинного шейдера в VBO, яке належить GPU. Зберігайте нову позицію в цьому VBO і в наступному фреймі використовуйте як стартову позицію і знову зберігайте там координати. +* Теж ж саме, але без Transform Feedback - збережіть свої координати в текстуру і оновлюйте за допомогою "малювання в текстуру" (Render-To-Texture). +* Використовуйте бібліотеки для GPU - CUDA, OpenCL які мають функції взаємодії з OpenGL. +* Використовуйте шейдери обчислень - Compute Shader. Чисте рішення, але доступне тільки на нових GPU. + +* Зверніть увагу, що для простоти, ця реалізація сортує `ParticleContainer` після оновлення GPU буферів. Це робить частинки не повністю відсортованими (є затримка в один кадр), але це не дуже помітно. Ви можете це виправити розділивши головний цикл в два - Симуляція, Сортування і Оновлення.date. diff --git a/uk/intermediate-tutorials/index.markdown b/uk/intermediate-tutorials/index.markdown new file mode 100644 index 000000000..b91b641c9 --- /dev/null +++ b/uk/intermediate-tutorials/index.markdown @@ -0,0 +1,45 @@ +--- +layout: page +type: section +status: publish +published: true +title: Туторіали середнього рівня +date: '2011-05-07 10:45:46 +0200' +date_gmt: '2011-05-07 10:45:46 +0200' +categories: [] +tags: [] +language: uk +--- + +Читайте їх в будь-якому порядку. + +{% assign sorted_pages = site.pages | sort:"order" %} +{% for p in sorted_pages %} + {% assign splt = p.url | split: page.url %} + {% if splt.size == 2 and splt[0] == '' %} + {% assign slash = splt[1] | split: '/' %} +{% if slash.size == 1 %} +- {{p.title}} +{% else %} + - {{p.title}} +{% endif %} + {% endif %} +{% endfor %} + + + + + diff --git a/uk/intermediate-tutorials/tutorial-10-transparency/index.markdown b/uk/intermediate-tutorials/tutorial-10-transparency/index.markdown new file mode 100644 index 000000000..529f1d354 --- /dev/null +++ b/uk/intermediate-tutorials/tutorial-10-transparency/index.markdown @@ -0,0 +1,123 @@ +--- +layout: tutorial +status: publish +published: true +title: 'Tutorial 10 : Прозорість' +date: '2011-05-13 23:00:42 +0200' +date_gmt: '2011-05-13 23:00:42 +0200' +categories: [tuto] +order: 500 +tags: [] +language: uk +--- + +* TOC +{:toc} + +# Альфа канал + +Концепція альфа каналу досить проста. Замість використання RGB, ви використовуєте RGBA : + +``` glsl +// Вихідні дані : тепер це vec4 +out vec4 color; +``` +{: .highlightglslfs } + +перші три компоненти все ще доступні за допомогою оператора .xyz, а о прозорість - .a : + +``` glsl +color.a = 0.3; +``` +{: .highlightglslfs } + +Не зовсім очевидно, та альфа це "непрозорість", тому альфа = 1 - повна непрозорість, альфа = 0 - повна прозорість. + +Тут ми просто використовуємо альфа канал рівний 0.3, та ви можете захотіти щось своє чи прочитати з RGBA текстури (TGA підтримує альфа канал, GLFW підтримує TGA). + +Ось як це виглядає. Переконайтесь, що ви вимкнули відсікання задньої поверхні (backface culling, `glDisable(GL_CULL_FACE)`), тому що якщо ми можемо бачити через меш (а він же прозорий), ми повинні побачити задню частину фігури. + + +![]({{site.baseurl}}/assets/images/tuto-10-transparency/transparencyok.png) + + +# Порядок має значення ! + +Попереднє зображення виглядає непогано, але це тому, що нам пощастило. + +## Проблема + +Тепер у нас намальовано два квадрата с прозорістю 50%, зелений та червоний. Тепер повинно бути видно, что порядок має значення, вихідний колір дає певні підказки для очей для правильного сприйняття. + +![]({{site.baseurl}}/assets/images/tuto-10-transparency/transparencyorder.png) + +Цей феномен також видно і в нашій сцені. Давайте змінимо трішки позицію перегляду : + +![]({{site.baseurl}}/assets/images/tuto-10-transparency/transparencybad.png) + +Тепер видно, що це може бути серйозною проблемою. Ви ніколи не бачили скільки прозорості в іграх, чи не так? + +## Просте рішення + +Просте рішення в тому, що би відсортувати всі прозорі трикутники. Так, всі прозорі трикутники. + +* Намалювати всі непрозорі складові світу і буфер глибини зможе відкинути всі сховані прозорі трикутники +* Відсортувати всі прозорі трикутники від дальніх до ближніх +* Намалювати прозорі трикутники + +Ви можете використати `qsort` (в C) чи `std::sort` (в C++). Я не буду заглиблюватись в деталі, тому що... + +## Застереження + +Так буде працювати (більше про це в наступній секції), але: + +* Швидкість заповнення обмежена. Кожний фрагмент буде записаний 10, 20 раз, можливо більше. Це погано для повільної шини пам'яті. Зазвичай буфер глибини дозволяє відкинути достатньо "далекі" фрагменти, але зараз, Ви самі сортуєте їх і буфер глибини не потрібен. +* Ви будете робити це 4 рази на піксель (ми використовуємо 4xMSAA), якщо тільки не використовувати більш якісні оптимізації. +* Сортування всіх прозорих трикутників - це досить довго +* Якщо Вам потрібно перемикати текстури чи, що ще гірше, шейдери на кожний трикутник, то у Вас будуть серйозні проблеми з продуктивністю (швидкістю виконання). Не робіть так. + +Більш краще рішення зазвичай є: + +* Обмежити максимальну кількість прозорих полігонів +* Використовувати один й той же шейдер та текстуру для них всіх +* Якщо вони мають виглядати зовсім по іншому, використовуйте ваші текстури! +* Якщо ви можете уникнути сортування и воно виглядає *достатньо добре* - ви щасливчик. + +## Прозорість, що не залежить від порядку + +Варто вивчити ряд сучасних технік, якщо ваш рушій дійсно потребує надсучасної прозорості: + +* [The original 2001 Depth Peeling paper](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.18.9286&rep=rep1&type=pdf): результати з точністью до пікселя, проте повільно. +* [Dual Depth Peeling](http://developer.download.nvidia.com/SDK/10/opengl/src/dual_depth_peeling/doc/DualDepthPeeling.pdf) : невеличке покращення +* Деякі статті по сортування методом комірок. Використовується масив фрагментів, сортування за глибиною в шейдері. +* [ATI's Mecha Demo](http://fr.slideshare.net/hgruen/oit-and-indirect-illumination-using-dx11-linked-lists) : гарний та швидкий, але з складний в реалізації, потребує сучасного заліза. Використовує зв'язаний список фрагментів. +* [Cyril Crassin's variation on the ATI's technique](http://blog.icare3d.org/2010/07/opengl-40-abuffer-v20-linked-lists-of.html) : ще складніша реалізація + +Зверніть увагу, що навіть сучасні ігри (наприклад, Little Big Planet), що запускаються на потужних консолях, використовують тільки один шар прозорості. + +# Функція змішування (blend) + +Що б код вище працював, потрібно налаштувати функцію змішування. + +``` cpp +// Дозволити змішування (blending) +glEnable(GL_BLEND); +glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); +``` + +Що значить : +``` + +New color in framebuffer = + current alpha in framebuffer * current color in framebuffer + + (1 - current alpha in framebuffer) * shader's output color +``` + +Приклад для зображення вище, з червоним на верху: + +``` cpp +new color = 0.5*(0,1,0) + (1-0.5)*(1,0.5,0.5); // (червоний був змішаний з білим фоном) +new color = (1, 0.75, 0.25) = the same orange +``` + + diff --git a/uk/intermediate-tutorials/tutorial-11-2d-text/index.markdown b/uk/intermediate-tutorials/tutorial-11-2d-text/index.markdown new file mode 100644 index 000000000..e615d78b5 --- /dev/null +++ b/uk/intermediate-tutorials/tutorial-11-2d-text/index.markdown @@ -0,0 +1,141 @@ +--- +layout: tutorial +status: publish +published: true +title: 'Tutorial 11 : 2D text' +date: '2011-05-16 22:38:44 +0200' +date_gmt: '2011-05-16 22:38:44 +0200' +categories: [tuto] +order: 510 +tags: [] +language: uk +--- + +* TOC +{:toc} + +В цьому туторіалі ми вивчимо як намалювати 2D текст поверх Вашого 3D зображення. В нашому випадку це буде звичайний таймер: + +![]({{site.baseurl}}/assets/images/tuto-11-2d-text/clock.png) + + +# API + +Зараз ми реалізуємо наступний простий інтерфейс (в common/text2D.h): + +``` cpp +void initText2D(const char * texturePath); +void printText2D(const char * text, int x, int y, int size); +void cleanupText2D(); +``` + +Щоб даний код працював на 640х480 і на 1080p, `x` та `y` координати повинні бути в діапазоні [0-800][0-600]. Вершинний шейдер прилаштує ще до дійсних розмірів екрану. + +Загляньте в common/text2D.cpp для повної реалізації. + +# Текстура + +initText2D просто читає текстуру і пару шейдерів. Тут нічого цікавого, та поглянемо на цю текстуру: + +![]({{site.baseurl}}/assets/images/tuto-11-2d-text/fontalpha.png) + +Ця текстура була згенерована за допомогою [CBFG](http://www.codehead.co.uk/cbfg/), одним з багатьох інструментів, що створють текстури з шрифтів. Потім це зображення було завантажено в Paint.NET, де я додав червоне тло (виключно для візуалізації - все що намальовано червоним буде прозорим). + +Таким чином, завданням printText2D буде створення чотирикутників з правильними екранними координатами і текстурними координатами. + +# Малюємо + +Ми маємо заповнити ці буфери: + +``` cpp +std::vector vertices; +std::vector UVs; +``` + +Для кожного символу ми розраховуємо координати чотирьох вершин що будуть вершинами чотирикутника і додамо два трикутники: + +``` cpp +for ( unsigned int i=0 ; i> просторі + // перетворюємо [0..800][0..600] в [-1..1][-1..1] + vec2 vertexPosition_homoneneousspace = vertexPosition_screenspace - vec2(400,300); // [0..800][0..600] -> [-400..400][-300..300] + vertexPosition_homoneneousspace /= vec2(400,300); + gl_Position = vec4(vertexPosition_homoneneousspace,0,1); + + // UV для вершин. ніяких перетворень + UV = vertexUV; +} +``` +{: .highlightglslvs } + +Фрагментний шейдер теж робить трішки роботи: + +``` glsl +void main(){ + color = texture( myTextureSampler, UV ); +} +``` +{: .highlightglslfs } + +Не використовуйте цей код для чогось серйозного, так як він оперує тільки латинським алфавітом. Або не продавайте свої програми в Індію, Китай, Японію чи Україну (та навіть Німеччину, тому що на малюнку немає символу ß). Ця текстура буде працювати для Франції (тут є символи é, à, ç, та інші), тому що це було згенеровано на моїй локалі. Також будьте обережними з кодом з інших бібліотек - вони зазвичай використовують OpenGL другої версії, яка не сумісна. На жаль я не знаю жодної гарної бібліотеки, що вміє працювати з UTF-8. + + + +Скоріш за все Вам буде цікаво прочитати [The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)](http://www.joelonsoftware.com/articles/Unicode.html) від Джоела Спольскі. + +Також подивіться [цю статтью від Valve](http://www.valvesoftware.com/publications/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf) якщо Вам потрібен великий текст. diff --git a/uk/intermediate-tutorials/tutorial-12-opengl-extensions/index.markdown b/uk/intermediate-tutorials/tutorial-12-opengl-extensions/index.markdown new file mode 100644 index 000000000..6fe4d783b --- /dev/null +++ b/uk/intermediate-tutorials/tutorial-12-opengl-extensions/index.markdown @@ -0,0 +1,131 @@ +--- +layout: tutorial +status: publish +published: true +title: 'Tutorial 12 : OpenGL Extensions' +date: '2012-02-03 20:46:21 +0100' +date_gmt: '2012-02-03 20:46:21 +0100' +categories: [tuto] +order: 520 +tags: [] +language: uk +--- + +* TOC +{:toc} + +# Розширення + +З кожним новим поколінням, продуктивність графічних карт збільшується, дозволяючи малювати більше трикутників та пікселів. Але продуктивність це не є основною проблемою. NVIDIA, AMD та Intel також покращують їх графічні карти, надаючи нову функціональність. Розглянемо декілька прикладів. + +## ARB_fragment_program + +Повернемося в 2002 рік, GPU ще не мають вершинних та фрагментних шейдерів - все було зашито в мікросхеми. Це називалося Fixed-Function Pipeline (FFP) - <<пайплайн>> з фіксованими функціями. Таким чином, сама свіжа версія API, а це була OpenGL 1.3 не пропонувала нічого, що можна було назвати чи використовувати як шейдери, тому що вони ще не існували. Але в NVIDIA вирішили, що це було би дуже зручно описувати процес рендеру за допомогою коду, а не сотнями змінних та прапорців, що описують стан системи. Ось чому було створено ARB_fragment_program. Так, тоді не існувало GLSL, але вже можна було писати так: + +``` +!!ARBfp1.0 MOV result.color, fragment.color; END +``` + +Але що б заставити OpenGL використовувати такий код, нам потрібні спеціальні функції, яких ще не існувало в OpenGL. Але перед поясненнями, розглянемо ще один приклад. + +## ARB_debug_output + +Гаразд, але `ARB_fragment_program` дуже стара штука, напевне нам вона більше не потрібна? Добре, але у нас є інші розширення, які бувають дуже зручні. Наприклад `ARB_debug_output`, яке додає функціональність, якої немає в OpenGL 3.3, але Ви маєте змогу використовувати його. Це розширення додає такі <<токени>> як `GL_DEBUG_OUTPUT_SYNCHRONOUS_ARB` чи `GL_DEBUG_SEVERITY_MEDIUM_ARB`, та функції, наприклад `DebugMessageCallbackARB`. Чудова особливість цієї функції в тому, що якщо Ви напишете не зовсім коректний код, наприклад: + +``` cpp +glEnable(GL_TEXTURE); // Помилка ! Скоріш за все Вам потрібен GL_TEXTURE_2D ! +``` +Ви отримаєте повідомлення про помилку та точні координати проблеми. Уроки вивчено: + +* Розширення все ще корисні, навіть в свіжому, 3.3 OpenGL +* Використовуйте `ARB_debug_output` ! Дивіться посилання нижче. + +![]({{site.baseurl}}/assets/images/tuto-12-ogl-ext/breakpoint.png) + + +## Отримання розширень - складний спосіб + +"Ручний спосіб" перевірки, чи доступне розширення полягає в використанні наступного шматка коду (взято з [OpenGL.org wiki](http://www.opengl.org/wiki/GlGetString)): + + +``` cpp +int NumberOfExtensions; +glGetIntegerv(GL_NUM_EXTENSIONS, &NumberOfExtensions); +for(i=0; i> + +З часів [Туторіалу 8 : базове затінення]({{site.baseurl}}/uk/beginners-tutorials/tutorial-8-basic-shading/), отримати гарне затінення, використовуючи нормалі трикутників. До цих пір у нас була одна нормаль на вершину - в середині трикутника вона плавно змінювалась, а ось з кольором все по іншому - там є текстури. Базова ідея відображення нормалей (normal mapping) полягає в наданні нормалям такої же гнучкості. + +# Текстури нормалей + +"Текстура нормалей" зазвичай виглядає десь так: + + +![]({{site.baseurl}}/assets/images/tuto-13-normal-mapping/normal.jpg) + +В кожному RGB текселі закодовано XYZ вектор: кожна кольорова компонента знаходиться в діапазоні 0..1 і кожний компонент вектора в діапазоні -1..1, отже це просте відображення з текселів в нормалі: + +``` c +normal = (2*color)-1 // для кожного компонента +``` + +Ці текстури в основному сині, тому що в основному нормалі направлені "з поверхні". Зазвичай, X координата вказує вправо на площині, Y - вгору (в контексті зображення текстури), отже за правилом правої руки, Z направлено назовні площини текстури. + +Ця текстура використовується точнісінько так само, як і дифузна. Та проблема полягає в тому, як конвертувати наші нормалі, які виражені в просторі кожного індивідуального трикутника (дотичний (tangent) простір, також "простір зображення"), в простір моделі (так як він використовується в нашому рівнянні затінення). + +# Tangent and Bitangent + +Тепер Ви достатньо знайомі з матрицями, тому Ви знаєте, що щоби визначити простір (в нашому випадку, дотичний простір), нам потрібно 3 вектори. Ми вже маємо наш "вгору" вектор - це нормаль, яку нам надає Blender чи розраховується на основі трикутника просто векторним добутком. Цей вектор представлений синім кольором, таким, який в середньому колір на карті нормалей : + +![]({{site.baseurl}}/assets/images/tuto-13-normal-mapping/NormalVector.png) + +Також нам потрібна дотична, T - вектор, який паралельний поверхні. Але є багато таких векторів : + +![]({{site.baseurl}}/assets/images/tuto-13-normal-mapping/TangentVectors.png) + +Який з них нам потрібно використовувати? В теорії, будь-який, але нам потрібно, щоб він був узгоджений з сусідніми, що б уникнути потворних країв. Стандартний спосіб полягає в тому, що б орієнтувати так само, як і текстурні координати: + +![]({{site.baseurl}}/assets/images/tuto-13-normal-mapping/TangentVectorFromUVs.png) + +Так як нам потрібно 3 вектори, що б визначити базис, нам потрібно розрахувати <<бідотичний вектор>> B (який насправді може бути будь-яким із дотичних векторів, але математика стає простою, коли все перпендикулярне) : + +![]({{site.baseurl}}/assets/images/tuto-13-normal-mapping/NTBFromUVs.png) + + + Ось алгоритм: якщо ми позначимо через deltaPos1 та deltaPos2 два ребра нашого трикутника, а також deltaUV1 та deltaUV2 відповідні різниці в UV, то ми можемо виразити нашу проблему наступним рівнянням: + +``` c +deltaPos1 = deltaUV1.x * T + deltaUV1.y * B +deltaPos2 = deltaUV2.x * T + deltaUV2.y * B +``` + +Потрібно просто розв'язати це рівняння відносно T і B і у Вас будуть Ваші вектора! (дивіться код нижче) + +Як тільки ми маємо вектора T, B, N, ми також маємо цю гарну матрицю яка дозволяє нам перейти від дотичного простору в простір моделі: + +![]({{site.baseurl}}/assets/images/tuto-13-normal-mapping/TBN.png) + +За допомогою цієї TBN матриці ми можемо трансформувати нормалі (які отримали з текстури) в простір моделі. Та зазвичай це робиться навпаки - трансформуємо все з простору моделі в дотичний простір і зберігаємо отримані нормалі такими, як вони є. Всі обчислення робляться в дотичному просторі, що нічого не змінює. + +<<Це зворотня трансформація, ми просто беремо інвертовану матрицю, яка в даному випадку (ортогональна матриця, тобто кожний вектор є перпендикулярний до інших. Дивіться розділ "йдемо далі" нижче) є просто транспонованою матрицею, що значно спрощує наші розрахунки:>> + + +``` c +invTBN = transpose(TBN) +``` + +, тобто : +![]({{site.baseurl}}/assets/images/tuto-13-normal-mapping/transposeTBN.png) + + +# Підготовка нашого VBO + + +## Розрахунок дотичної і бідотичної + +Оскільки нам потрібні дотичні і бідотичні поверх наших нормалей, ми розрахуємо їх для всього меша. Ми зробимо це в окремій функції: + +``` cpp +void computeTangentBasis( + // вхідні + std::vector & vertices, + std::vector & uvs, + std::vector & normals, + // вихідні + std::vector & tangent, + std::vector & bitangent +){ +``` + +для кожного трикутника ми розраховуємо ребро (deltaPos) та deltaUV + +``` cpp + for ( int i=0; i> + +``` cpp + GLuint ModelView3x3MatrixID = glGetUniformLocation(programID, "MV3x3"); +``` + +І повний код, що малює зображення буде наступним: + +``` cpp + // Очистимо екран + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + // Наші шейдери + glUseProgram(programID); + + // Розрахуємо MVP матрицю з клавіатури і матриці + computeMatricesFromInputs(); + glm::mat4 ProjectionMatrix = getProjectionMatrix(); + glm::mat4 ViewMatrix = getViewMatrix(); + glm::mat4 ModelMatrix = glm::mat4(1.0); + glm::mat4 ModelViewMatrix = ViewMatrix * ModelMatrix; + glm::mat3 ModelView3x3Matrix = glm::mat3(ModelViewMatrix); // Візьмемо ліву верхню частинку матриці ModelViewMatrix + glm::mat4 MVP = ProjectionMatrix * ViewMatrix * ModelMatrix; + + // Відправимо нашу матрицю до поточного шейдера, + // через змінну "MVP" + glUniformMatrix4fv(MatrixID, 1, GL_FALSE, &MVP[0][0]); + glUniformMatrix4fv(ModelMatrixID, 1, GL_FALSE, &ModelMatrix[0][0]); + glUniformMatrix4fv(ViewMatrixID, 1, GL_FALSE, &ViewMatrix[0][0]); + glUniformMatrix4fv(ViewMatrixID, 1, GL_FALSE, &ViewMatrix[0][0]); + glUniformMatrix3fv(ModelView3x3MatrixID, 1, GL_FALSE, &ModelView3x3Matrix[0][0]); + + glm::vec3 lightPos = glm::vec3(0,0,4); + glUniform3f(LightID, lightPos.x, lightPos.y, lightPos.z); + + // Прив'яжемо нашу дифузну текстуру в текстурний юніт 0 + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, DiffuseTexture); + // Set our "DiffuseTextureSampler" sampler to user Texture Unit 0 + glUniform1i(DiffuseTextureID, 0); + + // Прив'яжемо нашу текстуру з нормалями в текстурний юніт 1 + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, NormalTexture); + // Set our "Normal TextureSampler" sampler to user Texture Unit 0 + glUniform1i(NormalTextureID, 1); + + // перший атрибут в буфері: вершини + glEnableVertexAttribArray(0); + glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer); + glVertexAttribPointer( + 0, // атрибут + 3, // розмір + GL_FLOAT, // тип + GL_FALSE, // нормалізовано? + 0, // крок + (void*)0 // зміщення початку в буфері + ); + + // другий атрибут в буфері : UVs + glEnableVertexAttribArray(1); + glBindBuffer(GL_ARRAY_BUFFER, uvbuffer); + glVertexAttribPointer( + 1, // атрибут + 2, // розмір + GL_FLOAT, // тип + GL_FALSE, // нормалізовано? + 0, // крок + (void*)0 // зміщення початку в буфері + ); + + // 3 атрибут в буфері : нормалі + glEnableVertexAttribArray(2); + glBindBuffer(GL_ARRAY_BUFFER, normalbuffer); + glVertexAttribPointer( + 2, // атрибут + 3, // розмір + GL_FLOAT, // тип + GL_FALSE, // нормалізовано? + 0, // крок + (void*)0 // зміщення початку в буфері + ); + + // 4 атрибут в буфері: дотична + glEnableVertexAttribArray(3); + glBindBuffer(GL_ARRAY_BUFFER, tangentbuffer); + glVertexAttribPointer( + 3, // атрибут + 3, // розмір + GL_FLOAT, // тип + GL_FALSE, // нормалізовано? + 0, // крок + (void*)0 // зміщення початку в буфері + ); + + // 5 атрибут в буфері: бідотична + glEnableVertexAttribArray(4); + glBindBuffer(GL_ARRAY_BUFFER, bitangentbuffer); + glVertexAttribPointer( + 4, // атрибут + 3, // розмір + GL_FLOAT, // тип + GL_FALSE, // нормалізовано? + 0, // крок + (void*)0 // зміщення початку в буфері + ); + + // Індексний буфер + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementbuffer); + + // Намалюємо трикутники ! + glDrawElements( + GL_TRIANGLES, // режим + indices.size(), // кількість + GL_UNSIGNED_INT, // тип + (void*)0 // зміщення початку в буфері + ); + + glDisableVertexAttribArray(0); + glDisableVertexAttribArray(1); + glDisableVertexAttribArray(2); + glDisableVertexAttribArray(3); + glDisableVertexAttribArray(4); + + // обміняємо буфери + glfwSwapBuffers(); +``` + +## Вершинний шейдер + +Як було сказано раніше, ми робили все в просторі камери, тому що так простіше отримати позицію фрагмента в цьому просторі. Ось чому ми множимо наші T,B,N вектори на матрицю вида моделі. + +``` glsl + vertexNormal_cameraspace = MV3x3 * normalize(vertexNormal_modelspace); + vertexTangent_cameraspace = MV3x3 * normalize(vertexTangent_modelspace); + vertexBitangent_cameraspace = MV3x3 * normalize(vertexBitangent_modelspace); +``` +{: .highlightglslfs } + +Ці три вектора визначають матрицю TBN, яка конструюється наступним чином: +``` + + mat3 TBN = transpose(mat3( + vertexTangent_cameraspace, + vertexBitangent_cameraspace, + vertexNormal_cameraspace + )); // Ви можете використовувати векторний добуток замість побудови цієї матриці та транспонування. Дивіться Посилання для деталей. +``` +Ця матриця йде від простору камери до дотичного простору (Подібна матриця, але з XXX_modelspace, буде від моделі в дотичний простір). Ми можемо використати її для розрахунку напрямку світла та напрямку очей в дотичному просторі: + +``` + + LightDirection_tangentspace = TBN * LightDirection_cameraspace; + EyeDirection_tangentspace = TBN * EyeDirection_cameraspace; +``` + +## Фрагментний шейдер + +Наші нормалі в дотичному просторі дуже легко отримати - це є наша текстура: + +``` glsl + // Локальна нормаль, в дотичному просторі + vec3 TextureNormal_tangentspace = normalize(texture( NormalTextureSampler, UV ).rgb*2.0 - 1.0); +``` +{: .highlightglslfs } + +Отже, тепер у нас є все, що нам потрібно. Дифузне світло використовує *clamp( dot( n,l ), 0,1 )*, з `n` та `l`, які виражені в дотичному просторі (не має значення, в якому просторі ви зробите Ваші векторні та скалярні добутки, головне, щоб `n` та `l` були в одному просторі ). Дзеркальне освітлення використовує *clamp( dot( E,R ), 0,1 )* з `E` та `R`, які виражені в дотичному просторі. Так! + +# Результати + +Ось наш результат. Ви можете помітити, що: + +* Цеглини виглядають горбиками, тому що ми маємо багато різних нормалей +* Цемент виглядає плоским, тому що текстура в основаному синя + + +![]({{site.baseurl}}/assets/images/tuto-13-normal-mapping/normalmapping.png) + + +# Йдемо далі + + +## Ортогоналізація + +В нашому вершинному шейдері ми транспонуємо, а не робимо інверсію, тому, що це швидше. Але це працює тільки якщо простір, який представлений матрицею є ортогональним, чого ще немає. На щастя, це дуже легко виправити - ми просто робимо дотичну перпендикулярну до нормалі в кінці `computeTangentBasis()`: + + +``` glsl +t = glm::normalize(t - n * glm::dot(n, t)); +``` +{: .highlightglslvs } + +Ця формула може бути складною для розуміння, ця маленька схема може допомогти: + +![]({{site.baseurl}}/assets/images/tuto-13-normal-mapping/gramshmidt.png) + +`n` та `t` майже перпендикулярні, отже ми можемо "проштовхнути" `t` в напрямку `-n` помноженному на `dot(n,t)` + +[Ось](http://www.cse.illinois.edu/iem/least_squares/gram_schmidt/)' маленький аплет, який це також пояснює (Використовується тільки два вектори). + +## <> "Правша-лівша" + +Зазвичай Вам не потрібно турбуватись про це, але в деяких випадках, коли Ви використовуєте симетричні моделі, UV орієнтовано в невірному напрямку і Ваш `T` має невірну орієнтацію. + +Перевірка необхідності інвертування дуже проста: `TBN` має бути в правій системі координат, тобто `cross(n,t)` має мати ту саму орієнтацію, що і `b`. + +Мовою математики - "вектор A має ту саму орієнтацію, що і вектор B" значить, что `dot(A,B)>0`, отже нам потрібно перевірити `dot(cross(n,t), b) > 0` + +Якщо це не так, просто інвертуйте `t`: + +``` c +if (glm::dot(glm::cross(n, t), b) < 0.0f){ + t = t * -1.0f; + } +``` + +Це також робиться для кожної вершини в кінці `computeTangentBasis()`. + +## Дзеркальна текстура + +Для задоволення, я просто додав дзеркальну текстуру в код. Це виглядає так: + + +![]({{site.baseurl}}/assets/images/tuto-13-normal-mapping/specular.jpg) + +і використав це замість простого сірого `vec3(0.3,0.3,0.3)`, що використовувалось як дзеркальний колір. + +![]({{site.baseurl}}/assets/images/tuto-13-normal-mapping/normalmappingwithspeculartexture.png) + +Зверніть увагу, що зараз цемент завжди чорний - текстура говорить, що він не має дзеркальної компоненти. + +## Налагодження за допомогою immediate (безпосереднього) режиму + +Реальна ціль цього вебсайту в тому, що би Ви НЕ ВИКОРИСТОВУВАЛИ режим immediate (безпосередній), який є застарілим, повільним та проблематичним з багатьох причин. + +Проте цей режим іноді дуже зручний для налагодження: + +![]({{site.baseurl}}/assets/images/tuto-13-normal-mapping/immediatemodedebugging.png) + +Ось приклад візуалізації нашого дотичного простору за допомогою ліній, що намальовані в цьому режимі. + +Для цього Вам потрібно буде відмовитись від core профілю 3.3 : + +``` cpp +glfwOpenWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE); +``` +далі передамо наші матриці до OpengGl в старий спосіб (Ви можете написати інший шейдер, але це значно простіше зробити в цей спосіб, Ви все одно <<хачите>>): + +``` cpp +glMatrixMode(GL_PROJECTION); +glLoadMatrixf((const GLfloat*)&ProjectionMatrix[0]); +glMatrixMode(GL_MODELVIEW); +glm::mat4 MV = ViewMatrix * ModelMatrix; +glLoadMatrixf((const GLfloat*)&MV[0]); +``` + +Заборонимо шейдери : + +``` cpp +glUseProgram(0); +``` +І намалюємо Ваші лінії (в данному випадку, нормалі, які були нормалізовані, помножені на 0.1 і прикладені до правильних вершин): + +``` cpp +glColor3f(0,0,1); +glBegin(GL_LINES); +for (int i=0; i> + +Як вже зазначалось раніше, дуже важливо точно знати, в якому саме просторі знаходятся Ваші вектори. Не беріть скалярний добуток в просторі камери та векторний в просторі моделі. + +Додайте суффікси з назвою простору до до імен векторів (наприклад `..._modelspace`) і це допоможе виявити проблеми дуже легко. + +## Як створити карту нормалей + +Створено James O'Hare. Натисніть для збільшення. + +![]({{site.baseurl}}/assets/images/tuto-13-normal-mapping/normalMapMiniTut.jpg) + + +# Вправи + +* Нормалізуйте вектори в indexVBO_TBN перед тим як додавати і подивіться, що вийде. +* Візуалізуйте інші вектори (наприклад `EyeDirection_tangentspace`) в режимі кольорів і спробуйте знайти пояснення тому що Ви бачите. + +# Інструменти та посилання + + +* [Crazybump](http://www.crazybump.com/) , чудовий інструмент для створення карти нормалей. Не безкоштовний. +* [Плагін для photoshop від Nvidia](http://developer.nvidia.com/nvidia-texture-tools-adobe-photoshop). Безкоштовний, проте photoshop ні... +* [Створіть свої карти нормалей з декількох фотографій](http://www.zarria.net/nrmphoto/nrmphoto.html) +* [Створіть свої карти нормалей з однієї фотографії](http://www.katsbits.com/tutorials/textures/making-normal-maps-from-photographs.php) +* Більше інформації про [транспонування матриць](http://www.katjaas.nl/transpose/transpose.html) + + +# Посилання + + +* [Lengyel, Eric. "Computing Tangent Space Basis Vectors for an Arbitrary Mesh". Terathon Software 3D Graphics Library, 2001.](http://www.terathon.com/code/tangent.html) +* [Real Time Rendering, third edition](http://www.amazon.com/dp/1568814240) +* [ShaderX4](http://www.amazon.com/dp/1584504250) + + + + diff --git a/uk/intermediate-tutorials/tutorial-14-render-to-texture/index.markdown b/uk/intermediate-tutorials/tutorial-14-render-to-texture/index.markdown new file mode 100644 index 000000000..b1e2992f9 --- /dev/null +++ b/uk/intermediate-tutorials/tutorial-14-render-to-texture/index.markdown @@ -0,0 +1,225 @@ +--- +layout: tutorial +status: publish +published: true +title: 'Туторіал 14 : Рендер в текстуру' +date: '2011-05-26 19:33:15 +0200' +date_gmt: '2011-05-26 19:33:15 +0200' +categories: [tuto] +order: 540 +tags: [] +language: uk +--- + +* TOC +{:toc} + +Рендер (малювання) в текстуру - це зручний метод для створення різноманітних ефектів. Основна ідея полягає в тому, що Ви малюєте сцену як зазвичай, але в текстуру, яку потім можна буде використати. + +Може використовуватися для "камери в грі", подальшої обробки і багатьох GFX яких тільки Ви можете собі уявити. + +# Рендер в текстуру + +У нас є три задачі: створити текстуру, в яку будемо малювати, намалювати щось в неї та використати готову намальовану текстуру. + + +## Створення цілі рендеру + +Ми будемо малювати в те, що називається буфером фрейму. Це контейнер для текстур та необов'язкового буферу глибини. Він створюється точнісінько так само, як і будь-який інший об'єкт OpenGL: + +``` cpp +// The framebuffer, which regroups 0, 1, or more textures, and 0 or 1 depth buffer. +GLuint FramebufferName = 0; +glGenFramebuffers(1, &FramebufferName); +glBindFramebuffer(GL_FRAMEBUFFER, FramebufferName); +``` + +Тепер нам потрібно створити текстуру, яка буде містити RGB результат нашого шейдера. Цей код виглядає достатньо класично: + +``` cpp +// Текстура, куди будемо малювати +GLuint renderedTexture; +glGenTextures(1, &renderedTexture); + +// Прив'яжемо новостворену текстуру - все подальші функції, що працюють з текстурами, будуть її використовувати +glBindTexture(GL_TEXTURE_2D, renderedTexture); + +// Дамо OpenGL пусте зображення ( останній "0" ) +glTexImage2D(GL_TEXTURE_2D, 0,GL_RGB, 1024, 768, 0,GL_RGB, GL_UNSIGNED_BYTE, 0); + +// Погана фільтрація. Потрібно! +glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); +glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); +``` + +Також нам потрібен буфер глибини. Це не є обов'язковим, все залежить від того, що насправді потрібно намалювати в текстуру. Але так як ми хочемо намалювати Сюзанну, нам потрібен буфер глибини. + +``` cpp +// Буфер глибини +GLuint depthrenderbuffer; +glGenRenderbuffers(1, &depthrenderbuffer); +glBindRenderbuffer(GL_RENDERBUFFER, depthrenderbuffer); +glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, 1024, 768); +glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthrenderbuffer); +``` + +І в кінці налаштуємо буфер фрейму: + +``` cpp +// Налаштуємо "renderedTexture" як кольоровий <<"attachement">> №0 +glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, renderedTexture, 0); + +// Встановимо буфер для малювання +GLenum DrawBuffers[1] = {GL_COLOR_ATTACHMENT0}; +glDrawBuffers(1, DrawBuffers); // "1" - це розмір DrawBuffers +``` + +Дещо може піти не так на протязі цього процесу, це залежить від можливостей GPU. Ось як можна перевірити це: + +``` cpp +// Завжди перевіряйте, що з нашим буфером фрейму все гаразд +if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) +return false; +``` + +## Рендер в текстуру + +Rendering to the texture is straightforward. Simply bind your framebuffer, and draw your scene as usual. Easy ! + +``` cpp +// Малюємо в фрейм буферу +glBindFramebuffer(GL_FRAMEBUFFER, FramebufferName); +glViewport(0,0,1024,768); // Намалюємо весь буфер фрейму, заповнивши від нижнього лівого края до верхнього правого +``` + +Наш фрагментний шейдер потребує маленької адаптації: + +``` cpp +layout(location = 0) out vec3 color; +``` + +Це значить, що коли відбувається запис в змінну `color`, ми насправді записуємо в ціль для малювання №0, яка є нашою текстурою, тому що `DrawBuffers[0]` це GL_COLOR_ATTACHMENT*i*, що в нашому випадку *renderedTexture*. + +Підсумуємо : + +* *color* буде записано в перший буфер так як `layout(location=0)`. +* Перший буфер є `GL_COLOR_ATTACHMENT0` тому що `DrawBuffers[1] = {GL_COLOR_ATTACHMENT0}` +* GL_COLOR_ATTACHMENT0 має приєднане *renderedTexture* - сюди буде записуватись колір. +* Іншими словами, Ви можете замінити `GL_COLOR_ATTACHMENT0` на `GL_COLOR_ATTACHMENT2` і це все ще буде працювати. + +Зауважте: в OpenGL < 3.3 немає `layout(location=i)`, тому використовуйте `glFragData[i] = mvvalue`. + + +## Використання текстури, в яку відбувся малювання + +Ми збираємося намалювати простий чотирикутник, який заповнить екран. Нам потрібні звичайні буфери, шейдери, ідентифікатори... + +``` cpp +// FBO повноекранного чотирикутника +GLuint quad_VertexArrayID; +glGenVertexArrays(1, &quad_VertexArrayID); +glBindVertexArray(quad_VertexArrayID); + +static const GLfloat g_quad_vertex_buffer_data[] = { + -1.0f, -1.0f, 0.0f, + 1.0f, -1.0f, 0.0f, + -1.0f, 1.0f, 0.0f, + -1.0f, 1.0f, 0.0f, + 1.0f, -1.0f, 0.0f, + 1.0f, 1.0f, 0.0f, +}; + +GLuint quad_vertexbuffer; +glGenBuffers(1, &quad_vertexbuffer); +glBindBuffer(GL_ARRAY_BUFFER, quad_vertexbuffer); +glBufferData(GL_ARRAY_BUFFER, sizeof(g_quad_vertex_buffer_data), g_quad_vertex_buffer_data, GL_STATIC_DRAW); + +// Створимо та скомпілюємо GLSL програму з наших шейдерів +GLuint quad_programID = LoadShaders( "Passthrough.vertexshader", "SimpleTexture.fragmentshader" ); +GLuint texID = glGetUniformLocation(quad_programID, "renderedTexture"); +GLuint timeID = glGetUniformLocation(quad_programID, "time"); +``` + +Тепер Ви хочете намалювати це на екран. Це можна зробити, використовуючи 0 як другий параметр `glBindFramebuffer`. + +``` cpp +// Малюємо на екран +glBindFramebuffer(GL_FRAMEBUFFER, 0); +glViewport(0,0,1024,768); // Намалюємо весь буфер фрейму, з лівого нижнього кута в верхній правий +``` +Ми можемо намалювати наш повноекранний чотирикутник таким шейдером: + +``` glsl +#version 330 core + +in vec2 UV; + +out vec3 color; + +uniform sampler2D renderedTexture; +uniform float time; + +void main(){ + color = texture( renderedTexture, UV + 0.005*vec2( sin(time+1024.0*UV.x),cos(time+768.0*UV.y)) ).xyz; +} +``` +{: .highlightglslfs } + +Цей код просто показує текстуру, але додає невеликий зсув, який залежить від часу. + +# Результат + +![]({{site.baseurl}}/assets/images/tuto-14-render-to-texture/wavvy.png) + +# Йдемо далі + +## Використання глибини + +В деяких випадках Вам може знадобиться глибина, коли Ви малюєте текстуру. В цьому випадку, просто малюйте в текстуру, яку створіть наступним чином: + +``` cpp +glTexImage2D(GL_TEXTURE_2D, 0,GL_DEPTH_COMPONENT24, 1024, 768, 0,GL_DEPTH_COMPONENT, GL_FLOAT, 0); +``` +(24 - це точність в бітах. Ви можете обирати поміж 16, 24 та 32 в залежності від Ваших потреб. 24 зазвичай хороший вибір) + +Цього повинно буди достатньо для Вас, що б почати використовувати, але ось Вам код, який це реалізує. + +Зауважте, даний код може бути трішки повільний, тому що драйвер може не могти використовувати певні оптимізації, такі як [Hi-Z](http://fr.slideshare.net/pjcozzi/z-buffer-optimizations). + +На цьому знімку екрана, рівень глибини штучно "покращено". Зазвичай, це буває дуже складно побачити щоь в текстурі глибин. <<Близька координата Z біля нуля - чорна, дальня координата Z поблизу 1 - біла.>> + +![]({{site.baseurl}}/assets/images/tuto-14-render-to-texture/wavvydepth.png) + + +## Multisampling + +Ви можете записувати в <<мультисемпл>> текстури а не в "базові" - просто потрібно замінити `glTexImage2D` на [glTexImage2DMultisample](http://www.opengl.org/sdk/docs/man3/xhtml/glTexImage2DMultisample.xml) в C++ коді та `sampler2D`/`texture` на `sampler2DMS`/`texelFetch` в фрагментному шейдері. + +Та тут є одне застереження - `texelFetch` потребує інший аргумент, який я кількістью <<семплів|зразків>> для отримання. Іншими словами, нема автоматичної "фільтрації" (коректний термін, коли ми говоримо про <> є ""роздільна здатність"). + +Тому, можливо Вам доведеться вирішити це для мультисемпл текстури самостійно в іншу, не мультисемпл текстуру, використовуючи інший шейдер. + +Нічого складного, просто трішки громіздко. + +## Кілька цілей рендеру + +Ви можете записувати декілька текстур одночасно. + +Просто створіть декілька текстур (всі з правильними та однаковими розмірами !), викличте `glFramebufferTexture` з різними <<кольоровими вкладеннями>>, викличте `glDrawBuffers` з оновленими параметрами (щось виду `(2,{GL_COLOR_ATTACHMENT0,GL_COLOR_ATTACHMENT1}})`) і додайте ще одну змінну в Ваш фрагментний шейдер: + +``` glsl +layout(location = 1) out vec3 normal_tangentspace; // або що завгодно +``` +{: .highlightglslfs } + +Підказка 1: якщо Вам потрібно виводити вектор в текстуру, існують текстури з дійсними числами, з 16 чи 32 бітами точності в замін 8... Дивіться за посиланням [glTexImage2D](http://www.opengl.org/sdk/docs/man/xhtml/glTexImage2D.xml) (шукайте `GL_FLOAT`). + +Підказка 2: для попередньої версії OpenGL використовуйте `glFragData[1] = myvalue`. + + +# Вправи + +* Спробуйте використати `glViewport(0,0,512,768);` замість `glViewport(0,0,1024,768);` (<<спробуйте обидва буфера фрейма та екрана>>) +* Поекспериментуйте з іншими UB координатами в останньому фрагментному шейдері. +* Перетворіть квад використовуючи дійсну матрицю трансформації. Спочатку "захадркодьте", а потом спробуйте використати функції з controls.hpp. Що Ви помітили? + diff --git a/uk/intermediate-tutorials/tutorial-15-lightmaps/index.markdown b/uk/intermediate-tutorials/tutorial-15-lightmaps/index.markdown new file mode 100644 index 000000000..9492b5a0c --- /dev/null +++ b/uk/intermediate-tutorials/tutorial-15-lightmaps/index.markdown @@ -0,0 +1,60 @@ +--- +layout: tutorial +status: publish +published: true +title: 'Туторіал 15 : Світлові карти' +date: '2011-05-28 10:07:24 +0200' +date_gmt: '2011-05-28 10:07:24 +0200' +categories: [tuto] +order: 550 +tags: [] +language: uk +--- + +* TOC +{:toc} + +# Вступ + +Цей туторіал містить тільки відео. Він не вводить ніяких нових методик/синтаксису OpenGL, але показує, як використовувати методи, які Ви вже знаєте, що б побудувати високоякісні тіні. + +Цей туторіал пояснює, як побудувати простий світ в Blender та запекти світлові карти, так що ви зможете використовувати їх в Вашій програмі. + +![]({{site.baseurl}}/assets/images/tuto-15-lightmaps/lighmappedroom.png) + +Ніяких особливих знань про Blender не потрібно. Я все поясню в процесі. + +# Примітка про світлові карти + +Світлові карти запікаються. Раз і назавжди. Це значить, що вони повністю нерухомі, Ви не можете перемістити світло в процессі роботи програми. Чи навіть видалити. + +Це все ще може бути корисним для сонячного світла, чи сцен в приміщенні, де неможливо розбити лампочки. Mirror Edge, випущений в 2009 році, активно використовував їх, для сцен в приміщенні та назовні. + +Що ще цікаво, що їх легко налаштувати і вони мають чудову швидкість малювання. + +# Відео + +Це відео в форматі 1024x768p, використовуйте режим HD... + + + +# Додаток + +Коли ви малюєте це в OpenGL, ви можете помітити деякі дефекти (ось вони тут перебільшені трішки): + +![]({{site.baseurl}}/assets/images/tuto-15-lightmaps/positivebias.png) + +Це пов'язано з міпмаппінгом, який поєднує текселі, які видно на відстані. Чорні пікселі з текстури фону змішуються з хорошими частинами світлової карти. Що б уникнути цього, є декілька способів: + +* Ви можете попросити Blender згенерувати поля навколо меж UV карти. Це параметр "margin" (поля) в секції "bake". Для отримання гарного результату, спробуйте виставити воля до 20 текселів. +* Ви можете використовувати <<упередження|bias>> для текстури: + +``` glsl +color = texture( myTextureSampler, UV, -2.0 ).rgb; +``` +{: .highlightglslfs } + +-2 - це упередження. Можливо, Вам буде потрібно поекспериментувати з цим значенням. На зображенні вище значення було +2, що значить, що OpenGL буде обирати дві міпмапи вище тої, яка повинна була братись (це в 16 разів менше, звідси і проблеми) + +* Ви можете заповнювати фон чорним кольором в кроці післяобробки. Я напишу про це пізніше. + diff --git a/uk/intermediate-tutorials/tutorial-16-shadow-mapping/index.markdown b/uk/intermediate-tutorials/tutorial-16-shadow-mapping/index.markdown new file mode 100644 index 000000000..189306b7b --- /dev/null +++ b/uk/intermediate-tutorials/tutorial-16-shadow-mapping/index.markdown @@ -0,0 +1,466 @@ +--- +layout: tutorial +status: publish +published: true +title: 'Туторіал 16 : Тіні' +date: '2011-08-17 18:29:32 +0200' +date_gmt: '2011-08-17 18:29:32 +0200' +categories: [tuto] +order: 560 +tags: [] +language: uk +--- + +* TOC +{:toc} + +В туторіалі 15 ми вивчили, як створити світлові карти, які охоплюють статичне освітлення. Хоча вони дають гарні тіні, вони не працюють з анімованими моделями. + +Карти тіней - це сучасний (станом на 2016 рік) спосіб створити динамічні тіні. І у них є чудова властивість - їх легко запрограмувати. Погана сторона - це те, що їх страшенно важко заставити працювати *правильно*. + +В цьому туторіалі ми спочатку розглянемо базовий алгоритм, розглянемо його недоліки і потім реалізуємо деякі техніки для кращого результату. З часу їх написання (2012) карти тіней все ще є дуже не вивченою темою, ми дамо Вам деякі рекомендації для подальшого вдосконалення Ваших власних карт тіней, в залежності від Ваших потреб. + +# Основи карти тіней + +Основний алгоритм карти тіней складається з двох кроків. Спочатку, сцена малюється з точки зору світла. Обчислюється тільки глибина кожного фрагмента. Далі, сцена малюється як звичайно, але з додатковою перевіркою, що б побачити чи знаходиться поточний фрагмент в тіні. + +Тест "перебування в тіні" насправді достатньо простий. Якщо поточний зразок знаходить далі від світла, ніж карта тіней в одній і тій же точці, це значить, що сцена містить інший об'єкт, який ближче до світла. Іншими словами поточний фрагмент знаходиться в його тіні. + +Наступне зображення може допомогти Вам зрозуміти цей принцип: + +![]({{site.baseurl}}/assets/images/tuto-16-shadow-mapping/shadowmapping.png) + + +## Малювання карти тіней + +В цьому туторіалі ми будемо розглядати направлене світло - світло, яке знаходиться на великій відстані і промені можна вважати паралельними. Тому, малювання карти тіней здійснюється за допомогою ортографічної проекційної матриці. Ортографічна матриця подібна звичайній перспективній проекційній матриці, за виключенням того, що перспектива не враховується - об'єкт буде виглядати одинаково, не залежно від відстані до камери. + +### Налаштування цілей малювання та матриці MVP + +З туторіалу 14 Ви повинні знати, як намалювати сцену в текстуру для того, що б потом мати доступ до неї з шейдеру. + +Ми будемо використовувати карту тіней в 16 бітній текстурі 1024x1024. 16 біт зазвичай достатньо для карти тіней. Не бійтесь експериментувати з цими значеннями. Зверніть увагу, що ми використовуємо текстуру глибин, а не рендер буфер глибини, <<тому що нам потрібно зробити вибірку пізніше>>. + +``` cpp +// The framebuffer, which regroups 0, 1, or more textures, and 0 or 1 depth buffer. + GLuint FramebufferName = 0; + glGenFramebuffers(1, &FramebufferName); + glBindFramebuffer(GL_FRAMEBUFFER, FramebufferName); + + // Текстура глибин. Повільніше чим буфер глибин, але ви можете робити вибірки пізніше в шейдері + GLuint depthTexture; + glGenTextures(1, &depthTexture); + glBindTexture(GL_TEXTURE_2D, depthTexture); + glTexImage2D(GL_TEXTURE_2D, 0,GL_DEPTH_COMPONENT16, 1024, 1024, 0,GL_DEPTH_COMPONENT, GL_FLOAT, 0); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthTexture, 0); + + glDrawBuffer(GL_NONE); // Не малюємо в буфер кольору також + + // Завжди перевіряємо, що з нашим буфером фрейму все ок + if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + return false; +``` + +MVP матриця що використовується для малювання сцени з точки світла обчислюється наступним чином: + +* Матриця проекції є ортографічною матрицею, яка буде охоплювати все в коробці розмірами (-10,10),(-10,10),(-10,20), сторони якої співпадають з осями X, Y та Z відповідно. Ці значення такі, що б вся видима сцена була завжди видимою, більше про в секції "Йдемо далі". +* Матриця виду повертає світ так, що в просторі камери світло направлене по -Z (можете перечитати [Туторіал 3](/beginners-tutorials/tutorial-3-matrices/) ?) +* Матриця моделі - яка бажаєте. + +``` cpp + glm::vec3 lightInvDir = glm::vec3(0.5f,2,2); + // Обчислимо матрицю MVP з позиції освітлення + glm::mat4 depthProjectionMatrix = glm::ortho(-10,10,-10,10,-10,20); + glm::mat4 depthViewMatrix = glm::lookAt(lightInvDir, glm::vec3(0,0,0), glm::vec3(0,1,0)); + glm::mat4 depthModelMatrix = glm::mat4(1.0); + glm::mat4 depthMVP = depthProjectionMatrix * depthViewMatrix * depthModelMatrix; + + // Відсилаємо нашу трансформацію до поточного шейдеру, + // в "MVP" uniform + glUniformMatrix4fv(depthMatrixID, 1, GL_FALSE, &depthMVP[0][0]) +``` + +### Шейдери + +Шейдери, що використовуються в цьому проході дуже прості. Вершинний шейдер - це просто прохідний шейдер, який обчислює положення вершини в однорідних координатах: + +``` glsl +#version 330 core + +// вхідні данні вершини, різні для всіх запусків цього шейдеру. +layout(location = 0) in vec3 vertexPosition_modelspace; + +// значення, які залишаються постійними для всього меша +uniform mat4 depthMVP; + +void main(){ + gl_Position = depthMVP * vec4(vertexPosition_modelspace,1); +} +``` +{: .highlightglslvs } + +Фрагментний шейдер теж простий - він просто записую глибину фрагмента в location 0 (тобто в нашу текстуру глибини). + +``` glsl +#version 330 core + +// Вихідні данні +layout(location = 0) out float fragmentdepth; + +void main(){ + // Не дуже потрібно, OpenGL зробить це в будь-якому випадку + fragmentdepth = gl_FragCoord.z; +} +``` +{: .highlightglslfs } + +Малювання карти тіней зазвичай в два рази швидше звичайного малювання, тому що записується тільки глибина з низькою точністю, а не глибина та колір одночасно. Пропускна здатність пам'яті зазвичай є перешкодою швидкодії на GPU. + +### Результат + +Вихідна текстура буде виглядати десь так: + +![]({{site.baseurl}}/assets/images/tuto-16-shadow-mapping/DepthTexture.png) + +Темний колір це невелике значення z, отже правий верхній кут стіни знаходиться близько до камери. І навпаки, білий колір значить z=1 (в однорідних координатах), отже це "дуже далеко". + +## Використання карти тіней + + +### Базовий шейдер + +Тепер повернемося до нашого звичайного шейдера. Для кожного фрагменту, який ми обробляємо, ми повинні перевірити, чи знаходиться він "за" картою тіней чи ні. + +Для цього ми повинні розрахувати координати фрагменту *в тому самому просторі, що ми використовували для створення карти тіней*. Отже ми повинні трансформувати їх спочатку за допомогою звичайної MVP матриці, і потім ще за допомогою depthMVP матриці. + +Однак тут є одна маленька хитрість. Множення положення вершини на depthMVP дає однорідні координати, які знаходяться в [-1,1], але текстури повинні бути в діапазоні [0,1]. + +Наприклад, фрагмент в центрі екрану має координати (0,0) в однорідних координатах, але так як це середина текстури, то UV координати будуть (0.5, 0.5). + +Це може бути виправлено налаштуванням координат безпосередньо в фрагментному шейдері, але більш ефективно помножити координати на наступну матрицю, яка просто ділить координати на 2 (діагональ: [-1,1]->[-0.5,0.5]) і потім робить перенос (нижній рядок : [-0.5, 0.5] -> [0,1] ). + +``` cpp +glm::mat4 biasMatrix( +0.5, 0.0, 0.0, 0.0, +0.0, 0.5, 0.0, 0.0, +0.0, 0.0, 0.5, 0.0, +0.5, 0.5, 0.5, 1.0 +); +glm::mat4 depthBiasMVP = biasMatrix*depthMVP; +``` +Тепер ми маємо змогу написати наш вершинний шейдер. Він такий самий, як і раніше, але на виході має дві позиції, а не одну: + +* gl_Position це позиція вершини з точки зору поточної камери +* ShadowCoord це позиція вершини з точки зору "останньої камери" (світла) + +``` glsl +// Вихідна позиція вершини в просторі ==clip== : MVP * position +gl_Position = MVP * vec4(vertexPosition_modelspace,1); + +// тех саме, але матриця освітлення +ShadowCoord = DepthBiasMVP * vec4(vertexPosition_modelspace,1); +``` +{: .highlightglslvs } + +Фрагментний шейдер тепер досить простий: + +* `texture( shadowMap, ShadowCoord.xy ).z` це відстань між світлом та найближчим ==окклюдером/occluder== +* `ShadowCoord.z` це відстань між світом та поточним фрагментом + +... отже, якщо поточний фрагмент далі, чим найближчий окклюдер, це значить що ми в тіні (згаданого вище окклюдера) : + +``` glsl +float visibility = 1.0; +if ( texture( shadowMap, ShadowCoord.xy ).z < ShadowCoord.z){ + visibility = 0.5; +} +``` +{: .highlightglslfs } + +Ми просто використаємо наші знання, що б модифікувати затінення. Звичайно, колір навколишнього середовища не змінюється, оскільки його мета полягає в імітації вхідного освітлення, навіть коли ми знаходимося в тіні (чи навіть, якщо все чисто чорне). + +``` glsl +color = + // Навколишнє освітлення: симулюємо непряме освітлення + MaterialAmbientColor + + // Дифузний колір об'єкта + visibility * MaterialDiffuseColor * LightColor * LightPower * cosTheta+ + // Дзеркальність: відбиття світла подібно дзеркалу + visibility * MaterialSpecularColor * LightColor * LightPower * pow(cosAlpha,5); +``` +{: .highlightglslfs } + +### Результат - тіньові вугрі + +Ось результат нашого коду. Очевидно, що воно правдоподібно, але якість неприйнятна. + +![]({{site.baseurl}}/assets/images/tuto-16-shadow-mapping/1rstTry.png) + +Давайте поглянемо на кожну проблему на цьому зображені. В коді є два проекти shadowmaps та shadowmaps_simple, розпочніть з будь-якого. Другий проект (спрощена версія) така ж страшна, як і зображення вище, але простіша для розуміння. + +# Проблеми + + +## Тіньові вугрі + +Найбільш очевидна проблема називається *тіньові вугрі* (або прищі/акне) + +![]({{site.baseurl}}/assets/images/tuto-16-shadow-mapping/ShadowAcne.png) + +Цей ефект легко пояснити за допомогою простого зображення: + +![]({{site.baseurl}}/assets/images/tuto-16-shadow-mapping/shadow-acne.png) + +Типове "виправлення" для цієї проблеми - додати похибку - ми будемо затіняти тільки якщо поточна глибина фрагменту (пам'ятайте, в просторі світла) знаходиться дуже далеко від значення освітлення. Ми зробимо це за допомогою "коефіцієнту упередженості" (bias, можливо краще "допуску"): + +``` glsl +float bias = 0.005; +float visibility = 1.0; +if ( texture( shadowMap, ShadowCoord.xy ).z < ShadowCoord.z-bias){ + visibility = 0.5; +} +``` +{: .highlightglslfs } + +І результат тепер значно краще : + +![]({{site.baseurl}}/assets/images/tuto-16-shadow-mapping/FixedBias.png) + +Проте, Ви можете помітити, що через "упередження", артефакти між землею і стіною погіршились. Ба більше, коефіцієнт упередження в 0.005 занадто великий для землі (підлоги), але недостатній для вигнутих поверхонь - деякі артефакти залишаються на циліндрі та сфері. + +Загальний підхід - модифікувати коефіцієнт упередження відповідно до нахилу: + +``` glsl +float bias = 0.005*tan(acos(cosTheta)); // cosTheta це dot( n,l ), затиснуте в діапазоні 0 та 1 +bias = clamp(bias, 0,0.01); +``` +{: .highlightglslfs } + +І тепер вугрі пропали, навіть на вигнутих поверхнях. + +![]({{site.baseurl}}/assets/images/tuto-16-shadow-mapping/VariableBias.png) + +Інший трюк, який може працювати (а може і ні, в залежності від Вашої геометрії) це малювання тільки задніх поверхонь на карту тіней. Це вимагає від нас мати спеціальну геометрію (дивіться наступну секцію - "бути Пітером Пеном") з товстими стінами, але вугрі будуть на поверхнях, які в тіні: + +![]({{site.baseurl}}/assets/images/tuto-16-shadow-mapping/shadowmapping-backfaces.png) + +Під час малювання карти тіней, відсікайте передні трикутники: + +``` cpp + // Ми не використовуємо "упередження" в шейдері, а натомість малюємо задні поверхні, + // які вже відділені від передніх поверхонь невеликою відстанню + // (Якщо у Вас відповідна геометрія) + glCullFace(GL_FRONT); // Відсікання передніх трикутників (нормаль направлена до нас) - малювання задніх трикутників +``` + +І коли будите малювати сцену, малюйте нормально (відсікання задніх трикутників) + +``` cpp + glCullFace(GL_BACK); // Відсікання задніх трикутників -> малювання передніх +``` + +Цей метод використовується в коді, на додаток до методу "упередження". + +## Бути Пітером Пеном + +Ми позбулись вугрів, але у нас все ще є невірна тінь біля землі (підлоги), і виглядає так, наче наші стіни "літають" (звідси і Пітер Пен). А додавання "упередження" робить все ще гірше. + +![]({{site.baseurl}}/assets/images/tuto-16-shadow-mapping/PeterPanning.png) + +Це дуже легко виправити - просто не потрібно використовувати тонку геометрію. Це має дві переваги: + +* По перше, це вирішує проблему з "Пітером Пеном" - як тільки Ваша геометрія більш глибша, ніж "упередження", все добре. +* По друге, Ви можете ввімкнути відсікання задньої поверхні коли малюєте світлову карту, тому що тепер тут є полігон стіни, який освітлюється та закриває іншу сторону, яка не буде малюватись з ввімкненим відсіканням задньої поверхні. + +Мінус цього методу в тому, що потрібно малювати більше трикутників (вдвічі більше на кадр!). + +![]({{site.baseurl}}/assets/images/tuto-16-shadow-mapping/NoPeterPanning.png) + + +## Аліасинг (накладання - Aliasing) + +Навіть з цими двома трюками Ви можете помітити, що все ще є накладання (аліасинг) на межі тіні. Іншими словами, після білого пікселю відразу йде чорний без плавного переходу між ними. + +![]({{site.baseurl}}/assets/images/tuto-16-shadow-mapping/Aliasing.png) + + +### PCF + +Найпростіший спосіб покращити це - це мінити семплер карти тіней на *sampler2DShadow*. Як наслідок, коли Ви обробляєте карту тіней, "залізо" буде по факту обробляти і сусідні текселі, робити порівняння їх всіх і повертати дійсне число в діапазоні [0,1], що є білінійною фільтрацією результату порівняння. + +Наприклад, 0.5 означає, що дві вибірки в тіні і дві освітлені. + +Зверніть увагу на те, що це не те ж саме, як одинока вибірка з фільтрованої карти глибин! Порівняння завжди повертає true (істина) чи false(ні). PCF дає інтерполяцію 4 значень true/false. + +![]({{site.baseurl}}/assets/images/tuto-16-shadow-mapping/PCF_1tap.png) + +Як тепер видно, межі тіней стали гладенькі, але текселі карти тіней все ще видні. + +### Вибірка Пуассона + +Найпростіший спосіб це зробити - це робити вибірки з карти тіней N раз, а не один. Використовуючи це разом з PCF, можна отримати дуже гарні результати, навіть при малому N. Ось приклад коду для N=4: + +``` glsl +for (int i=0;i<4;i++){ + if ( texture( shadowMap, ShadowCoord.xy + poissonDisk[i]/700.0 ).z < ShadowCoord.z-bias ){ + visibility-=0.2; + } +} +``` +{: .highlightglslfs } + +`poissonDisk` це масив констант, яки може бути таким: + +``` glsl +vec2 poissonDisk[4] = vec2[]( + vec2( -0.94201624, -0.39906216 ), + vec2( 0.94558609, -0.76890725 ), + vec2( -0.094184101, -0.92938870 ), + vec2( 0.34495938, 0.29387760 ) +); +``` +{: .highlightglslfs } + +В цьому випадку, в залежності від того, як багато вибірок з карти тіней буде, результат може бути більше або менше темним: + +![]({{site.baseurl}}/assets/images/tuto-16-shadow-mapping/SoftShadows.png) + +Константа 700.0 визначає як сильно поширюється вибірка. Якщо трішки, то у Вас знову буде аліасинг. Якщо багато - буде цікавий ефект - бандаж (banding) - дивіться знімок екрана (на цьому знімку PCF спеціально відключений для більш драматичного ефекту, але використовується 16 вибірок). + + +![]({{site.baseurl}}/assets/images/tuto-16-shadow-mapping/SoftShadows_Close.png) + + + + +![]({{site.baseurl}}/assets/images/tuto-16-shadow-mapping/SoftShadows_Wide.png) + + +### Стратифікована вибірка Пуассона + +Ми можемо прибрати цей ефект, якщо обирати різне значення вибірок для кожного пікселя. Є два основних способи - Статифікований Пуассон чи Обернений (Провернутий) Пуассон. Стратифікований обирає різні вибірки, Обернений завжди використовує ті самі, але з випадковим поворотом, тому виглядає по іншому. В цьому туторіалі я розповім тільки про Стратифіковану версію. + +Єдина відмінність з попередньою версією тільки в тому, що береться випадковий індекс в *poissonDisk*: + +``` glsl + for (int i=0;i<4;i++){ + int index = // Випадкове число в діапазоні 0..15, яке різне для кожного пікселя і кожного i + visibility -= 0.2*(1.0-texture( shadowMap, vec3(ShadowCoord.xy + poissonDisk[index]/700.0, (ShadowCoord.z-bias)/ShadowCoord.w) )); + } +``` +{: .highlightglslfs } + +Ми будемо генерувати випадкове число наступним кодом, яке повертає випадкове число в діапазоні `[0;1)`: + +``` glsl + float dot_product = dot(seed4, vec4(12.9898,78.233,45.164,94.673)); + return fract(sin(dot_product) * 43758.5453); +``` +{: .highlightglslfs } + +В нашому випадку `seed4` буде комбінацією `i` (отже ми робимо вибірки з 4 різних місць) і ... ще чогось іншого. Ми можемо використовувати `gl_FragCoord` (позиція пікселя на екрані) або `Position_worldspace`: + +``` glsl + // - Випадкова вибірка на основі позиції пікселя на екрані. + // Немає "бандажу", але тінь переміщується разом з камерою, виглядає дико. + int index = int(16.0*random(gl_FragCoord.xyy, i))%16; + // - Випадкова вибірка на основі позиції пікселя в просторі світу (world space). + // Позиція округлена до міліметрів що б не було великого аліасингу + //int index = int(16.0*random(floor(Position_worldspace.xyz*1000.0), i))%16; +``` +{: .highlightglslfs } + +Це призведе то того, що такі візерунки, як на зображені вище, зникнуть в візуальному шумі. Та гарно зроблений шум буває менш неприємним, ніж такі візерунки. + +![]({{site.baseurl}}/assets/images/tuto-16-shadow-mapping/PCF_stratified_4tap.png) + +Дивіться [tutorial16/ShadowMapping.fragmentshader](https://github.com/opengl-tutorials/ogl/blob/master/tutorial16_shadowmaps/ShadowMapping.fragmentshader) де є три приклади. + +# Йдемо далі + +Навіть з цими всіма трюками є багато інших способів покращити тіні. Ось основні: + +## Дострокове звільнення (Early bailing) + +Замість того, що б брати 16 вибірок для кожного фрагменту (це ж багато), можна взяти 4 вибірки. Якщо вони всі освітелні чи в тіні, то можна припустити, що всі 16 вибірок будуть мати такий же результат. Якщо деякі відрізняються, то можливо ми на межі тіні і все 16 вибірок нам потрібні. + +## Прожектор (spot light) + +Робота з прожектором потребує невеликих змін. Найбільш очевидним є зміна матриці ортогональної проекції в перспективну: + +``` cpp +glm::vec3 lightPos(5, 20, 20); +glm::mat4 depthProjectionMatrix = glm::perspective(glm::radians(45.0f), 1.0f, 2.0f, 50.0f); +glm::mat4 depthViewMatrix = glm::lookAt(lightPos, lightPos-lightInvDir, glm::vec3(0,1,0)); +``` + +те саме, але з перспективний простір камери, а не ортографічний. Використовуйте `texture2Dproj` для врахування перспективного поділу (дивіться виноски в 4 туторіалі про матриці). + +Другим кроком візьмемо до уваги перспективу в шейдері. (дивіться виноски в 4 туторіалі про матриці. Коротко - метриця перспективної проекції насправді не створює ніякої перспективи. Це робиться апаратно шляхом ділення спроектованих координат на `w`. Тут ми емулюємо трансформацію в шейдері, отже маємо поділити самостійно. До речі, ортогональна матриця завжди генерує однорідний вектор з w = 1, тому вона не генерує жодної перспективи). + +Є два способи зробити це в GLSL. Другий спосіб використовує вбудовану функцію `textureProj`, але обидва способи дають однаковий результат. + +``` glsl +if ( texture( shadowMap, (ShadowCoord.xy/ShadowCoord.w) ).z < (ShadowCoord.z-bias)/ShadowCoord.w ) +if ( textureProj( shadowMap, ShadowCoord.xyw ).z < (ShadowCoord.z-bias)/ShadowCoord.w ) +``` +{: .highlightglslfs } + +## Точкове освітлення (point light) + +Те саме, але з кубічними картами глибини. Кубічна карта є набором з 6 текстур, по одній на кожну сторону куба, більш того, вона не доступна по стандартним UV координатам, але з 3D вектором, який в представляє напрямок. + +Глибина зберігається в всіх напрямках в просторі,що робить можливим тіням відкидатись у всіх напрямках відносно точкового освітлення. + +## Комбінація декількох освітлень + +Алгоритм може працювати з декількома джерелами світла, але майте на увазі, що кожне джерело світла вимагає додаткового малювання сцени для створення карти тіней. Це потребує дуже великої кількості пам'яті, коли використовуються тіні і Ви дуже швидко досягнете пропускної здатності "заліза". + +## Автоматичне освітлення зрізаної піраміди камери + +В цьому туторіалі, "піраміда світла" створена вручну, що б містити всю сцену. Хоча це працює в обмеженому прикладі, цього потрібно уникати. Якщо Ваша карта має розмір 1 км на 1 км, то кожний тексель Вашої 1024х1024 карти тіней буде займати 1 квадратний метр, це дуже погано. Проекційна матриця повинна бути максимально щільною. + +Для світильників типу прожектора це легко змінити, налаштувавши їх діапазон. + +Направлене світло, таке як сонце, є більш складним - воно дійсно освітлює всю сцену. Ось спосіб розрахувати "піраміду освітлення": + +1. Потенційні отримувачі світла, або PSR - це об'єкти, які належать до одночасно до однієї піраміди освітлення, піраміди камери та обмеженого простору сцени. Як підказую їх ім'я, ці об'єкти можуть бути затінені - вони видимі для камери і для світла. + +2. Потенційні тіньоутворювачі, або PCF - це Потенційні отримувачі світла та всі об'єкти, що розташовані між ними і світлом (об'єкт може бути невидимим, але все рівно створювати тіть на видимі об'єкти). + +Отже, для розрахутку проективної матриці освітлення, візьміть всі видимі об'єкти, видаліть ті, які дуже далеко та розрахуйте обмежувальну коробку (рамку). Додайте ті об'єкти, що розташовані між цією коробкою та світлом і розрахуйте нову обмежувальну коробку (але на цей раз вирівнену відповідно до напрямку світла). + +Точний розрахунок цих наборів вимагає розрахунку перетину опуклих оболонок, але він дуже легкий для реалізації. + +Цей метод призводить до "вискакування" об'єктів з піраміди, тому що роздільна здатність карти тіней раптово збільшиться. Каскадні карти тіней не мають цієї проблеми, але вони значно складніші в реалізації і Ви все ще можете компенсувати, використовуючи згладжування. + +## Експоненціальні карти тіней + +Експоненціальні карти тіней намагаються обмежити аліасинг (накладання) припускаючи, що фрагмент, який знаходиться в тіні, але поблизу освітленої поверхні насправді "десь посередині". Це пов'язано зі зміщенням, за винятком того, що тест більше не є "бінарним" - фрагмент стає все темнішим і темнішим, коли відстань до освітленої поверхні збільшується. + +Це маленьке шахрайство, насправді, і артефакти будуть з'являтись, коли два об'єкти перекриваються. + +## Light-space perspective Shadow Maps + +LiSPSM (Light-space perspective Shadow Maps - Світло-просторова перспектива карти тіней) - налаштовує проективну матрицю освітлення для отримання більшої точності біля камери. Це дуже важливо в випадку "марної дуелі" - Ви дивитесь в одному напрямку, а освітлення-прожектор - в протилежному. Біля джерела світла точність карти тіней дуже велика, але це далеко від Вас. Біля Вас (тобто камери) точність нижче, але тут вона потрібна більше. + +На щастя, можна використати трюк LiSPM. Дивіться посилання для деталей та реалізації. + +## Каскадні карти тіней + +CSM (каскадні карти тіней) мають справу з такою же проблемою, що і LiSPSM, але в інший спосіб. Вони просто використовують декілька (2-4) стандарті карти тіней, але для різних частин видимої піраміди. Перша карта працює з близькими об'єктами, тут буде гарна роздільна здатність, але для маленької зони. Наступна карта тіней працює для більш далеких об'єктів. Остання карта - для великої частини сцени, але через перспективу, вона не така візуально важлива, як ближчі. + +Каскадні карти тіней мають на час написання цього тексту (2012 рік) найкраще співвідношення складність/якість. Це гарне рішення для багатьох випадків. + +# Висновок + +Як можна побачити, карти тіней - складні штуки. Кожний рік публікуються нові варіації і покращення, і, наразі, ніяке рішення не є бездоганним. + +На щастя, більшість з наведених методів допускають сумісне використання - реально можна змішувати каскадні карти тіней в LiSPSM, згладжене за допомогою PCF. Пробуйте і експериментуйте з цими техніками. + +Як висновок, я пропоную зупинитись на попередньо розрахованих картах освітлення, де це можливо і використовувати карти тіней тільки для динамічних об'єктів. І переконайтесь, що візуальна якість обох методів однакова - це не буде гарно, якщо статичне освітлення бездоганне, а динамічне - погане. \ No newline at end of file diff --git a/uk/intermediate-tutorials/tutorial-17-quaternions/index.markdown b/uk/intermediate-tutorials/tutorial-17-quaternions/index.markdown new file mode 100644 index 000000000..3061c74ee --- /dev/null +++ b/uk/intermediate-tutorials/tutorial-17-quaternions/index.markdown @@ -0,0 +1,345 @@ +--- +layout: tutorial +status: publish +published: true +title: 'Туторіал 17 : Повороти' +date: '2012-08-22 14:12:14 +0200' +date_gmt: '2012-08-22 14:12:14 +0200' +categories: [tuto] +order: 570 +tags: [] +language: uk +--- + +* TOC +{:toc} + +Цей туторіал трішки не про OpenGL, але про дуже загальну проблему - як представити повороти? + +В третьому туторіалі - Матриці, ми вивчили як матриці здатні повертати точку навколо певної осі. Хоча матриці є акуратним способом для перетворення вершин, сама робота з матрицями досить складна - отримання осі обертання з фінальної матриці досить складна задача. + +Ми подивимось на два основних способи представлення поворотів - кути Ейлера та кватерніони. А головне, ми пояснимо, чому Ви повинні віддавати перевагу кватерніонам. + +![]({{site.baseurl}}/assets/images/tuto-17-rotation/tuto17.png) + + +# Передмова: обертання проти орієнтації + +Читаючи статтю про повороти та орієнтацію, Ви можете плутати ці поняття. В цьому туторіалі: + +* Орієнтація - це стан - "Об'єкт має наступну орієнтацію..." +* Поворот - це операція - "Виконаємо поворот об'єкта..." + +Тобто, коли Ви *виконуєте поворот* Ви *змінюєте орієнтацію*. Обидва вирази можуть позначати одне й теж, що може трішки заплутувати. Отже, почнемо... + +# Кути Ейлера + +Кути Ейлера - це найпростіший спосіб уявити орієнтацію. Ви просто зберігаєте значення поворотів навколо осі X, Y та Z. Це дуже проста концепція для розуміння. Ви можете використовувати `vec3`, що б їх зберегти. + + +``` cpp +vec3 EulerAngles( RotationAroundXInRadians, RotationAroundYInRadians, RotationAroundZInRadians); +``` + +Ці три повороти виконуються по черзі, зазвичай в наступному порядку - спочатку Y, потім Z і в кінці X (але це не обов'язково). Використання іншого порядку призведе до інших результатів. + +Одне з найпростіших використань кутів Ейлера - встановлення орієнтації персонажа. Зазвичай, ігровий персонах не крутиться по осям X та Z, тільки вертикально. Тому, це дуже легко написати, розуміти та підтримувати `float direction`, а не вектор з трьох орієнтацій. + +Інший гарний приклад використання кутів Ейлера - FPS камера - у Вас є один кут для напрямку (Y) і один для "вгору-вниз" (X). Дивіться приклад тут [common/controls.cpp](https://github.com/opengl-tutorials/ogl/blob/master/common/controls.cpp) + +Нажаль, коли все стає складнішим, кути Ейлера погано справляються з роботою. Наприклад: + +* Плавне переміщення між 2 орієнтаціями дуже складний. Якщо просто наївно робити інтерполяцію кутів по осями X,Y та Z, результат може бути дуже дивним. +* Виконання декількох поворотів одночасно досить складне та неточне - Вам потрібно розрахувати кінцеву матрицю повороту та вгадати кути з неї. +* Одна з відомих проблем - "Блокування обертання" - "Gimbal Lock" - який може блокувати Ваші повороти або просто перевернуть Вашу модель. +* Різні кути будуть давати однакові повороти (наприклад, -180° та 180°). +* І, як було сказано вище, зазвичай правильний порядок поворотів - YZX, але можуть бути бібліотеки, які використовують інший порядок і у Вас будуть проблеми. +* Деякі операцію дуже складні. Наприклад, поворот на N градусів в обраному напрямку. + +Кватерніони допомагають вирішити всі ці проблеми. + +# Кватерніони + +Кватерніон це масив з 4 чисел, [x y z w] які мають наступні значення: + +``` cpp +// RotationAngle в радіанах, так як sin/cos приймає радіани +x = RotationAxis.x * sin(RotationAngle / 2) +y = RotationAxis.y * sin(RotationAngle / 2) +z = RotationAxis.z * sin(RotationAngle / 2) +w = cos(RotationAngle / 2) +``` + +`RotationAxis` це, як говорить ім'я, осі, навколо яких будуть повороти. + +`RotationAngle` це кут, на який буде поворот. + +![]({{site.baseurl}}/assets/images/tuto-17-rotation/quaternion.png) + +Отже, по суті, кватерніони зберігають *вісь обертання* та *кут повороту* таким чином, що полегшує комбінування обертань. + +## Читання кватерніонів + +Цей формат точно менш інтуїтивний, ніж кути Ейлера, але все ще можна здогадатись - компонента xyz приблизно відповідає осям повороту, а w - це косинус кута повороту, поділений на 2. Наприклад, уявімо, що ви бачите таке значення - [ 0.7 0 0 0.7 ]. x=0.7, точно більше, ніх y та z, отже поворот відбувається в основному навколо осі X. 2*acos(0.7) = 1.59 радіан, це власне кут повороту, приблизно рівний 90°. + +Аналогічно, [0 0 0 1] (w=1) означає, що кут рівний 2*acos(1) = 0, отже це "одиничний кватерніон", який нічого не повертає. + +## Базові операції + +Знання математики, що стоїть за кватерніонами, рідко буває корисним - їх представлення настільки не інтуітивне, що Ви будете зазвичай покладатись на допоміжні функції, які зроблять всю математику. Якщо ж Вам цікаво, подивіться сторінку з математичними книгами [Корисні інструменти та посилання]({{site.baseurl}}/uk/miscellaneous/useful-tools-links/) + +### Як створити кватерніон в с++? + +``` cpp +// не забудьте про #include and + +// Створити одиничний кватерніон (нема повороту) +quat MyQuaternion; + +// Пряма передача всіх чотирьох компонентів +// Навряд чи Ви це будете використовувати +MyQuaternion = quat(w,x,y,z); + +// Конвертування з кутів Ейлера (в радіанах?) в кватерніон +vec3 EulerAngles(90, 45, 0); +MyQuaternion = quat(EulerAngles); + +// Перетворення з осей-кутів +// В GLM кути мають бути в градусах, отже, конвертуємо +MyQuaternion = gtx::quaternion::angleAxis(degrees(RotationAngle), RotationAxis); +``` + +### Як створити кватерніон в GLSL ? + +Ніяк. Конвертуйте Ваш кватерніон в матрицю повороту і використовуйте в Матриці Моделі. Ваші вершини будуть крутитись як і раніше, за допомогою MVP матриці. + +В деяких випадках, Вам може дійсно захотітись використовувати кватерніони в GLSL, наприклад, для скелетної анімації на GPU. В GLSL немає спеціального типу для кватерніонів, але Ви можете використовувати vec4 і виконувати всю математику самостійно. + +### Як конвертувати кватерніон в матрицю ? + +``` cpp +mat4 RotationMatrix = quaternion::toMat4(quaternion); +``` + +І тепер можете використовувати для отримання матриці Моделі як і раніше: + +``` cpp +mat4 RotationMatrix = quaternion::toMat4(quaternion); +... +mat4 ModelMatrix = TranslationMatrix * RotationMatrix * ScaleMatrix; +// Тепер Ви можете використовувати ModelMatrix для отримання MVP матриці +``` + +# Отже, що обрати: кути Ейлера чи кватерніони? + +Вибір між кутами Ейлера та кватерніонами є складним. Кути Ейлера є інтуітивно зрозумілими для дизайнерів, якщо ви пишете якийсь 3D редактор - використовуйте їх. А кватерніони зручні для програмістів і досить швидкі, отже їх варто використовувати в ядрі 3D рушія. + +Загальний консенсус саме такий - кватерніони всередині, кути Ейлера назовні, в інтерфейсі користувача. + +Ви зможете впоратись з всім, що вам потрібно (як мінімум, це буде не складно) і Ви все ще зможете використовувати кути Ейлера для сутностей, які потребують цього (як було сказано вище - камера, гуманоїди і подібне) і робити це з невеликими перетвореннями. + + +# Інші ресурси + +* Книги [Useful Tools & Links]({{site.baseurl}}/uk/miscellaneous/useful-tools-links/) ! +* Настільки старе, наскільки може бути, Game Programming Gems 1 має декілька чудових статей про кватерніони. Можливо, Ви зможете знайти їх онлайн. +* [Презентація GDC](http://www.essentialmath.com/GDC2012/GDC2012_JMV_Rotations.pdf) про повороти +* Ogre3D [FAQ про кватерніони](http://www.ogre3d.org/tikiwiki/Quaternion+and+Rotation+Primer). Друга половина в основному специфічна для ogre, між тим. +* Ogre3D [Vector3D.h](https://bitbucket.org/sinbad/ogre/src/3cbd67467fab3fef44d1b32bc42ccf4fb1ccfdd0/OgreMain/include/OgreVector3.h?at=default) і [Quaternion.cpp](https://bitbucket.org/sinbad/ogre/src/3cbd67467fab3fef44d1b32bc42ccf4fb1ccfdd0/OgreMain/src/OgreQuaternion.cpp?at=default) + + +# Шпаргалки + +## Як дізнатись, що два кватерніони подібні? + +Для векторів скалярний добуток дає косинус кута між ними. Якщо воно рівне 1, вони в одному напрямку. + +З кватерніонами те ж саме: + +``` cpp +float matching = quaternion::dot(q1, q2); +if ( abs(matching-1.0) < 0.001 ){ + // q1 і q2 подібні +} +``` + +Ви також можете отримати кут між q1 та q2, використавши арккосинус для скалярного добутку + +## Як повернути точку? + +Робіть наступне: + +``` cpp +rotated_point = orientation_quaternion * point; +``` + +... але якщо Ви хочете обчислити матрицю моделі, можливо краще конвертувати в матрицю. + +Зверніть увагу, що центр обертання завжди знаходиться в центрі координат. Якщо ж Ви хочете обертати навколо іншої точки: + +``` cpp +rotated_point = origin + (orientation_quaternion * (point-origin)); +``` + +## Як зробити інтерполяцію між двома кватерніонами? + +Це називається SLERP - Spherical Linear intERPolation - сферична лінійна інтерполяція. З GLM ви можете зробити це за допомогою функції `mix`: + +``` cpp +glm::quat interpolatedquat = quaternion::mix(quat1, quat2, 0.5f); // чи інший множник в діапазоні 0..1 +``` + +## Як накопичити два повороти? + +Це легко! Просто перемножте два кватерніони. Порядок такий самий, як і для матриць - зворотній: + +``` cpp +quat combined_rotation = second_rotation * first_rotation; +``` + +## Як знайти поворот між двома векторами? + +(іншими словами - знайти кватерніон, який потрібний для того, що б так повернути v1, що б він став v2) + +Ідея дуже проста: + +* Кут між цими двома векторами дуже легко знайти - скалярний добуток є косинусом цього кута. +* Потрібні осі теж легко знайти - це векторний добуток цих векторів. + +Наступний код робить саме це, але ще враховує декілька специфічний випадків: + +``` cpp +quat RotationBetweenVectors(vec3 start, vec3 dest){ + start = normalize(start); + dest = normalize(dest); + + float cosTheta = dot(start, dest); + vec3 rotationAxis; + + if (cosTheta < -1 + 0.001f){ + // Спеціальний випадок - вектори направлені в протилежному напрямку: + // тут немає ідеальної осі для повороту + // отже, оберемо будь-яку, яка перпендикулярна до початкової + rotationAxis = cross(vec3(0.0f, 0.0f, 1.0f), start); + if (gtx::norm::length2(rotationAxis) < 0.01 ) // Навдача, вони паралельні, спробуємо ще раз! + rotationAxis = cross(vec3(1.0f, 0.0f, 0.0f), start); + + rotationAxis = normalize(rotationAxis); + return gtx::quaternion::angleAxis(glm::radians(180.0f), rotationAxis); + } + + rotationAxis = cross(start, dest); + + float s = sqrt( (1+cosTheta)*2 ); + float invs = 1 / s; + + return quat( + s * 0.5f, + rotationAxis.x * invs, + rotationAxis.y * invs, + rotationAxis.z * invs + ); + +} +``` + +(Ви можете знайти цю функцію тут [common/quaternion_utils.cpp](https://github.com/opengl-tutorials/ogl/blob/master/common/quaternion_utils.cpp)) + +## Мені потрібен аналог функції gluLookAt. Як мені зорієнтувати об'єкт в напрямку точки? + +Використовуйте `RotationBetweenVectors` ! + +``` cpp +// Знайти поворот між передньою частиною об'єкта (ми допускаємо, що вісь Z направлена до нас +// але це залежить від Вашої моделі) і бажаним напрямком +quat rot1 = RotationBetweenVectors(vec3(0.0f, 0.0f, 1.0f), direction); +``` + +Now, you might also want to force your object to be upright: + +``` cpp +// Заново розрахуйте desiredUp так що воно буде перпендикулярно до напрямку +// Ви можете пропустити цю частину, якщо Ви дійсно бажаєте змусити desiredUp +vec3 right = cross(direction, desiredUp); +desiredUp = cross(right, direction); + +// Тому що через перший поворот, напрямок "вгору" може бути "зіпсованим" +// Знайдемо поворот між "вгору" об'єкта, який повертається і бажаного "вгору" +vec3 newUp = rot1 * vec3(0.0f, 1.0f, 0.0f); +quat rot2 = RotationBetweenVectors(newUp, desiredUp); +``` + +Тепер об'єднаємо їх: + +``` cpp +quat targetOrientation = rot2 * rot1; // пам'ятаєте, зворотній порядок. +``` + +Будьте обережні, "напрямок" це напрямок, а не бажана позиція! Але її легко розрахувати - `targetPos - currentPos`. + +Як тільки ви отримали цільову орієнтацію, ми можете захотіти інтерполювати орієнтацію між `startOrientation` і `targetOrientation`. + +(Ви можете знайти цю функцію в `common/quaternion_utils.cpp`) + +## Як використовувати `LookAt`, але обмежити поворот певною швидкістю? + +Основна ідея - використовувати SLERP ( = `use glm::mix` ), але погратись з значенням інтерполяції, що б кут не був більшим, ніж бажане значення: + +``` cpp +float mixFactor = maxAllowedAngle / angleBetweenQuaternions; +quat result = glm::gtc::quaternion::mix(q1, q2, mixFactor); +``` + +Ось практично вся реалізація, яка працює з більшістю спеціальних випадків. Зверніть увагу, що тут немає функції `mix()` для оптимізації. + +``` cpp +quat RotateTowards(quat q1, quat q2, float maxAngle){ + + if( maxAngle < 0.001f ){ + // Не потрібно робити поворот, що б не було ділення на нуль. + return q1; + } + + float cosTheta = dot(q1, q2); + + // q1 і q2 дуже подібні + // Форсуємо q2 для впевненості + if(cosTheta > 0.9999f){ + return q2; + } + + // Не будемо обирати "довгий шлях" через всю сферу + if (cosTheta < 0){ + q1 = q1*-1.0f; + cosTheta *= -1.0f; + } + + float angle = acos(cosTheta); + + // Якщо тут всього 2° різниці, а ми дозволяємо 5°, + // тоді ми нічого не робимо. + if (angle < maxAngle){ + return q2; + } + + float fT = maxAngle / angle; + angle = maxAngle; + + quat res = (sin((1.0f - fT) * angle) * q1 + sin(fT * angle) * q2) / sin(angle); + res = normalize(res); + return res; + +} +``` + +Використовувати наступним чином: + +``` cpp +CurrentOrientation = RotateTowards(CurrentOrientation, TargetOrientation, 3.14f * deltaTime ); +``` + +(Ви можете знайти цю функцію в common/quaternion_utils.cpp) + +## Як... + +Якщо Ви не можете чогось зрозуміти, напишіть нам лист, і ми додамо Ваше питання до списку! diff --git a/uk/intermediate-tutorials/tutorial-9-vbo-indexing/index.markdown b/uk/intermediate-tutorials/tutorial-9-vbo-indexing/index.markdown new file mode 100644 index 000000000..e2e4f10e7 --- /dev/null +++ b/uk/intermediate-tutorials/tutorial-9-vbo-indexing/index.markdown @@ -0,0 +1,96 @@ +--- +layout: tutorial +status: publish +published: true +title: 'Tutorial 9 : VBO Indexing' +date: '2011-05-12 19:21:49 +0200' +date_gmt: '2011-05-12 19:21:49 +0200' +categories: [tuto] +order: 490 +tags: [] +language: uk +--- + +* TOC +{:toc} + +# Принцип індексації + +До цих пір, коли ми будували VBO, ми завжди дублювали наші вершини коли два трикутника мають спільну вершину. + +В цьому туторіалі, ми введемо поняття індексації, що дозволить нам використовувати ті самі вершини повторно. Це буде досягнуто за допомогою *індексного буфера* (*index buffer*). + +![]({{site.baseurl}}/assets/images/tuto-9-vbo-indexing/indexing1.png) + +Індексний буфер містить числа, по три для кожного трикутника в меші, які посилаються на різноманітні *буфери атрибутів* (позиція, колір, UV координати, інші UV координати, нормалі ...). Це схоже на формат файлу OBJ, тільки з однією великою різницею: є тільки один буфер індексу. Це значить що для вершин, що є спільними для двох трикутників, всі атрибути мають бути однаковими. + +# Shared vs Separate + +Давайте розглянемо на прикладі нормалей. В цій фігурі, артист, який створив ці два трикутника, напевне хотів, що б вони представляли гладку поверхню. Ми можемо об'єднати ці нормалі двох трикутників в одну вершинну нормаль. Для візуалізації цього, я додав червону лінію, яка показую напрямок гладкої поверхні. + +![]({{site.baseurl}}/assets/images/tuto-9-vbo-indexing/goodsmooth.png) + +В другому випадку, артист мав на увазі "шов", грубий край. Але якщо ми об'єднаємо ці нормалі, це буде значити, що шейдер зробить інтерполяцію і створить гладкий напрямок, десь такий: + +![]({{site.baseurl}}/assets/images/tuto-9-vbo-indexing/badmooth.png) + +І цьому випадку насправді краще мати два набори нормалей, по одній на кожну вершину. Єдиний спосіб зробити це в OpenGL - здублювати вершину повністю з всіма атрибутами. + +![]({{site.baseurl}}/assets/images/tuto-9-vbo-indexing/spiky.png) + + +# Індексація VBO в OpenGL + +Використання індексів є дуже простим. По перше, необхідно створити додатковий буфер, який наповнити правильними індексами. Код для цього потрібен такий самий як і раніше, але тепер потрібен ELEMENT_ARRAY_BUFFER, а не ARRAY_BUFFER. + +``` cpp +std::vector indices; + +// заповнимо індекси + +// Згенеруємо буфер для індексів + GLuint elementbuffer; + glGenBuffers(1, &elementbuffer); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementbuffer); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW); +``` + +та намалюємо меш, просто замінивши glDrawArrays наступним : + +``` cpp +// буфер індексів + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementbuffer); + + // малюємо трикутник ! + glDrawElements( + GL_TRIANGLES, // режим + indices.size(), // кількість + GL_UNSIGNED_INT, // тип + (void*)0 // індекс початку даних в масиві + ); +``` + +(швидка примітка: краще використовувати "unsigned short" ніж "unsigned int", тому що такий масив займає менше пам'яті і швидше працює) + +# Заповнення буферного індексу + +Тепер у нас є проблема. Як було сказано раніше, OpenGL може використовувати тільки один буфер індексу, де OBJ (і деякі інші популярні 3D формати, наприклад Collada) використовують один буфер для *атрибута*. Це значить, що нам потрібно якимось чином перетворити з N буферів індексу в один. + +Алгоритм для реалізації цього такий: +``` +Для кожної вхідної вершини + Спробувати знайти подібну ( = з такими же атрибутами) вершину в тих, що вже вивели + Якщо знайшли: + Подібна вершина вже в VBO, використовуємо її! + Якщо не знайшли: + Додамо її до VBO +``` +Реальний с++ код можна знайти в common/vboindexer.cpp. Він містить достатньо багато коментарів, тому, якщо вам зрозумілий алгоритм вище, то і код буде зрозумілим. + +Критерій подібності є те, що позиція вершин, UV і нормалей повинна буди однаковою. Ви можете адаптувати це якщо ви маєте більше атрибутів. + +Пошук подібних вершин виконано простим лінійним алгоритмом для простоти. Вибір `std::map` може бути кращим для реального коду. + +# Додатково : лічильник кадрів (FPS counter) + +Це не відноситься безпосередньо до індексація, але це гарний момент що б подивитись на [лічильник FPS]({{site.baseurl}}/uk/miscellaneous/an-fps-counter/), тому що це допоможе побачити покращення від роботи індексації. Інші інструменті для налагодження <<перформанса>> [Tools - Debuggers]({{site.baseurl}}/uk/miscellaneous/useful-tools-links/#debugging-tools). diff --git a/uk/miscellaneous/an-fps-counter/index.markdown b/uk/miscellaneous/an-fps-counter/index.markdown new file mode 100644 index 000000000..ab59ae3ef --- /dev/null +++ b/uk/miscellaneous/an-fps-counter/index.markdown @@ -0,0 +1,46 @@ +--- +layout: page +status: publish +published: true +title: Лічильник FPS +date: '2012-01-28 16:24:41 +0100' +date_gmt: '2012-01-28 16:24:41 +0100' +categories: [] +order: 40 +tags: [] +print: true +language: uk +--- + +Коли розробляється графіка реального часу, дуже важливо пильнувати продуктивність. Хорошою ідеєю буде вибрати якийсь FPS (зазвичай 30 чи 60) і робити все, щоб дотримуватись її. + +Лічильник FPS зазвичай виглядає так : + +``` cpp + double lastTime = glfwGetTime(); + int nbFrames = 0; + + do{ + + // Вимірюємо швидкість + double currentTime = glfwGetTime(); + nbFrames++; + if ( currentTime - lastTime >= 1.0 ){ // якщо останній prinf() був більше 1 секунди тому + // printf і скидаємо таймер + printf("%f мс/кадру\n", 1000.0/double(nbFrames)); + nbFrames = 0; + lastTime += 1.0; + } + + ... інший код в головному циклі +``` + +В цьому коді є одна дивна річ. Цей код виводить час в мілісекундах, який потрібен для обробки одного кадру (в середньому за секунду), а не скільки кадрів було намальовано за останню секунду. + +Це в дійсності **значно краще**. Не покладайтесь на FPS. Ніколи. КадриВСекунду = 1/СекундНаКадр, це просто зворотня залежність и ми люди погано розуміємо подібні відношення. Давайте розглянемо приклад. + +Ви написали чудову функцію малювання, що виконує 1000 кадрів в секунду (1 мс на кадр). Але ви забули про маленький шейдер, що робить невеличкі розрахунки і це додає 0.1 мс. І ой, 1/(0.001 + 0.0001) = 900. Ви просто втрачаєте 100 кадрів в секунду. Мораль: **Ніколи не використовуйте FPS для аналізу продуктивності**. + +Якщо Ви хочете отримати 60fps, то Ваша ціль буде 16.6666мс, якщо Ви хочете 30fps, тоді Ваша ціль 33.3333мс. Це все, що Вам потрібно знати. + +Цей код доступний в всіх туторіалах, починаючи з [Туторіал 9 : VBO індекси]({{site.baseurl}}/intermediate-tutorials/tutorial-9-vbo-indexing/); дивіться [tutorial09_vbo_indexing/tutorial09.cpp](https://github.com/opengl-tutorials/ogl/blob/master/tutorial09_vbo_indexing/tutorial09.cpp#L142) . Інші інструменти вимірювання продуктивності [Інструменти - Відлагоджувач]({{site.baseurl}}/uk/miscellaneous/useful-tools-links/#debugging-tools). diff --git a/uk/miscellaneous/building-your-own-c-application/index.markdown b/uk/miscellaneous/building-your-own-c-application/index.markdown new file mode 100644 index 000000000..f640037b1 --- /dev/null +++ b/uk/miscellaneous/building-your-own-c-application/index.markdown @@ -0,0 +1,598 @@ +--- +layout: page +status: publish +published: true +title: Збирання Вашої власної програми на Сі +date: '2012-10-06 14:03:21 +0200' +date_gmt: '2012-10-06 14:03:21 +0200' +categories: [] +order: 50 +tags: [] +print: true +language: uk +--- + +Багато зусиль було вкладено в цей туторіал, що б він компілювався і запускався як можна простіше. На жаль, це також означає, що CMakes приховує те, як це зробити в власній програмі. + +Цей туторіал пояснить як побудувати Вашу власну програму на сі з нуля. Але для початку деякі базові знання про роботу компілятора. + +Будь-ласка, не пропускайте перші дві секції. Якщо Ви вирішили читати цей туторіал, Вам напевне це потрібно знати. + +# Модель Сі програми + + +## Препроцесор + +Це все про `#defines` і `#includes` і що вони роблять. + +Сі препроцесинг це простий процес - вирізати і вставити. + +Коли препроцесор бачить наступний файл MyCode.c : + +``` cpp +#include "MyHeader.h" + +void main(){ + FunctionDefinedInHeader(); +} +``` + +, він просто відкриває файл MyHeader.h, і вставляє його вміст в MyCode.c : + +``` c +// початок MyCode.c +// початок MyHeader.h +#ifndef MYHEADER_H +#define MYHEADER_H + +void FunctionDefinedInHeader(); // об'явлення функції + +# endif +// кінець MyHeader.h + +void main(){ + FunctionDefinedInHeader(); // використання +} + +// кінець MyCode +``` + +Аналогічно, `#define` також "вирізати і вставити", `#if` також аналізується і, за умови, видаляється, і тому подібне. + +В кінці цього всього ми маємо препроцесований файл без усіх `#define`, `#if`, `#ifdef`, `#include` і готовий до компіляції. + +Ось для прикладу `main.cpp` файл з 6 туторіалу, повністю препроцесований в Студії - [tutorial06_preprocessed]({{site.baseurl}}/assets/images/build-own-app/tutorial06_preprocessed.txt). Будьте уважні, це дуже великий текст! Але це дуже важливо розуміти, як виглядає cpp файл при компіляції. + +## Компіляція + +Компілятор переводить с++ код в такий вигляд, який процесор безпосередньо розуміє. Наприклад, наступний код: + +``` cpp +int i=3; +int j=4*i+2; +``` + +буде перетворений в наступні x86 інструкції. + +``` +mov dword ptr [i],3 +mov eax,dword ptr [i] +lea ecx,[eax*4+2] +mov dword ptr [j],ecx +``` + +Кожний .cpp файл компілюється окремо і результат (бінарний код) записується в .o/.obj файли. + +![]({{site.baseurl}}/assets/images/build-own-app/compilation.png) + +Зверніть увагу, у нас ще немає готової програми - залишився один крок. + +## Лінковка (зв'язування) + +Лінковщик бере всі бінарні файли (Ваші та від сторонніх бібліотек) і генерує готовий до запуску файл (програму). Пара приміток: + +* Бібліотеки мають розширення `.lib`. +* Деякі бібліотеки *статичні". Це значить, що файл бібліотеки .lib містить весь x86 код, який потрібен. +* Деякі бібліотеки *динамічні* (також відомі як "спільні" ). Це значить, що .lib не містить всього коду, він просто каже "я обіцяю, що що функії `Foo`, `Bar` та `WhatsNot` будуть доступні під час виконання. +(від перекладача - тут трішки щось наплутали, але я залишив як є. Насправді воно так - якщо Ваша програма використовує статичну бібліотеку, то весь код цієї бібліотеки додається до файлу Вашої програми. Якщо Ваша програма використовує динамічну бібліотеку, то при лінковці вона не потрібна, а ось при запуску програми файл бібліотеки буде знайдено і завантажено для використання програмою.) + +Після того, як лінкощик зробить свою роботу, ви отримаєте файл з програмою (.exe в випадку Windows, a.out чи без розширення взагалі на Linux/Unix): + +![]({{site.baseurl}}/assets/images/build-own-app/linking.png) + + +## Час виконання (Runtime) + +Коли Ви запускаєте програму, операційна система відкриває файл і копіює x86 код в пам'ять. Як було сказано вище, не весь код доступний в цей момент - код з динамічних бібліотек. Але лінковщик залишає певну інформацію для пошуку. Наприклад, функцію `glClearColor` потрібно шукати в OpenGL32.dll. + +![]({{site.baseurl}}/assets/images/build-own-app/dynamiclinking.png) + +Windows знайде відповідну dll і в ній функцію `glClearColor`: + +![]({{site.baseurl}}/assets/images/build-own-app/depends.png) + +Іноді операційна система не може знайти бібліотеку, можливо в процесі встановлення щось пішло не так (вона не скопіювалась або скопіювалась інша) і в такому випадку програма не запуститься. + +![]({{site.baseurl}}/assets/images/build-own-app/dynamiclinking.png) + + +# Як мені зробити операцію X в IDE Y? + +Інструкції щодо побудови OpenGL програм розділені на наступні прості операції з наступної причини: + +* Вам потрібно буде постійно виконувати їх, тому краще їх знати +* Вам потрібно знати, що відноситься до OpenGL, а що ні + + +## Visual Studio + + +### Створення нового проекту + +File -> New -> Project -> Empty project. Не використовуйте дивних майстрів (wizard). Не використовуйте те, чого не знаєте (вимкніть MFC, ATL, прикомпільовані заголовки, stdafx, файл main). + +### Додавання джерельних файлів в проект + +Правий клік по Source Files -> Add new. + +### Додавання каталогів на включення в проект + +Правий клік по назві проекту -> Project Properties -> C++ -> General -> Additional include directories. Тут насправді випадаючий список, його можна легко модифікувати. + +### Лінковка з бібліотекою + +Правий клік по назві проекту -> Project Properties -> Linker -> Input -> Additional dependencies : впишіть ім'я бібліотеки .lib. Наприклад : opengl32.lib + +В Project Properties -> Linker -> General -> Additional library directories, переконайтесь, що вказаний правильний шлях до бібліотеки. + +### Збирання, запуск і налагодження + +Встановіть робочу діректорію (папку) туди, де зберігаються Ваші текстури та шейдери - Project Properties -> Debugging -> Working directory + +Запуск - Shift-F5, але Вам зазвичай це не знадобиться - *Налагоджуйте* - F5. + +Короткий список гарячих кнопок: + +* F9 на певному рядку або клікнути зліва біля номеру рядка - встановити точку зупинки, З'явиться червона крапка. +* F10 - виконати один рядок коду +* F11 - виконати один рядок коду, але якщо це функція - "зайти в середину" +* Shift-F11 - виконувати функцію до виходу з неї + +Також є багато допоміжних вікон для перегляду змінних, поточного стеку, потоків... + +## QtCreator + +QtCreator доступний безкоштовно тут [http://qt-project.org/](http://qt-project.org/). + +### Створення нового проекту + +Використовуйте чистий C чи C++ проект; не використовуйте шаблон з Qt кодом. + +![]({{site.baseurl}}/assets/images/build-own-app/QtCreator_newproject.png) + + +Використовуйте налаштування за замовчуванням. + +### Додавання джерельних файлів в проект + +Використовуйте GUI, або просто додайте файл в .pro : + +``` Makefile +SOURCES += main.cpp \ + other.cpp \ + foo.cpp +``` + +### Додавання каталогів на включення в проект + +В .pro файлі : + +``` +INCLUDEPATH += <потрібний каталог> \ + <інші каталоги> +``` + +### Лінковка з бібліотекою + +Правий клік по project -> Add library + +* Якщо Ви використовуєете Лінукс і встановили бібліотеку через apt-get чи подібні інструменти, існує вірогідність, що бібліотека буде "зареєстрована" в системі. Ви можете обрати "System package" і ввести ім'я бібліотеки ( наприклад : *libglfw* чи *glew* ) + +![]({{site.baseurl}}/assets/images/build-own-app/QtCreator_linking.png) + + +* Якщо ні, то використовуйте "System Library". Вкажіть шлях до бібліотеки. + + +### Збирання, запуск і налагодження + +Збирання : Ctrl-B, або натиснути на іконку молотка внизу зліва. + +Запуск : зелена стрілка. Встановити аргументи програми і робочий каталог можна в Projects -> Run Settings + +Налагодження : + +* Встановити точку зупинки : Клікніть мишкою зліва від номеру рядка. З'явиться червона крапка. +* F10 : виконати поточний рядок +* F11 : виконати поточний рядок, але якщо це інша функція - зайти в неї ("step into") +* Shift-F11 : виконувати до кінця функції ("step out") + +Також є велика кількість вікон налагодження - перегляд змінних, стек викликів, потоки ... + +## XCode + +В процесі... + +### Створення нового проекту + + +### Додавання джерельних файлів в проект + + +### Додавання каталогів на включення в проект + + +### Лінковка з бібліотекою + + +### Збирання, запуск і налагодження + + +## CMake + +CMake вміє створювати проекти для більшості систем збирання - Visual, QtCreator, XCode, make, Code::Blocks, Eclipse і подібне і на різні операційні системи. Це звільняє Вас від підтримки різний файлів проектів. + +### Створення нового проекту + +Створіть файл CMakeLists.txt file і запишіть там наступне (модифікуйте за необхідності) : + +``` +cmake_minimum_required (VERSION 2.6) +project (your_project_name) + +find_package(OpenGL REQUIRED) + +add_executable(your_exe_name + tutorial04_colored_cube/tutorial04.cpp + common/shader.cpp + common/shader.hpp +) +``` + +Запустіть CMake GUI, знайдіть Ваш CMakeLists.txt і вкажіть каталог, куди будуть зберігатися файли при збиранні проекту. Натисніть Configure (конфігурувати), потім Generate (генерувати). Ваш проект/рішення буде створено в вказаному вище каталозі. + +### Додавання джерельних файлів в проект + +Просто додайте ім'я файлу в параметри команди add_executable. + +### Додавання каталогів на включення в проект + +``` +include_directories( + external/AntTweakBar-1.15/include/ + external/glfw-2.7.2/include/ + external/glm-0.9.1/ + external/glew-1.5.8/include/ + . +) +``` + +### Лінковка з бібліотекою + +``` +set(ALL_LIBS + ${OPENGL_LIBRARY} + GLFW_272 + GLEW_158 + ANTTWEAKBAR_151_OGLCORE_GLFW +) + +target_link_libraries(tutorial01_first_window + ${ALL_LIBS} +) +``` + +### Збирання, запуск і налагодження + +CMake не будує проекти, він їх створює. Використовуйте відповідну IDE чи інструмент. + +(від перекладача - насправді, CMake вже вміє запускати відповідну систему збірки. Для тих, хто не боїться консолі: + +``` bash +mkdir build # створимо каталог, де будемо збирати, ім'я можна обрати на власний розсуд +cd build # перейдемо в нього +cmake .. # запустимо конфігурування +cmake --build . # запустимо збирання проекту +``` +) + +## make + +Буль ласка, не використовуйте його. (від перекладача - можна його використовувати, як по мені, то для маленьких проектів на Linux - саме воно, але це дуже специфічний інструмент) + +## gcc + +Це може бути дуже корисно, скомпілювати невеликий проект "ручками", що б краще розуміти деталі процесу. Але краще не робіть це в реальних проектах... + +Це все можна зробити і Windows, використовуючи mingw. + +Скомпілюємо кожний .cpp файл окремо: + +``` bash +g++ -c main.cpp +g++ -c tools.cpp +``` + +
+В результаті у нас буде два файли main.o та tools.o. Злінкуємо їх : + +``` bash +g++ main.o tools.o +``` + +з'явиться файл *a.out*; це і є Ваша програма, запускаємо: + +``` bash +./a.out +``` + +Це все! + +# Збирання Вашого власного Сі проекту + +Після всіх отриманих знань, ми можемо зібрати влусну програму з використанням OpenGL. + +* Завантажимо залежності. Ми використовуємо GLFW, GLEW і GLM, але у Вас можуть бути інші. Збережемо їх в підкаталогу Вашого проекту, наприклад external/ +* Вони повинні бути попередньо скомпільовані, та деякі, такі як GLM не потребують цього (примітки перекладача - такі бібліотеки називаються "header only") +* Створіть новий проект в Вашій IDE за вибором +* Додайте новий cpp файл (перекладач - код нижче - практично чистий Сі. В заголовку теж сі. У автора туторілу тут постійна плутанина сі і с++) +* Скопіюйте і вставте наступний код (це насправді playground.cpp) + +``` cpp +#include +#include + +#include + +#include + +#include +using namespace glm; + +int main( void ) +{ + // ініціалізація GLFW + if( !glfwInit() ) + { + fprintf( stderr, "Помилка ініціалізації GLFW\n" ); + return -1; + } + + glfwOpenWindowHint(GLFW_FSAA_SAMPLES, 4); + glfwOpenWindowHint(GLFW_WINDOW_NO_RESIZE,GL_TRUE); + glfwOpenWindowHint(GLFW_OPENGL_VERSION_MAJOR, 3); + glfwOpenWindowHint(GLFW_OPENGL_VERSION_MINOR, 3); + glfwOpenWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + + // Створимо нове вікно та ініціалізуємо OpenGL контекст + if( !glfwOpenWindow( 1024, 768, 0,0,0,0, 32,0, GLFW_WINDOW ) ) + { + fprintf( stderr, "Неможливо створити вікно GLFW. Якщо у Вас Intel відеокарта, вона може бути не сумісна з 3.3 Можливо, Вам прийдеться скористатись туторіалом для версії 2.1.\n" ); + glfwTerminate(); + return -1; + } + + // Initialize GLEW + if (glewInit() != GLEW_OK) { + fprintf(stderr, "Помилка ініціалізації GLEW\n"); + return -1; + } + + glfwSetWindowTitle( "Майданчик для дослідів" ); + + // Переконаємося, що ми можемо перехопити кнопку Esc + glfwEnable( GLFW_STICKY_KEYS ); + + // Темно синє тло + glClearColor(0.0f, 0.0f, 0.3f, 0.0f); + + do{ + // Тут нічого не малюється, дивіться туторіал 2! + + // Обміняємо буфери + glfwSwapBuffers(); + + } // Якщо користувач натиснув ESC, закриємо вікно + while( glfwGetKey( GLFW_KEY_ESC ) != GLFW_PRESS && + glfwGetWindowParam( GLFW_OPENED ) ); + + // Закриємо вікно OpenGL та завершимо роботу GLFW + glfwTerminate(); + + return 0; +} +``` + +* Скомпілюйте проект. + +У Вас скоріше за все буде багато помилок. Ми проаналізуємо всі відомі, одна за одною. + +# Вирішення проблем + +Помилки, наведені нижче, взяті з Visual Studio 2010, але в GCC та інших системах вони подібні. (від перекладача - я свідомо не перекладаю тексти помилок, тому що так буде їх легше знайти, але додаю короткий переклад) + +## Visual Studio - fatal error C1083: Cannot open filetype file: 'GL/glew.h' : No such file or directory + +(файл не знайдено) + +(будь який файл) + +Деякі файли чи заголовки можуть знаходитись в дивних місцях. Наприклад, GLEW знаходяться в external/glew-x.y.z/include/ (ми їх туди розпакували, пам'ятаєте?). Компілятор не чарівник і не може магічним чином дізнатись де саме файл, Ви повинні йому підказати. В налаштуваннях проекту додайте каталог з даним файлом. Це файл з кодом, тому додавайте в секцію компілятора, а не лінковщика. + +За *жодних обставин* Ви не повинні копіювати ці файли в каталоги компілятора (Program Files/Visual Studio/...). Технічно, це може працювати, але це *дуже* погана практика. + +Також дуже гарною практикою буде використання відносних шляхів ( ./external/glew/... замість C:/Users/username/Downloads/... ) + +Наприклад, ось що містить CMake файл туторіалу: + +``` +external/glfw-2.7.2/include +external/glm-0.9.1 +external/glew-1.5.8/include +``` + +Продовжуйте, поки компілятор не знайде всі файли. + +## GCC - fatal error: GL/glew.h: No such file or directory + +(файл не знайдено) + +(може бути будь-який файли з кодом) + +Це значить, що бібліотека не встановлена. Якщо Ви везунчик і бібліотека достатньо відома, то можна просто спробувати її встановити. В випадку GLFW, GLEW та GLM: + +``` bash +sudo apt-get install libglfw-dev libglm-dev libglew1.6-dev +``` + +Якщо це не дуже відома бібліотека, то дивіться відповідь для Visual Studio. + +(від перекладача - в Ubuntu скоріше за все Вам потрібні пакети, які закінчуються на `-dev`) + +## Visual Studio - error LNK2019: unresolved external symbol glfwGetWindowParam referenced in function main + +(не вирішений/не знайдений зовнішній символ в певній функції) + +(Або будь-який інший символ в будь-якій функції) + +Поздоровляємо, у Вас помилка лінковщика. Тому це гарна новина - компіляція була успішна, залишився ще один крок. + +Функції glfw знаходяться в зовнішній бібліотеці. Вам потрібно підказати лінковщику, де саме вона знаходиться. Так, це опція лінковщика. Не забудьте додати шлях до бібліотеки. + +Як **приклад**, це те, що проект на Visual Studio використовує. Імена трішки дивні, тому що це користувацька збірка. Але для GLM не потребує окремої збірки і подібні проблеми лінковки для неї не характерні. + +``` +external\Debug\GLFW_272.lib +external\Debug\GLEW_158.lib +``` + +Якщо Ви завантажили ці бібліотеки з SourceForge ([GLFW](http://www.glfw.org/download.html), [GLEW](http://glew.sourceforge.net/index.html)) і зібрали бібліотеку самостійно, Вам потрібно вказати правильний шлях, наприклад десь такий: + +``` +C:\Where\You\Put\The\Library\glfw.lib +C:\Where\You\Put\The\Other\Library\glew32.lib +``` + +## GCC - main.cpp: undefined reference to `glfwInit' + +(або будь-який інше посилання в іншому файлі) + +Відповідь аналогічна, як і для Visual Studio. + +Зверніть увагу, що в випадку Лінукса, GLFW і GLEW (та багатьох інших) зазвичай вже встановлені через apt-get та подібні (`sudo apt-get install libglew-dev libglfw-dev`, може трішки відрізнятись). Якщо це так, то бібліотека буде скопійована в відомі компілятору каталоги і Вам не потрібно буде вказувати шлях. Просто лінкуйте, як було показано в першій секції. + +## Я все налаштував вірно, але отримую помилку "unresolved external symbol" ! + +Це може бути трішки складно, та ось декілька варіантів: + +### У мене помилка лінковщика, яка містить ім'я виду _imp_glewInit чи щось схоже, що починається з _imp + +Це значить, що бібліотека (в даному випадку - glew) була скомпільована як *статична* бібліотека, але Ви намагаєтесь використовувати її як *динамічну* бібліотеку. В данному випадку просто додайте наступну директиву в опції компілятора (Вашого проекту, не glew): + +``` +GLEW_STATIC +``` + +(від перекладача - для інших проектів можуть бути інші рішення, читайте документацію) + +### У мене якісь дивні проблеми з GLFW + +А може GLFW була скомпільована як динамічна бібліотека, але Ви намагаєтесь використовувати її як статичну? + +Спробуйте додати таку опції препроцесора (для компілятору): + +``` +GLFW_DLL +``` + +### У мене якась проблема лінковки! Допоможіть мені, я застряг ! + +Будь-ласка, надішліть нам детальній звіт і весь проект (в архіві) і ми спробуємо надати інструкції. + +### Я хочу вирішити це самостійно. Які загальні правила? + +Давайте допустимо, що Ви автор GLFW. Ви хочете надати доступ до функції `glfwInit()`. + +Коли бібліотека будується як DLL, Ви повинні сказати компілятору, що `glfwInit()` не така як інші функції в цій DLL - вона повинна бути видна/доступна іншим. А уявна функція `glfwPrivateImplementationMethodNobodyShouldCareAbout()` - ні. Це можна зробити, об'явивши функцію зовнішньою - "external" (для GCC) чи "__declspec(dllexport)" (для Visual). + +Коли Ви хочете використовувати glfw, Вам потрібно підказати компілятору/лінковщику, що реалізація (тіло) цієї функції відсутнє в коді і її потрібно злінкувати (додати) динамічно. Це можна зробити, об'явивши функцію зовнішньою - "external" (для GCC) чи "__declspec(dllimport)" (для Visual). (?????? external???) + +Можна скористатись зручним `#define` : `GLFWAPI` і об'явити функцію наступним чином: + +``` cpp +GLFWAPI int glfwInit( void ); +``` + +* Коли Ви збираєте як DLL, Ви додаєте `#define GLFW_BUILD_DLL`. `GLFWAPI` перетворюється за допомогою `#define` в `__declspec(dllexport)` +* Коли Ви використовуєте GLFW як DLL, Ви додаєте `#define GLFW_DLL`. `GLFWAPI` перетворюється за допомогою `#define` в `__declspec(dllimport)` +* Коли Ви збираєте як статичну бібліотеку, `GLFWAPI` перетворюється за допомогою `#define` в пустий рядок +* Коли Ви використовуєте GLFW як статичну бібліотеку, `GLFWAPI` перетворюється за допомогою `#define` в пустий рядок. + +Отже ось правило: ці ключі повинні бути узгодженими. Якщо Ви будуєте бібліотеку (будь-яку бібліотеку, не тільки GLFW), Ви повинні правильні слова препроцесора - GLFW_DLL, GLEW_STATIC + + +## Моя програма падає ! + +Є багато причин, чому програма на С++ з OpenGL може падати. Ось декілька. Якщо Ви не знаєте, в якому саме рядку Ваша програма падає, розберіться, як користуватись дебагером (дивіться інструкії вище). Будь-ласка, не використовуйте `printf` для налагодження програми. + +### Код в main навіть не починає виконуватись + +Причина скоріше за все в тому, що деякі dll не були знайдені. Спробуйте відкрити Вашу програму за допомогою Dependency Walker (Windows) чи ldd (Linux; а ще подивіться [тут](http://stackoverflow.com/questions/6977298/dependency-walker-equivalent-for-linux)) + +### Моя програма падає на виклику функції glfwOpenWindow(), чи інших фунціях, що створюють OpenGL контекст + +Декілька можливих причин: + +* Ваша відеокарта не підтримує версію OpenGL, яку Ви запросили. Спробуйте перевірити підтримувані версії за допомогою GPU Caps Viewer чи чогось подібного. Оновіть драйвер, якщо його версія занадто стара. Інтегровані відеокарти від Інтел на ноутбуках особливо "примхливі". Використовуйте ранні версії OpenGL (наприклад 2.1), та розширення, якщо Вам не хватає можливостей. +* Ваша операційна система не підтримує потрібну версію OpenGL. Mac OS - та сама відповідь. +* Ви намагаєтесь використовувати GLEW з контекстом OpenGL Core (у якого немає застарілих речей). Це баг GLEW. Використовуйте `glewExperimental=true` перед `glewInit()` або використовуйте профіль сумісності (тобто використовуйте `GLFW_OPENGL_COMPAT_PROFILE` замість `GLFW_OPENGL_CORE_PROFILE`) + + +## Моя програма падає на першому виклику функцій OpenGL чи не першому створенні буферу + +Можливі причини : + +* Ви не викликали `glewInit()` ПІСЛЯ `glfwOpenWindow()` +* Ви використовуєте профіль core OpenGL і не створили VAO, додайте наступний код після `glewInit()`: + +``` cpp + GLuint VertexArrayID; + glGenVertexArrays(1, &VertexArrayID); + glBindVertexArray(VertexArrayID); +``` + +* Ви використовуєте стандартну збірку GLEW, що містить баг. Ви не можете використовувати профіль Core OpenGL через цей баг. Або використовуйте `glewExperimental=true` перед `glewInit()` або запросіть профіль сумісності у GLFW: + +``` cpp + glfwOpenWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE); +``` + +## Моя програма падає, коли я намагаюсь завантажити якийсь файл + +Встановіть робочий каталог правильно, дивіться перший туторіал. + +Створіть файл test.txt і спробуйте наступний код: + +``` cpp +if ( fopen("test.txt", "r" ) == NULL ){ + printf("Скоріше за все моя програма запускається з невірним робочим каталогом"); +} +``` + +Використовуйте дебагер (налагоджувач) !!!! Я серьйозно! Не робіть налагодження Вашої програми за допомогою `printf()`, використовуйте гарну IDE. [http://www.dotnetperls.com/debugging](http://www.dotnetperls.com/debugging) в основному для C#, але працює і для с++. XCode та QtCreator можуть дещо відрізнятись, але принципи залишаються ті самі. + +## Щось інше зламалось + +Напишіть нам на електронну пошту + + diff --git a/uk/miscellaneous/clicking-on-objects/index.markdown b/uk/miscellaneous/clicking-on-objects/index.markdown new file mode 100644 index 000000000..03b01cf7a --- /dev/null +++ b/uk/miscellaneous/clicking-on-objects/index.markdown @@ -0,0 +1,18 @@ +--- +layout: page +status: publish +published: true +title: Вибір об'єкта мишкою +date: '2013-04-30 10:30:55 +0200' +date_gmt: '2013-04-30 10:30:55 +0200' +categories: [] +order: 60 +tags: [] +language: uk +--- + +Є багато способів визначити по якому об'єкту клацнув користувач: + +- [Використовуючи хак OpenGL](./picking-with-an-opengl-hack) +- [Використовуючи бібліотеку фізики](./picking-with-a-physics-library) +- [Використовуючи користувацьку функцію Ray-OBB](./picking-with-custom-ray-obb-function) diff --git a/uk/miscellaneous/clicking-on-objects/picking-with-a-physics-library/index.markdown b/uk/miscellaneous/clicking-on-objects/picking-with-a-physics-library/index.markdown new file mode 100644 index 000000000..e76bd261d --- /dev/null +++ b/uk/miscellaneous/clicking-on-objects/picking-with-a-physics-library/index.markdown @@ -0,0 +1,201 @@ +--- +layout: page +status: publish +published: true +title: Використовуючи бібліотеку фізики +date: '2013-05-18 20:42:49 +0200' +date_gmt: '2013-05-18 20:42:49 +0200' +categories: [] +order: 80 +tags: [] +print: true +language: uk +--- + +В цьому туторіалі ми побачимо "рекомендований" спосіб обирання об'єкту в класичному рушію для гри, що може бути не Ваш випадок. + +Основана ідея в тому, що ігровий рушій буде мати бібліотеку фізики в будь-якому випадку і всі бібліотеки фізики мають функції для визначення перетину променя з сценою. В основному, такі функції будуть більше оптимізовані ніж те, що Ви зможете написати власноруч - все рушії використовують структури для *поділу простору*, які дозволяють не робити розрахунків для об'єктів, що не знаходяться в потрібній області. + +В цьому туторіалі ми будемо використовувати рушій Bullet Physics, але в цілому принципи аналогічні в інших - PhysX, Havok. + +# Інтеграція Bullet + +Багато туторіалів пояснює, як інтегрувати Bullet, наприклад [Bullet's wiki](http://bulletphysics.org/mediawiki-1.5.8/index.php/Main_Page) - гарний варіант. + + +``` cpp +// Ініціалізація Bullet. Чітко по інструкції http://bulletphysics.org/mediawiki-1.5.8/index.php/Hello_World, +// хоча ми не будемо використовувати більшість можливостей. + +// Побудуємо "широку фазу" +btBroadphaseInterface* broadphase = new btDbvtBroadphase(); + +// Налаштуємо конфігурації колізій і диспетчер +btDefaultCollisionConfiguration* collisionConfiguration = new btDefaultCollisionConfiguration(); +btCollisionDispatcher* dispatcher = new btCollisionDispatcher(collisionConfiguration); + +// Справжній "розв'язувач" фізики +btSequentialImpulseConstraintSolver* solver = new btSequentialImpulseConstraintSolver; + +// Світ +btDiscreteDynamicsWorld* dynamicsWorld = new btDiscreteDynamicsWorld(dispatcher,broadphase,solver,collisionConfiguration); +dynamicsWorld->setGravity(btVector3(0,-9.81f,0)); +``` + +Кожний об'єкт повинен мати *форму зіткнення* (*Collision Shape*). Так, меш об'єкту можна використовувати як фігуру для зіткнення (колізії), та це погано для продуктивності (швидкості виконання). Тому, зазвичай, використовується значно простіші фігури - коробка (паралелепіпед), сфери, капсули. Є декілька фігур зіткнення. Зліва направо - сфера, куб, опукла оболонка меша, оригінальний меш. Сфери не такі точні, але значно значно швидші для перевірки. + +![]({{site.baseurl}}/assets/images/tuto-picking-physics-library/CollisionShapes.png) + + +В цьому прикладі, всі меши (сітки) будуть використовувати однакову коробку: + +``` cpp +btCollisionShape* boxCollisionShape = new btBoxShape(btVector3(1.0f, 1.0f, 1.0f)); +``` + +Рушії фізики нічого не знають про OpenGL і, як факт, всі вони можуть бути запущені без 3D візуалізації. Отже, ви не можете просто передати Ваш VBO в Bullet. Ви повинні додати *Жорстке тіло* (*Rigid Body*) в симуляцію. + +``` cpp +btDefaultMotionState* motionstate = new btDefaultMotionState(btTransform( + btQuaternion(orientations[i].x, orientations[i].y, orientations[i].z, orientations[i].w), + btVector3(positions[i].x, positions[i].y, positions[i].z) +)); + +btRigidBody::btRigidBodyConstructionInfo rigidBodyCI( + 0, // маса, в кг. 0 -> статичний об'єкт, ніколи не буде рухатись. + motionstate, + boxCollisionShape, // фігура для зіткнення + btVector3(0,0,0) // локальна інерція +); +btRigidBody *rigidBody = new btRigidBody(rigidBodyCI); + +dynamicsWorld->addRigidBody(rigidBody); +``` + +Зверніть увагу, що Жорстке тіло використовує фігуру зіткнень для визначення його форми. + +В повноцінному ігровому рушії скоріш за все буде клас з ім'ям наклашт MyGameObject, який буде містити позицію, орієнтацію, меш для OpenGL вказівник на об'єкт Жорсткого тіла. Та тут ми спрощуємо. + +Notice that the Rigid Body use the Collision Shape to determine its shape. + + +``` cpp +rigidbodies.push_back(rigidBody); + +// Маленький хак : Збережемо індекс меша в Bullet's User Pointer. +// І таким чином ми будемо знати, який об'єкт обрали +// В повноцінній програмі скоріше за все буде передано "MyGameObjectPointer" +rigidBody->setUserPointer((void*)i); +``` + +Простіше кажучи - не використовуйте наведений код в реальному житті. Це демо! + +# Кидання променів (Raycasting) + + +## Знаходження напрямку променя + +Спочатку нам потрібно знайти промінь, який починається в камері і йде "через вказівник миші". Це реалізовано в функції `ScreenPosToWorldRay()`. + +Знайдемо початкову та кінцеву позицію променя (відрізка?) в нормалізованих координатах пристрою (NDC). Ми робимо це в цих координатах, тому що це дуже легко: + +``` cpp +// Початок і кінець променя в нормалізованих координатах пристрою (Ви читали 4 туторіал?) +glm::vec4 lRayStart_NDC( + ((float)mouseX/(float)screenWidth - 0.5f) * 2.0f, // [0,1024] -> [-1,1] + ((float)mouseY/(float)screenHeight - 0.5f) * 2.0f, // [0, 768] -> [-1,1] + -1.0, // Ближня площина знаходиться на Z=-1 в Нормальних координатах пристрою + 1.0f +); +glm::vec4 lRayEnd_NDC( + ((float)mouseX/(float)screenWidth - 0.5f) * 2.0f, + ((float)mouseY/(float)screenHeight - 0.5f) * 2.0f, + 0.0, + 1.0f +); +``` + +Щоб зрозуміти цей код, давайте поглянемо на малюнок з 4 туторіалу: + +![]({{site.baseurl}}/assets/images/tuto-picking-physics-library/homogeneous.png) + +NDC це куб, 2*2*2 з центром в початку координат, в цьому просторі, промінь йде "через мишу" є просто прямою лінією, перпендикулярною до ближньої площини. Це робить `lRayStart_NDC` та `lEndStart_NDC` такими, що легко розраховуються. + +Тепер нам потрібно застосувати зворотне перетворення як звичайне: + +``` cpp +// Матриця проекції веде від Простору камери в NDC +// Отже inverse(ProjectionMatrix) веде з NDC в простір камери. +glm::mat4 InverseProjectionMatrix = glm::inverse(ProjectionMatrix); + +// Матриця виду веде з світового простору в прості камери. +// Отже inverse(ViewMatrix) веде з простору камери в світовий простір. +glm::mat4 InverseViewMatrix = glm::inverse(ViewMatrix); + +glm::vec4 lRayStart_camera = InverseProjectionMatrix * lRayStart_NDC; lRayStart_camera/=lRayStart_camera.w; +glm::vec4 lRayStart_world = InverseViewMatrix * lRayStart_camera; lRayStart_world /=lRayStart_world .w; +glm::vec4 lRayEnd_camera = InverseProjectionMatrix * lRayEnd_NDC; lRayEnd_camera /=lRayEnd_camera .w; +glm::vec4 lRayEnd_world = InverseViewMatrix * lRayEnd_camera; lRayEnd_world /=lRayEnd_world .w; + +// Найшвидший шлях (одна інверсія) +//glm::mat4 M = glm::inverse(ProjectionMatrix * ViewMatrix); +//glm::vec4 lRayStart_world = M * lRayStart_NDC; lRayStart_world/=lRayStart_world.w; +//glm::vec4 lRayEnd_world = M * lRayEnd_NDC ; lRayEnd_world /=lRayEnd_world.w; +``` + +За допомогою lRayStart_worldspace і lRayEnd_worldspace, напрямок променю (в світовому просторі) дуже легко обчислити: + +``` cpp +glm::vec3 lRayDir_world(lRayEnd_world - lRayStart_world); +lRayDir_world = glm::normalize(lRayDir_world); +``` + +## Використання rayTest() + +Raycasting дуже простий, навіть не потрібно спеціальних коментарів: + +``` cpp +glm::vec3 out_end = out_origin + out_direction*1000.0f; + +btCollisionWorld::ClosestRayResultCallback RayCallback( + btVector3(out_origin.x, out_origin.y, out_origin.z), + btVector3(out_end.x, out_end.y, out_end.z) +); +dynamicsWorld->rayTest( + btVector3(out_origin.x, out_origin.y, out_origin.z), + btVector3(out_end.x, out_end.y, out_end.z), + RayCallback +); + +if(RayCallback.hasHit()) { + std::ostringstream oss; + oss << "mesh " << (int)RayCallback.m_collisionObject->getUserPointer(); + message = oss.str(); +}else{ + message = "background"; +} +``` + +Є тільки одна дивна річ, Вам потрібно встановити початок променю і напрямок двічі. + +Це все, що Вам потрібно знати, що б реалізувати функцію вибору з Bullet! + +# Переваги та недоліки + +Переваги : + +* Дуже легко, якщо у Вас все є рушій фізики +* Швидко +* Не заважає швидкодії OpenGL. + +Недоліки : + +* Можливо це не найкраще рішення, якщо Вам більше ні для чого не потрібний рушій фізики чи зіткнень. + +# Висновки та зауваження + +Всі фізичні рушії містять засоби налагодження. Приклад коду вище показує, як це зробити з Bullet. У підсумку Ви отримуєте представлення того, що Bullet знає про Вашу сцену, що є неймовірно корисним для налагодження проблеми, пов'язаної з фізикою, особливо для того, щоб переконатися, що «візуальний світ» узгоджується зі «світом фізики»: + +![]({{site.baseurl}}/assets/images/tuto-picking-physics-library/BulletDebug.png) + +Зелений ящик є *Collision Shape* (формою зіткнення) з тією самою позицією і орієнтацією, що і меш. Червоний ящик є об'єктом *Axis-Aligned Bounding Box* (вирівненим по осям обмежувальним ящиком) - AABB, що використовується як швидкий тест на відкидання - якщо промінь не перетинається з AABB (а це дуже легко і швидко порахувати), то тоді немає шансу, що промінь перетнеться з самим об'єктом. А ще Ви бачите осі об'єкту, зображені синім та червоним (подивіться на ніс на вухо). Дуже зручно! \ No newline at end of file diff --git a/uk/miscellaneous/clicking-on-objects/picking-with-an-opengl-hack/index.markdown b/uk/miscellaneous/clicking-on-objects/picking-with-an-opengl-hack/index.markdown new file mode 100644 index 000000000..77ad33ca3 --- /dev/null +++ b/uk/miscellaneous/clicking-on-objects/picking-with-an-opengl-hack/index.markdown @@ -0,0 +1,210 @@ +--- +layout: page +status: publish +published: true +title: Вибір об'єкту, використовуючи трюк з OpenGL +date: '2013-05-18 20:43:28 +0200' +date_gmt: '2013-05-18 20:43:28 +0200' +categories: [] +order: 70 +tags: [] +print: true +language: uk +--- + +Цей спосіб не дуже рекомендується, але він легкий і швидкий для реалізації простого способу вибору об'єкта. Будь-ласка, не використовуйте його в реальній грі, так як він може спричинити помітне зменшення fps (кількості кадрів в секунду). Якщо у Вас якась симуляція чи швидкодія Вам не принципова, цей спосіб може бути чудовим вибором. + +Джерельний код для цього туторіалу доступний тут [misc05_picking/misc05_picking_slow_easy.cpp](https://github.com/opengl-tutorials/ogl/blob/master/misc05_picking/misc05_picking_slow_easy.cpp) з досить неочевидним іменем. + +# Основна ідея + +Основна ідея цього способу в тому, що б малювати сцену як зазвичай, але замість того, що б використовувати гарні тіні, Ви малюєте кожний меш своїм специфічним і унікальним кольором. + +Потім Ви отримуєте колір пікселю під курсором і конвертуєте цей колір в оригінальний ідентифікатор. І Ви знаєте об'єкт, по якому натиснули мишкою. + +Ось приклад: + +![]({{site.baseurl}}/assets/images/tuto-picking-color/UniqueColors.png) + +На цьому знімку екрана кожна мавпа має трішки відмінний колір, що робить можливим їх однозначну ідентифікацію. + +Звичайно, скоріше за все Ви не захочете дивитись на зображення з цими дивними кольорами, тому напевне Ви захочете очистити екран і перемалювати все. + +# Реалізація + +## Присвоєння ідентифікатора кожному об'єкту + +Кожний об'єкт на сцені буде мати свій унікальний колір. Найпростіший спосіб це зробити - дати кожному об'єкту число-ідентифікатор, який конвертувати в колір. Власне колір не має значення, це ж трюк:) + +В наведеному коді, створюється 100 об'єктів і зберігаються в `std::vector`, і ідентифікатор кожного об'єкту це просто індекс в векторі. Якщо у Вас більш складна ієрархія об'єктів, можливо є сенс використати `std::map` і зберігати в них зв'язок між об'єктами і їх ідентифікаторами. + + +## Виявлення клацання мишки + +В цьому простому прикладі обирання виконується в кожному кадрі, коли ліва кнопка миші натиснута: + +``` cpp + if (glfwGetMouseButton(GLFW_MOUSE_BUTTON_LEFT)){ +``` + +В реальній програмі Ви будете робити це, коли користувач відпускає кнопку, отже, скоріше за все Вам потрібно буде зберегти `bool wasLeftMouseButtonPressedLastFrame`, чи, що краще, використовувати `glfwSetMouseButtonCallback()` (читайте документацію GLFW). + +## Перетворення ідентифікатора в спеціальний колір + +Так як ми збираємося малювати кожний меш своїм кольором, перший крок - це обчислити цей колір. Самий простий спосіб це розділити ідентифікатор на байти і помістити в червоний, синій і зелений колір: + +``` cpp +// Перетворимо "i", цілочисельний ідентифікатор, в RGB колір +int r = (i & 0x000000FF) >> 0; +int g = (i & 0x0000FF00) >> 8; +int b = (i & 0x00FF0000) >> 16; +``` + +Це може Вас налякати, але це стандартна маніпуляція бітами. В результаті у Вас буде три цілих числа в діапазоні 0..255. За допомогою цього методу Ви зможете закодувати 255^3 = 16 мільйонів мешів, що повинно бути достатнім для більшості випадків. + +## Малювання сцени з цими кольорами + +Тепер нам потрібен шейдер, що б малювати цим кольором. Це дуже просто. Цей шейдер нічого не робить: + +``` glsl +#version 330 core + +// Вхідні дані вершини, різні для всіх запусків цього шейдеру +layout(location = 0) in vec3 vertexPosition_modelspace; + +// Значення, що константні для цілого меша. +uniform mat4 MVP; + +void main(){ + + // Вихідна позиція в просторі обрізання, : MVP * position + gl_Position = MVP * vec4(vertexPosition_modelspace,1); + +} +``` +{: .highlightglslvs } + +І фрагментний шейдер просто записує бажаний колів в буфер фрейму: + +``` glsl +#version 330 core + +// Ouput data +out vec4 color; + +// Значення, що константні для цілого меша. +uniform vec4 PickingColor; + +void main(){ + + color = PickingColor; + +} +``` +{: .highlightglslfs } + +Легко ! + +Єдиний трюк в тому, що Ви повинні надсилати колір як дійсне число (в діапазоні 0..1), але у Вас цілі числа (в діапазоні 0..255), отже потрібно поділити перед викликом `glUniformXX()`: + +``` cpp +// OpenGL очікує колір в діапазоні 0..1 отже поділимо на 255 +glUniform4f(pickingColorID, r/255.0f, g/255.0f, b/255.0f, 1.0f); +``` + +І тепер можна малювати, як зазвичай (`glBindBuffer`, `glVertexAttribPointer`, `glDrawElements`) і Ви отримаєте дивне зображення. + + + +## Отримання кольору під мишкою + +Коли Ви намалювали всі Ваші меші (напевне, використовуючи цикл), Вам потрібно викликати функцію `glReadPixels()`, яка отримає вихідний піксель. Але для правильної роботи цієї функції, потрібно декілька додаткових функцій. + +По перше, Ви повинні викликати `glFlush()`, вона попросить драйвер OpenGL відіслати всі команди, що очікують в черзі (включаючи Ваші останні `glDrawXX`) до GPU. Це зазвичай не робиться автоматично, тому що команди відправляються пачками і не відразу (це значить, що коли Ви викликаєте `glDrawElements()`, нічого в цей момент не малюється. Воно скоріше за все намалюється через декілька мілісекунд). Ця операція дуже ПОВІЛЬНА. + +Далі, Ви повинні викликати glFinish(), яка буде чекати, поки все дійсно намалюється. Ця функція відрізняється від `glFlush()` тим, що `glFlush()` просто посилає команду, `glFinish()` чекає поки ця команда буде виконана. Ця операція ДУЖЕ ПОВІЛЬНА. + +Також потрібно налаштувати, як `glReadPixels` буде вести себе з вирівнюванням пам'яті. Це трішки за межами туторіалу, але просто додайте наступний виклик `glPixelStorei(GL_UNPACK_ALIGNMENT, 1)`. + +І нарешті, можна викликати `glReadPixels`! Ось повний код: + +``` cpp +// Чекаємо, поки всі команди відмальовки будуть дійсно виконані. +// Це неймовірно супер повільно ! +// Насправді, між викликом glDrawElements() та тим, коли зображення буде на екрані +// проходить дуже багато часу +glFlush(); +glFinish(); + +glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + +// Прочитаємо піксель в центрі екрану. +// Можна також дізнатись позицію курсора функцією glfwGetMousePos(). +// Це теж супер-мега-ультра повільно навіть для одного пікселю, +// тому що це буфер кадру, який знаходиться на відеокарті. +unsigned char data[4]; +glReadPixels(1024/2, 768/2,1,1, GL_RGBA, GL_UNSIGNED_BYTE, data); +``` + +Ваш колір тепер зберігається в масиві `data`. Ось тут видно, що ID меша 19. + +![]({{site.baseurl}}/assets/images/tuto-picking-color/DataArray.png) + + +## Перетворення кольорі назад в ідентифікатор + +Тепер можна відновити Ваш ідентифікатор об'єкта з буфера `data`. Код повністю протилежний до перетворення ідентифікатору в колір: + +``` cpp +// Перетворення кольору в ідентифікатор +int pickedID = + data[0] + + data[1] * 256 + + data[2] * 256*256; +``` + +## Використання цього ідентифікатора + +Тепер Ви можете використовувати цей ідентифікатор для будь-чого. В цьому випадку оновлюється текст в UI. Але в Вашому випадку може бути все, що завгодно. + +``` cpp +if (pickedID == 0x00ffffff){ // Повністю білий, це фон! + message = "background"; +}else{ + std::ostringstream oss; // C++ рядки дивні + oss << "mesh " << pickedID; + message = oss.str(); +} +``` + +(від перекладача - в с++11 і новіших можна записати код компактніше +``` cpp +if (pickedID == 0x00ffffff){ // Повністю білий, це фон! + message = "background"; +}else{ + message = std::to_string(pickedID); +} +``` +) + + +# Переваги та недоліки + +Переваги : + +* Просто, легко реалізувати +* Не потрібні додаткові бібліотеки чи знання спеціальної математики + +Мінуси : + +* Використання `glFlush()`, `glFinish()`, `glReadPixels()` відомі своєю повільною роботою, тому що вони заставляють процесор чекати відеокарту, а це руйнує швидкодію. +* Вам зазвичай не потрібно знати, по якому саме трикутнику натиснули мишкою, яка тут нормаль і тому подібне. + + + +# Висновки + +Хоча цей метод не є рекомендованим, сам він дуже корисний, але трішки обмежений в використанні. Методи, описані в двох інших туторіалах можуть бути використані для інших цілей, таких як визначення зіткнень, забезпечення аватару можливості ходити по землі, видимість запитів до штучного інтелекту. + +Якщо Ви обрали цю техніку і Вам потрібно обрати декілька точок в одному кадрі, Ви можете зробити це одночасно, Вам не потрібно малювати 5 раз зображення, що б взяти 5 точок! + \ No newline at end of file diff --git a/uk/miscellaneous/clicking-on-objects/picking-with-custom-ray-obb-function/index.markdown b/uk/miscellaneous/clicking-on-objects/picking-with-custom-ray-obb-function/index.markdown new file mode 100644 index 000000000..43a868e31 --- /dev/null +++ b/uk/miscellaneous/clicking-on-objects/picking-with-custom-ray-obb-function/index.markdown @@ -0,0 +1,170 @@ +--- +layout: page +status: publish +published: true +title: Вибір за допомогою спеціальної функції Ray-OBB +date: '2013-05-18 22:13:41 +0200' +date_gmt: '2013-05-18 22:13:41 +0200' +categories: [] +order: 90 +tags: [] +print: true +language: uk +--- + +Цей останній метод є гарним вибором між "трюком" з чистим OpenGL та реалізацією за допомогою зручного, повнофункціонального графічного рушія, від якого нам потрібен тільки raycast і вибір об'єктів. + +Цей туторіал містить концепції і функції з туторіалу про Bullet, отже прочитайте спочатку його. + +# Базова ідея + +Ми не будемо покладатись на те, що Bullet за нас знайте перетин променя і об'єкту, ми зробимо це самостійно. + +Як ми бачили, є багато можливих об'єктів для "зіткнення". Сфери дуже легкі для пошуку зіткнення, але для багатьох оригінальних мешей це зовсім не так. З іншої сторони, перевіряти перетин променю з кожним трикутником меша може бути дуже дуже дорого. Гарна середина - OBB (Oriented Bounding Boxes) - Орієнтований обмежувальний паралелепіпед (ящик, коробка). Вони досить точні (але багато чого залежить від Вашої геометрії) та дешеві в розрахунках. + +OBB це паралелепіпед який вміщує меш і коли меш повертається чи переміщується, ці самі трансформації відбуваються і з ним: + +![]({{site.baseurl}}/assets/images/tuto-picking-obb/OBB.png) + + +# Ray-OBB алгоритм + +*( Алгоритм і малюнки значною мірою натхненні книгою Real-Time Rendering 3. Купіть її!!! )* + +Розглянемо OBB, наведений нижче. По осі X він обмежений двома вертикальними площинами, забарвленими в червоне. Коли вони перетинаються з променем (дуже проста операція), це дає нам два перетини, один "близький", один "далекий": + +![]({{site.baseurl}}/assets/images/tuto-picking-obb/RayObb11.png) + +Коли промінь перетинає дві інші площини, що обмежують вісь Y (виділено зеленим), це дає два додаткових перетини. Зверніть увагу, що перетини впорядковані - Ви входите через зелену область -> покидаєте зелену область -> входите в червону область -> покидаєте червону область. Це означає, що тут немає перетину. + +![]({{site.baseurl}}/assets/images/tuto-picking-obb/RayObb21.png) + +Але якщо порядок інший (Ви ввійшли через зелену область -> Ви ввійшли через червону область), то Ви знаєте, що у Вас перетин! + +![]({{site.baseurl}}/assets/images/tuto-picking-obb/RayOBB31.png) + + +Давайте попрактикуємося. + +# Реалізація алгоритму + +(повний код доступний тут [Misc05/misc05_picking_custom.cpp](https://github.com/opengl-tutorials/ogl/blob/master/misc05_picking/misc05_picking_custom.cpp)) + +Наша функція, яка знаходить перетин променя(Ray) та OBB виглядає насупним чином: + +``` cpp +bool TestRayOBBIntersection( + glm::vec3 ray_origin, // Початок променя, в світовому просторі + glm::vec3 ray_direction, // Напрям променя (не цільова позиція!) в світовому просторі. Має бути нормалізованою. + glm::vec3 aabb_min, // Мінімальні координати X,Y,Z меша, коли він повністю не трансформований. + glm::vec3 aabb_max, // Максимальні координати X,Y,Z. дуже часто це aabb_min*-1 якщо Ваш меш відцентровано, але це не завжди так. + glm::mat4 ModelMatrix, // Трансформації, які були накладені на меш. (і які повинні бути накладені на обмежувальний паралелепіпед) + float& intersection_distance // Результат - відстань між ray_origin і перетином з OBB +){ +``` +Ми починаємо з ініціалізації декількох змінних. `tMin` є найбільшим ближчим перетином, що ми знайшли, `tMax` - найменший "далекий" перетин, який був знайдений. `Delta` використовується для пошуку перетину двох площин. + +``` cpp +float tMin = 0.0f; +float tMax = 100000.0f; + +glm::vec3 OBBposition_worldspace(ModelMatrix[3].x, ModelMatrix[3].y, ModelMatrix[3].z); + +glm::vec3 delta = OBBposition_worldspace - ray_origin; +``` + +Тепер давайте обчислимо перетин з двома площинами, що обмежують OBB по осі X: + +``` cpp +glm::vec3 xaxis(ModelMatrix[0].x, ModelMatrix[0].y, ModelMatrix[0].z); +float e = glm::dot(xaxis, delta); +float f = glm::dot(ray_direction, xaxis); + +// Будьте обережні, не виконуйте ділення, якщо f близьке до нуля. Дивіться повний код для деталей. +float t1 = (e+aabb_min.x)/f; // Перетин з лівою площиною +float t2 = (e+aabb_max.x)/f; // Перетин з правою площиною +``` + +`t1` та `t2` тепер містять відстань між початком променя та перетином променя і площин, Але ми не знаємо в якому порядку, тому ми переконаємося, що `t1` буде "ближчим" перетином, а `t2` - "дальнім": + +``` cpp +if (t1>t2){ // якщо порядок невірний + float w=t1;t1=t2;t2=w; // обміняємо t1 та t2 +} +``` + +Можна оновити `tMin` та `tMax` : + +``` cpp +// tMax є найближчим "дальнім" перетином (серед всіх X,Y і Z пар площин) +if ( t2 < tMax ) tMax = t2; +// tMin найдальшим "ближнім" перетином (серед всіх X,Y і Z пар площин) +if ( t1 > tMin ) tMin = t1; +``` + +Тут є одна особливість, якщо "найдальший" ближче ніж "найближчий", то перетину немає. + +``` cpp +if (tMax < tMin ) + return false; +``` + +Це все було для осі Х. Для всіх інших робимо те ж саме! + + +# Використання алгоритму + +Функція `TestRayOBBIntersection()` дозволяє нам перевіряти перетини лише з OBB, тому ми повинні перевірити їх всіх. В цьому туторіалі ми просто перевіримо всі паралелепіпеди по черзі, але якщо у Вас дуже багато об'єктів, то можливо Вам знадобиться додаткова структура для прискорення, наприклад, Binary Space Partitionning Tree (BSP-Tree) - (дерево бінарного розбиття простору, BSP-дерево) або Bounding Volume Hierarchy (BVH) - (Ієрархія обмежених об'ємів). + +``` cpp +for(int i=0; i<100; i++){ + + float intersection_distance; // результатf TestRayOBBIntersection() + glm::vec3 aabb_min(-1.0f, -1.0f, -1.0f); + glm::vec3 aabb_max( 1.0f, 1.0f, 1.0f); + + // Трансформація матриці моделі ModelMatrix: + // - для меша, для отримання його позиції + // - також для AABB (визначеного за допомогою aabb_min та aabb_max) в OBB + glm::mat4 RotationMatrix = glm::toMat4(orientations[i]); + glm::mat4 TranslationMatrix = translate(mat4(), positions[i]); + glm::mat4 ModelMatrix = TranslationMatrix * RotationMatrix; + + if ( TestRayOBBIntersection( + ray_origin, + ray_direction, + aabb_min, + aabb_max, + ModelMatrix, + intersection_distance) + ){ + std::ostringstream oss; + oss << "mesh " << i; + message = oss.str(); + break; + } +} +``` + +Зверніть увагу, що алгоритм має певну ваду - він обирає перший OBB, який знайде. Але якщо потрібен OBB за ним, то це проблема. Отже Ви будете отримувати лише найближчий OBB!. Розв'язання цієї проблеми залишаємо читачу... + +# Переваги та недоліки + +Переваги : + +* Просто +* Невеликі витрати пам'яті (лише данні про OBB) +* Не сповільнює OpenGL як інший метод + +Недоліки : + +* Повільніше, ніж готова в рушію фізики, так як не використовує спеціальні структури для прискорення +* Може бути не зовсім точною. + + + +# Висновки + +Є ще багато інших способів розрахувати перетин, доступних для всіх видів фігур, що зіштовхуються, дивіться тут [http://www.realtimerendering.com/intersections.html](http://www.realtimerendering.com/intersections.html). + +Якщо Вам потрібен точний перетин, потрібно використовувати перетин променя і трикутника. Знову, це не дуже гарна ідея перевіряти кожний трикутник в меші буквально. Потрібні різноманітні структури для прискорення. \ No newline at end of file diff --git a/uk/miscellaneous/contribute/index.markdown b/uk/miscellaneous/contribute/index.markdown new file mode 100644 index 000000000..813348536 --- /dev/null +++ b/uk/miscellaneous/contribute/index.markdown @@ -0,0 +1,22 @@ +--- +layout: page +title: Внесок +categories: [] +order: 15 +tags: [] +language: uk +--- + +* TOC +{:toc} + +Чи знайшли Ви цей туторіал корисним? Хочете зробити свій внесок? + +Є багато способів зробити свій внесок і ми вітаємо будь-який з них! + +* Хочете поділитись своїм досвідом і зробити Ваш персональний туторіал? Напишіть нам листа з Вашою ідеєю і будемо раді допомогти Вам і додати це на наш сайт! +* Помітили помилку (в коді, математиці, одрук, граматичну) в туторіалі? Надішліть звіт в [Github issue tracker](https://github.com/opengl-tutorials/ogl). +* Хочете допомогти нам, переклавши один чи декілька туторіалів іншою мовою (наприклад, іспанською чи португальською) - прочитайте цю [інструкцію]( {{ site.baseurl }}/miscellaneous/contribute/translation/). +* Цей сайт недостатньо гарний? Якщо Ви вмієте готувати CSS і вебсайти - пишіть нам! + +Якщо у Вас є будь-які інші ідеї, якими Ви б хотіли поділитись - не бійтесь, пишіть нам. diff --git a/uk/miscellaneous/contribute/translation/index.markdown b/uk/miscellaneous/contribute/translation/index.markdown new file mode 100644 index 000000000..69ac53602 --- /dev/null +++ b/uk/miscellaneous/contribute/translation/index.markdown @@ -0,0 +1,43 @@ +--- +layout: page +title: Переклад +author: + display_name: Sistr + email: damien.mabin@gmail.com + url: '' +categories: [] +order: 17 +tags: [] +language: uk +--- + +* TOC +{:toc} + +Ви хочете перекласти одну сторінку? Нема проблем! +Хочете перекласти все? Значно краще (^_^)! + + +# Бажаєте завершити/скоригувати переклад, що вже є? + +Склонуйте гілку "gh-pages" з github репозиторію [тут](https://github.com/opengl-tutorials/ogl/tree/gh-pages). +Знайдіть сторінку, яку ви хочете редагувати, модифікуйте її і відправте нам **по електропошті** (або відправте **github pull request, якщо Вам так більше подобається**). + +Знайти файл, який Ви хочете редагувати дуже просто. +Наприклад, ви хочете перекласти наступну сторінку на японську мову: + + +[https://github.com/opengl-tutorials/ogl/tree/gh-pages/jp/intermediate-tutorials/tutorial-9-vbo-indexing](https://github.com/opengl-tutorials/ogl/tree/gh-pages/jp/intermediate-tutorials/tutorial-9-vbo-indexing) + +Шлях до цієї сторінки - наступний *"jp/intermediate-tutorials/tutorial-9-vbo-indexing/"*. +Далі, в репозиторії на github в гілці *gh-pages* Ви знайдете файл *"index.markdown"* по аналогічному шляху: + +[https://github.com/opengl-tutorials/ogl/tree/gh-pages/jp/intermediate-tutorials/tutorial-9-vbo-indexing](https://github.com/opengl-tutorials/ogl/tree/gh-pages/jp/intermediate-tutorials/tutorial-9-vbo-indexing) + +# Як щодо нової мови ? + +Зв'яжіться з нами, ми створимо новий мовний каталог для Вас і Ви зможете використати аналогічні кроки як вище - знайти файл, перекласти і відправити нам по елекропошті чи pull-request. + +# Цей Git такий складний... + +Зв'яжіться з нами, ми зробимо все, щоб спростити процес перекладу максимально! diff --git a/uk/miscellaneous/faq/index.markdown b/uk/miscellaneous/faq/index.markdown new file mode 100644 index 000000000..db1a50af4 --- /dev/null +++ b/uk/miscellaneous/faq/index.markdown @@ -0,0 +1,146 @@ +--- +layout: page +status: publish +published: true +title: FAQ +date: '2012-04-29 13:22:31 +0200' +date_gmt: '2012-04-29 13:22:31 +0200' +categories: [] +order: 10 +tags: [] +print: true +language: uk +--- + +# Як відсилати листи... + +Лист на contact@opengl-tutorial.org є самий ефективний спосіб отримати підтримку. Проте, якщо у Вас є проблема, намагайтесь додати до листа як можна більше інформації. А саме: + +* Операційна система : Gentoo ? Windows XP ? ... (пам'ятайте, використовуйте 2.1 порт, якщо у Вас мак !) +* 32 біти чи 64 біти ? +* Графічна карта : NVIDIA ? AMD ? Intel ? S3 ? Matrox ? (пам'ятайте, використовуйте 2.1 порт, якщо у Вас інтегрована відеокарта !) + +... та інша інформація, яка не є обов'язковою, проте може допомогти: + +* Версія драйвера відеокарти +* Call stack (трасування стеку) +* знімок екрана +* вивід в консолі +* мінідамп... + +І не забувайте читати цей FAQ - він не даремно так називається =) + +# Я не можу скомпілювати цей туторіал + +* Переконайтесь, що Ви прочитали туторіал 1. Будь-ласка, використовуйте CMake замість створення проекту заново. Або, як мінімум прочитайте [Побудова Вашої власної С програми]({{site.baseurl}}/uk/miscellaneous/building-your-own-c-application/). +* Якщо у Вас помилки, які відносяться до бібліотеки AssImp, то воно буде скоро виправлено. Та це відноситься лише до одного проекту, всі інші повинні компілюватись без проблем. +* Якщо у Вас помилки, які відносяться до бібліотеки AntTweakBar, це теж відноситься лише до одного туторіалу. +* Якщо у Вас дійсно проблеми, не бійтесь відправити нам електронного листа + +# Я все скомпілював, проте воно падає при старті. Що відбувається ? + +Я декілька варіантів: + +## Несумісна відеокарта/операційна система + +Будь-ласка, перевірте, чи не використовуєте Ви карту від Intel. Ви можете використати glewinfo, GPU Caps Viewer чи інший інструмент. + +Карти від Intel, за виключенням HD4000 не підтримують OpenGL 3.3. Насправді, більшість з них підтримує тільки OpenGL 2.1. Ви можете використовувати версію 2.1, яку можна завантажити з сторінки Download. + +Також можуть бути проблеми, якщо ви використовуєте Mac з pre-Lion версією. Що ж... + +## Невірна робоча папка/каталог + +Є шанс, що Ви запускаєте проект з невірним робочим каталогом. Спробуйте запустити .exe з провідника. + +Перегляньте перший туторіал, де описано, як налаштувати IDE для налагодження. + +Зверніть увагу, що зазвичай .exe компілюється в *build* каталозі, але автоматично копіюється в каталог з початковим кодом, де він може знайти потрібні ресурси (зображення, 3д моделі, шейдери). + +## Немає VAO + +Якщо Ви створюєте програму з "нуля", переконайтесь, що Ви створили VAO: + +``` +GLuint VertexArrayID; +glGenVertexArrays(1, &VertexArrayID); +glBindVertexArray(VertexArrayID); +``` + +## Баг в GLEW + +GLEW містить баг, який робить неможливим використання контексту core (якщо тільки Ви не використовуєте код з цього туторіалу, де проблема була виправлена). Є три рішення: + +* Попросити бібліотеку GLFW використовувати профіль сумісності : + +``` +glfwOpenWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE); +``` + +* Використовуйте `glewExperimental`; це рекомендований спосіб: + +``` +glewExperimental = true; +``` + +* Виправити glew... Дивіться [цю латку](http://code.google.com/p/opengl-tutorial-org/source/browse/external/glew-1.5.8.patch?name=0009_33). + + +## CMake + +Ви прочитали Туторіал 1, так? Ви не пробували написати Ваш власний makefile і побудувати все самостійно, так? + +# Чому я повинен використовувати OpenGL 3.3 якщо Intel та Mac не може їх запустити ?! + +... також відоме як : + +# Яку версії OpenGL я повинен використовувати ? + +Насправді, я не рекомендую вивчати OpenGL 3 або старше. Я використовую його в туторіалах, тому що це **чистий** спосіб для вивчення OpenGL без всіляких застарілих речей і як тільки ви знаєте версію 3.3, використовувати 2.1 буде легко. + +Що я рекомендую: + +* Вчіть OpenGL 3.3 і Ви будете знати "правильний шлях" +* Встановіть цільове обладнання для Вашої програми. Наприклад, *вимагають* FBO та GLSL. +* Використовуйте GLEW для завантаження всіх розширень. При старті відкидайте все обладнання (залізо), що не відповідає вимогам. +* І тепер Вим можете писати код як для версії 3.3 з деякими змінами. +* Якщо Ви дійсно хочете мати справи з старим/дешевим обладнанням, Ви все ще можете це робити, заблокував певні ефекти, які вимагають нову функціональність, наприклад FBO. + +
Є один спеціальний випадок, коли Ви будете використовувати саму останню версію, наприклад 4.2 - Ви студент (аспірант), який робить дослідження і Вам дійсно потрібні всі нові функції і Ви не турбуєтесь про зворотню сумісність, тому що Ваша програма ніколи не покине стін лабораторії. В цьому випадку не витрачайте даремно час і використовуйте ту версію OpenGL, яку підтримує Ваше обладнання.
+ +# Де я можу завантажити OpenGL 3 ? + +Ви не можете. + +В випадку Windows, для прикладу, у Вас є тільки opengl32.dll, яка містить тільки OpenGL 1.1. Але в тут є функція `wglGetProcAddress()`. яка робить можливим отримувати функції, які не реалізовані напряму в opengl32.dll, але доступні в драйвері. + +GLEW використовує `wglGetProcAddress` для всіх потрібних символів (функцій) і робить їх доступними для Вас. (Ви можете це зробити самостійно, але це жахливо нудно). Також GLEW містить деякі константи, які не існували 10 років тому, наприклад `GL_VERTEX_ATTRIB_ARRAY_DIVISOR_ARB`. + +Отже, просто переконайтесь, що Ваш GPU підтримує потрібну версію, використовуйте GLEW і вперед. + +# Чому ви створюєте VAO в кожному туторіалі і не ніколи не використовуєте його ? + +Це не так. Воно прив'язується і використовується протягом всього часу виконання програми. + +VAO є обгорткою навколо VBO. Вони пам'ятають які буфери прив'язані до яких атрибутів і багато інших речей. Вони зменшують кількість OpenGL викликів перед glDrawArrays/Elements(). Починаючи з OpenGL 3 Core вони є обов'язковими, але ви можете використовувати тільки один та модифікувати (що власне й робиться в цьому підручнику). + +VAO можуть здатись дещо складними для туторіалу для початківців і вони не мають еквіваленту в OpenGL 2 (Який я рекомендую для "продакшину", дивіться відповідний FAQ) і це не очевидно, чи буде він більш кращий. Якщо Вам цікаві VAO, будь-ласка подивіться в відповідний розділ wiki OpenGL. Вони *можуть* дещо покращити продуктивність програми, але не завжди +
+
+ +# У мене помилка "Unable to start program ALL_BUILD" (неможливо запустити програму ALL_BUILD) + +ALL_BUILD це допоміжний проект, який генерує CMake, це не є справжньою програмою. + +Як зазначено в першому туторіалі, Ви повинні обрати проект, який Ви хочете запустити, натиснувши правою кнопкою мишки на потрібному проекті (в студії) і обрати "Set up as startup project" (встановити стартовим проектом), як на цьому малюнку: + +![]({{site.baseurl}}/assets/images/faq/StartupProject.png) + + + + +# Я отримую помилку про робочий каталог і програма падає. + +Ви повинні запускати програму з каталогів tutorial01_first_window/, tutorial02_red_triangle/ і так далі. Якщо Ви запускаєте з IDE (студії), Ви повинні налатувати студію/проект для цього. + +Будь-ласка, прочитайте перший туторіал для деталей. diff --git a/uk/miscellaneous/index.markdown b/uk/miscellaneous/index.markdown new file mode 100644 index 000000000..3771a682e --- /dev/null +++ b/uk/miscellaneous/index.markdown @@ -0,0 +1,26 @@ +--- +layout: page +type: section +status: publish +published: true +title: Різне +date: '2011-05-08 08:24:33 +0200' +date_gmt: '2011-05-08 08:24:33 +0200' +categories: [] +tags: [] +language: uk +--- + +{% assign sorted_pages = site.pages | sort:"order" %} +{% for p in sorted_pages %} + {% assign splt = p.url | split: page.url %} + {% if splt.size == 2 and splt[0] == '' %} + {% assign slash = splt[1] | split: '/' %} +{% if slash.size == 1 %} +- {{p.title}} +{% else %} + - {{p.title}} +{% endif %} + {% endif %} +{% endfor %} + diff --git a/uk/miscellaneous/math-cheatsheet/index.markdown b/uk/miscellaneous/math-cheatsheet/index.markdown new file mode 100644 index 000000000..8cd733269 --- /dev/null +++ b/uk/miscellaneous/math-cheatsheet/index.markdown @@ -0,0 +1,130 @@ +--- +layout: page +status: publish +published: true +title: Шпаргалка з математики +date: '2011-05-11 20:04:48 +0200' +date_gmt: '2011-05-11 20:04:48 +0200' +categories: [] +order: 20 +tags: [] +print: true +language: uk +--- + +# Тригонометрія + + +## Pi + +const float pi = 3.14159265f; // але в реальності тут безкінечна послідовність + +## Косинус та синус + + +*(З http://commons.wikimedia.org/wiki/User:Geek3 , ліцензія GNU Free Documentation)* + +## Одиничне коло + +![]({{site.baseurl}}/assets/images/math-cheatsheet/UnitCircle.png) + +*( Це модифіковане зображення http://en.wikipedia.org/wiki/User:Gustavb оригінальне зображення під ліцензією Crative Commons 3.0 )* + +t це кут в радіанах. + +0 радіан = 0 градусів + +180 градусів = Pi радіан + +360 градусів ( повне коло ) = 2*Pi радіан + +90 градусів = Pi/2 радіан + +# Вектора + +Завжди знайте в яких координатах ваш вектор. Дивіться секцію 3 для деталей. + +## Однорідні координати + +3D вектор - це (x,y,z), але 3D вектор з однорідними координатами - це (x,y,z,w). + +* w=0 : це напрямок +* w=1 : це позиція +* щось інше : може бути коректним, але ви знаєте краще, що ви робите + +Ви можете множити матрицю 4х4 тільки з вектором з однорідними координатами. + +## Довжина + +Просто як декартова відстань : sqrt( x² + y² + z² ). w не рахується. + +## Векторний добуток (Cross product) + +![]({{site.baseurl}}/assets/images/math-cheatsheet/Right_hand_rule_cross_product.png) + +*( Це модифіковане зображення http://en.wikipedia.org/wiki/User:Acdx , оригінальне зображення під ліцензією Creative Commons 3.0 )* + +Символ Х використовується для позначення векторного добутку. `length( a x b ) == length(a) * length(b) * sin(θ)`, тому можливо ви захочете викликати normalize() для результату. + +## Скалярний добуток (Dot product) + + +##![]({{site.baseurl}}/assets/images/math-cheatsheet/DotProduct.png) + + +*( З http://en.wikipedia.org/wiki/File:Dot_Product.svg )* + +`A.B = length(A)*cos(Theta)` , але зазвичай розраховується за формулою `A.x*B.x +A.y*B.y +A.z*B.z` + +## Додавання та віднімання + +кодом : + +``` cpp +res.x = A.x + B.x +res.y = A.y + B.y +... +``` + +## Множення + +кодом : + +``` cpp +res.x = A.x * B.x +res.y = A.y * B.y +... +``` + +## Нормалізація + +Потрібно розділити вектор на його довжину : + +``` cpp +normalizedVector = vec * ( 1.0f / vec.length() ) +``` + +# Матриця + + +## Множення матриці на матрицю + +Приклад переміщення (зсуву) матриці: + +![]({{site.baseurl}}/assets/images/math-cheatsheet/translationExamplePosition1.png) + + + + +## Множення матриці на вектор + +![]({{site.baseurl}}/assets/images/math-cheatsheet/MatrixXVect.gif) + + +# Звичайні трансформації + +![]({{site.baseurl}}/assets/images/math-cheatsheet/MVP.png) + + +... але в шейдерах, ви можете використовувати вектори в дотичному просторі. А для обробки зображень - в просторі зображення. +'res.x = A.x + B.x' diff --git a/uk/miscellaneous/useful-tools-links/index.markdown b/uk/miscellaneous/useful-tools-links/index.markdown new file mode 100644 index 000000000..5fd890ece --- /dev/null +++ b/uk/miscellaneous/useful-tools-links/index.markdown @@ -0,0 +1,112 @@ +--- +layout: page +status: publish +published: true +title: Гарні інструменти та посилання +date: '2011-05-11 20:31:21 +0200' +date_gmt: '2011-05-11 20:31:21 +0200' +categories: [] +order: 30 +tags: [] +language: uk +--- + +# Документація + + + +## Посилання OpenGL + + +* [OpenGL Quick Reference PDF](http://www.opengl.org/sdk/docs/reference_card/opengl45-reference-card.pdf) +* [OpenGL 4.5 Reference Pages](http://www.opengl.org/sdk/docs/man4/) +* [Специфікація OpenGL 4.5 (Core Profile)](https://www.khronos.org/registry/OpenGL/specs/gl/glspec45.core.pdf). Важке для читання. +* [Специфікація GLSL](https://www.khronos.org/registry/OpenGL/specs/gl/GLSLangSpec.4.50.pdf) +* [Список всіх розширень та актуальні характеристики](http://www.opengl.org/registry/) Якщо Ваша відеокарта відносно нова, більшість з цього доступно через GLEW. + + +## Інші туторіали + + +* [ogldev](http://ogldev.atspace.co.uk/index.html) Гарний і ретельний туторіал +* [NeHe ](http://nehe.gamedev.net/) Детальний туторіал та ресурси по OpenGL 1.1. В основному все застаріло, але можна знайти цікаві речі. + + +## Книги + + +* [Real-time Rendering 3](http://www.realtimerendering.com/). Фантастична книга для вивчення високорівневих концепцій. +* [GPU Pro book series](http://gpupro.blogspot.fr/). Детальні пояснення певних алгоритмів. + +## Інше + + +* [A trip trough the graphic pipeline](http://fgiesen.wordpress.com/2011/07/09/a-trip-through-the-graphics-pipeline-2011-index/) : Детальна інформація про те, як працює драйвер та відеокарта насправді. Фантастичне джерело інформації. Рекомендовано. +* [Unity stats](https://hwstats.unity3d.com/) +* [Extensions stats](http://gpuinfo.org/) + + +# Інструменти для налагодження + + +* [NVidia Parallel NSight](http://developer.nvidia.com/nvidia-parallel-nsight) Дивовижний інструмент, який дає доступ до дуже детальних даних для профайлера. Рекомендується. Тільки для Visual Studio + Nvidia. Це те, що я використовую найбільше. +* [GLIntercept ](http://glintercept.nutty.org/) Генерує вебсторінку з списком OpenGL команд, кодом шейдерів, вмістом зображень, фреймбуферів. Трохи заскладний для освоєння, та дуже точний і корисний. +* [gdebugger](http://www.gremedy.com/) профайлер + + +# Бібліотеки + + +## Вікна та різноманітне + +* [GLFW ](http://www.glfw.org/), ми це використовуємо +* GLUT, найстаріше, не підтримується, жахлива ліцензія, не рекомендується. +* [FreeGlut](http://freeglut.sourceforge.net/), те саме API, але з відкритим кодом. +* [SDL](http://www.libsdl.org/) Я думаю, що тут трішки все дико, та деякі люди це люблять. +* [SFML](http://www.sfml-dev.org/index-fr.php). Включає кавоварку, пральну машину і мобільний телефон. + +## Завантаження розширень + +Обережно, більшість бібліотек для завантаження розширень погано працюють з OpenGL 3+ Core. Насправді, я зміг заставити лише GLEW працювати, дещо змінивши код. + +Якщо не впевнені, використовуйте GLEW, яка є в коді на сайті. + +* [GLEW ](http://glew.sourceforge.net/), це ми використовуємо +* [GLEE ](http://elf-stone.com/glee.php) Я чув, що ця непогана +* [gl3w ](https://github.com/skaslev/gl3w/wiki). Скрипт на Python, що генерує C++ файл. + +## Математичні бібліотеки + +* [GLM](http://glm.g-truc.net/), цю ми використовуємо. Добре зроблена. Пряма сумісність з OpenGl та CUDA. +* [Bullet's Vectormath](http://bulletphysics.com/Bullet/BulletFull/) Ніколи не використовував, та здається, вона використовує SIMD (а це значить швидше, навіть якщо Ви не часто робите великі обчислення на CPU) +* [Boost.ublas ](http://www.boost.org/). Дуже важка. Я не рекомендую її для OpenGL. +* [MathFu](https://google.github.io/mathfu/) математична бібліотека від Google з SIMD оптимізаціями. + +## Завантаження зображень + +* [stbimage](http://nothings.org/) Один сі файл до Вашого проекту і можна завантажувати jpeg, bmp та png файли !!! +* [SOIL ](http://www.lonesock.net/soil.html) Я рекомендую цю бібліотеку. Основана на stbimage, надає зручні функції для використання з OpenGL та DDS. +* [DevIL](http://openil.sourceforge.net/) Бібліотека для завантаження зображень все в одному. +* [FreeImage](http://freeimage.sourceforge.net/) ... іще одна +* [Boost.GIL](http://www.boost.org/) Велика і громіздка, не вміє jpg. +* libjpg Типова бібліотека для завантаження jpg. Дещо складна для використання, та в інтернеті є достатньо прикладів. +* libpng те саме для png. + +## Завантаження статичних об'єктів + +* [ASSIMP ](http://assimp.sourceforge.net/) + +## Завантаження об'єктів поділених на частини (з'єданних) + +* [Cal3D](http://gna.org/projects/cal3d/) Завантажувач об'єктів Quake III чи Quake IV + + +## Фізика + + +* [Bullet](http://bulletphysics.org/wordpress/) З відкритим кодом, використовується багатьма іграми і навіть фільмами. +* [Newton](http://newtondynamics.com/forum/newton.php) Безкоштовне, також дуже хороше. +* [ODE](http://www.ode.org/). Старе та нестабільне, уникайте. +* PhysX +* Havok (дорого) +