I'm trying to render a background and sun with a custom shader in three.js. The idea is to compute a screen space position for the sun and use these coordinates in the fragment shader for rendering. The expected behavior is that the sun is always rendered at the horizon at (0,1000,-1000). When you run the live example and just look up, it seems this is actually the case.
However, when you move the camera around (so it looks along the (0,-1,1) vector), you will notice that the sun is suddenly mirrored and flipped along the XY plane. Why is this happening? Is this related to the approach how screen space coordinates are computed and evaluated in the shader.
The live example is actually a reduced test case of the 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>