Мне кажется, стоит об этом говорить сразу на сайте, а не в комментах на хабре. Вот прямо так и пишите: «предложение кассы такой-то» и «стыковка readyto.travel». И выше вам очень правильно написали про подробности — трансфера от прилёта до вылета, время стыковки и все-все-все возможные подробности.
Да-да. Бриллиант из моей личной копилки — Autostart beim System Downloaden (переводчица не хотела работать с английским и переводила с русского, как могла) :)
Т.к. автор статьи использует самопальный SSAA, то эмулирует он его через Framebuffer, и делает рендер в текстуру. Отключение такого SSAA — просто перестаем ренедерить в FBO, рендерим сразу в окно, это очень быстро. Но MSAA точно так же можно рендерить в FBO. И такой MSAA включается одинаково на всех платформах. В общем нет вообще никакой принципиальной разницы, кроме того, что нельзя напрямую постпроцессить такую текстуру.
Я прекрасно знаю, где в ОГЛ кроссплатформенный код, а где нет (ибо писал кроссплатформенный рендер под линуксы). Так что отключается и включается MSAA очень быстро, если использовать FBO (который итак используется для SSAA сейчас).
Android != GNU/Linux. Мобильное устройство != персональный компьютер. OpenGL != OpenGL ES. Каким образом вы собираетесь создать на ES 2.0 рендербуфер с поддержкой мультисемплинга?
Может быть, всё-таки стоит говорить о реальных вещах?
Расскажите, как вы понимаете принцип работы MSAA, и объясните, почему в качестве теста производительности между SSAA и MSAA вы выбрали сцену с 1 600 000 треугольников?
И расскажите, как вы видите включение/отключение MSAA, и в чем принципиальная разница включения/отключения MSAA от включения/отключения вашего SSAA?
Да, чем больше геометрии, тем MSAA медленнее. Ну и что? Мы тестировали на пиковой нагрузке. Вот другая ситуация: iPad 1, 1000 треугольников, но довольно нагруженный шейдер отрисовки. MSAA даёт 14 fps, наш — 6 fps. Но при этом наш почти мгновенно выключается, так что все движения — на скорости 60 fps.
Включение/выключение MSAA на мобильных устройствах требует пересоздания рендербуферов. Кроме того, на разных платформах это будет делаться по-разному, что усложняет портирование. Наш механизм работает внутри движка, так что отключение оказывается удобнее. Ну и быстрее, так как нам не нужно изменять основной рендербуфер — что особенно актуально на андроидах, где пересоздание контекста может занимать до секунды (!).
Невероятно! Ничего нигде не торчит. Но если бы я просто залил скриншот — никто бы мне не поверил. Поэтому вот демка в которой можно покрутить правой кнопкой мыши диаграмму, погенерировать новую, а так же залелеть внутрь, и убедиться что все стенки на месте.
Нда, отличная демка, порадовавшая нескучной надписью «Access violation at address 00000000» при запуске. Пришлось смотреть, что же мы делаем не так. Проблема оказалась в следующем: получили вы адреса gl*EXT функций, отлично. Но, видимо, вы так спешили, что написать проверку их на NULL уже не успели. Если в вашем OpenGL драйвере существует функция glNamedBufferDataEXT, это не значит, что она есть всегда и у всех. Конец немного предсказуем. Ладно, плохие драйверы, бывает. Запустим демку на свежем компьютере… запустилась, но показывает красивый чёрный экран. Что на этот раз? Не линкуются шейдеры…
Бог троицу любит, и на третьем компьютере она-таки запустилась. На первый взгляд всё в порядке, и торчащих пикселов не видно. Объясняется это просто: у вас используется 24-битный буфер глубины, у которого проблем с точностью практически не возникает. У нас в режиме совместимости со старыми девайсами используется 16-битный: спецификация OpenGL ES 2.0 не гарантирует, что более точный буфер глубины будет доступен. На iOS с этим всё в порядке, а вот на старых андроидах бывает, что и не бывает.
Но это только на первый взгляд. Вращаем барабан… и через пару десятков секунд выпадает сектор «приз»:
И снова:
и снова:
Из-за этого вы запилили некрасивые костыли, которые на самом деле никакой красивости не дали. Они просто там есть
В размытости слов «Наш антиалиасинг» тоже наши костыли виноваты? Все претензии к habrastorage, пережавшему вот эту картинку. Вообще, там прекрасно видны волны вокруг прямых линий, уж можно было догадаться, что картинка пожата.
Вы почему-то постоянно забываете о том, что библиотека используется и на мобильных девайсах (а по факту, в основном на них). Упорно твердите, что причина дырок между полигонами — «неправильный» меш, забывая о том, что не у всех есть такие красивые и глубокие буферы. Рассказывая об антиалиасинге, не вспоминаете о том, что старые андроиды по производительности ближе к нокии 3310, чем к iphone 5. Да даже ваша собственная демка для вашей собственной винды демонстрирует, что вы оперируете очень малым количеством ситуаций.
В статье вы показали, что вообще не умеете работать ни с плавающей точкой, ни линейной алгеброй.
Прежде, чем разговаривать в таком тоне, нужно как минимум изучить линейную алгебру и графический конвеер (ц).
Вы, конечно, опытный теоретик, и очень убедительно учите всех жизни, но критерием истины по-прежнему остаётся практика. И эта самая практика прекрасно показала, чего стоят восемь лет вашего опыта.
Вы как-то очень легко рассуждаете о том, что мы знаем, а чего нет. Я же не говорю, например, что вы не знаете способы оптимизации и структурирования приложений, пока вы так упорно не соглашаетесь, что разделять рендеринг и main thread — это хороший тон.
Вы обвиняете нас в использовании «некрасивых костылей», а сами при этом предлагаете костыли едва ли не хуже. Да, после развёрнутого объяснения стало понятно, какой именно ужас вы имеете в виду. И да, этот ужас — при должном старании — тоже будет работать. При этом, однако, нагрузка ляжет на шейдер. Напоминаю, что мы ориентируемся на мобильные устройства, на которых дополнительные инструкции в коде шейдера крайне нежелательны. И лучше уж сделать костыли выравнивания один раз на меш, как у нас, чем повторять их на каждый кадр. Ваше решение, если его допилить как надо, имеет право на существование, но в этой конкретной ситуации не подходит.
Вы ставите диагноз по фотографии, утверждаете: «у вас неправильный меш», не потрудившись проверить подобную ситуацию, опираясь только на свой опыт — и, кстати, не потрудившись поинтересоваться опытом собеседника. Из-за этого беседа сводится к «я д’Артаньян, а вы — лоботрясы».
Насчёт понимания/непонимания преимущества MSAA над SSAA — при чём тут это вообще? Кроме того, по крайней мере на мобильных устройствах, падение производительности в три раза при MSAA — это довольно постоянная вещь, на сцене с меньшим количеством геометрии результат тот же.
Что касается производительности — да, наш способ медленнее MSAA. В итоге, пользователь получает на выходе 4.5 FPS против 5 FPS. Офигенно критичная разница. Ок, пускай так. Только наш АА выключается автоматом и быстро, а включать и выключать MSAA на лету можно, но это требует заметно больше времени. Особенно хватает нюансов с этим на андроиде. Да и не только там, на iOS тоже. У вас, видимо, какая-то специальная параллельная реальность, где ни отключение MSAA, ни отрисовка отдельных кадров не требуют времени, а мобильные устройства, вероятно, ничем не отличаются от десктопа. При этом вы сами сказали, что не работали с iOS — но утверждаете, что там всё есть, мы «просто не разобрались, как с ним работать».
Вообще, понятно что решать одну и ту же задачу (как, например, в нашем случае — отрисовка «опрятных» графиков) можно по-разному. В статье мы просто хотели показать, что иногда приходится запиливать некрасивые костыли во имя красивого результата.
Например что вершину можно двигать вправо-влево/вверх-вниз на пол пикселя. И двигать как-то так: coord.xy = round(coord.xy*0.5)*2.0;
Если coord.xy в экранных координатах — да. Но у нас-то задача выровнять по пикселям и то, что в экранных, и то, что в координатах сцены (а там у нас изначальные значения от –1.0 до 1.0, и округление уведёт их в 0). Суть в том, что линии сетки существуют в пространстве графика, то есть в системе координат сцены. А риски на оси — в пространстве экрана, потому что они торчат за пределы области графика (а эту область мы ограничиваем scissor-ом). И тут важно не только, чтобы и те, и другие были выровнены по сетке пикселей, но и чтобы они совпадали пиксель в пиксель друг с другом (то есть чтобы риски были «продолжением» линий сетки).
Ок, но боковина почему у вас имеет другие координаты? Если бы она имела те же координаты — не было бы z-fiting-а. Вы мне сейчас рассказываете те вещи, которые я итак прекрасно знаю, т.к. я уже лет 8 работаю с DirectX и OpenGL. Есть кстати такая техника, называется Depth prepass. Это когда сцену сначала рисуют только в буфер глубины. Потом эту же сцену рисуют снова, но уже в колор буфер без записи в глубину но с тестом глубины glDepthFunc(GL_EQUAL). Если на этапе растеризации были бы погрешности — оно бы вообще через пиксель все рисовалось, и просто файтилось бы.
А то что вы привели пример с блендера — это да, погрешность на этапе интерполяции. Пиксель же у нас квадратный, и глубина для него считается в центре, а проекция — перспективная. Но у вас то другая ситуация. У вас стенка может иметь те же координаты, что и кольцо вокруг. Но у вас она имеет «свои» координаты. Так чот эта погрешность создана вами, на CPU.
Во-первых, координаты стенки именно что те же самые. А во-вторых, хорошо, что вам повезло так и не столкнуться с такой вот ситуацией, когда то, что вы называете погрешностью интерполяции, возникает на границе перпендикулярных полигонов. Но это же не значит, что так в принципе не бывает.
У меня просто удивительно так совпало наверное. Сейчас пилю рендер, в нем есть рейтрейсинг, ну и из-за этого края, отрезанные рейтрейсингом не сглаживаются MSAA. Мне не досуг было думать, и я в качестве временного решения впихнул «гибрид» SSAA и FXAA. :) Вот так совпало, да. Потому что у меня однопиксельные линии есть (FXAA не подойдет), поэтому я делаю SSAA, линии становятся двухпиксельными, и я прохожу FXAA.
Но производительность SSAA ни в какие ворота не лезет, по сравнению с производительностью MSAA. Так что я не знаю откуда у вас там прирост. Но если оно действительно так, то это очень круто, и лучше бы вы поделились с сообществом этой технологией, чем постом выше.
По сути, основную идею вы сами озвучили: действительно, сначала делаем SSAA, затем поверх него FXAA. Общая производительность в целом получается такой же, как у MSAA, но выигрыш в том, что на периоды воспроизведения анимации мы отключаем антиалиасинг вообще. Поэтому у пользователя складывается ощущение плавности движений — но, конечно, ценой того, что пока сцена в движении, она не сглаживается.
Насчёт производительности SSAA: по нашим замерам, производительность SSAA лишь чуть-чуть меньше, чем у MSAA, можно сказать, такая же. Но у нас нет рейтрейсинга, возможно, с ним ситуация поменяется. Вот, для примера, цифры одного из тестов: iPad3, сцена 1 600 000 треугольников, освещение по Фонгу (то есть рендеринг сам по себе простой). Без АА: 15 fps, MSAA: 5 fps, SSAA: 4.5 fps, FXAA: 8 fps, наш гибрид (когда он включен): 4.5 fps.
Тоже когда-то грезил рендером в отдельном потоке. Но на практике оказалось — себе дороже. Отдельный поток нужен, если у тебя например тяжелая физика и относительно тяжелый рендер в одном потоке, и то только пожалуй для того, чтобы тяжелой физике было легче. Я в курсе что это не ваше изобретение, но в данном случае я не вижу уместности такому огромному велосипеду.
Ну да, мультитред — это сложно :) Но как раз тут это оказалось важно. Особенно для мобилок, которые, с одной стороны, довольно маломощные, с другой — во многих по два и больше ядер, так что два потока там хорошо уживаются и дают ощутимый для пользователя результат. Ну и велосипед — хотя, конечно, и не двадцать строчек кода — не такой уж и огромный :-)
Насколько я понимаю, гис-движок там написан на С++ с использованием boost’а. Если из их мобильного приложения вычесть вкладку «Карта», то оставшийся объём пара студентов сможет написать за неделю на обычном UIKit. При этом не нужно будет платить Digia по 150$ в месяц за использование Qt. //Это оценочное суждение, сделанное на основе изучения вывода команды mcview Payload/GrymMobile.app/GrymMobile, и если кто-то точно знает, что я ошибаюсь — прошу меня поправить :-)
Скроллинг и зумминг никак не связан с этим. В этом случае привязка к границам сетки должна дополнительно осуществляться в вершинном шейдере, а вершины должны иметь необходимую информацию о привязке.
Поясните, что значит «привязка к сетке в вершинном шейдере» и какую такую информацию должны иметь вершины?
Ну шейдер же. Вы думаете почему линии выкидывают из АПИ? Не гибко и держать код растеризации на пайплайне — неудобно.
Да, насчёт шейдера — да, сам по себе механизм не такой сложный. Но тут проблема не только в механизме, но ещё и в рефакторинге имеющегося кода.
Увы, такого не может быть. Иначе во всех 3д моделях в играх были бы такие же дырки. :)
В играх стараются не допускать ситуации, которую мы показали тут: habrahabr.ru/post/230671/#comment_7811885
Мы тоже стараемся её избегать, но вот не всегда получается.
И это таки не фон, а стенка. На том скриншоте, что в статье, просто так совпало по цветам. Вот другой скриншот:
Ну и именно из-за того, что стенка имеет одни и те же координаты с боковинами, происходит ситуация «борьбы» в z-буфере, потому что на этапе растеризации у фрагментов разных полигонов оказывается одинаковая глубина. И из-за ограниченной точности z-буфера появляются места, где фрагменты проходят тест 50/50: часть от одного полигона, часть от другого. Вот, например, скриншот из Blender3D. Ситуация там несколько иная, там площадь перекрытия у полигонов большая. Но суть та же:
Не заметил разницы между вашим антиалясингом и MSAA. Вы просто область для сравнения взяли удачную. И постпроцессовый антиалясинг (тот же FXAA например) — это полное фиаско для однопиксельных линий, идущих под углом.
Так во внешнем виде её особой и нет. Есть разница в производительности, потому что наш АА можно быстро отключить на период проигрывания анимации и включить обратно, когда картинка «устаканилась». Кроме того, у нас же не только постпроцессинговый, у нас гибрид SSAA и FXAA.
То есть параллельность только для того, чтобы memcpy выполнялся в отдельном потоке? Хм…
Да, и плюс ещё обсчёт анимаций и все вспомогательные преобразования. Но ещё раз: это не главное, главное — обратная ситуация.
Так это вообще странное решение. Т.е. вместо того чтобы вынести обновление данных, которое блокирует main-thread — вы выносите рендеринг? При этом тред все равно заблокирован, пользователь не может ничего сделать, зато он может посмотреть как у него рендерится сцена, которая реально рендерится только когда что-то изменилось?
В общем случае, действительно, обновление данных стоит выносить в отдельный поток. Но мы-то пишем компонент, который должен быть готов к разным ситуациям. И наличие плавных анимаций, не зависящих от нагруженности main thread-a — важный момент. «Когда что-то изменилось» включает в себя и воспроизведение анимаций, естественно.
Более того, подход разделения на main thread и rendering thread — это, мягко говоря, не наше изобретение, это best practice. Например, так работает Apple CoreAnimation.
Не работал с iOS, не могу ни подтвердить ни опровергнуть, но глядя на эти «проблемы» немного сомневаюсь что на iOS main thread блокируется наглухо (не приходят евенты ни таймеров, ни перерисовки)
Спасибо за развёрнутый комментарий! Да, теоретически-то Ваши доводы верны. Но есть нюансы.
По первой проблеме — вы как-то ловко пропустили тот момент, что при включении scissor test’а погрешность изменяется. Отрисовку сложной трёхмерной сцены, к сожалению, не получается свести к “xcoord = fullsize*i/5.0;”. График должен быть интерактивным, то есть должнен скроллиться и зумиться. Поэтому работа идёт в двух системах координат: сцены (где график, на неё действует масштаб и смещение, она ограничена scissor'ом) и экрана (где оси, она «статична», подвижны лишь риски на осях).
Для системы координат экрана используется матрица проекции, которую породил бы, например, вызов gluOrtho2D(0, screen_width, 0, screen_height).
Для двумерной сцены используется система координат, которую породил бы gluOrtho2D(-1, 1, -1, 1), чтобы по максимуму унифицировать случаи 2d и 3d графиков.
Чтобы риски на осях при скролле/зуме двигались вместе с графиком, мы вычисляем их координаты исходя из проекций соответствующих точек в системе координат сцены на систему координат экрана. Вот тут-то появляются те самые погрешности.
Дальше, про стыковку — ну да, такая проблема почти всюду. Но в условиях вышесказанного она иногда принимает весьма непредсказуемый характер, потому что сдвинуть что-то «на пиксель», когда оно подвержено довольно сложным многоступенчатым преобразованиям — не так-то просто.
Триангуляция линий — всё правильно. Но опять же нюанс, его вот уже тут озвучили: habrahabr.ru/post/230671/#comment_7810005
И проблема даже не в ретесселяции. Проблема в том, чтобы держать толщину линий одинаковой на разных масштабах контура. Не сказать, что это прямо так уж сложно, но сомнительно, что удастся уложиться в пару десятков строк кода :)
Про дырки между полигонами — там мы как раз ничего не прибавляем. И круг мы замыкаем (точнее, используем для начала следующего сектора координаты конца предыдущего). Тем не менее, такой вот спецэффект присутствует. Проблема в том, что у секторов есть ещё стенки (так как они могут выезжать из пирога и висеть рядом с ним). И вот то, что видно — это как раз не фон, а эта самая стенка, потому что в одном месте оказываются два стыкующихся полигона и один перпендикулярный им <картинка>. Погрешность растеризации вызывает как этот самый перпендикулярный полигон, который по идее скрыт стоящими рядом с ним, но из-за нехватки точности буфера глубины самый близкий к наблюдателю ряд точек проходит тест глубины и рисуется. Как избежать такой проблемы, мы пока что не придумали (очень не хочется костылить удаление стенки или же делать что-то с порядком вывода полигонов, потому что сейчас вся конструкция рисуется за один draw-call).
Про системный антиалиасинг Вы всё верно говорите и рисуете. Если в итоге у линии будут «правильные» координаты, она не размажется. Но проблема в том, что, как мы уже описали выше, «правильные» координаты организовать сложно (много промежуточных преобразований). Ну и артефакты — не самое страшное в системном антиалиасинге. Были бы они одни — мы б, может быть, смирились. Но вот его тормоза мы уже пережить не смогли :) А если писать свой антиалиасинг — так за компанию и от артефактов защититься решили.
Про многопоточность. Во-первых, да, мы рисуем только тогда, когда что-то изменилось. Во-вторых, рендеринг, конечно, распараллелен в плане SIMD. Но не в плане, например, загрузки данных. Те же glTexImage2D или glBufferData вполне себе синхронные. И не в плане всех остальных действий, которые производятся нами же по соседству (расчёт анимаций, применение преобразований и прочее). И вот в сумме все эти действия могут занимать довольно много времени, и их хочется производить где-то не там, где крутится обработка действий пользователя. Но главное даже не это (в большинстве случаев рендеринг действительно проходит быстро). Главное — это обратная ситуация: когда заблокирован main-thread, например, при обновлении данных. Или, ещё пример, на iOS, если на экране есть table view controller со включенной «пружинкой» и пользователь оттянул эту пружинку и держит, главный поток стоит с замиранием сердца ждёт, что же будет. И вот тут-то хотелось бы, чтобы анимация продолжала плавно проигрываться. Ну а велосипед наш работает довольно быстро — по крайней мере, поставленную задачу решает и обеспечивает удобный механизм анимаций.
Если вы про этот Blitter, то он предназначен для двумерной графики, а нам нужна и трёхмерная тоже. Кроме того, он же, вроде, чисто для спрайтов? На нём можно, например, динамически меняющиеся контуры рисовать? Или делать динамические эффекты освещения? Ну и насколько он на мобильных устройствах работает — тоже вопрос, надо проверять.
Научные диаграммы — это прекрасно. Их мы тоже с удовольствием рисуем. Но есть коммерческие заказчики, которым надо «вау-эффект» для презентаций, и они просили 3d, анимации, блестящие шарики и прочее.
Библиотека коммерческая, так что конкретные применения остаются за клиентами — мы-то делаем компонент, а не прикладные приложения. Есть демо-приложение (для iOS и мака), правда, там рандомные данные. Собственно, давайте данные — отрисуем :)
Из прикладного с реальными данными есть под рукой вот эти две: множественное выравнивание ДНК и спектрограмма звука. Последняя, кстати, в 3д лучше.
А что до невозникания проблем — так основные проблемы с точностью как раз касаются двумерных диаграмм. Четыре проблемы из шести про них.
Здесь иллюстрировались не научные данные, а возможности рендеринга, так что мы поотключали все милые сердцу подписи (они все включаются — и для данных, и для осей), выбрали ракурс понаряднее (всё трёхмерное можно вращать и масштабировать) и пообъёмнее (3d — это плюшка, а не единственно возможный вариант).
Мы прекрасно понимаем, что 3D в научных диаграммах не нужен, за редким исключением (типа спектрограммы линейного входа, как первая из 4-х картинок в конце — она ещё и обновляется в реальном времени) — но с всем вышеперечисленным технических трудностей при реализации не возникло :)
p.s. а за статью про паи спасибо, сложу в личную копилку аргументов :)
Ну во-первых, Qt Data Visualization & Charts зарелизили в 2014, мы в это время были уже глубоко и плотно в разработке. До этого был QWT, и он, мягко скажем, не фонтан.
Во-вторых, c Qt есть проблемы, если хочется зарелизить iOS-приложение с закрытыми исходниками. Аппстор требует, чтобы все использованные библиотеки лежали внутри в статике, а там с этим сложности: qt-project.org/wiki/Licensing-talk-about-mobile-platforms.
Во-третьих, как показывает практика, Qt не такой уж кроссплатформенный. Он великолепно подходит, когда надо что-то быстро сваять на коленке — но как только доходит дело до продакшена, его ненативность быстро начинает раздражать конечных пользователей.
В-четвёртых, у нас сделаны нативные оболочки для iOS и андроида (на Objective-C и Java), так что пользователь может не заморачиваться по поводу написания на С++.
Орфо:
> придайте информацию огласке
предайте
www.khronos.org/registry/gles/specs/3.0/es_spec_3.0.2.pdf, страницы 315, 316.
Да, чем больше геометрии, тем MSAA медленнее. Ну и что? Мы тестировали на пиковой нагрузке. Вот другая ситуация: iPad 1, 1000 треугольников, но довольно нагруженный шейдер отрисовки. MSAA даёт 14 fps, наш — 6 fps. Но при этом наш почти мгновенно выключается, так что все движения — на скорости 60 fps.
Включение/выключение MSAA на мобильных устройствах требует пересоздания рендербуферов. Кроме того, на разных платформах это будет делаться по-разному, что усложняет портирование. Наш механизм работает внутри движка, так что отключение оказывается удобнее. Ну и быстрее, так как нам не нужно изменять основной рендербуфер — что особенно актуально на андроидах, где пересоздание контекста может занимать до секунды (!).
Нда, отличная демка, порадовавшая нескучной надписью «Access violation at address 00000000» при запуске. Пришлось смотреть, что же мы делаем не так. Проблема оказалась в следующем: получили вы адреса gl*EXT функций, отлично. Но, видимо, вы так спешили, что написать проверку их на NULL уже не успели. Если в вашем OpenGL драйвере существует функция glNamedBufferDataEXT, это не значит, что она есть всегда и у всех. Конец немного предсказуем. Ладно, плохие драйверы, бывает. Запустим демку на свежем компьютере… запустилась, но показывает красивый чёрный экран. Что на этот раз? Не линкуются шейдеры…
Бог троицу любит, и на третьем компьютере она-таки запустилась. На первый взгляд всё в порядке, и торчащих пикселов не видно. Объясняется это просто: у вас используется 24-битный буфер глубины, у которого проблем с точностью практически не возникает. У нас в режиме совместимости со старыми девайсами используется 16-битный: спецификация OpenGL ES 2.0 не гарантирует, что более точный буфер глубины будет доступен. На iOS с этим всё в порядке, а вот на старых андроидах бывает, что и не бывает.
Но это только на первый взгляд. Вращаем барабан… и через пару десятков секунд выпадает сектор «приз»:
И снова:
и снова:
Оригиналы скриншотов: раз, два, три, четыре, пять, шесть.
В размытости слов «Наш антиалиасинг» тоже наши костыли виноваты? Все претензии к habrastorage, пережавшему вот эту картинку. Вообще, там прекрасно видны волны вокруг прямых линий, уж можно было догадаться, что картинка пожата.
Вы почему-то постоянно забываете о том, что библиотека используется и на мобильных девайсах (а по факту, в основном на них). Упорно твердите, что причина дырок между полигонами — «неправильный» меш, забывая о том, что не у всех есть такие красивые и глубокие буферы. Рассказывая об антиалиасинге, не вспоминаете о том, что старые андроиды по производительности ближе к нокии 3310, чем к iphone 5. Да даже ваша собственная демка для вашей собственной винды демонстрирует, что вы оперируете очень малым количеством ситуаций.
Прежде, чем разговаривать в таком тоне, нужно как минимум изучить линейную алгебру и графический конвеер (ц).
Вы, конечно, опытный теоретик, и очень убедительно учите всех жизни, но критерием истины по-прежнему остаётся практика. И эта самая практика прекрасно показала, чего стоят восемь лет вашего опыта.
Вы обвиняете нас в использовании «некрасивых костылей», а сами при этом предлагаете костыли едва ли не хуже. Да, после развёрнутого объяснения стало понятно, какой именно ужас вы имеете в виду. И да, этот ужас — при должном старании — тоже будет работать. При этом, однако, нагрузка ляжет на шейдер. Напоминаю, что мы ориентируемся на мобильные устройства, на которых дополнительные инструкции в коде шейдера крайне нежелательны. И лучше уж сделать костыли выравнивания один раз на меш, как у нас, чем повторять их на каждый кадр. Ваше решение, если его допилить как надо, имеет право на существование, но в этой конкретной ситуации не подходит.
Вы ставите диагноз по фотографии, утверждаете: «у вас неправильный меш», не потрудившись проверить подобную ситуацию, опираясь только на свой опыт — и, кстати, не потрудившись поинтересоваться опытом собеседника. Из-за этого беседа сводится к «я д’Артаньян, а вы — лоботрясы».
Насчёт понимания/непонимания преимущества MSAA над SSAA — при чём тут это вообще? Кроме того, по крайней мере на мобильных устройствах, падение производительности в три раза при MSAA — это довольно постоянная вещь, на сцене с меньшим количеством геометрии результат тот же.
Что касается производительности — да, наш способ медленнее MSAA. В итоге, пользователь получает на выходе 4.5 FPS против 5 FPS. Офигенно критичная разница. Ок, пускай так. Только наш АА выключается автоматом и быстро, а включать и выключать MSAA на лету можно, но это требует заметно больше времени. Особенно хватает нюансов с этим на андроиде. Да и не только там, на iOS тоже. У вас, видимо, какая-то специальная параллельная реальность, где ни отключение MSAA, ни отрисовка отдельных кадров не требуют времени, а мобильные устройства, вероятно, ничем не отличаются от десктопа. При этом вы сами сказали, что не работали с iOS — но утверждаете, что там всё есть, мы «просто не разобрались, как с ним работать».
Вообще, понятно что решать одну и ту же задачу (как, например, в нашем случае — отрисовка «опрятных» графиков) можно по-разному. В статье мы просто хотели показать, что иногда приходится запиливать некрасивые костыли во имя красивого результата.
Во-первых, координаты стенки именно что те же самые. А во-вторых, хорошо, что вам повезло так и не столкнуться с такой вот ситуацией, когда то, что вы называете погрешностью интерполяции, возникает на границе перпендикулярных полигонов. Но это же не значит, что так в принципе не бывает.
По сути, основную идею вы сами озвучили: действительно, сначала делаем SSAA, затем поверх него FXAA. Общая производительность в целом получается такой же, как у MSAA, но выигрыш в том, что на периоды воспроизведения анимации мы отключаем антиалиасинг вообще. Поэтому у пользователя складывается ощущение плавности движений — но, конечно, ценой того, что пока сцена в движении, она не сглаживается.
Насчёт производительности SSAA: по нашим замерам, производительность SSAA лишь чуть-чуть меньше, чем у MSAA, можно сказать, такая же. Но у нас нет рейтрейсинга, возможно, с ним ситуация поменяется. Вот, для примера, цифры одного из тестов: iPad3, сцена 1 600 000 треугольников, освещение по Фонгу (то есть рендеринг сам по себе простой). Без АА: 15 fps, MSAA: 5 fps, SSAA: 4.5 fps, FXAA: 8 fps, наш гибрид (когда он включен): 4.5 fps.
Ну да, мультитред — это сложно :) Но как раз тут это оказалось важно. Особенно для мобилок, которые, с одной стороны, довольно маломощные, с другой — во многих по два и больше ядер, так что два потока там хорошо уживаются и дают ощутимый для пользователя результат. Ну и велосипед — хотя, конечно, и не двадцать строчек кода — не такой уж и огромный :-)
Да, насчёт шейдера — да, сам по себе механизм не такой сложный. Но тут проблема не только в механизме, но ещё и в рефакторинге имеющегося кода.
В играх стараются не допускать ситуации, которую мы показали тут: habrahabr.ru/post/230671/#comment_7811885
Мы тоже стараемся её избегать, но вот не всегда получается.
И это таки не фон, а стенка. На том скриншоте, что в статье, просто так совпало по цветам. Вот другой скриншот:
Ну и именно из-за того, что стенка имеет одни и те же координаты с боковинами, происходит ситуация «борьбы» в z-буфере, потому что на этапе растеризации у фрагментов разных полигонов оказывается одинаковая глубина. И из-за ограниченной точности z-буфера появляются места, где фрагменты проходят тест 50/50: часть от одного полигона, часть от другого. Вот, например, скриншот из Blender3D. Ситуация там несколько иная, там площадь перекрытия у полигонов большая. Но суть та же:
Так во внешнем виде её особой и нет. Есть разница в производительности, потому что наш АА можно быстро отключить на период проигрывания анимации и включить обратно, когда картинка «устаканилась». Кроме того, у нас же не только постпроцессинговый, у нас гибрид SSAA и FXAA.
Да, и плюс ещё обсчёт анимаций и все вспомогательные преобразования. Но ещё раз: это не главное, главное — обратная ситуация.
В общем случае, действительно, обновление данных стоит выносить в отдельный поток. Но мы-то пишем компонент, который должен быть готов к разным ситуациям. И наличие плавных анимаций, не зависящих от нагруженности main thread-a — важный момент. «Когда что-то изменилось» включает в себя и воспроизведение анимаций, естественно.
Более того, подход разделения на main thread и rendering thread — это, мягко говоря, не наше изобретение, это best practice. Например, так работает Apple CoreAnimation.
В описанной ситуации — наглухо.
По первой проблеме — вы как-то ловко пропустили тот момент, что при включении scissor test’а погрешность изменяется. Отрисовку сложной трёхмерной сцены, к сожалению, не получается свести к “xcoord = fullsize*i/5.0;”. График должен быть интерактивным, то есть должнен скроллиться и зумиться. Поэтому работа идёт в двух системах координат: сцены (где график, на неё действует масштаб и смещение, она ограничена scissor'ом) и экрана (где оси, она «статична», подвижны лишь риски на осях).
Для системы координат экрана используется матрица проекции, которую породил бы, например, вызов gluOrtho2D(0, screen_width, 0, screen_height).
Для двумерной сцены используется система координат, которую породил бы gluOrtho2D(-1, 1, -1, 1), чтобы по максимуму унифицировать случаи 2d и 3d графиков.
Чтобы риски на осях при скролле/зуме двигались вместе с графиком, мы вычисляем их координаты исходя из проекций соответствующих точек в системе координат сцены на систему координат экрана. Вот тут-то появляются те самые погрешности.
Дальше, про стыковку — ну да, такая проблема почти всюду. Но в условиях вышесказанного она иногда принимает весьма непредсказуемый характер, потому что сдвинуть что-то «на пиксель», когда оно подвержено довольно сложным многоступенчатым преобразованиям — не так-то просто.
Триангуляция линий — всё правильно. Но опять же нюанс, его вот уже тут озвучили: habrahabr.ru/post/230671/#comment_7810005
И проблема даже не в ретесселяции. Проблема в том, чтобы держать толщину линий одинаковой на разных масштабах контура. Не сказать, что это прямо так уж сложно, но сомнительно, что удастся уложиться в пару десятков строк кода :)
Про дырки между полигонами — там мы как раз ничего не прибавляем. И круг мы замыкаем (точнее, используем для начала следующего сектора координаты конца предыдущего). Тем не менее, такой вот спецэффект присутствует. Проблема в том, что у секторов есть ещё стенки (так как они могут выезжать из пирога и висеть рядом с ним). И вот то, что видно — это как раз не фон, а эта самая стенка, потому что в одном месте оказываются два стыкующихся полигона и один перпендикулярный им <картинка>. Погрешность растеризации вызывает как этот самый перпендикулярный полигон, который по идее скрыт стоящими рядом с ним, но из-за нехватки точности буфера глубины самый близкий к наблюдателю ряд точек проходит тест глубины и рисуется. Как избежать такой проблемы, мы пока что не придумали (очень не хочется костылить удаление стенки или же делать что-то с порядком вывода полигонов, потому что сейчас вся конструкция рисуется за один draw-call).
Про системный антиалиасинг Вы всё верно говорите и рисуете. Если в итоге у линии будут «правильные» координаты, она не размажется. Но проблема в том, что, как мы уже описали выше, «правильные» координаты организовать сложно (много промежуточных преобразований). Ну и артефакты — не самое страшное в системном антиалиасинге. Были бы они одни — мы б, может быть, смирились. Но вот его тормоза мы уже пережить не смогли :) А если писать свой антиалиасинг — так за компанию и от артефактов защититься решили.
Про многопоточность. Во-первых, да, мы рисуем только тогда, когда что-то изменилось. Во-вторых, рендеринг, конечно, распараллелен в плане SIMD. Но не в плане, например, загрузки данных. Те же glTexImage2D или glBufferData вполне себе синхронные. И не в плане всех остальных действий, которые производятся нами же по соседству (расчёт анимаций, применение преобразований и прочее). И вот в сумме все эти действия могут занимать довольно много времени, и их хочется производить где-то не там, где крутится обработка действий пользователя. Но главное даже не это (в большинстве случаев рендеринг действительно проходит быстро). Главное — это обратная ситуация: когда заблокирован main-thread, например, при обновлении данных. Или, ещё пример, на iOS, если на экране есть table view controller со включенной «пружинкой» и пользователь оттянул эту пружинку и держит, главный поток стоит
с замиранием сердца ждёт, что же будет. И вот тут-то хотелось бы, чтобы анимация продолжала плавно проигрываться. Ну а велосипед наш работает довольно быстро — по крайней мере, поставленную задачу решает и обеспечивает удобный механизм анимаций.Библиотека коммерческая, так что конкретные применения остаются за клиентами — мы-то делаем компонент, а не прикладные приложения. Есть демо-приложение (для iOS и мака), правда, там рандомные данные. Собственно, давайте данные — отрисуем :)
Из прикладного с реальными данными есть под рукой вот эти две: множественное выравнивание ДНК и спектрограмма звука. Последняя, кстати, в 3д лучше.
А что до невозникания проблем — так основные проблемы с точностью как раз касаются двумерных диаграмм. Четыре проблемы из шести про них.
Здесь иллюстрировались не научные данные, а возможности рендеринга, так что мы поотключали все милые сердцу подписи (они все включаются — и для данных, и для осей), выбрали ракурс понаряднее (всё трёхмерное можно вращать и масштабировать) и пообъёмнее (3d — это плюшка, а не единственно возможный вариант).
Мы прекрасно понимаем, что 3D в научных диаграммах не нужен, за редким исключением (типа спектрограммы линейного входа, как первая из 4-х картинок в конце — она ещё и обновляется в реальном времени) — но с всем вышеперечисленным технических трудностей при реализации не возникло :)
p.s. а за статью про паи спасибо, сложу в личную копилку аргументов :)
Во-вторых, c Qt есть проблемы, если хочется зарелизить iOS-приложение с закрытыми исходниками. Аппстор требует, чтобы все использованные библиотеки лежали внутри в статике, а там с этим сложности: qt-project.org/wiki/Licensing-talk-about-mobile-platforms.
Во-третьих, как показывает практика, Qt не такой уж кроссплатформенный. Он великолепно подходит, когда надо что-то быстро сваять на коленке — но как только доходит дело до продакшена, его ненативность быстро начинает раздражать конечных пользователей.
В-четвёртых, у нас сделаны нативные оболочки для iOS и андроида (на Objective-C и Java), так что пользователь может не заморачиваться по поводу написания на С++.