Эта статья о различных проблемах точности в WebGL2
lowp
, mediump
, highp
В первой статье на этом сайте мы создали вершинный шейдер и фрагментный шейдер. Когда мы создавали фрагментный шейдер, было упомянуто почти вскользь, что фрагментный шейдер не имеет точности по умолчанию, и поэтому нам нужно было установить одну, добавив строку
precision highp float;
Что это вообще было?
lowp
, mediump
и highp
- это настройки точности. Точность в данном случае
эффективно означает, сколько битов используется для хранения значения. Число в
JavaScript использует 64 бита. Большинство чисел в WebGL только 32 бита. Меньше битов =
быстрее, больше битов = более точно и/или больший диапазон.
Я не знаю, смогу ли я объяснить это хорошо. Вы можете поискать
double vs float
для других примеров проблем точности, но один способ объяснить это как разницу
между байтом и шортом или в JavaScript Uint8Array
против
Uint16Array
.
Uint8Array
- это массив беззнаковых 8-битных целых чисел. 8 бит могут содержать 28 значений от 0 до 255.Uint16Array
- это массив беззнаковых 16-битных целых чисел. 16 бит могут содержать 216 значений от 0 до 65535.Uint32Array
- это массив беззнаковых 32-битных целых чисел. 32 бита могут содержать 232 значений от 0 до 4294967295.lowp
, mediump
и highp
похожи.
lowp
- это как минимум 9-битное значение. Для значений с плавающей точкой они могут варьироваться
от: -2 до +2, для целых значений они похожи на Uint8Array
или Int8Array
mediump
- это как минимум 16-битное значение. Для значений с плавающей точкой они могут варьироваться
от: -214 до +214, для целых значений они похожи на
Uint16Array
или Int16Array
highp
- это как минимум 32-битное значение. Для значений с плавающей точкой они могут варьироваться
от: -262 до +262, для целых значений они похожи на
Uint32Array
или Int32Array
Важно отметить, что не каждое значение внутри диапазона может быть представлено.
Самый простой для понимания, вероятно, lowp
. Есть только 9 бит, и поэтому только
512 уникальных значений могут быть представлены. Выше говорится, что диапазон от -2 до +2, но
есть бесконечное количество значений между -2 и +2. Например, 1.9999999
и 1.999998 - это 2 значения между -2 и +2. С только 9 битами lowp
не может
представить эти 2 значения. Так, например, если вы хотите сделать какую-то математику с цветом и
вы использовали lowp
, вы можете увидеть некоторую полосатость. Не вдаваясь в то, какие
фактические значения могут быть представлены, мы знаем, что цвета идут от 0 до 1. Если lowp
идет от -2 до +2 и может представлять только 512 уникальных значений, то кажется вероятным,
что только 128 из этих значений помещаются между 0 и 1. Это также предполагает, что если у вас есть
значение, которое составляет 4/128, и я пытаюсь добавить к нему 1/512, ничего не произойдет,
потому что 1/512 не может быть представлено lowp
, поэтому это эффективно 0.
Мы могли бы просто использовать highp
везде и полностью игнорировать эту проблему,
но на устройствах, которые действительно используют 9 бит для lowp
и/или 16 бит для
mediump
, они обычно быстрее, чем highp
. Часто значительно быстрее.
К последнему пункту, в отличие от значений в Uint8Array
или Uint16Array
, значение lowp
или mediump
, или, если на то пошло, даже значение highp
, может использовать
более высокую точность (больше бит). Так, например, на настольном GPU, если вы поставите
mediump
в ваш шейдер, он все равно, скорее всего, будет использовать 32 бита внутренне. Это
имеет проблему, что трудно тестировать ваши шейдеры, если вы используете lowp
или
mediump
. Чтобы увидеть, действительно ли ваши шейдеры работают правильно с lowp
или
mediump
, вы должны тестировать на устройстве, которое действительно использует 8 бит для lowp
и
16 бит для highp
.
Если вы действительно хотите попытаться использовать mediump
для скорости, вот некоторые из проблем,
которые возникают.
Хороший пример, вероятно, пример точечных источников света,
в частности, вычисление зеркального блика, передает значения в мировом или видовом пространстве в фрагментный шейдер,
эти значения могут легко выйти за пределы диапазона для значения mediump
. Так что, может быть, на
устройстве mediump
вы могли бы просто убрать зеркальные блики. Например, вот
шейдер точечного света из статьи о точечных источниках света,
измененный для mediump
.
#version 300 es
-precision highp float;
+precision mediump float;
// Переданный и измененный из вершинного шейдера.
in vec3 v_normal;
in vec3 v_surfaceToLight;
in vec3 v_surfaceToView;
uniform vec4 u_color;
uniform float u_shininess;
// нам нужно объявить выход для фрагментного шейдера
out vec4 outColor;
void main() {
// потому что v_normal - это varying, он интерполируется
// поэтому он не будет единичным вектором. Нормализация его
// сделает его снова единичным вектором
vec3 normal = normalize(v_normal);
vec3 surfaceToLightDirection = normalize(v_surfaceToLight);
- vec3 surfaceToViewDirection = normalize(v_surfaceToView);
- vec3 halfVector = normalize(surfaceToLightDirection + surfaceToViewDirection);
// вычисляем свет, взяв скалярное произведение
// нормали к обратному направлению света
float light = dot(normal, surfaceToLightDirection);
- float specular = 0.0;
- if (light > 0.0) {
- specular = pow(dot(normal, halfVector), u_shininess);
- }
outColor = u_color;
// Давайте умножим только цветовую часть (не альфа)
// на свет
outColor.rgb *= light;
- // Просто добавляем блик
- outColor.rgb += specular;
}
Примечание: Даже этого на самом деле недостаточно. В вершинном шейдере у нас есть
// вычисляем вектор поверхности к свету
// и передаем его в фрагментный шейдер
v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition;
Так что скажем, свет находится на расстоянии 1000 единиц от поверхности. Затем мы попадаем в фрагментный шейдер, и эта строка
vec3 surfaceToLightDirection = normalize(v_surfaceToLight);
выглядит достаточно невинно. За исключением того, что нормальный способ нормализовать вектор
float length = sqrt(v.x * v.x + v.y * v.y * v.z * v.z);
Если один из этих x, y или z равен 1000, то 1000*1000 = 1000000. 1000000
выходит за пределы диапазона для mediump
.
Одно решение здесь - нормализовать в вершинном шейдере.
// вычисляем вектор поверхности к свету
// и передаем его в фрагментный шейдер
- v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition;
+ v_surfaceToLight = normalize(u_lightWorldPosition - surfaceWorldPosition);
Теперь значения, присвоенные v_surfaceToLight
, находятся между -1 и +1, что
в пределах диапазона для mediump
.
Обратите внимание, что нормализация в вершинном шейдере на самом деле не даст те же результаты, но они могут быть достаточно близкими, что никто не заметит, если не сравнивать бок о бок.
Функции, такие как normalize
, length
, distance
, dot
, все имеют эту
проблему, что если значения слишком большие, они выйдут за пределы диапазона
для mediump
.
Но вы действительно должны тестировать на устройстве, для которого mediump
составляет 16 бит.
На настольном компьютере mediump
составляет 32 бита, то же самое, что и highp
, и поэтому любые проблемы
не будут видны.
mediump
Вы вызываете gl.getShaderPrecisionFormat
,
вы передаете тип шейдера, VERTEX_SHADER
или FRAGMENT_SHADER
, и вы
передаете один из LOW_FLOAT
, MEDIUM_FLOAT
, HIGH_FLOAT
,
LOW_INT
, MEDIUM_INT
, HIGH_INT
, и он
[возвращает информацию о точности].
gl.getShaderPrecisionFormat
возвращает объект с тремя значениями: precision
, rangeMin
и rangeMax
.
Для LOW_FLOAT
и MEDIUM_FLOAT
precision
будет 23, если они действительно
просто highp
. Иначе они, вероятно, будут 8 и 15 соответственно, или
по крайней мере они будут меньше 23. Для LOW_INT
и MEDIUM_INT
если они такие же, как highp
, то rangeMin
будет 31. Если они
меньше 31, то mediump int
на самом деле более эффективен, чем
highp int
, например.
Мой Pixel 2 XL использует 16 бит для mediump
, он также использует 16 бит для lowp
. Я не уверен, что когда-либо использовал устройство, которое использует 9 бит для lowp
, поэтому я не уверен, какие проблемы обычно возникают, если они есть.
На протяжении этих статей мы указывали точность по умолчанию в фрагментном шейдере. Мы также можем указать точность любой отдельной переменной. Например
uniform mediump vec4 color; // uniform
in lowp vec4 normal; // атрибут или varying вход
out lowp vec4 texcoord; // выход фрагментного шейдера или varying выход
lowp float foo; // переменная
Текстуры - это еще одно место, где спецификация говорит, что фактическая точность, используемая, может быть больше, чем запрошенная точность.
В качестве примера вы можете запросить 16-битную, 4-битную на канал текстуру, как это
gl.texImage2D(
gl.TEXTURE_2D, // target
0, // mip level
gl.RGBA4, // internal format
width, // width
height, // height
0, // border
gl.RGBA, // format
gl.UNSIGNED_SHORT_4_4_4_4, // type
null,
);
Но реализация может на самом деле использовать формат более высокого разрешения внутренне. Я считаю, что большинство настольных компьютеров делают это, а большинство мобильных GPU - нет.
Мы можем протестировать. Сначала мы запросим 4-битную на канал текстуру, как выше. Затем мы отрендерим в нее, рендеря некоторый градиент от 0 до 1.
Затем мы отрендерим эту текстуру на холст. Если текстура действительно 4 бита на канал внутренне, будет только 16 уровней цвета из градиента, который мы нарисовали. Если текстура действительно 8 бит на канал, мы увидим 256 уровней цветов.
Запуская это на моем смартфоне, я вижу, что текстура использует 4 бита на канал (или по крайней мере 4 бита в красном, поскольку я не тестировал другие каналы).
Тогда как на моем настольном компьютере я могу видеть, что текстура на самом деле использует 8 бит на канал, даже though я только запросил 4.
Одна вещь, которую стоит отметить, это то, что по умолчанию WebGL может дизерить свои результаты, чтобы сделать такие градации более гладкими. Вы можете отключить дизеринг с помощью
gl.disable(gl.DITHER);
Если я не отключаю дизеринг, то мой смартфон производит это.
Сходу единственное место, где это действительно возникло бы, это если бы вы использовали какую-то текстуру с более низким битовым разрешением как цель рендеринга и не тестировали на устройстве, где эта текстура действительно имеет это более низкое разрешение. Если вы тестировали только на настольном компьютере, любые проблемы, которые это вызывает, могут быть не очевидны.