Perspective Matrix - JSFiddle - Code Playground
Code panel options
Change code languages, preprocessors and plugins
Language
Doctype
XHTML 1.0 Strict
XHTML 1.0 Transitional
HTML 5
HTML 4.01 Strict
HTML 4.01 Transitional
HTML 4.01 Frameset
Body tag
Language
-
JavaScript
-
CoffeeScript
-
JavaScript 1.7
-
Babel + JSX
-
TypeScript
-
CoffeeScript 2
-
Vue
-
React
-
Preact
Extensions
script attribute
Language
-
CSS
-
SCSS
-
SASS
-
PostCSS (Stage 0+)
-
PostCSS (Stage 3+)
-
Tailwind CSS
Reset CSS
<!-- The vertex shader operates on individual vertices in our model data by setting gl_Position -->
<script id="vertex-shader" type="x-shader/x-vertex">
//Each point has a position and color
attribute vec3 position;
attribute vec4 color;
// The transformation matrices
uniform mat4 model;
uniform mat4 projection;
// Pass the color attribute down to the fragment shader
varying vec4 vColor;
void main() {
// Pass the color down to the fragment shader
vColor = color;
// Read the multiplication in reverse order, the point is taken from
// the original model space and moved into world space. It is then
// projected into clip space as a homogeneous point. Generally the
// W value will be something other than 1 at the end of it.
gl_Position = projection * model * vec4( position, 1.0 );
}
</script>
<!-- The fragment shader determines the color of the final pixel by setting gl_FragColor -->
<script id="fragment-shader" type="x-shader/x-fragment">
precision mediump float;
varying vec4 vColor;
void main() {
gl_FragColor = vColor;
// gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
</script>
<canvas id="canvas"></canvas>
<h1 class='lesson-title'>
Perspective Matrix
</h1>
<script>
//Shared code for the examples
// Define the MDN global
var MDN = {};
// Define the data that is needed to make a 3d cube
MDN.createCubeData = function() {
var positions = [
// Front face
-1.0, -1.0, 1.0,
1.0, -1.0, 1.0,
1.0, 1.0, 1.0,
-1.0, 1.0, 1.0,
// Back face
-1.0, -1.0, -1.0,
-1.0, 1.0, -1.0,
1.0, 1.0, -1.0,
1.0, -1.0, -1.0,
// Top face
-1.0, 1.0, -1.0,
-1.0, 1.0, 1.0,
1.0, 1.0, 1.0,
1.0, 1.0, -1.0,
// Bottom face
-1.0, -1.0, -1.0,
1.0, -1.0, -1.0,
1.0, -1.0, 1.0,
-1.0, -1.0, 1.0,
// Right face
1.0, -1.0, -1.0,
1.0, 1.0, -1.0,
1.0, 1.0, 1.0,
1.0, -1.0, 1.0,
// Left face
-1.0, -1.0, -1.0,
-1.0, -1.0, 1.0,
-1.0, 1.0, 1.0,
-1.0, 1.0, -1.0
];
var colorsOfFaces = [
[0.3, 1.0, 1.0, 1.0], // Front face: cyan
[1.0, 0.3, 0.3, 1.0], // Back face: red
[0.3, 1.0, 0.3, 1.0], // Top face: green
[0.3, 0.3, 1.0, 1.0], // Bottom face: blue
[1.0, 1.0, 0.3, 1.0], // Right face: yellow
[1.0, 0.3, 1.0, 1.0] // Left face: purple
];
var colors = [];
for (var j=0; j<6; j++) {
var polygonColor = colorsOfFaces[j];
for (var i=0; i<4; i++) {
colors = colors.concat( polygonColor );
}
}
var elements = [
0, 1, 2, 0, 2, 3, // front
4, 5, 6, 4, 6, 7, // back
8, 9, 10, 8, 10, 11, // top
12, 13, 14, 12, 14, 15, // bottom
16, 17, 18, 16, 18, 19, // right
20, 21, 22, 20, 22, 23 // left
]
return {
positions: positions,
elements: elements,
colors: colors
}
}
// Take the data for a cube and bind the buffers for it.
// Return an object collection of the buffers
MDN.createBuffersForCube = function( gl, cube ) {
var positions = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positions);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(cube.positions), gl.STATIC_DRAW);
var colors = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colors);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(cube.colors), gl.STATIC_DRAW);
var elements = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elements);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(cube.elements), gl.STATIC_DRAW);
return {
positions: positions,
colors: colors,
elements: elements
}
}
MDN.matrixArrayToCssMatrix = function (array) {
return "matrix3d(" + array.join(',') + ")";
}
MDN.multiplyPoint = function (matrix, point) {
var x = point[0], y = point[1], z = point[2], w = point[3];
var c1r1 = matrix[ 0], c2r1 = matrix[ 1], c3r1 = matrix[ 2], c4r1 = matrix[ 3],
c1r2 = matrix[ 4], c2r2 = matrix[ 5], c3r2 = matrix[ 6], c4r2 = matrix[ 7],
c1r3 = matrix[ 8], c2r3 = matrix[ 9], c3r3 = matrix[10], c4r3 = matrix[11],
c1r4 = matrix[12], c2r4 = matrix[13], c3r4 = matrix[14], c4r4 = matrix[15];
return [
x*c1r1 + y*c1r2 + z*c1r3 + w*c1r4,
x*c2r1 + y*c2r2 + z*c2r3 + w*c2r4,
x*c3r1 + y*c3r2 + z*c3r3 + w*c3r4,
x*c4r1 + y*c4r2 + z*c4r3 + w*c4r4
];
}
MDN.multiplyMatrices = function (a, b) {
// TODO - Simplify for explanation
// currently taken from https://github.com/toji/gl-matrix/blob/master/src/gl-matrix/mat4.js#L306-L337
var result = [];
var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15];
// Cache only the current line of the second matrix
var b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3];
result[0] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
result[1] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
result[2] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
result[3] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7];
result[4] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
result[5] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
result[6] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
result[7] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11];
result[8] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
result[9] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
result[10] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
result[11] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15];
result[12] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
result[13] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
result[14] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
result[15] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
return result;
}
MDN.multiplyArrayOfMatrices = function (matrices) {
var inputMatrix = matrices[0];
for(var i=1; i < matrices.length; i++) {
inputMatrix = MDN.multiplyMatrices(inputMatrix, matrices[i]);
}
return inputMatrix;
}
MDN.rotateXMatrix = function (a) {
var cos = Math.cos;
var sin = Math.sin;
return [
1, 0, 0, 0,
0, cos(a), -sin(a), 0,
0, sin(a), cos(a), 0,
0, 0, 0, 1
];
}
MDN.rotateYMatrix = function (a) {
var cos = Math.cos;
var sin = Math.sin;
return [
cos(a), 0, sin(a), 0,
0, 1, 0, 0,
-sin(a), 0, cos(a), 0,
0, 0, 0, 1
];
}
MDN.rotateZMatrix = function (a) {
var cos = Math.cos;
var sin = Math.sin;
return [
cos(a), -sin(a), 0, 0,
sin(a), cos(a), 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
];
}
MDN.translateMatrix = function (x, y, z) {
return [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
x, y, z, 1
];
}
MDN.scaleMatrix = function (w, h, d) {
return [
w, 0, 0, 0,
0, h, 0, 0,
0, 0, d, 0,
0, 0, 0, 1
];
}
MDN.perspectiveMatrix = function (fieldOfViewInRadians, aspectRatio, near, far) {
// Construct a perspective matrix
/*
Field of view - the angle in radians of what's in view along the Y axis
Aspect Ratio - the ratio of the canvas, typically canvas.width / canvas.height
Near - Anything before this point in the Z direction gets clipped (outside of the clip space)
Far - Anything after this point in the Z direction gets clipped (outside of the clip space)
*/
var f = 1.0 / Math.tan(fieldOfViewInRadians / 2);
var rangeInv = 1 / (near - far);
return [
f / aspectRatio, 0, 0, 0,
0, f, 0, 0,
0, 0, (near + far) * rangeInv, -1,
0, 0, near * far * rangeInv * 2, 0
];
}
MDN.orthographicMatrix = function(left, right, bottom, top, near, far) {
// Each of the parameters represents the plane of the bounding box
var lr = 1 / (left - right);
var bt = 1 / (bottom - top);
var nf = 1 / (near - far);
var row4col1 = (left + right) * lr;
var row4col2 = (top + bottom) * bt;
var row4col3 = (far + near) * nf;
return [
-2 * lr, 0, 0, 0,
0, -2 * bt, 0, 0,
0, 0, 2 * nf, 0,
row4col1, row4col2, row4col3, 1
];
}
MDN.createShader = function (gl, source, type) {
// Compiles either a shader of type gl.VERTEX_SHADER or gl.FRAGMENT_SHADER
var shader = gl.createShader( type );
gl.shaderSource( shader, source );
gl.compileShader( shader );
if ( !gl.getShaderParameter(shader, gl.COMPILE_STATUS) ) {
var info = gl.getShaderInfoLog( shader );
throw "Could not compile WebGL program. \n\n" + info;
}
return shader
}
MDN.linkProgram = function (gl, vertexShader, fragmentShader) {
var program = gl.createProgram();
gl.attachShader( program, vertexShader );
gl.attachShader( program, fragmentShader );
gl.linkProgram( program );
if ( !gl.getProgramParameter( program, gl.LINK_STATUS) ) {
var info = gl.getProgramInfoLog(program);
throw "Could not compile WebGL program. \n\n" + info;
}
return program;
}
MDN.createWebGLProgram = function (gl, vertexSource, fragmentSource) {
// Combines MDN.createShader() and MDN.linkProgram()
var vertexShader = MDN.createShader( gl, vertexSource, gl.VERTEX_SHADER );
var fragmentShader = MDN.createShader( gl, fragmentSource, gl.FRAGMENT_SHADER );
return MDN.linkProgram( gl, vertexShader, fragmentShader );
}
MDN.createWebGLProgramFromIds = function (gl, vertexSourceId, fragmentSourceId) {
var vertexSourceEl = document.getElementById(vertexSourceId);
var fragmentSourceEl = document.getElementById(fragmentSourceId);
return MDN.createWebGLProgram(
gl,
vertexSourceEl.innerHTML,
fragmentSourceEl.innerHTML
);
}
MDN.createContext = function (canvas) {
var gl;
try {
// Try to grab the standard context. If it fails, fallback to experimental.
gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
}
catch(e) {}
// If we don't have a GL context, give up now
if (!gl) {
var message = "Unable to initialize WebGL. Your browser may not support it.";
alert(message);
throw new Error(message);
gl = null;
}
return gl;
}
</script>
html, body {
width:100%;
height:100%;
margin: 0;
}
canvas {
width: 100% !important;
height: 100% !important;
}
.lesson-title {
position: absolute;
bottom: 0;
left: 0;
margin: 0.7em;
font-family: sans-serif;
font-size: 1em;
color: #888;
font-weight: normal;
}
.lesson-title a {
text-decoration: none;
color: #668;
}
.lesson-title a:hover {
color: #338;
}
/*
Up to this point we've proceeded step by step to create our own 3d rendering setup. However the current rig has some issues. For one it gets skewed whenever we resize our window. Another is that our simple projection doesn't handle a wide range of values for the scene data. Most scenes don't work in clip space and can have a wide range of values. It would be helpful to define what distance is relevant to the scene so that precision isn't lost in converting the numbers. Finally it's very helpful to have a fine-tuned control over what points get placed inside and outside of clip space. In the previous examples the corners of the cube in fact occasionally get clipped.
The perspective matrix is a type of projection matrix that accomplishes all of these requirements. The math also starts to get a lot more complicated and won't be explained in these examples. In short it combines dividing by W like was done with the previous examples plus some ingenious trigonometry manipulations. For more information about the math behind it check some of the following links:
http://www.songho.ca/opengl/gl_projectionmatrix.html
http://ogldev.atspace.co.uk/www/tutorial12/tutorial12.html
http://stackoverflow.com/questions/28286057/trying-to-understand-the-math-behind-the-perspective-matrix-in-webgl/28301213#28301213
One important thing to note about the perspective matrix used below is that it flips the z axis. In clip space the z+ goes away from the viewer, while with this matrix it comes towards the viewer.
The MDN.perspectiveMatrix() function takes 4 arguments.
fieldOfViewInRadians:
This represents the angle of how much is in view in the scene. The larger the number is, the more that is visible by the camera. The geometry at the edges becomes more and more distorted. This is equivalent to a wide angle lens. When the field of view is larger the objects typically get smaller.
When the field of view is smaller, then the camera can see less and less in the scene. The objects are distorted much less by perspective and objects seem much close to the camera.
aspectRatio:
This is the aspect ratio of the scene, the width divided by height. In these examples that's the window width divided by the window height. This parameter will finally make the example not warped by the size of the canvas.
nearClippingPlaneDistance
This positive number represents the plane that clips off geometry that is too close to the camera. Anything at this distance will be at -1 in clip space. It shouldn't be set to 0.
farClippingPlaneDistance
This positive number represents the plane that clips off geometry that is too far away from the camera. Anything at this distance will be at 1 in clip space. It should be kept reasonably close to the distance of the geometry so that precision errors don't creep into the rendering.
In the code below the projection matrix has been swapped out with the .computePerspectiveMatrix() method. The position and scale matrix of the model has been changed to take it more out of clip space and into a larger coordinate system.
Exercises:
1) Experiment with the parameters of the perspective matrix and the model matrix.
2) Swap out the perspective matrix to use orthographic projection. In the shared code there is the function MDN.orthographicMatrix() that can replace the MDN.perspectiveMatrix() function in .computePerspectiveMatrix().
*/
function CubeDemo () {
// Prep the canvas
this.canvas = document.getElementById("canvas");
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
// Grab a context
this.gl = MDN.createContext(this.canvas);
this.transforms = {}; // All of the matrix transforms
this.locations = {}; //All of the shader locations
// Get the rest going
this.buffers = MDN.createBuffersForCube(this.gl, MDN.createCubeData() );
this.webglProgram = this.setupProgram();
}
CubeDemo.prototype.setupProgram = function() {
var gl = this.gl;
// Setup a WebGL program
var webglProgram = MDN.createWebGLProgramFromIds(gl, "vertex-shader", "fragment-shader");
gl.useProgram(webglProgram);
// Save the attribute and uniform locations
this.locations.model = gl.getUniformLocation(webglProgram, "model");
this.locations.projection = gl.getUniformLocation(webglProgram, "projection");
this.locations.position = gl.getAttribLocation(webglProgram, "position");
this.locations.color = gl.getAttribLocation(webglProgram, "color");
// Tell WebGL to test the depth when drawing
gl.enable(gl.DEPTH_TEST);
return webglProgram;
};
CubeDemo.prototype.computePerspectiveMatrix = function() {
var fieldOfViewInRadians = Math.PI * 0.5;
var aspectRatio = window.innerWidth / window.innerHeight;
var nearClippingPlaneDistance = 1;
var farClippingPlaneDistance = 50;
this.transforms.projection = MDN.perspectiveMatrix(
fieldOfViewInRadians,
aspectRatio,
nearClippingPlaneDistance,
farClippingPlaneDistance
);
};
CubeDemo.prototype.computeModelMatrix = function( now ) {
//Scale up
var scale = MDN.scaleMatrix(5, 5, 5);
// Rotate a slight tilt
var rotateX = MDN.rotateXMatrix( now * 0.0003 );
// Rotate according to time
var rotateY = MDN.rotateYMatrix( now * 0.0005 );
// Move slightly down
var position = MDN.translateMatrix(0, 0, -20);
// Multiply together, make sure and read them in opposite order
this.transforms.model = MDN.multiplyArrayOfMatrices([
position, // step 4
rotateY, // step 3
rotateX, // step 2
scale // step 1
]);
// Performance caveat: in real production code it's best not to create
// new arrays and objects in a loop. This example chooses code clarity
// over performance.
};
CubeDemo.prototype.draw = function() {
var gl = this.gl;
var now = Date.now();
// Compute our matrices
this.computeModelMatrix( now );
this.computePerspectiveMatrix( 0.5 );
// Update the data going to the GPU
this.updateAttributesAndUniforms();
// Perform the actual draw
gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0);
// Run the draw as a loop
requestAnimationFrame( this.draw.bind(this) );
};
CubeDemo.prototype.updateAttributesAndUniforms = function() {
var gl = this.gl;
// Setup the color uniform that will be shared across all triangles
gl.uniformMatrix4fv(this.locations.model, false, new Float32Array(this.transforms.model));
gl.uniformMatrix4fv(this.locations.projection, false, new Float32Array(this.transforms.projection));
// Set the positions attribute
gl.enableVertexAttribArray(this.locations.position);
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.positions);
gl.vertexAttribPointer(this.locations.position, 3, gl.FLOAT, false, 0, 0);
// Set the colors attribute
gl.enableVertexAttribArray(this.locations.color);
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.colors);
gl.vertexAttribPointer(this.locations.color, 4, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.buffers.elements );
};
var cube = new CubeDemo();
cube.draw();
Color Palette Generator
Generate a cool color palette with a few clicks
CSS Flexbox Generator
Generate your CSS Flexbox layout in the simplest of ways
Coder Fonts
Curated list of quality monospace fonts for coders
Share or embed fiddle
Customize the embeddable experience for websites
Tabs:
JavaScript
HTML
CSS
Result
Visual:
Light
Dark
No autoresizing to fit the code
Render blocking of the parent page
Editor settings
Customize the behavior and feel of the editor
Behavior
Auto-run code
Only auto-run code that validates
Auto-save code
Live code validation
Hot reload CSS
Hot reload HTML
General
Line numbers
Wrap lines
Indent With Spaces
Code Autocomplete
Indent size:
2 spaces
4 spaces
Font size:
10px
11px
12px
13px
14px
15px
16px
17px
18px
19px
20px
Console
Console in the editor
Clear console on run
Your recent fiddles
Recently created fiddles, including ones created while logged out
JSFiddle changelog
A log of all the changes made to JSFiddle – big and small.
-
Curated list of monospace coder fonts
You can now use different monospace fonts in the editor − we now have a curated list of pretty awesome fonts available including premium ones. Just open the Coder Fonts mini-app from the sidebar or from Editor settings. My current favorites are Input and Commit Mono.
-
CSS Flexbox generator as a JSFiddle app
Our CSS Flexbox generator lets you create a layout, and skip knowing the confusing properties and value names (let's be honest the W3C did not make a good job here). Not gonna lie, this was heavily inspired by flexer.dev but coded completely from scratch.
-
Behavior change for External Resources
Adding External Resources will no longer create a list of resources in the sidebar but will be injected as a LINK or SCRIPT tag inside of the HTML panel.
-
Code Completion with additional context
The Code Completion will now also have the context of all panels before suggesting code to you - so if for example you
have some CSS or JS, the HTML panel will suggest code based on the other two panels.
-
🦄 AI Code Completion (beta)
Introducing some AI sprinkle in the editor - Code Completion based on the Codestral model (by Mistral).
For now it's a BYOK implmentation which means you need to provide your own API Key − you can get it for free.
-
Editor switch from CodeMirror to Monaco (same as VSCode)
After much deliberation I've decided to make the switch from CodeMirror to Monaco.
There's a few reasons for this. CodeMirror 5 is no longer being developed, and the switch to 6 would be a huge
rewrite since there's not much compatibility between the two versions.
Monaco itself has lots of features already built-in, things that took quite a few external plugins to get
into the CodeMirror implementation.
I'm incredibly thankful to Marijn for his work on CodeMirror, it has served well for many years.
-
JSFiddle will load faster
Technical debt is a drag man. Remember the time when MooTools was state-of-art JS framework? We do and so much of JSFiddle was still dependant on it till this day, but since almost all MooTools features are now
available in native JS it was high-time to strip it out of the codebase.
This took around a week of work, lots of testing, but it's now done. And the final package of our
JS bundle is ~30% smaller.
Add a new collection
Collect your fiddles in collections
Get a Mistral API Key
A short guide to getting a free Mistral API Key.
Sign up for a Mistral account, and pick the free Experiment subscription plan.
Log in, and go to your organization's API Keys section.
Click Create new key, fill "JSFiddle" as the name for the API key, and save.
Copy the key, and paste it into JSFiddle − under the AI Code Completion in the Sidebar.
Done! AI Code Completion should now be working.
Classic
Columns
Bottom results
Right results
Tabs (columns)
Tabs (rows)
System
Light
Dark
Set fiddle expiration
1 day
10 days
1 month
6 months
1 year
Keep forever
Please Whitelist JSFiddle in your content blocker.
Help keep JSFiddle free for always by one of two ways:
- Whitelist JSFiddle in your content blocker (two clicks)
- Go PRO and get access to additional PRO features →
-
Ad-free
All ads in the editor and listing pages are turned completely off.
-
Use pre-released features
You get to try and use features (like the Palette Color Generator) months before everyone else.
-
Fiddle collections
Sort and categorize your Fiddles into multiple collections.
-
Private collections and fiddles
You can make as many Private Fiddles, and Private Collections as you wish!
-
Console
Debug your Fiddle with a minimal built-in JavaScript console.
Join the 4+ million users, and keep the JSFiddle dream alive.
-
Ad-free
All ads in the editor and listing pages are turned completely off.
-
Use pre-released features
You get to try and use features (like the Palette Color Generator) months before everyone else.
-
Fiddle collections
Sort and categorize your Fiddles into multiple collections.
-
Private collections and fiddles
You can make as many Private Fiddles, and Private Collections as you wish!
-
Console
Debug your Fiddle with a minimal built-in JavaScript console.
JSFiddle is used by you and 4+ million other developers, in many companies ...
... and top educational institutions: