Canvas VS. Webgl VS. CSS 3D -> Which to Choose

canvas vs. webGL vs. CSS 3d - which to choose?

The reason there are so many different options for 3D is because the whole thing is still in a state of flux -- 3D in the browser isn't a finished standard, and of the options you listed, the only one that works in all currently available browsers is Canvas.

IE in particular is unlikely to give you much joy -- as you say, 3D isn't even slated for IE10 at this point. Having said that, SVG was added to IE9 quite late in the day, so there's always hope. But the reason it's unlikely is that Microsoft have made a point of only supporting features which have been formally ratified as standards.

Of the technologies you listed, Canvas is by far the best supported, but Canvas isn't a 3D technology; it's a 2D canvas, and if you want to have 3D effects in it, you need to write them yourself, and they won't be hardware accelerated.

I guess the real answer to your question depends on how important the feature is for your site. If it's just eye candy, which users of unsupported browsers could live without, then by all means do it with some 3D CSS. But if you need to make it consistent in all current browsers, then do it with Canvas.

I'd tend to recommend not using WebGL for your case, because it sounds like it would be overkill for what you're doing.

3D CSS is probably the right answer, but use Canvas for now, until the rest of the browsers add support for 3D CSS.

HTML5 Canvas vs. SVG vs. div

The short answer:

SVG would be easier for you, since selection and moving it around is already built in. SVG objects are DOM objects, so they have "click" handlers, etc.

DIVs are okay but clunky and have awful performance loading at large numbers.

Canvas has the best performance hands-down, but you have to implement all concepts of managed state (object selection, etc) yourself, or use a library.


The long answer:

HTML5 Canvas is simply a drawing surface for a bit-map. You set up to draw (Say with a color and line thickness), draw that thing, and then the Canvas has no knowledge of that thing: It doesn't know where it is or what it is that you've just drawn, it's just pixels. If you want to draw rectangles and have them move around or be selectable then you have to code all of that from scratch, including the code to remember that you drew them.

SVG on the other hand must maintain references to each object that it renders. Every SVG/VML element you create is a real element in the DOM. By default this allows you to keep much better track of the elements you create and makes dealing with things like mouse events easier by default, but it slows down significantly when there are a large number of objects

Those SVG DOM references mean that some of the footwork of dealing with the things you draw is done for you. And SVG is faster when rendering really large objects, but slower when rendering many objects.

A game would probably be faster in Canvas. A huge map program would probably be faster in SVG. If you do want to use Canvas, I have some tutorials on getting movable objects up and running here.

Canvas would be better for faster things and heavy bitmap manipulation (like animation), but will take more code if you want lots of interactivity.

I've run a bunch of numbers on HTML DIV-made drawing versus Canvas-made drawing. I could make a huge post about the benefits of each, but I will give some of the relevant results of my tests to consider for your specific application:

I made Canvas and HTML DIV test pages, both had movable "nodes." Canvas nodes were objects I created and kept track of in Javascript. HTML nodes were movable Divs.

I added 100,000 nodes to each of my two tests. They performed quite differently:

The HTML test tab took forever to load (timed at slightly under 5 minutes, chrome asked to kill the page the first time). Chrome's task manager says that tab is taking up 168MB. It takes up 12-13% CPU time when I am looking at it, 0% when I am not looking.

The Canvas tab loaded in one second and takes up 30MB. It also takes up 13% of CPU time all of the time, regardless of whether or not one is looking at it. (2013 edit: They've mostly fixed that)

Dragging on the HTML page is smoother, which is expected by the design, since the current setup is to redraw EVERYTHING every 30 milliseconds in the Canvas test. There are plenty of optimizations to be had for Canvas for this. (canvas invalidation being the easiest, also clipping regions, selective redrawing, etc.. just depends on how much you feel like implementing)

There is no doubt you could get Canvas to be faster at object manipulation as the divs in that simple test, and of course far faster in the load time. Drawing/loading is faster in Canvas and has far more room for optimizations, too (ie, excluding things that are off-screen is very easy).

Conclusion:

  • SVG is probably better for applications and apps with few items (less than 1000? Depends really)
  • Canvas is better for thousands of objects and careful manipulation, but a lot more code (or a library) is needed to get it off the ground.
  • HTML Divs are clunky and do not scale, making a circle is only possible with rounded corners, making complex shapes is possible but involves hundreds of tiny tiny pixel-wide divs. Madness ensues.

Canvas 3D drawing using both 2D and 3D context

No, unfortunately not.

The HTML 5 spec says that if you call getContext on a canvas element that is already in a different context mode and the two contexts are not compatible then return null.

Unfortunately "webgl" and "2d" canvases are not compatible and thus you will get null:

var canvas = document.getElementById('my-canvas');
var webgl = canvas.getContext("webgl"); // Get a 3D webgl context, returns a context
var twod = canvas.getContext("2d"); // Get a 2D context, returns null

What is drawing context exactly? What is the role of getcontext() method?

Context is a way to choose what you are going to do with your canvas.

For moment you can use getContext for 2d (2dcanvas) or for 3d (WebGL).

HTML5 Specification say's about getContext :
"Returns an object that exposes an API for drawing on the canvas. The first argument specifies the desired API. Subsequent arguments are handled by that API."

You can find specifications for each API there :
https://html.spec.whatwg.org/multipage/canvas.html#dom-canvas-getcontext

It is also good to know that "webgl" is the correct name for API but for moment, as it is experimental you should use "experimental-webgl" to start creating WebGL content

Quality differences between HTML, Canvas and WebGL

If you want your pictures to be drawn more smooth, then you could use mipmaps! Try to make the pictures to have both dimensions a power at 2 and after you can generate mipmaps, like this:

gl.generateMipmap(gl.TEXTURE_2D);

With mipmaps you can choose what WebGL does by setting the texture filtering for each texture. And if you want to your image to be smooth, then you have to change the texture filtering to be LINEAR_MIPMAP_LINEAR. So after you generate the mipmaps you have to write:

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);

The pictures with the differences are below ( LINEAR vs LINEAR_MIPMAP_LINEAR ):

LINEARLINEAR_MIPMAP_LINEAR

In first picture the tracks are asymmetric, but the second picture is drawn smooth. I think that the second picture looks more like the HTML picture that you provided.


There is a great tutorial about WebGL Textures: https://webglfundamentals.org/webgl/lessons/webgl-3d-textures.html. There is written that this type of texture filtering is the slowest and it takes 33% more memory, so be aware of that!

Multiple WebGL models on the same page

It's not clear why you think you need multiple webgl contexts. I'm guessing because you want a list like this

1. [img] description
description

2. [img] description
description

3. [img] description
description

Or something?

Some ideas

  1. make one canvas big enough for the screen, set its CSS so it doesn't scroll with the rest of the page. Draw the models aligned with whatever other HTML you want that does scroll.

  2. make an offscreen webgl canvas and use canvas2d elements to display.

    For each model render the model and then call

    someCanvas2DContextForElementN.drawImage(webGLcanvasElement, ...);

Given there are probably only ever a few canvases visible you only need to update those ones. In fact it's probably a good idea to recycle them. In other words, rather than make 12000 canvaes or a 12000 element list make just enough to fit on the screen and update them as you scroll.

Personally I'd probably pick #1 if my page design allowed it. Seems to work, see below.


It turned out to be really easy. I just took this sample that was drawing 100 objects and made it draw one object at a time.

After clearing the screen turn on the scissor test

gl.enable(gl.SCISSOR_TEST);

Then, for each object

// get the element that is a place holder for where we want to
// draw the object
var viewElement = obj.viewElement;

// get its position relative to the page's viewport
var rect = viewElement.getBoundingClientRect();

// check if it's offscreen. If so skip it
if (rect.bottom < 0 || rect.top > gl.canvas.clientHeight ||
rect.right < 0 || rect.left > gl.canvas.clientWidth) {
return; // it's off screen
}

// set the viewport
var width = rect.right - rect.left;
var height = rect.bottom - rect.top;
var left = rect.left;
var bottom = gl.canvas.clientHeight - rect.bottom - 1;

gl.viewport(left, bottom, width, height);
gl.scissor(left, bottom, width, height);

I'm not 100% sure if I need to add 1 the width and height or not. I suppose I should look that up.

In any case I compute a new projection matrix for every rendered object just to make the code generic. The placeholder divs could be different sizes.

Update:

the solution originally posted here used position: fixed on the canvas to keep it from scrolling. The new solution uses position: absolute and updates the transform just before rendering like this

  gl.canvas.style.transform = `translateY(${window.scrollY}px)`;

With the previous solution the shapes getting re-drawn in their matching positions could lag behind the scrolling. With the new solution the canvas scrolls until we get time to update it. That means shapes might be missing for a few frames if we can't draw quick enough but it looks much better than the scrolling not matching.

The sample below is the updated solution.

"use strict";// using twgl.js because I'm lazy    twgl.setAttributePrefix("a_");    var m4 = twgl.m4;    var gl = twgl.getWebGLContext(document.getElementById("c"));    // compiles shaders, links program, looks up locations    var programInfo = twgl.createProgramInfo(gl, ["vs", "fs"]);
// calls gl.creatBuffer, gl.bindBuffer, gl.bufferData for each shape // for positions, normals, texcoords var shapes = [ twgl.primitives.createCubeBufferInfo(gl, 2), twgl.primitives.createSphereBufferInfo(gl, 1, 24, 12), twgl.primitives.createPlaneBufferInfo(gl, 2, 2), twgl.primitives.createTruncatedConeBufferInfo(gl, 1, 0, 2, 24, 1), twgl.primitives.createCresentBufferInfo(gl, 1, 1, 0.5, 0.1, 24), twgl.primitives.createCylinderBufferInfo(gl, 1, 2, 24, 2), twgl.primitives.createDiscBufferInfo(gl, 1, 24), twgl.primitives.createTorusBufferInfo(gl, 1, 0.4, 24, 12), ];
function rand(min, max) { return min + Math.random() * (max - min); }
// Shared values var lightWorldPosition = [1, 8, -10]; var lightColor = [1, 1, 1, 1]; var camera = m4.identity(); var view = m4.identity(); var viewProjection = m4.identity();
var tex = twgl.createTexture(gl, { min: gl.NEAREST, mag: gl.NEAREST, src: [ 255, 255, 255, 255, 192, 192, 192, 255, 192, 192, 192, 255, 255, 255, 255, 255, ], }); var randColor = function() { var color = [Math.random(), Math.random(), Math.random(), 1]; color[Math.random() * 3 | 0] = 1; // make at least 1 bright return color; };
var objects = []; var numObjects = 100; var list = document.getElementById("list"); var listItemTemplate = document.getElementById("list-item-template").text; for (var ii = 0; ii < numObjects; ++ii) { var listElement = document.createElement("div"); listElement.innerHTML = listItemTemplate; listElement.className = "list-item"; var viewElement = listElement.querySelector(".view"); var uniforms = { u_lightWorldPos: lightWorldPosition, u_lightColor: lightColor, u_diffuseMult: randColor(), u_specular: [1, 1, 1, 1], u_shininess: 50, u_specularFactor: 1, u_diffuse: tex, u_viewInverse: camera, u_world: m4.identity(), u_worldInverseTranspose: m4.identity(), u_worldViewProjection: m4.identity(), }; objects.push({ ySpeed: rand(0.1, 0.3), zSpeed: rand(0.1, 0.3), uniforms: uniforms, viewElement: viewElement, programInfo: programInfo, bufferInfo: shapes[ii % shapes.length], }); list.appendChild(listElement); }
var showRenderingArea = false;
function render(time) { time *= 0.001; twgl.resizeCanvasToDisplaySize(gl.canvas); gl.canvas.style.transform = `translateY(${window.scrollY}px)`;
gl.enable(gl.DEPTH_TEST); gl.disable(gl.SCISSOR_TEST); gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.enable(gl.SCISSOR_TEST);
if (showRenderingArea) { gl.clearColor(0, 0, 1, 1); }
var eye = [0, 0, -8]; var target = [0, 0, 0]; var up = [0, 1, 0];
m4.lookAt(eye, target, up, camera); m4.inverse(camera, view);
objects.forEach(function(obj, ndx) { var viewElement = obj.viewElement; // get viewElement's position var rect = viewElement.getBoundingClientRect(); if (rect.bottom < 0 || rect.top > gl.canvas.clientHeight || rect.right < 0 || rect.left > gl.canvas.clientWidth) { return; // it's off screen }
var width = rect.right - rect.left; var height = rect.bottom - rect.top; var left = rect.left; var bottom = gl.canvas.clientHeight - rect.bottom - 1;
gl.viewport(left, bottom, width, height); gl.scissor(left, bottom, width, height);
if (showRenderingArea) { gl.clear(gl.COLOR_BUFFER_BIT); }
var projection = m4.perspective(30 * Math.PI / 180, width / height, 0.5, 100); m4.multiply(projection, view, viewProjection);
var uni = obj.uniforms; var world = uni.u_world; m4.identity(world); m4.rotateY(world, time * obj.ySpeed, world); m4.rotateZ(world, time * obj.zSpeed, world); m4.transpose(m4.inverse(world, uni.u_worldInverseTranspose), uni.u_worldInverseTranspose); m4.multiply(viewProjection, uni.u_world, uni.u_worldViewProjection);
gl.useProgram(obj.programInfo.program); // calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer twgl.setBuffersAndAttributes(gl, obj.programInfo, obj.bufferInfo); // calls gl.bindTexture, gl.activeTexture, gl.uniformXXX twgl.setUniforms(obj.programInfo, uni); // calls gl.drawArrays or gl.drawElements twgl.drawBufferInfo(gl, obj.bufferInfo); }); }
if (true) { // animated var renderContinuously = function(time) { render(time); requestAnimationFrame(renderContinuously); } requestAnimationFrame(renderContinuously); } else { var requestId; var renderRequest = function(time) { render(time); requestId = undefined; } // If animated var queueRender = function() { if (!requestId) { requestId = requestAnimationFrame(renderRequest); } }
window.addEventListener('resize', queueRender); window.addEventListener('scroll', queueRender);
queueRender(); }
* {          box-sizing: border-box;          -moz-box-sizing: border-box;      }      body {        font-family: monospace;        margin: 0;      }      #c {          position: absolute;          top: 0;          width: 100vw;          height: 100vh;      }      #outer {          width: 100%;          z-index: 2;          position: absolute;          top: 0px;      }      #content {          margin: auto;          padding: 2em;      }      #b {        width: 100%;        text-align: center;      }      .list-item {          border: 1px solid black;          margin: 2em;          padding: 1em;          width: 200px;          display: inline-block;      }      .list-item .view {          width: 100px;          height: 100px;          float: left;          margin: 0 1em 1em 0;      }      .list-item .description {          padding-left: 2em;      }
@media only screen and (max-width : 500px) { #content { width: 100%; } .list-item { margin: 0.5em; } .list-item .description { padding-left: 0em; } }
<script src="//twgljs.org/dist/4.x/twgl-full.min.js"></script>  <body>    <canvas id="c"></canvas>    <div id="outer">      <div id="content">        <div id="b">item list</div>        <div id="list"></div>      </div>    </div>  </body>  <script id="list-item-template" type="notjs">    <div class="view"></div>    <div class="description">Lorem ipsum dolor sit amet, conse ctetur adipi scing elit. </div>  </script>  <script id="vs" type="notjs">uniform mat4 u_worldViewProjection;uniform vec3 u_lightWorldPos;uniform mat4 u_world;uniform mat4 u_viewInverse;uniform mat4 u_worldInverseTranspose;
attribute vec4 a_position;attribute vec3 a_normal;attribute vec2 a_texcoord;
varying vec4 v_position;varying vec2 v_texCoord;varying vec3 v_normal;varying vec3 v_surfaceToLight;varying vec3 v_surfaceToView;
void main() { v_texCoord = a_texcoord; v_position = (u_worldViewProjection * a_position); v_normal = (u_worldInverseTranspose * vec4(a_normal, 0)).xyz; v_surfaceToLight = u_lightWorldPos - (u_world * a_position).xyz; v_surfaceToView = (u_viewInverse[3] - (u_world * a_position)).xyz; gl_Position = v_position;} </script> <script id="fs" type="notjs">precision mediump float;
varying vec4 v_position;varying vec2 v_texCoord;varying vec3 v_normal;varying vec3 v_surfaceToLight;varying vec3 v_surfaceToView;
uniform vec4 u_lightColor;uniform vec4 u_diffuseMult;uniform sampler2D u_diffuse;uniform vec4 u_specular;uniform float u_shininess;uniform float u_specularFactor;
vec4 lit(float l ,float h, float m) { return vec4(1.0, abs(l),//max(l, 0.0), (l > 0.0) ? pow(max(0.0, h), m) : 0.0, 1.0);}
void main() { vec4 diffuseColor = texture2D(u_diffuse, v_texCoord) * u_diffuseMult; vec3 a_normal = normalize(v_normal); vec3 surfaceToLight = normalize(v_surfaceToLight); vec3 surfaceToView = normalize(v_surfaceToView); vec3 halfVector = normalize(surfaceToLight + surfaceToView); vec4 litR = lit(dot(a_normal, surfaceToLight), dot(a_normal, halfVector), u_shininess); vec4 outColor = vec4(( u_lightColor * (diffuseColor * litR.y + u_specular * litR.z * u_specularFactor)).rgb, diffuseColor.a); gl_FragColor = outColor;} </script>


Related Topics



Leave a reply



Submit