KhronosGroup / WebGL

The Official Khronos WebGL Repository
Other
2.65k stars 670 forks source link

CSS pixel size -> WebGL canvas size example is subtly wrong? #2460

Open juj opened 7 years ago

juj commented 7 years ago

Consider the example of choosing a WebGL backbuffer size based on the CSS pixel size of the canvas window.devicePixelRatio to match 1:1 with high DPI/physical screen pixels at

https://www.khronos.org/webgl/wiki/HandlingHighDPI

The example reads

<style>
 #theCanvas {
    width: 50%;
    height: 50%;
}
<style>
<canvas id="theCanvas"></canvas>
<script>
window.onload = main();

function main() {
  var canvas = document.getElementById("theCanvas");
  var devicePixelRatio = window.devicePixelRatio || 1;

  // set the size of the drawingBuffer based on the size it's displayed.
  canvas.width = canvas.clientWidth * devicePixelRatio;
  canvas.height = canvas.clientHeight * devicePixelRatio;

  var gl = canvas.getContext("webgl");
  ...
}
</script>

Here window.devicePixelRatio can be an arbitrary number like 1.353239x (it is affected by page zoom level) and the CSS pixel size (canvas.clientWidth/Height) can also be arbitrary float numbers like 732.4315135px as they come from width: 50%, height: 50% sizes, but canvas.width/.height need to be integers, so there's a double->int conversion that takes place.

In this example the physical size of the canvas is set to truncate/round down the computed CSS pixel size to integer units at

  canvas.width = canvas.clientWidth * devicePixelRatio;
  canvas.height = canvas.clientHeight * devicePixelRatio;

however to my understanding there is no guarantee that rounding down is the correct action to perform, nor that rounding to nearest, or rounding up would be the correct action to perform either, in order to get a WebGL rendering output that matches up 1:1 with the pixel size on screen.

In practice this kind of "I don't know whether I should round down, round up or round to nearest" problem can result in off-by-one pixel scenarios for the WebGL render target in different browser implementations, and as result, either the whole WebGL canvas output becomes blurry, as its composited to widen/tallen/shrink by one pixel, or one column or row of the WebGL canvas is duplicated in the middle, or there will be a one-pixel black/white border on the canvas.

To my experience the only correct way to choose the backbuffer size of a WebGL canvas, given an (arbitrary) CSS pixel size, is to first compute the WebGL backbuffer size from the CSS pixel size, e.g. using the above formula, but then after having set the WebGL backbuffer size, propagate that integer size back to the CSS pixel size to ensure that the two match up. That is,


  // set the size of the drawingBuffer based on the size it's displayed.
  canvas.width = canvas.clientWidth * devicePixelRatio;
  canvas.height = canvas.clientHeight * devicePixelRatio;

  // propagate the integer size back to CSS pixels to ensure they align up 1:1.
  canvas.style.width = (canvas.width / devicePixelRatio) + 'px';
  canvas.style.height = (canvas.height / devicePixelRatio) + 'px';

A result of this issue is that to avoid compositing artifacts, it's not possible to use automatic CSS directives such as width: 50% to size a canvas, while expecting a 1:1 match with physical screen size.

Is there anything better to ensure 1:1 match for a canvas size without glitches, or if not, perhaps the example should be updated to instruct developers with this gotcha?