WebGL month. Day 12. Highdpi displays and webgl viewport
Andrei Lesnitsky
Posted on July 12, 2019
Day 12. Highdpi displays and webgl viewport
This is a series of blog posts related to WebGL. New post will be available every day
Join mailing list to get new posts right to your inbox
Built with
Hey 👋 Welcome back to WebGL month
All previous tutorials where done on a default size canvas, let's make the picture bigger!
We'll need to tune a bit of css first to make body fill the screen
📄 index.html
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>WebGL Month</title>
+
+ <style>
+ html, body {
+ height: 100%;
+ }
+
+ body {
+ margin: 0;
+ }
+ </style>
</head>
<body>
<canvas></canvas>
Now we can read body dimensions
📄 src/texture.js
const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl');
+ const width = document.body.offsetWidth;
+ const height = document.body.offsetHeight;
+
const vShader = gl.createShader(gl.VERTEX_SHADER);
const fShader = gl.createShader(gl.FRAGMENT_SHADER);
And set canvas dimensions
📄 src/texture.js
const width = document.body.offsetWidth;
const height = document.body.offsetHeight;
+ canvas.width = width;
+ canvas.height = height;
+
const vShader = gl.createShader(gl.VERTEX_SHADER);
const fShader = gl.createShader(gl.FRAGMENT_SHADER);
Ok, canvas size changed, but our picture isn't full screen, why?
Turns out that changing canvas size isn't enought, we also need to specify a viwport. Treat viewport as a rectangle which will be used as drawing area and interpolate it to [-1...1]
clipspace
📄 src/texture.js
gl.uniform2fv(programInfo.uniformLocations.resolution, [canvas.width, canvas.height]);
+ gl.viewport(0, 0, canvas.width, canvas.height);
+
gl.drawElements(gl.TRIANGLES, indexBuffer.data.length, gl.UNSIGNED_BYTE, 0);
});
Now our picture fills the whole document, but it is a bit blurry. Obvious reason – our texture is not big enough, so it should be stretched and loses quality. That's correct, but there is another reason.
Modern displays fit higher amount of actual pixels in a physical pixel size (apple calls it retina). There is a global variable devicePixelRatio
which might help us.
📄 src/texture.js
const width = document.body.offsetWidth;
const height = document.body.offsetHeight;
- canvas.width = width;
- canvas.height = height;
+ canvas.width = width * devicePixelRatio;
+ canvas.height = height * devicePixelRatio;
const vShader = gl.createShader(gl.VERTEX_SHADER);
const fShader = gl.createShader(gl.FRAGMENT_SHADER);
Ok, now our canvas has an appropriate size, but it is bigger than body on retina displays. How do we fix it?
We can downscale canvas to a physical size with css width
and height
property
📄 src/texture.js
canvas.width = width * devicePixelRatio;
canvas.height = height * devicePixelRatio;
+ canvas.style.width = `${width}px`;
+ canvas.style.height = `${height}px`;
+
const vShader = gl.createShader(gl.VERTEX_SHADER);
const fShader = gl.createShader(gl.FRAGMENT_SHADER);
Just to summarize, width
and height
attributes of canvas specify actual size in pixels, but in order to make picture sharp on highdpi displays we need to multiply width and hegiht on devicePixelRatio
and downscale canvas back with css
Now we can alos make our canvas resizable
📄 src/texture.js
gl.drawElements(gl.TRIANGLES, indexBuffer.data.length, gl.UNSIGNED_BYTE, 0);
});
+
+
+ window.addEventListener('resize', () => {
+ const width = document.body.offsetWidth;
+ const height = document.body.offsetHeight;
+
+ canvas.width = width * devicePixelRatio;
+ canvas.height = height * devicePixelRatio;
+
+ canvas.style.width = `${width}px`;
+ canvas.style.height = `${height}px`;
+
+ gl.viewport(0, 0, canvas.width, canvas.height);
+ });
Oops, canvas clears after resize. Turns out that modification of width
or height
attribute forces browser to clear canvas (the same for 2d
context), so we need to issue a draw call again.
📄 src/texture.js
canvas.style.height = `${height}px`;
gl.viewport(0, 0, canvas.width, canvas.height);
+
+ gl.drawElements(gl.TRIANGLES, indexBuffer.data.length, gl.UNSIGNED_BYTE, 0);
});
That's it for today, see you tomorrow 👋
Join mailing list to get new posts right to your inbox
Built with
Posted on July 12, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.