Неточность координат текстуры

Я получаю то, что выглядит как проблема с точностью в 16 бит для текстуры uv. Это может быть общеизвестно, и texture приведение к нормализованному целому числу, но я не смог найти много¹ об этом, и это не имело бы для меня особого смысла (зачем ухудшать координаты на последнем шаге?).

В реальном коде (который слишком длинный, чтобы его показывать) это приводит к растеканию текстуры при слишком большом увеличении, задолго до ожидаемых проблем для float32. Следующая демонстрация содержит много шаблонов, поэтому вот части, которые я считаю важными:

  • Тестовая текстура представляет собой шахматную доску размером 512x512 из плиток 8x8, зеленую и синюю.
  • Визуализируется квадрат с uv, установленным на синюю плитку 8x8 над серединой, расположенную так, что она покрывает половину холста.
  • const offset = -1 / (1 << bitsRequired); немного смещает эти UV по оси Y.
  • const zoom = 2 ** (bitsRequired - 14); увеличивает край.

По конструкции смещение uv затем устанавливается на шаг, который требует более 16 бит. Масштаб выбирается таким образом, чтобы отображался ровно один зеленый пиксель без проблем с точностью. Однако при требуемой точности ровно 17 бит пиксель исчезает (по крайней мере, для меня, но это может зависеть от оборудования/драйвера/чего-то еще - если кто-то не может воспроизвести и об этом еще не упоминалось, пожалуйста, сделайте).

Сначала я подумал, что совершил ошибку. Однако добавление округления вручную перед вызовом texture и раскомментирование следующего приводит к тому, что зеленая линия пикселей снова появляется:

vec2 texCoordRounded = vec2(
  (floor(vTextureCoord.x * 512.) + 0.5) / 512.,
  (floor(vTextureCoord.y * 512.) + 0.5) / 512.
);

Теперь я в замешательстве. Я что-то пропустил или ошибся? Приводит ли texture к некоторому нормализованному целому числу? Почему это выглядит так, как будто моя точность иссякает на 16 бит?

Ниже приведен пример копирования и вставки того же кода с измененными параметрами (слишком громоздко для параметризации этого демонстрационного кода):

При смещении, требующем менее 16 бит, появляется зеленая линия пикселей:

const assert = (condition, message) => {
  if (!condition) throw new Error(message);
};

const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl2');
assert(gl !== null, `WebGL2 was unexpectedly not supported.`);

const testImage = new Uint8Array(Array.from(
  { length: 512 * 512 },
  (_, i) => (i % 16 > 7) !== (Math.floor(i / 512) % 16 > 7)
    ? [0, 0xff, 0, 0xff]
    : [0, 0, 0xff, 0xff],
).flat());
const bitsRequired = 16;
const offset = -1 / (1 << bitsRequired);
const vData = new Float32Array([
  -1, 0, 0, 0.5 + offset, 1, 0, 0.015625, 0.5 + offset,
  1, 2, 0.015625, 0.515625 + offset, -1, 2, 0, 0.515625 + offset,
]);
const zoom = 2 ** (bitsRequired - 14);
const projection = new Float32Array([
  zoom, 0,    0,     0,
  0,    zoom, 0,     0,
  0,    0,    -zoom, 0,
  0,    0,    0,     1,
]);

const indices = new Uint16Array([0, 1, 2, 0, 2, 3]);
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 512, 512, 0, gl.RGBA, gl.UNSIGNED_BYTE, testImage);

const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
gl.enableVertexAttribArray(0);
gl.enableVertexAttribArray(1);

const vBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vBuffer);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 16, 0);
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 16, 8);
gl.bufferData(gl.ARRAY_BUFFER, vData, gl.STATIC_DRAW);

const iBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, iBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

const vertexShaderSrc = `#version 300 es
precision highp float;
uniform mat4 projection;
layout(location = 0) in vec2 aPosition;
layout(location = 1) in vec2 aTextureCoord;
out vec2 vTextureCoord;
void main(void) {
  gl_Position = projection * vec4(aPosition, 0.0, 1.0);
  vTextureCoord = aTextureCoord;
}`;

const fragmentShaderSrc = `#version 300 es
precision highp float;
uniform sampler2D sampler;
in vec2 vTextureCoord;
out vec4 fColor;

void main(void){
  // vec2 texCoordRounded = vec2(
    //   (floor(vTextureCoord.x * 512.) + 0.5) / 512.,
    //   (floor(vTextureCoord.y * 512.) + 0.5) / 512.
    // );
  // vec4 color = texture(sampler, texCoordRounded);
  vec4 color = texture(sampler, vTextureCoord);
  fColor = color;
}`;

const program = gl.createProgram();
assert(program !== null, `Program was unexpectedly \`null\`.`);

const vertexShader = gl.createShader(gl.VERTEX_SHADER);
assert(vertexShader !== null, `Vertex-shader was unexpectedly \`null\`.`);
gl.shaderSource(vertexShader, vertexShaderSrc);
gl.compileShader(vertexShader);
assert(gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS), `Vertex-shader failed to compile:\n${gl.getShaderInfoLog(vertexShader)}`);

const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
assert(fragmentShader !== null, `Vertex-shader was unexpectedly \`null\`.`);
gl.shaderSource(fragmentShader, fragmentShaderSrc);
gl.compileShader(fragmentShader);
assert(gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS), `Fragment-shader failed to compile:\n${gl.getShaderInfoLog(fragmentShader)}`);

gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
assert(gl.getProgramParameter(program, gl.LINK_STATUS), `Program linking failed:\n${gl.getProgramInfoLog(program)}`);

gl.useProgram(program);

const uniformLocationSampler = gl.getUniformLocation(program, 'sampler');
gl.uniform1i(uniformLocationSampler, 0);
const uniformLocationProjection = gl.getUniformLocation(program, 'projection');
gl.uniformMatrix4fv(uniformLocationProjection, false, projection);

gl.clearColor(0, 0, 0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);

gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
<canvas id='canvas' width='256' height='256'></canvas>

Изменение bitsRequired на 17 (только изменение) вызывает проблему, исчезает зеленая линия пикселей:

const assert = (condition, message) => {
  if (!condition) throw new Error(message);
};

const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl2');
assert(gl !== null, `WebGL2 was unexpectedly not supported.`);

const testImage = new Uint8Array(Array.from(
  { length: 512 * 512 },
  (_, i) => (i % 16 > 7) !== (Math.floor(i / 512) % 16 > 7)
    ? [0, 0xff, 0, 0xff]
    : [0, 0, 0xff, 0xff],
).flat());
const bitsRequired = 17;
const offset = -1 / (1 << bitsRequired);
const vData = new Float32Array([
  -1, 0, 0, 0.5 + offset, 1, 0, 0.015625, 0.5 + offset,
  1, 2, 0.015625, 0.515625 + offset, -1, 2, 0, 0.515625 + offset,
]);
const zoom = 2 ** (bitsRequired - 14);
const projection = new Float32Array([
  zoom, 0,    0,     0,
  0,    zoom, 0,     0,
  0,    0,    -zoom, 0,
  0,    0,    0,     1,
]);

const indices = new Uint16Array([0, 1, 2, 0, 2, 3]);
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 512, 512, 0, gl.RGBA, gl.UNSIGNED_BYTE, testImage);

const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
gl.enableVertexAttribArray(0);
gl.enableVertexAttribArray(1);

const vBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vBuffer);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 16, 0);
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 16, 8);
gl.bufferData(gl.ARRAY_BUFFER, vData, gl.STATIC_DRAW);

const iBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, iBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

const vertexShaderSrc = `#version 300 es
precision highp float;
uniform mat4 projection;
layout(location = 0) in vec2 aPosition;
layout(location = 1) in vec2 aTextureCoord;
out vec2 vTextureCoord;
void main(void) {
  gl_Position = projection * vec4(aPosition, 0.0, 1.0);
  vTextureCoord = aTextureCoord;
}`;

const fragmentShaderSrc = `#version 300 es
precision highp float;
uniform sampler2D sampler;
in vec2 vTextureCoord;
out vec4 fColor;

void main(void){
  // vec2 texCoordRounded = vec2(
    //   (floor(vTextureCoord.x * 512.) + 0.5) / 512.,
    //   (floor(vTextureCoord.y * 512.) + 0.5) / 512.
    // );
  // vec4 color = texture(sampler, texCoordRounded);
  vec4 color = texture(sampler, vTextureCoord);
  fColor = color;
}`;

const program = gl.createProgram();
assert(program !== null, `Program was unexpectedly \`null\`.`);

const vertexShader = gl.createShader(gl.VERTEX_SHADER);
assert(vertexShader !== null, `Vertex-shader was unexpectedly \`null\`.`);
gl.shaderSource(vertexShader, vertexShaderSrc);
gl.compileShader(vertexShader);
assert(gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS), `Vertex-shader failed to compile:\n${gl.getShaderInfoLog(vertexShader)}`);

const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
assert(fragmentShader !== null, `Vertex-shader was unexpectedly \`null\`.`);
gl.shaderSource(fragmentShader, fragmentShaderSrc);
gl.compileShader(fragmentShader);
assert(gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS), `Fragment-shader failed to compile:\n${gl.getShaderInfoLog(fragmentShader)}`);

gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
assert(gl.getProgramParameter(program, gl.LINK_STATUS), `Program linking failed:\n${gl.getProgramInfoLog(program)}`);

gl.useProgram(program);

const uniformLocationSampler = gl.getUniformLocation(program, 'sampler');
gl.uniform1i(uniformLocationSampler, 0);
const uniformLocationProjection = gl.getUniformLocation(program, 'projection');
gl.uniformMatrix4fv(uniformLocationProjection, false, projection);

gl.clearColor(0, 0, 0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);

gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
<canvas id='canvas' width='256' height='256'></canvas>

Активация ранее прокомментированного ручного округления перед вызовом texture (только изменение) для того, что все еще является float32, вызывает повторное появление зеленой линии пикселей, устраняя проблему, но почему?

const assert = (condition, message) => {
  if (!condition) throw new Error(message);
};

const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl2');
assert(gl !== null, `WebGL2 was unexpectedly not supported.`);

const testImage = new Uint8Array(Array.from(
  { length: 512 * 512 },
  (_, i) => (i % 16 > 7) !== (Math.floor(i / 512) % 16 > 7)
    ? [0, 0xff, 0, 0xff]
    : [0, 0, 0xff, 0xff],
).flat());
const bitsRequired = 17;
const offset = -1 / (1 << bitsRequired);
const vData = new Float32Array([
  -1, 0, 0, 0.5 + offset, 1, 0, 0.015625, 0.5 + offset,
  1, 2, 0.015625, 0.515625 + offset, -1, 2, 0, 0.515625 + offset,
]);
const zoom = 2 ** (bitsRequired - 14);
const projection = new Float32Array([
  zoom, 0,    0,     0,
  0,    zoom, 0,     0,
  0,    0,    -zoom, 0,
  0,    0,    0,     1,
]);

const indices = new Uint16Array([0, 1, 2, 0, 2, 3]);
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 512, 512, 0, gl.RGBA, gl.UNSIGNED_BYTE, testImage);

const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
gl.enableVertexAttribArray(0);
gl.enableVertexAttribArray(1);

const vBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vBuffer);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 16, 0);
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 16, 8);
gl.bufferData(gl.ARRAY_BUFFER, vData, gl.STATIC_DRAW);

const iBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, iBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

const vertexShaderSrc = `#version 300 es
precision highp float;
uniform mat4 projection;
layout(location = 0) in vec2 aPosition;
layout(location = 1) in vec2 aTextureCoord;
out vec2 vTextureCoord;
void main(void) {
  gl_Position = projection * vec4(aPosition, 0.0, 1.0);
  vTextureCoord = aTextureCoord;
}`;

const fragmentShaderSrc = `#version 300 es
precision highp float;
uniform sampler2D sampler;
in vec2 vTextureCoord;
out vec4 fColor;

void main(void){
  vec2 texCoordRounded = vec2(
      (floor(vTextureCoord.x * 512.) + 0.5) / 512.,
      (floor(vTextureCoord.y * 512.) + 0.5) / 512.
    );
  vec4 color = texture(sampler, texCoordRounded);
  // vec4 color = texture(sampler, vTextureCoord);
  fColor = color;
}`;

const program = gl.createProgram();
assert(program !== null, `Program was unexpectedly \`null\`.`);

const vertexShader = gl.createShader(gl.VERTEX_SHADER);
assert(vertexShader !== null, `Vertex-shader was unexpectedly \`null\`.`);
gl.shaderSource(vertexShader, vertexShaderSrc);
gl.compileShader(vertexShader);
assert(gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS), `Vertex-shader failed to compile:\n${gl.getShaderInfoLog(vertexShader)}`);

const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
assert(fragmentShader !== null, `Vertex-shader was unexpectedly \`null\`.`);
gl.shaderSource(fragmentShader, fragmentShaderSrc);
gl.compileShader(fragmentShader);
assert(gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS), `Fragment-shader failed to compile:\n${gl.getShaderInfoLog(fragmentShader)}`);

gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
assert(gl.getProgramParameter(program, gl.LINK_STATUS), `Program linking failed:\n${gl.getProgramInfoLog(program)}`);

gl.useProgram(program);

const uniformLocationSampler = gl.getUniformLocation(program, 'sampler');
gl.uniform1i(uniformLocationSampler, 0);
const uniformLocationProjection = gl.getUniformLocation(program, 'projection');
gl.uniformMatrix4fv(uniformLocationProjection, false, projection);

gl.clearColor(0, 0, 0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);

gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
<canvas id='canvas' width='256' height='256'></canvas>

[1]: Обновлено: я нашел комментарий в другом несвязанном вопросе, который поддерживает мое предположение о выборке с использованием нормализованных целых чисел, но до сих пор нет официальной документации:

Многие современные графические процессоры используют некоторое представление с фиксированной точкой для texcoords, поэтому существует только ограниченный набор позиций, которые могут быть сэмплированы между двумя текселями (обычно 256).

Обратите внимание, что комментарий относится к 2014 году (9 лет), и я предполагаю, что по умолчанию используется 16-битное нормализованное целое число вместо 8-битного.

Edit2: теперь я также нашел это в спецификации directx d3d (спасибо сообщению в блоге)

Координаты текстуры для операций выборки привязываются к фиксированной точке (после масштабирования по размеру текстуры), чтобы равномерно распределить точность в пространстве текстуры при выборе местоположения/веса касания фильтра. Значения веса преобразуются обратно в числа с плавающей запятой перед выполнением фактической арифметической фильтрации.

Я до сих пор не могу найти авторитетную документацию для opengl/webgl. Становится все яснее, что происходящее — это именно мое предположение, но где документация, и достаточно ли «равномерного распределения» для того, чтобы отсечь 8-битную точность?

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
1
0
115
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

зачем ухудшать координаты на последнем шаге?

... потому что графические процессоры могут выполнять несколько миллиардов операций фильтрации текстур в секунду, и вы действительно не хотите тратить энергию и площадь кремния на вычисления с точностью fp32, если во всех практических случаях использования требуется 8-битная фиксированная точка.

Обратите внимание, что это 8-битная точность субтекселей (т. е. степень детализации для фильтрации GL_LINEAR между двумя соседними текселями). Выбор текселей выполняется с любой необходимой точностью (большинство современных графических процессоров могут однозначно адресовать 16-килобайтные текстуры с 8-битной точностью субтекселей).

Спасибо! Я до сих пор не совсем понимаю «почему» (нужно также конвертировать из числа с плавающей запятой один раз на эти миллиарды операций), но вижу, что вполне вероятно, что есть веские причины, которых на данный момент достаточно. Чтобы решить насущную проблему, является ли какая-либо операция (например, округление) во фрагментном шейдере нормальной при увеличении до 256x или более (где видно смещение 1/512-го текселя в пространстве текстуры) или есть лучший трюк?

Doofus 25.01.2023 18:55

Зачем -> Посмотрите, сколько математики вам нужно сделать с образцами компонентов для трилинейного фильтра =)

solidpixel 25.01.2023 19:53

В вопросе есть короткая демонстрация. Насколько я понял, это проблема округления до ближайшего четного при достаточном увеличении (например, один тексель составляет 256 пикселей на экране). Интерполированный uv фрагментов с плавающей запятой по-прежнему сможет приспособиться к этому, но 16,8 с фиксированной точкой не будет. Он будет округлен до границы текселя, и тогда одна из двух сторон будет неправильной. Возможно, такое масштабирование недостаточно распространено, чтобы было решение по умолчанию.

Doofus 25.01.2023 19:53

Да, извините - пропустил код шейдера в исходном посте.

solidpixel 25.01.2023 19:54

Я случайно связал полную запись в блоге во время поиска части спецификации openGL, но только сейчас прочитал ее (после того, как понял, что это не специфично для openGL). Также я вижу смысл с трилинейной фильтрацией — я играю в пиксели, с GL_NEAREST, но есть смысл, что железо не будет внезапно переключаться только на один случай.

Doofus 25.01.2023 19:56

Да, как только вы получите 256-кратный зум, вы приблизитесь к пределу того, что может дать точность субтекселей. Как упоминалось в вашем другом посте, вы действительно находитесь в сфере поведения, определяемого реализацией - даже плавание графического процессора в шейдерах не является строгим IEEE, поэтому реализации могут свободно выбирать, например. режимы округления, переассоциировать некоторые виды операций и т.д.

solidpixel 25.01.2023 19:56

Угадайте какое-то округление/фиксацию/что угодно в фрагментном шейдере, до середины текселя, прежде чем передавать данные для выборки. Я могу перейти к precision highp float, и в этом случае у меня есть гарантированный float32+ (webgl2) и пока нет проблем с точностью при 256-кратном увеличении. Что хорошо в пиксельных играх, так это то, что GPU вряд ли когда-либо является пределом производительности, но мне нужно избегать размытия текстур :)

Doofus 25.01.2023 20:06

Другие вопросы по теме