Лучшие практики WebGL
WebGL — сложный API, и часто не очевидно, как его использовать оптимально. Эта страница содержит рекомендации для разных уровней экспертизы, и не только указывает, что делать и чего не делать, но также объясняет почему. Вы можете использовать это руководство, чтобы выбрать правильный подход и быть уверенными в том, что ваш код работает правильно на любом браузере и с любым оборудованием.
Обработка и устранение ошибок WebGL
Ваше приложение должно работать без генерации ошибок WebGL (как возвращаемых getError). Каждая ошибка WebGL отображается в консоли браузера в качестве JavaScript-предупреждения с описательным сообщением. После слишком большого количества ошибок (32 в Firefox) WebGL прекращает генерировать описательные сообщения, что сильно затрудняет отладку.
Единственные ошибки, которые должна генерировать правильно сформированная страница, это OUT_OF_MEMORY и CONTEXT_LOST.
Понимание доступности расширений
Доступность большинства расширений WebGL зависит от системы клиента. При использовании расширений WebGL, если это возможно, постарайтесь сделать их необязательными, обеспечив плавную адаптацию к случаям, когда они не поддерживаются.
Эти расширения WebGL 1 универсально поддерживаются и могут использоваться без опасений:
- ANGLE_instanced_arrays
- EXT_blend_minmax
- OES_element_index_uint
- OES_standard_derivatives
- OES_vertex_array_object
- WEBGL_debug_renderer_info
- WEBGL_lose_context
(см. также: Уровни поддержки функций WebGL и % поддержка)
Рассмотрите возможность добавления полифиллов для этих расширений в WebGLRenderingContext, например: https://github.com/kdashg/misc/blob/tip/webgl/webgl-v1.1.js
Понимание системных ограничений
Аналогично расширениям, ограничения вашей системы могут отличаться от ограничений систем ваших клиентов! Не предполагайте, что вы можете использовать тридцать текстурных сэмплеров на шейдер просто потому, что это работает на вашей машине!
Минимальные требования к WebGL довольно низкие. На практике практически все системы поддерживают, по крайней мере, следующее:
MAX_CUBE_MAP_TEXTURE_SIZE: 4096 MAX_RENDERBUFFER_SIZE: 4096 MAX_TEXTURE_SIZE: 4096 MAX_VIEWPORT_DIMS: [4096,4096] MAX_VERTEX_TEXTURE_IMAGE_UNITS: 4 MAX_TEXTURE_IMAGE_UNITS: 8 MAX_COMBINED_TEXTURE_IMAGE_UNITS: 8 MAX_VERTEX_ATTRIBS: 16 MAX_VARYING_VECTORS: 8 MAX_VERTEX_UNIFORM_VECTORS: 128 MAX_FRAGMENT_UNIFORM_VECTORS: 64 ALIASED_POINT_SIZE_RANGE: [1,100]
Ваш компьютер может поддерживать текстуры размером 16К или, возможно, 16 текстурных единиц в вершинном шейдере, но большинство других систем не поддерживают, и код, работающий у вас, может не работать у них!
Избегайте недействительности привязок подключений FBO
Практически любое изменение привязок подключений FBO сделает недействительным его буфер кадра. Настройте необходимые буферы заранее.
В Firefox, установка префа webgl.perf.max-warnings на -1 в about:config включит предупреждения о производительности, включая предупреждения об invalidations FB completeness.
Избегайте изменения привязок VAO (vertexAttribPointer, disable/enableVertexAttribArray)
Отрисовка из статических, неизменных VAO быстрее, чем изменение одного и того же VAO для каждого вызова отрисовки. Для неизменных VAO браузеры могут кэшировать пределы извлечения, в то время как при изменении VAO браузеры должны перепроверить и пересчитать пределы. Накладные расходы на это относительно невелики, но повторное использование VAO также означает меньше вызовов vertexAttribPointer, поэтому это стоит делать, где это просто.
Удалите объекты немедленно
Не ждите, пока сборщик мусора/сборщик циклов поймут, что объекты стали ненужными, и удалите их. Реализации отслеживают жизнеспособность объектов, поэтому «удаление» их на уровне API только освобождает указатель, который ссылается на фактический объект. (понятийно освобождая указатель ссылки на объект) Только когда объект не используется в реализации, он фактически освобождается. Например, если вам больше никогда не нужно обращаться к вашим объектам шейдеров напрямую, просто удалите их указатели после привязки их к объекту программы.
Немедленно освобождайте контексты
Также рассмотрите возможность немедленного освобождения контекстов WebGL с помощью расширения WEBGL_lose_context в тех случаях, когда вы определенно закончили с ними и больше не нуждаетесь в результатах рендеринга целевого холста. Обратите внимание, что это не обязательно, когда вы переходите от страницы - не добавляйте обработчик события unload только для этой цели.
Выполняйте flush при ожидании результатов
Вызовите flush() при ожидании результатов, таких как запросы, или по завершении кадра рендеринга.
Flush указывает реализации отправить все ожидающие команды на выполнение, очистив очередь, вместо ожидания поступления большего количества команд в очередь перед отправкой на выполнение.
Например, следующее может никогда не завершиться без потери контекста:
sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); glClientWaitSync(sync, 0, GL_TIMEOUT_IGNORED);
WebGL по умолчанию не имеет вызова SwapBuffers, поэтому flush может помочь заполнить пробел.
Используйте webgl.flush() при отсутствии requestAnimationFrame
Когда не используется RAF, используйте webgl.flush() для стимулирования немедленного выполнения очереди команд.
Поскольку RAF непосредственно предшествует границе кадра, явный webgl.flush() фактически не нужен с RAF.
Избегайте блокирующих вызовов API в производственном коде
Некоторые точки входа WebGL, включая getError и getParameter, вызывают синхронные зависания в потоке вызова. Даже базовые запросы могут занимать до 1 мс, но они могут занимать ещё больше времени, если им нужно ждать завершения всей графической работы (с эффектом, похожим на glFinish() в нативном OpenGL).
В производственном коде избегайте таких точек входа, особенно в основном потоке браузера, где они могут вызвать подтормаживание всей страницы (часто включая прокрутку или даже весь браузер).
-
getError(): вызывает flush + обмен с получением ошибок из процесса GPU).Например, в Firefox glGetError проверяется только после выделения памяти (
bufferData,*texImage*,texStorage*) для получения ошибок GL_OUT_OF_MEMORY. -
getShader/ProgramParameter(),getShader/ProgramInfoLog(), другиеgetдля шейдеров/программ: flush + компиляция шейдеров + обмен, если это не сделано после завершения компиляции шейдеров. (См. также параллельную компиляцию шейдеров ниже.) -
get*Parameter()в целом: возможный flush + обмен. В некоторых случаях они будут кэшироваться, чтобы избежать обмена, но старайтесь не полагаться на это. -
checkFramebufferStatus(): возможный flush + обмен. -
getBufferSubData(): обычный финиш + обмен. (Это приемлемо для буферов ЧТЕНИЯ в сочетании с заборами - см. асинхронную обратную запись данных ниже.) -
readPixels()в ЦП (т.е. без привязанного буфера UNPACK): финиш + обмен. Вместо этого используйте GPU-GPUreadPixelsв сочетании с асинхронной обратной записью данных.
Всегда включайте вершинный атрибут 0 в качестве массива
Если вы рисуете без включенного вершинного атрибута 0 как массив, вы заставите браузер выполнять сложную эмуляцию при работе на настольном OpenGL (например, на macOS). Это потому, что в настольном OpenGL ничего не рисуется, если вершинный атрибут 0 не включен как массив. Вы можете использовать bindAttribLocation для принудительного использования вершинного атрибута с номером 0 и использовать enableVertexAttribArray(0) для включения его как массива.
Оцените бюджет VRAM на пиксель
WebGL не предоставляет API для запроса максимального объема видеопамяти на системе, потому что такие запросы не являются переносимыми. Тем не менее, приложения должны учитывать использование VRAM и не выделять как можно больше памяти.
Один из приемов, разработанный командой Google Maps, — это понятие бюджета VRAM на пиксель:
1) Для одной системы (например, конкретного настольного ПК/ноутбука) определите максимальный объем VRAM, который ваше приложение должно использовать. 2) Вычислите количество пикселей, охватываемых максимализированным окном браузера. Например, (window.innerWidth * devicePixelRatio) * (window.innerHeight * window.devicePixelRatio) 3) Бюджет VRAM на пиксель равен (1) деленному на (2) и является константой.
Эта константа обычно должна быть переносимой между системами. Мобильные устройства, как правило, имеют экраны меньшего размера, чем мощные настольные компьютеры с большими мониторами. Пересчитайте эту константу на нескольких целевых системах, чтобы получить надежную оценку.
Теперь настройте весь внутренний кэш приложения (WebGLBuffers, WebGLTextures и т. д.) таким образом, чтобы соблюдался максимальный размер, рассчитанный как эта константа, умноженная на количество пикселей, охватываемых текущим окном браузера. Это требует оценки количества байтов, потребляемых каждой текстурой, например. Предел также, как правило, должен обновляться при изменении размера окна браузера, а более старые ресурсы, превышающие лимит, должны удаляться.
Поддержание использования VRAM приложения в рамках этого предела поможет избежать ошибок «недостаточно памяти» и связанной с ними нестабильности.
Рассмотрите рендеринг в меньший буфер заднего плана
Распространенный (и простой) способ обмена качества на скорость — рендеринг в меньший буфер заднего плана и масштабирование результата. Рассмотрите возможность уменьшения canvas.width и canvas.height и сохранения canvas.style.width и canvas.height в неизменном размере.
Группировка вызовов отрисовки
«Группировка» вызовов отрисовки в меньшее количество, но более крупных вызовов, как правило, улучшает производительность. Если у вас есть 1000 спрайтов для отрисовки, попробуйте сделать это с помощью одного вызова drawArrays() или drawElements().
Часто используются «вырожденные треугольники», если нужно отрисовать несоседние объекты одним вызовом drawArrays(TRIANGLE_STRIP). Вырожденные треугольники — это треугольники с нулевой площадью, поэтому любой треугольник, где более одной точки находятся в одном и том же месте. Эти треугольники эффективно пропускаются, что позволяет начать новую полосу треугольников, не привязанную к предыдущей, без разделения на несколько вызовов.
Еще один важный метод группировки — это создание атласов текстур, где несколько изображений помещаются в одну текстуру, часто как в шахматном порядке. Поскольку вам нужно разделить группы вызовов отрисовки, чтобы изменить текстуры, использование атласов текстур позволяет объединить больше вызовов отрисовки в меньшее количество более крупных групп. См. этот пример, демонстрирующий, как объединить даже спрайты, ссылающиеся на несколько атласов текстур, в один вызов отрисовки.
Избегайте "#ifdef GL_ES"
Вы никогда не должны использовать #ifdef GL_ES в своих WebGL-шейдерах; это условие всегда истинно в WebGL. Хотя некоторые ранние примеры использовали это, это не нужно.
Предпочитайте выполнять операции в вершинном шейдере
Выполняйте как можно больше работы в шейдере вершин, а не в шейдере фрагментов. Это связано с тем, что шейдеры фрагментов, как правило, выполняются намного чаще, чем шейдеры вершин, на каждый вызов отрисовки. Любые вычисления, которые можно выполнить над вершинами, а затем просто интерполировать между фрагментами (через varyings), повышают производительность. (Интерполяция переменных очень дешевая и выполняется автоматически в рамках фиксированной функциональности фазы растеризации графического конвейера.)
Например, простую анимацию текстурированной поверхности можно получить путем временной трансформации координат текстуры. (Простейшим случаем является добавление однородного вектора к вектору атрибута координат текстуры) Если это визуально приемлемо, можно преобразовать координаты текстуры в шейдере вершин, а не в шейдере фрагментов, чтобы повысить производительность.
Часто используется компромисс, заключающийся в вычислении некоторых осветительных параметров на вершинах вместо фрагментов (пикселей). В некоторых случаях, особенно с простыми моделями или плотно расположенными вершинами, этого достаточно.
Обратная сторона этого заключается в том, что если у модели больше вершин, чем пикселей на выходном изображении. Тем не менее, решением этой проблемы обычно являются LOD-сетки, редко перемещая работу из шейдера вершин в шейдер фрагментов.
Компиляция шейдеров и связывание программ параллельно
Искушение состоит в том, чтобы компилировать шейдеры и связывать программы последовательно, но многие браузеры могут выполнять компиляцию и связывание параллельно на фоновых потоках.
Вместо этого:
function compileOnce(gl, shader) {
if (shader.compiled) return;
gl.compileShader(shader);
shader.compiled = true;
}
for (const [vs, fs, prog] of programs) {
compileOnce(gl, vs);
compileOnce(gl, fs);
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
console.error(`Link failed: ${gl.getProgramInfoLog(prog)}`);
console.error(`vs info-log: ${gl.getShaderInfoLog(vs)}`);
console.error(`fs info-log: ${gl.getShaderInfoLog(fs)}`);
}
}
Рассмотрите:
function compileOnce(gl, shader) {
if (shader.compiled) return;
gl.compileShader(shader);
shader.compiled = true;
}
for (const [vs, fs, prog] of programs) {
compileOnce(gl, vs);
compileOnce(gl, fs);
}
for (const [vs, fs, prog] of programs) {
gl.linkProgram(prog);
}
for (const [vs, fs, prog] of programs) {
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
console.error(`Link failed: ${gl.getProgramInfoLog(prog)}`);
console.error(`vs info-log: ${gl.getShaderInfoLog(vs)}`);
console.error(`fs info-log: ${gl.getShaderInfoLog(fs)}`);
}
}
Предпочитайте KHR_parallel_shader_compile
Хотя мы описали шаблон, позволяющий браузерам компилировать и связывать программы параллельно, обычно проверка COMPILE_STATUS или LINK_STATUS блокирует выполнение до завершения компиляции или связывания. В браузерах, где это доступно, расширение KHR_parallel_shader_compile предоставляет неблокирующую COMPLETION_STATUS проверку. Предпочтительно включить и использовать это расширение.
Пример использования:
ext = gl.getExtension("KHR_parallel_shader_compile");
gl.compileProgram(vs);
gl.compileProgram(fs);
gl.attachShader(prog, vs);
gl.attachShader(prog, fs);
gl.linkProgram(prog);
// Store program in your data structure.
// Later, for example the next frame:
if (ext) {
if (gl.getProgramParameter(prog, ext.COMPLETION_STATUS_KHR)) {
// Check program link status; if OK, use and draw with it.
}
} else {
// Program linking is synchronous.
// Check program link status; if OK, use and draw with it.
}
Этот метод может не работать во всех приложениях, например, в тех, которым необходимо, чтобы программы были немедленно доступны для рендеринга. Тем не менее, рассмотрите, как могут работать различные варианты.
Не проверяйте статус компиляции шейдера, если связывание не завершилось ошибкой
Существует очень мало ошибок, которые гарантированно приведут к ошибке компиляции шейдеров, но которые нельзя отложить до этапа связывания. Спецификация ESSL3 говорит об этом в разделе «Обработка ошибок»:
Реализация должна сообщать об ошибках как можно раньше, но в любом случае должна удовлетворять следующим требованиям:
- Все лексические, грамматические и семантические ошибки должны быть обнаружены после вызова glLinkProgram
- Ошибки из-за несоответствия между шейдером вершин и фрагментным шейдером (ошибки связывания) должны быть обнаружены после вызова glLinkProgram
- Ошибки из-за превышения лимитов ресурсов должны быть обнаружены после любого вызова отрисовки или вызова glValidateProgram
- Вызов glValidateProgram должен сообщать обо всех ошибках, связанных с объектом программы, учитывая текущее состояние OpenGL.
Распределение задач между компилятором и линковщиком зависит от реализации. Поэтому существует множество ошибок, которые могут быть обнаружены на этапе компиляции или связывания в зависимости от реализации.
Кроме того, запросить статус компиляции — это синхронный вызов, который нарушает конвейерную обработку.
Вместо этого:
gl.compileShader(vs);
if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) {
console.error(`vs compile failed: ${gl.getShaderInfoLog(vs)}`);
}
gl.compileShader(fs);
if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) {
console.error(`fs compile failed: ${gl.getShaderInfoLog(fs)}`);
}
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
console.error(`Link failed: ${gl.getProgramInfoLog(prog)}`);
}
Рассмотрите:
gl.compileShader(vs);
gl.compileShader(fs);
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
console.error(`Link failed: ${gl.getProgramInfoLog(prog)}`);
console.error(`vs info-log: ${gl.getShaderInfoLog(vs)}`);
console.error(`fs info-log: ${gl.getShaderInfoLog(fs)}`);
}
Будьте точны с аннотациями точности GLSL
Если вы ожидаете передать int essl300 между шейдерами и вам нужно, чтобы он имел 32 бита, вы обязательно должны использовать highp, иначе возникнут проблемы с портируемостью. (Работает на настольных ПК, не работает на Android)
Если у вас есть текстура float, iOS требует, чтобы вы использовали highp sampler2D foo;, в противном случае вы получите lowp образцы текстуры! (+/-2.0 максимум, вероятно, не достаточно для вас)
Неявные значения по умолчанию
Язык вершин имеет следующие предварительно объявленные глобальные операторы точности по умолчанию:
precision highp float; precision highp int; precision lowp sampler2D; precision lowp samplerCube;
Язык фрагментов имеет следующие предварительно объявленные глобальные операторы точности по умолчанию:
precision mediump int; precision lowp sampler2D; precision lowp samplerCube;
В WebGL 1 поддержка «highp float» необязательна в шейдерах фрагментов
Безусловное использование highp точности в шейдерах фрагментов помешает вашему контенту работать на некоторых старых мобильных устройствах.
Вы можете использовать mediump float вместо этого, но имейте в виду, что это часто приводит к искажению рендеринга из-за недостатка точности (особенно на мобильных устройствах), хотя искажение не будет видно на типичном настольном компьютере.
Если вам известны ваши требования к точности, getShaderPrecisionFormat() расскажет вам о том, что поддерживает система.
Если highp float доступен, GL_FRAGMENT_PRECISION_HIGH будет определен как 1.
Хороший шаблон для «всегда давать максимальную точность»:
#ifdef GL_FRAGMENT_PRECISION_HIGH precision highp float; #else precision mediump float; #endif
Минимальные требования ESSL100 (WebGL 1)
float | представление | диапазон | мин. выше нуля | точность |
|---|---|---|---|---|
highp | float24* | (-2^62, 2^62) | 2^-62 | относительная 2^-16 |
mediump | IEEE float16 | (-2^14, 2^14) | 2^-14 | относительная 2^-10 |
lowp | 10-битное знаковое фиксированное | (-2, 2) | 2^-8 | абсолютная 2^-8 |
int | представление | диапазон |
|---|---|---|
highp | int17 | (-2^16, 2^16) |
mediump | int11 | (-2^10, 2^10) |
lowp | int9 | (-2^8, 2^8) |
*float24: знаковый бит, 7-битный для показателя, 16-битный для мантиссы.
Минимальные требования ESSL300 (WebGL 2)
float | представление | диапазон | мин. выше нуля | точность |
|---|---|---|---|---|
highp | IEEE float32 | (-2^126, 2^127) | 2^-126 | относительная 2^-24 |
mediump | IEEE float16 | (-2^14, 2^14) | 2^-14 | относительная 2^-10 |
lowp | 10-битное знаковое фиксированное | (-2, 2) | 2^-8 | абсолютная 2^-8 |
(u)int | представление |
int диапазон |
unsigned int диапазон |
|---|---|---|---|
highp | (u)int32 | [-2^31, 2^31] | [0, 2^32] |
mediump | (u)int16 | [-2^15, 2^15] | [0, 2^16] |
lowp | (u)int9 | [-2^8, 2^8] | [0, 2^9] |
Предпочитайте встроенные функции вместо создания собственных
Предпочитайте встроенные функции, такие как dot, mix, и normalize. В лучшем случае, пользовательские реализации могут работать так же быстро, как и встроенные функции, которые они заменяют, но не ожидайте этого. Аппаратное обеспечение часто имеет гипер-оптимизированные или даже специализированные инструкции для встроенных функций, и компилятор не может надежно заменить ваши пользовательские замены встроенных функций на специальные пути кода встроенных функций.
Используйте mip-карты для всех текстур, которые вы будете видеть в 3D
В случае сомнений, вызывайте generateMipmaps() после загрузки текстуры. Mip-карты экономичны по памяти (только 30% накладных расходов), но часто обеспечивают значительные преимущества производительности, когда текстуры «уменьшаются» или уменьшаются на расстоянии в 3D или даже для кубовых карт!
Обработка образцов из меньших изображений текстур происходит быстрее из-за лучшей локальности кеша извлечения текстуры: масштабирование без mip-карт разрушает локальность кеша извлечения текстуры, поскольку соседние пиксели больше не берут образцы из соседних текстурных элементов!
Однако для 2D-ресурсов, которые никогда не «уменьшаются», не платите 30% накладных расходов на mip-карты:
const tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); // Defaults to NEAREST_MIPMAP_LINEAR, for mipmapping!
(В WebGL 2 вы должны просто использовать texStorage с levels=1)
Одно замечание: generateMipmaps работает только в том случае, если вы могли бы отрисовать в текстуру, если бы прикрепили ее к буферу фрейма. (В спецификации это называется «форматами, пригодными для рендеринга цвета») Например, если система поддерживает float-текстуры, но не рендеринг в float, generateMipmaps потерпит неудачу для float-форматов.
Не предполагайте, что вы можете отрисовать в float-текстуры
Существует множество систем, которые поддерживают текстуры RGBA32F, но если вы прикрепите одну к буферу фрейма, вы получите FRAMEBUFFER_INCOMPLETE_ATTACHMENT от checkFramebufferStatus(). Это может работать на вашей системе, но большинство мобильных систем не поддерживает это!
В WebGL 1 используйте расширения EXT_color_buffer_half_float и WEBGL_color_buffer_float для проверки поддержки рендеринга в float-текстуру для float16 и float32 соответственно.
В WebGL 2 EXT_color_buffer_float проверяет поддержку рендеринга в float-текстуру для float32 и float16. EXT_color_buffer_half_float присутствует на системах, которые поддерживают только рендеринг в float16 текстуры.
Рендеринг в float32 не подразумевает смешение float32!
Это может работать на вашей системе, но на многих других нет. Избегайте этого, если можете. Проверьте расширение EXT_float_blend для проверки поддержки.
Смешение float16 всегда поддерживается.
Некоторые форматы (например, RGB) могут быть эмулированы
Эмулируется ряд форматов (особенно трехканальные форматы). Например, RGB32F часто фактически является RGBA32F, а Luminance8 может фактически быть RGBA8. В частности, RGB8 часто оказывается удивительно медленным, так как маскирование альфа-канала и/или исправление функций смешивания имеют довольно высокую нагрузку. Для лучшей производительности предпочтительно использовать RGBA8 и игнорировать альфа-канал самостоятельно.
Избегайте alpha:false, что может быть дорогостоящим
Указание alpha:false во время создания контекста заставляет браузер компоновать холст WebGL, как будто он непрозрачен, игнорируя любые значения альфа, которые приложение записывает в своем фрагментном шейдере. К сожалению, на некоторых платформах эта возможность имеет значительную стоимость производительности. Буфер заднего плана RGB может потребоваться эмулировать поверх поверхности RGBA, и в API OpenGL доступно относительно мало техник для создания иллюзии для приложения, что у поверхности RGBA нет альфа-канала. Было установлено, что все эти техники имеют примерно одинаковый эффект на производительность на затронутых платформах.
Большинство приложений, даже те, которые требуют смешивания альфа-каналов, могут быть спроектированы таким образом, чтобы генерировать 1.0 для альфа-канала. Основное исключение — любое приложение, требующее альфа-канала назначения в функции смешивания. Если это возможно, рекомендуется сделать это вместо использования alpha:false.
Рассмотрите сжатые форматы текстур
Хотя JPG и PNG обычно меньше в сети, сжатые форматы текстур на GPU занимают меньше места в памяти GPU и быстрее подбираются. (Это уменьшает пропускную способность памяти текстур, которая важна на мобильных устройствах) Однако у сжатых форматов текстур хуже качество, чем у JPG, и они обычно приемлемы только для цветов (а не, например, для нормалей или координат).
К сожалению, нет одного универсального поддерживаемого формата. Однако каждая система имеет хотя бы один из следующих:
- WEBGL_compressed_texture_s3tc (десктоп)
- WEBGL_compressed_texture_etc1 (Android)
- WEBGL_compressed_texture_pvrtc (iOS)
WebGL 2 имеет универсальную поддержку, объединяя:
- WEBGL_compressed_texture_s3tc (десктоп)
- WEBGL_compressed_texture_etc (мобильные устройства)
WEBGL_compressed_texture_astc имеет как более высокое качество, так и/или более высокую степень сжатия, но поддерживается только на более новых устройствах.
Формат/библиотека сжатия текстур Basis Universal
Basis Universal решает несколько упомянутых выше проблем. Он предлагает способ поддержки всех распространенных сжатых форматов текстур с помощью одного сжатого файла текстур через JavaScript-библиотеку, которая эффективно конвертирует форматы во время загрузки. Он также добавляет дополнительное сжатие, которое делает сжатые файлы текстур Basis Universal значительно меньше, чем обычные сжатые текстуры в сети, более сопоставимые с JPEG.
https://github.com/BinomialLLC/basis_universal/blob/master/webgl/README.md
Использование памяти форматами глубины и трафарета
На многих устройствах глубинный и трафаретный дополнения и форматы фактически неразделимы. Вы можете запросить DEPTH_COMPONENT24 или STENCIL_INDEX8, но часто получаете форматы D24X8 и X24S8 32bpp за кулисами. Предполагайте, что использование памяти форматами глубины и трафарета округляется до ближайшего четного значения.
Загрузки texImage/texSubImage (особенно видео) могут вызывать сброс конвейера
Большинство загрузок текстур из элементов DOM вызовут обработочный этап, который временно переключится на программы GL внутри, вызвав сброс конвейера. (Конвейеры формализованы явно в Vulkan и др., но подразумеваются за кулисами в OpenGL и WebGL. Конвейеры в основном представляют собой кортеж программы шейдеров, состояния глубины/трафарета/многообразия/смешивания/растеризации)
В WebGL:
…
useProgram(prog1)
<pipeline flush>
bindFramebuffer(target)
drawArrays()
bindTexture(webgl_texture)
texImage2D(HTMLVideoElement)
drawArrays()
…
За кулисами в браузере:
…
useProgram(prog1)
<pipeline flush>
bindFramebuffer(target)
drawArrays()
bindTexture(webgl_texture)
-texImage2D(HTMLVideoElement):
+useProgram(_internal_tex_transform_prog)
<pipeline flush>
+bindFramebuffer(webgl_texture._internal_framebuffer)
+bindTexture(HTMLVideoElement._internal_video_tex)
+drawArrays() // y-flip/colorspace-transform/alpha-(un)premultiply
+bindTexture(webgl_texture)
+bindFramebuffer(target)
+useProgram(prog1)
<pipeline flush>
drawArrays()
…
Предпочтительнее выполнять загрузки перед запуском отрисовки или хотя бы между конвейерами:
В WebGL:
…
bindTexture(webgl_texture)
texImage2D(HTMLVideoElement)
useProgram(prog1)
<pipeline flush>
bindFramebuffer(target)
drawArrays()
bindTexture(webgl_texture)
drawArrays()
…
За кулисами в браузере:
…
bindTexture(webgl_texture)
-texImage2D(HTMLVideoElement):
+useProgram(_internal_tex_transform_prog)
<pipeline flush>
+bindFramebuffer(webgl_texture._internal_framebuffer)
+bindTexture(HTMLVideoElement._internal_video_tex)
+drawArrays() // y-flip/colorspace-transform/alpha-(un)premultiply
+bindTexture(webgl_texture)
+bindFramebuffer(target)
useProgram(prog1)
<pipeline flush>
bindFramebuffer(target)
drawArrays()
bindTexture(webgl_texture)
drawArrays()
…
Используйте texStorage для создания текстур
API WebGL 2.0 texImage* позволяет определять каждый уровень мип независимо и любого размера, даже несоответствующие размеры мипов не являются ошибкой до времени отрисовки, что означает, что драйвер не может подготовить текстуру в памяти GPU до первого использования текстуры.
Кроме того, некоторые драйверы могут безусловно выделять всю цепочку мип (+30% памяти!), даже если вам нужен только один уровень.
Поэтому для текстур в WebGL 2 предпочтительнее texStorage + texSubImage.
Используйте invalidateFramebuffer
Хранение данных, которые вы больше не будете использовать, может иметь высокую стоимость, особенно на GPU с разделением экрана, распространенных на мобильных устройствах. Когда вы закончили с содержимым дополнения фреймбуфера, используйте WebGL 2.0's invalidateFramebuffer для удаления данных, а не оставляйте драйверу тратить время на хранение данных на будущее. В частности, дополнения DEPTH/STENCIL и/или многообразные дополнения являются отличными кандидатами для invalidateFramebuffer.
Используйте асинхронное неблокирующее считывание данных
Операции, такие как readPixels и getBufferSubData обычно синхронные, но с использованием тех же API можно добиться неблокирующего асинхронного считывания данных. Подход в WebGL 2 аналогичен подходу в OpenGL: Асинхронные загрузки в блокирующих API
function clientWaitAsync(gl, sync, flags, interval_ms) {
return new Promise((resolve, reject) => {
function test() {
const res = gl.clientWaitSync(sync, flags, 0);
if (res === gl.WAIT_FAILED) {
reject();
return;
}
if (res === gl.TIMEOUT_EXPIRED) {
setTimeout(test, interval_ms);
return;
}
resolve();
}
test();
});
}
async function getBufferSubDataAsync(
gl,
target,
buffer,
srcByteOffset,
dstBuffer,
/* optional */ dstOffset,
/* optional */ length,
) {
const sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
gl.flush();
await clientWaitAsync(gl, sync, 0, 10);
gl.deleteSync(sync);
gl.bindBuffer(target, buffer);
gl.getBufferSubData(target, srcByteOffset, dstBuffer, dstOffset, length);
gl.bindBuffer(target, null);
return dstBuffer;
}
async function readPixelsAsync(gl, x, y, w, h, format, type, dest) {
const buf = gl.createBuffer();
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, buf);
gl.bufferData(gl.PIXEL_PACK_BUFFER, dest.byteLength, gl.STREAM_READ);
gl.readPixels(x, y, w, h, format, type, 0);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
await getBufferSubDataAsync(gl, gl.PIXEL_PACK_BUFFER, buf, 0, dest);
gl.deleteBuffer(buf);
return dest;
}
devicePixelRatio и рендеринг с высокой плотностью пикселей
Обработка devicePixelRatio !== 1.0 сложная. Хотя стандартный подход заключается в установке canvas.width = width * devicePixelRatio, это вызовет артефакты моайра с нецелочисленными значениями devicePixelRatio, как это часто бывает при масштабировании пользовательского интерфейса в Windows, а также при масштабировании на всех платформах.
Вместо этого мы можем использовать нецелочисленные значения для CSS top/bottom/left/right, чтобы достаточно надежно «предварительно обрезать» наш холст до целых координат устройства.
ResizeObserver и «device-pixel-content-box»
В поддерживающих браузерах (Chromium?) ResizeObserver можно использовать с 'device-pixel-content-box' для запроса обратного вызова, который включает истинные размеры пикселей устройства элемента. Это можно использовать для построения асинхронной, но точной функции:
window.getDevicePixelSize =
window.getDevicePixelSize ||
(async (elem) => {
await new Promise((fn_resolve) => {
const observer = new ResizeObserver((entries) => {
for (const cur of entries) {
const dev_size = cur.devicePixelContentBoxSize;
const ret = {
width: dev_size[0].inlineSize,
height: dev_size[0].blockSize,
};
fn_resolve(ret);
observer.disconnect();
return;
}
throw `device-pixel-content-box not observed for elem ${elem}`;
});
observer.observe(elem, { box: "device-pixel-content-box" });
});
});
Для получения более подробной информации обратитесь к спецификации.
Создание ImageBitmap
Использование словаря ImageBitmapOptions имеет важное значение для правильной подготовки текстур для загрузки в WebGL, но, к сожалению, нет очевидного способа узнать, какие члены словаря поддерживаются конкретным браузером.
В этом JSFiddle показано, как определить, какие члены словаря поддерживает конкретный браузер.
Используйте WEBGL_provoking_vertex при его наличии
При сборке вершин в примитивы, такие как треугольники и линии, в соглашении OpenGL последняя вершина примитива считается «вызывающей вершиной». Это имеет отношение при использовании интерполяции атрибутов вершин flat в ESSL300 (WebGL 2); значение атрибута из вершины вызова используется для всех вершин примитива.
В наши дни многие реализации WebGL браузеров размещены на основе различных графических API, отличных от OpenGL, и некоторые из этих API используют первую вершину в качестве вызывающей вершины для команд отрисовки. Эмуляция соглашения OpenGL о вызывающей вершине может быть вычислительно сложной на некоторых из этих API.
По этой причине был представлен расширение WEBGL_provoking_vertex. Если реализация WebGL раскрывает это расширение, это подсказка для приложения, что изменение соглашения на FIRST_VERTEX_CONVENTION_WEBGL улучшит производительность. Сильно рекомендуется, чтобы приложения, использующие плоское затенение, проверяли наличие этого расширения и использовали его, если оно доступно. Обратите внимание, что это может потребовать изменений в буферах вершин или шейдерах приложения.
© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices