OpenGL ES 2.0. Один мільйон часток

У цій статті ми розглянемо один з варіантів реалізації системи частинок на OpenGL ES 2.0. Докладно поговоримо про обмеження, опишемо принципи і розглянемо невеликий приклад.

image

Обмеження

В загальному випадку, від OpenGL ES 2.0 ми будемо вимагати два додаткові властивості (специфікація не вимагає їх наявності):
  • Vertex Texture Fetch. Дозволяє нам отримати доступ до текстурних карт через текстурні юніти з вершинного шейдера. Запитати максимальна кількість підтримуваних графічним процесором, юнітів можна з допомогою функції glGetIntegerv з ім'ям параметра GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS. У наведеній нижче таблиці представлені дані по популярним, на сьогоднішній день, процесорам.
  • Fragment high floating-point precision. Дозволяє нам проводити обчислення з високою точністю у фрагментном шейдере. Запитати точність і діапазон значень можна з допомогою функції glGetShaderPrecisionFormat з іменами параметрів GL_FRAGMENT_SHADER і GL_HIGH_FLOAT для типу шейдерів та типу даних відповідно. Для всіх, перерахованих у таблиці, процесорів точність складає 23 біта з діапазоном значення від -2^127 до 2^127, за винятком Snapdragon Andreno 2xx для цієї серії діапазон значення від -2^62 до 2^62.


Інформація по процесорам:




















Процесор Vertex TIU Точність Діапазон Snapdragon Adreno 2xx 4 23 [-2^62, 2^62] Snapdragon Adreno 3xx 16 23 [-2^127, 2^127] Snapdragon Adreno 4xx 16 23 [-2^127, 2^127] Snapdragon Adreno 5xx 16 23 [-2^127, 2^127] Intel HD Graphics 16 23 [-2^127, 2^127] ARM Mali-T6xx 16 23 [-2^127, 2^127] ARM Mali-T7xx 16 23 [-2^127, 2^127] ARM Mali-T8xx 16 23 [-2^127, 2^127] NVIDIA Tegra 2/3/4 0 0 0 NVIDIA Tegra K1/X1 32 23 [-2^127, 2^127] PowerVR SGX (Series5) 8 23 [-2^127, 2^127] PowerVR SGX (Series5XT) 8 23 [-2^127, 2^127] PowerVR Rogue (Series6) 16 23 [-2^127, 2^127] PowerVR Rogue (Series6XT) 16 23 [-2^127, 2^127] VideoCore IV 8 23 [-2^127, 2^127] Vivante GC1000 4 23 [-2^127, 2^127] Vivante GC4000 16 23 [-2^127, 2^127]


Є проблема з NVIDIA Tegra 2/3/4, на цій серії працює ряд популярних пристроїв таких, як Nexus 7, HTC One X, ASUS Transformer.
Система частинок

Розглядаючи системи частинок, що генеруються на CPU, в розрізі збільшення кількості оброблюваних даних (кількості частинок), основною проблемою продуктивності стає копіювання (вивантаження) даних з оперативної пам'яті в пам'ять видеоутсройства на кожному кадрі. Тому наше основне завдання — уникнути цього копіювання, перенісши обчислення неоперативный режим на графічний процесор.
Нагадаємо, що в OpenGL ES 2.0 немає вбудованих механізмів таких, як Transform Feedback (доступного в OpenGL ES 3.0) або Compute Shader (доступного в OpenGL ES 3.1), що дозволяють виробляти обчислення на GPU.


Суть методу полягає в тому, щоб використовувати в якості буффера даних для зберігання, що характеризують частку, величин (координати, прискорення і т. д.) — текстури і обробляти їх засобами верхових і фрагментных шейдерів. Також, як ми зберігаємо і завантажуємо нормалі, говорячи про Normal mapping. Розмір буффера, в нашому випадку, пропорційний кількості оброблюваних частинок. Кожен тексель зберігає окрему величину (величини, якщо їх декілька) для окремої частинки. Відповідно, кількість оброблюваних величин перебувати в зворотній залежності від кількості частинок. Наприклад, для того, щоб обробити позиції і прискорення для 1048576 частинок, нам знадобитися дві текстури 1024x1024 (якщо немає необхідності збереження співвідношення сторін)

Тут є додаткові обмеження, які необхідно врахувати. Щоб мати можливість записати будь-яку інформацію, формат піксельних даних текстури повинен підтримуватися реалізацією як color-renderable format. Це означає, що ми можемо використовувати текстуру в якості колірного буффера в рамках закадрової відтворення. Специфікація описує тільки три таких формату: GL_RGBA4, GL_RGB5_A1, GL_RGB565. Беручи до уваги предметну область, нам потрібно як мінімум 32 біти на піксель, щоб обробляти такі величини, як координати або прискорення (для двомірного випадку). Тому, згаданих вище форматів нам недостатньо.

Для забезпечення необхідного мінімуму, ми розглянемо два додаткових типу текстур: GL_RGBA8 і GL_RGBA16F. Такі текстури часто називають LDR(SDR) і HDR-текстурами відповідно.
  • GL_RGBA8 підтримується специфікацією, ми можемо завантажувати і читати текстури з таким форматом. Для запису нам необхідно зажадати розширення OES_rgb8_rgba8.
  • GL_RGBA16F не підтримується специфікацією, для того щоб завантажувати і читати текстури з таким форматом нам необхідно розширення GL_OES_texture_half_float. Більше того, щоб отримати прийнятний за якістю результат, нам необхідна підтримка лінійних фільтрів минификации і магнификации таких текстур. За це відповідає розширення GL_OES_texture_half_float_linear. Для запису нам необхідно розширення GL_EXT_color_buffer_half_float.


За даними GPUINFO на 2013 — 2015 рік підтримка розширень наступна:







Розширення Пристрою (%) OES_rgb8_rgba8 98.69% GL_OES_texture_half_float 61.5% GL_OES_texture_half_float_linear 43.86% GL_EXT_color_buffer_half_float 32.78%


Взагалі кажучи, для наших цілей більше підходять HDR-текстури. По-перше, вони дозволяють нам обробляти більше інформації без шкоди продуктивності, наприклад, маніпулювати частками в тривимірному просторі, не збільшуючи кількості буфферов. По-друге, відпадає необхідність в проміжних механізми розпакування і запаковування даних при читанні і запису відповідно. Але, в силу слабкої підтримки HDR-текстур, ми зупинимо свій вибір на LDR.

Отже, повертаючись до суті, загальна схема того, що ми будемо робити, виглядає так:

image

Перше, що нам необхідно, це розбити обчислення на проходи. Розбиття залежить від кількості і типу характеризують величин, які ми збираємося орабатывать. Виходячи з того, що в якості буффера даних у нас виступає текстура і з урахуванням описаних вище, обмежень на формат піксельних даних, кожен прохід може обробляти не більше 32 біт інформації з кожної частки. Наприклад, на першому проході ми расчитали прискорення (32 біт, 16 біт на компоненту) на другому оновили позиції (32 біт, 16 біт на компоненту).

Кожен прохід обробляє дані в режимі подвійної буфферизации. Тим самим забезпечується доступ до стану системи попереднього кадру.

Ядро проходу, це звичайний texture mapping на два трикутника, де в якості текстурних карт виступають наші буфферы даних. Загальний вигляд шейдерів наступний:
// Вершинний шейдер
attribute vec2 a_vertex_xy;
attribute vec2 a_vertex_uv;

varying vec2 v_uv;

void main()
{
gl_Position = vec4(a_vertex_xy, 0.0, 1.0);
v_uv = a_vertex_uv;
}

// Фрагментний шейдер
precision highp float;

varying vec2 v_uv;
// стан попереднього кадру
// (якщо необхідно)
uniform sampler2D u_prev_state;
// дані з попередніх проходів
// (якщо необхідно)
uniform sampler2D u_pass_0;
...
uniform sampler2D u_pass_n;

// функції розпакування
<type> unpack(vec4 raw);
<type_0> unpack_0(vec4 raw);
...
<type_n> unpack_1(vec4 raw)
// функція запаковування
vec4 pack(<type> data);

void main()
{
// розпаковуємо необхідні для розрахунків дані
// для частинки з індексом v_uv
<type> data = unpack(texture2D(u_prev_state, v_uv));
<type_0> data_pass_0 = unpack_0(texture2D(u_pass_0, v_uv));
...
<type_n> data_pass_n = unpack_n(texture2D(u_pass_n, v_uv));

// отримуємо результат
<type> result = ...

// упаковуємо і зберігаємо результат
gl_FragColor = pack(result);
}


Реалізація функцій розпакування/запаковування залежить від величин, які ми обробляємо. На даному етапі ми спираємося на вимогу, описане на початку, про високої точності обчислень.
Наприклад, для двовимірних координат (компоненти [x, y] по 16 біт) функції можуть виглядати так:
vec4 pack(vec2 value)
{
vec2 shift = vec2(255.0, 1.0);
vec2 mask = vec2(0.0, 1.0 / 255.0);

vec4 result = fract(value.xxyy * shift.xyxy);
return result - result.xxzz * mask.xyxy;
}

vec2 unpack(vec4 value)
{
vec2 shift = vec2(1.0 / 255.0, 1.0);
return vec2(dot(value.xy, shift), dot(value.zw, shift));
}

Отрисовка

Після етапу обчислень слід етап рендеринга. Для доступу до частинок на цьому етапі нам необхідний, для перебору, певний зовнішній індекс. В якості такого індексу буде виступати верховий буффер (Vertex Buffer Object з текстурними координатами буффера даних. Індекс створюється і ініціалізується (вивантажується в пам'ять відеопристрої) один раз і в процесі не змінюється.

На цьому кроці, набирає чинності вимога про доступ до текстурних карт. Вершинний шейдер схожий на фрагментний з етапу обчислень:
// Вершинний шейдер
// індекс даних
attribute vec2 a_data_uv;
// буффер даних, що зберігає позиції
uniform sampler2D u_positions;
// додаткові дані (якщо необхідно)
uniform sampler2D u_data_0;
...
uniform sampler2D u_data_n;

// функція розпакування позицій
vec2 unpack(vec4 data);
// функції розпакування додаткових даних
<type_0> unpack_0(vec4 data);
...
<type_n> unpack_n(vec4 data);

void main()
{
// розпаковуємо і обробляємо позиції частинок
vec2 position = unpack(texture2D(u_positions, a_data_uv));
gl_Position = vec4(position * 2.0 - 1.0, 0.0, 1.0);

// розпаковуємо і обробляємо додаткові дані
<type_0> data_0 = unpack(texture2D(u_data_0, a_data_uv));
...
<type_n> data_n = unpack(texture2D(u_data_n, a_data_uv));
}

Приклад

В якості невеликого прикладу, ми спробуємо створити динамічну систему з 1048576 частинок, відому як Strange Attractor.

Strange Attractors

Обробка кадру складається з декількох етапів:

image
Compute Stage

На етапі обчислень у нас буде всього один незалежний прохід, який відповідає за позиціонування частинок. В його основі лежить проста формула:
Xn+1 = sin(a * Yn) - cos(b * Xn)
Yn+1 = sin(c * Xn) - cos(d * Yn)


Така система ще називається Peter de Jong Attractors. З плином часу, ми будемо міняти тільки коефіцієнти.
// Вершинний шейдер
attribute vec2 a_vertex_xy;

varying vec2 v_uv;

void main()
{
gl_Position = vec4(a_vertex_xy, 0.0, 1.0);
v_uv = a_vertex_xy * 0.5 + 0.5;
}

// Фрагментний шейдер
precision highp float;

varying vec2 v_uv;

uniform lowp float u_attractor_a;
uniform lowp float u_attractor_b;
uniform lowp float u_attractor_c;
uniform lowp float u_attractor_d;

vec4 pack(vec2 value)
{
vec2 shift = vec2(255.0, 1.0);
vec2 mask = vec2(0.0, 1.0 / 255.0);

vec4 result = fract(value.xxyy * shift.xyxy);
return result - result.xxzz * mask.xyxy;
}

void main()
{
vec2 pos = v_uv * 4.0 - 2.0;
for(int i = 0; i < 3; i++)
{
pos = vec2(sin(u_attractor_a * pos.y) - cos(u_attractor_b * pos.x),
sin(u_attractor_c * pos.x) - cos(u_attractor_d * pos.y));
}

pos = clamp(pos, vec2(-2.0), vec2(2.0));
gl_FragColor = pack(pos * 0.25 + 0.5);
}

Renderer Stage

На етапі візуалізації сцени, ми отрисуем наші частинки звичайними спрайтами
// Вершинний шейдер
// індекс
attribute vec2 a_positions_uv;
// позиції (буффер, отриманий на етапі обчислень)
uniform sampler2D u_positions;

varying vec4 v_color;

vec2 unpack(vec4 value)
{
vec2 shift = vec2(0.00392156863, 1.0);
return vec2(dot(value.xy, shift), dot(value.zw, shift));
}

void main()
{
vec2 position = unpack(texture2D(u_positions, a_positions_uv));
gl_Position = vec4(position * 2.0 - 1.0, 0.0, 1.0);
v_color = vec4(0.8);
}

// Фрагментый шейдер
precision lowp float;
varying vec4 v_color;
void main()
{
gl_FragColor = v_color;
}

Результатimage

Postprocessing

На закінчення, на етапі постобробки, ми накладемо кілька ефектів

Gradient mapping. Додає колірне зміст на основі яскравості вихідного зображення.
Результатimage


Bloom. Додає невелику світіння.
Результатimage

Код

Репозиторій проекту на GitHub.
На даний момент доступні:
  • Основна база коду (C++11/C++14) разом з шейдерами;
  • Приклади та демо-версії програм.
    • Liquid. Симуляція рідини.
    • Scattered Light. Адаптація ефекту розсіяну світла.

    • Strange Attractors. Приклад, описаний в статті.
    • Wind Field. Реалізація Нав'є-Stokes з великою кількістю частинок (2^20).
    • Flame Simulation. Симуляція полум'я.


Нажаль, поки не викладений клієнт під Android, з'явиться найближчим часом.
Продовження

На цьому прикладі ми бачимо що етап обчислення, етап рендеринга сцени і постобробка складаються з кількох залежних між собою проходів.
У наступній частині ми постараємося розглянути реалізацію многопроходного рендеринга з урахуванням вимог, накладених кожним етапом.

Буду радий коментарям та пропозиціям (можна поштою yegorov.alex@gmail.com)
Спасибі!

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.