Почему GL разделяет `gl_Position` на W для вас, а не позволяет вам сделать это самостоятельно?

Примечание: Я понимаю основную математику. Я понимаю, что типичная функция perspective в различных математических библиотеках создает матрицу, которая преобразует значения z из -zNear в -zFar обратно в -1 в +1, но только если результат делится на w

Конкретный вопрос заключается в том, что GPU делает это для вас, а не для того, чтобы сделать это самостоятельно?

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

attribute vec4 position;
uniform mat4 worldViewProjection;

void main() {
  gl_Position = worldViewProjection * position;

  // imaginary version of GL where we must divide by W ourselves
  gl_Position /= gl_Position.w;
}

Что ломается в этом воображаемом GL из-за этого? Будет ли это работать или есть что-то о передаче значения, прежде чем он будет разделен на w, который предоставляет дополнительную необходимую информацию для GPU?

Обратите внимание: если я на самом деле это сделаю, перспектива отображения текстуры ломается.

"use strict";
var m4 = twgl.m4;
var gl = twgl.getWebGLContext(document.getElementById("c"));
var programInfo = twgl.createProgramInfo(gl, ["vs", "fs"]);

var bufferInfo = twgl.primitives.createCubeBufferInfo(gl, 2);

var tex = twgl.createTexture(gl, {
  min: gl.NEAREST,
  mag: gl.NEAREST,
  src: [
    255, 255, 255, 255,
    192, 192, 192, 255,
    192, 192, 192, 255,
    255, 255, 255, 255,
  ],
});

var uniforms = {
  u_diffuse: tex,
};

function render(time) {
  time *= 0.001;
  twgl.resizeCanvasToDisplaySize(gl.canvas);
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

  gl.enable(gl.DEPTH_TEST);
  gl.enable(gl.CULL_FACE);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

  var projection = m4.perspective(
      30 * Math.PI / 180, 
      gl.canvas.clientWidth / gl.canvas.clientHeight, 
      0.5, 10);
  var eye = [1, 4, -6];
  var target = [0, 0, 0];
  var up = [0, 1, 0];

  var camera = m4.lookAt(eye, target, up);
  var view = m4.inverse(camera);
  var viewProjection = m4.multiply(projection, view);
  var world = m4.rotationY(time);

  uniforms.u_worldInverseTranspose = m4.transpose(m4.inverse(world));
  uniforms.u_worldViewProjection = m4.multiply(viewProjection, world);

  gl.useProgram(programInfo.program);
  twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
  twgl.setUniforms(programInfo, uniforms);
  gl.drawElements(gl.TRIANGLES, bufferInfo.numElements, gl.UNSIGNED_SHORT, 0);

  requestAnimationFrame(render);
}
requestAnimationFrame(render);
body {  margin: 0; }
canvas { display: block; width: 100vw; height: 100vh; }
  <script id="vs" type="notjs">
uniform mat4 u_worldViewProjection;
uniform mat4 u_worldInverseTranspose;

attribute vec4 position;
attribute vec3 normal;
attribute vec2 texcoord;

varying vec2 v_texcoord;
varying vec3 v_normal;

void main() {
  v_texcoord = texcoord;
  v_normal = (u_worldInverseTranspose * vec4(normal, 0)).xyz;
  gl_Position = u_worldViewProjection * position;
  gl_Position /= gl_Position.w;
}
  </script>
  <script id="fs" type="notjs">
precision mediump float;

varying vec2 v_texcoord;
varying vec3 v_normal;

uniform sampler2D u_diffuse;

void main() {
  vec4 diffuseColor = texture2D(u_diffuse, v_texcoord);
  vec3 a_normal = normalize(v_normal);
  float l = dot(a_normal, vec3(1, 0, 0));
  gl_FragColor.rgb = diffuseColor.rgb * (l * 0.5 + 0.5);
  gl_FragColor.a = diffuseColor.a;
}
  </script>
  <script src="https://twgljs.org/dist/2.x/twgl-full.min.js"></script>
  <canvas id="c"></canvas>

Ответ 1

Причина в том, что не только gl_Position делится на однородную координату, но также и на все другие интерполированные изменения. Это называется перспективной правильной интерполяцией, которая требует деления после интерполяции (и, следовательно, после растеризации). Таким образом, разделение в вершинном шейдере просто не работает. См. Также этот пост.

Ответ 2

Я хотел бы рассказать о BDL-ответе. Речь идет не только о перспективной интерполяции. Это также касается отсечения. Предполагается, что пространство, в котором должно быть предусмотрено значение gl_Position, называется пространством клипов, и это до деления на w.

Общий объем клипа OpenGL (по умолчанию) для выполнения

-1 <= x,y,z <= 1   (in NDC coordinates).

Однако, когда вы смотрите на это ограничение перед делением, вы получите

-w <= x,y,z <= w   (in clip space, with w varying per vertex)

однако, что только половина истины, так как и все точки пространства клипов, заполняющие это, полностью заполнят ограничение NDC после деления

 w <= x,y,z <= -w (in clip space)

Дело в том, что точки за камерой будут преобразованы где-то перед камерой, зеркально отразится (так как x/-1 совпадает с -x/1). Это также происходит с координатой z. Можно утверждать, что это не имеет значения, потому что любая точка за камерой проецируется сзади (в смысле более далекой, чем) дальняя, согласно конструкции типичной матрицы проекции, поэтому она будет лежать вне объема просмотра в любом случае.

Но если у вас есть примитив, где по крайней мере одна точка находится внутри объема представления, и по крайней мере одна точка находится за камерой, вы должны иметь примитив, который также пересекает ближнюю плоскость. Однако после деления на w он теперь пересечет плоскость far!. Таким образом, отсечение в пространстве NDC, после разделения, намного сложнее получить право. Я попытался представить это на этом рисунке:

сверху вниз взгляд на пространство глаз и NDC с обрезкой и без обрезки (рисунок является масштабным, диапазон глубины проецирования намного короче, чем обычно использовали бы люди, чтобы лучше проиллюстрировать проблему).

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

(Примечание: фактические графические процессоры могут вообще не использовать дополнительный этап отсечения, на самом деле они могут также использовать растеризатор без клипов, как это предполагается в Fabian Giesen blog статья там. Существуют некоторые алгоритмы, такие как Olano and Greer (1997). Однако это все работает, делая растрирование непосредственно в однородном координаты, поэтому нам еще нужно w...)

Ответ 3

Это еще проще; отсечение происходит после затенения вершин. Если вершинный шейдер был разрешен (или, более строго, назначен), чтобы сделать перспективное разделение, обрезание должно было бы произойти в однородных координатах, что было бы очень неудобно. Атрибуты вершин по-прежнему линейны по координатам клипа, что делает обрезку детского воспроизведения вместо того, чтобы закрепить в однородных координатах:

v '= 1.0f/(lerp (1.0/v0, 1.0/v1, t))

Посмотрите, как будет тяжело разделение? В координатах клипов это просто:

v '= lerp (v0, v1, t)

Это даже лучше: пределы отсечения в координатах клипа:

-w < x < ш

Это означает, что расстояния до плоскостей клипов (слева и справа) тривиальны для вычисления в координатах клипа:

x - w и w - x. Это гораздо проще и эффективнее для клипа в координатах клипов, что просто делает весь смысл в мире настоять на том, что вершинные шейдерные выходы находятся в координатах клипа. Затем позвольте аппаратным средствам отсекать и делить на w-координату, так как нет причин оставлять его пользователю. Это также проще, так как нам не нужен шейдер вершинного пост-клипа (который также включает отображение в окно просмотра, но это еще одна история). То, как они его разработали, на самом деле довольно приятно.:)