Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
March 9, 2017 12:00 pm

Getting Started in WebGL, Part 4: WebGL Viewport and Clipping

In the previous parts of this series, we learned much about shaders, the canvas element, WebGL contexts, and how the browser alpha-composites our color buffer over the rest of the page elements. 

In this article, we continue writing our WebGL boilerplate code. We are still preparing our canvas for WebGL drawing, this time taking viewports and primitives clipping into account. 

This article is part of the "Getting Started in WebGL" series. If you haven't read the previous parts, I recommend reading them first:


  1. Introduction to Shaders


  2. The Canvas Element for Our First Shader



  3. WebGL Context and Clear


Recap


  • In the first article of this series, we wrote a simple shader that draws a colorful gradient and fades it in and out slightly.

  • In the second article of this series, we started working towards using this shader in a webpage. Taking small steps, we explained the necessary background of the canvas element.


  • In the third article, we acquired our WebGL context and used it to clear the color buffer. We also explained how the canvas blends with the other elements of the page.

In this article, we continue from where we left, this time learning about WebGL viewports and how they affect primitives clipping.

Next in this series—if Allah wills—we'll compile our shader program, learn about WebGL buffers, draw primitives, and actually run the shader program we wrote in the first article. Almost there!

Canvas Size

This is our code so far:

Note that I've restored the CSS background color to black and the clear-color to opaque red.

Thanks to our CSS, we have a canvas that stretches to fill our webpage, but the underlying 1x1 drawing buffer is hardly useful. We need to set a proper size for our drawing buffer. If the buffer is smaller than the canvas, then we are not making full use of the device's resolution and are subject to scaling artifacts (as discussed in a previous article). If the buffer is larger than the canvas, well, the quality actually benefits a lot! It's because of the super-sampling anti-aliasing the browser applies to downscale the buffer before it's handed over to the compositor. 

However, the performance takes a good hit. If anti-aliasing is desired, it's better achieved through MSAA (multi-sampling anti-aliasing) and texture filtering. For now, we should aim at a drawing buffer of the same size of our canvas to make full use of the device's resolution and avoid scaling altogether.

To do this, we'll borrow the adjustCanvasBitmapSize from part 2 (with some modifications):

Changes:


  • We used clientWidth and clientHeight instead of offsetWidth and offsetHeight. The latter ones include the canvas borders, so they may not be exactly what we are looking for. clientWidth and clientHeight are more suited for this purpose. My bad!


  • adjustDrawingBufferSize is now scheduled to run only if changes took place. Therefore, we needn't explicitly check and abort if nothing changed.


  • We no longer need to call drawScene every time the size changes. We'll make sure it's called on a regular basis somewhere else.


  • A glContext.viewport appeared! It gets its own section, so let it pass for now!


We'll also borrow the resize events throttling function, onWindowResize (with some modifications too):

Changes:


  • It's now onCanvasResize instead of onWindowResize. It's ok in our example to assume that the canvas size changes only when the window size is changed, but in the real world, our canvas can be a part of a page where other elements exist, elements that are resizable and affect our canvas size.

  • Instead of listening to the events related to changes in canvas size, we'll just check for changes every time we are about to redraw the canvas contents. In other words, onCanvasResize gets called whether changes occurred or not, so aborting when nothing has changed is necessary.


Now, let's call onCanvasResize from drawScene:

I mentioned that we'll be calling drawScene regularly. This means that we are rendering continuously, not only when changes occur (aka when dirty). Drawing continuously consumes more power than drawing only when dirty, but it saves us the trouble of having to track when the contents have to be updated. 

But it's worth considering if you are planning to make an application that runs for extended periods of time, like wallpapers and launchers (but you wouldn't do these in WebGL to begin with, would you?). Therefore, for this tutorial, we'll be rendering continuously. The easiest way to do it is by scheduling re-running drawScene from within itself:

No, we didn't use setInterval or setTimeout for this. requestAnimationFrame tells the browser that you wish to perform an animation and requests calling drawScene before the next repaint. It's the most suitable for animations among the three, because:


  • The timings of setInterval and setTimeout are often not honored precisely—they are best-effort based. With requestAnimationFrame, the timing will generally match the display refresh rate.


  • If the scheduled code contains changes in page contents layout, setInterval and setTimeout could cause layout-thrashing (but that's not our case). requestAnimationFrame takes care of that and doesn't trigger unnecessary reflow and repaint cycles.


  • Using requestAnimationFrame allows the browser to decide how often to call our animation/drawing function. This means it can throttle it down if the page/iframe becomes hidden or inactive, which means more battery life for mobile devices. This also happens with setInterval and setTimeout in several browsers (FirefoxChrome)—just pretend you don't know!


Back to our page. Now, our resizing mechanism is complete:



  • drawScene is being called regularly, and it calls onCanvasResize every time.


  • onCanvasResize checks the canvas size, and if changes took place, schedules an adjustDrawingBufferSize call, or postpones it if it was already scheduled.



  • adjustDrawingBufferSize actually changes the drawing buffer size, and sets the new viewport dimensions while at it.


Putting everything together:

I've added an alert that pops up every time the drawing buffer is resized. You may want to open the above sample in a new tab and resize the window or change the device orientation to test it. Notice that it only resizes when you've stopped resizing for 0.6 seconds (as if you'll measure that!).

One last remark before we end this buffer resizing thing. There are limits to how large a drawing buffer can be. These depend on the hardware and browser in use. If you happen to be:


  • using a smartphone, or

  • a ridiculously high-resolution screen, or


  • have multiple monitors/work-spaces/virtual desktops set, or


  • are using a smartphone, or


  • are viewing your page from within a very large iframe (which is the easiest way to test this), or

  • are using a smartphone

there's a chance that the canvas gets resized to more than the possible limits. In such case, the canvas width and height will show no objections, but that actual buffer size will be clamped to the maximum possible. You can get the actual buffer size using the read-only members glContext.drawingBufferWidth and glContext.drawingBufferHeight, which I used to construct the alert. 

Other than that, everything should work fine... except that on some browsers, parts of what you draw (or all of it) may actually never end up on the screen! In this case, adding these two lines to adjustDrawingBufferSize after resizing might be worthwhile:

Now we are back to where stuff makes sense. But note that clamping to drawingBufferWidth and drawingBufferHeight may not be the best action. You might want to consider maintaining a certain aspect ratio.

Now let's do some drawing!

Viewport and Scissoring 

Remember in the first article of this series when I mentioned that inside the shader, WebGL uses the coordinates (-1, -1) to represent the lower left corner of your viewport, and (1, 1) to represent the upper right corner? That's it. viewport tells WebGL which rectangle in our drawing buffer should be mapped to (-1, -1) and (1, 1). It's just a transformation, nothing more. It doesn't affect buffers or anything.

I also said that anything outside the viewport dimensions is skipped and is not drawn altogether. That's almost entirely true, but has a twist to it. The trick lies in the words "drawn" and "outside". What really counts as drawing or as outside?

This line limits our viewport rectangle to the left half of the canvas. I've added it to the drawScene function. We usually don't need to call viewport except when the canvas size changes, and we actually did it there. You can delete the one in the resize function, but I'll just leave it be. In practice, try to minimize your WebGL calls as much as you can. Let's see what this line does:

Oh, clear(glContext.COLOR_BUFFER_BIT) totally ignored our viewport settings! That's what it does, duh! viewport has no effect on clear calls at all. What the viewport dimensions affect is the clipping of primitives. Remember in the first article, I said that we can only draw points, lines and triangles in WebGL. These will be clipped against the viewport dimensions the way you think they are ... except points. 

Points

A point is drawn entirely if its center lies within the viewport dimensions, and will be omitted entirely if its center lies outside them. If a point is fat enough, its center can still be inside the viewport while a part of it extends outside. This extending part should be drawn. That's how it should be, but that's not necessarily the case in practice:

You should see something that resembles this if your browser, device and drivers stick to the standard (in this regard):

The points' size depends on your device's actual resolution, so don't mind the difference in size. Just pay attention to how much of the points appear. In the above sample, I've set the viewport area to the middle section of the canvas (the area with the gradient), but since the points' centers are still inside the viewport, they should be entirely drawn (the green things). If this is the case in your browser, then great! But not all users are that lucky. Some users will see the outside parts trimmed, something like this:

Most of the time, it really makes no difference. If the viewport is going to cover the entire canvas, then we don't care whether the outsides will be trimmed or not. But it would matter if these points were moving smoothly heading outside the canvas, and then they suddenly disappeared because their centers went outside:

(Press Result to restart the animation.)

Again, this behavior is not necessarily what you see. According to history, Nvidia devices won't clip the points when their centers go outside, but will trim the parts that go outside. On my machine (using an AMD device), Chrome, Firefox and Edge behave the same way when run on Windows. However, on the same machine, Chrome and Firefox will clip the points and won't trim them when run on Linux. On my Android phone, Chrome and Firefox will both clip and trim the points!

Scissoring

It seems that drawing points is bothersome. Why even care? Because points needn't be circular. They are axis-aligned rectangular regions. It's the fragment shader that decides how to draw them. They can be textured, in which case they are known as point-sprites. These can be used to make plenty of stuff, like tile-maps and particle effects, in which they are really handy since you only need to pass one vertex per sprite (the center), instead of four in the case of a triangle-strip. Reducing the amount of data transferred from the CPU to the GPU can really pay off in complex scenes. In WebGL 2, we can use geometry instancing (which has its own catches), but we are not there yet.

So, how do we deal with points clipping? To get the outer parts trimmed, we use scissoring:

Scissoring is now enabled, so here's how to set the scissored region:

While primitives' positions are relative to the viewport dimensions, the scissor box dimensions are not. They specify a raw rectangle in the drawing buffer, not minding how much it overlaps the viewport (or not). In the following sample, I've set the viewport and scissor box to the middle section of the canvas:

(Press Result to restart the animation.)

Note that the scissor test is a per-sample operation that discards the fragments that fall outside the test box. It has nothing to do with what's being drawn; it just discards the fragments that go outside. Even clear respects the scissor test! That's why the blue color (the clear color) is bound to the scissor box. All that remains is to prevent the points from disappearing when their centers go outside. To do this, I'll make sure the viewport is larger than the scissor box, with a margin that allows the points to still be drawn until they are completely outside the scissor box:

(Press Result to restart the animation.)

Yay! This should work nicely everywhere. But in the above code, we only used a part of the canvas to do the drawing. What if we wanted to occupy the whole canvas? It really makes no difference. The viewport can be larger than the drawing buffer without problems (just ignore Firefox's ranting about it in the console output):

See:

Be mindful of the viewport size, though. Even if the viewport is but a transformation that costs you no resources, you don't want to rely on per-sample clipping alone. Consider changing the viewport only when needed, and restore it for the rest of the drawing. And remember that the viewport affects the position of the primitives on the screen, so account for this as well.

That's it for now! Next time, let's put the whole size, viewport and clipping things behind us. On to drawing some triangles! Thanks for reading so far, and I hope it was helpful.

References


Original Link:

Share this article:    Share on Facebook
No Article Link

TutsPlus - Code

Tuts+ is a site aimed at web developers and designers offering tutorials and articles on technologies, skills and techniques to improve how you design and build websites.

More About this Source Visit TutsPlus - Code