├── .babelrc ├── .editorconfig ├── .github └── main.workflow ├── .gitignore ├── LICENSE.md ├── README.md ├── assets ├── 3d │ └── car.glb ├── boulder.png ├── boulder2.png ├── car-army.png ├── car-blue.png ├── car-green.png ├── car-red.png ├── car-yellow.png ├── clouds.png ├── clouds2.png ├── fonts │ ├── cosmicavenger.png │ ├── cosmicavenger.xml │ ├── impact-24-outline.png │ ├── impact-24-outline.xml │ ├── number-font.png │ └── number-font.xml ├── hills.png ├── mountain.png ├── smoke-particle.png ├── sound │ ├── car-collision.wav │ ├── confirm.wav │ ├── drozerix_-_dream_candy.xm │ ├── engine-loop.wav │ ├── explosion.wav │ ├── select.wav │ ├── time-extended.wav │ └── tire-squeal.wav ├── tree.png ├── tree2.png ├── tree3.png └── turn-sign.png ├── ava.config.js ├── buildConfig.js ├── number-font.png ├── number-font.xml ├── package-lock.json ├── package.json ├── src ├── Components │ ├── Car.ts │ ├── CarManager.ts │ ├── Colors.ts │ ├── Player.ts │ ├── Prop.ts │ ├── RadarCar.ts │ ├── Renderer.ts │ ├── Road.ts │ ├── SegmentPoint.ts │ ├── SegmentType.ts │ ├── SpeedGauge.ts │ ├── TrackRadar.ts │ ├── TrackSegment.ts │ └── Util.ts ├── config │ ├── GameConfig.ts │ └── GameSettings.ts ├── css │ └── styles.css ├── custom.d.ts ├── favicon.ico ├── game.ts ├── index.html ├── libs │ ├── GLTFLoader.js │ └── Phaser3D.js ├── phaser.d.ts └── scenes │ ├── BaseScene.ts │ ├── BootScene.ts │ ├── GameScene.ts │ ├── LoadScene.ts │ └── RaceUiScene.ts ├── tasks └── createZip.js ├── tsconfig.json ├── tslint.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/typescript", 4 | ["@babel/preset-env", { "modules": false }] 5 | ], 6 | "plugins": [ 7 | "@babel/proposal-class-properties", 8 | "@babel/proposal-object-rest-spread", 9 | ], 10 | "env": { 11 | "production": { 12 | "plugins": [ 13 | ["transform-remove-console", { "exclude": [ "error", "warn"] }], 14 | ] 15 | }, 16 | "development": {} 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | insert_final_newline = true 6 | indent_style = tab 7 | indent_size = 4 8 | 9 | [{package.json,*.yml}] 10 | indent_style = space 11 | indent_size = 2 -------------------------------------------------------------------------------- /.github/main.workflow: -------------------------------------------------------------------------------- 1 | workflow "Publish master to gh-pages" { 2 | on = "push" 3 | resolves = ["Publish to gh-pages branch"] 4 | } 5 | 6 | action "Filter branch" { 7 | uses = "actions/bin/filter@master" 8 | args = "branch master" 9 | } 10 | 11 | action "Publish to gh-pages branch" { 12 | uses = "pinkkis/page-publisher-gh-action@master" 13 | env = { 14 | TARGET_REPO = "pinkkis/phaser-driving" 15 | CNAME = "driver.poisonvial.com" 16 | } 17 | secrets = [ 18 | "GITHUB_TOKEN", 19 | ] 20 | args = "gh-pages" 21 | needs = ["Filter branch"] 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Apple garbage 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directory 30 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 31 | node_modules 32 | 33 | # build/dist folders 34 | dist/ 35 | build/ 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2019 Kristian Koivisto-Kokko 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Phaser Driver 2 | 3 | Outrun style driving game in phaser. Try it on Itch: https://poisonvial.itch.io/phaser-driving 4 | 5 | Code licensed under MIT, see LICENSE.md. 6 | Original Assets licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), excluding assets from other sources, listed below. 7 | 8 | Shameless plug of using my phaser plugin for playing tracker music: https://github.com/pinkkis/phaser-plugin-pasuuna 9 | 10 | --- 11 | 12 | Thanks to Jake Gordon for their blog post on javascript pseudo 3d racers 13 | * https://codeincomplete.com/posts/javascript-racer/ 14 | 15 | Assets used: 16 | * Low Poly Jeep by https://sketchfab.com/BosstoneBaga - used under CC-BY license 17 | * Dream Candy by Drozerix https://modarchive.org/index.php?request=view_by_moduleid&query=178565 - Public Domain 18 | -------------------------------------------------------------------------------- /assets/3d/car.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/3d/car.glb -------------------------------------------------------------------------------- /assets/boulder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/boulder.png -------------------------------------------------------------------------------- /assets/boulder2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/boulder2.png -------------------------------------------------------------------------------- /assets/car-army.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/car-army.png -------------------------------------------------------------------------------- /assets/car-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/car-blue.png -------------------------------------------------------------------------------- /assets/car-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/car-green.png -------------------------------------------------------------------------------- /assets/car-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/car-red.png -------------------------------------------------------------------------------- /assets/car-yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/car-yellow.png -------------------------------------------------------------------------------- /assets/clouds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/clouds.png -------------------------------------------------------------------------------- /assets/clouds2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/clouds2.png -------------------------------------------------------------------------------- /assets/fonts/cosmicavenger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/fonts/cosmicavenger.png -------------------------------------------------------------------------------- /assets/fonts/cosmicavenger.xml: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /assets/fonts/impact-24-outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/fonts/impact-24-outline.png -------------------------------------------------------------------------------- /assets/fonts/impact-24-outline.xml: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /assets/fonts/number-font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/fonts/number-font.png -------------------------------------------------------------------------------- /assets/fonts/number-font.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /assets/hills.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/hills.png -------------------------------------------------------------------------------- /assets/mountain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/mountain.png -------------------------------------------------------------------------------- /assets/smoke-particle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/smoke-particle.png -------------------------------------------------------------------------------- /assets/sound/car-collision.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/sound/car-collision.wav -------------------------------------------------------------------------------- /assets/sound/confirm.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/sound/confirm.wav -------------------------------------------------------------------------------- /assets/sound/drozerix_-_dream_candy.xm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/sound/drozerix_-_dream_candy.xm -------------------------------------------------------------------------------- /assets/sound/engine-loop.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/sound/engine-loop.wav -------------------------------------------------------------------------------- /assets/sound/explosion.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/sound/explosion.wav -------------------------------------------------------------------------------- /assets/sound/select.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/sound/select.wav -------------------------------------------------------------------------------- /assets/sound/time-extended.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/sound/time-extended.wav -------------------------------------------------------------------------------- /assets/sound/tire-squeal.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/sound/tire-squeal.wav -------------------------------------------------------------------------------- /assets/tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/tree.png -------------------------------------------------------------------------------- /assets/tree2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/tree2.png -------------------------------------------------------------------------------- /assets/tree3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/tree3.png -------------------------------------------------------------------------------- /assets/turn-sign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/assets/turn-sign.png -------------------------------------------------------------------------------- /ava.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | files: [ 3 | 'src/**/*spec.ts', 4 | ], 5 | sources: [ 6 | 'src/**/*.ts', 7 | ], 8 | compileEnhancements: false, 9 | verbose: true, 10 | extensions: ["ts"], 11 | require: [ 12 | "ts-node/register/transpile-only" 13 | ] 14 | }; 15 | -------------------------------------------------------------------------------- /buildConfig.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const {name, description, version} = require('./package.json'); 4 | const buildDir = path.join(__dirname + '/build'); 5 | const distDir = path.join(__dirname + '/dist'); 6 | 7 | module.exports = { 8 | name, 9 | description, 10 | version, 11 | buildDir, 12 | distDir, 13 | }; 14 | -------------------------------------------------------------------------------- /number-font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/number-font.png -------------------------------------------------------------------------------- /number-font.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phaser-driver", 3 | "version": "0.8.0", 4 | "description": "Outrun style driving game in Phaser 3", 5 | "main": "index.js", 6 | "scripts": { 7 | "itchio": "npm run build && node ./tasks/createZip.js", 8 | "build": "cross-env NODE_ENV=production webpack --mode production", 9 | "start": "cross-env NODE_ENV=development webpack-dev-server --mode development --progress --port=8000 --open" 10 | }, 11 | "browserslist": "> 1%, not dead", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/pinkkis/phaser-driver.git" 15 | }, 16 | "keywords": ["phaser", "game", "driving", "outrun"], 17 | "author": "Kristian Koivisto-Kokko (http://pinkkis.com/)", 18 | "license": "MIT", 19 | "licenses": [ 20 | { 21 | "type": "MIT", 22 | "url": "https://github.com/pinkkis/phaser-minesweeper/blob/master/LICENSE.md" 23 | } 24 | ], 25 | "bugs": { 26 | "url": "https://github.com/pinkkis/phaser-driver/issues" 27 | }, 28 | "homepage": "https://github.com/pinkkis/phaser-driver#readme", 29 | "devDependencies": { 30 | "@babel/core": "^7.4.0", 31 | "@babel/plugin-proposal-class-properties": "^7.4.0", 32 | "@babel/plugin-proposal-object-rest-spread": "^7.4.0", 33 | "@babel/preset-env": "^7.4.2", 34 | "@babel/preset-typescript": "^7.3.3", 35 | "@csstools/normalize.css": "^9.0.1", 36 | "@pinkkis/phaser-plugin-pasuuna": "^0.5.2", 37 | "archiver": "^3.0.0", 38 | "babel-loader": "^8.0.5", 39 | "babel-plugin-transform-remove-console": "^6.9.4", 40 | "clean-webpack-plugin": "^2.0.1", 41 | "copy-webpack-plugin": "^5.0.2", 42 | "cross-env": "^5.2.0", 43 | "css-loader": "^2.1.1", 44 | "expose-loader": "^0.7.5", 45 | "file-loader": "^3.0.1", 46 | "html-loader": "^0.5.5", 47 | "html-webpack-plugin": "^3.2.0", 48 | "mini-css-extract-plugin": "^0.5.0", 49 | "optimize-css-assets-webpack-plugin": "^5.0.1", 50 | "phaser": "^3.16.2", 51 | "raw-loader": "^2.0.0", 52 | "source-map-loader": "^0.2.4", 53 | "stats-js": "^1.0.0", 54 | "terser-webpack-plugin": "^1.2.3", 55 | "three": "^0.103.0", 56 | "ts-loader": "^5.3.3", 57 | "ts-node": "^8.0.3", 58 | "tslint": "^5.14.0", 59 | "typescript": "^3.4.1", 60 | "webpack": "^4.29.6", 61 | "webpack-cli": "^3.3.0", 62 | "webpack-dev-server": "^3.2.1" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Components/Car.ts: -------------------------------------------------------------------------------- 1 | import { GameScene } from '../scenes/GameScene'; 2 | import { TrackSegment } from './TrackSegment'; 3 | import { gameSettings } from '../config/GameSettings'; 4 | import { Util } from './Util'; 5 | import { Road } from './Road'; 6 | 7 | export class Car { 8 | public scene: GameScene; 9 | public road: Road; 10 | public sprite: Phaser.GameObjects.Sprite; 11 | public offset: number = 0; 12 | public speed: number = 0; 13 | public trackPosition: number = 0; 14 | public percent: number = 0; 15 | public scale: number = 1500; 16 | 17 | constructor(scene: GameScene, road: Road, offset: number, trackPosition: number, sprite: string, speed: number) { 18 | this.scene = scene; 19 | this.road = road; 20 | this.offset = offset; 21 | this.speed = speed; 22 | this.trackPosition = trackPosition; 23 | this.sprite = this.scene.add.sprite(-999, 999, sprite, 0).setOrigin(0.5, 1); 24 | } 25 | 26 | public get isOnGravel(): boolean { 27 | return Math.abs(this.offset) > 1; 28 | } 29 | 30 | public update(delta: number, carSegment: TrackSegment, playerSegment: TrackSegment, playerOffset: number): void { 31 | this.updateOffset(delta, carSegment, playerSegment); 32 | this.updateAngleFrame(carSegment, playerSegment, playerOffset); 33 | } 34 | 35 | public draw(x: number = 0, y: number = 0, scale: number = 1, segmentClip: number = 0) { 36 | this.sprite.setPosition(x, y); 37 | this.sprite.setScale(this.scale * scale); 38 | this.sprite.setDepth(10 + scale); // draw order 39 | 40 | if (!this.sprite.visible) { 41 | this.sprite.setVisible(true); 42 | } 43 | 44 | // calculate clipping behind hills 45 | if (y > segmentClip) { 46 | const clipped = (y - segmentClip) / this.sprite.scaleY; 47 | const cropY = this.sprite.height - clipped; 48 | this.sprite.setCrop(0, 0, this.sprite.width, cropY); 49 | } else { 50 | this.sprite.setCrop(); 51 | } 52 | } 53 | 54 | public updateAngleFrame(carSegment: TrackSegment, playerSegment: TrackSegment, playerOffset: number): void { 55 | const roadDistance = Math.abs(carSegment.index - playerSegment.index); 56 | const offsetDistance = Math.abs(playerOffset - this.offset); 57 | const isLeft = playerOffset > this.offset; 58 | 59 | if (roadDistance < 20 && offsetDistance > 0.3) { 60 | this.sprite.setFrame(1); 61 | this.sprite.flipX = !isLeft; 62 | } else { 63 | this.sprite.setFrame(0); 64 | } 65 | } 66 | 67 | public updateOffset(delta: number, carSegment: TrackSegment, playerSegment: TrackSegment): void { 68 | // segments ahead to see if there's somethign to avoid 69 | const lookahead = 50; 70 | 71 | const player = this.scene.player; 72 | 73 | // car not visible, don't do ai behaviour 74 | if (carSegment.index - playerSegment.index > gameSettings.drawDistance) { 75 | return; 76 | } 77 | 78 | for (let i = 0; i < lookahead; i++) { 79 | const segment = this.road.segments[ (carSegment.index + i) % this.road.segments.length]; 80 | 81 | if (segment === playerSegment && this.speed > player.speed && Util.overlapPlayer(player, this)) { 82 | if (player.x < this.offset) { 83 | this.offset = Util.interpolate(this.offset, 1, delta * 0.1); 84 | } else { 85 | this.offset = Util.interpolate(this.offset, -1, delta * 0.1); 86 | } 87 | } 88 | 89 | if (segment.cars.size) { 90 | segment.cars.forEach((car: Car) => { 91 | if (car === this) { return; } 92 | 93 | if (this.speed > car.speed && Util.overlapSprite(car.sprite, this.sprite)) { 94 | if (car.offset < this.offset) { 95 | this.offset = Util.interpolate(this.offset, 1, delta * 0.1); 96 | } else { 97 | this.offset = Util.interpolate(this.offset, -1, delta * 0.1); 98 | } 99 | } 100 | }); 101 | } 102 | 103 | } 104 | 105 | // steer towards center of track if outside it 106 | if (Math.abs(this.offset) > 0.9) { 107 | this.offset = Util.interpolate(this.offset, 0, delta); 108 | } 109 | } 110 | 111 | public destroy(): void { 112 | this.sprite.destroy(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Components/CarManager.ts: -------------------------------------------------------------------------------- 1 | import { Car } from './Car'; 2 | import { Road } from './Road'; 3 | import { gameSettings } from '../config/GameSettings'; 4 | import { GameScene } from '../scenes/GameScene'; 5 | import { TrackSegment } from './TrackSegment'; 6 | import { Util } from './Util'; 7 | 8 | export class CarManager { 9 | public scene: GameScene; 10 | public cars: Set = new Set(); 11 | 12 | private road: Road; 13 | 14 | constructor(scene: GameScene, road: Road) { 15 | this.scene = scene; 16 | this.road = road; 17 | } 18 | 19 | public resetCars(): void { 20 | this.destroy(); 21 | 22 | for (let i = 0; i < gameSettings.totalCars; i++) { 23 | const roadOffset = Phaser.Math.FloatBetween(-0.8, 0.8); 24 | const trackPosition = Phaser.Math.Between(0, this.road.trackLength); 25 | const spriteString = this.getRandomCarType(); 26 | const speed = Phaser.Math.Between(gameSettings.maxSpeed * 0.2, gameSettings.maxSpeed * 0.8); 27 | 28 | const car = new Car(this.scene, this.road, roadOffset, trackPosition, spriteString, speed); 29 | const carSegment = this.road.findSegmentByZ(trackPosition); 30 | 31 | this.cars.add(car); 32 | carSegment.cars.add(car); 33 | } 34 | } 35 | 36 | public update(delta: number, playerSegment: TrackSegment, playerOffset: number): void { 37 | for (const car of this.cars) { 38 | const oldSegment = this.road.findSegmentByZ(car.trackPosition); 39 | 40 | car.update(delta, oldSegment, playerSegment, playerOffset); 41 | car.trackPosition = Util.increase(car.trackPosition, delta * car.speed, this.road.trackLength); 42 | car.percent = Util.percentRemaining(car.trackPosition, gameSettings.segmentLength); 43 | 44 | const newSegment = this.road.findSegmentByZ(car.trackPosition); 45 | if (newSegment.index !== oldSegment.index) { 46 | oldSegment.cars.delete(car); 47 | newSegment.cars.add(car); 48 | } 49 | } 50 | } 51 | 52 | public hideAll(): void { 53 | this.cars.forEach( (car: Car) => { 54 | car.sprite.setVisible(false); 55 | }); 56 | } 57 | 58 | public destroy(): void { 59 | this.cars.forEach( (car: Car) => car.destroy() ); 60 | this.cars.clear(); 61 | } 62 | 63 | private getRandomCarType(): string { 64 | const availableSprites = ['car-army', 'car-yellow', 'car-red', 'car-green', 'car-blue']; 65 | const rndIndex = Phaser.Math.Between(0, availableSprites.length - 1); 66 | 67 | return availableSprites[rndIndex]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Components/Colors.ts: -------------------------------------------------------------------------------- 1 | export const Colors = { 2 | ROAD_LIGHT: new Phaser.Display.Color(127, 127, 127, 1), 3 | ROAD_DARK: new Phaser.Display.Color(123, 123, 123, 1), 4 | 5 | GRASS_LIGHT: new Phaser.Display.Color(118, 197, 120, 1), 6 | GRASS_DARK: new Phaser.Display.Color(130, 211, 34, 1), 7 | 8 | LANE_MARKER: new Phaser.Display.Color(220, 220, 220, 1), 9 | 10 | RUMBLE_LIGHT: new Phaser.Display.Color(200, 200, 200, 1), 11 | RUMBLE_DARK: new Phaser.Display.Color(177, 64, 64, 1), 12 | 13 | SKY: new Phaser.Display.Color(127, 127, 255, 1), 14 | }; 15 | 16 | export const DarkColors = { 17 | ROAD: Colors.ROAD_DARK.color, 18 | GRASS: Colors.GRASS_DARK.color, 19 | RUMBLE: Colors.RUMBLE_LIGHT.color, 20 | }; 21 | 22 | export const LightColors = { 23 | ROAD: Colors.ROAD_LIGHT.color, 24 | GRASS: Colors.GRASS_LIGHT.color, 25 | RUMBLE: Colors.RUMBLE_DARK.color, 26 | LANE: Colors.LANE_MARKER.color, 27 | }; 28 | -------------------------------------------------------------------------------- /src/Components/Player.ts: -------------------------------------------------------------------------------- 1 | import { GameScene } from '../scenes/GameScene'; 2 | import { Phaser3D } from '../libs/Phaser3D'; 3 | import { gameSettings } from '../config/GameSettings'; 4 | import { Util } from './Util'; 5 | 6 | const HALFPI = Math.PI / 2; 7 | 8 | export class Player { 9 | public position: Phaser.Math.Vector3; 10 | public sprite: Phaser.GameObjects.Rectangle; 11 | public scene: GameScene; 12 | public p3d: Phaser3D; 13 | public model: any; 14 | public smokeParticles: Phaser.GameObjects.Particles.ParticleEmitterManager; 15 | public smokeEmitterLeft: Phaser.GameObjects.Particles.ParticleEmitter; 16 | public smokeEmitterRight: Phaser.GameObjects.Particles.ParticleEmitter; 17 | 18 | public engineSound: Phaser.Sound.WebAudioSound; 19 | public tireScreechSound: Phaser.Sound.WebAudioSound; 20 | public explosionSound: Phaser.Sound.WebAudioSound; 21 | public collideSound: Phaser.Sound.WebAudioSound; 22 | 23 | public turn: number; 24 | public pitch: number; 25 | public speed: number; 26 | public trackPosition: number; 27 | public accelerating: boolean = false; 28 | public screeching: boolean = false; 29 | public collisionRadius: number = 20; 30 | private turnVector: Phaser.Math.Vector3; 31 | 32 | constructor(scene: GameScene, x: number, y: number, z: number, modelKey: string) { 33 | this.position = new Phaser.Math.Vector3(x, y, z); 34 | this.scene = scene; 35 | this.turn = 0; 36 | this.pitch = 0; 37 | this.speed = 0; 38 | this.trackPosition = 0; 39 | this.turnVector = new Phaser.Math.Vector3(0, 0, 0); 40 | 41 | this.smokeParticles = this.scene.add.particles('particles').setDepth(21); 42 | const particleSettings = { 43 | x: -100, 44 | y: -100, 45 | lifespan: 500, 46 | frequency: 66, 47 | frame: 0, 48 | blendMode: 'NORMAL', 49 | gravityY: -100, 50 | speed: 0, 51 | rotate: { onEmit: () => Math.random() * 359 }, 52 | scale: { start: 0.3, end: 2 }, 53 | }; 54 | 55 | this.engineSound = this.scene.sound.add('engine', { volume: 0.7, loop: true }) as Phaser.Sound.WebAudioSound; 56 | this.engineSound.play(); 57 | 58 | this.tireScreechSound = this.scene.sound.add('tire-squeal', { volume: 0.5, loop: true}) as Phaser.Sound.WebAudioSound; 59 | this.explosionSound = this.scene.sound.add('explosion', { volume: 0.75 }) as Phaser.Sound.WebAudioSound; 60 | this.collideSound = this.scene.sound.add('collision', { volume: 0.75 }) as Phaser.Sound.WebAudioSound; 61 | 62 | this.smokeEmitterLeft = this.smokeParticles.createEmitter(particleSettings); 63 | this.smokeEmitterRight = this.smokeParticles.createEmitter(particleSettings); 64 | 65 | this.p3d = new Phaser3D(this.scene, { fov: 35, x: 0, y: 7, z: -20, antialias: false }); 66 | this.p3d.view.setDepth(20); 67 | this.p3d.addGLTFModel(modelKey); 68 | 69 | this.p3d.camera.lookAt(0, 5.1, 0); 70 | 71 | this.p3d.add.hemisphereLight({ skyColor: 0xefefff, groundColor: 0x111111, intensity: 2 }); 72 | this.p3d.on('loadgltf', (gltf: any, model: any) => { 73 | model.rotateY(HALFPI); 74 | model.position.set(0, 0, 0); 75 | model.scale.set(1, 1, 1); 76 | this.model = model; 77 | }); 78 | } 79 | 80 | public get x(): number { return this.position.x; } 81 | public set x(x: number) { 82 | this.position.x = x; 83 | this.scene.registry.set('playerx', x); 84 | } 85 | 86 | public get y(): number { return this.position.y; } 87 | public set y(y: number) { 88 | this.position.y = y; 89 | } 90 | 91 | public get z(): number { return this.position.z; } 92 | public set z(z: number) { 93 | this.position.z = z; 94 | } 95 | 96 | public get isOnGravel(): boolean { 97 | return Math.abs(this.x) > 1; 98 | } 99 | 100 | public update(delta: number, dx: number) { 101 | this.position.x += (this.turn * 0.08) * (this.speed / gameSettings.maxSpeed); 102 | 103 | if (this.model) { 104 | this.turnVector.y = HALFPI + -this.turn; 105 | this.turnVector.x = Phaser.Math.Clamp(this.pitch, -0.3, 0.3); 106 | this.model.rotation.setFromVector3(this.turnVector); 107 | this.p3d.camera.rotation.z = Math.PI + Phaser.Math.DegToRad(this.scene.cameraAngle); 108 | 109 | if (this.pitch > 0) { 110 | this.model.position.y = Util.interpolate(this.model.position.y, -this.pitch * 3, 0.33); 111 | } 112 | 113 | if (this.speed > 20) { 114 | this.model.position.y = Util.interpolate(this.model.position.y + Phaser.Math.Between(-1, 1) * (this.isOnGravel ? 0.05 : 0.01), 0, 0.2); 115 | } 116 | 117 | this.updateParticles(); 118 | 119 | this.playEngineSound(); 120 | this.tireScreech(this.screeching); 121 | } 122 | } 123 | 124 | public playEngineSound(): void { 125 | if (this.speed > 0 && this.engineSound.isPlaying) { 126 | this.engineSound.setDetune( this.speed * 1.25 ); 127 | this.engineSound.setVolume( 0.7 + Phaser.Math.Clamp(this.speed * 0.0001, 0, 0.2) ); 128 | } 129 | } 130 | 131 | public tireScreech(play = false): void { 132 | if (play && !this.tireScreechSound.isPlaying) { 133 | this.tireScreechSound.play(); 134 | } else { 135 | this.tireScreechSound.stop(); 136 | } 137 | } 138 | 139 | public collide(type: string): void { 140 | switch (type) { 141 | case 'car': 142 | if (!this.collideSound.isPlaying) { 143 | this.collideSound.play(); 144 | } 145 | break; 146 | 147 | case 'prop': 148 | if (!this.explosionSound.isPlaying && this.speed > 700) { 149 | this.explosionSound.play(); 150 | } else if (!this.collideSound.isPlaying && this.speed > 200) { 151 | this.collideSound.play(); 152 | } 153 | break; 154 | 155 | default: 156 | break; 157 | } 158 | } 159 | 160 | public updateParticles() { 161 | const particleSpeed = -this.turn * 100; 162 | const particleAngle = this.turn < 0 ? { min: -30, max: 0 } : { min: 180, max: 210 }; 163 | const halfWidth = this.scene.scale.gameSize.width / 2; 164 | const particleX = halfWidth + (-this.turn * 20); 165 | 166 | this.smokeEmitterLeft.setPosition(particleX - 13, this.scene.scale.gameSize.height - 5 - this.pitch * 15 - (this.turn > 0 ? this.turn * 7 : 0)); 167 | this.smokeEmitterRight.setPosition(particleX + 13, this.scene.scale.gameSize.height - 5 - this.pitch * 15 + (this.turn < 0 ? this.turn * 7 : 0)); 168 | 169 | this.smokeEmitterLeft.setSpeed(particleSpeed); 170 | this.smokeEmitterRight.setSpeed(particleSpeed); 171 | 172 | this.smokeEmitterLeft.setAngle(particleAngle); 173 | this.smokeEmitterRight.setAngle(particleAngle); 174 | 175 | if (this.isOnGravel) { 176 | this.smokeEmitterLeft.setFrame(1); 177 | this.smokeEmitterRight.setFrame(1); 178 | } else { 179 | this.smokeEmitterLeft.setFrame(0); 180 | this.smokeEmitterRight.setFrame(0); 181 | } 182 | 183 | if (this.speed > 300 && Math.abs(this.turn) > 0.66 && !this.smokeEmitterLeft.on) { 184 | this.smokeEmitterLeft.on = true; 185 | this.smokeEmitterRight.on = true; 186 | this.screeching = true; 187 | } else if (this.speed > 100 && this.isOnGravel) { 188 | this.smokeEmitterLeft.on = true; 189 | this.smokeEmitterRight.on = true; 190 | this.screeching = false; 191 | } else if (this.speed < 400 && this.accelerating) { 192 | this.smokeEmitterLeft.on = true; 193 | this.smokeEmitterRight.on = true; 194 | this.screeching = true; 195 | } else { 196 | this.smokeEmitterLeft.stop(); 197 | this.smokeEmitterRight.stop(); 198 | this.screeching = false; 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/Components/Prop.ts: -------------------------------------------------------------------------------- 1 | import { GameScene } from '../scenes/GameScene'; 2 | 3 | export class Prop { 4 | public scene: GameScene; 5 | public sprite: Phaser.GameObjects.Sprite; 6 | public offset: number; 7 | public height: number; 8 | public collides: boolean; 9 | public scale: number = 1; // scale calculation in game is _really_ small 10 | 11 | constructor(scene: GameScene, name: string, offset: number, height: number, scale: number = 3000, flipX: boolean = false, collides: boolean = false) { 12 | this.scene = scene; 13 | this.sprite = scene.add.sprite(-999, -999, name).setOrigin(0.5, 1).setVisible(false); 14 | this.offset = offset; 15 | this.height = height; 16 | this.collides = collides; 17 | this.scale = scale; 18 | 19 | this.sprite.flipX = flipX; 20 | } 21 | 22 | public update(x: number = 0, y: number = 0, scale: number = 1, segmentClip: number = 0) { 23 | this.sprite.setPosition(x, y + this.height); 24 | this.sprite.setScale(this.scale * scale); 25 | this.sprite.setDepth(10 + scale); // draw order 26 | 27 | if (!this.sprite.visible) { 28 | this.sprite.setVisible(true); 29 | } 30 | 31 | // calculate clipping behind hills 32 | if (y > segmentClip) { 33 | const clipped = (y - segmentClip) / this.sprite.scaleY; 34 | const cropY = this.sprite.height - clipped; 35 | this.sprite.setCrop(0, 0, this.sprite.width, cropY); 36 | } else { 37 | this.sprite.setCrop(); 38 | } 39 | } 40 | 41 | public destroy(): void { 42 | this.sprite.destroy(); 43 | this.scene = undefined; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Components/RadarCar.ts: -------------------------------------------------------------------------------- 1 | export class RadarCar { 2 | public x: number; 3 | public y: number; 4 | public color: number; 5 | 6 | constructor(x: number, y: number, color: number) { 7 | this.x = x; 8 | this.y = y; 9 | this.color = color; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Components/Renderer.ts: -------------------------------------------------------------------------------- 1 | import { SegmentPoint } from './SegmentPoint'; 2 | import { Util } from './Util'; 3 | import { gameSettings } from '../config/GameSettings'; 4 | import { GameScene } from '../scenes/GameScene'; 5 | 6 | export class Renderer { 7 | public static project(sp: SegmentPoint, cameraX: number, cameraY: number, cameraZ: number, cameraDepth: number, width: number, height: number, roadWidth: number) { 8 | sp.camera.x = (sp.world.x || 0) - cameraX; 9 | sp.camera.y = (sp.world.y || 0) - cameraY; 10 | sp.camera.z = (sp.world.z || 0) - cameraZ; 11 | sp.screen.scale = cameraDepth / sp.camera.z; 12 | sp.screen.x = Math.round((width / 2) + (sp.screen.scale * sp.camera.x * width / 2)); 13 | sp.screen.y = Math.round((height / 2) - (sp.screen.scale * sp.camera.y * height / 2)); 14 | sp.screen.w = Math.round((sp.screen.scale * roadWidth * width / 2)); 15 | } 16 | 17 | public static drawPolygon(g: Phaser.GameObjects.Graphics, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number, color: number) { 18 | g.save().fillStyle(color).beginPath() 19 | .moveTo(x1, y1).lineTo(x2, y2).lineTo(x3, y3).lineTo(x4, y4) 20 | .closePath().fillPath().restore(); 21 | } 22 | 23 | public static drawSegment(ctx: Phaser.GameObjects.Graphics, width: number, lanes: number, x1: number, y1: number, w1: number, x2: number, y2: number, w2: number, colors: any) { 24 | const r1 = Util.rumbleWidth(w1, lanes); 25 | const r2 = Util.rumbleWidth(w2, lanes); 26 | const l1 = Util.laneMarkerWidth(w1, lanes); 27 | const l2 = Util.laneMarkerWidth(w1, lanes); 28 | const h = y1 - y2; 29 | 30 | ctx.fillStyle(colors.GRASS); 31 | ctx.fillRect(-10, y2, width, h); 32 | 33 | Renderer.drawPolygon(ctx, x1 - w1 - r1, y1, x1 - w1, y1, x2 - w2, y2, x2 - w2 - r2, y2, colors.RUMBLE); 34 | Renderer.drawPolygon(ctx, x1 + w1 + r1, y1, x1 + w1, y1, x2 + w2, y2, x2 + w2 + r2, y2, colors.RUMBLE); 35 | Renderer.drawPolygon(ctx, x1 - w1, y1, x1 + w1, y1, x2 + w2, y2, x2 - w2, y2, colors.ROAD); 36 | 37 | const lanew1 = w1 * 2 / lanes; 38 | const lanew2 = w2 * 2 / lanes; 39 | let lanex1 = x1 - w1 + lanew1; 40 | let lanex2 = x2 - w2 + lanew2; 41 | 42 | if (colors.LANE) { 43 | for (let lane = 1; lane < lanes; lanex1 += lanew1, lanex2 += lanew2, lane++) { 44 | Renderer.drawPolygon(ctx, lanex1 - l1 / 2, y1, lanex1 + l1 / 2, y1, lanex2 + l2 / 2, y2, lanex2 - l2 / 2, y2, colors.LANE); 45 | } 46 | } 47 | } 48 | 49 | // ------------- 50 | 51 | public scene: GameScene; 52 | public roadGraphics: Phaser.GameObjects.Graphics; 53 | 54 | constructor(scene: GameScene, depth: number = 0) { 55 | this.scene = scene; 56 | this.roadGraphics = this.scene.add.graphics().setDepth(depth); 57 | } 58 | 59 | public update(time: number, delta: number): void { 60 | this.roadGraphics.clear(); 61 | this.drawRoad(); 62 | } 63 | 64 | public drawRoad(): void { 65 | const gameWidth = this.scene.scale.gameSize.width + 20; 66 | const gameHeight = this.scene.scale.gameSize.height + 20; 67 | 68 | const baseSegment = this.scene.road.findSegmentByZ(this.scene.player.trackPosition); 69 | const basePercent = Util.percentRemaining(this.scene.player.trackPosition, gameSettings.segmentLength); 70 | 71 | let maxY = gameHeight; // used for clipping things behind a hill 72 | let roadCenterX = 0; 73 | let deltaX = -(baseSegment.curve * basePercent); 74 | 75 | // draw road front to back 76 | for (let n = 0; n < gameSettings.drawDistance; n++) { 77 | const segmentIndex = (baseSegment.index + n) % this.scene.road.segments.length; 78 | const segment = this.scene.road.segments[segmentIndex]; 79 | 80 | segment.clip = maxY; 81 | segment.looped = segment.index < baseSegment.index; 82 | 83 | Renderer.project(segment.p1, this.scene.player.x * gameSettings.roadWidth - roadCenterX, this.scene.player.y + gameSettings.cameraHeight, 84 | this.scene.player.trackPosition - (segment.looped ? this.scene.road.trackLength : 0), gameSettings.cameraDepth, 85 | gameWidth, gameHeight - gameSettings.projectYCompensation, gameSettings.roadWidth); 86 | 87 | Renderer.project(segment.p2, this.scene.player.x * gameSettings.roadWidth - roadCenterX - deltaX, this.scene.player.y + gameSettings.cameraHeight, 88 | this.scene.player.trackPosition - (segment.looped ? this.scene.road.trackLength : 0), gameSettings.cameraDepth, 89 | gameWidth, gameHeight - gameSettings.projectYCompensation, gameSettings.roadWidth); 90 | 91 | roadCenterX = roadCenterX + deltaX; 92 | deltaX = deltaX + segment.curve; 93 | 94 | if (segment.p1.camera.z <= gameSettings.cameraDepth || segment.p2.screen.y >= maxY || segment.p2.screen.y >= segment.p1.screen.y) { 95 | continue; 96 | } 97 | 98 | Renderer.drawSegment(this.roadGraphics, gameWidth, gameSettings.lanes, 99 | segment.p1.screen.x - 10, segment.p1.screen.y, segment.p1.screen.w, 100 | segment.p2.screen.x - 10, segment.p2.screen.y, segment.p2.screen.w, 101 | segment.colors); 102 | 103 | maxY = segment.p2.screen.y; 104 | } 105 | 106 | // draw props and cars back to front 107 | for (let n = gameSettings.drawDistance; n > 0; n--) { 108 | const segmentIndex = (baseSegment.index + n) % this.scene.road.segments.length; 109 | const segment = this.scene.road.segments[segmentIndex]; 110 | 111 | const scale = segment.p1.screen.scale; 112 | 113 | for (const prop of segment.props) { 114 | const x = segment.p1.screen.x - 10 + (scale * prop.offset * gameSettings.roadWidth * gameWidth / 2); 115 | prop.update(x, segment.p1.screen.y, scale, segment.clip); 116 | } 117 | 118 | for (const car of segment.cars) { 119 | const spriteScale = Util.interpolate(segment.p1.screen.scale, segment.p2.screen.scale, car.percent); 120 | const spriteX = Util.interpolate(segment.p1.screen.x - 10, segment.p2.screen.x - 10, car.percent) + (spriteScale * car.offset * gameSettings.roadWidth * gameWidth / 2); 121 | const spriteY = Util.interpolate(segment.p1.screen.y, segment.p2.screen.y, car.percent); 122 | car.draw(spriteX, spriteY, spriteScale, segment.clip); 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Components/Road.ts: -------------------------------------------------------------------------------- 1 | import { TrackSegment } from './TrackSegment'; 2 | import { gameSettings } from '../config/GameSettings'; 3 | import { Util } from './Util'; 4 | import { SEGMENT } from './SegmentType'; 5 | import { Prop } from './Prop'; 6 | import { GameScene } from '../scenes/GameScene'; 7 | 8 | export class Road { 9 | public scene: GameScene; 10 | public segments: TrackSegment[]; 11 | public trackLength: number; 12 | 13 | constructor(scene: GameScene) { 14 | this.scene = scene; 15 | this.segments = []; 16 | this.trackLength = 0; 17 | } 18 | 19 | public addRoadSegment(curve: number, y: number): void { 20 | this.segments.push(new TrackSegment(this.segments.length, curve, y, this.getLastSegmentYPos())); 21 | } 22 | 23 | public addStraight(num: number = SEGMENT.LENGTH.MEDIUM): void { 24 | this.addRoad(num, num, num, 0, 0); 25 | } 26 | 27 | public addCurve(num: number = SEGMENT.LENGTH.MEDIUM, curve: number = SEGMENT.CURVE.MEDIUM, height: number = SEGMENT.HILL.NONE): void { 28 | this.addRoad(num, num, num, curve, height); 29 | } 30 | 31 | public addHill(num: number = SEGMENT.LENGTH.MEDIUM, height: number = SEGMENT.HILL.NONE): void { 32 | this.addRoad(num, num, num, 0, height); 33 | } 34 | 35 | public addRoad(enter: number, hold: number, leave: number, curve: number, y: number): void { 36 | const startY = this.getLastSegmentYPos(); 37 | const endY = startY + Util.toInt(y, 0) * gameSettings.segmentLength; 38 | const totalLength = enter + hold + leave; 39 | 40 | for (let n = 0; n < enter; n++) { 41 | this.addRoadSegment(Util.easeIn(0, curve, n / enter), Util.easeInOut(startY, endY, n / totalLength)); 42 | } 43 | 44 | for (let n = 0; n < hold; n++) { 45 | this.addRoadSegment(curve, Util.easeInOut(startY, endY, (enter + n) / totalLength)); 46 | } 47 | 48 | for (let n = 0; n < leave; n++) { 49 | this.addRoadSegment(Util.easeInOut(curve, 0, n / leave), Util.easeInOut(startY, endY, (enter + hold + n) / totalLength)); 50 | } 51 | } 52 | 53 | public getLastSegmentYPos(): number { 54 | const lastSegment = this.getLastSegment(); 55 | return lastSegment ? lastSegment.p2.world.y : 0; 56 | } 57 | 58 | public getLastSegment(): TrackSegment { 59 | return this.segments.length > 0 ? this.segments[this.segments.length - 1] : null; 60 | } 61 | 62 | public findSegmentByZ(z: number): TrackSegment { 63 | const index = Math.floor(z / gameSettings.segmentLength) % this.segments.length; 64 | return this.segments[index]; 65 | } 66 | 67 | public resetRoad(): void { 68 | this.segments = []; 69 | 70 | this.addStraight(SEGMENT.LENGTH.MEDIUM); 71 | this.addCurve(SEGMENT.LENGTH.MEDIUM, SEGMENT.CURVE.MEDIUM, SEGMENT.HILL.LOW); 72 | this.addHill(SEGMENT.LENGTH.SHORT, -SEGMENT.HILL.LOW); 73 | this.addHill(SEGMENT.LENGTH.SHORT, SEGMENT.HILL.LOW); 74 | this.addHill(SEGMENT.LENGTH.SHORT, -SEGMENT.HILL.LOW); 75 | this.addCurve(SEGMENT.LENGTH.LONG, SEGMENT.CURVE.MEDIUM, SEGMENT.HILL.MEDIUM); 76 | this.addHill(SEGMENT.LENGTH.SHORT, -SEGMENT.HILL.LOW); 77 | this.addCurve(SEGMENT.LENGTH.LONG, SEGMENT.CURVE.MEDIUM, -SEGMENT.HILL.MEDIUM); 78 | this.addCurve(SEGMENT.LENGTH.LONG, -SEGMENT.CURVE.MINIMAL, SEGMENT.HILL.HIGH); 79 | this.addStraight(SEGMENT.LENGTH.SHORT); 80 | this.addCurve(SEGMENT.LENGTH.VERYLONG, SEGMENT.CURVE.MINIMAL); 81 | this.addHill(SEGMENT.LENGTH.SHORT, -SEGMENT.HILL.MEDIUM); 82 | this.addStraight(SEGMENT.LENGTH.SHORT); 83 | this.addHill(SEGMENT.LENGTH.MEDIUM, SEGMENT.HILL.HIGH); 84 | this.addCurve(SEGMENT.LENGTH.SHORT, SEGMENT.CURVE.MEDIUM, SEGMENT.HILL.LOW); 85 | this.addHill(SEGMENT.LENGTH.LONG, -SEGMENT.HILL.HIGH); 86 | this.addCurve(SEGMENT.LENGTH.LONG, -SEGMENT.CURVE.MEDIUM); 87 | this.addStraight(); 88 | this.addCurve(SEGMENT.LENGTH.LONG, SEGMENT.CURVE.MEDIUM); 89 | this.addStraight(); 90 | this.addCurve(SEGMENT.LENGTH.LONG, -SEGMENT.CURVE.EASY); 91 | this.addHill(SEGMENT.LENGTH.LONG, -SEGMENT.HILL.MEDIUM); 92 | this.addCurve(SEGMENT.LENGTH.LONG, SEGMENT.CURVE.MEDIUM, -SEGMENT.HILL.LOW); 93 | 94 | this.addRoad(200, 200, 200, SEGMENT.CURVE.NONE, Math.round(-this.getLastSegmentYPos() / gameSettings.segmentLength)); 95 | 96 | this.trackLength = this.segments.length * gameSettings.segmentLength; 97 | 98 | this.createRandomProps(); 99 | this.createTurnSigns(); 100 | } 101 | 102 | public addProp(scene: GameScene, segmentIndex: number, name: string, offset: number, height: number = 0, scale: number = 3000, flipX: boolean = false, collides: boolean = false): boolean { 103 | try { 104 | const seg = this.segments[segmentIndex]; 105 | const prop = new Prop(scene, name, offset, height, scale, flipX, collides); 106 | seg.props.add(prop); 107 | 108 | return true; 109 | } catch (e) { 110 | return false; 111 | } 112 | } 113 | 114 | public hideAllProps(): void { 115 | this.segments.forEach( (segment: TrackSegment) => { 116 | for (const prop of segment.props) { 117 | prop.sprite.setVisible(false); 118 | } 119 | }); 120 | } 121 | 122 | // add some road side props 123 | // offsets <-1 & >1 are outside of the road 124 | public createRandomProps(): void { 125 | for (let n = 0; n < this.segments.length; n += Phaser.Math.Between(1, 5)) { 126 | const offset = Phaser.Math.FloatBetween(1.75, 10); 127 | const negated = Math.random() - 0.5 > 0; 128 | 129 | let type; 130 | let scale = 3000; 131 | switch (Phaser.Math.Between(1, 5)) { 132 | case 1: 133 | type = 'boulder1'; 134 | scale = 1500; 135 | break; 136 | case 2: 137 | type = 'boulder2'; 138 | scale = 2000; 139 | break; 140 | case 3: 141 | type = 'tree1'; 142 | scale = 5000; 143 | break; 144 | case 4: 145 | type = 'tree2'; 146 | scale = 5500; 147 | break; 148 | case 5: 149 | type = 'tree3'; 150 | scale = 5000; 151 | break; 152 | } 153 | 154 | this.addProp(this.scene, n, type, negated ? -offset : offset, 0, scale, false); 155 | } 156 | } 157 | 158 | public createTurnSigns(): void { 159 | const signOffset = 1.75; 160 | const signScale = 5000; 161 | 162 | for (let n = 0; n < this.segments.length; n++) { 163 | const segment = this.segments[n]; 164 | const isCurve = Math.abs(segment.curve) > 1; 165 | 166 | if (isCurve) { 167 | if (segment.curve > 0.33) { 168 | this.removeProps(segment, 1, -signOffset); 169 | this.addProp(this.scene, n, 'turnsign', -signOffset, 0, signScale, true, false); 170 | } else { 171 | this.removeProps(segment, 1, signOffset); 172 | this.addProp(this.scene, n, 'turnsign', signOffset, 0, signScale, false, false); 173 | } 174 | 175 | // increment n so we don't put signs too close 176 | n += 10; 177 | } 178 | } 179 | } 180 | 181 | public removeProps(sourceSegment: TrackSegment, breadth: number, side: number) { 182 | const isLeft = side < 0; 183 | const breadthSegments = this.segments.slice(sourceSegment.index - breadth, sourceSegment.index + breadth); 184 | 185 | for (const segment of breadthSegments) { 186 | if (segment.props.size) { 187 | for (const prop of segment.props) { 188 | if ((prop.offset < 0 && isLeft) || (prop.offset > 0 && !isLeft)) { 189 | segment.props.delete(prop); 190 | } 191 | } 192 | } 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/Components/SegmentPoint.ts: -------------------------------------------------------------------------------- 1 | export interface IScreenPoint { 2 | x: number; 3 | y: number; 4 | w: number; 5 | scale: number; 6 | } 7 | 8 | export class SegmentPoint { 9 | public world: Phaser.Math.Vector3; 10 | public camera: Phaser.Math.Vector3; 11 | public screen: IScreenPoint; 12 | 13 | constructor(x: number = 0, y: number = 0, z: number = 0) { 14 | this.world = new Phaser.Math.Vector3(x, y, z); 15 | this.camera = new Phaser.Math.Vector3(0, 0, 0); 16 | this.screen = { x: 0, y: 0, w: 0, scale: 0 }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Components/SegmentType.ts: -------------------------------------------------------------------------------- 1 | export const SEGMENT = { 2 | LENGTH: { 3 | NONE: 0, 4 | SHORT: 25, 5 | MEDIUM: 50, 6 | LONG: 100, 7 | VERYLONG: 200, 8 | }, 9 | CURVE: { 10 | NONE: 0, 11 | MINIMAL: 1, 12 | EASY: 2, 13 | MEDIUM: 4, 14 | HARD: 6, 15 | }, 16 | HILL: { 17 | NONE: 0, 18 | LOW: 20, 19 | MEDIUM: 40, 20 | HIGH: 60, 21 | }, 22 | WIDTH: { 23 | NORMAL: 1, 24 | WIDE: 2, 25 | NARROW: 0.5, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /src/Components/SpeedGauge.ts: -------------------------------------------------------------------------------- 1 | import { RaceUiScene } from '../scenes/RaceUiScene'; 2 | import { Util } from './Util'; 3 | import { gameSettings } from '../config/GameSettings'; 4 | 5 | export class SpeedGauge { 6 | public scene: RaceUiScene; 7 | public speedText: Phaser.GameObjects.BitmapText; 8 | public graphics: Phaser.GameObjects.Graphics; 9 | 10 | private x: number; 11 | private y: number; 12 | private radius: number; 13 | private speedValue: number = 0; 14 | 15 | constructor(scene: RaceUiScene, x: number, y: number, radius: number) { 16 | this.scene = scene; 17 | this.x = x; 18 | this.y = y; 19 | this.radius = radius; 20 | 21 | this.graphics = this.scene.add.graphics(); 22 | this.speedText = this.scene.add.bitmapText(x + 35, y - 15, 'numbers', this.speedValue.toString(), 48).setOrigin(1, 0.5); 23 | this.scene.add.bitmapText(x - 58, y - 8, 'impact', `kmh`, 12).setTint(0xffff00); 24 | 25 | this.update(); 26 | } 27 | 28 | public get speed(): number { 29 | return this.speedValue; 30 | } 31 | 32 | public set speed(value: number) { 33 | this.speedValue = value; 34 | this.update(); 35 | } 36 | 37 | public update(): void { 38 | this.graphics.clear(); 39 | this.drawGauge(); 40 | this.speedText.setText(this.speedValue.toString()); 41 | } 42 | 43 | public destroy(): void { 44 | // 45 | } 46 | 47 | // ------------- 48 | 49 | private drawGauge(): void { 50 | const speedColor = Phaser.Display.Color.HSVToRGB(0.3 - this.speedValue / 600, 1, 1) as Phaser.Display.Color; 51 | 52 | this.graphics.lineStyle(17, 0x555555, 1) 53 | .beginPath() 54 | .arc(this.x, this.y, this.radius, Phaser.Math.DegToRad(180), Phaser.Math.DegToRad(315), false) 55 | .strokePath(); 56 | 57 | this.graphics.lineStyle(12, speedColor.color) 58 | .beginPath() 59 | .arc(this.x, this.y, this.radius, Phaser.Math.DegToRad(183), Phaser.Math.DegToRad(183) + this.speedToAngle(), false) 60 | .strokePath(); 61 | } 62 | 63 | private speedToAngle(): number { 64 | const speedPercentage = this.speedValue > 0 ? (this.speedValue * 1000) / gameSettings.maxSpeed : 0; 65 | const angle = Util.interpolate(0, 130, speedPercentage * .01); 66 | return Phaser.Math.DegToRad(angle); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Components/TrackRadar.ts: -------------------------------------------------------------------------------- 1 | import { RaceUiScene } from '../scenes/RaceUiScene'; 2 | import { gameSettings } from '../config/GameSettings'; 3 | import { Util } from './Util'; 4 | import { RadarCar } from './RadarCar'; 5 | 6 | const centerX = 17; 7 | const playerY = 130; 8 | 9 | export class TrackRadar { 10 | public scene: RaceUiScene; 11 | // public radarText: Phaser.GameObjects.BitmapText; 12 | public graphics: Phaser.GameObjects.Graphics; 13 | public container: Phaser.GameObjects.Container; 14 | public bg: Phaser.GameObjects.Rectangle; 15 | public grid: Phaser.GameObjects.Grid; 16 | 17 | public player: Phaser.GameObjects.Rectangle; 18 | public cars: Set; 19 | 20 | private x: number; 21 | private y: number; 22 | 23 | 24 | constructor(scene: RaceUiScene, x: number, y: number) { 25 | this.scene = scene; 26 | this.x = x; 27 | this.y = y; 28 | 29 | this.cars = new Set(); 30 | 31 | this.container = this.scene.add.container(this.x, this.y); 32 | 33 | this.bg = this.scene.add.rectangle(0, 0, 33, 163, 0xffffff, .75).setOrigin(0, 0); 34 | this.grid = this.scene.add.grid(2, 2, 30, 160, 30 / gameSettings.lanes, 160 / 5, 0x333333, 0.8).setOrigin(0, 0); 35 | 36 | this.graphics = this.scene.add.graphics(); 37 | 38 | this.player = this.scene.add.rectangle(centerX, playerY, 3, 5, 0xffff00); 39 | 40 | this.container.add([this.bg, this.grid, this.player, this.graphics]); 41 | } 42 | 43 | public update(): void { 44 | this.graphics.clear(); 45 | } 46 | 47 | public destroy(): void { 48 | // 49 | } 50 | 51 | public updatePlayerX(value: number): void { 52 | const interpolatedValue = Util.interpolate(2, 17, value + 1); 53 | this.player.x = Phaser.Math.Clamp(interpolatedValue, 4, 30); 54 | } 55 | 56 | public drawCar(offset: number, distance: number) { 57 | const interpolatedOffset = Util.interpolate(1, 16, offset + 1); 58 | const x = Phaser.Math.Clamp(interpolatedOffset, 4, 30); 59 | 60 | const interpolatedDistance = Util.interpolate(0, 156, 1 / (distance * 0.0007)); 61 | const y = Phaser.Math.Clamp(interpolatedDistance, 2, 156); 62 | 63 | this.graphics.fillStyle(0xff0000).fillRect(x, y, 3, 5); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Components/TrackSegment.ts: -------------------------------------------------------------------------------- 1 | import { SegmentPoint } from './SegmentPoint'; 2 | import { gameSettings } from '../config/GameSettings'; 3 | import { DarkColors, LightColors } from './Colors'; 4 | import { Prop } from './Prop'; 5 | import { Car } from './Car'; 6 | 7 | export class TrackSegment { 8 | public index: number; 9 | public p1: SegmentPoint; 10 | public p2: SegmentPoint; 11 | public looped: boolean = false; 12 | public fog: number = 0; 13 | public curve: number; 14 | public colors: any; 15 | public props: Set; 16 | public cars: Set; 17 | public clip: number; 18 | 19 | constructor(z: number, curve: number, y: number, lastY: number) { 20 | this.index = z; 21 | this.p1 = new SegmentPoint(0, lastY, z * gameSettings.segmentLength); 22 | this.p2 = new SegmentPoint(0, y, (z + 1) * gameSettings.segmentLength); 23 | this.colors = Math.floor(z / gameSettings.rumbleLength) % 2 ? DarkColors : LightColors; 24 | this.curve = curve; 25 | this.clip = 0; 26 | 27 | this.props = new Set(); 28 | this.cars = new Set(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Components/Util.ts: -------------------------------------------------------------------------------- 1 | import { Player } from './Player'; 2 | import { Prop } from './Prop'; 3 | import { Car } from './Car'; 4 | 5 | export class Util { 6 | public static rumbleWidth(projectedRoadWidth: number, lanes: number): number { 7 | return projectedRoadWidth / Math.max(6, 2 * lanes); 8 | } 9 | 10 | public static laneMarkerWidth(projectedRoadWidth: number, lanes: number): number { 11 | return projectedRoadWidth / Math.max(32, 8 * lanes); 12 | } 13 | 14 | public static increase(start: number, increment: number, max: number): number { // with looping 15 | let result = start + increment; 16 | while (result >= max) { 17 | result -= max; 18 | } 19 | 20 | while (result < 0) { 21 | result += max; 22 | } 23 | return result; 24 | } 25 | 26 | public static accelerate(current: number, accel: number, delta: number): number { 27 | return current + (accel * delta); 28 | } 29 | 30 | public static interpolate(a: number, b: number, percent: number): number { 31 | return a + (b - a) * percent; 32 | } 33 | 34 | public static easeIn(a: number, b: number, percent: number): number { 35 | return a + (b - a) * Math.pow(percent, 2); 36 | } 37 | 38 | public static easeOut(a: number, b: number, percent: number): number { 39 | return a + (b - a) * (1 - Math.pow(1 - percent, 2)); 40 | } 41 | 42 | public static easeInOut(a: number, b: number, percent: number): number { 43 | return a + (b - a) * ((-Math.cos(percent * Math.PI) / 2) + 0.5); 44 | } 45 | 46 | public static percentRemaining(n: number, total: number): number { 47 | return (n % total) / total; 48 | } 49 | 50 | public static toInt(obj: any, def: any): number { 51 | if (obj !== null) { 52 | const x = parseInt(obj, 10); 53 | if (!isNaN(x)) { 54 | return x; 55 | } 56 | } 57 | 58 | return Util.toInt(def, 0); 59 | } 60 | 61 | public static overlapPlayer(player: Player, target: Prop|Car): boolean { 62 | const playerCenter = player.scene.scale.gameSize.width / 2; 63 | const rect = new Phaser.Geom.Rectangle(); 64 | const overlaps = target.sprite.getBounds(rect).contains(playerCenter - player.collisionRadius, 150) 65 | || target.sprite.getBounds(rect).contains(playerCenter + player.collisionRadius, 150); 66 | 67 | return overlaps; 68 | } 69 | 70 | public static overlapSprite(a: Phaser.GameObjects.Sprite, b: Phaser.GameObjects.Sprite): boolean { 71 | const aBounds = a.getBounds(); 72 | const bBounds = b.getBounds(); 73 | 74 | return Phaser.Geom.Rectangle.Overlaps(aBounds, bBounds); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/config/GameConfig.ts: -------------------------------------------------------------------------------- 1 | import { PasuunaPlugin } from '@pinkkis/phaser-plugin-pasuuna'; 2 | 3 | // phaser game config 4 | export const gameConfig: GameConfig = { 5 | type: Phaser.AUTO, 6 | scale: { 7 | parent: 'game-container', 8 | mode: Phaser.Scale.FIT, 9 | autoCenter: Phaser.Scale.CENTER_BOTH, 10 | width: 320, 11 | height: 180, 12 | }, 13 | render: { 14 | pixelArt: true, 15 | }, 16 | plugins: { 17 | global: [ 18 | { 19 | key: 'PasuunaPlayerPlugin', 20 | plugin: PasuunaPlugin, 21 | start: true, 22 | mapping: 'pasuuna', 23 | }, 24 | ] as any[], 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/config/GameSettings.ts: -------------------------------------------------------------------------------- 1 | class GameSettings { 2 | public roadWidth = 2200; 3 | public roadWidthClamp = 3; 4 | public segmentLength = 200; 5 | public rumbleLength = 6; 6 | public lanes = 3; 7 | public fieldOfView = 110; 8 | public cameraHeight = 2000; 9 | public cameraDepth = 1 / Math.tan( (this.fieldOfView / 2) * Math.PI / 180 ); 10 | public projectYCompensation = 30; 11 | public drawDistance = 500; 12 | public fogDensity = 5; 13 | public maxSpeed = this.segmentLength * 9; 14 | public accel = this.maxSpeed / 50; 15 | public decel = -this.maxSpeed / 70; 16 | public screechDecel = -this.maxSpeed / 100; 17 | public breaking = -this.maxSpeed / 20; 18 | public offRoadDecel = -this.maxSpeed / 10; 19 | public offRoadLimit = this.maxSpeed / 4; 20 | public centrifugal = 0.2; 21 | public steerCompensation = 0.5; 22 | public maxTurn = 1; 23 | public turnResetMultiplier = 0.1; 24 | public cameraAngleResetMultiplier = 0.07; 25 | public totalCars = 30; 26 | } 27 | 28 | export const gameSettings = new GameSettings(); 29 | -------------------------------------------------------------------------------- /src/css/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, body { 6 | margin: 0; 7 | padding: 0; 8 | } 9 | 10 | #game-container { 11 | margin: 0; 12 | padding: 0; 13 | max-height: 100vh; 14 | } 15 | -------------------------------------------------------------------------------- /src/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "worker-loader!*" { 2 | export default class WebpackWorker extends Worker { 3 | constructor(); 4 | } 5 | } 6 | 7 | declare var require: any; 8 | 9 | interface Window { 10 | env?: any; 11 | } 12 | 13 | declare module '@pinkkis/phaser-plugin-pasuuna' { 14 | export class PasuunaPlugin extends Phaser.Plugins.BasePlugin { 15 | loadSongFromCache(key: string, autoplay: boolean): void; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinkkis/phaser-driving/b974b3a14ce06c23337fdddac78ceb8db9f39c47/src/favicon.ico -------------------------------------------------------------------------------- /src/game.ts: -------------------------------------------------------------------------------- 1 | import './libs/GLTFLoader'; 2 | import 'phaser'; 3 | import '@csstools/normalize.css'; 4 | import './css/styles.css'; 5 | import { BootScene } from './scenes/BootScene'; 6 | import { gameConfig } from './config/GameConfig'; 7 | import { LoadScene } from './scenes/LoadScene'; 8 | import { GameScene } from './scenes/GameScene'; 9 | import { RaceUiScene } from './scenes/RaceUiScene'; 10 | 11 | // set up game class, and global stuff 12 | export class PoisonVialGame extends Phaser.Game { 13 | private debug: boolean = false; 14 | 15 | constructor(config: GameConfig) { 16 | super(config); 17 | } 18 | } 19 | 20 | // start the game 21 | window.onload = () => { 22 | const game = new PoisonVialGame(gameConfig); 23 | 24 | // set up stats 25 | if (window.env.buildType !== 'production') { 26 | const Stats = require('stats-js'); 27 | const stats = new Stats(); 28 | stats.setMode(0); // 0: fps, 1: ms 29 | stats.domElement.style.position = 'absolute'; 30 | stats.domElement.style.left = '0px'; 31 | stats.domElement.style.top = '0px'; 32 | document.body.appendChild(stats.domElement); 33 | 34 | game.events.on('prestep', () => stats.begin()); 35 | game.events.on('postrender', () => stats.end()); 36 | } 37 | 38 | game.scene.add('BootScene', BootScene, true); 39 | game.scene.add('LoadScene', LoadScene, false); 40 | game.scene.add('GameScene', GameScene, false); 41 | game.scene.add('RaceUiScene', RaceUiScene, false); 42 | }; 43 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | Phaser Driving 11 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /src/libs/GLTFLoader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Rich Tibbett / https://github.com/richtr 3 | * @author mrdoob / http://mrdoob.com/ 4 | * @author Tony Parisi / http://www.tonyparisi.com/ 5 | * @author Takahiro / https://github.com/takahirox 6 | * @author Don McCurdy / https://www.donmccurdy.com 7 | */ 8 | 9 | THREE.GLTFLoader = ( function () { 10 | 11 | function GLTFLoader( manager ) { 12 | 13 | this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager; 14 | this.dracoLoader = null; 15 | 16 | } 17 | 18 | GLTFLoader.prototype = { 19 | 20 | constructor: GLTFLoader, 21 | 22 | crossOrigin: 'anonymous', 23 | 24 | load: function ( url, onLoad, onProgress, onError ) { 25 | 26 | var scope = this; 27 | 28 | var resourcePath; 29 | 30 | if ( this.resourcePath !== undefined ) { 31 | 32 | resourcePath = this.resourcePath; 33 | 34 | } else if ( this.path !== undefined ) { 35 | 36 | resourcePath = this.path; 37 | 38 | } else { 39 | 40 | resourcePath = THREE.LoaderUtils.extractUrlBase( url ); 41 | 42 | } 43 | 44 | // Tells the LoadingManager to track an extra item, which resolves after 45 | // the model is fully loaded. This means the count of items loaded will 46 | // be incorrect, but ensures manager.onLoad() does not fire early. 47 | scope.manager.itemStart( url ); 48 | 49 | var _onError = function ( e ) { 50 | 51 | if ( onError ) { 52 | 53 | onError( e ); 54 | 55 | } else { 56 | 57 | console.error( e ); 58 | 59 | } 60 | 61 | scope.manager.itemError( url ); 62 | scope.manager.itemEnd( url ); 63 | 64 | }; 65 | 66 | var loader = new THREE.FileLoader( scope.manager ); 67 | 68 | loader.setPath( this.path ); 69 | loader.setResponseType( 'arraybuffer' ); 70 | 71 | loader.load( url, function ( data ) { 72 | 73 | try { 74 | 75 | scope.parse( data, resourcePath, function ( gltf ) { 76 | 77 | onLoad( gltf ); 78 | 79 | scope.manager.itemEnd( url ); 80 | 81 | }, _onError ); 82 | 83 | } catch ( e ) { 84 | 85 | _onError( e ); 86 | 87 | } 88 | 89 | }, onProgress, _onError ); 90 | 91 | }, 92 | 93 | setCrossOrigin: function ( value ) { 94 | 95 | this.crossOrigin = value; 96 | return this; 97 | 98 | }, 99 | 100 | setPath: function ( value ) { 101 | 102 | this.path = value; 103 | return this; 104 | 105 | }, 106 | 107 | setResourcePath: function ( value ) { 108 | 109 | this.resourcePath = value; 110 | return this; 111 | 112 | }, 113 | 114 | setDRACOLoader: function ( dracoLoader ) { 115 | 116 | this.dracoLoader = dracoLoader; 117 | return this; 118 | 119 | }, 120 | 121 | parse: function ( data, path, onLoad, onError ) { 122 | 123 | var content; 124 | var extensions = {}; 125 | 126 | if ( typeof data === 'string' ) { 127 | 128 | content = data; 129 | 130 | } else { 131 | 132 | var magic = THREE.LoaderUtils.decodeText( new Uint8Array( data, 0, 4 ) ); 133 | 134 | if ( magic === BINARY_EXTENSION_HEADER_MAGIC ) { 135 | 136 | try { 137 | 138 | extensions[ EXTENSIONS.KHR_BINARY_GLTF ] = new GLTFBinaryExtension( data ); 139 | 140 | } catch ( error ) { 141 | 142 | if ( onError ) onError( error ); 143 | return; 144 | 145 | } 146 | 147 | content = extensions[ EXTENSIONS.KHR_BINARY_GLTF ].content; 148 | 149 | } else { 150 | 151 | content = THREE.LoaderUtils.decodeText( new Uint8Array( data ) ); 152 | 153 | } 154 | 155 | } 156 | 157 | var json = JSON.parse( content ); 158 | 159 | if ( json.asset === undefined || json.asset.version[ 0 ] < 2 ) { 160 | 161 | if ( onError ) onError( new Error( 'THREE.GLTFLoader: Unsupported asset. glTF versions >=2.0 are supported. Use LegacyGLTFLoader instead.' ) ); 162 | return; 163 | 164 | } 165 | 166 | if ( json.extensionsUsed ) { 167 | 168 | for ( var i = 0; i < json.extensionsUsed.length; ++ i ) { 169 | 170 | var extensionName = json.extensionsUsed[ i ]; 171 | var extensionsRequired = json.extensionsRequired || []; 172 | 173 | switch ( extensionName ) { 174 | 175 | case EXTENSIONS.KHR_LIGHTS_PUNCTUAL: 176 | extensions[ extensionName ] = new GLTFLightsExtension( json ); 177 | break; 178 | 179 | case EXTENSIONS.KHR_MATERIALS_UNLIT: 180 | extensions[ extensionName ] = new GLTFMaterialsUnlitExtension( json ); 181 | break; 182 | 183 | case EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 184 | extensions[ extensionName ] = new GLTFMaterialsPbrSpecularGlossinessExtension( json ); 185 | break; 186 | 187 | case EXTENSIONS.KHR_DRACO_MESH_COMPRESSION: 188 | extensions[ extensionName ] = new GLTFDracoMeshCompressionExtension( json, this.dracoLoader ); 189 | break; 190 | 191 | case EXTENSIONS.MSFT_TEXTURE_DDS: 192 | extensions[ EXTENSIONS.MSFT_TEXTURE_DDS ] = new GLTFTextureDDSExtension(); 193 | break; 194 | 195 | case EXTENSIONS.KHR_TEXTURE_TRANSFORM: 196 | extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] = new GLTFTextureTransformExtension( json ); 197 | break; 198 | 199 | default: 200 | 201 | if ( extensionsRequired.indexOf( extensionName ) >= 0 ) { 202 | 203 | console.warn( 'THREE.GLTFLoader: Unknown extension "' + extensionName + '".' ); 204 | 205 | } 206 | 207 | } 208 | 209 | } 210 | 211 | } 212 | 213 | var parser = new GLTFParser( json, extensions, { 214 | 215 | path: path || this.resourcePath || '', 216 | crossOrigin: this.crossOrigin, 217 | manager: this.manager 218 | 219 | } ); 220 | 221 | parser.parse( onLoad, onError ); 222 | 223 | } 224 | 225 | }; 226 | 227 | /* GLTFREGISTRY */ 228 | 229 | function GLTFRegistry() { 230 | 231 | var objects = {}; 232 | 233 | return { 234 | 235 | get: function ( key ) { 236 | 237 | return objects[ key ]; 238 | 239 | }, 240 | 241 | add: function ( key, object ) { 242 | 243 | objects[ key ] = object; 244 | 245 | }, 246 | 247 | remove: function ( key ) { 248 | 249 | delete objects[ key ]; 250 | 251 | }, 252 | 253 | removeAll: function () { 254 | 255 | objects = {}; 256 | 257 | } 258 | 259 | }; 260 | 261 | } 262 | 263 | /*********************************/ 264 | /********** EXTENSIONS ***********/ 265 | /*********************************/ 266 | 267 | var EXTENSIONS = { 268 | KHR_BINARY_GLTF: 'KHR_binary_glTF', 269 | KHR_DRACO_MESH_COMPRESSION: 'KHR_draco_mesh_compression', 270 | KHR_LIGHTS_PUNCTUAL: 'KHR_lights_punctual', 271 | KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 'KHR_materials_pbrSpecularGlossiness', 272 | KHR_MATERIALS_UNLIT: 'KHR_materials_unlit', 273 | KHR_TEXTURE_TRANSFORM: 'KHR_texture_transform', 274 | MSFT_TEXTURE_DDS: 'MSFT_texture_dds' 275 | }; 276 | 277 | /** 278 | * DDS Texture Extension 279 | * 280 | * Specification: 281 | * https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/MSFT_texture_dds 282 | * 283 | */ 284 | function GLTFTextureDDSExtension() { 285 | 286 | if ( ! THREE.DDSLoader ) { 287 | 288 | throw new Error( 'THREE.GLTFLoader: Attempting to load .dds texture without importing THREE.DDSLoader' ); 289 | 290 | } 291 | 292 | this.name = EXTENSIONS.MSFT_TEXTURE_DDS; 293 | this.ddsLoader = new THREE.DDSLoader(); 294 | 295 | } 296 | 297 | /** 298 | * Lights Extension 299 | * 300 | * Specification: PENDING 301 | */ 302 | function GLTFLightsExtension( json ) { 303 | 304 | this.name = EXTENSIONS.KHR_LIGHTS_PUNCTUAL; 305 | 306 | var extension = ( json.extensions && json.extensions[ EXTENSIONS.KHR_LIGHTS_PUNCTUAL ] ) || {}; 307 | this.lightDefs = extension.lights || []; 308 | 309 | } 310 | 311 | GLTFLightsExtension.prototype.loadLight = function ( lightIndex ) { 312 | 313 | var lightDef = this.lightDefs[ lightIndex ]; 314 | var lightNode; 315 | 316 | var color = new THREE.Color( 0xffffff ); 317 | if ( lightDef.color !== undefined ) color.fromArray( lightDef.color ); 318 | 319 | var range = lightDef.range !== undefined ? lightDef.range : 0; 320 | 321 | switch ( lightDef.type ) { 322 | 323 | case 'directional': 324 | lightNode = new THREE.DirectionalLight( color ); 325 | lightNode.target.position.set( 0, 0, - 1 ); 326 | lightNode.add( lightNode.target ); 327 | break; 328 | 329 | case 'point': 330 | lightNode = new THREE.PointLight( color ); 331 | lightNode.distance = range; 332 | break; 333 | 334 | case 'spot': 335 | lightNode = new THREE.SpotLight( color ); 336 | lightNode.distance = range; 337 | // Handle spotlight properties. 338 | lightDef.spot = lightDef.spot || {}; 339 | lightDef.spot.innerConeAngle = lightDef.spot.innerConeAngle !== undefined ? lightDef.spot.innerConeAngle : 0; 340 | lightDef.spot.outerConeAngle = lightDef.spot.outerConeAngle !== undefined ? lightDef.spot.outerConeAngle : Math.PI / 4.0; 341 | lightNode.angle = lightDef.spot.outerConeAngle; 342 | lightNode.penumbra = 1.0 - lightDef.spot.innerConeAngle / lightDef.spot.outerConeAngle; 343 | lightNode.target.position.set( 0, 0, - 1 ); 344 | lightNode.add( lightNode.target ); 345 | break; 346 | 347 | default: 348 | throw new Error( 'THREE.GLTFLoader: Unexpected light type, "' + lightDef.type + '".' ); 349 | 350 | } 351 | 352 | // Some lights (e.g. spot) default to a position other than the origin. Reset the position 353 | // here, because node-level parsing will only override position if explicitly specified. 354 | lightNode.position.set( 0, 0, 0 ); 355 | 356 | lightNode.decay = 2; 357 | 358 | if ( lightDef.intensity !== undefined ) lightNode.intensity = lightDef.intensity; 359 | 360 | lightNode.name = lightDef.name || ( 'light_' + lightIndex ); 361 | 362 | return Promise.resolve( lightNode ); 363 | 364 | }; 365 | 366 | /** 367 | * Unlit Materials Extension (pending) 368 | * 369 | * PR: https://github.com/KhronosGroup/glTF/pull/1163 370 | */ 371 | function GLTFMaterialsUnlitExtension( json ) { 372 | 373 | this.name = EXTENSIONS.KHR_MATERIALS_UNLIT; 374 | 375 | } 376 | 377 | GLTFMaterialsUnlitExtension.prototype.getMaterialType = function ( material ) { 378 | 379 | return THREE.MeshBasicMaterial; 380 | 381 | }; 382 | 383 | GLTFMaterialsUnlitExtension.prototype.extendParams = function ( materialParams, material, parser ) { 384 | 385 | var pending = []; 386 | 387 | materialParams.color = new THREE.Color( 1.0, 1.0, 1.0 ); 388 | materialParams.opacity = 1.0; 389 | 390 | var metallicRoughness = material.pbrMetallicRoughness; 391 | 392 | if ( metallicRoughness ) { 393 | 394 | if ( Array.isArray( metallicRoughness.baseColorFactor ) ) { 395 | 396 | var array = metallicRoughness.baseColorFactor; 397 | 398 | materialParams.color.fromArray( array ); 399 | materialParams.opacity = array[ 3 ]; 400 | 401 | } 402 | 403 | if ( metallicRoughness.baseColorTexture !== undefined ) { 404 | 405 | pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture ) ); 406 | 407 | } 408 | 409 | } 410 | 411 | return Promise.all( pending ); 412 | 413 | }; 414 | 415 | /* BINARY EXTENSION */ 416 | 417 | var BINARY_EXTENSION_BUFFER_NAME = 'binary_glTF'; 418 | var BINARY_EXTENSION_HEADER_MAGIC = 'glTF'; 419 | var BINARY_EXTENSION_HEADER_LENGTH = 12; 420 | var BINARY_EXTENSION_CHUNK_TYPES = { JSON: 0x4E4F534A, BIN: 0x004E4942 }; 421 | 422 | function GLTFBinaryExtension( data ) { 423 | 424 | this.name = EXTENSIONS.KHR_BINARY_GLTF; 425 | this.content = null; 426 | this.body = null; 427 | 428 | var headerView = new DataView( data, 0, BINARY_EXTENSION_HEADER_LENGTH ); 429 | 430 | this.header = { 431 | magic: THREE.LoaderUtils.decodeText( new Uint8Array( data.slice( 0, 4 ) ) ), 432 | version: headerView.getUint32( 4, true ), 433 | length: headerView.getUint32( 8, true ) 434 | }; 435 | 436 | if ( this.header.magic !== BINARY_EXTENSION_HEADER_MAGIC ) { 437 | 438 | throw new Error( 'THREE.GLTFLoader: Unsupported glTF-Binary header.' ); 439 | 440 | } else if ( this.header.version < 2.0 ) { 441 | 442 | throw new Error( 'THREE.GLTFLoader: Legacy binary file detected. Use LegacyGLTFLoader instead.' ); 443 | 444 | } 445 | 446 | var chunkView = new DataView( data, BINARY_EXTENSION_HEADER_LENGTH ); 447 | var chunkIndex = 0; 448 | 449 | while ( chunkIndex < chunkView.byteLength ) { 450 | 451 | var chunkLength = chunkView.getUint32( chunkIndex, true ); 452 | chunkIndex += 4; 453 | 454 | var chunkType = chunkView.getUint32( chunkIndex, true ); 455 | chunkIndex += 4; 456 | 457 | if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.JSON ) { 458 | 459 | var contentArray = new Uint8Array( data, BINARY_EXTENSION_HEADER_LENGTH + chunkIndex, chunkLength ); 460 | this.content = THREE.LoaderUtils.decodeText( contentArray ); 461 | 462 | } else if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.BIN ) { 463 | 464 | var byteOffset = BINARY_EXTENSION_HEADER_LENGTH + chunkIndex; 465 | this.body = data.slice( byteOffset, byteOffset + chunkLength ); 466 | 467 | } 468 | 469 | // Clients must ignore chunks with unknown types. 470 | 471 | chunkIndex += chunkLength; 472 | 473 | } 474 | 475 | if ( this.content === null ) { 476 | 477 | throw new Error( 'THREE.GLTFLoader: JSON content not found.' ); 478 | 479 | } 480 | 481 | } 482 | 483 | /** 484 | * DRACO Mesh Compression Extension 485 | * 486 | * Specification: https://github.com/KhronosGroup/glTF/pull/874 487 | */ 488 | function GLTFDracoMeshCompressionExtension( json, dracoLoader ) { 489 | 490 | if ( ! dracoLoader ) { 491 | 492 | throw new Error( 'THREE.GLTFLoader: No DRACOLoader instance provided.' ); 493 | 494 | } 495 | 496 | this.name = EXTENSIONS.KHR_DRACO_MESH_COMPRESSION; 497 | this.json = json; 498 | this.dracoLoader = dracoLoader; 499 | 500 | } 501 | 502 | GLTFDracoMeshCompressionExtension.prototype.decodePrimitive = function ( primitive, parser ) { 503 | 504 | var json = this.json; 505 | var dracoLoader = this.dracoLoader; 506 | var bufferViewIndex = primitive.extensions[ this.name ].bufferView; 507 | var gltfAttributeMap = primitive.extensions[ this.name ].attributes; 508 | var threeAttributeMap = {}; 509 | var attributeNormalizedMap = {}; 510 | var attributeTypeMap = {}; 511 | 512 | for ( var attributeName in gltfAttributeMap ) { 513 | 514 | var threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase(); 515 | 516 | threeAttributeMap[ threeAttributeName ] = gltfAttributeMap[ attributeName ]; 517 | 518 | } 519 | 520 | for ( attributeName in primitive.attributes ) { 521 | 522 | var threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase(); 523 | 524 | if ( gltfAttributeMap[ attributeName ] !== undefined ) { 525 | 526 | var accessorDef = json.accessors[ primitive.attributes[ attributeName ] ]; 527 | var componentType = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ]; 528 | 529 | attributeTypeMap[ threeAttributeName ] = componentType; 530 | attributeNormalizedMap[ threeAttributeName ] = accessorDef.normalized === true; 531 | 532 | } 533 | 534 | } 535 | 536 | return parser.getDependency( 'bufferView', bufferViewIndex ).then( function ( bufferView ) { 537 | 538 | return new Promise( function ( resolve ) { 539 | 540 | dracoLoader.decodeDracoFile( bufferView, function ( geometry ) { 541 | 542 | for ( var attributeName in geometry.attributes ) { 543 | 544 | var attribute = geometry.attributes[ attributeName ]; 545 | var normalized = attributeNormalizedMap[ attributeName ]; 546 | 547 | if ( normalized !== undefined ) attribute.normalized = normalized; 548 | 549 | } 550 | 551 | resolve( geometry ); 552 | 553 | }, threeAttributeMap, attributeTypeMap ); 554 | 555 | } ); 556 | 557 | } ); 558 | 559 | }; 560 | 561 | /** 562 | * Texture Transform Extension 563 | * 564 | * Specification: 565 | */ 566 | function GLTFTextureTransformExtension( json ) { 567 | 568 | this.name = EXTENSIONS.KHR_TEXTURE_TRANSFORM; 569 | 570 | } 571 | 572 | GLTFTextureTransformExtension.prototype.extendTexture = function ( texture, transform ) { 573 | 574 | texture = texture.clone(); 575 | 576 | if ( transform.offset !== undefined ) { 577 | 578 | texture.offset.fromArray( transform.offset ); 579 | 580 | } 581 | 582 | if ( transform.rotation !== undefined ) { 583 | 584 | texture.rotation = transform.rotation; 585 | 586 | } 587 | 588 | if ( transform.scale !== undefined ) { 589 | 590 | texture.repeat.fromArray( transform.scale ); 591 | 592 | } 593 | 594 | if ( transform.texCoord !== undefined ) { 595 | 596 | console.warn( 'THREE.GLTFLoader: Custom UV sets in "' + this.name + '" extension not yet supported.' ); 597 | 598 | } 599 | 600 | texture.needsUpdate = true; 601 | 602 | return texture; 603 | 604 | }; 605 | 606 | /** 607 | * Specular-Glossiness Extension 608 | * 609 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness 610 | */ 611 | function GLTFMaterialsPbrSpecularGlossinessExtension() { 612 | 613 | return { 614 | 615 | name: EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS, 616 | 617 | specularGlossinessParams: [ 618 | 'color', 619 | 'map', 620 | 'lightMap', 621 | 'lightMapIntensity', 622 | 'aoMap', 623 | 'aoMapIntensity', 624 | 'emissive', 625 | 'emissiveIntensity', 626 | 'emissiveMap', 627 | 'bumpMap', 628 | 'bumpScale', 629 | 'normalMap', 630 | 'displacementMap', 631 | 'displacementScale', 632 | 'displacementBias', 633 | 'specularMap', 634 | 'specular', 635 | 'glossinessMap', 636 | 'glossiness', 637 | 'alphaMap', 638 | 'envMap', 639 | 'envMapIntensity', 640 | 'refractionRatio', 641 | ], 642 | 643 | getMaterialType: function () { 644 | 645 | return THREE.ShaderMaterial; 646 | 647 | }, 648 | 649 | extendParams: function ( params, material, parser ) { 650 | 651 | var pbrSpecularGlossiness = material.extensions[ this.name ]; 652 | 653 | var shader = THREE.ShaderLib[ 'standard' ]; 654 | 655 | var uniforms = THREE.UniformsUtils.clone( shader.uniforms ); 656 | 657 | var specularMapParsFragmentChunk = [ 658 | '#ifdef USE_SPECULARMAP', 659 | ' uniform sampler2D specularMap;', 660 | '#endif' 661 | ].join( '\n' ); 662 | 663 | var glossinessMapParsFragmentChunk = [ 664 | '#ifdef USE_GLOSSINESSMAP', 665 | ' uniform sampler2D glossinessMap;', 666 | '#endif' 667 | ].join( '\n' ); 668 | 669 | var specularMapFragmentChunk = [ 670 | 'vec3 specularFactor = specular;', 671 | '#ifdef USE_SPECULARMAP', 672 | ' vec4 texelSpecular = texture2D( specularMap, vUv );', 673 | ' texelSpecular = sRGBToLinear( texelSpecular );', 674 | ' // reads channel RGB, compatible with a glTF Specular-Glossiness (RGBA) texture', 675 | ' specularFactor *= texelSpecular.rgb;', 676 | '#endif' 677 | ].join( '\n' ); 678 | 679 | var glossinessMapFragmentChunk = [ 680 | 'float glossinessFactor = glossiness;', 681 | '#ifdef USE_GLOSSINESSMAP', 682 | ' vec4 texelGlossiness = texture2D( glossinessMap, vUv );', 683 | ' // reads channel A, compatible with a glTF Specular-Glossiness (RGBA) texture', 684 | ' glossinessFactor *= texelGlossiness.a;', 685 | '#endif' 686 | ].join( '\n' ); 687 | 688 | var lightPhysicalFragmentChunk = [ 689 | 'PhysicalMaterial material;', 690 | 'material.diffuseColor = diffuseColor.rgb;', 691 | 'material.specularRoughness = clamp( 1.0 - glossinessFactor, 0.04, 1.0 );', 692 | 'material.specularColor = specularFactor.rgb;', 693 | ].join( '\n' ); 694 | 695 | var fragmentShader = shader.fragmentShader 696 | .replace( 'uniform float roughness;', 'uniform vec3 specular;' ) 697 | .replace( 'uniform float metalness;', 'uniform float glossiness;' ) 698 | .replace( '#include ', specularMapParsFragmentChunk ) 699 | .replace( '#include ', glossinessMapParsFragmentChunk ) 700 | .replace( '#include ', specularMapFragmentChunk ) 701 | .replace( '#include ', glossinessMapFragmentChunk ) 702 | .replace( '#include ', lightPhysicalFragmentChunk ); 703 | 704 | delete uniforms.roughness; 705 | delete uniforms.metalness; 706 | delete uniforms.roughnessMap; 707 | delete uniforms.metalnessMap; 708 | 709 | uniforms.specular = { value: new THREE.Color().setHex( 0x111111 ) }; 710 | uniforms.glossiness = { value: 0.5 }; 711 | uniforms.specularMap = { value: null }; 712 | uniforms.glossinessMap = { value: null }; 713 | 714 | params.vertexShader = shader.vertexShader; 715 | params.fragmentShader = fragmentShader; 716 | params.uniforms = uniforms; 717 | params.defines = { 'STANDARD': '' }; 718 | 719 | params.color = new THREE.Color( 1.0, 1.0, 1.0 ); 720 | params.opacity = 1.0; 721 | 722 | var pending = []; 723 | 724 | if ( Array.isArray( pbrSpecularGlossiness.diffuseFactor ) ) { 725 | 726 | var array = pbrSpecularGlossiness.diffuseFactor; 727 | 728 | params.color.fromArray( array ); 729 | params.opacity = array[ 3 ]; 730 | 731 | } 732 | 733 | if ( pbrSpecularGlossiness.diffuseTexture !== undefined ) { 734 | 735 | pending.push( parser.assignTexture( params, 'map', pbrSpecularGlossiness.diffuseTexture ) ); 736 | 737 | } 738 | 739 | params.emissive = new THREE.Color( 0.0, 0.0, 0.0 ); 740 | params.glossiness = pbrSpecularGlossiness.glossinessFactor !== undefined ? pbrSpecularGlossiness.glossinessFactor : 1.0; 741 | params.specular = new THREE.Color( 1.0, 1.0, 1.0 ); 742 | 743 | if ( Array.isArray( pbrSpecularGlossiness.specularFactor ) ) { 744 | 745 | params.specular.fromArray( pbrSpecularGlossiness.specularFactor ); 746 | 747 | } 748 | 749 | if ( pbrSpecularGlossiness.specularGlossinessTexture !== undefined ) { 750 | 751 | var specGlossMapDef = pbrSpecularGlossiness.specularGlossinessTexture; 752 | pending.push( parser.assignTexture( params, 'glossinessMap', specGlossMapDef ) ); 753 | pending.push( parser.assignTexture( params, 'specularMap', specGlossMapDef ) ); 754 | 755 | } 756 | 757 | return Promise.all( pending ); 758 | 759 | }, 760 | 761 | createMaterial: function ( params ) { 762 | 763 | // setup material properties based on MeshStandardMaterial for Specular-Glossiness 764 | 765 | var material = new THREE.ShaderMaterial( { 766 | defines: params.defines, 767 | vertexShader: params.vertexShader, 768 | fragmentShader: params.fragmentShader, 769 | uniforms: params.uniforms, 770 | fog: true, 771 | lights: true, 772 | opacity: params.opacity, 773 | transparent: params.transparent 774 | } ); 775 | 776 | material.isGLTFSpecularGlossinessMaterial = true; 777 | 778 | material.color = params.color; 779 | 780 | material.map = params.map === undefined ? null : params.map; 781 | 782 | material.lightMap = null; 783 | material.lightMapIntensity = 1.0; 784 | 785 | material.aoMap = params.aoMap === undefined ? null : params.aoMap; 786 | material.aoMapIntensity = 1.0; 787 | 788 | material.emissive = params.emissive; 789 | material.emissiveIntensity = 1.0; 790 | material.emissiveMap = params.emissiveMap === undefined ? null : params.emissiveMap; 791 | 792 | material.bumpMap = params.bumpMap === undefined ? null : params.bumpMap; 793 | material.bumpScale = 1; 794 | 795 | material.normalMap = params.normalMap === undefined ? null : params.normalMap; 796 | if ( params.normalScale ) material.normalScale = params.normalScale; 797 | 798 | material.displacementMap = null; 799 | material.displacementScale = 1; 800 | material.displacementBias = 0; 801 | 802 | material.specularMap = params.specularMap === undefined ? null : params.specularMap; 803 | material.specular = params.specular; 804 | 805 | material.glossinessMap = params.glossinessMap === undefined ? null : params.glossinessMap; 806 | material.glossiness = params.glossiness; 807 | 808 | material.alphaMap = null; 809 | 810 | material.envMap = params.envMap === undefined ? null : params.envMap; 811 | material.envMapIntensity = 1.0; 812 | 813 | material.refractionRatio = 0.98; 814 | 815 | material.extensions.derivatives = true; 816 | 817 | return material; 818 | 819 | }, 820 | 821 | /** 822 | * Clones a GLTFSpecularGlossinessMaterial instance. The ShaderMaterial.copy() method can 823 | * copy only properties it knows about or inherits, and misses many properties that would 824 | * normally be defined by MeshStandardMaterial. 825 | * 826 | * This method allows GLTFSpecularGlossinessMaterials to be cloned in the process of 827 | * loading a glTF model, but cloning later (e.g. by the user) would require these changes 828 | * AND also updating `.onBeforeRender` on the parent mesh. 829 | * 830 | * @param {THREE.ShaderMaterial} source 831 | * @return {THREE.ShaderMaterial} 832 | */ 833 | cloneMaterial: function ( source ) { 834 | 835 | var target = source.clone(); 836 | 837 | target.isGLTFSpecularGlossinessMaterial = true; 838 | 839 | var params = this.specularGlossinessParams; 840 | 841 | for ( var i = 0, il = params.length; i < il; i ++ ) { 842 | 843 | target[ params[ i ] ] = source[ params[ i ] ]; 844 | 845 | } 846 | 847 | return target; 848 | 849 | }, 850 | 851 | // Here's based on refreshUniformsCommon() and refreshUniformsStandard() in WebGLRenderer. 852 | refreshUniforms: function ( renderer, scene, camera, geometry, material, group ) { 853 | 854 | if ( material.isGLTFSpecularGlossinessMaterial !== true ) { 855 | 856 | return; 857 | 858 | } 859 | 860 | var uniforms = material.uniforms; 861 | var defines = material.defines; 862 | 863 | uniforms.opacity.value = material.opacity; 864 | 865 | uniforms.diffuse.value.copy( material.color ); 866 | uniforms.emissive.value.copy( material.emissive ).multiplyScalar( material.emissiveIntensity ); 867 | 868 | uniforms.map.value = material.map; 869 | uniforms.specularMap.value = material.specularMap; 870 | uniforms.alphaMap.value = material.alphaMap; 871 | 872 | uniforms.lightMap.value = material.lightMap; 873 | uniforms.lightMapIntensity.value = material.lightMapIntensity; 874 | 875 | uniforms.aoMap.value = material.aoMap; 876 | uniforms.aoMapIntensity.value = material.aoMapIntensity; 877 | 878 | // uv repeat and offset setting priorities 879 | // 1. color map 880 | // 2. specular map 881 | // 3. normal map 882 | // 4. bump map 883 | // 5. alpha map 884 | // 6. emissive map 885 | 886 | var uvScaleMap; 887 | 888 | if ( material.map ) { 889 | 890 | uvScaleMap = material.map; 891 | 892 | } else if ( material.specularMap ) { 893 | 894 | uvScaleMap = material.specularMap; 895 | 896 | } else if ( material.displacementMap ) { 897 | 898 | uvScaleMap = material.displacementMap; 899 | 900 | } else if ( material.normalMap ) { 901 | 902 | uvScaleMap = material.normalMap; 903 | 904 | } else if ( material.bumpMap ) { 905 | 906 | uvScaleMap = material.bumpMap; 907 | 908 | } else if ( material.glossinessMap ) { 909 | 910 | uvScaleMap = material.glossinessMap; 911 | 912 | } else if ( material.alphaMap ) { 913 | 914 | uvScaleMap = material.alphaMap; 915 | 916 | } else if ( material.emissiveMap ) { 917 | 918 | uvScaleMap = material.emissiveMap; 919 | 920 | } 921 | 922 | if ( uvScaleMap !== undefined ) { 923 | 924 | // backwards compatibility 925 | if ( uvScaleMap.isWebGLRenderTarget ) { 926 | 927 | uvScaleMap = uvScaleMap.texture; 928 | 929 | } 930 | 931 | if ( uvScaleMap.matrixAutoUpdate === true ) { 932 | 933 | uvScaleMap.updateMatrix(); 934 | 935 | } 936 | 937 | uniforms.uvTransform.value.copy( uvScaleMap.matrix ); 938 | 939 | } 940 | 941 | if ( material.envMap ) { 942 | 943 | uniforms.envMap.value = material.envMap; 944 | uniforms.envMapIntensity.value = material.envMapIntensity; 945 | 946 | // don't flip CubeTexture envMaps, flip everything else: 947 | // WebGLRenderTargetCube will be flipped for backwards compatibility 948 | // WebGLRenderTargetCube.texture will be flipped because it's a Texture and NOT a CubeTexture 949 | // this check must be handled differently, or removed entirely, if WebGLRenderTargetCube uses a CubeTexture in the future 950 | uniforms.flipEnvMap.value = material.envMap.isCubeTexture ? - 1 : 1; 951 | 952 | uniforms.reflectivity.value = material.reflectivity; 953 | uniforms.refractionRatio.value = material.refractionRatio; 954 | 955 | uniforms.maxMipLevel.value = renderer.properties.get( material.envMap ).__maxMipLevel; 956 | 957 | } 958 | 959 | uniforms.specular.value.copy( material.specular ); 960 | uniforms.glossiness.value = material.glossiness; 961 | 962 | uniforms.glossinessMap.value = material.glossinessMap; 963 | 964 | uniforms.emissiveMap.value = material.emissiveMap; 965 | uniforms.bumpMap.value = material.bumpMap; 966 | uniforms.normalMap.value = material.normalMap; 967 | 968 | uniforms.displacementMap.value = material.displacementMap; 969 | uniforms.displacementScale.value = material.displacementScale; 970 | uniforms.displacementBias.value = material.displacementBias; 971 | 972 | if ( uniforms.glossinessMap.value !== null && defines.USE_GLOSSINESSMAP === undefined ) { 973 | 974 | defines.USE_GLOSSINESSMAP = ''; 975 | // set USE_ROUGHNESSMAP to enable vUv 976 | defines.USE_ROUGHNESSMAP = ''; 977 | 978 | } 979 | 980 | if ( uniforms.glossinessMap.value === null && defines.USE_GLOSSINESSMAP !== undefined ) { 981 | 982 | delete defines.USE_GLOSSINESSMAP; 983 | delete defines.USE_ROUGHNESSMAP; 984 | 985 | } 986 | 987 | } 988 | 989 | }; 990 | 991 | } 992 | 993 | /*********************************/ 994 | /********** INTERPOLATION ********/ 995 | /*********************************/ 996 | 997 | // Spline Interpolation 998 | // Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#appendix-c-spline-interpolation 999 | function GLTFCubicSplineInterpolant( parameterPositions, sampleValues, sampleSize, resultBuffer ) { 1000 | 1001 | THREE.Interpolant.call( this, parameterPositions, sampleValues, sampleSize, resultBuffer ); 1002 | 1003 | } 1004 | 1005 | GLTFCubicSplineInterpolant.prototype = Object.create( THREE.Interpolant.prototype ); 1006 | GLTFCubicSplineInterpolant.prototype.constructor = GLTFCubicSplineInterpolant; 1007 | 1008 | GLTFCubicSplineInterpolant.prototype.copySampleValue_ = function ( index ) { 1009 | 1010 | // Copies a sample value to the result buffer. See description of glTF 1011 | // CUBICSPLINE values layout in interpolate_() function below. 1012 | 1013 | var result = this.resultBuffer, 1014 | values = this.sampleValues, 1015 | valueSize = this.valueSize, 1016 | offset = index * valueSize * 3 + valueSize; 1017 | 1018 | for ( var i = 0; i !== valueSize; i ++ ) { 1019 | 1020 | result[ i ] = values[ offset + i ]; 1021 | 1022 | } 1023 | 1024 | return result; 1025 | 1026 | }; 1027 | 1028 | GLTFCubicSplineInterpolant.prototype.beforeStart_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_; 1029 | 1030 | GLTFCubicSplineInterpolant.prototype.afterEnd_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_; 1031 | 1032 | GLTFCubicSplineInterpolant.prototype.interpolate_ = function ( i1, t0, t, t1 ) { 1033 | 1034 | var result = this.resultBuffer; 1035 | var values = this.sampleValues; 1036 | var stride = this.valueSize; 1037 | 1038 | var stride2 = stride * 2; 1039 | var stride3 = stride * 3; 1040 | 1041 | var td = t1 - t0; 1042 | 1043 | var p = ( t - t0 ) / td; 1044 | var pp = p * p; 1045 | var ppp = pp * p; 1046 | 1047 | var offset1 = i1 * stride3; 1048 | var offset0 = offset1 - stride3; 1049 | 1050 | var s2 = - 2 * ppp + 3 * pp; 1051 | var s3 = ppp - pp; 1052 | var s0 = 1 - s2; 1053 | var s1 = s3 - pp + p; 1054 | 1055 | // Layout of keyframe output values for CUBICSPLINE animations: 1056 | // [ inTangent_1, splineVertex_1, outTangent_1, inTangent_2, splineVertex_2, ... ] 1057 | for ( var i = 0; i !== stride; i ++ ) { 1058 | 1059 | var p0 = values[ offset0 + i + stride ]; // splineVertex_k 1060 | var m0 = values[ offset0 + i + stride2 ] * td; // outTangent_k * (t_k+1 - t_k) 1061 | var p1 = values[ offset1 + i + stride ]; // splineVertex_k+1 1062 | var m1 = values[ offset1 + i ] * td; // inTangent_k+1 * (t_k+1 - t_k) 1063 | 1064 | result[ i ] = s0 * p0 + s1 * m0 + s2 * p1 + s3 * m1; 1065 | 1066 | } 1067 | 1068 | return result; 1069 | 1070 | }; 1071 | 1072 | /*********************************/ 1073 | /********** INTERNALS ************/ 1074 | /*********************************/ 1075 | 1076 | /* CONSTANTS */ 1077 | 1078 | var WEBGL_CONSTANTS = { 1079 | FLOAT: 5126, 1080 | //FLOAT_MAT2: 35674, 1081 | FLOAT_MAT3: 35675, 1082 | FLOAT_MAT4: 35676, 1083 | FLOAT_VEC2: 35664, 1084 | FLOAT_VEC3: 35665, 1085 | FLOAT_VEC4: 35666, 1086 | LINEAR: 9729, 1087 | REPEAT: 10497, 1088 | SAMPLER_2D: 35678, 1089 | POINTS: 0, 1090 | LINES: 1, 1091 | LINE_LOOP: 2, 1092 | LINE_STRIP: 3, 1093 | TRIANGLES: 4, 1094 | TRIANGLE_STRIP: 5, 1095 | TRIANGLE_FAN: 6, 1096 | UNSIGNED_BYTE: 5121, 1097 | UNSIGNED_SHORT: 5123 1098 | }; 1099 | 1100 | var WEBGL_TYPE = { 1101 | 5126: Number, 1102 | //35674: THREE.Matrix2, 1103 | 35675: THREE.Matrix3, 1104 | 35676: THREE.Matrix4, 1105 | 35664: THREE.Vector2, 1106 | 35665: THREE.Vector3, 1107 | 35666: THREE.Vector4, 1108 | 35678: THREE.Texture 1109 | }; 1110 | 1111 | var WEBGL_COMPONENT_TYPES = { 1112 | 5120: Int8Array, 1113 | 5121: Uint8Array, 1114 | 5122: Int16Array, 1115 | 5123: Uint16Array, 1116 | 5125: Uint32Array, 1117 | 5126: Float32Array 1118 | }; 1119 | 1120 | var WEBGL_FILTERS = { 1121 | 9728: THREE.NearestFilter, 1122 | 9729: THREE.LinearFilter, 1123 | 9984: THREE.NearestMipMapNearestFilter, 1124 | 9985: THREE.LinearMipMapNearestFilter, 1125 | 9986: THREE.NearestMipMapLinearFilter, 1126 | 9987: THREE.LinearMipMapLinearFilter 1127 | }; 1128 | 1129 | var WEBGL_WRAPPINGS = { 1130 | 33071: THREE.ClampToEdgeWrapping, 1131 | 33648: THREE.MirroredRepeatWrapping, 1132 | 10497: THREE.RepeatWrapping 1133 | }; 1134 | 1135 | var WEBGL_SIDES = { 1136 | 1028: THREE.BackSide, // Culling front 1137 | 1029: THREE.FrontSide // Culling back 1138 | //1032: THREE.NoSide // Culling front and back, what to do? 1139 | }; 1140 | 1141 | var WEBGL_DEPTH_FUNCS = { 1142 | 512: THREE.NeverDepth, 1143 | 513: THREE.LessDepth, 1144 | 514: THREE.EqualDepth, 1145 | 515: THREE.LessEqualDepth, 1146 | 516: THREE.GreaterEqualDepth, 1147 | 517: THREE.NotEqualDepth, 1148 | 518: THREE.GreaterEqualDepth, 1149 | 519: THREE.AlwaysDepth 1150 | }; 1151 | 1152 | var WEBGL_BLEND_EQUATIONS = { 1153 | 32774: THREE.AddEquation, 1154 | 32778: THREE.SubtractEquation, 1155 | 32779: THREE.ReverseSubtractEquation 1156 | }; 1157 | 1158 | var WEBGL_BLEND_FUNCS = { 1159 | 0: THREE.ZeroFactor, 1160 | 1: THREE.OneFactor, 1161 | 768: THREE.SrcColorFactor, 1162 | 769: THREE.OneMinusSrcColorFactor, 1163 | 770: THREE.SrcAlphaFactor, 1164 | 771: THREE.OneMinusSrcAlphaFactor, 1165 | 772: THREE.DstAlphaFactor, 1166 | 773: THREE.OneMinusDstAlphaFactor, 1167 | 774: THREE.DstColorFactor, 1168 | 775: THREE.OneMinusDstColorFactor, 1169 | 776: THREE.SrcAlphaSaturateFactor 1170 | // The followings are not supported by Three.js yet 1171 | //32769: CONSTANT_COLOR, 1172 | //32770: ONE_MINUS_CONSTANT_COLOR, 1173 | //32771: CONSTANT_ALPHA, 1174 | //32772: ONE_MINUS_CONSTANT_COLOR 1175 | }; 1176 | 1177 | var WEBGL_TYPE_SIZES = { 1178 | 'SCALAR': 1, 1179 | 'VEC2': 2, 1180 | 'VEC3': 3, 1181 | 'VEC4': 4, 1182 | 'MAT2': 4, 1183 | 'MAT3': 9, 1184 | 'MAT4': 16 1185 | }; 1186 | 1187 | var ATTRIBUTES = { 1188 | POSITION: 'position', 1189 | NORMAL: 'normal', 1190 | TANGENT: 'tangent', 1191 | TEXCOORD_0: 'uv', 1192 | TEXCOORD_1: 'uv2', 1193 | COLOR_0: 'color', 1194 | WEIGHTS_0: 'skinWeight', 1195 | JOINTS_0: 'skinIndex', 1196 | }; 1197 | 1198 | var PATH_PROPERTIES = { 1199 | scale: 'scale', 1200 | translation: 'position', 1201 | rotation: 'quaternion', 1202 | weights: 'morphTargetInfluences' 1203 | }; 1204 | 1205 | var INTERPOLATION = { 1206 | CUBICSPLINE: undefined, // We use a custom interpolant (GLTFCubicSplineInterpolation) for CUBICSPLINE tracks. Each 1207 | // keyframe track will be initialized with a default interpolation type, then modified. 1208 | LINEAR: THREE.InterpolateLinear, 1209 | STEP: THREE.InterpolateDiscrete 1210 | }; 1211 | 1212 | var STATES_ENABLES = { 1213 | 2884: 'CULL_FACE', 1214 | 2929: 'DEPTH_TEST', 1215 | 3042: 'BLEND', 1216 | 3089: 'SCISSOR_TEST', 1217 | 32823: 'POLYGON_OFFSET_FILL', 1218 | 32926: 'SAMPLE_ALPHA_TO_COVERAGE' 1219 | }; 1220 | 1221 | var ALPHA_MODES = { 1222 | OPAQUE: 'OPAQUE', 1223 | MASK: 'MASK', 1224 | BLEND: 'BLEND' 1225 | }; 1226 | 1227 | var MIME_TYPE_FORMATS = { 1228 | 'image/png': THREE.RGBAFormat, 1229 | 'image/jpeg': THREE.RGBFormat 1230 | }; 1231 | 1232 | /* UTILITY FUNCTIONS */ 1233 | 1234 | function resolveURL( url, path ) { 1235 | 1236 | // Invalid URL 1237 | if ( typeof url !== 'string' || url === '' ) return ''; 1238 | 1239 | // Absolute URL http://,https://,// 1240 | if ( /^(https?:)?\/\//i.test( url ) ) return url; 1241 | 1242 | // Data URI 1243 | if ( /^data:.*,.*$/i.test( url ) ) return url; 1244 | 1245 | // Blob URL 1246 | if ( /^blob:.*$/i.test( url ) ) return url; 1247 | 1248 | // Relative URL 1249 | return path + url; 1250 | 1251 | } 1252 | 1253 | var defaultMaterial; 1254 | 1255 | /** 1256 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material 1257 | */ 1258 | function createDefaultMaterial() { 1259 | 1260 | defaultMaterial = defaultMaterial || new THREE.MeshStandardMaterial( { 1261 | color: 0xFFFFFF, 1262 | emissive: 0x000000, 1263 | metalness: 1, 1264 | roughness: 1, 1265 | transparent: false, 1266 | depthTest: true, 1267 | side: THREE.FrontSide 1268 | } ); 1269 | 1270 | return defaultMaterial; 1271 | 1272 | } 1273 | 1274 | function addUnknownExtensionsToUserData( knownExtensions, object, objectDef ) { 1275 | 1276 | // Add unknown glTF extensions to an object's userData. 1277 | 1278 | for ( var name in objectDef.extensions ) { 1279 | 1280 | if ( knownExtensions[ name ] === undefined ) { 1281 | 1282 | object.userData.gltfExtensions = object.userData.gltfExtensions || {}; 1283 | object.userData.gltfExtensions[ name ] = objectDef.extensions[ name ]; 1284 | 1285 | } 1286 | 1287 | } 1288 | 1289 | } 1290 | 1291 | /** 1292 | * @param {THREE.Object3D|THREE.Material|THREE.BufferGeometry} object 1293 | * @param {GLTF.definition} gltfDef 1294 | */ 1295 | function assignExtrasToUserData( object, gltfDef ) { 1296 | 1297 | if ( gltfDef.extras !== undefined ) { 1298 | 1299 | if ( typeof gltfDef.extras === 'object' ) { 1300 | 1301 | object.userData = gltfDef.extras; 1302 | 1303 | } else { 1304 | 1305 | console.warn( 'THREE.GLTFLoader: Ignoring primitive type .extras, ' + gltfDef.extras ); 1306 | 1307 | } 1308 | 1309 | } 1310 | 1311 | } 1312 | 1313 | /** 1314 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets 1315 | * 1316 | * @param {THREE.BufferGeometry} geometry 1317 | * @param {Array} targets 1318 | * @param {GLTFParser} parser 1319 | * @return {Promise} 1320 | */ 1321 | function addMorphTargets( geometry, targets, parser ) { 1322 | 1323 | var hasMorphPosition = false; 1324 | var hasMorphNormal = false; 1325 | 1326 | for ( var i = 0, il = targets.length; i < il; i ++ ) { 1327 | 1328 | var target = targets[ i ]; 1329 | 1330 | if ( target.POSITION !== undefined ) hasMorphPosition = true; 1331 | if ( target.NORMAL !== undefined ) hasMorphNormal = true; 1332 | 1333 | if ( hasMorphPosition && hasMorphNormal ) break; 1334 | 1335 | } 1336 | 1337 | if ( ! hasMorphPosition && ! hasMorphNormal ) return Promise.resolve( geometry ); 1338 | 1339 | var pendingPositionAccessors = []; 1340 | var pendingNormalAccessors = []; 1341 | 1342 | for ( var i = 0, il = targets.length; i < il; i ++ ) { 1343 | 1344 | var target = targets[ i ]; 1345 | 1346 | if ( hasMorphPosition ) { 1347 | 1348 | var pendingAccessor = target.POSITION !== undefined 1349 | ? parser.getDependency( 'accessor', target.POSITION ) 1350 | : geometry.attributes.position; 1351 | 1352 | pendingPositionAccessors.push( pendingAccessor ); 1353 | 1354 | } 1355 | 1356 | if ( hasMorphNormal ) { 1357 | 1358 | var pendingAccessor = target.NORMAL !== undefined 1359 | ? parser.getDependency( 'accessor', target.NORMAL ) 1360 | : geometry.attributes.normal; 1361 | 1362 | pendingNormalAccessors.push( pendingAccessor ); 1363 | 1364 | } 1365 | 1366 | } 1367 | 1368 | return Promise.all( [ 1369 | Promise.all( pendingPositionAccessors ), 1370 | Promise.all( pendingNormalAccessors ) 1371 | ] ).then( function ( accessors ) { 1372 | 1373 | var morphPositions = accessors[ 0 ]; 1374 | var morphNormals = accessors[ 1 ]; 1375 | 1376 | // Clone morph target accessors before modifying them. 1377 | 1378 | for ( var i = 0, il = morphPositions.length; i < il; i ++ ) { 1379 | 1380 | if ( geometry.attributes.position === morphPositions[ i ] ) continue; 1381 | 1382 | morphPositions[ i ] = cloneBufferAttribute( morphPositions[ i ] ); 1383 | 1384 | } 1385 | 1386 | for ( var i = 0, il = morphNormals.length; i < il; i ++ ) { 1387 | 1388 | if ( geometry.attributes.normal === morphNormals[ i ] ) continue; 1389 | 1390 | morphNormals[ i ] = cloneBufferAttribute( morphNormals[ i ] ); 1391 | 1392 | } 1393 | 1394 | for ( var i = 0, il = targets.length; i < il; i ++ ) { 1395 | 1396 | var target = targets[ i ]; 1397 | var attributeName = 'morphTarget' + i; 1398 | 1399 | if ( hasMorphPosition ) { 1400 | 1401 | // Three.js morph position is absolute value. The formula is 1402 | // basePosition 1403 | // + weight0 * ( morphPosition0 - basePosition ) 1404 | // + weight1 * ( morphPosition1 - basePosition ) 1405 | // ... 1406 | // while the glTF one is relative 1407 | // basePosition 1408 | // + weight0 * glTFmorphPosition0 1409 | // + weight1 * glTFmorphPosition1 1410 | // ... 1411 | // then we need to convert from relative to absolute here. 1412 | 1413 | if ( target.POSITION !== undefined ) { 1414 | 1415 | var positionAttribute = morphPositions[ i ]; 1416 | positionAttribute.name = attributeName; 1417 | 1418 | var position = geometry.attributes.position; 1419 | 1420 | for ( var j = 0, jl = positionAttribute.count; j < jl; j ++ ) { 1421 | 1422 | positionAttribute.setXYZ( 1423 | j, 1424 | positionAttribute.getX( j ) + position.getX( j ), 1425 | positionAttribute.getY( j ) + position.getY( j ), 1426 | positionAttribute.getZ( j ) + position.getZ( j ) 1427 | ); 1428 | 1429 | } 1430 | 1431 | } 1432 | 1433 | } 1434 | 1435 | if ( hasMorphNormal ) { 1436 | 1437 | // see target.POSITION's comment 1438 | 1439 | if ( target.NORMAL !== undefined ) { 1440 | 1441 | var normalAttribute = morphNormals[ i ]; 1442 | normalAttribute.name = attributeName; 1443 | 1444 | var normal = geometry.attributes.normal; 1445 | 1446 | for ( var j = 0, jl = normalAttribute.count; j < jl; j ++ ) { 1447 | 1448 | normalAttribute.setXYZ( 1449 | j, 1450 | normalAttribute.getX( j ) + normal.getX( j ), 1451 | normalAttribute.getY( j ) + normal.getY( j ), 1452 | normalAttribute.getZ( j ) + normal.getZ( j ) 1453 | ); 1454 | 1455 | } 1456 | 1457 | } 1458 | 1459 | } 1460 | 1461 | } 1462 | 1463 | if ( hasMorphPosition ) geometry.morphAttributes.position = morphPositions; 1464 | if ( hasMorphNormal ) geometry.morphAttributes.normal = morphNormals; 1465 | 1466 | return geometry; 1467 | 1468 | } ); 1469 | 1470 | } 1471 | 1472 | /** 1473 | * @param {THREE.Mesh} mesh 1474 | * @param {GLTF.Mesh} meshDef 1475 | */ 1476 | function updateMorphTargets( mesh, meshDef ) { 1477 | 1478 | mesh.updateMorphTargets(); 1479 | 1480 | if ( meshDef.weights !== undefined ) { 1481 | 1482 | for ( var i = 0, il = meshDef.weights.length; i < il; i ++ ) { 1483 | 1484 | mesh.morphTargetInfluences[ i ] = meshDef.weights[ i ]; 1485 | 1486 | } 1487 | 1488 | } 1489 | 1490 | // .extras has user-defined data, so check that .extras.targetNames is an array. 1491 | if ( meshDef.extras && Array.isArray( meshDef.extras.targetNames ) ) { 1492 | 1493 | var targetNames = meshDef.extras.targetNames; 1494 | 1495 | if ( mesh.morphTargetInfluences.length === targetNames.length ) { 1496 | 1497 | mesh.morphTargetDictionary = {}; 1498 | 1499 | for ( var i = 0, il = targetNames.length; i < il; i ++ ) { 1500 | 1501 | mesh.morphTargetDictionary[ targetNames[ i ] ] = i; 1502 | 1503 | } 1504 | 1505 | } else { 1506 | 1507 | console.warn( 'THREE.GLTFLoader: Invalid extras.targetNames length. Ignoring names.' ); 1508 | 1509 | } 1510 | 1511 | } 1512 | 1513 | } 1514 | function isObjectEqual( a, b ) { 1515 | 1516 | if ( Object.keys( a ).length !== Object.keys( b ).length ) return false; 1517 | 1518 | for ( var key in a ) { 1519 | 1520 | if ( a[ key ] !== b[ key ] ) return false; 1521 | 1522 | } 1523 | 1524 | return true; 1525 | 1526 | } 1527 | 1528 | function createPrimitiveKey( primitiveDef ) { 1529 | 1530 | var dracoExtension = primitiveDef.extensions && primitiveDef.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ]; 1531 | var geometryKey; 1532 | 1533 | if ( dracoExtension ) { 1534 | 1535 | geometryKey = 'draco:' + dracoExtension.bufferView 1536 | + ':' + dracoExtension.indices 1537 | + ':' + createAttributesKey( dracoExtension.attributes ); 1538 | 1539 | } else { 1540 | 1541 | geometryKey = primitiveDef.indices + ':' + createAttributesKey( primitiveDef.attributes ) + ':' + primitiveDef.mode; 1542 | 1543 | } 1544 | 1545 | return geometryKey; 1546 | 1547 | } 1548 | 1549 | function createAttributesKey( attributes ) { 1550 | 1551 | var attributesKey = ''; 1552 | 1553 | var keys = Object.keys( attributes ).sort(); 1554 | 1555 | for ( var i = 0, il = keys.length; i < il; i ++ ) { 1556 | 1557 | attributesKey += keys[ i ] + ':' + attributes[ keys[ i ] ] + ';'; 1558 | 1559 | } 1560 | 1561 | return attributesKey; 1562 | 1563 | } 1564 | 1565 | function cloneBufferAttribute( attribute ) { 1566 | 1567 | if ( attribute.isInterleavedBufferAttribute ) { 1568 | 1569 | var count = attribute.count; 1570 | var itemSize = attribute.itemSize; 1571 | var array = attribute.array.slice( 0, count * itemSize ); 1572 | 1573 | for ( var i = 0, j = 0; i < count; ++ i ) { 1574 | 1575 | array[ j ++ ] = attribute.getX( i ); 1576 | if ( itemSize >= 2 ) array[ j ++ ] = attribute.getY( i ); 1577 | if ( itemSize >= 3 ) array[ j ++ ] = attribute.getZ( i ); 1578 | if ( itemSize >= 4 ) array[ j ++ ] = attribute.getW( i ); 1579 | 1580 | } 1581 | 1582 | return new THREE.BufferAttribute( array, itemSize, attribute.normalized ); 1583 | 1584 | } 1585 | 1586 | return attribute.clone(); 1587 | 1588 | } 1589 | 1590 | /* GLTF PARSER */ 1591 | 1592 | function GLTFParser( json, extensions, options ) { 1593 | 1594 | this.json = json || {}; 1595 | this.extensions = extensions || {}; 1596 | this.options = options || {}; 1597 | 1598 | // loader object cache 1599 | this.cache = new GLTFRegistry(); 1600 | 1601 | // BufferGeometry caching 1602 | this.primitiveCache = {}; 1603 | 1604 | this.textureLoader = new THREE.TextureLoader( this.options.manager ); 1605 | this.textureLoader.setCrossOrigin( this.options.crossOrigin ); 1606 | 1607 | this.fileLoader = new THREE.FileLoader( this.options.manager ); 1608 | this.fileLoader.setResponseType( 'arraybuffer' ); 1609 | 1610 | } 1611 | 1612 | GLTFParser.prototype.parse = function ( onLoad, onError ) { 1613 | 1614 | var parser = this; 1615 | var json = this.json; 1616 | var extensions = this.extensions; 1617 | 1618 | // Clear the loader cache 1619 | this.cache.removeAll(); 1620 | 1621 | // Mark the special nodes/meshes in json for efficient parse 1622 | this.markDefs(); 1623 | 1624 | Promise.all( [ 1625 | 1626 | this.getDependencies( 'scene' ), 1627 | this.getDependencies( 'animation' ), 1628 | this.getDependencies( 'camera' ), 1629 | 1630 | ] ).then( function ( dependencies ) { 1631 | 1632 | var result = { 1633 | scene: dependencies[ 0 ][ json.scene || 0 ], 1634 | scenes: dependencies[ 0 ], 1635 | animations: dependencies[ 1 ], 1636 | cameras: dependencies[ 2 ], 1637 | asset: json.asset, 1638 | parser: parser, 1639 | userData: {} 1640 | }; 1641 | 1642 | addUnknownExtensionsToUserData( extensions, result, json ); 1643 | 1644 | onLoad( result ); 1645 | 1646 | } ).catch( onError ); 1647 | 1648 | }; 1649 | 1650 | /** 1651 | * Marks the special nodes/meshes in json for efficient parse. 1652 | */ 1653 | GLTFParser.prototype.markDefs = function () { 1654 | 1655 | var nodeDefs = this.json.nodes || []; 1656 | var skinDefs = this.json.skins || []; 1657 | var meshDefs = this.json.meshes || []; 1658 | 1659 | var meshReferences = {}; 1660 | var meshUses = {}; 1661 | 1662 | // Nothing in the node definition indicates whether it is a Bone or an 1663 | // Object3D. Use the skins' joint references to mark bones. 1664 | for ( var skinIndex = 0, skinLength = skinDefs.length; skinIndex < skinLength; skinIndex ++ ) { 1665 | 1666 | var joints = skinDefs[ skinIndex ].joints; 1667 | 1668 | for ( var i = 0, il = joints.length; i < il; i ++ ) { 1669 | 1670 | nodeDefs[ joints[ i ] ].isBone = true; 1671 | 1672 | } 1673 | 1674 | } 1675 | 1676 | // Meshes can (and should) be reused by multiple nodes in a glTF asset. To 1677 | // avoid having more than one THREE.Mesh with the same name, count 1678 | // references and rename instances below. 1679 | // 1680 | // Example: CesiumMilkTruck sample model reuses "Wheel" meshes. 1681 | for ( var nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex ++ ) { 1682 | 1683 | var nodeDef = nodeDefs[ nodeIndex ]; 1684 | 1685 | if ( nodeDef.mesh !== undefined ) { 1686 | 1687 | if ( meshReferences[ nodeDef.mesh ] === undefined ) { 1688 | 1689 | meshReferences[ nodeDef.mesh ] = meshUses[ nodeDef.mesh ] = 0; 1690 | 1691 | } 1692 | 1693 | meshReferences[ nodeDef.mesh ] ++; 1694 | 1695 | // Nothing in the mesh definition indicates whether it is 1696 | // a SkinnedMesh or Mesh. Use the node's mesh reference 1697 | // to mark SkinnedMesh if node has skin. 1698 | if ( nodeDef.skin !== undefined ) { 1699 | 1700 | meshDefs[ nodeDef.mesh ].isSkinnedMesh = true; 1701 | 1702 | } 1703 | 1704 | } 1705 | 1706 | } 1707 | 1708 | this.json.meshReferences = meshReferences; 1709 | this.json.meshUses = meshUses; 1710 | 1711 | }; 1712 | 1713 | /** 1714 | * Requests the specified dependency asynchronously, with caching. 1715 | * @param {string} type 1716 | * @param {number} index 1717 | * @return {Promise} 1718 | */ 1719 | GLTFParser.prototype.getDependency = function ( type, index ) { 1720 | 1721 | var cacheKey = type + ':' + index; 1722 | var dependency = this.cache.get( cacheKey ); 1723 | 1724 | if ( ! dependency ) { 1725 | 1726 | switch ( type ) { 1727 | 1728 | case 'scene': 1729 | dependency = this.loadScene( index ); 1730 | break; 1731 | 1732 | case 'node': 1733 | dependency = this.loadNode( index ); 1734 | break; 1735 | 1736 | case 'mesh': 1737 | dependency = this.loadMesh( index ); 1738 | break; 1739 | 1740 | case 'accessor': 1741 | dependency = this.loadAccessor( index ); 1742 | break; 1743 | 1744 | case 'bufferView': 1745 | dependency = this.loadBufferView( index ); 1746 | break; 1747 | 1748 | case 'buffer': 1749 | dependency = this.loadBuffer( index ); 1750 | break; 1751 | 1752 | case 'material': 1753 | dependency = this.loadMaterial( index ); 1754 | break; 1755 | 1756 | case 'texture': 1757 | dependency = this.loadTexture( index ); 1758 | break; 1759 | 1760 | case 'skin': 1761 | dependency = this.loadSkin( index ); 1762 | break; 1763 | 1764 | case 'animation': 1765 | dependency = this.loadAnimation( index ); 1766 | break; 1767 | 1768 | case 'camera': 1769 | dependency = this.loadCamera( index ); 1770 | break; 1771 | 1772 | case 'light': 1773 | dependency = this.extensions[ EXTENSIONS.KHR_LIGHTS_PUNCTUAL ].loadLight( index ); 1774 | break; 1775 | 1776 | default: 1777 | throw new Error( 'Unknown type: ' + type ); 1778 | 1779 | } 1780 | 1781 | this.cache.add( cacheKey, dependency ); 1782 | 1783 | } 1784 | 1785 | return dependency; 1786 | 1787 | }; 1788 | 1789 | /** 1790 | * Requests all dependencies of the specified type asynchronously, with caching. 1791 | * @param {string} type 1792 | * @return {Promise>} 1793 | */ 1794 | GLTFParser.prototype.getDependencies = function ( type ) { 1795 | 1796 | var dependencies = this.cache.get( type ); 1797 | 1798 | if ( ! dependencies ) { 1799 | 1800 | var parser = this; 1801 | var defs = this.json[ type + ( type === 'mesh' ? 'es' : 's' ) ] || []; 1802 | 1803 | dependencies = Promise.all( defs.map( function ( def, index ) { 1804 | 1805 | return parser.getDependency( type, index ); 1806 | 1807 | } ) ); 1808 | 1809 | this.cache.add( type, dependencies ); 1810 | 1811 | } 1812 | 1813 | return dependencies; 1814 | 1815 | }; 1816 | 1817 | /** 1818 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views 1819 | * @param {number} bufferIndex 1820 | * @return {Promise} 1821 | */ 1822 | GLTFParser.prototype.loadBuffer = function ( bufferIndex ) { 1823 | 1824 | var bufferDef = this.json.buffers[ bufferIndex ]; 1825 | var loader = this.fileLoader; 1826 | 1827 | if ( bufferDef.type && bufferDef.type !== 'arraybuffer' ) { 1828 | 1829 | throw new Error( 'THREE.GLTFLoader: ' + bufferDef.type + ' buffer type is not supported.' ); 1830 | 1831 | } 1832 | 1833 | // If present, GLB container is required to be the first buffer. 1834 | if ( bufferDef.uri === undefined && bufferIndex === 0 ) { 1835 | 1836 | return Promise.resolve( this.extensions[ EXTENSIONS.KHR_BINARY_GLTF ].body ); 1837 | 1838 | } 1839 | 1840 | var options = this.options; 1841 | 1842 | return new Promise( function ( resolve, reject ) { 1843 | 1844 | loader.load( resolveURL( bufferDef.uri, options.path ), resolve, undefined, function () { 1845 | 1846 | reject( new Error( 'THREE.GLTFLoader: Failed to load buffer "' + bufferDef.uri + '".' ) ); 1847 | 1848 | } ); 1849 | 1850 | } ); 1851 | 1852 | }; 1853 | 1854 | /** 1855 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views 1856 | * @param {number} bufferViewIndex 1857 | * @return {Promise} 1858 | */ 1859 | GLTFParser.prototype.loadBufferView = function ( bufferViewIndex ) { 1860 | 1861 | var bufferViewDef = this.json.bufferViews[ bufferViewIndex ]; 1862 | 1863 | return this.getDependency( 'buffer', bufferViewDef.buffer ).then( function ( buffer ) { 1864 | 1865 | var byteLength = bufferViewDef.byteLength || 0; 1866 | var byteOffset = bufferViewDef.byteOffset || 0; 1867 | return buffer.slice( byteOffset, byteOffset + byteLength ); 1868 | 1869 | } ); 1870 | 1871 | }; 1872 | 1873 | /** 1874 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#accessors 1875 | * @param {number} accessorIndex 1876 | * @return {Promise} 1877 | */ 1878 | GLTFParser.prototype.loadAccessor = function ( accessorIndex ) { 1879 | 1880 | var parser = this; 1881 | var json = this.json; 1882 | 1883 | var accessorDef = this.json.accessors[ accessorIndex ]; 1884 | 1885 | if ( accessorDef.bufferView === undefined && accessorDef.sparse === undefined ) { 1886 | 1887 | // Ignore empty accessors, which may be used to declare runtime 1888 | // information about attributes coming from another source (e.g. Draco 1889 | // compression extension). 1890 | return Promise.resolve( null ); 1891 | 1892 | } 1893 | 1894 | var pendingBufferViews = []; 1895 | 1896 | if ( accessorDef.bufferView !== undefined ) { 1897 | 1898 | pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.bufferView ) ); 1899 | 1900 | } else { 1901 | 1902 | pendingBufferViews.push( null ); 1903 | 1904 | } 1905 | 1906 | if ( accessorDef.sparse !== undefined ) { 1907 | 1908 | pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.indices.bufferView ) ); 1909 | pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.values.bufferView ) ); 1910 | 1911 | } 1912 | 1913 | return Promise.all( pendingBufferViews ).then( function ( bufferViews ) { 1914 | 1915 | var bufferView = bufferViews[ 0 ]; 1916 | 1917 | var itemSize = WEBGL_TYPE_SIZES[ accessorDef.type ]; 1918 | var TypedArray = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ]; 1919 | 1920 | // For VEC3: itemSize is 3, elementBytes is 4, itemBytes is 12. 1921 | var elementBytes = TypedArray.BYTES_PER_ELEMENT; 1922 | var itemBytes = elementBytes * itemSize; 1923 | var byteOffset = accessorDef.byteOffset || 0; 1924 | var byteStride = accessorDef.bufferView !== undefined ? json.bufferViews[ accessorDef.bufferView ].byteStride : undefined; 1925 | var normalized = accessorDef.normalized === true; 1926 | var array, bufferAttribute; 1927 | 1928 | // The buffer is not interleaved if the stride is the item size in bytes. 1929 | if ( byteStride && byteStride !== itemBytes ) { 1930 | 1931 | var ibCacheKey = 'InterleavedBuffer:' + accessorDef.bufferView + ':' + accessorDef.componentType; 1932 | var ib = parser.cache.get( ibCacheKey ); 1933 | 1934 | if ( ! ib ) { 1935 | 1936 | // Use the full buffer if it's interleaved. 1937 | array = new TypedArray( bufferView ); 1938 | 1939 | // Integer parameters to IB/IBA are in array elements, not bytes. 1940 | ib = new THREE.InterleavedBuffer( array, byteStride / elementBytes ); 1941 | 1942 | parser.cache.add( ibCacheKey, ib ); 1943 | 1944 | } 1945 | 1946 | bufferAttribute = new THREE.InterleavedBufferAttribute( ib, itemSize, byteOffset / elementBytes, normalized ); 1947 | 1948 | } else { 1949 | 1950 | if ( bufferView === null ) { 1951 | 1952 | array = new TypedArray( accessorDef.count * itemSize ); 1953 | 1954 | } else { 1955 | 1956 | array = new TypedArray( bufferView, byteOffset, accessorDef.count * itemSize ); 1957 | 1958 | } 1959 | 1960 | bufferAttribute = new THREE.BufferAttribute( array, itemSize, normalized ); 1961 | 1962 | } 1963 | 1964 | // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#sparse-accessors 1965 | if ( accessorDef.sparse !== undefined ) { 1966 | 1967 | var itemSizeIndices = WEBGL_TYPE_SIZES.SCALAR; 1968 | var TypedArrayIndices = WEBGL_COMPONENT_TYPES[ accessorDef.sparse.indices.componentType ]; 1969 | 1970 | var byteOffsetIndices = accessorDef.sparse.indices.byteOffset || 0; 1971 | var byteOffsetValues = accessorDef.sparse.values.byteOffset || 0; 1972 | 1973 | var sparseIndices = new TypedArrayIndices( bufferViews[ 1 ], byteOffsetIndices, accessorDef.sparse.count * itemSizeIndices ); 1974 | var sparseValues = new TypedArray( bufferViews[ 2 ], byteOffsetValues, accessorDef.sparse.count * itemSize ); 1975 | 1976 | if ( bufferView !== null ) { 1977 | 1978 | // Avoid modifying the original ArrayBuffer, if the bufferView wasn't initialized with zeroes. 1979 | bufferAttribute.setArray( bufferAttribute.array.slice() ); 1980 | 1981 | } 1982 | 1983 | for ( var i = 0, il = sparseIndices.length; i < il; i ++ ) { 1984 | 1985 | var index = sparseIndices[ i ]; 1986 | 1987 | bufferAttribute.setX( index, sparseValues[ i * itemSize ] ); 1988 | if ( itemSize >= 2 ) bufferAttribute.setY( index, sparseValues[ i * itemSize + 1 ] ); 1989 | if ( itemSize >= 3 ) bufferAttribute.setZ( index, sparseValues[ i * itemSize + 2 ] ); 1990 | if ( itemSize >= 4 ) bufferAttribute.setW( index, sparseValues[ i * itemSize + 3 ] ); 1991 | if ( itemSize >= 5 ) throw new Error( 'THREE.GLTFLoader: Unsupported itemSize in sparse BufferAttribute.' ); 1992 | 1993 | } 1994 | 1995 | } 1996 | 1997 | return bufferAttribute; 1998 | 1999 | } ); 2000 | 2001 | }; 2002 | 2003 | /** 2004 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#textures 2005 | * @param {number} textureIndex 2006 | * @return {Promise} 2007 | */ 2008 | GLTFParser.prototype.loadTexture = function ( textureIndex ) { 2009 | 2010 | var parser = this; 2011 | var json = this.json; 2012 | var options = this.options; 2013 | var textureLoader = this.textureLoader; 2014 | 2015 | var URL = window.URL || window.webkitURL; 2016 | 2017 | var textureDef = json.textures[ textureIndex ]; 2018 | 2019 | var textureExtensions = textureDef.extensions || {}; 2020 | 2021 | var source; 2022 | 2023 | if ( textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ] ) { 2024 | 2025 | source = json.images[ textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ].source ]; 2026 | 2027 | } else { 2028 | 2029 | source = json.images[ textureDef.source ]; 2030 | 2031 | } 2032 | 2033 | var sourceURI = source.uri; 2034 | var isObjectURL = false; 2035 | 2036 | if ( source.bufferView !== undefined ) { 2037 | 2038 | // Load binary image data from bufferView, if provided. 2039 | 2040 | sourceURI = parser.getDependency( 'bufferView', source.bufferView ).then( function ( bufferView ) { 2041 | 2042 | isObjectURL = true; 2043 | var blob = new Blob( [ bufferView ], { type: source.mimeType } ); 2044 | sourceURI = URL.createObjectURL( blob ); 2045 | return sourceURI; 2046 | 2047 | } ); 2048 | 2049 | } 2050 | 2051 | return Promise.resolve( sourceURI ).then( function ( sourceURI ) { 2052 | 2053 | // Load Texture resource. 2054 | 2055 | var loader = THREE.Loader.Handlers.get( sourceURI ); 2056 | 2057 | if ( ! loader ) { 2058 | 2059 | loader = textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ] 2060 | ? parser.extensions[ EXTENSIONS.MSFT_TEXTURE_DDS ].ddsLoader 2061 | : textureLoader; 2062 | 2063 | } 2064 | 2065 | return new Promise( function ( resolve, reject ) { 2066 | 2067 | loader.load( resolveURL( sourceURI, options.path ), resolve, undefined, reject ); 2068 | 2069 | } ); 2070 | 2071 | } ).then( function ( texture ) { 2072 | 2073 | // Clean up resources and configure Texture. 2074 | 2075 | if ( isObjectURL === true ) { 2076 | 2077 | URL.revokeObjectURL( sourceURI ); 2078 | 2079 | } 2080 | 2081 | texture.flipY = false; 2082 | 2083 | if ( textureDef.name !== undefined ) texture.name = textureDef.name; 2084 | 2085 | // Ignore unknown mime types, like DDS files. 2086 | if ( source.mimeType in MIME_TYPE_FORMATS ) { 2087 | 2088 | texture.format = MIME_TYPE_FORMATS[ source.mimeType ]; 2089 | 2090 | } 2091 | 2092 | var samplers = json.samplers || {}; 2093 | var sampler = samplers[ textureDef.sampler ] || {}; 2094 | 2095 | texture.magFilter = WEBGL_FILTERS[ sampler.magFilter ] || THREE.LinearFilter; 2096 | texture.minFilter = WEBGL_FILTERS[ sampler.minFilter ] || THREE.LinearMipMapLinearFilter; 2097 | texture.wrapS = WEBGL_WRAPPINGS[ sampler.wrapS ] || THREE.RepeatWrapping; 2098 | texture.wrapT = WEBGL_WRAPPINGS[ sampler.wrapT ] || THREE.RepeatWrapping; 2099 | 2100 | return texture; 2101 | 2102 | } ); 2103 | 2104 | }; 2105 | 2106 | /** 2107 | * Asynchronously assigns a texture to the given material parameters. 2108 | * @param {Object} materialParams 2109 | * @param {string} mapName 2110 | * @param {Object} mapDef 2111 | * @return {Promise} 2112 | */ 2113 | GLTFParser.prototype.assignTexture = function ( materialParams, mapName, mapDef ) { 2114 | 2115 | var parser = this; 2116 | 2117 | return this.getDependency( 'texture', mapDef.index ).then( function ( texture ) { 2118 | 2119 | switch ( mapName ) { 2120 | 2121 | case 'aoMap': 2122 | case 'emissiveMap': 2123 | case 'metalnessMap': 2124 | case 'normalMap': 2125 | case 'roughnessMap': 2126 | texture.format = THREE.RGBFormat; 2127 | break; 2128 | 2129 | } 2130 | 2131 | if ( parser.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] ) { 2132 | 2133 | var transform = mapDef.extensions !== undefined ? mapDef.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] : undefined; 2134 | 2135 | if ( transform ) { 2136 | 2137 | texture = parser.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ].extendTexture( texture, transform ); 2138 | 2139 | } 2140 | 2141 | } 2142 | 2143 | materialParams[ mapName ] = texture; 2144 | 2145 | } ); 2146 | 2147 | }; 2148 | 2149 | /** 2150 | * Assigns final material to a Mesh, Line, or Points instance. The instance 2151 | * already has a material (generated from the glTF material options alone) 2152 | * but reuse of the same glTF material may require multiple threejs materials 2153 | * to accomodate different primitive types, defines, etc. New materials will 2154 | * be created if necessary, and reused from a cache. 2155 | * @param {THREE.Object3D} mesh Mesh, Line, or Points instance. 2156 | */ 2157 | GLTFParser.prototype.assignFinalMaterial = function ( mesh ) { 2158 | 2159 | var geometry = mesh.geometry; 2160 | var material = mesh.material; 2161 | var extensions = this.extensions; 2162 | 2163 | var useVertexTangents = geometry.attributes.tangent !== undefined; 2164 | var useVertexColors = geometry.attributes.color !== undefined; 2165 | var useFlatShading = geometry.attributes.normal === undefined; 2166 | var useSkinning = mesh.isSkinnedMesh === true; 2167 | var useMorphTargets = Object.keys( geometry.morphAttributes ).length > 0; 2168 | var useMorphNormals = useMorphTargets && geometry.morphAttributes.normal !== undefined; 2169 | 2170 | if ( mesh.isPoints ) { 2171 | 2172 | var cacheKey = 'PointsMaterial:' + material.uuid; 2173 | 2174 | var pointsMaterial = this.cache.get( cacheKey ); 2175 | 2176 | if ( ! pointsMaterial ) { 2177 | 2178 | pointsMaterial = new THREE.PointsMaterial(); 2179 | THREE.Material.prototype.copy.call( pointsMaterial, material ); 2180 | pointsMaterial.color.copy( material.color ); 2181 | pointsMaterial.map = material.map; 2182 | pointsMaterial.lights = false; // PointsMaterial doesn't support lights yet 2183 | 2184 | this.cache.add( cacheKey, pointsMaterial ); 2185 | 2186 | } 2187 | 2188 | material = pointsMaterial; 2189 | 2190 | } else if ( mesh.isLine ) { 2191 | 2192 | var cacheKey = 'LineBasicMaterial:' + material.uuid; 2193 | 2194 | var lineMaterial = this.cache.get( cacheKey ); 2195 | 2196 | if ( ! lineMaterial ) { 2197 | 2198 | lineMaterial = new THREE.LineBasicMaterial(); 2199 | THREE.Material.prototype.copy.call( lineMaterial, material ); 2200 | lineMaterial.color.copy( material.color ); 2201 | lineMaterial.lights = false; // LineBasicMaterial doesn't support lights yet 2202 | 2203 | this.cache.add( cacheKey, lineMaterial ); 2204 | 2205 | } 2206 | 2207 | material = lineMaterial; 2208 | 2209 | } 2210 | 2211 | // Clone the material if it will be modified 2212 | if ( useVertexTangents || useVertexColors || useFlatShading || useSkinning || useMorphTargets ) { 2213 | 2214 | var cacheKey = 'ClonedMaterial:' + material.uuid + ':'; 2215 | 2216 | if ( material.isGLTFSpecularGlossinessMaterial ) cacheKey += 'specular-glossiness:'; 2217 | if ( useSkinning ) cacheKey += 'skinning:'; 2218 | if ( useVertexTangents ) cacheKey += 'vertex-tangents:'; 2219 | if ( useVertexColors ) cacheKey += 'vertex-colors:'; 2220 | if ( useFlatShading ) cacheKey += 'flat-shading:'; 2221 | if ( useMorphTargets ) cacheKey += 'morph-targets:'; 2222 | if ( useMorphNormals ) cacheKey += 'morph-normals:'; 2223 | 2224 | var cachedMaterial = this.cache.get( cacheKey ); 2225 | 2226 | if ( ! cachedMaterial ) { 2227 | 2228 | cachedMaterial = material.isGLTFSpecularGlossinessMaterial 2229 | ? extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].cloneMaterial( material ) 2230 | : material.clone(); 2231 | 2232 | if ( useSkinning ) cachedMaterial.skinning = true; 2233 | if ( useVertexTangents ) cachedMaterial.vertexTangents = true; 2234 | if ( useVertexColors ) cachedMaterial.vertexColors = THREE.VertexColors; 2235 | if ( useFlatShading ) cachedMaterial.flatShading = true; 2236 | if ( useMorphTargets ) cachedMaterial.morphTargets = true; 2237 | if ( useMorphNormals ) cachedMaterial.morphNormals = true; 2238 | 2239 | this.cache.add( cacheKey, cachedMaterial ); 2240 | 2241 | } 2242 | 2243 | material = cachedMaterial; 2244 | 2245 | } 2246 | 2247 | // workarounds for mesh and geometry 2248 | 2249 | if ( material.aoMap && geometry.attributes.uv2 === undefined && geometry.attributes.uv !== undefined ) { 2250 | 2251 | console.log( 'THREE.GLTFLoader: Duplicating UVs to support aoMap.' ); 2252 | geometry.addAttribute( 'uv2', new THREE.BufferAttribute( geometry.attributes.uv.array, 2 ) ); 2253 | 2254 | } 2255 | 2256 | if ( material.isGLTFSpecularGlossinessMaterial ) { 2257 | 2258 | // for GLTFSpecularGlossinessMaterial(ShaderMaterial) uniforms runtime update 2259 | mesh.onBeforeRender = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].refreshUniforms; 2260 | 2261 | } 2262 | 2263 | mesh.material = material; 2264 | 2265 | }; 2266 | 2267 | /** 2268 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#materials 2269 | * @param {number} materialIndex 2270 | * @return {Promise} 2271 | */ 2272 | GLTFParser.prototype.loadMaterial = function ( materialIndex ) { 2273 | 2274 | var parser = this; 2275 | var json = this.json; 2276 | var extensions = this.extensions; 2277 | var materialDef = json.materials[ materialIndex ]; 2278 | 2279 | var materialType; 2280 | var materialParams = {}; 2281 | var materialExtensions = materialDef.extensions || {}; 2282 | 2283 | var pending = []; 2284 | 2285 | if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ] ) { 2286 | 2287 | var sgExtension = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ]; 2288 | materialType = sgExtension.getMaterialType( materialDef ); 2289 | pending.push( sgExtension.extendParams( materialParams, materialDef, parser ) ); 2290 | 2291 | } else if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ] ) { 2292 | 2293 | var kmuExtension = extensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ]; 2294 | materialType = kmuExtension.getMaterialType( materialDef ); 2295 | pending.push( kmuExtension.extendParams( materialParams, materialDef, parser ) ); 2296 | 2297 | } else { 2298 | 2299 | // Specification: 2300 | // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material 2301 | 2302 | materialType = THREE.MeshStandardMaterial; 2303 | 2304 | var metallicRoughness = materialDef.pbrMetallicRoughness || {}; 2305 | 2306 | materialParams.color = new THREE.Color( 1.0, 1.0, 1.0 ); 2307 | materialParams.opacity = 1.0; 2308 | 2309 | if ( Array.isArray( metallicRoughness.baseColorFactor ) ) { 2310 | 2311 | var array = metallicRoughness.baseColorFactor; 2312 | 2313 | materialParams.color.fromArray( array ); 2314 | materialParams.opacity = array[ 3 ]; 2315 | 2316 | } 2317 | 2318 | if ( metallicRoughness.baseColorTexture !== undefined ) { 2319 | 2320 | pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture ) ); 2321 | 2322 | } 2323 | 2324 | materialParams.metalness = metallicRoughness.metallicFactor !== undefined ? metallicRoughness.metallicFactor : 1.0; 2325 | materialParams.roughness = metallicRoughness.roughnessFactor !== undefined ? metallicRoughness.roughnessFactor : 1.0; 2326 | 2327 | if ( metallicRoughness.metallicRoughnessTexture !== undefined ) { 2328 | 2329 | pending.push( parser.assignTexture( materialParams, 'metalnessMap', metallicRoughness.metallicRoughnessTexture ) ); 2330 | pending.push( parser.assignTexture( materialParams, 'roughnessMap', metallicRoughness.metallicRoughnessTexture ) ); 2331 | 2332 | } 2333 | 2334 | } 2335 | 2336 | if ( materialDef.doubleSided === true ) { 2337 | 2338 | materialParams.side = THREE.DoubleSide; 2339 | 2340 | } 2341 | 2342 | var alphaMode = materialDef.alphaMode || ALPHA_MODES.OPAQUE; 2343 | 2344 | if ( alphaMode === ALPHA_MODES.BLEND ) { 2345 | 2346 | materialParams.transparent = true; 2347 | 2348 | } else { 2349 | 2350 | materialParams.transparent = false; 2351 | 2352 | if ( alphaMode === ALPHA_MODES.MASK ) { 2353 | 2354 | materialParams.alphaTest = materialDef.alphaCutoff !== undefined ? materialDef.alphaCutoff : 0.5; 2355 | 2356 | } 2357 | 2358 | } 2359 | 2360 | if ( materialDef.normalTexture !== undefined && materialType !== THREE.MeshBasicMaterial ) { 2361 | 2362 | pending.push( parser.assignTexture( materialParams, 'normalMap', materialDef.normalTexture ) ); 2363 | 2364 | materialParams.normalScale = new THREE.Vector2( 1, 1 ); 2365 | 2366 | if ( materialDef.normalTexture.scale !== undefined ) { 2367 | 2368 | materialParams.normalScale.set( materialDef.normalTexture.scale, materialDef.normalTexture.scale ); 2369 | 2370 | } 2371 | 2372 | } 2373 | 2374 | if ( materialDef.occlusionTexture !== undefined && materialType !== THREE.MeshBasicMaterial ) { 2375 | 2376 | pending.push( parser.assignTexture( materialParams, 'aoMap', materialDef.occlusionTexture ) ); 2377 | 2378 | if ( materialDef.occlusionTexture.strength !== undefined ) { 2379 | 2380 | materialParams.aoMapIntensity = materialDef.occlusionTexture.strength; 2381 | 2382 | } 2383 | 2384 | } 2385 | 2386 | if ( materialDef.emissiveFactor !== undefined && materialType !== THREE.MeshBasicMaterial ) { 2387 | 2388 | materialParams.emissive = new THREE.Color().fromArray( materialDef.emissiveFactor ); 2389 | 2390 | } 2391 | 2392 | if ( materialDef.emissiveTexture !== undefined && materialType !== THREE.MeshBasicMaterial ) { 2393 | 2394 | pending.push( parser.assignTexture( materialParams, 'emissiveMap', materialDef.emissiveTexture ) ); 2395 | 2396 | } 2397 | 2398 | return Promise.all( pending ).then( function () { 2399 | 2400 | var material; 2401 | 2402 | if ( materialType === THREE.ShaderMaterial ) { 2403 | 2404 | material = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].createMaterial( materialParams ); 2405 | 2406 | } else { 2407 | 2408 | material = new materialType( materialParams ); 2409 | 2410 | } 2411 | 2412 | if ( materialDef.name !== undefined ) material.name = materialDef.name; 2413 | 2414 | // baseColorTexture, emissiveTexture, and specularGlossinessTexture use sRGB encoding. 2415 | if ( material.map ) material.map.encoding = THREE.sRGBEncoding; 2416 | if ( material.emissiveMap ) material.emissiveMap.encoding = THREE.sRGBEncoding; 2417 | if ( material.specularMap ) material.specularMap.encoding = THREE.sRGBEncoding; 2418 | 2419 | assignExtrasToUserData( material, materialDef ); 2420 | 2421 | if ( materialDef.extensions ) addUnknownExtensionsToUserData( extensions, material, materialDef ); 2422 | 2423 | return material; 2424 | 2425 | } ); 2426 | 2427 | }; 2428 | 2429 | /** 2430 | * @param {THREE.BufferGeometry} geometry 2431 | * @param {GLTF.Primitive} primitiveDef 2432 | * @param {GLTFParser} parser 2433 | * @return {Promise} 2434 | */ 2435 | function addPrimitiveAttributes( geometry, primitiveDef, parser ) { 2436 | 2437 | var attributes = primitiveDef.attributes; 2438 | 2439 | var pending = []; 2440 | 2441 | function assignAttributeAccessor( accessorIndex, attributeName ) { 2442 | 2443 | return parser.getDependency( 'accessor', accessorIndex ) 2444 | .then( function ( accessor ) { 2445 | 2446 | geometry.addAttribute( attributeName, accessor ); 2447 | 2448 | } ); 2449 | 2450 | } 2451 | 2452 | for ( var gltfAttributeName in attributes ) { 2453 | 2454 | var threeAttributeName = ATTRIBUTES[ gltfAttributeName ] || gltfAttributeName.toLowerCase(); 2455 | 2456 | // Skip attributes already provided by e.g. Draco extension. 2457 | if ( threeAttributeName in geometry.attributes ) continue; 2458 | 2459 | pending.push( assignAttributeAccessor( attributes[ gltfAttributeName ], threeAttributeName ) ); 2460 | 2461 | } 2462 | 2463 | if ( primitiveDef.indices !== undefined && ! geometry.index ) { 2464 | 2465 | var accessor = parser.getDependency( 'accessor', primitiveDef.indices ).then( function ( accessor ) { 2466 | 2467 | geometry.setIndex( accessor ); 2468 | 2469 | } ); 2470 | 2471 | pending.push( accessor ); 2472 | 2473 | } 2474 | 2475 | assignExtrasToUserData( geometry, primitiveDef ); 2476 | 2477 | return Promise.all( pending ).then( function () { 2478 | 2479 | return primitiveDef.targets !== undefined 2480 | ? addMorphTargets( geometry, primitiveDef.targets, parser ) 2481 | : geometry; 2482 | 2483 | } ); 2484 | 2485 | } 2486 | 2487 | /** 2488 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry 2489 | * 2490 | * Creates BufferGeometries from primitives. 2491 | * 2492 | * @param {Array} primitives 2493 | * @return {Promise>} 2494 | */ 2495 | GLTFParser.prototype.loadGeometries = function ( primitives ) { 2496 | 2497 | var parser = this; 2498 | var extensions = this.extensions; 2499 | var cache = this.primitiveCache; 2500 | 2501 | function createDracoPrimitive( primitive ) { 2502 | 2503 | return extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] 2504 | .decodePrimitive( primitive, parser ) 2505 | .then( function ( geometry ) { 2506 | 2507 | return addPrimitiveAttributes( geometry, primitive, parser ); 2508 | 2509 | } ); 2510 | 2511 | } 2512 | 2513 | var pending = []; 2514 | 2515 | for ( var i = 0, il = primitives.length; i < il; i ++ ) { 2516 | 2517 | var primitive = primitives[ i ]; 2518 | var cacheKey = createPrimitiveKey( primitive ); 2519 | 2520 | // See if we've already created this geometry 2521 | var cached = cache[ cacheKey ]; 2522 | 2523 | if ( cached ) { 2524 | 2525 | // Use the cached geometry if it exists 2526 | pending.push( cached.promise ); 2527 | 2528 | } else { 2529 | 2530 | var geometryPromise; 2531 | 2532 | if ( primitive.extensions && primitive.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] ) { 2533 | 2534 | // Use DRACO geometry if available 2535 | geometryPromise = createDracoPrimitive( primitive ); 2536 | 2537 | } else { 2538 | 2539 | // Otherwise create a new geometry 2540 | geometryPromise = addPrimitiveAttributes( new THREE.BufferGeometry(), primitive, parser ); 2541 | 2542 | } 2543 | 2544 | // Cache this geometry 2545 | cache[ cacheKey ] = { primitive: primitive, promise: geometryPromise }; 2546 | 2547 | pending.push( geometryPromise ); 2548 | 2549 | } 2550 | 2551 | } 2552 | 2553 | return Promise.all( pending ); 2554 | 2555 | }; 2556 | 2557 | /** 2558 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#meshes 2559 | * @param {number} meshIndex 2560 | * @return {Promise} 2561 | */ 2562 | GLTFParser.prototype.loadMesh = function ( meshIndex ) { 2563 | 2564 | var parser = this; 2565 | var json = this.json; 2566 | var extensions = this.extensions; 2567 | 2568 | var meshDef = json.meshes[ meshIndex ]; 2569 | var primitives = meshDef.primitives; 2570 | 2571 | var pending = []; 2572 | 2573 | for ( var i = 0, il = primitives.length; i < il; i ++ ) { 2574 | 2575 | var material = primitives[ i ].material === undefined 2576 | ? createDefaultMaterial() 2577 | : this.getDependency( 'material', primitives[ i ].material ); 2578 | 2579 | pending.push( material ); 2580 | 2581 | } 2582 | 2583 | return Promise.all( pending ).then( function ( originalMaterials ) { 2584 | 2585 | return parser.loadGeometries( primitives ).then( function ( geometries ) { 2586 | 2587 | var meshes = []; 2588 | 2589 | for ( var i = 0, il = geometries.length; i < il; i ++ ) { 2590 | 2591 | var geometry = geometries[ i ]; 2592 | var primitive = primitives[ i ]; 2593 | 2594 | // 1. create Mesh 2595 | 2596 | var mesh; 2597 | 2598 | var material = originalMaterials[ i ]; 2599 | 2600 | if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLES || 2601 | primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP || 2602 | primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN || 2603 | primitive.mode === undefined ) { 2604 | 2605 | // .isSkinnedMesh isn't in glTF spec. See .markDefs() 2606 | mesh = meshDef.isSkinnedMesh === true 2607 | ? new THREE.SkinnedMesh( geometry, material ) 2608 | : new THREE.Mesh( geometry, material ); 2609 | 2610 | if ( mesh.isSkinnedMesh === true ) mesh.normalizeSkinWeights(); // #15319 2611 | 2612 | if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ) { 2613 | 2614 | mesh.drawMode = THREE.TriangleStripDrawMode; 2615 | 2616 | } else if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ) { 2617 | 2618 | mesh.drawMode = THREE.TriangleFanDrawMode; 2619 | 2620 | } 2621 | 2622 | } else if ( primitive.mode === WEBGL_CONSTANTS.LINES ) { 2623 | 2624 | mesh = new THREE.LineSegments( geometry, material ); 2625 | 2626 | } else if ( primitive.mode === WEBGL_CONSTANTS.LINE_STRIP ) { 2627 | 2628 | mesh = new THREE.Line( geometry, material ); 2629 | 2630 | } else if ( primitive.mode === WEBGL_CONSTANTS.LINE_LOOP ) { 2631 | 2632 | mesh = new THREE.LineLoop( geometry, material ); 2633 | 2634 | } else if ( primitive.mode === WEBGL_CONSTANTS.POINTS ) { 2635 | 2636 | mesh = new THREE.Points( geometry, material ); 2637 | 2638 | } else { 2639 | 2640 | throw new Error( 'THREE.GLTFLoader: Primitive mode unsupported: ' + primitive.mode ); 2641 | 2642 | } 2643 | 2644 | if ( Object.keys( mesh.geometry.morphAttributes ).length > 0 ) { 2645 | 2646 | updateMorphTargets( mesh, meshDef ); 2647 | 2648 | } 2649 | 2650 | mesh.name = meshDef.name || ( 'mesh_' + meshIndex ); 2651 | 2652 | if ( geometries.length > 1 ) mesh.name += '_' + i; 2653 | 2654 | assignExtrasToUserData( mesh, meshDef ); 2655 | 2656 | parser.assignFinalMaterial( mesh ); 2657 | 2658 | meshes.push( mesh ); 2659 | 2660 | } 2661 | 2662 | if ( meshes.length === 1 ) { 2663 | 2664 | return meshes[ 0 ]; 2665 | 2666 | } 2667 | 2668 | var group = new THREE.Group(); 2669 | 2670 | for ( var i = 0, il = meshes.length; i < il; i ++ ) { 2671 | 2672 | group.add( meshes[ i ] ); 2673 | 2674 | } 2675 | 2676 | return group; 2677 | 2678 | } ); 2679 | 2680 | } ); 2681 | 2682 | }; 2683 | 2684 | /** 2685 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#cameras 2686 | * @param {number} cameraIndex 2687 | * @return {Promise} 2688 | */ 2689 | GLTFParser.prototype.loadCamera = function ( cameraIndex ) { 2690 | 2691 | var camera; 2692 | var cameraDef = this.json.cameras[ cameraIndex ]; 2693 | var params = cameraDef[ cameraDef.type ]; 2694 | 2695 | if ( ! params ) { 2696 | 2697 | console.warn( 'THREE.GLTFLoader: Missing camera parameters.' ); 2698 | return; 2699 | 2700 | } 2701 | 2702 | if ( cameraDef.type === 'perspective' ) { 2703 | 2704 | camera = new THREE.PerspectiveCamera( THREE.Math.radToDeg( params.yfov ), params.aspectRatio || 1, params.znear || 1, params.zfar || 2e6 ); 2705 | 2706 | } else if ( cameraDef.type === 'orthographic' ) { 2707 | 2708 | camera = new THREE.OrthographicCamera( params.xmag / - 2, params.xmag / 2, params.ymag / 2, params.ymag / - 2, params.znear, params.zfar ); 2709 | 2710 | } 2711 | 2712 | if ( cameraDef.name !== undefined ) camera.name = cameraDef.name; 2713 | 2714 | assignExtrasToUserData( camera, cameraDef ); 2715 | 2716 | return Promise.resolve( camera ); 2717 | 2718 | }; 2719 | 2720 | /** 2721 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#skins 2722 | * @param {number} skinIndex 2723 | * @return {Promise} 2724 | */ 2725 | GLTFParser.prototype.loadSkin = function ( skinIndex ) { 2726 | 2727 | var skinDef = this.json.skins[ skinIndex ]; 2728 | 2729 | var skinEntry = { joints: skinDef.joints }; 2730 | 2731 | if ( skinDef.inverseBindMatrices === undefined ) { 2732 | 2733 | return Promise.resolve( skinEntry ); 2734 | 2735 | } 2736 | 2737 | return this.getDependency( 'accessor', skinDef.inverseBindMatrices ).then( function ( accessor ) { 2738 | 2739 | skinEntry.inverseBindMatrices = accessor; 2740 | 2741 | return skinEntry; 2742 | 2743 | } ); 2744 | 2745 | }; 2746 | 2747 | /** 2748 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations 2749 | * @param {number} animationIndex 2750 | * @return {Promise} 2751 | */ 2752 | GLTFParser.prototype.loadAnimation = function ( animationIndex ) { 2753 | 2754 | var json = this.json; 2755 | 2756 | var animationDef = json.animations[ animationIndex ]; 2757 | 2758 | var pendingNodes = []; 2759 | var pendingInputAccessors = []; 2760 | var pendingOutputAccessors = []; 2761 | var pendingSamplers = []; 2762 | var pendingTargets = []; 2763 | 2764 | for ( var i = 0, il = animationDef.channels.length; i < il; i ++ ) { 2765 | 2766 | var channel = animationDef.channels[ i ]; 2767 | var sampler = animationDef.samplers[ channel.sampler ]; 2768 | var target = channel.target; 2769 | var name = target.node !== undefined ? target.node : target.id; // NOTE: target.id is deprecated. 2770 | var input = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.input ] : sampler.input; 2771 | var output = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.output ] : sampler.output; 2772 | 2773 | pendingNodes.push( this.getDependency( 'node', name ) ); 2774 | pendingInputAccessors.push( this.getDependency( 'accessor', input ) ); 2775 | pendingOutputAccessors.push( this.getDependency( 'accessor', output ) ); 2776 | pendingSamplers.push( sampler ); 2777 | pendingTargets.push( target ); 2778 | 2779 | } 2780 | 2781 | return Promise.all( [ 2782 | 2783 | Promise.all( pendingNodes ), 2784 | Promise.all( pendingInputAccessors ), 2785 | Promise.all( pendingOutputAccessors ), 2786 | Promise.all( pendingSamplers ), 2787 | Promise.all( pendingTargets ) 2788 | 2789 | ] ).then( function ( dependencies ) { 2790 | 2791 | var nodes = dependencies[ 0 ]; 2792 | var inputAccessors = dependencies[ 1 ]; 2793 | var outputAccessors = dependencies[ 2 ]; 2794 | var samplers = dependencies[ 3 ]; 2795 | var targets = dependencies[ 4 ]; 2796 | 2797 | var tracks = []; 2798 | 2799 | for ( var i = 0, il = nodes.length; i < il; i ++ ) { 2800 | 2801 | var node = nodes[ i ]; 2802 | var inputAccessor = inputAccessors[ i ]; 2803 | var outputAccessor = outputAccessors[ i ]; 2804 | var sampler = samplers[ i ]; 2805 | var target = targets[ i ]; 2806 | 2807 | if ( node === undefined ) continue; 2808 | 2809 | node.updateMatrix(); 2810 | node.matrixAutoUpdate = true; 2811 | 2812 | var TypedKeyframeTrack; 2813 | 2814 | switch ( PATH_PROPERTIES[ target.path ] ) { 2815 | 2816 | case PATH_PROPERTIES.weights: 2817 | 2818 | TypedKeyframeTrack = THREE.NumberKeyframeTrack; 2819 | break; 2820 | 2821 | case PATH_PROPERTIES.rotation: 2822 | 2823 | TypedKeyframeTrack = THREE.QuaternionKeyframeTrack; 2824 | break; 2825 | 2826 | case PATH_PROPERTIES.position: 2827 | case PATH_PROPERTIES.scale: 2828 | default: 2829 | 2830 | TypedKeyframeTrack = THREE.VectorKeyframeTrack; 2831 | break; 2832 | 2833 | } 2834 | 2835 | var targetName = node.name ? node.name : node.uuid; 2836 | 2837 | var interpolation = sampler.interpolation !== undefined ? INTERPOLATION[ sampler.interpolation ] : THREE.InterpolateLinear; 2838 | 2839 | var targetNames = []; 2840 | 2841 | if ( PATH_PROPERTIES[ target.path ] === PATH_PROPERTIES.weights ) { 2842 | 2843 | // Node may be a THREE.Group (glTF mesh with several primitives) or a THREE.Mesh. 2844 | node.traverse( function ( object ) { 2845 | 2846 | if ( object.isMesh === true && object.morphTargetInfluences ) { 2847 | 2848 | targetNames.push( object.name ? object.name : object.uuid ); 2849 | 2850 | } 2851 | 2852 | } ); 2853 | 2854 | } else { 2855 | 2856 | targetNames.push( targetName ); 2857 | 2858 | } 2859 | 2860 | for ( var j = 0, jl = targetNames.length; j < jl; j ++ ) { 2861 | 2862 | var track = new TypedKeyframeTrack( 2863 | targetNames[ j ] + '.' + PATH_PROPERTIES[ target.path ], 2864 | inputAccessor.array, 2865 | outputAccessor.array, 2866 | interpolation 2867 | ); 2868 | 2869 | // Override interpolation with custom factory method. 2870 | if ( sampler.interpolation === 'CUBICSPLINE' ) { 2871 | 2872 | track.createInterpolant = function InterpolantFactoryMethodGLTFCubicSpline( result ) { 2873 | 2874 | // A CUBICSPLINE keyframe in glTF has three output values for each input value, 2875 | // representing inTangent, splineVertex, and outTangent. As a result, track.getValueSize() 2876 | // must be divided by three to get the interpolant's sampleSize argument. 2877 | 2878 | return new GLTFCubicSplineInterpolant( this.times, this.values, this.getValueSize() / 3, result ); 2879 | 2880 | }; 2881 | 2882 | // Mark as CUBICSPLINE. `track.getInterpolation()` doesn't support custom interpolants. 2883 | track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline = true; 2884 | 2885 | } 2886 | 2887 | tracks.push( track ); 2888 | 2889 | } 2890 | 2891 | } 2892 | 2893 | var name = animationDef.name !== undefined ? animationDef.name : 'animation_' + animationIndex; 2894 | 2895 | return new THREE.AnimationClip( name, undefined, tracks ); 2896 | 2897 | } ); 2898 | 2899 | }; 2900 | 2901 | /** 2902 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodes-and-hierarchy 2903 | * @param {number} nodeIndex 2904 | * @return {Promise} 2905 | */ 2906 | GLTFParser.prototype.loadNode = function ( nodeIndex ) { 2907 | 2908 | var json = this.json; 2909 | var extensions = this.extensions; 2910 | var parser = this; 2911 | 2912 | var meshReferences = json.meshReferences; 2913 | var meshUses = json.meshUses; 2914 | 2915 | var nodeDef = json.nodes[ nodeIndex ]; 2916 | 2917 | return ( function () { 2918 | 2919 | // .isBone isn't in glTF spec. See .markDefs 2920 | if ( nodeDef.isBone === true ) { 2921 | 2922 | return Promise.resolve( new THREE.Bone() ); 2923 | 2924 | } else if ( nodeDef.mesh !== undefined ) { 2925 | 2926 | return parser.getDependency( 'mesh', nodeDef.mesh ).then( function ( mesh ) { 2927 | 2928 | var node; 2929 | 2930 | if ( meshReferences[ nodeDef.mesh ] > 1 ) { 2931 | 2932 | var instanceNum = meshUses[ nodeDef.mesh ] ++; 2933 | 2934 | node = mesh.clone(); 2935 | node.name += '_instance_' + instanceNum; 2936 | 2937 | // onBeforeRender copy for Specular-Glossiness 2938 | node.onBeforeRender = mesh.onBeforeRender; 2939 | 2940 | for ( var i = 0, il = node.children.length; i < il; i ++ ) { 2941 | 2942 | node.children[ i ].name += '_instance_' + instanceNum; 2943 | node.children[ i ].onBeforeRender = mesh.children[ i ].onBeforeRender; 2944 | 2945 | } 2946 | 2947 | } else { 2948 | 2949 | node = mesh; 2950 | 2951 | } 2952 | 2953 | // if weights are provided on the node, override weights on the mesh. 2954 | if ( nodeDef.weights !== undefined ) { 2955 | 2956 | node.traverse( function ( o ) { 2957 | 2958 | if ( ! o.isMesh ) return; 2959 | 2960 | for ( var i = 0, il = nodeDef.weights.length; i < il; i ++ ) { 2961 | 2962 | o.morphTargetInfluences[ i ] = nodeDef.weights[ i ]; 2963 | 2964 | } 2965 | 2966 | } ); 2967 | 2968 | } 2969 | 2970 | return node; 2971 | 2972 | } ); 2973 | 2974 | } else if ( nodeDef.camera !== undefined ) { 2975 | 2976 | return parser.getDependency( 'camera', nodeDef.camera ); 2977 | 2978 | } else if ( nodeDef.extensions 2979 | && nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS_PUNCTUAL ] 2980 | && nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS_PUNCTUAL ].light !== undefined ) { 2981 | 2982 | return parser.getDependency( 'light', nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS_PUNCTUAL ].light ); 2983 | 2984 | } else { 2985 | 2986 | return Promise.resolve( new THREE.Object3D() ); 2987 | 2988 | } 2989 | 2990 | }() ).then( function ( node ) { 2991 | 2992 | if ( nodeDef.name !== undefined ) { 2993 | 2994 | node.name = THREE.PropertyBinding.sanitizeNodeName( nodeDef.name ); 2995 | 2996 | } 2997 | 2998 | assignExtrasToUserData( node, nodeDef ); 2999 | 3000 | if ( nodeDef.extensions ) addUnknownExtensionsToUserData( extensions, node, nodeDef ); 3001 | 3002 | if ( nodeDef.matrix !== undefined ) { 3003 | 3004 | var matrix = new THREE.Matrix4(); 3005 | matrix.fromArray( nodeDef.matrix ); 3006 | node.applyMatrix( matrix ); 3007 | 3008 | } else { 3009 | 3010 | if ( nodeDef.translation !== undefined ) { 3011 | 3012 | node.position.fromArray( nodeDef.translation ); 3013 | 3014 | } 3015 | 3016 | if ( nodeDef.rotation !== undefined ) { 3017 | 3018 | node.quaternion.fromArray( nodeDef.rotation ); 3019 | 3020 | } 3021 | 3022 | if ( nodeDef.scale !== undefined ) { 3023 | 3024 | node.scale.fromArray( nodeDef.scale ); 3025 | 3026 | } 3027 | 3028 | } 3029 | 3030 | return node; 3031 | 3032 | } ); 3033 | 3034 | }; 3035 | 3036 | /** 3037 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenes 3038 | * @param {number} sceneIndex 3039 | * @return {Promise} 3040 | */ 3041 | GLTFParser.prototype.loadScene = function () { 3042 | 3043 | // scene node hierachy builder 3044 | 3045 | function buildNodeHierachy( nodeId, parentObject, json, parser ) { 3046 | 3047 | var nodeDef = json.nodes[ nodeId ]; 3048 | 3049 | return parser.getDependency( 'node', nodeId ).then( function ( node ) { 3050 | 3051 | if ( nodeDef.skin === undefined ) return node; 3052 | 3053 | // build skeleton here as well 3054 | 3055 | var skinEntry; 3056 | 3057 | return parser.getDependency( 'skin', nodeDef.skin ).then( function ( skin ) { 3058 | 3059 | skinEntry = skin; 3060 | 3061 | var pendingJoints = []; 3062 | 3063 | for ( var i = 0, il = skinEntry.joints.length; i < il; i ++ ) { 3064 | 3065 | pendingJoints.push( parser.getDependency( 'node', skinEntry.joints[ i ] ) ); 3066 | 3067 | } 3068 | 3069 | return Promise.all( pendingJoints ); 3070 | 3071 | } ).then( function ( jointNodes ) { 3072 | 3073 | var meshes = node.isGroup === true ? node.children : [ node ]; 3074 | 3075 | for ( var i = 0, il = meshes.length; i < il; i ++ ) { 3076 | 3077 | var mesh = meshes[ i ]; 3078 | 3079 | var bones = []; 3080 | var boneInverses = []; 3081 | 3082 | for ( var j = 0, jl = jointNodes.length; j < jl; j ++ ) { 3083 | 3084 | var jointNode = jointNodes[ j ]; 3085 | 3086 | if ( jointNode ) { 3087 | 3088 | bones.push( jointNode ); 3089 | 3090 | var mat = new THREE.Matrix4(); 3091 | 3092 | if ( skinEntry.inverseBindMatrices !== undefined ) { 3093 | 3094 | mat.fromArray( skinEntry.inverseBindMatrices.array, j * 16 ); 3095 | 3096 | } 3097 | 3098 | boneInverses.push( mat ); 3099 | 3100 | } else { 3101 | 3102 | console.warn( 'THREE.GLTFLoader: Joint "%s" could not be found.', skinEntry.joints[ j ] ); 3103 | 3104 | } 3105 | 3106 | } 3107 | 3108 | mesh.bind( new THREE.Skeleton( bones, boneInverses ), mesh.matrixWorld ); 3109 | 3110 | } 3111 | 3112 | return node; 3113 | 3114 | } ); 3115 | 3116 | } ).then( function ( node ) { 3117 | 3118 | // build node hierachy 3119 | 3120 | parentObject.add( node ); 3121 | 3122 | var pending = []; 3123 | 3124 | if ( nodeDef.children ) { 3125 | 3126 | var children = nodeDef.children; 3127 | 3128 | for ( var i = 0, il = children.length; i < il; i ++ ) { 3129 | 3130 | var child = children[ i ]; 3131 | pending.push( buildNodeHierachy( child, node, json, parser ) ); 3132 | 3133 | } 3134 | 3135 | } 3136 | 3137 | return Promise.all( pending ); 3138 | 3139 | } ); 3140 | 3141 | } 3142 | 3143 | return function loadScene( sceneIndex ) { 3144 | 3145 | var json = this.json; 3146 | var extensions = this.extensions; 3147 | var sceneDef = this.json.scenes[ sceneIndex ]; 3148 | var parser = this; 3149 | 3150 | var scene = new THREE.Scene(); 3151 | if ( sceneDef.name !== undefined ) scene.name = sceneDef.name; 3152 | 3153 | assignExtrasToUserData( scene, sceneDef ); 3154 | 3155 | if ( sceneDef.extensions ) addUnknownExtensionsToUserData( extensions, scene, sceneDef ); 3156 | 3157 | var nodeIds = sceneDef.nodes || []; 3158 | 3159 | var pending = []; 3160 | 3161 | for ( var i = 0, il = nodeIds.length; i < il; i ++ ) { 3162 | 3163 | pending.push( buildNodeHierachy( nodeIds[ i ], scene, json, parser ) ); 3164 | 3165 | } 3166 | 3167 | return Promise.all( pending ).then( function () { 3168 | 3169 | return scene; 3170 | 3171 | } ); 3172 | 3173 | }; 3174 | 3175 | }(); 3176 | 3177 | return GLTFLoader; 3178 | 3179 | } )(); 3180 | -------------------------------------------------------------------------------- /src/libs/Phaser3D.js: -------------------------------------------------------------------------------- 1 | export class Phaser3D extends Phaser.Events.EventEmitter { 2 | constructor(phaserScene, { fov = 75, aspect = null, near = 0.1, far = 1000, x = 0, y = 0, z = 0, anisotropy = 1, antialias = false } = {}) { 3 | super(); 4 | 5 | this.root = phaserScene; 6 | 7 | this.view = phaserScene.add.extern(); 8 | 9 | this.scene = new THREE.Scene(); 10 | 11 | this.textureAnisotropy = anisotropy; 12 | 13 | if (!aspect) { 14 | aspect = phaserScene.scale.gameSize.aspectRatio; 15 | } 16 | 17 | this.camera = new THREE.PerspectiveCamera(fov, aspect, near, far); 18 | 19 | this.camera.position.set(x, y, z); 20 | 21 | this.renderer = new THREE.WebGLRenderer({ 22 | canvas: phaserScene.sys.game.canvas, 23 | context: phaserScene.sys.game.context, 24 | antialias: antialias 25 | }); 26 | 27 | // We don't want three.js to wipe our gl context! 28 | this.renderer.autoClear = false; 29 | 30 | // Create our Extern render callback 31 | this.view.render = () => { 32 | 33 | // This is important to retain GL state between renders 34 | this.renderer.state.reset(); 35 | 36 | this.renderer.render(this.scene, this.camera); 37 | 38 | }; 39 | 40 | // Some basic factory helpers 41 | this.add = { 42 | 43 | // Lights 44 | ambientLight: (config) => this.addAmbientLight(config), 45 | directionalLight: (config) => this.addDirectionalLight(config), 46 | hemisphereLight: (config) => this.addHemisphereLight(config), 47 | pointLight: (config) => this.addPointLight(config), 48 | spotLight: (config) => this.addSpotLight(config), 49 | 50 | mesh: (mesh) => this.addMesh(mesh), 51 | group: (...children) => this.addGroup(children), 52 | 53 | // Geometry 54 | ground: (config) => this.addGround(config), 55 | box: (config) => this.addBox(config), 56 | cone: (config) => this.addCone(config), 57 | circle: (config) => this.addCircle(config), 58 | cylinder: (config) => this.addCylinder(config), 59 | dodecahedron: (config) => this.addDodecahedron(config), 60 | extrude: (config) => this.addExtrude(config), 61 | lathe: (config) => this.addLathe(config), 62 | icosahedron: (config) => this.addIcosahedron(config), 63 | plane: (config) => this.addPlane(config), 64 | parametric: (config) => this.addParametric(config), 65 | ring: (config) => this.addRing(config), 66 | sphere: (config) => this.addSphere(config), 67 | text: (config) => this.addText(config), 68 | tube: (config) => this.addTube(config), 69 | octahedron: (config) => this.addOctahedron(config), 70 | polyhedron: (config) => this.addPolyhedron(config), 71 | shape: (config) => this.addShape(config), 72 | tetrahedron: (config) => this.addTetrahedron(config), 73 | torus: (config) => this.addTorus(config), 74 | torusKnot: (config) => this.addTorusKnot(config), 75 | }; 76 | 77 | // Some basic factory helpers 78 | this.make = { 79 | box: (config) => this.makeBox(config), 80 | cone: (config) => this.makeCone(config), 81 | circle: (config) => this.makeCircle(config), 82 | cylinder: (config) => this.makeCylinder(config), 83 | dodecahedron: (config) => this.makeDodecahedron(config), 84 | extrude: (config) => this.makeExtrude(config), 85 | icosahedron: (config) => this.makeIcosahedron(config), 86 | lathe: (config) => this.makeLathe(config), 87 | plane: (config) => this.makePlane(config), 88 | parametric: (config) => this.makeParametric(config), 89 | ring: (config) => this.makeRing(config), 90 | sphere: (config) => this.makeSphere(config), 91 | shape: (config) => this.makeShape(config), 92 | text: (config) => this.makeText(config), 93 | tube: (config) => this.makeTube(config), 94 | octahedron: (config) => this.makeOctahedron(config), 95 | polyhedron: (config) => this.makePolyhedron(config), 96 | tetrahedron: (config) => this.makeTetrahedron(config), 97 | torus: (config) => this.makeTorus(config), 98 | torusKnot: (config) => this.makeTorusKnot(config), 99 | }; 100 | } 101 | 102 | enableFog(color = 0x000000, near = 1, far = 1000) { 103 | this.scene.fog = new THREE.Fog(color, near, far); 104 | 105 | return this; 106 | } 107 | 108 | enableFogExp2(color = 0x000000, density = 0.00025) { 109 | this.scene.fog = new THREE.FogExp2(color, density); 110 | 111 | return this; 112 | } 113 | 114 | enableGamma(input = true, output = true) { 115 | this.renderer.gammaInput = input; 116 | this.renderer.gammaOutput = output; 117 | 118 | return this; 119 | } 120 | 121 | enableShadows(type = THREE.PCFShadowMap) { 122 | this.renderer.shadowMap.enabled = true; 123 | this.renderer.shadowMap.type = type; 124 | 125 | return this; 126 | } 127 | 128 | setCubeBackground(...files) { 129 | this.scene.background = this.createCubeTexture(...files) 130 | 131 | return this; 132 | } 133 | 134 | // three.js uses a right-handed coordinate system! 135 | // So cubemaps found on the web likely need their nx/px swapped and ny = rotate 90 deg clockwise and py = rotate 90 deg counter clockwise 136 | createCubeTexture(path, right = 'px.png', left = 'nx.png', up = 'py.png', down = 'ny.png', back = 'pz.png', front = 'nz.png') { 137 | if (path.substr(-1) !== '/') { 138 | path = path.concat('/'); 139 | } 140 | 141 | return new THREE.CubeTextureLoader().setPath(path).load([right, left, up, down, back, front]); 142 | } 143 | 144 | castShadow(...meshes) { 145 | for (const mesh of meshes.values()) { 146 | mesh.castShadow = true; 147 | } 148 | 149 | return this; 150 | } 151 | 152 | receiveShadow(...meshes) { 153 | for (const mesh of meshes.values()) { 154 | mesh.receiveShadow = true; 155 | } 156 | 157 | return this; 158 | } 159 | 160 | setShadow(light, width = 512, height = 512, near = 1, far = 1000) { 161 | light.castShadow = true; 162 | 163 | light.shadow.mapSize.width = width; 164 | light.shadow.mapSize.height = height; 165 | 166 | light.shadow.camera.near = near; 167 | light.shadow.camera.far = far; 168 | 169 | return light; 170 | } 171 | 172 | addGLTFModel(key, resourcePath, onLoad) { 173 | const data = this.root.cache.binary.get(key); 174 | 175 | const loader = new THREE.GLTFLoader(); 176 | 177 | loader.parse(data, resourcePath, (gltf) => { 178 | const group = new THREE.Group(); 179 | 180 | for (let i = gltf.scene.children.length - 1; i > -1; i--) { 181 | group.add(gltf.scene.children[i]); 182 | } 183 | 184 | this.scene.add(group); 185 | 186 | this.emit('loadgltf', gltf, group); 187 | 188 | if (onLoad) { 189 | onLoad(gltf, group); 190 | } 191 | 192 | }); 193 | } 194 | 195 | parseGLTFModel(key, resourcePath, onLoad) { 196 | const data = this.root.cache.binary.get(key); 197 | 198 | const loader = new THREE.GLTFLoader(); 199 | 200 | loader.parse(data, resourcePath, (gltf) => { 201 | 202 | this.emit('loadgltf', gltf); 203 | 204 | if (onLoad) { 205 | onLoad(gltf); 206 | } 207 | 208 | }); 209 | } 210 | 211 | getTexture(key) { 212 | let texture = new THREE.Texture(); 213 | 214 | texture.image = this.root.textures.get(key).getSourceImage(); 215 | 216 | texture.format = THREE.RGBAFormat; 217 | texture.needsUpdate = true; 218 | texture.anisotropy = this.textureAnisotropy; 219 | 220 | return texture; 221 | } 222 | 223 | addHemisphereLight({ skyColor = 0xffffff, groundColor = 0x000000, intensity = 1 } = {}) { 224 | const light = new THREE.HemisphereLight(skyColor, groundColor, intensity); 225 | 226 | this.scene.add(light); 227 | 228 | return light; 229 | } 230 | 231 | addAmbientLight({ color = 0xffffff, intensity = 1 } = {}) { 232 | const light = new THREE.AmbientLight(color, intensity); 233 | 234 | this.scene.add(light); 235 | 236 | return light; 237 | } 238 | 239 | addDirectionalLight({ color = 0xffffff, intensity = 1, x = 0, y = 0, z = 0 } = {}) { 240 | const light = new THREE.DirectionalLight(color, intensity); 241 | 242 | light.position.set(x, y, z); 243 | 244 | this.scene.add(light); 245 | 246 | return light; 247 | } 248 | 249 | addPointLight({ color = 0xffffff, intensity = 1, distance = 0, decay = 1, x = 0, y = 0, z = 0 } = {}) { 250 | const light = new THREE.PointLight(color, intensity, distance, decay); 251 | 252 | light.position.set(x, y, z); 253 | 254 | this.scene.add(light); 255 | 256 | return light; 257 | } 258 | 259 | addSpotLight({ color = 0xffffff, intensity = 1, distance = 0, angle = Math.PI / 4, penumbra = 0.05, decay = 1, x = 0, y = 0, z = 0 } = {}) { 260 | const light = new THREE.SpotLight(color, intensity, distance, angle, penumbra, decay); 261 | 262 | light.position.set(x, y, z); 263 | 264 | this.scene.add(light); 265 | 266 | return light; 267 | } 268 | 269 | addGroup(children) { 270 | const group = new THREE.Group(); 271 | 272 | if (Array.isArray(children)) { 273 | for (let i = 0; i < children.length; i++) { 274 | group.add(children[i]); 275 | } 276 | } 277 | 278 | this.scene.add(group); 279 | 280 | return group 281 | } 282 | 283 | addMesh(mesh) { 284 | if (Array.isArray(mesh)) { 285 | for (let i = 0; i < mesh.length; i++) { 286 | this.scene.add(mesh[i]); 287 | } 288 | } 289 | else { 290 | this.scene.add(mesh); 291 | } 292 | 293 | 294 | return this; 295 | } 296 | 297 | createTexture({ key, wrap, wrapS = THREE.ClampToEdgeWrapping, wrapT = THREE.ClampToEdgeWrapping, repeatX = 2, repeatY = 2 } = {}) { 298 | const texture = this.getTexture(key); 299 | 300 | if (wrap) { 301 | wrapS = wrap; 302 | wrapT = wrap; 303 | } 304 | 305 | texture.wrapS = wrapS; 306 | texture.wrapT = wrapT; 307 | texture.repeat.set(repeatX, repeatY); 308 | texture.premultiplyAlpha = true; 309 | 310 | return texture; 311 | } 312 | 313 | createMaterial(texture, color, material) { 314 | if (material && !Phaser.Utils.Objects.IsPlainObject(material)) { 315 | return material; 316 | } 317 | else { 318 | if (material === null) { 319 | material = {}; 320 | } 321 | 322 | let config = { ...material }; 323 | 324 | if (texture) { 325 | config.map = (typeof (texture) === 'string') ? this.getTexture(texture) : texture; 326 | } 327 | 328 | if (color) { 329 | config.color = color; 330 | } 331 | 332 | const isBasic = config.basic; 333 | const isPhong = config.phong; 334 | const isLine = config.line; 335 | 336 | delete config.basic; 337 | delete config.phong; 338 | delete config.line; 339 | 340 | if (isBasic) { 341 | return new THREE.MeshBasicMaterial(config); 342 | } 343 | else if (isPhong) { 344 | return new THREE.MeshPhongMaterial(config); 345 | } 346 | else if (isLine) { 347 | return new THREE.LineBasicMaterial(config); 348 | } 349 | else { 350 | return new THREE.MeshStandardMaterial(config); 351 | } 352 | } 353 | } 354 | 355 | createMesh(geometry, material, x = 0, y = 0, z = 0) { 356 | let obj = new THREE.Mesh(geometry, material); 357 | 358 | obj.position.set(x, y, z); 359 | 360 | return obj; 361 | } 362 | 363 | makeText({ text = '', font = '', size = 100, height = 50, curveSegments = 12, bevelEnabled = false, bevelThickness = 10, bevelSize = 8, bevelSegments = 3, texture = null, color = 0xffffff, material = null, x = 0, y = 0, z = 0 } = {}) { 364 | font = new THREE.Font(this.root.sys.cache.json.get(font)); 365 | 366 | const geometry = new THREE.TextBufferGeometry(text, { font, size, height, curveSegments, bevelEnabled, bevelThickness, bevelSize, bevelSegments }); 367 | 368 | return this.createMesh(geometry, this.createMaterial(texture, color, material), x, y, z); 369 | } 370 | 371 | addText(config) { 372 | const obj = this.makeText(config); 373 | 374 | this.scene.add(obj); 375 | 376 | return obj; 377 | } 378 | 379 | makeCircle({ radius = 1, segments = 8, thetaStart = 0, thetaLength = Math.PI * 2, texture = null, color = 0xffffff, material = null, x = 0, y = 0, z = 0 } = {}) { 380 | const geometry = new THREE.CircleBufferGeometry(radius, segments, thetaStart, thetaLength); 381 | 382 | return this.createMesh(geometry, this.createMaterial(texture, color, material), x, y, z); 383 | } 384 | 385 | addCircle(config) { 386 | const obj = this.makeCircle(config); 387 | 388 | this.scene.add(obj); 389 | 390 | return obj; 391 | } 392 | 393 | makeRing({ innerRadius = 0.5, outerRadius = 1, thetaSegments = 8, phiSegments = 1, thetaStart = 0, thetaLength = Math.PI * 2, texture = null, color = 0xffffff, material = null, x = 0, y = 0, z = 0 } = {}) { 394 | const geometry = new THREE.RingBufferGeometry(innerRadius, outerRadius, thetaSegments, phiSegments, thetaStart, thetaLength); 395 | 396 | return this.createMesh(geometry, this.createMaterial(texture, color, material), x, y, z); 397 | } 398 | 399 | addRing(config) { 400 | const obj = this.makeRing(config); 401 | 402 | this.scene.add(obj); 403 | 404 | return obj; 405 | } 406 | 407 | makeExtrude({ shapes, options, texture = null, color = 0xffffff, material = null, x = 0, y = 0, z = 0 } = {}) { 408 | const geometry = new THREE.ExtrudeBufferGeometry(shapes, options); 409 | 410 | return this.createMesh(geometry, this.createMaterial(texture, color, material), x, y, z); 411 | } 412 | 413 | addExtrude(config) { 414 | const obj = this.makeExtrude(config); 415 | 416 | this.scene.add(obj); 417 | 418 | return obj; 419 | } 420 | 421 | makeShape({ shapes, curveSegments = 12, texture = null, color = 0xffffff, material = null, x = 0, y = 0, z = 0 } = {}) { 422 | const geometry = new THREE.ShapeBufferGeometry(shapes, curveSegments); 423 | 424 | return this.createMesh(geometry, this.createMaterial(texture, color, material), x, y, z); 425 | } 426 | 427 | addShape(config) { 428 | const obj = this.makeShape(config); 429 | 430 | this.scene.add(obj); 431 | 432 | return obj; 433 | } 434 | 435 | makeLathe({ points, texture = null, color = 0xffffff, material = null, x = 0, y = 0, z = 0 } = {}) { 436 | const geometry = new THREE.LatheBufferGeometry(points); 437 | 438 | return this.createMesh(geometry, this.createMaterial(texture, color, material), x, y, z); 439 | } 440 | 441 | addLathe(config) { 442 | const obj = this.makeLathe(config); 443 | 444 | this.scene.add(obj); 445 | 446 | return obj; 447 | } 448 | 449 | makeTube({ path, tubularSegments = 64, radius = 1, radialSegments = 8, closed = false, texture = null, color = 0xffffff, material = null, x = 0, y = 0, z = 0 } = {}) { 450 | const geometry = new THREE.TubeBufferGeometry(path, tubularSegments, radius, radialSegments, closed); 451 | 452 | return this.createMesh(geometry, this.createMaterial(texture, color, material), x, y, z); 453 | } 454 | 455 | addTube(config) { 456 | const obj = this.makeTube(config); 457 | 458 | this.scene.add(obj); 459 | 460 | return obj; 461 | } 462 | 463 | makeParametric({ func, slices, stacks, texture = null, color = 0xffffff, material = null, x = 0, y = 0, z = 0 } = {}) { 464 | const geometry = new THREE.ParametricBufferGeometry(func, slices, stacks); 465 | 466 | return this.createMesh(geometry, this.createMaterial(texture, color, material), x, y, z); 467 | } 468 | 469 | addParametric(config) { 470 | const obj = this.makeParametric(config); 471 | 472 | this.scene.add(obj); 473 | 474 | return obj; 475 | } 476 | 477 | makeTorus({ radius = 1, tube = 0.4, radialSegments = 8, tubularSegments = 6, arc = Math.PI * 2, texture = null, color = 0xffffff, material = null, x = 0, y = 0, z = 0 } = {}) { 478 | const geometry = new THREE.TorusBufferGeometry(radius, tube, radialSegments, tubularSegments, arc); 479 | 480 | return this.createMesh(geometry, this.createMaterial(texture, color, material), x, y, z); 481 | } 482 | 483 | addTorus(config) { 484 | const obj = this.makeTorus(config); 485 | 486 | this.scene.add(obj); 487 | 488 | return obj; 489 | } 490 | 491 | makePolyhedron({ vertices = [], indices = [], radius = 6, detail = 2, texture = null, color = 0xffffff, material = null, x = 0, y = 0, z = 0 } = {}) { 492 | const geometry = new THREE.PolyhedronBufferGeometry(vertices, indices, radius, detail); 493 | 494 | return this.createMesh(geometry, this.createMaterial(texture, color, material), x, y, z); 495 | } 496 | 497 | addPolyhedron(config) { 498 | const obj = this.makePolyhedron(config); 499 | 500 | this.scene.add(obj); 501 | 502 | return obj; 503 | } 504 | 505 | makeOctahedron({ radius = 1, detail = 0, texture = null, color = 0xffffff, material = null, x = 0, y = 0, z = 0 } = {}) { 506 | const geometry = new THREE.OctahedronBufferGeometry(radius, detail); 507 | 508 | return this.createMesh(geometry, this.createMaterial(texture, color, material), x, y, z); 509 | } 510 | 511 | addOctahedron(config) { 512 | const obj = this.makeOctahedron(config); 513 | 514 | this.scene.add(obj); 515 | 516 | return obj; 517 | } 518 | 519 | makeIcosahedron({ radius = 1, detail = 0, texture = null, color = 0xffffff, material = null, x = 0, y = 0, z = 0 } = {}) { 520 | const geometry = new THREE.IcosahedronBufferGeometry(radius, detail); 521 | 522 | return this.createMesh(geometry, this.createMaterial(texture, color, material), x, y, z); 523 | } 524 | 525 | addIcosahedron(config) { 526 | const obj = this.makeIcosahedron(config); 527 | 528 | this.scene.add(obj); 529 | 530 | return obj; 531 | } 532 | 533 | makeTetrahedron({ radius = 1, detail = 0, texture = null, color = 0xffffff, material = null, x = 0, y = 0, z = 0 } = {}) { 534 | const geometry = new THREE.TetrahedronBufferGeometry(radius, detail); 535 | 536 | return this.createMesh(geometry, this.createMaterial(texture, color, material), x, y, z); 537 | } 538 | 539 | addTetrahedron(config) { 540 | const obj = this.makeTetrahedron(config); 541 | 542 | this.scene.add(obj); 543 | 544 | return obj; 545 | } 546 | 547 | makeTorusKnot({ radius = 1, tube = 0.4, tubularSegments = 64, radialSegments = 8, p = 2, q = 3, texture = null, color = 0xffffff, material = null, x = 0, y = 0, z = 0 } = {}) { 548 | const geometry = new THREE.TorusKnotBufferGeometry(radius, tube, tubularSegments, radialSegments, p, q); 549 | 550 | return this.createMesh(geometry, this.createMaterial(texture, color, material), x, y, z); 551 | } 552 | 553 | addTorusKnot(config) { 554 | const obj = this.makeTorusKnot(config); 555 | 556 | this.scene.add(obj); 557 | 558 | return obj; 559 | } 560 | 561 | makePlane({ width = 1, height = 1, widthSegments = 1, heightSegments = 1, texture = null, color = 0xffffff, material = null, x = 0, y = 0, z = 0 } = {}) { 562 | const geometry = new THREE.PlaneBufferGeometry(width, height, widthSegments, heightSegments); 563 | 564 | return this.createMesh(geometry, this.createMaterial(texture, color, material), x, y, z); 565 | } 566 | 567 | addPlane(config) { 568 | const obj = this.makePlane(config); 569 | 570 | this.scene.add(obj); 571 | 572 | return obj; 573 | } 574 | 575 | addGround({ receiveShadow = false, texture = null, color = 0xffffff, material = null } = {}) { 576 | const plane = this.makePlane({ width: 2000, height: 2000, texture, color, material }); 577 | 578 | plane.rotation.x = -Math.PI * 0.5; 579 | 580 | plane.receiveShadow = receiveShadow; 581 | 582 | this.scene.add(plane); 583 | 584 | return plane; 585 | } 586 | 587 | makeSphere({ radius = 1, widthSegments = 8, heightSegments = 6, texture = null, color = 0xffffff, material = null, x = 0, y = 0, z = 0 } = {}) { 588 | const geometry = new THREE.SphereBufferGeometry(radius, widthSegments, heightSegments); 589 | 590 | return this.createMesh(geometry, this.createMaterial(texture, color, material), x, y, z); 591 | } 592 | 593 | addSphere(config) { 594 | const obj = this.makeSphere(config); 595 | 596 | this.scene.add(obj); 597 | 598 | return obj; 599 | } 600 | 601 | makeCone({ radius = 1, height = 1, radialSegments = 8, heightSegments = 1, openEnded = false, thetaStart = 0, thetaLength = Math.PI * 2, texture = null, color = 0xffffff, material = null, x = 0, y = 0, z = 0 } = {}) { 602 | const geometry = new THREE.ConeBufferGeometry(radius, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength); 603 | 604 | return this.createMesh(geometry, this.createMaterial(texture, color, material), x, y, z); 605 | } 606 | 607 | addCone(config) { 608 | const obj = this.makeCone(config); 609 | 610 | this.scene.add(obj); 611 | 612 | return obj; 613 | } 614 | 615 | makeCylinder({ radiusTop = 1, radiusBottom = 1, height = 1, radialSegments = 8, heightSegments = 1, openEnded = false, thetaStart = 0, thetaLength = Math.PI * 2, texture = null, color = 0xffffff, material = null, x = 0, y = 0, z = 0 } = {}) { 616 | const geometry = new THREE.CylinderBufferGeometry(radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength); 617 | 618 | return this.createMesh(geometry, this.createMaterial(texture, color, material), x, y, z); 619 | } 620 | 621 | addCylinder(config) { 622 | const obj = this.makeCylinder(config); 623 | 624 | this.scene.add(obj); 625 | 626 | return obj; 627 | } 628 | 629 | makeDodecahedron({ radius = 1, detail = 0, texture = null, color = 0xffffff, material = null, x = 0, y = 0, z = 0 } = {}) { 630 | const geometry = new THREE.DodecahedronBufferGeometry(radius, detail); 631 | 632 | return this.createMesh(geometry, this.createMaterial(texture, color, material), x, y, z); 633 | } 634 | 635 | addDodecahedron(config) { 636 | const obj = this.makeDodecahedron(config); 637 | 638 | this.scene.add(obj); 639 | 640 | return obj; 641 | } 642 | 643 | makeBox({ size = null, width = 1, height = 1, depth = 1, texture = null, color = 0xffffff, material = null, x = 0, y = 0, z = 0 } = {}) { 644 | if (size) { 645 | width = size; 646 | height = size; 647 | depth = size; 648 | } 649 | 650 | const geometry = new THREE.BoxBufferGeometry(width, height, depth); 651 | 652 | return this.createMesh(geometry, this.createMaterial(texture, color, material), x, y, z); 653 | } 654 | 655 | addBox(config) { 656 | const obj = this.makeBox(config); 657 | 658 | this.scene.add(obj); 659 | 660 | return obj; 661 | } 662 | } 663 | -------------------------------------------------------------------------------- /src/scenes/BaseScene.ts: -------------------------------------------------------------------------------- 1 | import { PoisonVialGame } from '../game'; 2 | import { PasuunaPlugin } from '@pinkkis/phaser-plugin-pasuuna'; 3 | 4 | export class BaseScene extends Phaser.Scene { 5 | public game: PoisonVialGame; 6 | public pasuuna: PasuunaPlugin; 7 | 8 | constructor(key: string, options?: any) { 9 | super(key); 10 | } 11 | 12 | public setTimerEvent(timeMin: number, timeMax: number, callback: () => {}, params?: any[]): Phaser.Time.TimerEvent { 13 | return this.time.delayedCall(Phaser.Math.Between(timeMin, timeMax), callback, params || [], this); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/scenes/BootScene.ts: -------------------------------------------------------------------------------- 1 | import { BaseScene } from './BaseScene'; 2 | 3 | export class BootScene extends BaseScene { 4 | constructor(key: string, options: any) { 5 | super('BootScene'); 6 | } 7 | 8 | public preload(): void { 9 | this.load.bitmapFont('retro', './assets/fonts/cosmicavenger.png', './assets/fonts/cosmicavenger.xml'); 10 | } 11 | 12 | public create(): void { 13 | this.registry.set('speed', 0); 14 | this.registry.set('racetime', 0); 15 | this.registry.set('trackposition', 0); 16 | this.registry.set('raceposition', 0); 17 | this.registry.set('playerx', 0); 18 | 19 | this.scene.start('LoadScene', {}); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/scenes/GameScene.ts: -------------------------------------------------------------------------------- 1 | import { BaseScene } from './BaseScene'; 2 | import { Input } from 'phaser'; 3 | import { Colors } from '../Components/Colors'; 4 | import { gameSettings } from '../config/GameSettings'; 5 | import { Util } from '../Components/Util'; 6 | import { Player } from '../Components/Player'; 7 | import { Road } from '../Components/Road'; 8 | import { Renderer } from '../Components/Renderer'; 9 | import { TrackSegment } from '../Components/TrackSegment'; 10 | import { CarManager } from '../Components/CarManager'; 11 | import { Car } from '../Components/Car'; 12 | 13 | export class GameScene extends BaseScene { 14 | public position: number; 15 | public player: Player; 16 | public road: Road; 17 | public renderer: Renderer; 18 | public carManager: CarManager; 19 | 20 | public debugText: Phaser.GameObjects.BitmapText; 21 | public sky: Phaser.GameObjects.Rectangle; 22 | public clouds1: Phaser.GameObjects.TileSprite; 23 | public clouds2: Phaser.GameObjects.TileSprite; 24 | public clouds3: Phaser.GameObjects.TileSprite; 25 | public mountains: Phaser.GameObjects.TileSprite; 26 | 27 | public camera: Phaser.Cameras.Scene2D.Camera; 28 | public cameraAngle: number = 0; 29 | 30 | public hills: Phaser.GameObjects.TileSprite; 31 | public hillsBaseY: number; 32 | 33 | public cursors: Input.Keyboard.CursorKeys; 34 | 35 | constructor(key: string, options: any) { 36 | super('GameScene'); 37 | } 38 | 39 | public create(): void { 40 | this.scene.launch('RaceUiScene', this); 41 | 42 | const gameWidth = this.scale.gameSize.width; 43 | const gameHeight = this.scale.gameSize.height; 44 | 45 | this.cursors = this.input.keyboard.createCursorKeys(); 46 | this.camera = this.cameras.main; 47 | 48 | this.road = new Road(this); 49 | this.carManager = new CarManager(this, this.road); 50 | 51 | this.sky = this.add.rectangle(-10, -20, gameWidth + 20, gameHeight + 30, Colors.SKY.color).setOrigin(0).setZ(0).setDepth(0); 52 | this.clouds2 = this.add.tileSprite(-10, 10, gameWidth + 20, 64, 'clouds1').setOrigin(0).setZ(3).setDepth(1); 53 | this.clouds3 = this.add.tileSprite(-10, 20, gameWidth + 20, 64, 'clouds2').setOrigin(0).setZ(4).setDepth(2); 54 | this.mountains = this.add.tileSprite(-10, gameHeight / 2 - 85, gameWidth + 20, 128, 'mountain').setOrigin(0).setZ(3).setDepth(3); 55 | this.clouds1 = this.add.tileSprite(-10, 0, gameWidth + 20, 64, 'clouds1').setOrigin(0).setZ(2).setDepth(4); 56 | 57 | this.hillsBaseY = gameHeight / 2 - 40; 58 | this.hills = this.add.tileSprite(-10, this.hillsBaseY, gameWidth + 10, 64, 'hills').setOrigin(0).setZ(5).setDepth(4); 59 | 60 | this.renderer = new Renderer(this, 5); 61 | this.player = new Player(this, 0, gameHeight - 5, gameSettings.cameraHeight * gameSettings.cameraDepth + 300, 'playercar'); // player z helps with collision distances 62 | 63 | this.debugText = this.add.bitmapText(5, 5, 'retro', '', 16).setTint(0xff0000).setDepth(200); 64 | 65 | // reset road to empty 66 | // currently creates test track 67 | this.road.resetRoad(); 68 | this.carManager.resetCars(); 69 | 70 | this.pasuuna.loadSongFromCache('dream-candy', true); 71 | this.pasuuna.setVolume(0.70); 72 | } 73 | 74 | public update(time: number, delta: number): void { 75 | const dlt = delta * 0.01; 76 | 77 | const playerSegment = this.road.findSegmentByZ(this.player.trackPosition + this.player.z); 78 | const playerPercent = Util.percentRemaining(this.player.trackPosition + this.player.z, gameSettings.segmentLength); 79 | const speedMultiplier = this.player.speed / gameSettings.maxSpeed; 80 | const dx = this.player.speed <= 0 ? 0 : dlt * speedMultiplier; 81 | 82 | this.handleInput(delta, playerSegment); 83 | 84 | this.player.y = Util.interpolate(playerSegment.p1.world.y, playerSegment.p2.world.y, playerPercent); 85 | this.player.x = this.player.x - (dx * speedMultiplier * playerSegment.curve * gameSettings.centrifugal); 86 | 87 | this.player.speed = Phaser.Math.Clamp(this.player.speed, 0, gameSettings.maxSpeed); 88 | this.player.x = Phaser.Math.Clamp(this.player.x, -gameSettings.roadWidthClamp, gameSettings.roadWidthClamp); 89 | this.player.turn = Phaser.Math.Clamp(this.player.turn, -gameSettings.maxTurn, gameSettings.maxTurn); 90 | this.player.trackPosition = Util.increase(this.player.trackPosition, dlt * this.player.speed, this.road.trackLength); 91 | 92 | this.player.pitch = (playerSegment.p1.world.y - playerSegment.p2.world.y) * 0.002; 93 | 94 | if (this.player.isOnGravel && this.player.speed > gameSettings.offRoadLimit) { 95 | this.player.speed = Util.accelerate(this.player.speed, gameSettings.offRoadDecel, dlt); 96 | } 97 | 98 | // collision check with props if outside of road 99 | if (playerSegment.props.size && Math.abs(this.player.x) > 1) { 100 | for (const prop of playerSegment.props) { 101 | if ( Util.overlapPlayer(this.player, prop) ) { 102 | this.player.collide('prop'); 103 | this.player.trackPosition = Util.increase(playerSegment.p1.world.z, -this.player.z, this.road.trackLength); 104 | this.player.speed = this.player.speed > 50 ? 50 : this.player.speed; 105 | } 106 | } 107 | } 108 | 109 | // collision check with cars if on road 110 | if (playerSegment.cars.size && Math.abs(this.player.x) < 1) { 111 | for (const car of playerSegment.cars) { 112 | if ( Util.overlapPlayer(this.player, car) ) { 113 | this.player.collide('car'); 114 | this.player.trackPosition = Util.increase(car.trackPosition, -this.player.z, this.road.trackLength); 115 | this.player.speed = this.player.speed / 2; 116 | } 117 | } 118 | } 119 | 120 | // hide all props 121 | this.road.hideAllProps(); 122 | this.carManager.hideAll(); 123 | 124 | // update parallax bg's 125 | this.updateBg(dx * playerSegment.curve); 126 | 127 | // draw road 128 | this.renderer.update(time, delta); 129 | 130 | // update other cars on track 131 | this.carManager.update(dlt, playerSegment, this.player.x); 132 | 133 | // update player turn 134 | this.player.update(delta, dx); 135 | 136 | // update registry 137 | this.registry.set('speed', Math.floor(this.player.speed / 10)); 138 | 139 | // camera tilt 140 | this.cameraAngle = Phaser.Math.Clamp(this.cameraAngle, -6, 6); 141 | this.camera.setAngle(this.cameraAngle); 142 | 143 | // this.debugText.setText(`speed: ${this.player.speed.toFixed()} 144 | // position: ${this.player.trackPosition.toFixed(2)} 145 | // curve: ${playerSegment.curve.toFixed(2)} 146 | // player y: ${this.player.y.toFixed(2)} 147 | // player x: ${this.player.x.toFixed(2)} 148 | // turn: ${this.player.turn.toFixed(2)} 149 | // pitch: ${(this.player.pitch).toFixed(2)} 150 | // speedX: ${(this.player.speed / gameSettings.maxSpeed).toFixed(3)} 151 | // dx: ${dx.toFixed(3)}`); 152 | } 153 | 154 | public getRadarCars(length: number): Car[] { 155 | const cars: Car[] = []; 156 | 157 | const baseSegment = this.road.findSegmentByZ(this.player.trackPosition); 158 | 159 | for (let n = 0; n < length; n++) { 160 | const segmentIndex = (baseSegment.index + n) % this.road.segments.length; 161 | const segment = this.road.segments[segmentIndex]; 162 | 163 | for (const car of segment.cars) { 164 | cars.push(car); 165 | } 166 | } 167 | 168 | return cars; 169 | } 170 | 171 | // private ------------------------------------ 172 | private updateBg(offset: number): void { 173 | this.clouds1.tilePositionX += 0.05 + offset * this.clouds1.z; 174 | this.clouds2.tilePositionX += 0.1 + offset * this.clouds2.z; 175 | this.clouds3.tilePositionX += 0.125 + offset * this.clouds3.z; 176 | this.mountains.tilePositionX += offset * this.mountains.z; 177 | this.hills.tilePositionX += offset * this.hills.z; 178 | this.hills.setY(this.hillsBaseY - this.player.pitch * 20); 179 | } 180 | 181 | private handleInput(delta: number, playerSegment: TrackSegment) { 182 | const dlt = delta * 0.01; 183 | 184 | if (this.cursors.up.isDown) { 185 | this.player.speed = Util.accelerate(this.player.speed, Util.interpolate(gameSettings.accel, 0, Util.percentRemaining(this.player.speed, gameSettings.maxSpeed) ), dlt); 186 | this.player.accelerating = true; 187 | } else if (this.cursors.down.isDown) { 188 | this.player.speed = Util.accelerate(this.player.speed, gameSettings.breaking, dlt); 189 | } else { 190 | this.player.accelerating = false; 191 | this.player.speed = Util.accelerate(this.player.speed, gameSettings.decel, dlt); 192 | } 193 | 194 | if (this.player.speed > 500 && this.player.screeching) { 195 | this.player.speed = Util.accelerate(this.player.speed, gameSettings.screechDecel, dlt); 196 | } 197 | 198 | if (this.cursors.left.isDown) { 199 | this.player.turn -= dlt * (Math.abs(playerSegment.curve) > 0.1 ? 0.5 : 0.25); 200 | this.cameraAngle += dlt; 201 | } else if (this.cursors.right.isDown) { 202 | this.player.turn += dlt * (Math.abs(playerSegment.curve) > 0.1 ? 0.5 : 0.25); 203 | this.cameraAngle -= dlt; 204 | } else { 205 | this.player.turn = Math.abs(this.player.turn) < 0.01 ? 0 : Util.interpolate(this.player.turn, 0, gameSettings.turnResetMultiplier); 206 | this.cameraAngle = Math.abs(this.cameraAngle) < 0.02 ? 0 : Util.interpolate(this.cameraAngle, 0, gameSettings.cameraAngleResetMultiplier); 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/scenes/LoadScene.ts: -------------------------------------------------------------------------------- 1 | import { BaseScene } from './BaseScene'; 2 | 3 | export class LoadScene extends BaseScene { 4 | constructor(key: string, options: any) { 5 | super('LoadScene'); 6 | } 7 | 8 | public preload(): void { 9 | const progress = this.add.graphics(); 10 | 11 | this.load.on('progress', (value: number) => { 12 | progress.clear(); 13 | progress.fillStyle(0xffffff, 1); 14 | progress.fillRect( 15 | 0, 16 | this.scale.gameSize.height / 2, 17 | this.scale.gameSize.width * value, 18 | 60, 19 | ); 20 | }); 21 | 22 | this.load.on('complete', () => { 23 | progress.destroy(); 24 | }); 25 | 26 | this.load.image('clouds1', './assets/clouds.png'); 27 | this.load.image('clouds2', './assets/clouds2.png'); 28 | this.load.image('mountain', './assets/mountain.png'); 29 | this.load.image('hills', './assets/hills.png'); 30 | this.load.image('boulder1', './assets/boulder.png'); 31 | this.load.image('boulder2', './assets/boulder2.png'); 32 | this.load.image('tree1', './assets/tree.png'); 33 | this.load.image('tree2', './assets/tree2.png'); 34 | this.load.image('tree3', './assets/tree3.png'); 35 | this.load.image('turnsign', './assets/turn-sign.png'); 36 | 37 | this.load.spritesheet('particles', './assets/smoke-particle.png', { frameWidth: 16, frameHeight: 16 }); 38 | 39 | this.load.spritesheet('car-green', './assets/car-green.png', { frameWidth: 64, frameHeight: 64 }); 40 | this.load.spritesheet('car-army', './assets/car-army.png', { frameWidth: 64, frameHeight: 64 }); 41 | this.load.spritesheet('car-red', './assets/car-red.png', { frameWidth: 64, frameHeight: 64 }); 42 | this.load.spritesheet('car-yellow', './assets/car-yellow.png', { frameWidth: 64, frameHeight: 64 }); 43 | this.load.spritesheet('car-blue', './assets/car-blue.png', { frameWidth: 64, frameHeight: 64 }); 44 | 45 | this.load.binary('playercar', './assets/3d/car.glb'); 46 | 47 | this.load.audio('engine', ['./assets/sound/engine-loop.wav']); 48 | this.load.audio('tire-squeal', ['./assets/sound/tire-squeal.wav']); 49 | this.load.audio('collision', ['./assets/sound/car-collision.wav']); 50 | this.load.audio('confirm', ['./assets/sound/confirm.wav']); 51 | this.load.audio('explosion', ['./assets/sound/explosion.wav']); 52 | this.load.audio('select', ['./assets/sound/select.wav']); 53 | this.load.audio('time-extended', ['./assets/sound/time-extended.wav']); 54 | 55 | this.load.binary('dream-candy', './assets/sound/drozerix_-_dream_candy.xm'); 56 | 57 | this.load.bitmapFont('numbers', './assets/fonts/number-font.png', './assets/fonts/number-font.xml'); 58 | this.load.bitmapFont('impact', './assets/fonts/impact-24-outline.png', './assets/fonts/impact-24-outline.xml'); 59 | } 60 | 61 | public create(): void { 62 | this.scene.start('GameScene', {}); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/scenes/RaceUiScene.ts: -------------------------------------------------------------------------------- 1 | import { BaseScene } from './BaseScene'; 2 | import { SpeedGauge } from '../Components/SpeedGauge'; 3 | import { TrackRadar } from '../Components/TrackRadar'; 4 | import { GameScene } from './GameScene'; 5 | 6 | export class RaceUiScene extends BaseScene { 7 | public timerText: Phaser.GameObjects.BitmapText; 8 | public timeLargeText: Phaser.GameObjects.BitmapText; 9 | public timeSmallText: Phaser.GameObjects.BitmapText; 10 | public speedGauge: SpeedGauge; 11 | public trackRadar: TrackRadar; 12 | public gameScene: GameScene; 13 | 14 | public timer: Phaser.Tweens.Tween; 15 | 16 | constructor(key: string, options: any) { 17 | super('RaceUiScene'); 18 | } 19 | 20 | public create(gameScene: GameScene): void { 21 | this.gameScene = gameScene; 22 | this.timeLargeText = this.add.bitmapText(this.scale.gameSize.width / 2 + 30, 5, 'numbers', '000', 32).setOrigin(1, 0).setTint(0xffcccc); 23 | this.timeSmallText = this.add.bitmapText(this.scale.gameSize.width / 2 + 33, 8, 'numbers', '000', 16).setOrigin(0, 0).setTint(0xffcccc); 24 | this.timerText = this.add.bitmapText(this.scale.gameSize.width / 2 + 54, 8, 'impact', 'time', 16).setOrigin(0, 0); 25 | 26 | this.speedGauge = new SpeedGauge(this, 60, 60, 50); 27 | this.trackRadar = new TrackRadar(this, this.scale.gameSize.width - 40, 10); 28 | 29 | this.timer = this.tweens.addCounter({ 30 | from: 180, 31 | to: 0, 32 | duration: 180000, 33 | }); 34 | 35 | this.setupEvents(); 36 | } 37 | 38 | public update(): void { 39 | const timerValue = this.timer.getValue().toFixed(2).split('.'); 40 | this.timeLargeText.setText(timerValue[0]); 41 | this.timeSmallText.setText(timerValue[1]); 42 | 43 | this.trackRadar.update(); 44 | const radarCars = this.gameScene.getRadarCars(700); 45 | for (const car of radarCars) { 46 | this.trackRadar.drawCar(car.offset, car.trackPosition - this.gameScene.player.trackPosition); 47 | } 48 | } 49 | 50 | public destroy(): void { 51 | this.registry.events.off('changedata'); 52 | } 53 | 54 | private setupEvents(): void { 55 | this.registry.events.on('changedata', (parent: any, key: string, data: any) => { 56 | switch (key) { 57 | case 'speed': 58 | this.speedGauge.speed = data; 59 | break; 60 | 61 | case 'playerx': 62 | this.trackRadar.updatePlayerX(data); 63 | break; 64 | 65 | default: 66 | console.warn('unknown registry change'); 67 | } 68 | }, this); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /tasks/createZip.js: -------------------------------------------------------------------------------- 1 | // require modules 2 | const fs = require('fs'); 3 | const archiver = require('archiver'); 4 | 5 | const {version, buildDir, distDir} = require('../buildConfig'); 6 | 7 | // check that build exists 8 | console.log(buildDir); 9 | console.log(distDir); 10 | if (!fs.existsSync(distDir)) { 11 | throw new Error('production build does not exist. run `npm run build` first.'); 12 | } 13 | 14 | // create a file to stream archive data to. 15 | if (!fs.existsSync(buildDir)) { 16 | fs.mkdirSync(buildDir); 17 | } 18 | 19 | const output = fs.createWriteStream(`${buildDir}/release-${version}.zip`); 20 | const archive = archiver('zip', { zlib: { level: 9 } }); 21 | 22 | archive 23 | .on('warning', function (err) { 24 | if (err.code === 'ENOENT') { 25 | console.warn(err); 26 | } else { 27 | throw err; 28 | } 29 | }) 30 | .on('error', function (err) { 31 | throw err; 32 | }); 33 | 34 | archive.pipe(output); 35 | archive.glob('**/*', {cwd: distDir}); 36 | archive.finalize(); 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": true, 5 | "module": "esnext", 6 | "target": "esnext", 7 | "allowJs": true, 8 | "sourceMap": true, 9 | "moduleResolution": "node", 10 | }, 11 | "exclude": [ 12 | "node_modules", 13 | ".vscode" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "object-literal-sort-keys": false, 9 | "no-var-requires": false, 10 | "no-console": false, 11 | "max-line-length": false, 12 | "ordered-imports": false, 13 | "indent": [ 14 | true, 15 | "tabs", 16 | 4 17 | ], 18 | "quotemark": [ 19 | true, 20 | "single", 21 | "avoid-escape" 22 | ] 23 | }, 24 | "rulesDirectory": [], 25 | "linterOptions": { 26 | "exclude": [ 27 | "**/node_modules/**" 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebPackPlugin = require('html-webpack-plugin'); 4 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 5 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 6 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 7 | const TerserPlugin = require('terser-webpack-plugin'); 8 | const CleanPlugin = require('clean-webpack-plugin'); 9 | 10 | const {version, distOutput} = require('./buildConfig'); 11 | 12 | // Phaser webpack config 13 | const phaserModule = path.join(__dirname, '/node_modules/phaser/'); 14 | const phaser = path.join(phaserModule, process.env.NODE_ENV === 'production' ? 'dist/phaser.min.js' : 'dist/phaser.js'); 15 | const threeModule = path.join(__dirname, '/node_modules/three/'); 16 | const three = path.join(threeModule, 'build/three.module.js'); 17 | 18 | module.exports = { 19 | output: { 20 | globalObject: 'this', 21 | path: distOutput, 22 | }, 23 | entry: { 24 | game: ['./src/game.ts'] 25 | }, 26 | devtool: process.env.NODE_ENV === 'production' ? undefined : 'eval-source-map', 27 | module: { 28 | rules: [{ 29 | test: /\.tsx?$/, 30 | use: 'babel-loader', 31 | exclude: /node_modules/ 32 | }, 33 | { 34 | test: /\.map.js$/, 35 | use: ["source-map-loader"], 36 | enforce: "pre" 37 | }, 38 | { 39 | test: [/\.vert$/, /\.frag$/], 40 | use: 'raw-loader' 41 | }, 42 | { 43 | test: /.worker.js$/, 44 | use: [{ 45 | loader: 'worker-loader' 46 | }] 47 | }, 48 | { 49 | test: /\.css$/, 50 | use: [ 51 | MiniCssExtractPlugin.loader, 52 | "css-loader" 53 | ] 54 | }, 55 | { 56 | test: /phaser\.min\.js$/, 57 | use: [{ 58 | loader: 'expose-loader', 59 | options: 'Phaser' 60 | }] 61 | }, 62 | { 63 | test: /\.(eot|svg|ttf|woff|woff2)$/, 64 | loader: 'file-loader?name=assets/fonts/[name].[ext]' 65 | } 66 | ] 67 | }, 68 | plugins: [ 69 | new CleanPlugin(), 70 | new webpack.DefinePlugin({ 71 | 'CANVAS_RENDERER': JSON.stringify(true), 72 | 'WEBGL_RENDERER': JSON.stringify(true) 73 | }), 74 | new HtmlWebPackPlugin({ 75 | template: './src/index.html', 76 | filename: './index.html', 77 | chunks: ['game', 'vendor'], 78 | buildType: process.env.NODE_ENV, 79 | version: version, 80 | favicon: './src/favicon.ico', 81 | }), 82 | new CopyWebpackPlugin([ 83 | { 84 | from: './assets/', 85 | to: './assets/' 86 | }, 87 | ], {}), 88 | new MiniCssExtractPlugin({ 89 | filename: '[name].css', 90 | chunkFilename: '[name].css', 91 | }), 92 | new webpack.ProvidePlugin({ 93 | 'THREE': 'three', 94 | }) 95 | ], 96 | resolve: { 97 | extensions: ['.ts', '.js'], 98 | alias: { 99 | 'phaser': phaser, 100 | 'three': three, 101 | } 102 | }, 103 | optimization: { 104 | minimizer: [ 105 | new TerserPlugin(), 106 | new OptimizeCSSAssetsPlugin() 107 | ], 108 | splitChunks: { 109 | cacheGroups: { 110 | commons: { 111 | test: /[\\/]node_modules[\\/]/, 112 | name: 'vendor', 113 | chunks: 'initial' 114 | } 115 | } 116 | } 117 | }, 118 | watchOptions: { 119 | ignored: [ 120 | 'node_modules', 121 | 'assets/**/*' 122 | ] 123 | } 124 | }; 125 | --------------------------------------------------------------------------------