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 освещением: Несмотря на многочисленные свидетельства того, что 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: Отладка вёб-приложения в мобильном браузере удовольствие весьма сомнительное, поэтому первым делом были подобраны альтернативные конфигурации с 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 шейдере кодом: Пара скриншотов внизу показывает как бы выглядели 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 важен для корректной цветопередачи. К сожалению, 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, которая уже давно используется другими браузерами), хотя сроки, как обычно, остаются неизвестными. Оригинальную публикацию на английском можно найти здесь.