有关 sun/background 渲染的屏幕 space 坐标的问题

Question about screen space coordinates for sun/background rendering

我正在尝试在 three.js 中使用自定义着色器渲染背景和太阳。这个想法是计算太阳的屏幕 space 位置,并在片段着色器中使用这些坐标进行渲染。预期的行为是太阳始终呈现在 (0,1000,-1000) 处的 horizon 处。当你运行这个活生生的例子一查,好像真的是这样。

然而,当您四处移动相机时(因此它沿着 (0,-1,1) 矢量观察),您会注意到太阳突然被镜像并沿着 XY 平面翻转。为什么会这样?这是否与在着色器中计算和评估屏幕 space 坐标的方法有关。

实例实际上是 this GitHub issue 的简化测试用例。

var container;

var camera, cameraFX, scene, sceneFX, renderer;

var uniforms;

var sunPosition = new THREE.Vector3( 0, 1000, - 1000 );
var screenSpacePosition = new THREE.Vector3();

init();
animate();

function init() {

 container = document.getElementById( 'container' );

 camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.1, 2000 );
 camera.position.set( 0, 0, 10 );
 
 cameraFX = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );

 scene = new THREE.Scene();
 
 scene.add( new THREE.AxesHelper( 5 ) );
 
 sceneFX = new THREE.Scene();

 var geometry = new THREE.PlaneBufferGeometry( 2, 2 );

 uniforms = {
  "aspect": { value: window.innerWidth / window.innerHeight },
  "sunPositionScreenSpace": { value: new THREE.Vector2() }
 };

 var material = new THREE.ShaderMaterial( {

  uniforms: uniforms,
  vertexShader: document.getElementById( 'vertexShader' ).textContent,
  fragmentShader: document.getElementById( 'fragmentShader' ).textContent

 } );

 var quad = new THREE.Mesh( geometry, material );
 sceneFX.add( quad );

 renderer = new THREE.WebGLRenderer();
 renderer.setSize( window.innerWidth, window.innerHeight );
 renderer.autoClear = false;
 container.appendChild( renderer.domElement );

 var controls = new THREE.OrbitControls( camera, renderer.domElement );

}


//

function animate( timestamp ) {

 requestAnimationFrame( animate );
 
 renderer.clear();
 
 // background/sun pass

 screenSpacePosition.copy( sunPosition ).project( camera );
 
 screenSpacePosition.x = ( screenSpacePosition.x + 1 ) / 2;
 screenSpacePosition.y = ( screenSpacePosition.y + 1 ) / 2;

 uniforms[ "sunPositionScreenSpace" ].value.copy( screenSpacePosition );

 renderer.render( sceneFX, cameraFX );
 
 // beauty pass
 
 renderer.clearDepth();
 renderer.render( scene, camera );

}
body {
  margin: 0;
}
canvas {
  display: block;
}
<script src="https://cdn.jsdelivr.net/npm/three@0.116.1/build/three.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.116.1/examples/js/controls/OrbitControls.js"></script>

<div id="container">

</div>
<script id="vertexShader" type="x-shader/x-vertex">

   varying vec2 vUv;

   void main() {

    vUv = uv;

    gl_Position = vec4( position, 1.0 );

   }

</script>

<script id="fragmentShader" type="x-shader/x-fragment">

   varying vec2 vUv;

   uniform vec2 sunPositionScreenSpace;
   uniform float aspect;

   const vec3 sunColor = vec3( 1.0, 0.0, 0.0 );
   const vec3 bgColor = vec3( 1.0, 1.0, 1.0 );

   void main() {

    vec2 diff = vUv - sunPositionScreenSpace;
    diff.x *= aspect;

    // background/sun drawing

    float prop = clamp( length( diff ) / 0.5, 0.0, 1.0 );
    prop = 0.35 * pow( 1.0 - prop, 3.0 );

    gl_FragColor.rgb = mix( sunColor, bgColor, 1.0 - prop );
    gl_FragColor.a = 1.0;

   }

</script>

这个问题是因为太阳在视角的后面。请注意,在透视投影中,观看体积是 Frustum。每个点都沿着穿过视口上相机位置的射线进行投影。如果该点在视点后面,则它是镜像的,因为它沿着这条射线投射。
通常这无关紧要,因为近平面前面的所有几何体都被剪裁了。

剪辑space坐标是Homogeneous coordinate。您必须计算 clipspace 坐标,并评估 z 分量是否小于 0。
请注意,您不能使用 Vector3.project, because project computes the normalized device space coordinate. In NDC it cannot be distinguished if the position is in front of or behind the camera, because after the Perspective divide z 分量的单数丢失。在clipspace中进行裁剪,裁剪规则为:

-w <= x, y, z <= w. 

按均匀方向定义太阳位置:

var sunPosition = new THREE.Vector4( 0, 1, - 1, 0 );

计算剪辑 space 坐标并评估 z 分量是否为负:

let clipPosition = sunPosition
   .clone()
   .applyMatrix4(camera.matrixWorldInverse)
   .applyMatrix4(camera.projectionMatrix);

screenSpacePosition.x = ( clipPosition.x / clipPosition.w + 1 ) / 2;
screenSpacePosition.y = ( clipPosition.y / clipPosition.w + 1 ) / 2;

if (clipPosition.z < 0.0) {
    // [...]
}