Рендеринг діаграм: не так просто, як здається

    Що складніше: отрендеріть сцену зі вибухаючими вертольотами або намалювати сумовитий графік функції y = x 2 ? Так, вірно, вертольоти підривати дорого і складно — але народ справляється, використовуючи для цього такі потужні штуки, як OpenGL або DirectX. А малювати графік, начебто, просто. А якщо хочеться красивий інтерактивний графік — можна його намалювати тими ж потужними штуками? Раз плюнути, мабуть?
 
А ось і ні. Щоб змусити похмурі графіки осудно виглядати і при цьому працювати без гальм, нам довелося попотіти: чи не на кожному кроці підстерігали несподівані труднощі.
 
 Задача: розробити кроссплатформенную бібліотеку для побудови діаграм, яка була б інтерактивною, з коробки підтримувала анімаційні переходи і, головне, не гальмувала.
 
 

Проблема 1: float і піксельні відповідності

 
Здавалося б, поділити відрізок на n рівних частин зможе і першокласник. У чому ж наша проблема? Математично тут все вірно. Життя псує точність float'a. Поєднати дві лінії піксель в піксель, якщо на них діють еквівалентні, але різні перетворення, виявляється практично неможливо: в надрах графічного процесора виникають похибки, які проявляються в процесі растеризації, кожен раз по-різному. А піксель вліво-піксель вправо — досить помітно, коли мова йде про контурах, відмітках на осях і т.п. Налагодити це практично неможливо, так як неможливо ні передбачити наявність похибки, ні вплинути на механізм растеризації, в якому вона виникає. При цьому похибка виявляється різною в залежності від того, чи включений Scissor Test (який ми використовуємо для обмеження області отрисовки графіка).
 
Доводиться робити милиці. Наприклад, ми округляємо значення зміщень в перетворенні перенесення до 10 -4 . Звідки таке число? Підібрали! Код виглядає страшно, зате працює:
 
const float m[16] = {
                        1.0f, 0.0f, 0.0f, 0.0f,
                        0.0f, 1.0f, 0.0f, 0.0f,
                        0.0f, 0.0f, 1.0f, 0.0f,
                        (float)(ceil(t.x() * 10000.0f) / 10000.0),
                        (float)(ceil(t.y() * 10000.0f) / 10000.0),
                        (float)(ceil(t.z() * 10000.0f) / 10000.0),
                        1.0f
                    };

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

Проблема 2: стиковка перпендикулярних ліній

 
Тут вже справа не в похибки, а в тому, як реалізуються «апаратно прискорені» лінії. Товщина 2 px, координати однакові, перетин в центрі. І — чудовий «викушенний» кут, як слідство. Рішення — знову ж, костильного зміщення Х-або Y-координати одного з кінців на один піксель. Але змістити щось на піксель, працюючи з координатами полігонів — ціла проблема. Координати сцени і координати екрану пов'язані один з одним перетвореннями, пронизаними похибкою — особливо якщо розмір області видимості, яку описує матриця проекції, що не дорівнює розміру екрана.
 
Зрештою, ми підібрали зміщення, які дають прийнятні результати, «але осад залишився»: рішення все-таки ненадійне і завжди є ймовірність, що у користувачів куточки виявляться щербатими. Виглядає це приблизно так:
 
m_border->setFrame(NRect(rect.origin.x + 0.5f, rect.origin.y + 0.5f, rect.size.width - 3.5f, rect.size.height - 3.0f));
m_xAxisLine->setFrame(NRect(rect.origin.x, rect.origin.y, rect.size.width - 1.5f, rect.size.height - 1.0f));
m_yAxisLine->setFrame(NRect(rect.origin.x, rect.origin.y, rect.size.width - 1.5f, rect.size.height - 0.5f));

 
 

Проблема 3: лінії взагалі

І знову лінії. У будь-якій діаграмі присутня досить багато ліній — звичайних ліній, без надмірностей. Це і осі, і сітка, і ділення на осях, і межі елементів графіка, іноді й сам графік. І ці лінії треба якось малювати. Здавалося б, що простіше? Як це не парадоксально, сучасні графічні API чим далі, тим впевненіше викидають підтримку звичайних ліній: пруф для OpenGL , пруф для Direct3D .
 
Поки що лінії ще підтримуються, але сильно обмежена їх допустима товщина. Практика показала, що на iOS-пристроях це 8 px, а на деяких андроїдах і того менше. Колись була у специфікації OpenGL функція установки шаблону пунктиру (glLineStipple ) більш не підтримується, на мобільних пристроях в OpenGLES 2.0 вона не доступна. Самі ж лінії — навіть ті, які по товщині вписуються в допустимі межі — виглядають страхітливо:
 
 
Поки ми миримося з тим, що є, але все йде до того, що доведеться писати свій визуализатор ліній, який зберігав би постійну товщину на екрані, не залежну від масштабу контуру (як зараз робить GL_LINES), але вмів би робити красиві зчленування на вигинах. Ймовірно, для цього доведеться будувати їх з полігонів:
 
 
 
 

Проблема 4: дірки між полігонами

 
І знову проблема точності. На скріншоті видно світлі «вкраплення» на круговій діаграмі. Це не що інше, як результат похибки растеризації (знову!), і тут ніякі милиці вже не рятують. Трохи краще стає, якщо включити згладжування меж:
 
 
На даний момент змирилися і залишили в такому вигляді.
 
 

Проблема 5: особливості системного антиалиасинга

Зовсім без згладжування меж результат рендеринга ріже око навіть на ретина-дисплеях. Але системний алгоритм згладжування MSAA, доступний на будь-якій сучасній платформі, має три серйозні проблеми:
 
     
  1. Зниження продуктивності: за нашими спостереженнями, на мобілках вона падає в середньому в три рази, і при відтворенні анімації на складних сценах з'являються відчутні лаги.
  2.  
  3. Утруднення мультіплатформенності (а ми за нею ганяємося): на різних платформах системний антиалиасинг включається по-різному, ми ж намагаємося по максимуму уніфікувати код.
  4.  
  5. Артефакти зображення: об'єкти, сторони яких паралельні сторонам екрану (наприклад, лінії сітки на графіку) розмиваються під дією системного антиалиасинга (якщо у них в результаті всіх перетворень вийшли дробові координати), хоча повинні залишатися різкими:
  6.  
 
 
Через все це нам довелося відмовитися від стандартного згладжування і винаходити черговий велосипед реалізувати власний алгоритм. У підсумку, ми зібрали оптимізований під мобілки гібрид SSAA і FXAA , який:
 
     
  1. Вміє автоматично відключатися на періоди відтворення анімації (при анімації користувачеві потрібна плавність руху, а в статиці — згладженість меж).
  2.  
  3. По продуктивності згладжування збігається з системним антиалиасинг, при цьому реалізується виключно внутрішніми механізмами нашого графічного движка (тобто зберігає мультиплатформеність).
  4.  
  5. Може впливати на частину сцени, а не на всю цілком (так вдається уникнути артефактів розмиття: просто виключаємо з безлічі згладжуються об'єктів ті, яким воно явно не піде на користь).
  6.  
Вплив на частину сцени організовується через «пошаровий» рендеринг, коли всі безліч об'єктів ділиться на групи (верстви) за їх взаємному розташуванню (передній, середній, задній план і т.д.) і необхідності згладжування. Шари отрісовиваємих послідовно, і згладжування застосовується тільки до тих, у яких виставлено відповідний атрибут.
 
 
 

Проблема 6: Нить і економія енергії

Хороший тон — обробляти події користувача інтерфейсу і рендеринг графічної сцени в різних потоках. Однак, дії користувача впливають на зовнішній вигляд сцени, а значить, необхідна синхронізація. Ми вирішили, що розставляти м'ютекси у всіх візуальних об'єктах — це занадто, і замість цього реалізували трансакціонної пам'ять.
 
Ідея полягає в тому, що є дві хеш-таблиці властивостей: для головного потоку (Main thread table, MTT) і для потоку рендеринга (Render thread table, RTT). Всі зміни налаштувань зовнішнього вигляду об'єктів потрапляють в MTT. Попадання в неї черговий записи призводить до планування «тика синхронізації» (якщо він ще не був запланований), який відбудеться на початку наступної ітерації головного потоку (передбачається, що обробка користувальницького інтерфейсу відбувається саме в головному потоці). Під час тика синхронізації вміст MTT переміщається в RTT (ця дія захищено м'ютексів — єдиним на всю графічну сцену). На початку кожної ітерації потоку рендеринга перевіряється, чи немає записів у RTT, і якщо вони є — вони застосовуються до відповідних об'єктів.
 
Тут же реалізується установка тих чи інших властивостей з анімацією. Наприклад, можна вказати зміна масштабу від 0 до 1 за певний час, і запис з RTT застосується не відразу, а за кілька кроків, на кожному з яких конкретне значення буде результатом інтерполяції значення масштабу від 0 до 1 по заданому закону.
 
І цей же механізм забезпечує можливість візуалізації на вимогу: фактичний рендеринг виконується тільки в тому випадку, якщо в RTT є записи (тобто стан сцени змінилося). Візуалізація на вимогу дуже актуальна для мобільних пристроїв, так як розвантажує процесор і тим самим дозволяє економити дорогоцінний заряд акумулятора.
 
Якось так. Вистачало, звичайно, і завдань на вміння користуватися гуглом — але найнесподіваніші граблі ми начебто перерахували. У підсумку, незважаючи на зусилля організаторів, свято відбулося вдалося-таки отримати картинки, за які не дуже соромно:
 
 
    
Джерело: Хабрахабр

0 коментарів

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