├── .gitignore ├── README.md ├── index.html ├── js ├── Detector.js ├── OrbitControls.js ├── THREEx.FullScreen.js ├── THREEx.KeyboardState.js ├── THREEx.WindowResize.js ├── index.js └── three.min.js ├── sound └── explode.mp3 └── test ├── cube_collision.html └── move_cube.html /.gitignore: -------------------------------------------------------------------------------- 1 | ### Example user template template 2 | ### Example user template 3 | 4 | # IntelliJ project files 5 | .idea 6 | *.iml 7 | out 8 | gen 9 | # Created by .ignore support plugin (hsz.mobi) 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # race-game-threejs 2 | A simple race game using three.js 3 | 4 | [click here to play](http://noiron.github.io/race-game-threejs/) 5 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Race car game 6 | 7 | 8 | 9 | 10 | 11 | 12 | 39 | 40 | 41 |
42 |
43 |
44 | 45 | 46 |
47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /js/Detector.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author alteredq / http://alteredqualia.com/ 3 | * @author mr.doob / http://mrdoob.com/ 4 | */ 5 | 6 | Detector = { 7 | 8 | canvas: !! window.CanvasRenderingContext2D, 9 | webgl: ( function () { try { return !! window.WebGLRenderingContext && !! document.createElement( 'canvas' ).getContext( 'experimental-webgl' ); } catch( e ) { return false; } } )(), 10 | workers: !! window.Worker, 11 | fileapi: window.File && window.FileReader && window.FileList && window.Blob, 12 | 13 | getWebGLErrorMessage: function () { 14 | 15 | var element = document.createElement( 'div' ); 16 | element.id = 'webgl-error-message'; 17 | element.style.fontFamily = 'monospace'; 18 | element.style.fontSize = '13px'; 19 | element.style.fontWeight = 'normal'; 20 | element.style.textAlign = 'center'; 21 | element.style.background = '#fff'; 22 | element.style.color = '#000'; 23 | element.style.padding = '1.5em'; 24 | element.style.width = '400px'; 25 | element.style.margin = '5em auto 0'; 26 | 27 | if ( ! this.webgl ) { 28 | 29 | element.innerHTML = window.WebGLRenderingContext ? [ 30 | 'Your graphics card does not seem to support WebGL.
', 31 | 'Find out how to get it here.' 32 | ].join( '\n' ) : [ 33 | 'Your browser does not seem to support WebGL.
', 34 | 'Find out how to get it here.' 35 | ].join( '\n' ); 36 | 37 | } 38 | 39 | return element; 40 | 41 | }, 42 | 43 | addGetWebGLMessage: function ( parameters ) { 44 | 45 | var parent, id, element; 46 | 47 | parameters = parameters || {}; 48 | 49 | parent = parameters.parent !== undefined ? parameters.parent : document.body; 50 | id = parameters.id !== undefined ? parameters.id : 'oldie'; 51 | 52 | element = Detector.getWebGLErrorMessage(); 53 | element.id = id; 54 | 55 | parent.appendChild( element ); 56 | 57 | } 58 | 59 | }; 60 | -------------------------------------------------------------------------------- /js/OrbitControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author qiao / https://github.com/qiao 3 | * @author mrdoob / http://mrdoob.com 4 | * @author alteredq / http://alteredqualia.com/ 5 | * @author WestLangley / http://github.com/WestLangley 6 | */ 7 | 8 | THREE.OrbitControls = function ( object, domElement ) { 9 | 10 | this.object = object; 11 | this.domElement = ( domElement !== undefined ) ? domElement : document; 12 | 13 | // API 14 | 15 | this.enabled = true; 16 | 17 | this.center = new THREE.Vector3(); 18 | 19 | this.userZoom = true; 20 | this.userZoomSpeed = 1.0; 21 | 22 | this.userRotate = true; 23 | this.userRotateSpeed = 1.0; 24 | 25 | this.userPan = true; 26 | this.userPanSpeed = 2.0; 27 | 28 | this.autoRotate = false; 29 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 30 | 31 | this.minPolarAngle = 0; // radians 32 | this.maxPolarAngle = Math.PI; // radians 33 | 34 | this.minDistance = 0; 35 | this.maxDistance = Infinity; 36 | 37 | // 65 /*A*/, 83 /*S*/, 68 /*D*/ 38 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40, ROTATE: 65, ZOOM: 83, PAN: 68 }; 39 | 40 | // internals 41 | 42 | var scope = this; 43 | 44 | var EPS = 0.000001; 45 | var PIXELS_PER_ROUND = 1800; 46 | 47 | var rotateStart = new THREE.Vector2(); 48 | var rotateEnd = new THREE.Vector2(); 49 | var rotateDelta = new THREE.Vector2(); 50 | 51 | var zoomStart = new THREE.Vector2(); 52 | var zoomEnd = new THREE.Vector2(); 53 | var zoomDelta = new THREE.Vector2(); 54 | 55 | var phiDelta = 0; 56 | var thetaDelta = 0; 57 | var scale = 1; 58 | 59 | var lastPosition = new THREE.Vector3(); 60 | 61 | var STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2 }; 62 | var state = STATE.NONE; 63 | 64 | // events 65 | 66 | var changeEvent = { type: 'change' }; 67 | 68 | 69 | this.rotateLeft = function ( angle ) { 70 | 71 | if ( angle === undefined ) { 72 | 73 | angle = getAutoRotationAngle(); 74 | 75 | } 76 | 77 | thetaDelta -= angle; 78 | 79 | }; 80 | 81 | this.rotateRight = function ( angle ) { 82 | 83 | if ( angle === undefined ) { 84 | 85 | angle = getAutoRotationAngle(); 86 | 87 | } 88 | 89 | thetaDelta += angle; 90 | 91 | }; 92 | 93 | this.rotateUp = function ( angle ) { 94 | 95 | if ( angle === undefined ) { 96 | 97 | angle = getAutoRotationAngle(); 98 | 99 | } 100 | 101 | phiDelta -= angle; 102 | 103 | }; 104 | 105 | this.rotateDown = function ( angle ) { 106 | 107 | if ( angle === undefined ) { 108 | 109 | angle = getAutoRotationAngle(); 110 | 111 | } 112 | 113 | phiDelta += angle; 114 | 115 | }; 116 | 117 | this.zoomIn = function ( zoomScale ) { 118 | 119 | if ( zoomScale === undefined ) { 120 | 121 | zoomScale = getZoomScale(); 122 | 123 | } 124 | 125 | scale /= zoomScale; 126 | 127 | }; 128 | 129 | this.zoomOut = function ( zoomScale ) { 130 | 131 | if ( zoomScale === undefined ) { 132 | 133 | zoomScale = getZoomScale(); 134 | 135 | } 136 | 137 | scale *= zoomScale; 138 | 139 | }; 140 | 141 | this.pan = function ( distance ) { 142 | 143 | distance.transformDirection( this.object.matrix ); 144 | distance.multiplyScalar( scope.userPanSpeed ); 145 | 146 | this.object.position.add( distance ); 147 | this.center.add( distance ); 148 | 149 | }; 150 | 151 | this.update = function () { 152 | 153 | var position = this.object.position; 154 | var offset = position.clone().sub( this.center ); 155 | 156 | // angle from z-axis around y-axis 157 | 158 | var theta = Math.atan2( offset.x, offset.z ); 159 | 160 | // angle from y-axis 161 | 162 | var phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y ); 163 | 164 | if ( this.autoRotate ) { 165 | 166 | this.rotateLeft( getAutoRotationAngle() ); 167 | 168 | } 169 | 170 | theta += thetaDelta; 171 | phi += phiDelta; 172 | 173 | // restrict phi to be between desired limits 174 | phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, phi ) ); 175 | 176 | // restrict phi to be betwee EPS and PI-EPS 177 | phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) ); 178 | 179 | var radius = offset.length() * scale; 180 | 181 | // restrict radius to be between desired limits 182 | radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) ); 183 | 184 | offset.x = radius * Math.sin( phi ) * Math.sin( theta ); 185 | offset.y = radius * Math.cos( phi ); 186 | offset.z = radius * Math.sin( phi ) * Math.cos( theta ); 187 | 188 | position.copy( this.center ).add( offset ); 189 | 190 | this.object.lookAt( this.center ); 191 | 192 | thetaDelta = 0; 193 | phiDelta = 0; 194 | scale = 1; 195 | 196 | if ( lastPosition.distanceTo( this.object.position ) > 0 ) { 197 | 198 | this.dispatchEvent( changeEvent ); 199 | 200 | lastPosition.copy( this.object.position ); 201 | 202 | } 203 | 204 | }; 205 | 206 | 207 | function getAutoRotationAngle() { 208 | 209 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 210 | 211 | } 212 | 213 | function getZoomScale() { 214 | 215 | return Math.pow( 0.95, scope.userZoomSpeed ); 216 | 217 | } 218 | 219 | function onMouseDown( event ) { 220 | 221 | if ( scope.enabled === false ) return; 222 | if ( scope.userRotate === false ) return; 223 | 224 | event.preventDefault(); 225 | 226 | if ( state === STATE.NONE ) 227 | { 228 | if ( event.button === 0 ) 229 | state = STATE.ROTATE; 230 | if ( event.button === 1 ) 231 | state = STATE.ZOOM; 232 | if ( event.button === 2 ) 233 | state = STATE.PAN; 234 | } 235 | 236 | 237 | if ( state === STATE.ROTATE ) { 238 | 239 | //state = STATE.ROTATE; 240 | 241 | rotateStart.set( event.clientX, event.clientY ); 242 | 243 | } else if ( state === STATE.ZOOM ) { 244 | 245 | //state = STATE.ZOOM; 246 | 247 | zoomStart.set( event.clientX, event.clientY ); 248 | 249 | } else if ( state === STATE.PAN ) { 250 | 251 | //state = STATE.PAN; 252 | 253 | } 254 | 255 | document.addEventListener( 'mousemove', onMouseMove, false ); 256 | document.addEventListener( 'mouseup', onMouseUp, false ); 257 | 258 | } 259 | 260 | function onMouseMove( event ) { 261 | 262 | if ( scope.enabled === false ) return; 263 | 264 | event.preventDefault(); 265 | 266 | 267 | 268 | if ( state === STATE.ROTATE ) { 269 | 270 | rotateEnd.set( event.clientX, event.clientY ); 271 | rotateDelta.subVectors( rotateEnd, rotateStart ); 272 | 273 | scope.rotateLeft( 2 * Math.PI * rotateDelta.x / PIXELS_PER_ROUND * scope.userRotateSpeed ); 274 | scope.rotateUp( 2 * Math.PI * rotateDelta.y / PIXELS_PER_ROUND * scope.userRotateSpeed ); 275 | 276 | rotateStart.copy( rotateEnd ); 277 | 278 | } else if ( state === STATE.ZOOM ) { 279 | 280 | zoomEnd.set( event.clientX, event.clientY ); 281 | zoomDelta.subVectors( zoomEnd, zoomStart ); 282 | 283 | if ( zoomDelta.y > 0 ) { 284 | 285 | scope.zoomIn(); 286 | 287 | } else { 288 | 289 | scope.zoomOut(); 290 | 291 | } 292 | 293 | zoomStart.copy( zoomEnd ); 294 | 295 | } else if ( state === STATE.PAN ) { 296 | 297 | var movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0; 298 | var movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0; 299 | 300 | scope.pan( new THREE.Vector3( - movementX, movementY, 0 ) ); 301 | 302 | } 303 | 304 | } 305 | 306 | function onMouseUp( event ) { 307 | 308 | if ( scope.enabled === false ) return; 309 | if ( scope.userRotate === false ) return; 310 | 311 | document.removeEventListener( 'mousemove', onMouseMove, false ); 312 | document.removeEventListener( 'mouseup', onMouseUp, false ); 313 | 314 | state = STATE.NONE; 315 | 316 | } 317 | 318 | function onMouseWheel( event ) { 319 | 320 | if ( scope.enabled === false ) return; 321 | if ( scope.userZoom === false ) return; 322 | 323 | var delta = 0; 324 | 325 | if ( event.wheelDelta ) { // WebKit / Opera / Explorer 9 326 | 327 | delta = event.wheelDelta; 328 | 329 | } else if ( event.detail ) { // Firefox 330 | 331 | delta = - event.detail; 332 | 333 | } 334 | 335 | if ( delta > 0 ) { 336 | 337 | scope.zoomOut(); 338 | 339 | } else { 340 | 341 | scope.zoomIn(); 342 | 343 | } 344 | 345 | } 346 | 347 | function onKeyDown( event ) { 348 | 349 | if ( scope.enabled === false ) return; 350 | if ( scope.userPan === false ) return; 351 | 352 | switch ( event.keyCode ) { 353 | 354 | /*case scope.keys.UP: 355 | scope.pan( new THREE.Vector3( 0, 1, 0 ) ); 356 | break; 357 | case scope.keys.BOTTOM: 358 | scope.pan( new THREE.Vector3( 0, - 1, 0 ) ); 359 | break; 360 | case scope.keys.LEFT: 361 | scope.pan( new THREE.Vector3( - 1, 0, 0 ) ); 362 | break; 363 | case scope.keys.RIGHT: 364 | scope.pan( new THREE.Vector3( 1, 0, 0 ) ); 365 | break; 366 | */ 367 | case scope.keys.ROTATE: 368 | state = STATE.ROTATE; 369 | break; 370 | case scope.keys.ZOOM: 371 | state = STATE.ZOOM; 372 | break; 373 | case scope.keys.PAN: 374 | state = STATE.PAN; 375 | break; 376 | 377 | } 378 | 379 | } 380 | 381 | function onKeyUp( event ) { 382 | 383 | switch ( event.keyCode ) { 384 | 385 | case scope.keys.ROTATE: 386 | case scope.keys.ZOOM: 387 | case scope.keys.PAN: 388 | state = STATE.NONE; 389 | break; 390 | } 391 | 392 | } 393 | 394 | this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false ); 395 | this.domElement.addEventListener( 'mousedown', onMouseDown, false ); 396 | this.domElement.addEventListener( 'mousewheel', onMouseWheel, false ); 397 | this.domElement.addEventListener( 'DOMMouseScroll', onMouseWheel, false ); // firefox 398 | window.addEventListener( 'keydown', onKeyDown, false ); 399 | window.addEventListener( 'keyup', onKeyUp, false ); 400 | 401 | }; 402 | 403 | THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 404 | -------------------------------------------------------------------------------- /js/THREEx.FullScreen.js: -------------------------------------------------------------------------------- 1 | // This THREEx helper makes it easy to handle the fullscreen API 2 | // * it hides the prefix for each browser 3 | // * it hides the little discrepencies of the various vendor API 4 | // * at the time of this writing (nov 2011) it is available in 5 | // [firefox nightly](http://blog.pearce.org.nz/2011/11/firefoxs-html-full-screen-api-enabled.html), 6 | // [webkit nightly](http://peter.sh/2011/01/javascript-full-screen-api-navigation-timing-and-repeating-css-gradients/) and 7 | // [chrome stable](http://updates.html5rocks.com/2011/10/Let-Your-Content-Do-the-Talking-Fullscreen-API). 8 | 9 | // # Code 10 | 11 | /** @namespace */ 12 | var THREEx = THREEx || {}; 13 | THREEx.FullScreen = THREEx.FullScreen || {}; 14 | 15 | /** 16 | * test if it is possible to have fullscreen 17 | * 18 | * @returns {Boolean} true if fullscreen API is available, false otherwise 19 | */ 20 | THREEx.FullScreen.available = function() 21 | { 22 | return this._hasWebkitFullScreen || this._hasMozFullScreen; 23 | } 24 | 25 | /** 26 | * test if fullscreen is currently activated 27 | * 28 | * @returns {Boolean} true if fullscreen is currently activated, false otherwise 29 | */ 30 | THREEx.FullScreen.activated = function() 31 | { 32 | if( this._hasWebkitFullScreen ){ 33 | return document.webkitIsFullScreen; 34 | }else if( this._hasMozFullScreen ){ 35 | return document.mozFullScreen; 36 | }else{ 37 | console.assert(false); 38 | } 39 | } 40 | 41 | /** 42 | * Request fullscreen on a given element 43 | * @param {DomElement} element to make fullscreen. optional. default to document.body 44 | */ 45 | THREEx.FullScreen.request = function(element) 46 | { 47 | element = element || document.body; 48 | if( this._hasWebkitFullScreen ){ 49 | element.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT); 50 | }else if( this._hasMozFullScreen ){ 51 | element.mozRequestFullScreen(); 52 | }else{ 53 | console.assert(false); 54 | } 55 | } 56 | 57 | /** 58 | * Cancel fullscreen 59 | */ 60 | THREEx.FullScreen.cancel = function() 61 | { 62 | if( this._hasWebkitFullScreen ){ 63 | document.webkitCancelFullScreen(); 64 | }else if( this._hasMozFullScreen ){ 65 | document.mozCancelFullScreen(); 66 | }else{ 67 | console.assert(false); 68 | } 69 | } 70 | 71 | // internal functions to know which fullscreen API implementation is available 72 | THREEx.FullScreen._hasWebkitFullScreen = 'webkitCancelFullScreen' in document ? true : false; 73 | THREEx.FullScreen._hasMozFullScreen = 'mozCancelFullScreen' in document ? true : false; 74 | 75 | /** 76 | * Bind a key to renderer screenshot 77 | * usage: THREEx.FullScreen.bindKey({ charCode : 'a'.charCodeAt(0) }); 78 | */ 79 | THREEx.FullScreen.bindKey = function(opts){ 80 | opts = opts || {}; 81 | var charCode = opts.charCode || 'f'.charCodeAt(0); 82 | var dblclick = opts.dblclick !== undefined ? opts.dblclick : false; 83 | var element = opts.element 84 | 85 | var toggle = function(){ 86 | if( THREEx.FullScreen.activated() ){ 87 | THREEx.FullScreen.cancel(); 88 | }else{ 89 | THREEx.FullScreen.request(element); 90 | } 91 | } 92 | 93 | var onKeyPress = function(event){ 94 | if( event.which !== charCode ) return; 95 | toggle(); 96 | }.bind(this); 97 | 98 | document.addEventListener('keypress', onKeyPress, false); 99 | 100 | dblclick && document.addEventListener('dblclick', toggle, false); 101 | 102 | return { 103 | unbind : function(){ 104 | document.removeEventListener('keypress', onKeyPress, false); 105 | dblclick && document.removeEventListener('dblclick', toggle, false); 106 | } 107 | }; 108 | } 109 | -------------------------------------------------------------------------------- /js/THREEx.KeyboardState.js: -------------------------------------------------------------------------------- 1 | // THREEx.KeyboardState.js keep the current state of the keyboard. 2 | // It is possible to query it at any time. No need of an event. 3 | // This is particularly convenient in loop driven case, like in 4 | // 3D demos or games. 5 | // 6 | // # Usage 7 | // 8 | // **Step 1**: Create the object 9 | // 10 | // ```var keyboard = new THREEx.KeyboardState();``` 11 | // 12 | // **Step 2**: Query the keyboard state 13 | // 14 | // This will return true if shift and A are pressed, false otherwise 15 | // 16 | // ```keyboard.pressed("shift+A")``` 17 | // 18 | // **Step 3**: Stop listening to the keyboard 19 | // 20 | // ```keyboard.destroy()``` 21 | // 22 | // NOTE: this library may be nice as standaline. independant from three.js 23 | // - rename it keyboardForGame 24 | // 25 | // # Code 26 | // 27 | 28 | /** @namespace */ 29 | var THREEx = THREEx || {}; 30 | 31 | /** 32 | * - NOTE: it would be quite easy to push event-driven too 33 | * - microevent.js for events handling 34 | * - in this._onkeyChange, generate a string from the DOM event 35 | * - use this as event name 36 | */ 37 | THREEx.KeyboardState = function() 38 | { 39 | // to store the current state 40 | this.keyCodes = {}; 41 | this.modifiers = {}; 42 | 43 | // create callback to bind/unbind keyboard events 44 | var self = this; 45 | this._onKeyDown = function(event){ self._onKeyChange(event, true); }; 46 | this._onKeyUp = function(event){ self._onKeyChange(event, false);}; 47 | 48 | // bind keyEvents 49 | document.addEventListener("keydown", this._onKeyDown, false); 50 | document.addEventListener("keyup", this._onKeyUp, false); 51 | } 52 | 53 | /** 54 | * To stop listening of the keyboard events 55 | */ 56 | THREEx.KeyboardState.prototype.destroy = function() 57 | { 58 | // unbind keyEvents 59 | document.removeEventListener("keydown", this._onKeyDown, false); 60 | document.removeEventListener("keyup", this._onKeyUp, false); 61 | } 62 | 63 | THREEx.KeyboardState.MODIFIERS = ['shift', 'ctrl', 'alt', 'meta']; 64 | THREEx.KeyboardState.ALIAS = { 65 | 'left' : 37, 66 | 'up' : 38, 67 | 'right' : 39, 68 | 'down' : 40, 69 | 'space' : 32, 70 | 'pageup' : 33, 71 | 'pagedown' : 34, 72 | 'tab' : 9 73 | }; 74 | 75 | /** 76 | * to process the keyboard dom event 77 | */ 78 | THREEx.KeyboardState.prototype._onKeyChange = function(event, pressed) 79 | { 80 | // log to debug 81 | //console.log("onKeyChange", event, pressed, event.keyCode, event.shiftKey, event.ctrlKey, event.altKey, event.metaKey) 82 | 83 | // update this.keyCodes 84 | var keyCode = event.keyCode; 85 | this.keyCodes[keyCode] = pressed; 86 | 87 | // update this.modifiers 88 | this.modifiers['shift']= event.shiftKey; 89 | this.modifiers['ctrl'] = event.ctrlKey; 90 | this.modifiers['alt'] = event.altKey; 91 | this.modifiers['meta'] = event.metaKey; 92 | } 93 | 94 | /** 95 | * query keyboard state to know if a key is pressed of not 96 | * 97 | * @param {String} keyDesc the description of the key. format : modifiers+key e.g shift+A 98 | * @returns {Boolean} true if the key is pressed, false otherwise 99 | */ 100 | THREEx.KeyboardState.prototype.pressed = function(keyDesc) 101 | { 102 | var keys = keyDesc.split("+"); 103 | for(var i = 0; i < keys.length; i++){ 104 | var key = keys[i]; 105 | var pressed; 106 | if( THREEx.KeyboardState.MODIFIERS.indexOf( key ) !== -1 ){ 107 | pressed = this.modifiers[key]; 108 | }else if( Object.keys(THREEx.KeyboardState.ALIAS).indexOf( key ) != -1 ){ 109 | pressed = this.keyCodes[ THREEx.KeyboardState.ALIAS[key] ]; 110 | }else { 111 | pressed = this.keyCodes[key.toUpperCase().charCodeAt(0)] 112 | } 113 | if( !pressed) return false; 114 | }; 115 | return true; 116 | } 117 | -------------------------------------------------------------------------------- /js/THREEx.WindowResize.js: -------------------------------------------------------------------------------- 1 | // This THREEx helper makes it easy to handle window resize. 2 | // It will update renderer and camera when window is resized. 3 | // 4 | // # Usage 5 | // 6 | // **Step 1**: Start updating renderer and camera 7 | // 8 | // ```var windowResize = THREEx.WindowResize(aRenderer, aCamera)``` 9 | // 10 | // **Step 2**: Start updating renderer and camera 11 | // 12 | // ```windowResize.stop()``` 13 | // # Code 14 | 15 | // 16 | 17 | /** @namespace */ 18 | var THREEx = THREEx || {}; 19 | 20 | /** 21 | * Update renderer and camera when the window is resized 22 | * 23 | * @param {Object} renderer the renderer to update 24 | * @param {Object} Camera the camera to update 25 | */ 26 | THREEx.WindowResize = function(renderer, camera){ 27 | var callback = function(){ 28 | // notify the renderer of the size change 29 | renderer.setSize( window.innerWidth, window.innerHeight ); 30 | // update the camera 31 | camera.aspect = window.innerWidth / window.innerHeight; 32 | camera.updateProjectionMatrix(); 33 | } 34 | // bind the resize event 35 | window.addEventListener('resize', callback, false); 36 | // return .stop() the function to stop watching window resize 37 | return { 38 | /** 39 | * Stop watching window resize 40 | */ 41 | stop : function(){ 42 | window.removeEventListener('resize', callback); 43 | } 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | // 定义全局变量 2 | var container, scene, camera, renderer, controls; 3 | var keyboard = new THREEx.KeyboardState(); 4 | var clock = new THREE.Clock; 5 | 6 | var movingCube; 7 | var collideMeshList = []; 8 | var cubes = []; 9 | var message = document.getElementById("message"); 10 | var crash = false; 11 | var score = 0; 12 | var scoreText = document.getElementById("score"); 13 | var id = 0; 14 | var crashId = " "; 15 | var lastCrashId = " "; 16 | 17 | init(); 18 | animate(); 19 | 20 | function init() { 21 | // Scene 22 | scene = new THREE.Scene(); 23 | // Camera 24 | var screenWidth = window.innerWidth; 25 | var screenHeight = window.innerHeight; 26 | camera = new THREE.PerspectiveCamera(45, screenWidth / screenHeight, 1, 20000); 27 | camera.position.set(0, 170, 400); 28 | 29 | // Renderer 30 | if (Detector.webgl) { 31 | renderer = new THREE.WebGLRenderer({ antialias: true }); 32 | } else { 33 | renderer = new THREE.CanvasRenderer(); 34 | } 35 | renderer.setSize(screenWidth * 0.85, screenHeight * 0.85); 36 | container = document.getElementById("ThreeJS"); 37 | container.appendChild(renderer.domElement); 38 | 39 | THREEx.WindowResize(renderer, camera); 40 | controls = new THREE.OrbitControls(camera, renderer.domElement); 41 | 42 | // 加入两条直线 43 | geometry = new THREE.Geometry(); 44 | geometry.vertices.push(new THREE.Vector3(-250, -1, -3000)); 45 | geometry.vertices.push(new THREE.Vector3(-300, -1, 200)); 46 | material = new THREE.LineBasicMaterial({ 47 | color: 0x6699FF, linewidth: 5, fog: true 48 | }); 49 | var line1 = new THREE.Line(geometry, material); 50 | scene.add(line1); 51 | geometry = new THREE.Geometry(); 52 | geometry.vertices.push(new THREE.Vector3(250, -1, -3000)); 53 | geometry.vertices.push(new THREE.Vector3(300, -1, 200)); 54 | var line2 = new THREE.Line(geometry, material); 55 | scene.add(line2); 56 | 57 | 58 | // 加入控制的cube 59 | var cubeGeometry = new THREE.CubeGeometry(50, 25, 60, 5, 5, 5); 60 | var wireMaterial = new THREE.MeshBasicMaterial({ 61 | color: 0x00ff00, 62 | wireframe: true 63 | }); 64 | 65 | 66 | movingCube = new THREE.Mesh(cubeGeometry, wireMaterial); 67 | // movingCube = new THREE.Mesh(cubeGeometry, material); 68 | // movingCube = new THREE.BoxHelper(movingCube); 69 | movingCube.position.set(0, 25, -20); 70 | scene.add(movingCube); 71 | 72 | 73 | } 74 | 75 | function animate() { 76 | requestAnimationFrame(animate); 77 | update(); 78 | renderer.render(scene, camera); 79 | 80 | } 81 | 82 | function update() { 83 | var delta = clock.getDelta(); 84 | var moveDistance = 200 * delta; 85 | //console.log(moveDistance); 86 | var rotateAngle = Math.PI / 2 * delta; 87 | 88 | // if (keyboard.pressed("A")) { 89 | // camera.rotation.z -= 0.2 * Math.PI / 180; 90 | // console.log("press A") 91 | // } 92 | // if (keyboard.pressed("D")) { 93 | // movingCube.rotation.y += rotateAngle; 94 | // } 95 | 96 | if (keyboard.pressed("left") || keyboard.pressed("A")) { 97 | if (movingCube.position.x > -270) 98 | movingCube.position.x -= moveDistance; 99 | if (camera.position.x > -150) { 100 | camera.position.x -= moveDistance * 0.6; 101 | if (camera.rotation.z > -5 * Math.PI / 180) { 102 | camera.rotation.z -= 0.2 * Math.PI / 180; 103 | } 104 | } 105 | } 106 | if (keyboard.pressed("right") || keyboard.pressed("D")) { 107 | if (movingCube.position.x < 270) 108 | movingCube.position.x += moveDistance; 109 | if (camera.position.x < 150) { 110 | camera.position.x += moveDistance * 0.6; 111 | if (camera.rotation.z < 5 * Math.PI / 180) { 112 | camera.rotation.z += 0.2 * Math.PI / 180; 113 | } 114 | } 115 | } 116 | if (keyboard.pressed("up") || keyboard.pressed("W")) { 117 | movingCube.position.z -= moveDistance; 118 | } 119 | if (keyboard.pressed("down") || keyboard.pressed("S")) { 120 | movingCube.position.z += moveDistance; 121 | } 122 | 123 | if (!(keyboard.pressed("left") || keyboard.pressed("right") || 124 | keyboard.pressed("A") || keyboard.pressed("D"))) { 125 | delta = camera.rotation.z; 126 | camera.rotation.z -= delta / 10; 127 | } 128 | 129 | 130 | var originPoint = movingCube.position.clone(); 131 | 132 | for (var vertexIndex = 0; vertexIndex < movingCube.geometry.vertices.length; vertexIndex++) { 133 | // 顶点原始坐标 134 | var localVertex = movingCube.geometry.vertices[vertexIndex].clone(); 135 | // 顶点经过变换后的坐标 136 | var globalVertex = localVertex.applyMatrix4(movingCube.matrix); 137 | var directionVector = globalVertex.sub(movingCube.position); 138 | 139 | var ray = new THREE.Raycaster(originPoint, directionVector.clone().normalize()); 140 | var collisionResults = ray.intersectObjects(collideMeshList); 141 | if (collisionResults.length > 0 && collisionResults[0].distance < directionVector.length()) { 142 | crash = true; 143 | crashId = collisionResults[0].object.name; 144 | break; 145 | } 146 | crash = false; 147 | } 148 | 149 | if (crash) { 150 | // message.innerText = "crash"; 151 | movingCube.material.color.setHex(0x346386); 152 | console.log("Crash"); 153 | if (crashId !== lastCrashId) { 154 | score -= 100; 155 | lastCrashId = crashId; 156 | } 157 | 158 | document.getElementById('explode_sound').play() 159 | } else { 160 | // message.innerText = "Safe"; 161 | movingCube.material.color.setHex(0x00ff00); 162 | } 163 | 164 | if (Math.random() < 0.03 && cubes.length < 30) { 165 | makeRandomCube(); 166 | } 167 | 168 | for (i = 0; i < cubes.length; i++) { 169 | if (cubes[i].position.z > camera.position.z) { 170 | scene.remove(cubes[i]); 171 | cubes.splice(i, 1); 172 | collideMeshList.splice(i, 1); 173 | } else { 174 | cubes[i].position.z += 10; 175 | } 176 | // renderer.render(scene, camera); 177 | } 178 | 179 | score += 0.1; 180 | scoreText.innerText = "Score:" + Math.floor(score); 181 | 182 | //controls.update(); 183 | } 184 | 185 | 186 | // 返回一个介于min和max之间的随机数 187 | function getRandomArbitrary(min, max) { 188 | return Math.random() * (max - min) + min; 189 | } 190 | 191 | // 返回一个介于min和max之间的整型随机数 192 | function getRandomInt(min, max) { 193 | return Math.floor(Math.random() * (max - min + 1) + min); 194 | } 195 | 196 | 197 | function makeRandomCube() { 198 | var a = 1 * 50, 199 | b = getRandomInt(1, 3) * 50, 200 | c = 1 * 50; 201 | var geometry = new THREE.CubeGeometry(a, b, c); 202 | var material = new THREE.MeshBasicMaterial({ 203 | color: Math.random() * 0xffffff, 204 | size: 3 205 | }); 206 | 207 | 208 | var object = new THREE.Mesh(geometry, material); 209 | var box = new THREE.BoxHelper(object); 210 | // box.material.color.setHex(Math.random() * 0xffffff); 211 | box.material.color.setHex(0xff0000); 212 | 213 | box.position.x = getRandomArbitrary(-250, 250); 214 | box.position.y = 1 + b / 2; 215 | box.position.z = getRandomArbitrary(-800, -1200); 216 | cubes.push(box); 217 | box.name = "box_" + id; 218 | id++; 219 | collideMeshList.push(box); 220 | 221 | scene.add(box); 222 | } -------------------------------------------------------------------------------- /sound/explode.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noiron/race-game-threejs/7af36f4494ed2919856eb7640ed1df6ae792764e/sound/explode.mp3 -------------------------------------------------------------------------------- /test/cube_collision.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Move a cube 6 | 7 | 8 | 9 | 10 | 11 | 12 | 22 | 23 | 24 |
25 |
26 | 27 | 182 | 183 | -------------------------------------------------------------------------------- /test/move_cube.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Move a cube 6 | 7 | 8 | 9 | 10 | 11 | 12 | 22 | 23 | 24 |
25 | 115 | 116 | --------------------------------------------------------------------------------