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 +=...