Как влияет геометрия на окончательный вывод текстуры в WebGL? - PullRequest
0 голосов
/ 14 сентября 2018

Обновлено с большим количеством объяснений моего замешательства

(Вот как разработчик не-графики представляет процесс рендеринга!)

Я указываю квадрат 2х2, который нужно нарисовать двумя треугольниками. Я больше не буду говорить о треугольнике. Квадрат намного лучше. Скажем, квадрат рисуется одним куском.

Я не указал ни одной единицы для своего рисунка. Единственные места в моем коде, где я делаю что-то подобное, это: размер холста (в моем случае - 1x1) и область просмотра (я всегда устанавливаю это в соответствии с размерами моей выходной текстуры).

Затем я вызываю draw ().

Что происходит так: независимо от размера моей текстуры (размером 1x1 или 10000x10000) все мои тексели заполняются данными (цветом), которые я вернул из своего фрагментирующего шейдера. Это работает каждый раз идеально.

Так что теперь я пытаюсь объяснить это себе:

  • Графический процессор занимается только окраской пикселей.
  • Пиксель - это самая маленькая единица, с которой работает графический процессор (цвета).
  • В зависимости от того, на сколько пикселей отображается мой квадрат 2х2, я должен столкнуться с одним из следующих 3 случаев:
    1. Количество пикселей (подлежащих окраске) ​​и яркость выходной текстуры совпадают один к одному: в этом идеальном случае для каждого пикселя для выходной текстуры будет назначено одно значение. Мне очень ясно.
    2. Количество пикселей меньше, чем у меня выходной цвет текстуры тускнеет. В этом случае я должен ожидать, что некоторые из выходных текселей будут иметь точно такое же значение (цвет пикселя, на который падает). Например, если графический процессор заканчивает рисовать 16x16 пикселей, а моя текстура - 64x64, тогда у меня будут блоки из 4 текселей, которые получат одинаковое значение. Я не наблюдал такого случая независимо от размера моей текстуры. Это означает, что никогда не бывает случая, когда у нас будет меньше пикселей (очень сложно представить - давайте продолжим)
    3. Количество пикселей в конечном итоге превышает количество текселей. В этом случае графический процессор должен решить, какое значение присвоить моему текселю. Будет ли это усреднить цвета пикселей? Если графический процессор раскрашивает 64x64 пикселя, а моя выходная текстура имеет размер 16x16, тогда я должен ожидать, что каждый тексель получит средний цвет 4x4 пикселей, которые он содержит. В любом случае, в этом случае моя текстура должна быть полностью заполнена значениями, которые я специально для них не предназначал (например, усредненными), однако это не так.

Я даже не говорил о том, сколько раз вызывали мой фрагментный шейдер, потому что это не имело значения. В любом случае результаты будут детерминированными.

Итак, учитывая, что я никогда не сталкивался со вторым и третьим случаями, когда значения в моих текселях не соответствуют ожиданиям, я ожидал их. Единственный вывод, который я могу сделать, состоит в том, что все предположение о том, что GPU пытается визуализировать пиксели, на самом деле неверно , Когда я назначаю ему выходную текстуру (которая должна постоянно растягиваться на моем квадрате 2x2), тогда графический процессор будет радовать вас, и для каждого текселя будет вызывать мой фрагментный шейдер. Где-то вдоль линии пиксели тоже окрашены.

Но приведенное выше сумасшедшее объяснение также не дает ответа, почему я не получаю значений в текселях или неверных значений, если я растягиваю свою геометрию до 1x1 или 4x4 вместо 2x2.

Надеюсь, сказанное выше фантастическое повествование о процессе окраски графического процессора дало вам подсказку, где я ошибаюсь.

Исходное сообщение:

Мы используем WebGL для общих вычислений. Таким образом, мы создаем прямоугольник и рисуем в нем 2 треугольника. В конечном итоге нам нужны данные внутри текстуры, сопоставленные с этой геометрией.

Чего я не понимаю, так это того, что если я изменю прямоугольник с (-1,-1):(1,1) на (-0.5,-0.5):(0.5,0.5), то внезапно данные будут сброшены из текстуры, связанной с кадровым буфером.

Буду признателен, если кто-нибудь заставит меня понять взаимосвязи. Единственные места, где реальные размеры выходной текстуры вступают в игру, это вызов viewPort() и readPixels().

Ниже приведены соответствующие фрагменты кода, чтобы вы могли увидеть, что я делаю:

  ... // canvas is created with size: 1x1
  ... // context attributes passed to canvas.getContext()
  contextAttributes = {
    alpha: false,
    depth: false,
    antialias: false,
    stencil: false,
    preserveDrawingBuffer: false,
    premultipliedAlpha: false,
    failIfMajorPerformanceCaveat: true
  };
  ... // default geometry
    // Sets of x,y,z (for rectangle) and s,t coordinates (for texture)
    return new Float32Array([
      -1.0, 1.0,  0.0, 0.0, 1.0,  // upper left
      -1.0, -1.0, 0.0, 0.0, 0.0,  // lower left
      1.0,  1.0,  0.0, 1.0, 1.0,  // upper right
      1.0,  -1.0, 0.0, 1.0, 0.0 // lower right
    ]);  
... 
    const geometry = this.createDefaultGeometry();
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, geometry, gl.STATIC_DRAW);
... // binding to the vertex shader attribs
    gl.vertexAttribPointer(positionHandle, 3, gl.FLOAT, false, 20, 0);
    gl.vertexAttribPointer(textureCoordHandle, 2, gl.FLOAT, false, 20, 12);
    gl.enableVertexAttribArray(positionHandle);
    gl.enableVertexAttribArray(textureCoordHandle);
... // setting up framebuffer; I set the viewport to output texture dimensions (I think this is absolutely needed but not sure)
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
    gl.framebufferTexture2D(
        gl.FRAMEBUFFER,        // The target is always a FRAMEBUFFER.
        gl.COLOR_ATTACHMENT0,  // We are providing the color buffer.
        gl.TEXTURE_2D,         // This is a 2D image texture.
        texture,               // The texture.
        0);                    // 0, we aren't using MIPMAPs
    gl.viewport(0, 0, width, height);
... // reading from output texture
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.framebufferTexture2D(
        gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture,
        0);
    gl.readPixels(0, 0, width, height, gl.FLOAT, gl.RED, buffer);

1 Ответ

0 голосов
/ 15 сентября 2018

новый ответ

Я просто повторяю то же самое снова (в третий раз?)

Скопировано снизу

WebGL основан на пункте назначения. Это означает, что он будет перебирать пиксели линии / точки / треугольника, которые он рисует, и для каждой точки вызывать фрагментный шейдер и спрашивать «какое значение я должен хранить здесь»?

Это пункт назначения . Каждый пиксель будет нарисован ровно один раз. Для этого пикселя будет задан вопрос «какого цвета мне сделать это»

цикл на основе пункта назначения

for (let i = start; i < end; ++i) {
  fragmentShaderFunction();  // must set gl_FragColor
  destinationTextureOrCanvas[i] = gl_FragColor;

Вы можете видеть в вышеприведенном цикле, что нет никакого случайного назначения. Там нет установки какой-либо части назначения дважды. Он просто будет работать с start до end и ровно один раз для каждого пикселя в пункте назначения между началом и концом спрашивать, какого цвета должен быть этот пиксель.

Как настроить начало и конец? Опять же, чтобы упростить задачу, давайте примем текстуру 200x1, чтобы мы могли игнорировать Y. Она работает следующим образом

 vertexShaderFunction(); // must set gl_Position
 const start = clipspaceToArrayspaceViaViewport(viewport, gl_Position.x);

 vertexShaderFunction(); // must set gl_Position
 const end = clipspaceToArrayspaceViaViewport(viewport, gl_Position.x);

 for (let i = start; i < end; ++i) {
   fragmentShaderFunction();  // must set gl_FragColor
   texture[i] = gl_FragColor;
 }

см. Ниже для clipspaceToArrayspaceViaViewport

Что такое viewport? viewport - это то, что вы устанавливаете, когда вызываете `gl.viewport (x, y, width, height)

Итак, установите gl_Position.x в -1 и +1, viewport.x в 0 и viewport.width = 200 (ширина текстуры), тогда start будет 0, end будет 200

установите gl_Position.x в .25 и .75, viewport.x в 0 и viewport.width = 200 (ширина текстуры). start будет 125, а end будет 175

Честно говоря, я чувствую, что этот ответ ведет вас по неверному пути. Это не так сложно. Вам не нужно понимать это, чтобы использовать WebGL IMO.

Простой ответ:

  1. Вы задаете для gl.viewport под прямоугольник, на который хотите повлиять в пункте назначения (холст или текстура это не имеет значения)

  2. Вы создаете вершинный шейдер, который каким-то образом устанавливает gl_Position для обрезки пространственных координат (они идут от -1 до +1) по текстуре

  3. Эти координаты пространства клипа преобразуются в пространство области просмотра. Это базовая математика для отображения одного диапазона в другой , но в основном это , не важно . Кажется интуитивно понятным, что -1 будет рисовать до viewport.x пикселя, а +1 - до viewport.x + viewport.width - 1 пикселя. Вот что означает «отображение из пространства клипа в настройках области просмотра».

Чаще всего в настройках области просмотра обычно (x = 0, y = 0, ширина = ширина целевой текстуры или холста, высота = высота целевой текстуры или холста)

Так что остается только то, что вы установили gl_Position. Эти значения находятся в пространстве клипа , как это объясняется в этой статье .

Вы можете сделать это просто, если хотите, преобразовав из пиксельного пространства в пространство клипа , как это объясняется в этой статье

 zeroToOne = someValueInPixels / destinationDimensions;
 zeroToTwo = zeroToOne * 2.0;
 clipspace = zeroToTwo - 1.0; 
 gl_Position = clipspace;

Если вы продолжите статьи, они также покажут добавление значения (перевод) и умножение на значение (масштаб)

Используя только эти 2 вещи и единицу площади (от 0 до 1), вы можете выбрать любой прямоугольник на экране. Вы хотите увеличить от 123 до 127. Это 5 единиц, поэтому масштаб = 5, перевод = 123. Затем примените приведенную выше математическую схему для преобразования пикселей в пространство клипов, и вы получите нужный прямоугольник.

Если продолжить эти статьи, вы в конечном итоге получите точку, в которой эта математика сделана с матрицами , но вы можете делать эту математику как хотите. Это все равно что спросить «как мне вычислить значение 3». Ну, 1 + 1 + 1, или 3 + 0, или 9/3, или 100 - 50 + 20 * 2/30, или (7 ^ 2 - 19) / 10, или ????

Я не могу сказать вам, как установить gl_Position. Я могу только сказать вам make up whatever math you want and set it to *clip space*, а затем привести пример преобразования пикселей в пространство клипа (см. Выше) в качестве одного из примеров возможной математики.

старый ответ

Я понимаю, что это может быть не ясно, я не знаю, как помочь.WebGL рисует линии, точки или треугольники в двухмерном массиве.Этот 2D-массив представляет собой холст, текстуру (как вложение кадрового буфера) или рендер-буфер (как вложение кадрового буфера).

Размер области определяется размером холста, текстуры, рендеринга буфера.

Вы пишете вершинный шейдер.Когда вы звоните gl.drawArrays(primitiveType, offset, count), вы говорите WebGL вызывать ваш вершинный шейдер count раз.Если предположить, что primitiveType равен gl.TRIANGLES, то для каждых 3 вершин, сгенерированных вашим вершинным шейдером, WebGL нарисует треугольник.Вы указываете этот треугольник, устанавливая gl_Position в пространство клипа .

Предполагая, что gl_Position.w равен 1, Пространство клипа изменяется от -1 до +1 в X и Y по целевому холсту / текстуре / визуализирующему буферу.(gl_Position.x и gl_Position.y делятся на gl_Position.w), что не очень важно для вашего случая.

Чтобы преобразовать обратно в фактически пиксели, ваши X и Y конвертируются на основе настроек gl.viewport,Давайте просто сделаем X

pixelX = ((clipspace.x / clipspace.w) * .5 + .5) * viewport.width + viewport.x

WebGL на основе пункта назначения.Это означает, что он будет перебирать пиксели линии / точки / треугольника, которые он рисует, и для каждой точки вызывать фрагментный шейдер и спрашивать «какое значение я должен хранить здесь»?

Давайте переведем это в JavaScript в 1D.Предположим, у вас есть 1D массив

const dst = new Array(100);

Давайте создадим функцию, которая принимает начало и конец и устанавливает значения в диапазоне

function setRange(dst, start, end, value) {
  for (let i = start; i < end; ++i) {
    dst[i] = value;
  }
}

. Вы можете заполнить весь массив из 100 элементов 123

const dst = new Array(100);
setRange(dst, 0, 99, 123);

Чтобы установить в последней половине массива значение 456

const dst = new Array(100);
setRange(dst, 50, 99, 456);

Давайте изменим это, чтобы использовать пространство клипа как координаты

function setClipspaceRange(dst, clipStart, clipEnd, value) {
  const start = clipspaceToArrayspace(dst, clipStart);
  const end = clipspaceToArrayspace(dst, clipEnd);
  for (let i = start; i < end; ++i) {
    dst[i] = value;
  }
}

function clipspaceToArrayspace(array, clipspaceValue) {
  // convert clipspace value (-1 to +1) to (0 to 1)
  const zeroToOne = clipspaceValue * .5 + .5;

  // convert zeroToOne value to array space
  return Math.floor(zeroToOne * array.length);
}

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

// fill entire array with 123
const dst = new Array(100);
setClipspaceRange(dst, -1, +1, 123);

Установите для последней половины массива значение 456

setClipspaceRange(dst, 0, +1, 456);

Теперь абстрагируйте еще раз.Вместо использования длины массива используйте настройку

// viewport looks like `{ x: number, width: number} `

function setClipspaceRangeViaViewport(dst, viewport, clipStart, clipEnd, value) {
  const start = clipspaceToArrayspaceViaViewport(viewport, clipStart);
  const end = clipspaceToArrayspaceViaViewport(viewport, clipEnd);
  for (let i = start; i < end; ++i) {
    dst[i] = value;
  }
}

function clipspaceToArrayspaceViaViewport(viewport, clipspaceValue) {
  // convert clipspace value (-1 to +1) to (0 to 1)
  const zeroToOne = clipspaceValue * .5 + .5;

  // convert zeroToOne value to array space
  return Math.floor(zeroToOne * viewport.width) + viewport.x;
}

Теперь, чтобы заполнить весь массив 123

const dst = new Array(100);
const viewport = { x: 0, width: 100; }
setClipspaceRangeViaViewport(dst, viewport, -1, 1, 123);

Установите для последней половины массива значение 456, теперь есть 2 способа.Первый способ такой же, как и предыдущий, с 0 до + 1

setClipspaceRangeViaViewport(dst, viewport, 0, 1, 456);

Вы также можете настроить область просмотра так, чтобы она начиналась на полпути через массив

const halfViewport = { x: 50, width: 50; }
setClipspaceRangeViaViewport(dst, halfViewport, -1, +1, 456);

Я не знаю, так ли этобыло полезно или нет.

Единственное, что нужно добавить - вместо value заменить это функцией, которая вызывается при каждой итерации для предоставления value

function setClipspaceRangeViaViewport(dst, viewport, clipStart, clipEnd, fragmentShaderFunction) {
  const start = clipspaceToArrayspaceViaViewport(viewport, clipStart);
  const end = clipspaceToArrayspaceViaViewport(viewport, clipEnd);
  for (let i = start; i < end; ++i) {
    dst[i] = fragmentShaderFunction();
  }
}

Обратите внимание, что этото же самое, что сказано в этой статье и несколько прояснено в этой статье .

...