WebGL 2.0 вышел в далёком 2017ом году, принёс графический стек OpenGL ES 3.0 (2012го года), и, казалось бы, все современные браузеры давно должны были его поддерживать. Однако, среди лидеров затесались отстающие, и пользователи Safari до сих пор (начало 2021го) вынуждены ограничиваться возможностями WebGL 1.0, опубликованным в 2011ом году на основе OpenGL ES 2.0.
Те разработчики, что сталкивались с OpenGL ES 2.0, знают не понаслышке насколько ограниченным является этот графический стек. Ограничения программного интерфейса во многом отражали немощность мобильных графических карт того времени, поэтому массовый переход Android устройств на OpenGL ES 3.0 несколько лет назад оказался очень кстати, хоть и начался с серьёзным запозданием от десктопных видеокарт.
Браузерные технологии оказались ещё более инертными - в то время как Android устройства уже давно поддерживают OpenGL ES 3.2 с вычислительными шейдерами, внедряют поддержку Vulkan, а разработчики web-стандартов готовят WebGPU, обычному разработчику по-прежнему доступен устаревший уже на момент публикации WebGL 2.0, а то и вовсе WebGL 1.0 каменного века…
Некоторое время назад, графический движок C++ фреймворка Open CASCADE Technology (OCCT) обзавёлся поддержкой PBR (основанной на физике освещению) модели материалов металл-шероховатость (metal-roughness). Продвижение формата glTF 2.0, известного как “JPEG для 3D”, способствовало выводу этой модели материалов как стандарта обмена между графическими движками.
PBR освещение требует существенно больше вычислительных ресурсов по сравнению с устаревшими эмпирическими моделями Гуро/Фонга времён OpenGL 1.1. Поэтому неудивительно, что реализация PBR в движке была изначально написана на основе относительно “современного” графического стека OpenGL 3.0 (выпущенного в 2008ом году!) и адаптированного под его мобильную версию OpenGL ES 3.0.
Однако тестирование графического движка в браузерах (в виде модуля WebAssembly) выявило уже озвученную аномалию - браузерный движок WebKit в основе одного из самых распространённых браузеров Safari до сих пор полноценно не поддерживает WebGL 2.0! И если пользователи Safari на macOS могут отделаться лёгким шоком и установить браузер посовременнее, то пользователи iOS такой возможности лишены политикой Apple. В AppStore можно найти альтернативные браузеры, но к сожалению все они основаны на том же движке WebKit, встроенном в систему, что и Safari - в силу ограничений магазина AppStore у разработчиков других браузеров просто нет выбора.
При этом вполне очевидно, что ограничение WebGL 1.0 связаны именно с программной стороной, ведь графические процессоры мобильных устройств Apple давно считаются относительно производительными, да и OpenGL ES 3.0 поддерживается на iOS нативными приложениями.
Более того, уже некоторое продолжительное время Safari имеет опцию, активирующую экспериментальную поддержку WebGL 2.0. На практике, экспериментальная опция всё ещё не проходит ряд важных тестов, хотя прогресс на лицо - WebGL 2.0 уже почти работает в iOS 14. И действительно, с локальным патчем для Emscripten, обходящим баги реализации экспериментального WebGL 2.0, мне удалось увидеть пример OCCT с работающим PBR освещением:
function _glUniform4fv(location, count, value) {
GL.validateGLObjectID(GL.uniforms, location, 'glUniform4fv', 'location');
if (GL.currentContext.version >= 2) {
// WebGL 2 provides new garbage-free entry points to call to WebGL.
// Use those always when possible.
- GLctx.uniform4fv(GL.uniforms[location], HEAPF32, value>>2, count*4);
- return;
+ //GLctx.uniform4fv(GL.uniforms[location], HEAPF32, value>>2, count*4);
+ //return;
}
...
Мини-вызов: OCCT PBR на WebGL 1.0
Несмотря на многочисленные свидетельства того, что Safari вот-вот обзаведётся поддержкой WebGL 2.0, текущие пользователи по-прежнему страдают от его отсутствия (ну или радуются экономии заряда батареи). Некоторые графические движки прямо заявляют, что не поддерживают PBR освещение без WebGL 2.0, однако мне стало любопытно, реалистично ли запустить PBR на WebGL 1.0 и с какими ограничениями.
Впрочем, конечной, целью была выбрана не поддержка “голого” WebGL 1.0, а запуск PBR на современных устройствах iPad с доступными расширениями WebGL. Вот список таких расширений для устройства iPad ‘2020 (Apple A12 Bionic) на iOS 14.4 / Safari:
EGLVersion: 1.4 Emscripten EGL
EGLVendor: Emscripten
EGLClientAPIs: OpenGL_ES
GLvendor: WebKit
GLdevice: WebKit WebGL
GLunmaskedVendor: Apple Inc.
GLunmaskedDevice: Apple GPU
GLversion: OpenGL ES 2.0 (WebGL 1.0)
GLSL: OpenGL ES GLSL ES 1.00 (WebGL GLSL ES 1.0 (1.0))
Max texture size: 16384
Max FBO dump size: 16384x16384
Max combined texture units: 32
Viewport: 1560x1080
GLextensions: GL_EXT_blend_minmax GL_EXT_sRGB GL_OES_texture_float
GL_OES_texture_half_float GL_OES_texture_half_float_linear
GL_OES_standard_derivatives GL_EXT_shader_texture_lod
GL_EXT_texture_filter_anisotropic GL_OES_vertex_array_object
GL_OES_element_index_uint GL_WEBGL_lose_context GL_WEBGL_compressed_texture_astc
GL_WEBGL_compressed_texture_etc GL_WEBGL_compressed_texture_etc1
GL_WEBKIT_WEBGL_compressed_texture_pvrtc GL_WEBGL_depth_texture
GL_ANGLE_instanced_arrays GL_WEBGL_debug_shaders GL_WEBGL_debug_renderer_info
GL_EXT_color_buffer_half_float
Отладка вёб-приложения в мобильном браузере удовольствие весьма сомнительное, поэтому первым делом были подобраны альтернативные конфигурации с WebGL 1.0:
Отключение WebGL 2.0 в скрытых опциях Firefox.
Управляется опцией “
webgl.enable-webgl2=false
” на странице “about:config
”.Предоставляет аппаратно-ускоренную реализацию WebGL 1.0, допускающую некоторые отклонения от WebGL 1.0 спецификаций на железе уровня WebGL 2.0.
Поддерживает вывод JavaScript консоли.
Отключение WebGL 2.0 в опциях сборки Emscripten.
Управляется флагом сборки “
MAX_WEBGL_VERSION=1
”.Предоставляет аппаратно-ускоренную реализацию WebGL 1.0.
Поддерживает JavaScript консоль (в десктопных браузерах).
Отключение аппаратного ускорения в браузерах на движке Chromium в паре с опцией сборки “
MAX_WEBGL_VERSION=1
”.Предоставляет программную реализацию WebGL 1.0, которая обычно ближе придерживается к спецификациям по сравнению с аппаратно-ускоренными реализациями.
Поддерживает JavaScript консоль (в десктопных браузерах).
Сборка Draw Harness на десктопе с опцией OpenGL ES, реализованной библиотекой ANGLE.
Использует ту же реализацию OpenGL ES, которую используют десктопные браузеры.
Команда “
vcaps -maxversion 2 0
” активирует создание OpenGL ES 2.0 контекста (вместо OpenGL ES 3.0).Очень полезный и удобный вариант для отладки, однако поведение не идентично вёб-приложению с дополнительным уровнем WebGL реализации.
Запуск в браузере Safari на macOS.
Предоставляет аппаратно-ускоренную реализацию WebGL 1.0.
Поведение на Apple M1 (ARM64) очень близко в iPad, но есть расхождения!
Поддерживает вывод JavaScript консоли.
Запуск в браузере Safari на iOS.
Предоставляет аппаратно-ускоренную реализацию WebGL 1.0.
Нет JavaScript консоли.
Разные комбинации дают отличные результаты - разный набор расширений, разные ошибки, разные баги… а в сумме много полезной информации для отладки. Прослеживается одна характерную особенность WebGL реализаций - там где проприетарные драйвера OpenGL и OpenGL ES допускают отклонения, в случае если железо в принципе поддерживает какую-то функциональность, реализации WebGL упрямо стоят на своём и ругаются на синтаксические ошибки в коде GLSL.
Наиболее упрямыми реализациями оказались программный WebGL, реализуемый средствами библиотеки ANGLE, а также реализация OpenGL поверх Metal от Apple. Там где CG компилятор NVIDIA не скажет ни слова, драйвер AMD мягко предупредит в логе компиляции шейдера, OpenGL реализация Apple не оставит безобразие без внимания и ошибкой скажет, что такой функции в GLSL 110 нет и появилась она только в GLSL 120!
Портирование кода PBR на WebGL 1.0 было встречено следующими проблемами:
Загрузка данных PBR таблицы-кеша
128x128
форматаGL_RG32F
в текстуру форматаGL_RG16F
.Проблема #1: текстурные форматы
GL_RG32F
/GL_RG16F
не поддерживаются iPad + WebGL 1.0 (расширение GL_EXT_texture_rg недоступно).Проблема #2: текстуры формата
GL_RGBA32F
не поддерживают фильтрацию на iPad + WebGL 1.0. iPad не поддерживает расширение GL_OES_texture_float_linear, однако нефильтруемые текстуры с плавающей запятой поддерживается через расширение GL_OES_texture_float. В тоже время, iPad поддерживает расширение GL_OES_texture_half_float_linear, так что текстуры с плавающей точкой половинчатой точности поддерживают фильтрацию.Проблема #3: текстуры формата
GL_RGBA16F
могут быть загружены напрямую из данных плавающей запятой одинарной точности в случае с OpenGL ES 3.0 / WebGL 2.0, однако WebGL 1.0 + GL_OES_texture_half_float не допускает этого.
Запекание спекулярной PBR карты в текстуру
9x1 GL_RGBA32F
.Проблема: в текстуру формата
GL_RGBA32F
нельзя производить отрисовку через FBO на iPad + WebGL 1.0. iPad не поддерживает расширение WEBGL_color_buffer_float.
Запекание мип-уровней диффузной PBR кубической текстуры
512x512x6
GL_RGBA8
.Проблема: iPad + WebGL 1.0 не допускают отрисовку в мип-уровни, отличные от нулевого (расширение GL_OES_fbo_render_mipmap не поддерживается).
PBR GLSL программы полагаются на явное задание мип-уровня текстуры, в зависимости от шероховатости материала.
Проблема:
textureCubeLod()
недоступна вGLSL 100 es
, но доступна посредством расширения GL_EXT_shader_texture_lod на iPad + WebGL 1.0.
PBR GLSL программы содержат большие блоки циклов, ветвления и оператор модуля
%
.Проблема #1: оператор модуля
%
недоступен вGLSL 100 es
, но может быть заменён функциейmod()
.Проблема #2:
GLSL 100 es
не предусматривает синтаксиса для инициализации массива констант.Проблема #3:
GLSL 100 es
не допускает неконстантные выражения для определения индекса (non-constant index expressions).
Буфер цвета sRGB и кубическая карта окружения.
Проблема #1: расширение GL_EXT_sRGB доступно на iPad + WenGL 1.0, но требует иных констант для инициализации, а также запрещает генерацию мип-уровней посредством
glGenerateMipmap()
.Проблема #2: ужасно медленная генерация мип-уровней для sRGB текстур на WebGL 2.0 (5 секунд!).
Поиск решений
Случайное использование нефильтруемые форматы текстур - одна из самых противных проблем в OpenGL. Логи выглядят абсолютно чистыми - ни ошибок glGetError()
, ни предупреждений WebGL в консоли, просто на экран выводится некорректный результат. С практической точки зрения, нефильтруемая текстура возвращает нули в GLSL программу, и требуется приличное время для того, чтобы обнаружить источник проблемы.
Загрузка GL_RGBA16F
текстуры в случае с WebGL 1.0 + GL_OES_texture_half_float требует программной реализации конвертера 32битных чисел с плавающей запятой в 16битные - ведь центральные процессоры и C/C++ не имеют встроенной поддержки чисел с половинной точностью. OpenGL 3.0 и OpenGL ES 3.0 позволяют избежать этого чудного кода, а вот для поддержки WebGL 1.0 придётся его добавить в приложение. В процессе отладки удалось запечатлеть вот такой забавный эффект при интерпретации GL_RGBA32F
данных как массива GL_RGBA16F
:
Невозможность отрисовки в текстуру формата GL_RGBA32F
стала неприятной проблемой для реализации PBR, так как меньшая точность будет недостаточно для данной текстуры. К счастью, PBR спекулярная карта имеет размер всего 9x1 текселей - можно было бы даже подумать о вычислении значений без помощи OpenGL, если бы это не тянуло за собой необходимость реализовать выборки с фильтрацией из кубической текстуры… Вместо этого, следующий подход был реализован: значения с плавающей запятой упаковываются шейдером в текстуру формата 9x3 GL_RGBA8
(по строке на RGB компоненту), затем читаются с посредством glReadPixels()
, распаковываются и загружаются в финальную текстуру формата GL_RGBA32F
.
Заполнение мип-уровней PBR диффузной кубмапы также заставило задуматься, но обходной путь оказался проще - отрисовка во временную текстуру и копирование результата в нужный мип-уровень посредством glCopyTexImage2D()
. Как ни странно, использование нулевого мип-уровня той же самой текстуры сработало, хотя не могу с уверенностью сказать, что такая логика не чревата неопределённым поведением.
Без функции textureCubeLod()
в шейдере практически невозможно реализовать корректное поведение PBR освещение с разными уровнями шероховатости, но к счастью, все тестируемые реализации WebGL 1.0 поддерживали расширение GL_EXT_shader_texture_lod, активируемое в GLSL шейдере кодом:
+#extension GL_EXT_shader_texture_lod : enable
+#define textureCubeLod textureCubeLodEXT
Пара скриншотов внизу показывает как бы выглядели PBR материалы, если просто заменить textureCubeLod()
на textureCube()
, т.е. на автоматический выбор мип-уровней текстуры вместо ручного на основании шероховатости:
Многочисленные ограничения синтаксиса GLSL 100 es
не раз заставляли задумываться о напрасной борьбе с тенью прошлого. И если оператор модуля %
легко заменяется на функцию mod()
, а прочие ограничения могут ввести в ступор:
Ранние версии GLSL просто не предусматривали синтаксис для инициализации массива констант:
> const float aSHBasisFuncCoeffs[9] = float[9] { 0.0, 1.0, 2.0, … };
Выходом из этой проблемы послужило объявление таких массивов как uniform переменных, загружаемых из C/C++ кода - что, в общем-то, не тоже самое, что константа уровня компиляции, но по производительности может быть близка к этому.
Переменное количество итераций цикла
for(;;)
. Программа запекания PBR текстур задаёт несколько параметров, задающих точность (качество):> uniform int uSamplesNum;
> for (int aSampleIter = 0; aSampleIter < uSamplesNum; ++aSampleIter) {}Большее количество выборок увеличивают качество, но требуют более тяжёлых расчётов, поэтому эти параметры были вынесены в настройки, и более того, автоматически калибруются в зависимости от мип-уровня текстуры. Ограничения
GLSL 100 es
нарушают эту логику -uniform
переменная должна быть заменена на константу компиляции. Хотя типичным обходным путём может быть также написание такого цикла:> uniform int uSamplesNum;
> int TheMaxSamples = 1024;
> for (int aSampleIter = 0; aSampleIter < TheMaxSamples; ++aSampleIter) {
> if (aSampleIter >= uSamplesNum) { break; }
> }
“Non-constant index expressions are disallowed”. Программа запекания PBR карт использует такие конструкции:
> int anIndex = int(gl_FragCoord.x);
> float aCoef = aSHCosCoeffs[anId] * aSHBasisFuncCoeffs[anId];Ограничения
GLSL 100 es
приводят к написанию следующего ужасного кода с использованием if/else.> if (anId == 0) { aCoef = aSHCosCoeffs[0] * aSHBasisFuncCoeffs[0]; }
> else if (anId == 1) { aCoef = aSHCosCoeffs[1] * aSHBasisFuncCoeffs[1]; }
> else if (anId == 2) { aCoef = aSHCosCoeffs[2] * aSHBasisFuncCoeffs[2]; }
Большинство ограничений языка шейдеров GLSL 100 es
являются не более чем эхом графических процессоров прошлого, не поддерживающих ветвление. И хотя синтакс GLSL заделывался на будущее, первая версия требовала написание циклов и условий ветвлений, которые могли быть тривиально раскручены (trivially unrolled) компилятором, т.к. графические процессоры того времени иного варианта просто не поддерживали.
sRGB текстуры
Рендеринг с учётом цветового пространства sRGB важен для корректной цветопередачи. К сожалению, WebGL 1.0 + GL_EXT_sRGB имеет существенное ограничение - невозможность генерации мип-уровней. Это делает поддержку sRGB текстур практически бесполезной, если только не использовать форматы с предварительно подготовленными мип-уровнями. И если в случае обычных текстур мип-уровнями можно пожертвовать (снизится качество картинки), то при запекании PBR карт из кубмапы окружения просто обойтись без мип-уровней уже нельзя.
Но даже в случае с WebGL 2.0, формально поддерживающим генерацию мип-уровней sRGB текстур, данная функциональность реализована чудовищно медленной. Генерация мип-уровней кубмапы размером 2048x2048x6 выполняется 5 секунд на быстром десктопном компьютере! При этом тот же самый код отрабатывает за считанные доли секунд при использовании нативной OpenGL / OpenGL ES реализации вместо браузера.
Всё указывает на то, что WebGL реализует какой-то код не на графическом процессоре - возможно с целью обеспечения строгих условий спецификаций WebGL в контексте конвертации цветового пространства sRGB, которым аппаратные реализации не следуют, хотя мне и не удалось найти какие-либо упоминания на эту тему. По всей видимости, в WebGL лучше избегать sRGB текстуры (ценой качества картинки или искажений цветового пространства), обеспечить загрузку предварительно подготовленных мип-уровней текстур, или реализовать генератор mip-map уровней собственной шейдерной программой.
Послесловие
В результате потраченных усилий наконец-то удалось увидеть PBR материалы посредством графического движка OCCT в Safari на iPad c контекстом WebGL 1.0. Конечно, тестируемое устройство относится к относительно новому поколению (iPad ‘2020, основанному на Apple A12 SoC анонсированной в 2018ом году), но есть надежда на то, что более старые устройства Apple также справятся с задачей.
Хочется верить, что уже в скором времени WebGL 2.0 наконец-то станет минимальным стандартом и необходимость в поддержке допотопных технологий десятилетней давности отпадёт - многие разработчики уже несколько лет отказывают поддерживать такие браузеры и их решение легко понять.
В этом контексте, отказ Microsoft от поддержки устаревших движков вроде Internet Explorer и Microsoft Edge Legacy (базирующимся на не-Chromium движке) ощущается как свежий воздух для вёб-разработчика, измученного проблемами совместимости. Хотя исчезающе малое количество конкурирующих полноценных вёб-движков не может не настораживать (Mozilla Firefox, Chromium, Safari/WebKit) и опасаться за будущее открытых вёб-стандартов в мире, где один браузер станет бесконтрольно доминировать.
Стратегия Apple по удержанию экосистемы iOS под колпаком и не пускать решения конкурентов лишает пользователей системы собственного выбора. Многочисленные слухи свидетельствуют, что измученная экспериментальная поддержка WebGL 2.0 в движке WebKit вот-вот перестанет быть экспериментальной (в том числе благодаря переходу на реализацию OpenGL ES библиотекой ANGLE, которая уже давно используется другими браузерами), хотя сроки, как обычно, остаются неизвестными.
Оригинальную публикацию на английском можно найти здесь.
Elsajalee
Это, оказывается, WebKit… Как и firefox и всё остальное.
apps.apple.com/ru/app/chrome-браузер-от-google/id535886823#?platform=iphone
Честно, не знал до этой статьи.
gkv311 Автор
По той ссылке из статьи надо аккуратно прочитать ответ ;):
То есть современный Chrome сейчас базируется на собственном форке движка WebKit, который развивается независимо. Поэтому Chrome на macOS нормально выдаёт WebGL 2.0.
Но это НЕ касается версии Chrome для iOS.
Elsajalee
Уже прочитал :)
Вопросы правда не по теме появились: например, как так получилось, что даже MS когда-то обязали делать выбор браузера ( habr.com/ru/post/85925 ), а Apple по сути практически запрещает ПО такого типа, разрешая только обёртку.
static_cast
Потому что Apple двигает Metal. Сейчас повторяется ситуация DirectX vs OpenGL пятнадцатилетней давности.
gkv311 Автор
Как раз сейчас пытаются запускать подобные процессы (вроде "Epic Games против Apple", но там дело больше в жадности, нежели в борьбе за справедливость и свободы пользователей), но, очевидно, с очень большим запозданием.
Искусственные ограничения конкуренции и свобод пользователей были очевидны с самого запуска iPhone, и антимонопольные структуры должны были начать давить на Apple как только стало понятно, что их продукты стали массовыми.
Но тут есть и другие неудачные прецеденты, вроде тех же игровых приставок от Sony и Microsoft, таких же закрытых и навязывающих свои условия — практику, которую они ввели задолго до появления iOS…
ivinnic
Это шок, до сегодняшнего дня я был уверен что WebGl2 на iphone будет работать в хроме. Но сейчас я как будто упал с небес на землю. Как же так?? Может я все же, что то не правильно понял?
gkv311 Автор
Да, всё именно так — достаточно зайти на айфоне на WebGL Report страничку.
Думаю, если бы на iOS были бы альтернативы, то можно было бы и забросить эту затею с мучениями на WebGL 1.0, описанными в статье…
Хотя есть некоторые причины ожидать, что одной проблемой станет меньше с очередным крупным обновлением iOS.