Spaces and coordinates (in THREE.js)

by PhilQ

HTML

<script type="x-shader/x-vertex" id="vertexShader">
	uniform vec2 resolution;

	varying vec4 vPos;
	varying vec3 vNormal;
	varying vec2 vUv;
	varying mat4 matModel;
	varying mat4 matModelView;
	varying mat4 matProjection;
	varying mat3 matNormal;

	void main() {
		// Send to fragment shader
		vNormal = normal;
		vUv = uv;
		vPos = vec4(position, 1.0);
		matModel = modelMatrix;
		matModelView = modelViewMatrix;
		matProjection = projectionMatrix;
		matNormal = normalMatrix;

		// Local space (object)
		gl_Position = vPos;
		
		// to World space (scene)
		gl_Position = modelMatrix * vPos;
		
		// to View space (camera)
		gl_Position = viewMatrix * modelMatrix * vPos;
		
		// to Clip space
		gl_Position = projectionMatrix * viewMatrix * modelMatrix * vPos;

		// Note: Three.js will convert to NDC space and then to Screen/Viewport space
	}
</script>
<script type="x-shader/x-fragment" id="fragmentShader">
	uniform vec2 resolution;

	varying vec4 vPos;
	varying vec3 vNormal;
	varying vec2 vUv;
	varying mat4 matModel;
	varying mat4 matModelView;
	varying mat4 matProjection;
	varying mat3 matNormal;

	void main() {
		vec4 localUv = vec4(vUv, 0.0, 1.0); // Local uv, bottom-left (0,0) to top-right (1,1)
		vec4 localPos = vPos;
		vec4 worldPos = matModel * vPos;
		vec4 viewPos = viewMatrix * matModel * vPos;
		vec4 clipPos = matProjection * viewMatrix * matModel * vPos;
		vec4 ndcPos = vec4(clipPos.xyz / clipPos.w, clipPos.w); // Added w to be able to back-calculate to clip space
		vec2 screenPos = (ndcPos.xy + 1.0) * 0.5; // Screen uv, bottom-left (0,0) to top-right (1,1)
		vec2 viewportPos = vec2(screenPos.x, -screenPos.y); // Top-left (0,0) to bottom-right (1,1)
		vec2 pixelPos = screenPos * resolution; // Actual pixel coordinates
		
		// gl_FragColor = vec4(screenPos.xy, 0.0, 1.0);
		
		// Basic color + lighting
		vec4 color = vec4(1.0, 0.0, 1.0, 1.0);
		vec3 light = normalize( vec3( 0.5, 0.2, 1.0 ) ); // Normalized direction (like sunlight)
		float...

CSS

body { margin: 0; }
canvas { width: 100%; height: 100% }

JavaScript

/*
	For reference, read:
	https://learnopengl.com/Getting-started/Coordinate-Systems
	https://www.songho.ca/opengl/gl_projectionmatrix.html
	https://www.songho.ca/opengl/gl_transform.html
	https://threejs.org/docs/#api/en/renderers/webgl/WebGLProgram
	
	Perspective vs orthographic projection:
	https://threejs.org/examples/?q=camera#webgl_camera
	https://threejs.org/examples/?q=camera#webgl_camera_cinematic
*/

var camera, scene, renderer;
var geometry, material, cube;
var running = true;

var init = function () {
	// Add scene, camera and renderer
	scene = new THREE.Scene();
	camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 );
	camera.position.z = 1;
	renderer = new THREE.WebGLRenderer();
	renderer.setSize( window.innerWidth, window.innerHeight );
	document.body.appendChild( renderer.domElement );

	// Add cube
	geometry = new THREE.BoxGeometry( 1, 1, 1 );
	material = new THREE.ShaderMaterial({
		uniforms: {
			resolution: { value: new THREE.Vector2(window.innerWidth,  window.innerHeight) }
		},
		vertexShader: document.getElementById( 'vertexShader' ).textContent,
		fragmentShader: document.getElementById( 'fragmentShader' ).textContent
	});
	cube = new THREE.Mesh( geometry, material );
	scene.add( cube );

	// On resize, update camera, renderer and resolution uniform
	window.addEventListener('resize', function(event) {
		camera.aspect = window.innerWidth / window.innerHeight;
		camera.updateProjectionMatrix();

		material.uniforms.resolution.value.set(window.innerWidth,  window.innerHeight);

		renderer.setSize( window.innerWidth, window.innerHeight );
	});

	// Play/pause animation by pressing P
	window.addEventListener('keydown', function(event) {
		if (event.code == 'KeyP') {
			running = !running;
			if (running) {
				animate();
			}
		}
	});
};

var animate = function () {
	if (running) {
		requestAnimationFrame( animate );
	}

	cube.rotation.x += 0.01;
	cube.rotation.y += 0.01;
	camera.position.z +=...