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

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import Stats from 'three/addons/libs/stats.module.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';

/*
	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
*/

let camera, scene, renderer, controls, stats, gui;
let cube, triangle;
let running = true;
let geometry, vertices, indices, material;

const init = function () {
	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.setPixelRatio( window.devicePixelRatio );
	renderer.setSize( window.innerWidth, window.innerHeight );
	document.body.appendChild( renderer.domElement );
	
	controls = new OrbitControls( camera, renderer.domElement );
	controls.update();
	
	stats = new Stats();
	document.body.appendChild( stats.dom );

	gui = new GUI();

	const light = new THREE.HemisphereLight();
	light.intensity = 3;
	scene.add( light );

	const axesHelper = new THREE.AxesHelper( 5 );
	scene.add( axesHelper );

	// On resize, update camera and renderer
	window.addEventListener('resize', function(event) {
		camera.aspect = window.innerWidth / window.innerHeight;
		camera.updateProjectionMatrix();
		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();
			}
		}
	});
};

const render = () => {
	renderer.render( scene, camera...