{"id":632,"date":"2014-12-28T08:55:19","date_gmt":"2014-12-28T08:55:19","guid":{"rendered":"http:\/\/www.nikola-breznjak.com\/blog\/?p=632"},"modified":"2015-08-10T07:08:06","modified_gmt":"2015-08-10T07:08:06","slug":"notes-from-the-book-game-development-with-three-js-by-isaac-sukin","status":"publish","type":"post","link":"https:\/\/nikola-breznjak.com\/blog\/books\/programming\/notes-from-the-book-game-development-with-three-js-by-isaac-sukin\/","title":{"rendered":"Notes from the book Game development with Three.js by Isaac Sukin"},"content":{"rendered":"<p>My notes from the book <a href=\"http:\/\/amzn.to\/1OO6KNg\">Game development with Three.js<\/a> by Isaac Sukin:<\/p>\n<p><a href=\"http:\/\/threejs.org\/\">Three.js<\/a> is usually used with a new technology called <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/WebGL\">WebGL<\/a>, a JavaScript API for rendering graphics without plugins. The API is based on OpenGL, a desktop graphics API (GL stands for graphics library).<\/p>\n<p>Because it uses the client&#8217;s graphics processing unit to accelerate rendering, WebGL is fast! However, many mobile browsers as well as Internet Explorer 10 and below do not support WebGL. Luckily, Three.js supports rendering with the HTML5 Canvas API as well as other technologies such as Scalable Vector Graphics instead.<\/p>\n<pre class=\"lang:default decode:true\">&lt;!DOCTYPE html&gt;\r\n&lt;html&gt;\r\n\t&lt;head&gt;\r\n\t\t&lt;script src=\"http:\/\/cdnjs.cloudflare.com\/ajax\/libs\/three.js\/r57\/three.min.js\"&gt;&lt;\/script&gt;\r\n\t&lt;\/head&gt;\r\n\t&lt;body&gt;\r\n\t\t\r\n\t\t&lt;script&gt;\r\n\t\t\tvar camera, scene, renderer;\r\n\t\t\tvar geometry, material, mesh;\r\n\r\n\t\t\tvar init = function () {\r\n\r\n\t\t\t\trenderer = new THREE.CanvasRenderer();\r\n\t\t\t\trenderer.setSize( window.innerWidth, window.innerHeight );\r\n\t\t\t\tdocument.body.appendChild( renderer.domElement );\r\n\r\n\t\t\t\tcamera = new THREE.PerspectiveCamera( 95, window.innerWidth \/ window.innerHeight, 1, 5000 );\r\n\t\t\t\tcamera.position.z = 900;\r\n\r\n\t\t\t\tscene = new THREE.Scene();\r\n\r\n\t\t\t\tgeometry = new THREE.CubeGeometry( 100, 100, 100 );\r\n\t\t\t\tmaterial = new THREE.MeshNormalMaterial( { color: 0x000000, wireframe: true, wireframeLinewidth: 2 } );\r\n\r\n\t\t\t\tmesh = new THREE.Mesh( geometry, material );\r\n\t\t\t\tscene.add( mesh );\r\n\t\t\t}\r\n\r\n\t\t\tvar animate = function () {\r\n\r\n\t\t\t\trequestAnimationFrame( animate );\r\n\r\n\t\t\t\tmesh.rotation.x = Date.now() * 0.001;\r\n\t\t\t\tmesh.rotation.y = Date.now() * 0.001;\r\n\r\n\t\t\t\trenderer.render( scene, camera );\r\n\r\n\t\t\t}\r\n\r\n\t\t\tinit();\r\n\t\t\tanimate();\r\n\t\t&lt;\/script&gt;\r\n\t&lt;\/body&gt;\r\n&lt;\/html&gt;<\/pre>\n<p>The renderer creates a new<strong> &lt;canvas&gt;<\/strong> element by default that should be\u00a0added to the DOM. Avoid changing the canvas&#8217; size with <strong>CSS<\/strong>; use the renderer&#8217;s\u00a0setSize method instead, which sets the width and height HTML\u00a0attributes on the canvas element.<\/p>\n<p>This is because CSS describes the\u00a0display size but not the render size. That is, if the canvas is rendered at 800 x 600, but the CSS shows it at 1024 x 768, the rendering will be\u00a0stretched to fill the space just like if you specified the CSS size of an\u00a0image to be larger than its true size. This can result in distortion and\u00a0difficulty converting between &#8220;screen space&#8221; and &#8220;canvas space.&#8221;<\/p>\n<p>The one last thing we need is a camera object as shown in the following code\u00a0snippet, which is something Three.js uses to tell the renderer from what perspective\u00a0the scene should be displayed. If the player was standing in your virtual world and\u00a0their screen represented what they could see, camera would be their eyes, renderer\u00a0would be their brain, and scene would be their universe.<\/p>\n<p>All objects are initialized at the position (0, 0, 0), also\u00a0called the origin. The key here is requestAnimationFrame(), which executes the function passed to\u00a0it when the browser is ready to paint a new frame. Geometries are instances of THREE.Geometry that define the shape of an object in\u00a0a scene. They are made up of vertices and faces (which are themselves objects and\u00a0are accessible through the vertices and faces array properties). Vertices are the\u00a0THREE.Vector3 objects representing points in three-dimensional space, while faces\u00a0are the THREE.Face3 objects representing triangular surfaces.<\/p>\n<p>Triangle:<\/p>\n<pre class=\"lang:default decode:true\">var geo = new THREE.Geometry();\r\ngeo.vertices = [\r\n    new THREE.Vector3(0, 0, 0),\r\n    new THREE.Vector3(0, 100, 0),\r\n    new THREE.Vector3(0, 0, 100)\r\n];\r\n\r\ngeo.faces.push(new THREE.Face3(0, 1, 2));\r\ngeo.computeBoundingSphere();<\/pre>\n<p>3D:<\/p>\n<pre class=\"lang:default decode:true\">THREE.Sphere(radius, horizontalSegments = 8, verticalSegments = 6)\r\n\r\nTHREE.Icosahedron(radius, detail = 0);\r\nTHREE.Octahedron(radius, detail = 0);\r\nTHREE.Tetrahedron(radius, detail = 0);\r\n\r\nTHREE.CylinderGeometry(radiusTop, radiusBottom, height, radiusSegments = 8, heightSegments = 1, openEnded= false)\r\n\r\nTHREE.TorusGeometry(radius, tubeWidth = 40, radialSegments = 8, tubularSegments = 6)\r\n\r\nTHREE.TorusKnotGeometry(radius, tubeWidth = 40, radialSegments, tubularSegments, p = 2, q = 3, heightScale = 1)<\/pre>\n<p>2D:<\/p>\n<pre class=\"lang:default decode:true \">Plane THREE.PlaneGeometry(width, height, widthSegments = 1, heightSegments = 1)\r\n\r\nCircle THREE.CircleGeometry(radius, numberOfSides = 8)\r\n\r\nRing THREE.RingGeometry(innerRadius, outerRadius, radialSegments = 8, ringSegments = 8)<\/pre>\n<p>Extruding:<\/p>\n<pre class=\"lang:default decode:true\">var triangle = new THREE.Shape([\r\nnew THREE.Vector2 (0, 50),\r\nnew THREE.Vector2 (50, 50),\r\nnew THREE.Vector2 (50, 0)\r\n]);\r\nvar geometry = new THREE.ExtrudeGeometry(triangle, {\r\nbevelEnabled: false,\r\namount: 30\r\n});<\/pre>\n<p>Custom fonts\u00a0must be in the typeface.js format (you can convert OpenType and TrueType fonts\u00a0to Typeface format at <a href=\"http:\/\/typeface.neocracy.org\/fonts.html\">http:\/\/typeface.neocracy.org\/fonts.html<\/a>). Use the\u00a0following code to create text geometry:<\/p>\n<pre class=\"lang:default decode:true\">new THREE.TextGeometry(\"Text message goes here\", {\r\n    size: 30,\r\n    height: 20, \/\/ extrude thickness\r\n    font: \"helvetiker\", \/\/ font family in lower case\r\n    weight: \"normal\", \/\/ or e.g. bold\r\n    style: \"normal\", \/\/ or e.g. italics\r\n    bevelEnabled: false\r\n});\r\n\r\n<\/pre>\n<p>Procedural city:<\/p>\n<pre class=\"lang:default decode:true\">var camera, scene, renderer;\r\n\r\nfunction setup() {\r\n    document.body.style.backgroundColor = '#d7f0f7';\r\n    setupThreeJS();\r\n    setupWorld();\r\n    requestAnimationFrame(function animate() {\r\n        renderer.render(scene, camera);\r\n        requestAnimationFrame(animate);\r\n    });\r\n}\r\n\r\nfunction setupThreeJS() {\r\n    scene = new THREE.Scene();\r\n    camera = new THREE.PerspectiveCamera(75, window.innerWidth \/ window.innerHeight, 1, 10000);\r\n    camera.position.y = 400;\r\n    camera.position.z = 400;\r\n    camera.rotation.x = -45 * Math.PI \/ 180;\r\n    renderer = new THREE.CanvasRenderer();\r\n    renderer.setSize(window.innerWidth, window.innerHeight);\r\n    document.body.appendChild(renderer.domElement);\r\n}\r\n\r\nfunction setupWorld() {\r\n    \/\/ Floor\r\n    var geo = new THREE.PlaneGeometry(2000, 2000, 20, 20);\r\n    var mat = new THREE.MeshBasicMaterial({color: 0x9db3b5, overdraw:true});\r\n    var floor = new THREE.Mesh(geo, mat);\r\n    floor.rotation.x = -90 * Math.PI \/ 180;\r\n    scene.add(floor);\r\n    \/\/ Original building\r\n    var geometry = new THREE.CubeGeometry(1, 1, 1);\r\n    geometry.applyMatrix(new THREE.Matrix4().makeTranslation(0, 0.5,0));\r\n\r\n    var material = new THREE.MeshDepthMaterial({overdraw: true});\r\n    \/\/ Cloned buildings\r\n    for (var i = 0; i &lt; 300; i++) {\r\n        var building = new THREE.Mesh(geometry.clone(), material.clone());\r\n        building.position.x = Math.floor(Math.random() * 200 - 100) * 4;\r\n        building.position.z = Math.floor(Math.random() * 200 - 100) * 4;\r\n        building.scale.x = Math.random() * 50 + 10;\r\n        building.scale.y = Math.random() * building.scale.x * 8 + 8;\r\n        building.scale.z = building.scale.x;\r\n        scene.add(building);\r\n    }\r\n}\r\n\r\n\/\/ Run it!\r\nsetup();<\/pre>\n<p>Fog:<\/p>\n<pre class=\"lang:default decode:true \">scene.fog = new THREE.FogExp2(0x9db3b5, 0.002);<\/pre>\n<p>Only the DirectionalLight and PointLight objects can cast <strong>shadows<\/strong>. Casting shadows first requires that we enable shadows on the renderer:<\/p>\n<pre class=\"lang:default decode:true \">renderer.shadowMapEnabled = true;<\/pre>\n<p>For our\u00a0city scene, we&#8217;ll enable shadow receiving for our floor and both casting and receiving\u00a0for our buildings:<\/p>\n<pre class=\"lang:default decode:true \">floor.receiveShadow = true;\r\ncity.castShadow = true;\r\ncity.receiveShadow = true;<\/pre>\n<p>Finally, we configure our DirectionalLight object to use shadows:<\/p>\n<pre class=\"lang:default decode:true\">light.castShadow = true;\r\nlight.shadowDarkness = 0.5;\r\nlight.shadowMapWidth = 2048;\r\nlight.shadowMapHeight = 2048;\r\nlight.position.set(500, 1500, 1000);\r\nlight.shadowCameraFar = 2500;\r\n\r\n\/\/ DirectionalLight only; not necessary for PointLight\r\nlight.shadowCameraLeft = -1000;\r\nlight.shadowCameraRight = 1000;\r\nlight.shadowCameraTop = 1000;\r\nlight.shadowCameraBottom = -1000;<\/pre>\n<p>Earlier, we switched from CanvasRenderer to WebGLRenderer in order to support\u00a0shadows and fog. A<strong>s a rule of thumb, WebGLRenderer is faster and has the most features, while CanvasRenderer has fewer features but broader browser support.<\/strong> One particularly nice feature of WebGLRenderer is that it supports antialiasing to\u00a0smooth out jagged edges. We can enable this for our cityscape by passing the option\u00a0in to the renderer constructor:<\/p>\n<pre class=\"lang:default decode:true \">renderer = new THREE.WebGLRenderer({antialias: true});<\/pre>\n<p>Already written navigation from examples:<\/p>\n<pre class=\"lang:default decode:true \">&lt;script src=\"FirstPersonControls.js\"&gt;&lt;\/script&gt;<\/pre>\n<p>Add timer:<\/p>\n<pre class=\"lang:default decode:true \">clock = new THREE.Clock();\r\ncontrols = new THREE.FirstPersonControls(camera);\r\ncontrols.movementSpeed = 100;\r\ncontrols.lookSpeed = 0.1;<\/pre>\n<p>Add this to requestAnimationFrame:<\/p>\n<pre class=\"lang:default decode:true \">controls.update(clock.getDelta());<\/pre>\n<p><strong>Clicking<\/strong> on the screen in order to select or interact with something is a common\u00a0requirement, but it&#8217;s somewhat harder than it sounds because of the need to project\u00a0the location of the click in the 2D plane of your screen into the 3D world of Three.js. To\u00a0do this, we draw an imaginary line, called a ray, from the camera toward the position where the mouse might be in 3D space and see if it intersects with anything.\u00a0In order to project, we first need a projector:<\/p>\n<pre class=\"lang:default decode:true\">projector = new THREE.Projector();<\/pre>\n<p>Then we need to register a listener on the click event for the canvas:<\/p>\n<pre class=\"lang:default decode:true \">renderer.domElement.addEventListener('mousedown', function(event) {\r\nvar vector = new THREE.Vector3(\r\nrenderer.devicePixelRatio * (event.pageX - this.offsetLeft) \/\r\nthis.width * 2 - 1,\r\n-renderer.devicePixelRatio * (event.pageY - this.offsetTop) \/\r\nthis.height * 2 + 1,\r\n0\r\n);\r\nprojector.unprojectVector(vector, camera);\r\nvar raycaster = new THREE.Raycaster(\r\ncamera.position,\r\nvector.sub(camera.position).normalize()\r\n);\r\nvar intersects = raycaster.intersectObjects(OBJECTS);\r\nif (intersects.length) {\r\n\/\/ intersects[0] describes the clicked object\r\n}\r\n}, false);<\/pre>\n<p>The last thing you want when your player is clicking madly to shoot at enemies is for the whole screen to suddenly turn blue because the browser thinks the user is trying to select something. To avoid this, you can either cancel the select event in JavaScript with document.<\/p>\n<pre class=\"lang:default decode:true \">onselectstart = function() { return false; }<\/pre>\n<p>or disable\u00a0it in CSS:<\/p>\n<pre class=\"lang:default decode:true \">* {\r\n-webkit-user-select: none;\r\n-moz-user-select: none;\r\n-ms-user-select: none;\r\nuser-select: none;\r\n}<\/pre>\n<p>Colission libraries:<\/p>\n<ul>\n<li><strong>Ammo.js<\/strong> is a large but complete library compiled to JavaScript from\u00a0C++. It is available at https:\/\/github.com\/kripken\/ammo.js\/<\/li>\n<li><strong>Cannon.js<\/strong> is a smaller library written from scratch in JavaScript\u00a0and inspired in part by concepts from Three.js. It is available at\u00a0https:\/\/github.com\/schteppe\/cannon.js<\/li>\n<li><strong>Physi.js<\/strong> is a bridge between Ammo or Cannon and Three.js that also\u00a0runs the physics simulation in a separate thread to avoid blocking the\u00a0rendering. It is available at https:\/\/github.com\/chandlerprall\/Physijs<\/li>\n<\/ul>\n<p>Well, tbh, stopped at page 61\/100 as the examples from the book didn&#8217;t quite work for me so all in all very disappointed with the book in that department. But, as far as the writing and explanations up till this point go I think it was good. Grade: 2.7\/5 <span style=\"line-height: 1.5;\">\u00a0<\/span><\/p>\n","protected":false},"excerpt":{"rendered":"<p>My notes from the book Game development with Three.js by Isaac Sukin: Three.js is usually used with a new technology called WebGL, a JavaScript API for rendering graphics&hellip;<\/p>\n","protected":false},"author":1,"featured_media":1032,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[34],"tags":[],"class_list":["post-632","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-programming"],"_links":{"self":[{"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/posts\/632","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/comments?post=632"}],"version-history":[{"count":9,"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/posts\/632\/revisions"}],"predecessor-version":[{"id":1471,"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/posts\/632\/revisions\/1471"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/media\/1032"}],"wp:attachment":[{"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/media?parent=632"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/categories?post=632"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/tags?post=632"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}