为什么GL将'gl_Position`除以W而不是让你自己做?

时间:2016-12-11 10:18:08

标签: opengl opengl-es glsl

注意:我理解基本的数学。我理解各种数学库中的典型perspective函数会产生一个矩阵,它将-zNear到-zFar的z值转换回-1到+1,但前提是结果除以w

具体的问题是GPU为你做了什么,而不是你必须自己做?

换句话说,让我们说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>

但是,是因为GPU实际上需要z和w不同,还是仅仅是GPU设计,如果我们自己进行w划分,不同的设计可以获得所需的信息?

更新

在提出这个问题后,我最终写了this article that illustrates the perspective interpolation

3 个答案:

答案 0 :(得分:21)

我想了解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与{相同} {1}})。这也发生在-x/1坐标上。有人可能认为这是无关紧要的,因为根据典型投影矩阵的构造,相机背后的任何点都被投射到远处平面的背后(在更远的地方),因此它将位于观察体积之外在任何一种情况下。

但是如果你有一个图元,其中至少有一个点位于视图体内,并且至少有一个点位于相机后面,那么你应该有一个与近平面相交的图元。 然而,在z除法之后,它现在将与w平面相交!。因此,在划分之后,在NDC空间中进行削波很难做到正确。我试图在这张图中想象这个:

top-down view of eye space and NDC with and without clipping (图纸是按比例绘制的,投影的深度范围比任何人通常使用的都要短得多,以便更好地说明问题)。

裁剪是作为硬件中的固定功能阶段完成的,必须在分割之前完成,因此您应该提供正确的剪辑空间坐标来处理。

(注意:实际的GPU根本不会使用额外的剪辑阶段,它们实际上也可能使用无剪辑光栅化器,就像在Fabian Giesen's blog article there中推测的那样。有一些算法如Olano and Greer (1997)。但是,这一切都是通过直接在同质坐标中进行光栅化来实现的,所以我们仍然需要far ...)

答案 1 :(得分:16)

原因是,不仅gl_Position除以齐次坐标,还包括所有其他插值变化。这称为透视校正插值,要求除法在插值之后(因此在光栅化之后)。因此,在顶点着色器中进行除法根本不起作用。另见this post

答案 2 :(得分:2)

它更简单;剪辑发生在顶点着色之后。如果允许顶点着色器(或更强烈地,强制要求)进行透视分割,则裁剪必须在均匀坐标中发生,这将非常不方便。顶点属性在剪辑坐标中仍然是线性的,这使剪辑成为孩子的游戏,而不是必须在齐次坐标中剪辑:

V&#39; = 1.0f /(lerp(1.0 / v0,1.0 / v1,t))

看看分裂会怎么样?在剪辑坐标中,它只是:

V&#39; = lerp(v0,v1,t)

甚至比这更好:剪辑坐标中的剪裁限制为:

-w&lt; x&lt;瓦特

这意味着剪辑平面的距离(左和右)在剪辑坐标中计算是微不足道的:

x - w,和w - x。在剪辑坐标中进行剪辑非常简单有效,它让世界上的所有意义都坚持要求顶点着色器输出处于剪辑坐标中。然后让硬件进行剪切并用w坐标分割,因为没有理由让它留给用户了。它也更简单,因为我们不需要后剪辑顶点着色器(这也包括映射到视口但这是另一个故事)。他们设计它的方式实际上非常好。 :)