KhronosGroup/WebGL

devicePixelRatio

letochagone opened this issue ยท 8 comments

I figured out the method to display WebGL elements with their "true" sizes.
But I can't figure out how to change the size of their context. Here is an example that will certainly describe my question more clearly.
My screen resolution is 2880x1800, so my window.devicePixelRatio give 2

let canvas= document.querySelector('.canvas');

var desiredCSSWidth = 7;
var desiredCSSHeight = 4;
var devicePixelRatio = window.devicePixelRatio || 1;
console.log(devicePixelRatio);

canvas.width  = desiredCSSWidth *devicePixelRatio;
canvas.height = desiredCSSHeight*devicePixelRatio;

canvas.style.width  = desiredCSSWidth  + "px";
canvas.style.height = desiredCSSHeight + "px";

var gl = canvas.getContext("webgl");

gl.clearColor(X);
gl.clear(gl.COLOR_BUFFER_BIT);

gl.enable(gl.SCISSOR_TEST);
gl.scissor(0, 0, 7, 4);
gl.clearColor(A);
gl.clear(gl.COLOR_BUFFER_BIT);

result :

XXXXXXXXXXXXXX
XXXXXXXXXXXXXX
XXXXXXXXXXXXXX
XXXXXXXXXXXXXX
AAAAAAAXXXXXXX
AAAAAAAXXXXXXX
AAAAAAAXXXXXXX
AAAAAAAXXXXXXX

applying the scissor produces a 7x4 rectangle, as desired.
But the size of the canvas remains double, yet its "style" is also 7x4.
It's definitely not a issues, but I must have misunderstood something

To make Canvas WebGL context default framebuffer 7x4 pixels in size, you should use:

canvas.width  = 7;
canvas.height = 4;

The css style changes do not affect what happens to the context:

canvas.style.width  = desiredCSSWidth  + "px";
canvas.style.height = desiredCSSHeight + "px";

This does not affect the Canvas WebGL context default framebuffer size. This affects how the canvas element is then displayed on screen.

image

what I don't understand is that I specify a display size of 10x10 , yet the displayed size is double.
(maybe it's because I'm on Ubuntu?)

toji commented

CSS pixels (which are what the size of the canvas on the page is measured in) are not necessarily 1:1 with the pixels on your physical display. That's what the devicePixelRatio is communicating: The number of device pixels that correspond to a single CSS pixel. Your devicePixelRatio is 2, so on your device each CSS pixel is equal to 2 pixels on your display, and as a result if you specify that canvas.style.width = 10px; the canvas will be 20 pixels wide on your screen.

If you want to have a canvas that is exactly 10px by 10px then you need to divide that size by the devicePixelRatio when setting the canvas style width and height. (But set the cavas.width and canvas.height to 10 exactly.)

const desiredPixelWidth = 10;
const desiredPixelHeight = 10;
const devicePixelRatio = window.devicePixelRatio || 1;

const canvas = document.querySelector('.canvas');
canvas.width  = desiredPixelWidth;
canvas.height = desiredPixelHeight;
canvas.style.width  = (desiredPixelWidth / devicePixelRatio)  + "px";
canvas.style.height = (desiredPixelHeight / devicePixelRatio) + "px";

The reason for the difference between CSS pixels and device pixels is simply that as displays have gotten higher resolution we don't necessarily want web pages to get smaller and harder to read. So CSS pixels are a virtual pixel size that should keep everything approximately the same scale across the board to make web developers lives a little easier. It does makes it a bit confusing when you start doing graphics work like WebGL that cares more about exact pixel counts than the virtualized CSS pixels, so sorry about that.

@toji
what if the result of the division is not an integer? Isn't that a problem in some cases?

what if the result of the division is not an integer? Isn't that a problem in some cases?

If you want to know the size something is displayed you use ResizeObserver and devicePixelContentBoxSize

See: https://webglfundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html

terse version:

const observer = new ResizeObserver(onSizeChange);
observer.observe(someCanvas, {box: 'device-pixel-content-box'});

function onSizeChange(entries) {
  // entries is an array of elements that have been resized. If we only observer 1 element
  // then the first entry is that element
  const entry = entries[0];

  // size of element in pixels (works in Firefox and Chrome, not Safari)
  const width = entry.devicePixelContentBoxSize[0].inlineSize;
  const height = entry.devicePixelContentBoxSize[0].blockSize;

  // .. do something with width and height, for example size resolution of the canvas ...
  someCanvas.width = width;
  someCanvas.height = height;
}

It is the only correct way to know what size an element is.

A good example of why this API is needed. Assume devicePixelRatio = 2. Then with this HTML

<div style="width: 99.5px; height: 20px; display: flex;">
  <div style="flex: 1 1; width: 50%; height: 100%; background: red;></div>
  <div style="flex: 1 1; width: 50%; height: 100%; background: blue;></div>
</div>

At devicePixelRatio = 2 we've asked the browser to make the outer div 99.5 * 2 CSS pixels which is 199 device pixels. We've ask the 2 children to be 50% big. 50% of 199 is 99.5. You can't have 99.5 pixels so one element will be 99 pixels and the other element will be 100. Which one gets the extra pixel?

<div style="width: 99.5px; height: 20px; display: flex;">
  <div class="a" style="flex: 1 1; width: 50%; height: 100%; background: red;"></div>
  <div class="a" style="flex: 1 1; width: 50%; height: 100%; background: blue;"></div>
</div>
<script>
const observer = new ResizeObserver(onSizeChange);
document.querySelectorAll('.a').forEach(elem => observer.observe(elem, {box: 'device-pixel-content-box'}));

function onSizeChange(entries) {
  for (const entry of entries) {
    // size of element in pixels (works in Firefox and Chrome, not Safari)
    const width = entry.devicePixelContentBoxSize[0].inlineSize;
    const height = entry.devicePixelContentBoxSize[0].blockSize;

    const elem = entry.target;
    console.log('element: ',  elem.style.background);
    console.log('  width we asked for:', elem.getBoundingClientRect().width * devicePixelRatio);
    console.log('  width we got:      ', width);
  }
}
</script>

https://jsgist.org/?src=d1b39a2251266b4480e8027d8e7dba79

On Chrome the first one gets the extra pixel

element:  red 
  width we asked for: 99.5  
  width we got:       100  
element:  blue  index.js:11
  width we asked for: 99.5  
  width we got:       99  

Note: The fact that this API does not exist in Safari means there is no way to know this info in Safari

Also note that devicePixelRatio changes in response to the user zooming the page in Chrome and Firefox (but not Safari)

toji commented

@toji what if the result of the division is not an integer? Isn't that a problem in some cases?

CSS Pixels do not have to be an integer. In fact, devicePixelRatio is frequently a value like 1.5 or 1.3, which naturally leads to a lot of element widths and heights that aren't whole numbers. The exact algorithm for how these are rounded into exact pixel values during compositing is an implementation detail and can vary between browsers/OSes.

@toji

OK !!

image
image

@greggman

(https://stackoverflow.com/a/72611819/6143315)
in this link, I realize that you had explained this problem well
and that you have to "divide" by devicepixelratio
canvas.style.width = px(devicePixelsAcross / devicePixelRatio);