Working through yesterday’s post I found it necessary to test the code in multiple browsers and, when it failed, to use the debuggers to figure out where. The different examples I’ve put together for this site and for last week’s presentation all run and ran fine on Firefox, which is where I do most of my work. Everything worked in IE and Edge as well. Nothing worked in Chrome or Opera, even though everything worked in both last week.
That’s right — now you see it, now you don’t.
Exactly why things do or do not work is dependent on two major things: details of the latest browser version (particularly in how it leverages any GPU in the system for hardware acceleration) and the version of the WebGL and Three.js libraries themselves. As of today the failures always occur when trying to create the renderer object.
1 2 3 4 5 6 7 8 9 10 |
aspectRatio = window.innerWidth/window.innerHeight; camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.1, 1000); renderer = new THREE.WebGLRenderer(); //fails here renderer.setSize(window.innerWidth, window.innerHeight); canvas = renderer.domElement; document.body.appendChild(canvas); //can also be appended to a DIV scene = new THREE.Scene(); |
Within the Three.js code the code for creating the renderer object is here, and the variable _gl
ends up with a value of null and throws an exception.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
// initialize //this is line 19672 in Three.js unminified var _gl; try { var attributes = { alpha: _alpha, depth: _depth, stencil: _stencil, antialias: _antialias, premultipliedAlpha: _premultipliedAlpha, preserveDrawingBuffer: _preserveDrawingBuffer }; _gl = _context || _canvas.getContext( 'webgl', attributes ) || _canvas.getContext( 'experimental-webgl', attributes ); if ( _gl === null ) { if ( _canvas.getContext( 'webgl' ) !== null ) { throw 'Error creating WebGL context with your selected attributes.'; } else { throw 'Error creating WebGL context.'; } } // Some experimental-webgl implementations do not have getShaderPrecisionFormat if ( _gl.getShaderPrecisionFormat === undefined ) { _gl.getShaderPrecisionFormat = function () { return { 'rangeMin': 1, 'rangeMax': 1, 'precision': 1 }; }; } _canvas.addEventListener( 'webglcontextlost', onContextLost, false ); } catch ( error ) { console.error( 'THREE.WebGLRenderer: ' + error ); } |
This happens regardless of which (current) version of Three.js I use, with some possibilities shown below:
1 2 3 |
<!--script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r82/three.min.js"></script--> <!--script src="js/three.min.js"></script--> <script src="js/three.js"></script> |
What’s really mystifying is that this worked in Chrome sometimes and not other times. It never works in Opera, but did last week when I was giving the presentation.
There are a couple of ways to test to see whether your browser supports WebGL. On is to go to this site:
It tries to initialize a simple animation of a rotating cube. If you don’t see the cube then WebGL isn’t working on your browser on your system. The page gives you a link you can use to get more information about your browser but it only leads to a dead end when you do this in Opera.
There’s also a page that tells you how to include an upfront test in your own code that allows you to provide a message that WebGL has failed. I actually used the Detector.js version I found here, but any of the tests should work.
If you run into problems one of the other important things you can do is check the status of your display drivers. When I examined my graphics drivers on Windows 10 64-bit I found an error and went ahead an deleted and re-installed the driver. Surprise of surprises, WebGL and Three.js now work in all browsers on Windows. I know it has also worked reliably on iOS.
Interestingly, however, and I remember this from testing on my 3rd gen iPad a while back, the default background color is actually white in 3D scenes, unless you explicitly set it to something else, like in this example:
1 |
renderer.setClearColor("#000000",1); |
Obviously this has to happen after the 3D renderer is successfully initialized.
Here’s the test code I’ve been using, which is a slight rearrangement of one of the examples I included in my presentation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
<html> <head> <title>Handle Resize</title> <style> body { margin: 0; } canvas { width: 100%; height: 100% } </style> </head> <body onload="init()"> <!--script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r82/three.min.js"></script--> <!--script src="js/three.min.js"></script--> <script src="js/three.js"></script> <script src="js/Detector.js"></script> <script> var success = true; if (!Detector.webgl) { var warning = Detector.getWebGLErrorMessage(); document.body.appendChild(warning); success = false; } //basic initialization var aspectRatio; var camera; var renderer; var canvas; var scene; function init() { if (success) { init3D(); grid(); addCubes(); render(); } } //set up 3D function init3D() { aspectRatio = window.innerWidth/window.innerHeight; camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.1, 1000); renderer = new THREE.WebGLRenderer({antialias: true, alpha: true}); renderer.setSize(window.innerWidth, window.innerHeight); canvas = renderer.domElement; document.body.appendChild(canvas); //can also be appended to a DIV scene = new THREE.Scene(); renderer.setClearColor("#000000",1); } var global3DBaseX = 0; var global3DBaseY = 0; var global3DBaseZ = 0; var global3DLookTarget = new THREE.Vector3(global3DBaseX, global3DBaseY, global3DBaseZ); //define a line in 3D function define3DLine(x1,y1,z1,x2,y2,z2,lineColor) { var material = new THREE.LineBasicMaterial({ color: lineColor }); var geometry = new THREE.Geometry(); geometry.vertices.push( new THREE.Vector3(x1, y1, z1), new THREE.Vector3(x2, y2, z2) ); var line = new THREE.Line(geometry, material); scene.add(line); return line; } function grid() { //build a grid var xMin = -3; var xMax = 3; var xInc = 0.5; var zMin = -3; var zMax = 3; var zInc = 0.5; var gridLineColor; for (var z=zMin; z<=zMax; z+=zInc) { if (z != 0) { gridLineColor = "#0000FF"; } else { gridLineColor = "#FF0000"; } define3DLine(xMin,0,z,xMax,0,z,gridLineColor); } for (var x=xMin; x<=xMax; x+=xInc) { if (x != 0) { gridLineColor = "#0000FF"; } else { gridLineColor = "#FF0000"; } define3DLine(x,0,zMin,x,0,zMax,gridLineColor); } } var cube1; var cube2; var cube3; function addCubes() { //add a green cube at 0, 0, 0 var geometry = new THREE.BoxGeometry(1, 1, 1); var material1 = new THREE.MeshBasicMaterial({ wireframe: true, color: 0x00ff00 }); cube1 = new THREE.Mesh( geometry, material1 ); scene.add(cube1); //add a yellow cube at 2, 0, -2 var material2 = new THREE.MeshBasicMaterial({ wireframe: true, color: 0xffff00 }); cube2 = new THREE.Mesh( geometry, material2 ); cube2.position.set(2, 0, -2); scene.add(cube2); //add a cyan cube at -2, 2, 2 var material3 = new THREE.MeshBasicMaterial({ wireframe: true, color: 0x00ffff }); cube3 = new THREE.Mesh( geometry, material3 ); cube3.position.set(-2, 2, 2); scene.add(cube3); } //initialization for actions var twoPi = Math.PI * 2.0; var halfPi = Math.PI * 0.5; var cRadius = 5; //5; var cHeight = 4; var cInc = 0.005; var cAngle = (1.5 * Math.PI) - cInc; //do this for every update function cylRotations() { cAngle += cInc; if (cAngle > twoPi) { cAngle -= twoPi; } else if (cAngle < 0.0) { cAngle += twoPi; } var x = cRadius * Math.cos(cAngle); var z = -cRadius * Math.sin(cAngle); var y = cHeight; //update location of viewport camera camera.position.set(x,y,z); camera.lookAt(global3DLookTarget); } var cYIncrement = 0.1; var cYMax = 10; var cYMin = -2; var cRIncrement = 0.1; var cRMax = 20; var cRMin = 2; function moveUp() { cHeight += cYIncrement; if (cHeight > cYMax) { cHeight = cYMax; } } function moveDown() { cHeight -= cYIncrement; if (cHeight < cYMin) { cHeight = cYMin; } } function moveIn() { cRadius -= cRIncrement; if (cRadius < cRMin) { cRadius = cRMin; } } function moveOut() { cRadius += cRIncrement; if (cRadius > cRMax) { cRadius = cRMax; } } function restoreDefaults() { cHeight = 4; cRadius = 5; } document.onkeydown = checkKey; function checkKey(e) { e = e || window.event; //up 38 down 40 if (e.keyCode == '37') { //left arrow } else if (e.keyCode == '39') { //right arrow } else if (e.keyCode == '38') { //up arrow moveUp(); } else if (e.keyCode == '40') { //down arrow moveDown(); } else if (e.keyCode == '66') { //b key moveOut(); } else if (e.keyCode == '70') { //f key moveIn(); } else if (e.keyCode == '36') { //home key restoreDefaults(); } } //from: http://stackoverflow.com/questions/641857/javascript-window-resize-event /* function: addEvent @param: obj (Object)(Required) - The object which you wish to attach your event to. @param: type (String)(Required) - The type of event you wish to establish. @param: callback (Function)(Required) - The method you wish to be called by your event listener. @param: eventReturn (Boolean)(Optional) - Whether you want the event object returned to your callback method. */ var addEvent = function(obj, type, callback, eventReturn) { if(obj == null || typeof obj === 'undefined') return; if(obj.addEventListener) obj.addEventListener(type, callback, eventReturn ? true : false); else if(obj.attachEvent) obj.attachEvent("on" + type, callback); else obj["on" + type] = callback; }; //An example call to the new addEvent function: var watch = function(evt) { //Older browser versions may return evt.srcElement //Newer browser versions should return evt.currentTarget var dimensions = { height: (evt.srcElement || evt.currentTarget).innerHeight, width: (evt.srcElement || evt.currentTarget).innerWidth }; //https://threejs.org/docs/api/cameras/PerspectiveCamera.html renderer.setSize(dimensions.width, dimensions.height); camera.aspect = dimensions.width / dimensions.height; //var newFov = calcFovAngle(dimensions.height); //camera.fov = newFov; camera.updateProjectionMatrix(); }; addEvent(window, 'resize', watch, true); //animation loop function render() { //each cube rotates around a different axis cube1.rotation.x += 0.01; cube2.rotation.y += 0.01; cube3.rotation.z += 0.01; cylRotations(); renderer.render(scene, camera); requestAnimationFrame(render); } </script> </body> </html> |
A direct link to this is here. Don’t forget to grab a copy of Detector.js if you want to test this on your own system.
Bottom line: don’t be afraid to dig into any of this stuff. This technology is proven and should be fairly stable. If it doesn’t work it’s very likely that something in your system is out of whack.