首页 > 解决方案 > 关于太阳/背景渲染的屏幕空间坐标问题

问题描述

我正在尝试使用自定义着色器渲染背景和太阳three.js。这个想法是计算太阳的屏幕空间位置,并在片段着色器中使用这些坐标进行渲染。预期的行为是太阳总是在地平线处渲染(0,1000,-1000)。当您运行实时示例并查看时,似乎情况确实如此。

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

现场示例实际上是这个GitHub 问题的简化测试用例。

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>

标签: javascriptthree.jsglslshader

解决方案


造成这个问题,是因为太阳是落后的观点。请注意,在透视投影中,查看体积是Frustum。每个点通过视口上的相机位置沿射线投影。如果该点位于视点后面,则它是镜像的,因为它是沿着这条射线投影的。
一般来说,这并不重要,因为近平面前面的所有几何图形都被剪裁了。

剪辑空间坐标是一个齐次坐标。您必须计算剪辑空间坐标,并评估 z 分量是否小于 0。
注意,您不能使用Vector3.project,因为project计算标准化的设备空间坐标。在 NDC 中,无法区分位置是在摄像机的前面还是后面,因为在透视除法之后,z 分量的单分量丢失了。裁剪是在裁剪空间中进行的,裁剪规则是:

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

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

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

计算裁剪空间坐标并评估 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) {
    // [...]
}

推荐阅读