web-dev-qa-db-ja.com

なぜGL `gl_Position`を自分でやらせるのではなくWで割るのですか?

注:私は基本的な数学を理解しています。さまざまな数学ライブラリの典型的なperspective関数は、結果がwで除算された場合にのみ、z値を-zNearから-zFarに変換して-1から+1に戻す行列を生成することを理解しています。

具体的な質問は、GPUが自分でこれを行うのではなく、これを行うことで何が得られるかということです。

言い換えると、GPUがgl_Positiongl_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が原因で何が壊れますか?それは機能しますか?それとも、GPUに追加の必要な情報を提供するwで除算される前に値を渡すことについて何かありますか? ?

実際に実行すると、テクスチャマッピングのパースペクティブが壊れることに注意してください。

"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を自分で分割した場合、異なる設計が必要な情報を導き出すことができるのでしょうか。

更新:

この質問をした後、私は パースペクティブ補間を説明するこの記事 を書くことになりました。

21
gman

その理由は、gl_Positionが同次座標で除算されるだけでなく、他のすべての補間された変数でも除算されるためです。これはパースペクティブ補正補間と呼ばれ、分割は補間後(したがってラスタライズ後)である必要があります。したがって、頂点シェーダーで分割を行うと、単純に機能しません。 この投稿 も参照してください。

17
BDL

BDLの答えを拡張したいと思います。遠近法の補間だけではありません。 クリッピングについても説明します。値gl_Positionが提供されることになっているスペースは、クリップスペースと呼ばれ、これは before 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座標にも起こります。典型的な投影マトリックスの構築に従って、カメラの後ろのポイントは遠い平面の後ろに(より遠いという意味で)投影されるため、これは無関係であると主張する人もいるかもしれません。したがって、それは表示ボリュームの外側にあります。どちらの場合にも。

ただし、少なくとも1つのポイントがビューボリュームの内側にあり、少なくとも1つのポイントがカメラの後ろにあるプリミティブがある場合は、ニアプレーンとも交差するプリミティブが必要です。 ただし、wで除算すると、far平面と交差します!。したがって、除算後のNDCスペースでのクリッピングは、正しく行うのがはるかに困難です。私はこの図面でこれを視覚化しようとしました:

top-down view of eye space and NDC with and without clipping (図面は実物大であり、投影の深度範囲は、問題をよりよく説明するために、誰もが通常使用するよりもはるかに短いです)。

クリッピングはハードウェアの固定機能ステージとして実行され、分割の前に実行する必要があるため、作業する正しいクリップスペース座標を指定する必要があります。

(注:実際のGPUは、追加のクリッピングステージをまったく使用しない場合があります。実際には、 FabianGiesenのブログ記事 で推測されているように、クリップレスラスタライザーも使用する場合があります。 Olano and Greer(1997) のようないくつかのアルゴリズムです。ただし、これはすべて同次座標で直接ラスタライズを行うことで機能するため、wが必要です。 ...)

22
derhass

それはさらに簡単です。クリッピングは、頂点シェーディングの後に発生します。頂点シェーダーが遠近法の分割を行うことを許可されている(またはより強力に義務付けられている)場合、クリッピングは同次座標で行われる必要があり、非常に不便です。頂点属性はクリップ座標で線形であるため、同次座標でクリップする代わりに、子供の遊びをクリップできます。

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

それがどれほど分割が重いか見てみましょう。クリップ座標では、それは単純です:

v '= lerp(v0、v1、t)

それよりもさらに優れています。クリップ座標のクリッピング制限は次のとおりです。

-w <x <w

これは、クリップ平面(左と右)までの距離がクリップ座標で計算するのが簡単であることを意味します。

x-w、およびw-x。クリップ座標でクリップするのは非常に簡単で効率的であるため、頂点シェーダーの出力がクリップ座標であると主張することは世界中で理にかなっています。次に、ハードウェアにクリッピングとw座標による除算を実行させます。これは、ユーザーに任せる理由がなくなったためです。また、クリップ後の頂点シェーダーが必要ないため、より簡単です(ビューポートへのマッピングも含まれますが、これは別の話です)。彼らがそれを設計した方法は実際にはかなりいいです。 :)

2
t0rakka