├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json └── src ├── assets ├── aseprite │ └── bullet-01.ase ├── audio │ └── crash-01.wav ├── icon.png ├── models │ ├── droid.fbx │ └── jet.glb ├── splash.png ├── sprite-sheets │ ├── bullet-01.png │ ├── cuphead.png │ └── player.png └── textures │ ├── grid.png │ ├── particle.png │ └── perlin.png ├── game ├── components │ ├── animated-model.js │ ├── box.js │ ├── camera.js │ ├── cuphead.js │ ├── cylinder.js │ ├── droid.js │ ├── hud.js │ ├── jet.js │ ├── particles.js │ ├── portal.js │ ├── sprite.js │ └── turntable.js ├── entities.js ├── graphics │ ├── effect-composer.js │ ├── gpu-particle-system.js │ ├── passes │ │ ├── clear-mask-pass.js │ │ ├── mask-pass.js │ │ ├── pass.js │ │ ├── render-pass.js │ │ ├── shader-pass.js │ │ └── unreal-bloom-pass.js │ ├── renderer.js │ └── shaders │ │ ├── copy-shader.js │ │ ├── luminosity-high-pass-shader.js │ │ ├── pixel-shader.js │ │ ├── scanline-shader.js │ │ ├── sepia-shader.js │ │ └── tri-color-shader.js ├── index.js ├── systems │ ├── basic-physics.js │ ├── camera.js │ ├── collisions.js │ ├── gamepad-controller.js │ ├── gravity.js │ ├── hud.js │ ├── index.js │ ├── keyboard-controller.js │ ├── mouse-controller.js │ ├── particles.js │ ├── physics.js │ ├── removal.js │ ├── rotation.js │ ├── spawn.js │ ├── spring.js │ └── timeline.js └── utils │ ├── index.js │ ├── perlin.js │ └── three │ ├── dds-loader.js │ ├── fbx-loader.js │ ├── gltf-loader.js │ ├── index.js │ └── skeleton-utils.js ├── index.css ├── index.js └── service-worker.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # firebase 26 | .firebase/** 27 | 28 | # houdini 29 | backup/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Boris Berak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Game Engine Template 2 | 3 | This repo is designed to be a sort of game kickstarter. It contains some general systems and components that should be somewhat useful when developing a variety of games using [React Game Engine](https://github.com/bberak/react-game-engine). 4 | 5 | The underlying renderer is [ThreeJS](https://github.com/mrdoob/three.js). 6 | 7 | The template will contain both 3D and 2D game entities (sprites) and potentially some particles. 8 | 9 | This project uses [Create React App](https://github.com/facebook/create-react-app) because quite frankly, it is the easiest and most stable way to get up and running with a React web project. 10 | 11 | ## How to start 12 | 13 | ``` 14 | git clone https://github.com/bberak/react-game-engine-template.git [new-game] 15 | 16 | cd [new-game] 17 | 18 | rm -rf .git # Windows: rmdir /S .git 19 | 20 | git init 21 | 22 | git add . 23 | 24 | git commit -m "First commit" 25 | 26 | git remote add origin https://github.com/[you]/[new-game].git 27 | 28 | git push -u origin master 29 | ``` 30 | 31 | Then, install the dependencies and start the app: 32 | 33 | ``` 34 | npm install 35 | 36 | npm run start 37 | ``` 38 | 39 | This template contains the following: 40 | 41 | - Stick (Gamepad) controllers 42 | - A simple HUD 43 | - Particle systems 44 | - Sound support 45 | - Physics implementation powered by [Oimo](https://github.com/lo-th/Oimo.js/) 46 | - [ThreeJS](https://github.com/mrdoob/three.js) rendering 47 | - Post-processing effects 48 | - Sprite support with animations 49 | 50 | > All of the above systems and components are hackable and extensible - which *should* allow for quick[er] prototyping. 51 | 52 | ## Available Scripts 53 | 54 | In the project directory, you can run: 55 | 56 | ### `npm start` 57 | 58 | Runs the app in the development mode.
59 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 60 | 61 | The page will reload if you make edits.
62 | You will also see any lint errors in the console. 63 | 64 | ### `npm test` 65 | 66 | Launches the test runner in the interactive watch mode.
67 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 68 | 69 | ### `npm run build` 70 | 71 | Builds the app for production to the `build` folder.
72 | It correctly bundles React in production mode and optimizes the build for the best performance. 73 | 74 | The build is minified and the filenames include the hashes.
75 | Your app is ready to be deployed! 76 | 77 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 78 | 79 | ### `npm run eject` 80 | 81 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 82 | 83 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 84 | 85 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 86 | 87 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 88 | 89 | ## Learn More 90 | 91 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 92 | 93 | To learn React, check out the [React documentation](https://reactjs.org/). 94 | 95 | ### Code Splitting 96 | 97 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 98 | 99 | ### Analyzing the Bundle Size 100 | 101 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 102 | 103 | ### Making a Progressive Web App 104 | 105 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 106 | 107 | ### Advanced Configuration 108 | 109 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 110 | 111 | ### Deployment 112 | 113 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 114 | 115 | ### `npm run build` fails to minify 116 | 117 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 118 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@popmotion/popcorn": "^0.4.0", 4 | "lodash": "^4.17.11", 5 | "oimo": "^1.0.9", 6 | "react": "^16.8.4", 7 | "react-dom": "^16.8.4", 8 | "react-game-engine": "^1.0.0", 9 | "react-scripts": "2.1.8", 10 | "three": "^0.105.2" 11 | }, 12 | "scripts": { 13 | "start": "run-script-os", 14 | "start:win32": "set 'BROWSER=chrome' && react-scripts start", 15 | "start:darwin:linux": "BROWSER='google chrome' react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "eslintConfig": { 21 | "extends": "react-app" 22 | }, 23 | "browserslist": [ 24 | ">0.2%", 25 | "not dead", 26 | "not ie <= 11", 27 | "not op_mini all" 28 | ], 29 | "devDependencies": { 30 | "run-script-os": "^1.0.5" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-game-engine-template/8972e96c0d113ae079ddb6eaa28ffa4e2ec30a47/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | RGE Template 26 | 27 | 28 | 29 |
30 | 40 | 41 | 65 | 66 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Acid App", 3 | "name": "Acid App", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff", 15 | "orientation": "landscape" 16 | } 17 | -------------------------------------------------------------------------------- /src/assets/aseprite/bullet-01.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-game-engine-template/8972e96c0d113ae079ddb6eaa28ffa4e2ec30a47/src/assets/aseprite/bullet-01.ase -------------------------------------------------------------------------------- /src/assets/audio/crash-01.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-game-engine-template/8972e96c0d113ae079ddb6eaa28ffa4e2ec30a47/src/assets/audio/crash-01.wav -------------------------------------------------------------------------------- /src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-game-engine-template/8972e96c0d113ae079ddb6eaa28ffa4e2ec30a47/src/assets/icon.png -------------------------------------------------------------------------------- /src/assets/models/droid.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-game-engine-template/8972e96c0d113ae079ddb6eaa28ffa4e2ec30a47/src/assets/models/droid.fbx -------------------------------------------------------------------------------- /src/assets/models/jet.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-game-engine-template/8972e96c0d113ae079ddb6eaa28ffa4e2ec30a47/src/assets/models/jet.glb -------------------------------------------------------------------------------- /src/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-game-engine-template/8972e96c0d113ae079ddb6eaa28ffa4e2ec30a47/src/assets/splash.png -------------------------------------------------------------------------------- /src/assets/sprite-sheets/bullet-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-game-engine-template/8972e96c0d113ae079ddb6eaa28ffa4e2ec30a47/src/assets/sprite-sheets/bullet-01.png -------------------------------------------------------------------------------- /src/assets/sprite-sheets/cuphead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-game-engine-template/8972e96c0d113ae079ddb6eaa28ffa4e2ec30a47/src/assets/sprite-sheets/cuphead.png -------------------------------------------------------------------------------- /src/assets/sprite-sheets/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-game-engine-template/8972e96c0d113ae079ddb6eaa28ffa4e2ec30a47/src/assets/sprite-sheets/player.png -------------------------------------------------------------------------------- /src/assets/textures/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-game-engine-template/8972e96c0d113ae079ddb6eaa28ffa4e2ec30a47/src/assets/textures/grid.png -------------------------------------------------------------------------------- /src/assets/textures/particle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-game-engine-template/8972e96c0d113ae079ddb6eaa28ffa4e2ec30a47/src/assets/textures/particle.png -------------------------------------------------------------------------------- /src/assets/textures/perlin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-game-engine-template/8972e96c0d113ae079ddb6eaa28ffa4e2ec30a47/src/assets/textures/perlin.png -------------------------------------------------------------------------------- /src/game/components/animated-model.js: -------------------------------------------------------------------------------- 1 | import { add, cloneMesh } from "../utils/three"; 2 | import { clamp } from "../utils"; 3 | 4 | export default async ({ parent, x = 0, z = 0, y = 0, scale = 1, mesh, morphTargets = {} }) => { 5 | 6 | const model = cloneMesh(await Promise.resolve(mesh)); 7 | 8 | model.position.x = x; 9 | model.position.y = y; 10 | model.position.z = z; 11 | model.scale.x = scale; 12 | model.scale.y = scale; 13 | model.scale.z = scale; 14 | 15 | add(parent, model); 16 | 17 | const poses = {}; 18 | 19 | Object.keys(morphTargets).forEach(key => { 20 | const index = morphTargets[key]; 21 | 22 | poses[key] = weight => { 23 | if (weight === undefined || weight === null) 24 | return model.morphTargetInfluences[index]; 25 | 26 | model.morphTargetInfluences[index] = clamp(weight, 0, 1); 27 | }; 28 | }) 29 | 30 | return { 31 | model, 32 | poses 33 | }; 34 | }; -------------------------------------------------------------------------------- /src/game/components/box.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { add } from "../utils/three"; 3 | import { sound } from "../utils"; 4 | import CrashFile from "../../assets/audio/crash-01.wav"; 5 | 6 | export default ({ 7 | parent, 8 | world, 9 | dynamic = true, 10 | x = 0, 11 | y = 0, 12 | z = 0, 13 | width = 1.1, 14 | breadth = 1.1, 15 | height = 1.1, 16 | scale = 1, 17 | color = 0x00e6ff 18 | }) => { 19 | const geometry = new THREE.BoxGeometry(width, height, breadth); 20 | const material = new THREE.MeshStandardMaterial({ color }); 21 | const box = new THREE.Mesh(geometry, material); 22 | 23 | box.position.x = x; 24 | box.position.y = y; 25 | box.position.z = z; 26 | box.scale.x = scale; 27 | box.scale.y = scale; 28 | box.scale.z = scale; 29 | 30 | add(parent, box); 31 | 32 | const crash = sound(CrashFile, 16 * 40); 33 | 34 | return { 35 | model: box, 36 | bodies: [ 37 | world.add({ 38 | type: "box", 39 | size: [width * scale, height * scale, breadth * scale], 40 | pos: [x, y, z], 41 | rot: [0, 0, 0], 42 | move: dynamic, 43 | density: 0.1, 44 | friction: 0.9, 45 | restitution: 0.2, 46 | belongsTo: 1, 47 | collidesWith: 0xffffffff 48 | }) 49 | ], 50 | collision: (self, other, contact, entities, { gamepadController }) => { 51 | if (!contact.close) { 52 | crash(); 53 | 54 | const camera = entities.camera; 55 | 56 | if (camera) 57 | camera.shake(); 58 | 59 | if (gamepadController) 60 | gamepadController.vibrate({ duration: 300, strongMagnitude: 0.3 }); 61 | } 62 | }, 63 | removable: (frustum, self) => !frustum.intersectsObject(self.model) 64 | }; 65 | }; 66 | -------------------------------------------------------------------------------- /src/game/components/camera.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { remap } from "../utils"; 3 | import { noise } from "../utils/perlin"; 4 | 5 | export default () => { 6 | const camera = new THREE.PerspectiveCamera( 7 | 90, 8 | window.innerWidth / window.innerHeight, 9 | 1, 10 | 1000 11 | ); 12 | 13 | const lookAt = camera.lookAt; 14 | 15 | //-- Overriding the lookAt function so I always 16 | //-- have a quick reference to the lookAt vector 17 | camera.lookAt = vec => { 18 | lookAt.apply(camera, [vec]); 19 | camera.target = vec; 20 | }; 21 | 22 | camera.timelines = {}; 23 | 24 | camera.shake = (duration = 400) => { 25 | if (!camera.timelines.shake) { 26 | camera.timelines.shake = { 27 | duration, 28 | startPos: camera.position.clone(), 29 | seed: Date.now(), 30 | update(self, entities, percent, { seed, startPos }) { 31 | self.position.x = 32 | startPos.x + remap(noise(seed + percent), 0, 1, -1.25, 1.25); 33 | self.position.y = 34 | startPos.y + 35 | remap(noise(seed + 250 + percent), 0, 1, -1.25, 1.25); 36 | } 37 | }; 38 | } 39 | }; 40 | 41 | camera.resize = (width, height, dpr) => { 42 | camera.aspect = width / height; 43 | camera.updateProjectionMatrix(); 44 | }; 45 | 46 | return camera; 47 | }; 48 | -------------------------------------------------------------------------------- /src/game/components/cuphead.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import Sprite from "./sprite"; 3 | import { between } from "../utils"; 4 | import { promisifyLoader } from "../utils/three"; 5 | import CupheadFile from "../../assets/sprite-sheets/cuphead.png"; 6 | 7 | const loader = promisifyLoader(new THREE.TextureLoader()); 8 | const spriteSheet = loader.load(CupheadFile); 9 | 10 | export default async ({ parent, x = 0, y = 0, z = 0}) => { 11 | 12 | const sprite = await Sprite({ 13 | parent, 14 | x, 15 | y, 16 | z, 17 | spriteSheet, 18 | columns: 16, 19 | rows: 8, 20 | actions: { 21 | idle: { 22 | start: { row: 2, column: 0 } 23 | }, 24 | jump: { 25 | start: { row: 0, column: 0 }, 26 | end: { row: 0, column: 9 }, 27 | loop: false 28 | }, 29 | s: { 30 | start: { row: 1, column: 0 }, 31 | end: { row: 1, column: 12 } 32 | }, 33 | se: { 34 | start: { row: 3, column: 0 }, 35 | end: { row: 3, column: 15 } 36 | }, 37 | e: { 38 | start: { row: 4, column: 0 }, 39 | end: { row: 4, column: 13 } 40 | }, 41 | ne: { 42 | start: { row: 6, column: 0 }, 43 | end: { row: 6, column: 14 } 44 | }, 45 | n: { 46 | start: { row: 7, column: 1 }, 47 | end: { row: 7, column: 15 } 48 | }, 49 | nw: { 50 | start: { row: 6, column: 0 }, 51 | end: { row: 6, column: 14 }, 52 | flipX: true 53 | }, 54 | w: { 55 | start: { row: 4, column: 0 }, 56 | end: { row: 4, column: 13 }, 57 | flipX: true 58 | }, 59 | sw: { 60 | start: { row: 3, column: 0 }, 61 | end: { row: 3, column: 15 }, 62 | flipX: true 63 | } 64 | } 65 | }); 66 | 67 | sprite.timelines.controls = { 68 | while: true, 69 | directions: [ 70 | { heading: 0, action: "e" }, 71 | { heading: -45, action: "ne" }, 72 | { heading: -90, action: "n" }, 73 | { heading: -135, action: "nw" }, 74 | { heading: -180, action: "w" }, 75 | { heading: 45, action: "se" }, 76 | { heading: 90, action: "s" }, 77 | { heading: 135, action: "sw" }, 78 | { heading: 180, action: "w" } 79 | ], 80 | update(self, entities, { directions }, { gamepadController }) { 81 | if (gamepadController.leftStick.heading !== null ) { 82 | const degrees = THREE.Math.radToDeg(gamepadController.leftStick.heading) 83 | const direction = directions.find(x => between(degrees, x.heading - 25, x.heading + 25)) 84 | 85 | self.actions[direction.action]() 86 | } else self.actions.idle(); 87 | } 88 | }; 89 | 90 | return sprite; 91 | }; 92 | -------------------------------------------------------------------------------- /src/game/components/cylinder.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { add } from "../utils/three"; 3 | 4 | export default ({ 5 | parent, 6 | world, 7 | dynamic = true, 8 | x = 0, 9 | y = 0, 10 | z = 0, 11 | radius = 0.5, 12 | height = 1.1, 13 | segments = 32, 14 | scale = 1, 15 | color = 0x0fe61f, 16 | opacity = 1, 17 | }) => { 18 | const geometry = new THREE.CylinderGeometry(radius, radius, height, segments); 19 | const material = new THREE.MeshStandardMaterial({ color, transparent: opacity < 1, opacity, flatShading: true }); 20 | const cylinder = new THREE.Mesh(geometry, material); 21 | 22 | cylinder.position.x = x; 23 | cylinder.position.y = y; 24 | cylinder.position.z = z; 25 | cylinder.scale.x = scale; 26 | cylinder.scale.y = scale; 27 | cylinder.scale.z = scale; 28 | 29 | add(parent, cylinder); 30 | 31 | return { 32 | model: cylinder, 33 | bodies: [ 34 | world.add({ 35 | type: "cylinder", 36 | size: [radius * scale, height * scale], 37 | pos: [x, y, z], 38 | rot: [0, 0, 0], 39 | move: dynamic, 40 | density: 0.1, 41 | friction: 0.9, 42 | restitution: 0.2, 43 | belongsTo: 1, 44 | collidesWith: 0xffffffff 45 | }) 46 | ], 47 | removable: (frustum, self) => !frustum.intersectsObject(self.model) 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /src/game/components/droid.js: -------------------------------------------------------------------------------- 1 | import { promisifyLoader } from "../utils/three"; 2 | import FBXLoader from "../utils/three/fbx-loader"; 3 | import AnimatedModel from "./animated-model"; 4 | import DroidFile from "../../assets/models/droid.fbx"; 5 | 6 | const loader = promisifyLoader(new FBXLoader()); 7 | const mesh = loader.load(DroidFile); 8 | 9 | export default async ({ parent, x = 0, y = 0, z = 0}) => { 10 | 11 | const animated = await AnimatedModel({ parent, x, y, z, mesh, scale: 0.0035 }) 12 | 13 | return animated; 14 | }; 15 | -------------------------------------------------------------------------------- /src/game/components/hud.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | class HUDRenderer extends React.Component { 4 | shouldComponentUpdate(nextProps) { 5 | const k1 = this.props.keyboardController || {}; 6 | const k2 = nextProps.keyboardController || {}; 7 | 8 | const g1 = this.props.gamepadController || {}; 9 | const g2 = nextProps.gamepadController || {}; 10 | 11 | const m1 = this.props.mouseController || {}; 12 | const m2 = nextProps.mouseController || {}; 13 | 14 | return ( 15 | k1.w !== k2.w || 16 | k1.a !== k2.a || 17 | k1.s !== k2.s || 18 | k1.d !== k2.d || 19 | k1.space !== k2.space || 20 | k1.control !== k2.control || 21 | g1.leftTrigger !== g2.leftTrigger || 22 | g1.rightTrigger !== g2.rightTrigger || 23 | g1.leftStick.x !== g2.leftStick.x || 24 | g1.leftStick.y !== g2.leftStick.y || 25 | g1.rightStick.x !== g2.rightStick.x || 26 | g1.rightStick.y !== g2.rightStick.y || 27 | g1.button0 !== g2.button0 || 28 | g1.button1 !== g2.button1 || 29 | m1.wheel !== m2.wheel || 30 | m1.left !== m2.left || 31 | m1.right !== m2.right || 32 | m1.middle !== m2.middle || 33 | m1.position.x !== m2.position.x || 34 | m1.position.y !== m2.position.y 35 | ); 36 | } 37 | 38 | render() { 39 | const { w, a, s, d, space, control } = this.props.keyboardController || {}; 40 | const { 41 | button0, 42 | button1, 43 | leftTrigger = 0, 44 | rightTrigger = 0, 45 | leftStick = { x: 0, y: 0 }, 46 | rightStick = { x: 0, y: 0 } 47 | } = this.props.gamepadController || {}; 48 | const { wheel, left, middle, right, position = { x: 0, y: 0 } } = 49 | this.props.mouseController || {}; 50 | const onColor = "cornflowerblue"; 51 | const offColor = "white"; 52 | 53 | return ( 54 |
55 |

56 | B0 57 |    58 | B1 59 |    60 | {`LS(${leftStick.x.toFixed(2)}, ${leftStick.y.toFixed(2)})`} 61 |    62 | {`LT(${leftTrigger.toFixed(2)})`} 63 |    64 | {`RS(${rightStick.x.toFixed(2)}, ${rightStick.y.toFixed(2)})`} 65 |    66 | {`RT(${rightTrigger.toFixed(2)})`} 67 |    68 | W 69 | A 70 | S 71 | D 72 |    73 | SPACE 74 |    75 | CONTROL 76 |    77 | 78 | MOUSE( 79 | {wheel} 80 |    81 | L 82 | M 83 | R 84 |    85 | {`${position.x}, ${position.y}`} 86 | ) 87 | 88 |

89 |
90 | ); 91 | } 92 | } 93 | 94 | const css = { 95 | hud: { 96 | fontSize: 10, 97 | color: "white", 98 | zIndex: 100, 99 | marginTop: -100 100 | } 101 | } 102 | 103 | export default () => { 104 | return { renderer: }; 105 | }; 106 | -------------------------------------------------------------------------------- /src/game/components/jet.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import AnimatedModel from "./animated-model"; 3 | import { firstMesh, promisifyLoader } from "../utils/three"; 4 | import GLTFLoader from "../utils/three/gltf-loader"; 5 | import { between } from "../utils"; 6 | import JetFile from "../../assets/models/jet.glb"; 7 | 8 | const loader = promisifyLoader(new GLTFLoader()); 9 | const mesh = loader.load(JetFile).then(gltf => firstMesh(gltf.scene)); 10 | 11 | export default async ({ parent, x = 0, y = 0, z = 0}) => { 12 | 13 | const animated = await AnimatedModel({ 14 | parent, 15 | x, 16 | y, 17 | z, 18 | mesh, 19 | morphTargets: { 20 | rudderLeft: 0, 21 | rudderRight: 1, 22 | leftFlapUp: 2, 23 | leftFlapDown: 3, 24 | rightFlapUp: 4, 25 | rightFlapDown: 5 26 | } 27 | }); 28 | 29 | const timelines = {}; 30 | 31 | timelines.controls = { 32 | while: true, 33 | directions: [ 34 | { heading: 0, pose: "rudderRight" }, 35 | { heading: -60, pose: "leftFlapDown" }, 36 | { heading: -120, pose: "leftFlapUp" }, 37 | { heading: -180, pose: "rudderLeft" }, 38 | { heading: 60, pose: "rightFlapUp" }, 39 | { heading: 120, pose: "rightFlapDown" }, 40 | { heading: 180, pose: "rudderLeft" } 41 | ], 42 | update(self, entities, { directions }, { gamepadController }) { 43 | let target = null; 44 | 45 | if (gamepadController.leftStick.heading !== null ) { 46 | const degrees = THREE.Math.radToDeg(gamepadController.leftStick.heading) 47 | const direction = directions.find(x => between(degrees, x.heading - 30, x.heading + 30)) 48 | 49 | if (direction) 50 | target = direction.pose; 51 | } 52 | 53 | directions.forEach(x => { 54 | const pose = self.poses[x.pose]; 55 | const val = pose(); 56 | 57 | pose(val + (x.pose === target ? 0.01 : -0.01)) 58 | }); 59 | } 60 | }; 61 | 62 | return { ...animated, ...{ timelines }}; 63 | }; 64 | -------------------------------------------------------------------------------- /src/game/components/particles.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import GPUParticleSystem from "../graphics/gpu-particle-system"; 3 | import { add, promisifyLoader } from "../utils/three"; 4 | import NoiseFile from "../../assets/textures/perlin.png"; 5 | 6 | const loader = promisifyLoader(new THREE.TextureLoader()); 7 | const _noiseTexture = loader.load(NoiseFile); 8 | 9 | export default async ({ 10 | maxParticles = 250, 11 | noiseTexture, 12 | particleTexture, 13 | parent, 14 | options = {}, 15 | spawnOptions = {}, 16 | beforeSpawn = () => {} 17 | }) => { 18 | const emitter = new GPUParticleSystem({ 19 | maxParticles, 20 | particleNoiseTex: await Promise.resolve(noiseTexture || _noiseTexture), 21 | particleSpriteTex: await Promise.resolve(particleTexture) 22 | }); 23 | 24 | add(parent, emitter); 25 | 26 | return { 27 | emitter, 28 | options, 29 | spawnOptions, 30 | beforeSpawn, 31 | tick: 0 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /src/game/components/portal.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { promisifyLoader } from "../utils/three"; 3 | import Particles from "./particles"; 4 | import ParticleFile from "../../assets/textures/particle.png"; 5 | 6 | const loader = promisifyLoader(new THREE.TextureLoader()); 7 | const particleTexture = loader.load(ParticleFile); 8 | 9 | export default async ({ 10 | parent, 11 | x = 0, 12 | y = 0, 13 | z = 0, 14 | height = 0.5, 15 | radius = 0.5, 16 | verticalSpeed = 0.01, 17 | horizontalSpeed = 0.3, 18 | color = 0xffffff 19 | }) => { 20 | 21 | const swirl = await Particles({ 22 | parent, 23 | particleTexture, 24 | maxParticles: 250, 25 | options: { 26 | position: new THREE.Vector3(x, y, z), 27 | positionRandomness: 0, 28 | velocity: new THREE.Vector3(), 29 | velocityRandomness: 0, 30 | color, 31 | colorRandomness: 0, 32 | turbulence: 0, 33 | lifetime: 12, 34 | size: 10, 35 | sizeRandomness: 0, 36 | verticalSpeed, 37 | theta: 0 38 | }, 39 | spawnOptions: { 40 | spawnRate: 20, 41 | timeScale: 1 42 | }, 43 | beforeSpawn(self, entities, { options }) { 44 | options.theta += horizontalSpeed; 45 | options.position.x = x + Math.cos(options.theta) * radius; 46 | options.position.y += options.verticalSpeed; 47 | options.position.z = z + Math.sin(options.theta) * radius; 48 | 49 | if (Math.abs(options.position.y - y) > height) 50 | options.verticalSpeed *= -1; 51 | } 52 | }); 53 | 54 | return { 55 | model: swirl.emitter, 56 | particles: { 57 | swirl 58 | } 59 | }; 60 | }; 61 | -------------------------------------------------------------------------------- /src/game/components/sprite.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { cloneTexture, add } from "../utils/three"; 3 | import { remap, clamp } from "../utils"; 4 | 5 | export default async ({ parent, x = 0, z = 0, y = 0, spriteSheet, rows, columns, actions: mappings = {} }) => { 6 | 7 | const texture = cloneTexture(await Promise.resolve(spriteSheet)); 8 | 9 | texture.needsUpdate = true; 10 | texture.repeat.set(1 / columns, 1 / rows); 11 | 12 | const spriteMaterial = new THREE.SpriteMaterial({ map: texture, color: 0xffffff }); 13 | const sprite = new THREE.Sprite(spriteMaterial); 14 | 15 | sprite.position.x = x; 16 | sprite.position.y = y; 17 | sprite.position.z = z; 18 | 19 | add(parent, sprite); 20 | 21 | const actions = {}; 22 | const timelines = {}; 23 | 24 | Object.keys(mappings).forEach(key => { 25 | actions[key] = () => { 26 | 27 | if (timelines.action && timelines.action.key === key) 28 | return; 29 | 30 | let { start, end, loop = true, speed = 0.25, update, scaleX = 1, scaleY = 1, flipX = false, flipY = false } = mappings[key]; 31 | end = end || start; 32 | 33 | sprite.scale.x = scaleX; 34 | sprite.scale.y = scaleY; 35 | 36 | texture.repeat.x = Math.abs(texture.repeat.x) * (flipX ? -1 : 1); 37 | texture.repeat.y = Math.abs(texture.repeat.y) * (flipY ? -1 : 1); 38 | 39 | let startColumn = start.column; 40 | let startRow = start.row; 41 | let endColumn = end.column; 42 | let endRow = end.row; 43 | 44 | if (flipX) { 45 | startColumn++; 46 | endColumn++; 47 | } 48 | 49 | if (flipY) { 50 | startRow++; 51 | endRow++; 52 | } 53 | 54 | const increment = speed * 1 / Math.max(Math.abs(endColumn - startColumn), Math.abs(endRow - startRow), 1) 55 | 56 | if (loop) { 57 | endColumn++; 58 | endRow++; 59 | } 60 | 61 | timelines.action = { 62 | while: true, 63 | counter: 0, 64 | key, 65 | update(entity, entities, timeline, args) { 66 | const percentage = loop ? timeline.counter % 1 : clamp(timeline.counter, 0, 1) 67 | const column = Math.trunc(remap(percentage, 0, 1, startColumn, endColumn)) 68 | const row = Math.trunc(remap(percentage, 0, 1, startRow, endRow)) 69 | 70 | texture.offset.x = column / columns; 71 | texture.offset.y = row / rows; 72 | timeline.counter += increment; 73 | 74 | if (update) 75 | update(entity, entities, { column, row }, args) 76 | } 77 | } 78 | } 79 | }); 80 | 81 | return { 82 | model: sprite, 83 | actions, 84 | timelines 85 | }; 86 | }; -------------------------------------------------------------------------------- /src/game/components/turntable.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { add, rotateAroundPoint } from "../utils/three"; 3 | 4 | export default ({ parent, world, items = [], x = 0, y = 0, z = 0, radius = 4, height = 0.2, color = 0xdddddd, segments = 32, opacity = 1 }) => { 5 | 6 | const geometry = new THREE.CylinderGeometry(radius, radius + radius * 0.1, height, segments); 7 | const material = new THREE.MeshStandardMaterial({ color, transparent: opacity < 1, opacity, flatShading: true }); 8 | const cylinder = new THREE.Mesh(geometry, material); 9 | 10 | cylinder.position.x = x; 11 | cylinder.position.y = y; 12 | cylinder.position.z = z; 13 | 14 | items.forEach((item, idx) => { 15 | item.model.position.z = radius - 1; 16 | rotateAroundPoint(item.model, cylinder.position, { y: ((Math.PI * 2) / items.length) * idx }) 17 | add(cylinder, item); 18 | 19 | if (item.bodies) 20 | item.bodies[0].position.set(item.model.position.x, item.model.position.y, item.model.position.z) 21 | }) 22 | 23 | add(parent, cylinder); 24 | 25 | const primary = world.add({ 26 | type: "cylinder", 27 | size: [radius, height], 28 | pos: [x, y, z], 29 | rot: [0, 0, 0], 30 | move: true, 31 | density: 0.9, 32 | friction: 0.9, 33 | restitution: 0.2, 34 | belongsTo: 1, 35 | collidesWith: 0xffffffff 36 | }); 37 | 38 | const base = world.add({ type: "cylinder", size: [radius, height], pos: [x, y, z], move: false }); 39 | 40 | const hinge = world.add({ 41 | type: "jointHinge", 42 | body1: primary, 43 | body2: base, 44 | axe1: [0, 1, 0], 45 | axe2: [0, 1, 0], 46 | pos1: [primary.position.x, primary.position.y, primary.position.z], 47 | pos2: [base.position.x, base.position.y, base.position.z] 48 | }); 49 | 50 | return { 51 | model: cylinder, 52 | bodies: [primary, base, hinge], 53 | timelines: { 54 | swipe: { 55 | while: true, 56 | update(self, entities, timeline, { gamepadController }) { 57 | if (gamepadController.rightTrigger) 58 | self.bodies[0].angularVelocity.set(0, gamepadController.rightTrigger, 0) 59 | else if (gamepadController.leftTrigger) 60 | self.bodies[0].angularVelocity.set(0, -gamepadController.leftTrigger, 0) 61 | } 62 | } 63 | } 64 | } 65 | }; -------------------------------------------------------------------------------- /src/game/entities.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import Camera from "./components/camera"; 3 | import Cuphead from "./components/cuphead"; 4 | import HUD from "./components/hud"; 5 | import Turntable from "./components/turntable"; 6 | import Droid from "./components/droid"; 7 | import Portal from "./components/portal"; 8 | import Jet from "./components/jet"; 9 | import { clear } from "./utils/three"; 10 | import * as OIMO from "oimo"; 11 | 12 | const scene = new THREE.Scene(); 13 | const camera = Camera(); 14 | const world = new OIMO.World({ 15 | timestep: 1 / 60, 16 | iterations: 8, 17 | broadphase: 2, 18 | worldscale: 1, 19 | random: true, 20 | info: false, 21 | gravity: [0, -9.8 ,0] 22 | }); 23 | 24 | export default async () => { 25 | clear(scene); 26 | world.clear(); 27 | 28 | const ambient = new THREE.AmbientLight(0xffffff, 1); 29 | const sunlight = new THREE.DirectionalLight(0xffffff, 0.95); 30 | 31 | sunlight.position.set(50, 50, 50); 32 | 33 | scene.add(ambient); 34 | scene.add(sunlight); 35 | 36 | camera.position.set(0, 2, 6); 37 | camera.lookAt(new THREE.Vector3(0, 0, 0)); 38 | 39 | const cuphead = await Cuphead({ y: 1 }); 40 | const droid = await Droid({ y: 1 }); 41 | const portal = await Portal({ y: 1 }); 42 | const jet = await Jet({ y: 1 }); 43 | 44 | const turntable = Turntable({ parent: scene, world, items: [droid, cuphead, portal, jet] }); 45 | const hud = HUD(); 46 | 47 | const entities = { 48 | scene, 49 | camera, 50 | world, 51 | droid, 52 | cuphead, 53 | portal, 54 | jet, 55 | turntable, 56 | hud 57 | } 58 | 59 | return entities; 60 | }; 61 | -------------------------------------------------------------------------------- /src/game/graphics/effect-composer.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import CopyShader from "./shaders/copy-shader"; 3 | import ShaderPass from "./passes/shader-pass"; 4 | import MaskPass from "./passes/mask-pass"; 5 | import ClearMaskPass from "./passes/clear-mask-pass"; 6 | 7 | /** 8 | * @author alteredq / http://alteredqualia.com/ 9 | */ 10 | 11 | const EffectComposer = function ( renderer, renderTarget ) { 12 | 13 | this.renderer = renderer; 14 | 15 | if ( renderTarget === undefined ) { 16 | 17 | var parameters = { 18 | minFilter: THREE.LinearFilter, 19 | magFilter: THREE.LinearFilter, 20 | format: THREE.RGBAFormat, 21 | stencilBuffer: false 22 | }; 23 | 24 | var size = new THREE.Vector2(); 25 | renderer.getDrawingBufferSize(size); 26 | renderTarget = new THREE.WebGLRenderTarget( size.width, size.height, parameters ); 27 | renderTarget.texture.name = 'EffectComposer.rt1'; 28 | 29 | } 30 | 31 | this.renderTarget1 = renderTarget; 32 | this.renderTarget2 = renderTarget.clone(); 33 | this.renderTarget2.texture.name = 'EffectComposer.rt2'; 34 | 35 | this.writeBuffer = this.renderTarget1; 36 | this.readBuffer = this.renderTarget2; 37 | 38 | this.passes = []; 39 | 40 | // dependencies 41 | 42 | if ( CopyShader === undefined ) { 43 | 44 | console.error( 'THREE.EffectComposer relies on THREE.CopyShader' ); 45 | 46 | } 47 | 48 | if ( ShaderPass === undefined ) { 49 | 50 | console.error( 'THREE.EffectComposer relies on THREE.ShaderPass' ); 51 | 52 | } 53 | 54 | this.copyPass = new ShaderPass( CopyShader ); 55 | 56 | }; 57 | 58 | Object.assign( EffectComposer.prototype, { 59 | 60 | swapBuffers: function () { 61 | 62 | var tmp = this.readBuffer; 63 | this.readBuffer = this.writeBuffer; 64 | this.writeBuffer = tmp; 65 | 66 | }, 67 | 68 | addPass: function ( pass ) { 69 | 70 | this.passes.push( pass ); 71 | 72 | var size = new THREE.Vector2(); 73 | this.renderer.getDrawingBufferSize(size); 74 | pass.setSize( size.width, size.height ); 75 | 76 | }, 77 | 78 | insertPass: function ( pass, index ) { 79 | 80 | this.passes.splice( index, 0, pass ); 81 | 82 | }, 83 | 84 | render: function ( delta ) { 85 | 86 | var maskActive = false; 87 | 88 | var pass, i, il = this.passes.length; 89 | 90 | for ( i = 0; i < il; i ++ ) { 91 | 92 | pass = this.passes[ i ]; 93 | 94 | if ( pass.enabled === false ) continue; 95 | 96 | pass.render( this.renderer, this.writeBuffer, this.readBuffer, delta, maskActive ); 97 | 98 | if ( pass.needsSwap ) { 99 | 100 | if ( maskActive ) { 101 | 102 | var context = this.renderer.context; 103 | 104 | context.stencilFunc( context.NOTEQUAL, 1, 0xffffffff ); 105 | 106 | this.copyPass.render( this.renderer, this.writeBuffer, this.readBuffer, delta ); 107 | 108 | context.stencilFunc( context.EQUAL, 1, 0xffffffff ); 109 | 110 | } 111 | 112 | this.swapBuffers(); 113 | 114 | } 115 | 116 | if ( MaskPass !== undefined ) { 117 | 118 | if ( pass instanceof MaskPass ) { 119 | 120 | maskActive = true; 121 | 122 | } else if ( pass instanceof ClearMaskPass ) { 123 | 124 | maskActive = false; 125 | 126 | } 127 | 128 | } 129 | 130 | } 131 | 132 | }, 133 | 134 | reset: function ( renderTarget ) { 135 | 136 | if ( renderTarget === undefined ) { 137 | 138 | var size = new THREE.Vector2(); 139 | this.renderer.getDrawingBufferSize(size); 140 | 141 | renderTarget = this.renderTarget1.clone(); 142 | renderTarget.setSize( size.width, size.height ); 143 | 144 | } 145 | 146 | this.renderTarget1.dispose(); 147 | this.renderTarget2.dispose(); 148 | this.renderTarget1 = renderTarget; 149 | this.renderTarget2 = renderTarget.clone(); 150 | 151 | this.writeBuffer = this.renderTarget1; 152 | this.readBuffer = this.renderTarget2; 153 | 154 | }, 155 | 156 | setSize: function ( width, height ) { 157 | 158 | this.renderTarget1.setSize( width, height ); 159 | this.renderTarget2.setSize( width, height ); 160 | 161 | for ( var i = 0; i < this.passes.length; i ++ ) { 162 | 163 | this.passes[ i ].setSize( width, height ); 164 | 165 | } 166 | 167 | } 168 | 169 | } ); 170 | 171 | export default EffectComposer; -------------------------------------------------------------------------------- /src/game/graphics/gpu-particle-system.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | /* 4 | * GPU Particle System 5 | * @author flimshaw - Charlie Hoey - http://charliehoey.com 6 | * 7 | * A simple to use, general purpose GPU system. Particles are spawn-and-forget with 8 | * several options available, and do not require monitoring or cleanup after spawning. 9 | * Because the paths of all particles are completely deterministic once spawned, the scale 10 | * and direction of time is also variable. 11 | * 12 | * Currently uses a static wrapping perlin noise texture for turbulence, and a small png texture for 13 | * particles, but adding support for a particle texture atlas or changing to a different type of turbulence 14 | * would be a fairly light day's work. 15 | * 16 | * Shader and javascript packing code derrived from several Stack Overflow examples. 17 | * 18 | * https://github.com/mrdoob/three.js/blob/dev/examples/js/GPUParticleSystem.js 19 | * 20 | */ 21 | 22 | const GPUParticleSystem = function ( options ) { 23 | 24 | THREE.Object3D.apply( this, arguments ); 25 | 26 | options = options || {}; 27 | 28 | // parse options and use defaults 29 | 30 | this.PARTICLE_COUNT = options.maxParticles || 1000000; 31 | this.PARTICLE_CONTAINERS = options.containerCount || 1; 32 | 33 | this.PARTICLE_NOISE_TEXTURE = options.particleNoiseTex || null; 34 | this.PARTICLE_SPRITE_TEXTURE = options.particleSpriteTex || null; 35 | 36 | this.PARTICLES_PER_CONTAINER = Math.ceil( this.PARTICLE_COUNT / this.PARTICLE_CONTAINERS ); 37 | this.PARTICLE_CURSOR = 0; 38 | this.time = 0; 39 | this.particleContainers = []; 40 | this.rand = []; 41 | 42 | // custom vertex and fragement shader 43 | 44 | var GPUParticleShader = { 45 | 46 | vertexShader: [ 47 | 48 | 'uniform float uTime;', 49 | 'uniform float uScale;', 50 | 'uniform sampler2D tNoise;', 51 | 52 | 'attribute vec3 positionStart;', 53 | 'attribute float startTime;', 54 | 'attribute vec3 velocity;', 55 | 'attribute float turbulence;', 56 | 'attribute vec3 color;', 57 | 'attribute float size;', 58 | 'attribute float lifeTime;', 59 | 60 | 'varying vec4 vColor;', 61 | 'varying float lifeLeft;', 62 | 63 | 'void main() {', 64 | 65 | // unpack things from our attributes' 66 | 67 | ' vColor = vec4( color, 1.0 );', 68 | 69 | // convert our velocity back into a value we can use' 70 | 71 | ' vec3 newPosition;', 72 | ' vec3 v;', 73 | 74 | ' float timeElapsed = uTime - startTime;', 75 | 76 | ' lifeLeft = 1.0 - ( timeElapsed / lifeTime );', 77 | 78 | ' gl_PointSize = ( uScale * size ) * lifeLeft;', 79 | 80 | ' v.x = ( velocity.x - 0.5 ) * 3.0;', 81 | ' v.y = ( velocity.y - 0.5 ) * 3.0;', 82 | ' v.z = ( velocity.z - 0.5 ) * 3.0;', 83 | 84 | ' newPosition = positionStart + ( v * 10.0 ) * timeElapsed;', 85 | 86 | ' vec3 noise = texture2D( tNoise, vec2( newPosition.x * 0.015 + ( uTime * 0.05 ), newPosition.y * 0.02 + ( uTime * 0.015 ) ) ).rgb;', 87 | ' vec3 noiseVel = ( noise.rgb - 0.5 ) * 30.0;', 88 | 89 | ' newPosition = mix( newPosition, newPosition + vec3( noiseVel * ( turbulence * 5.0 ) ), ( timeElapsed / lifeTime ) );', 90 | 91 | ' if( v.y > 0. && v.y < .05 ) {', 92 | 93 | ' lifeLeft = 0.0;', 94 | 95 | ' }', 96 | 97 | ' if( v.x < - 1.45 ) {', 98 | 99 | ' lifeLeft = 0.0;', 100 | 101 | ' }', 102 | 103 | ' if( timeElapsed > 0.0 ) {', 104 | 105 | ' gl_Position = projectionMatrix * modelViewMatrix * vec4( newPosition, 1.0 );', 106 | 107 | ' } else {', 108 | 109 | ' gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );', 110 | ' lifeLeft = 0.0;', 111 | ' gl_PointSize = 0.;', 112 | 113 | ' }', 114 | 115 | '}' 116 | 117 | ].join( '\n' ), 118 | 119 | fragmentShader: [ 120 | 121 | 'float scaleLinear( float value, vec2 valueDomain ) {', 122 | 123 | ' return ( value - valueDomain.x ) / ( valueDomain.y - valueDomain.x );', 124 | 125 | '}', 126 | 127 | 'float scaleLinear( float value, vec2 valueDomain, vec2 valueRange ) {', 128 | 129 | ' return mix( valueRange.x, valueRange.y, scaleLinear( value, valueDomain ) );', 130 | 131 | '}', 132 | 133 | 'varying vec4 vColor;', 134 | 'varying float lifeLeft;', 135 | 136 | 'uniform sampler2D tSprite;', 137 | 138 | 'void main() {', 139 | 140 | ' float alpha = 0.;', 141 | 142 | ' if( lifeLeft > 0.995 ) {', 143 | 144 | ' alpha = scaleLinear( lifeLeft, vec2( 1.0, 0.995 ), vec2( 0.0, 1.0 ) );', 145 | 146 | ' } else {', 147 | 148 | ' alpha = lifeLeft * 0.75;', 149 | 150 | ' }', 151 | 152 | ' vec4 tex = texture2D( tSprite, gl_PointCoord );', 153 | ' gl_FragColor = vec4( vColor.rgb * tex.a, alpha * tex.a );', 154 | 155 | '}' 156 | 157 | ].join( '\n' ) 158 | 159 | }; 160 | 161 | // preload a million random numbers 162 | 163 | var i; 164 | 165 | for ( i = 1e5; i > 0; i -- ) { 166 | 167 | this.rand.push( Math.random() - 0.5 ); 168 | 169 | } 170 | 171 | this.random = function () { 172 | 173 | return ++ i >= this.rand.length ? this.rand[ i = 1 ] : this.rand[ i ]; 174 | 175 | }; 176 | 177 | var textureLoader = new THREE.TextureLoader(); 178 | 179 | this.particleNoiseTex = this.PARTICLE_NOISE_TEXTURE || textureLoader.load( 'textures/perlin-512.png' ); 180 | this.particleNoiseTex.wrapS = this.particleNoiseTex.wrapT = THREE.RepeatWrapping; 181 | 182 | this.particleSpriteTex = this.PARTICLE_SPRITE_TEXTURE || textureLoader.load( 'textures/particle2.png' ); 183 | this.particleSpriteTex.wrapS = this.particleSpriteTex.wrapT = THREE.RepeatWrapping; 184 | 185 | this.particleShaderMat = new THREE.ShaderMaterial( { 186 | transparent: true, 187 | depthWrite: false, 188 | uniforms: { 189 | 'uTime': { 190 | value: 0.0 191 | }, 192 | 'uScale': { 193 | value: 1.0 194 | }, 195 | 'tNoise': { 196 | value: this.particleNoiseTex 197 | }, 198 | 'tSprite': { 199 | value: this.particleSpriteTex 200 | } 201 | }, 202 | blending: THREE.AdditiveBlending, 203 | vertexShader: GPUParticleShader.vertexShader, 204 | fragmentShader: GPUParticleShader.fragmentShader 205 | } ); 206 | 207 | // define defaults for all values 208 | 209 | this.particleShaderMat.defaultAttributeValues.particlePositionsStartTime = [ 0, 0, 0, 0 ]; 210 | this.particleShaderMat.defaultAttributeValues.particleVelColSizeLife = [ 0, 0, 0, 0 ]; 211 | 212 | this.init = function () { 213 | 214 | for ( var i = 0; i < this.PARTICLE_CONTAINERS; i ++ ) { 215 | 216 | var c = new GPUParticleContainer( this.PARTICLES_PER_CONTAINER, this ); 217 | this.particleContainers.push( c ); 218 | this.add( c ); 219 | 220 | } 221 | 222 | }; 223 | 224 | this.spawnParticle = function ( options ) { 225 | 226 | this.PARTICLE_CURSOR ++; 227 | 228 | if ( this.PARTICLE_CURSOR >= this.PARTICLE_COUNT ) { 229 | 230 | this.PARTICLE_CURSOR = 1; 231 | 232 | } 233 | 234 | var currentContainer = this.particleContainers[ Math.floor( this.PARTICLE_CURSOR / this.PARTICLES_PER_CONTAINER ) ]; 235 | 236 | currentContainer.spawnParticle( options ); 237 | 238 | }; 239 | 240 | this.update = function ( time ) { 241 | 242 | for ( var i = 0; i < this.PARTICLE_CONTAINERS; i ++ ) { 243 | 244 | this.particleContainers[ i ].update( time ); 245 | 246 | } 247 | 248 | }; 249 | 250 | this.dispose = function () { 251 | 252 | this.particleShaderMat.dispose(); 253 | this.particleNoiseTex.dispose(); 254 | this.particleSpriteTex.dispose(); 255 | 256 | for ( var i = 0; i < this.PARTICLE_CONTAINERS; i ++ ) { 257 | 258 | this.particleContainers[ i ].dispose(); 259 | 260 | } 261 | 262 | }; 263 | 264 | this.init(); 265 | 266 | }; 267 | 268 | GPUParticleSystem.prototype = Object.create( THREE.Object3D.prototype ); 269 | GPUParticleSystem.prototype.constructor = GPUParticleSystem; 270 | 271 | 272 | // Subclass for particle containers, allows for very large arrays to be spread out 273 | 274 | const GPUParticleContainer = function ( maxParticles, particleSystem ) { 275 | 276 | THREE.Object3D.apply( this, arguments ); 277 | 278 | this.PARTICLE_COUNT = maxParticles || 100000; 279 | this.PARTICLE_CURSOR = 0; 280 | this.time = 0; 281 | this.offset = 0; 282 | this.count = 0; 283 | this.DPR = window.devicePixelRatio; 284 | this.GPUParticleSystem = particleSystem; 285 | this.particleUpdate = false; 286 | 287 | // geometry 288 | 289 | this.particleShaderGeo = new THREE.BufferGeometry(); 290 | 291 | this.particleShaderGeo.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT * 3 ), 3 ).setDynamic( true ) ); 292 | this.particleShaderGeo.addAttribute( 'positionStart', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT * 3 ), 3 ).setDynamic( true ) ); 293 | this.particleShaderGeo.addAttribute( 'startTime', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT ), 1 ).setDynamic( true ) ); 294 | this.particleShaderGeo.addAttribute( 'velocity', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT * 3 ), 3 ).setDynamic( true ) ); 295 | this.particleShaderGeo.addAttribute( 'turbulence', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT ), 1 ).setDynamic( true ) ); 296 | this.particleShaderGeo.addAttribute( 'color', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT * 3 ), 3 ).setDynamic( true ) ); 297 | this.particleShaderGeo.addAttribute( 'size', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT ), 1 ).setDynamic( true ) ); 298 | this.particleShaderGeo.addAttribute( 'lifeTime', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT ), 1 ).setDynamic( true ) ); 299 | 300 | // material 301 | 302 | this.particleShaderMat = this.GPUParticleSystem.particleShaderMat; 303 | 304 | var position = new THREE.Vector3(); 305 | var velocity = new THREE.Vector3(); 306 | var color = new THREE.Color(); 307 | 308 | this.spawnParticle = function ( options ) { 309 | 310 | var positionStartAttribute = this.particleShaderGeo.getAttribute( 'positionStart' ); 311 | var startTimeAttribute = this.particleShaderGeo.getAttribute( 'startTime' ); 312 | var velocityAttribute = this.particleShaderGeo.getAttribute( 'velocity' ); 313 | var turbulenceAttribute = this.particleShaderGeo.getAttribute( 'turbulence' ); 314 | var colorAttribute = this.particleShaderGeo.getAttribute( 'color' ); 315 | var sizeAttribute = this.particleShaderGeo.getAttribute( 'size' ); 316 | var lifeTimeAttribute = this.particleShaderGeo.getAttribute( 'lifeTime' ); 317 | 318 | options = options || {}; 319 | 320 | // setup reasonable default values for all arguments 321 | 322 | position = options.position !== undefined ? position.copy( options.position ) : position.set( 0, 0, 0 ); 323 | velocity = options.velocity !== undefined ? velocity.copy( options.velocity ) : velocity.set( 0, 0, 0 ); 324 | color = options.color !== undefined ? color.set( options.color ) : color.set( 0xffffff ); 325 | 326 | var positionRandomness = options.positionRandomness !== undefined ? options.positionRandomness : 0; 327 | var velocityRandomness = options.velocityRandomness !== undefined ? options.velocityRandomness : 0; 328 | var colorRandomness = options.colorRandomness !== undefined ? options.colorRandomness : 1; 329 | var turbulence = options.turbulence !== undefined ? options.turbulence : 1; 330 | var lifetime = options.lifetime !== undefined ? options.lifetime : 5; 331 | var size = options.size !== undefined ? options.size : 10; 332 | var sizeRandomness = options.sizeRandomness !== undefined ? options.sizeRandomness : 0; 333 | var smoothPosition = options.smoothPosition !== undefined ? options.smoothPosition : false; 334 | 335 | if ( this.DPR !== undefined ) size *= this.DPR; 336 | 337 | var i = this.PARTICLE_CURSOR; 338 | 339 | // position 340 | 341 | positionStartAttribute.array[ i * 3 + 0 ] = position.x + ( particleSystem.random() * positionRandomness ); 342 | positionStartAttribute.array[ i * 3 + 1 ] = position.y + ( particleSystem.random() * positionRandomness ); 343 | positionStartAttribute.array[ i * 3 + 2 ] = position.z + ( particleSystem.random() * positionRandomness ); 344 | 345 | if ( smoothPosition === true ) { 346 | 347 | positionStartAttribute.array[ i * 3 + 0 ] += - ( velocity.x * particleSystem.random() ); 348 | positionStartAttribute.array[ i * 3 + 1 ] += - ( velocity.y * particleSystem.random() ); 349 | positionStartAttribute.array[ i * 3 + 2 ] += - ( velocity.z * particleSystem.random() ); 350 | 351 | } 352 | 353 | // velocity 354 | 355 | var maxVel = 2; 356 | 357 | var velX = velocity.x + particleSystem.random() * velocityRandomness; 358 | var velY = velocity.y + particleSystem.random() * velocityRandomness; 359 | var velZ = velocity.z + particleSystem.random() * velocityRandomness; 360 | 361 | velX = THREE.Math.clamp( ( velX - ( - maxVel ) ) / ( maxVel - ( - maxVel ) ), 0, 1 ); 362 | velY = THREE.Math.clamp( ( velY - ( - maxVel ) ) / ( maxVel - ( - maxVel ) ), 0, 1 ); 363 | velZ = THREE.Math.clamp( ( velZ - ( - maxVel ) ) / ( maxVel - ( - maxVel ) ), 0, 1 ); 364 | 365 | velocityAttribute.array[ i * 3 + 0 ] = velX; 366 | velocityAttribute.array[ i * 3 + 1 ] = velY; 367 | velocityAttribute.array[ i * 3 + 2 ] = velZ; 368 | 369 | // color 370 | 371 | color.r = THREE.Math.clamp( color.r + particleSystem.random() * colorRandomness, 0, 1 ); 372 | color.g = THREE.Math.clamp( color.g + particleSystem.random() * colorRandomness, 0, 1 ); 373 | color.b = THREE.Math.clamp( color.b + particleSystem.random() * colorRandomness, 0, 1 ); 374 | 375 | colorAttribute.array[ i * 3 + 0 ] = color.r; 376 | colorAttribute.array[ i * 3 + 1 ] = color.g; 377 | colorAttribute.array[ i * 3 + 2 ] = color.b; 378 | 379 | // turbulence, size, lifetime and starttime 380 | 381 | turbulenceAttribute.array[ i ] = turbulence; 382 | sizeAttribute.array[ i ] = size + particleSystem.random() * sizeRandomness; 383 | lifeTimeAttribute.array[ i ] = lifetime; 384 | startTimeAttribute.array[ i ] = this.time + particleSystem.random() * 2e-2; 385 | 386 | // offset 387 | 388 | if ( this.offset === 0 ) { 389 | 390 | this.offset = this.PARTICLE_CURSOR; 391 | 392 | } 393 | 394 | // counter and cursor 395 | 396 | this.count ++; 397 | this.PARTICLE_CURSOR ++; 398 | 399 | if ( this.PARTICLE_CURSOR >= this.PARTICLE_COUNT ) { 400 | 401 | this.PARTICLE_CURSOR = 0; 402 | 403 | } 404 | 405 | this.particleUpdate = true; 406 | 407 | }; 408 | 409 | this.init = function () { 410 | 411 | this.particleSystem = new THREE.Points( this.particleShaderGeo, this.particleShaderMat ); 412 | this.particleSystem.frustumCulled = false; 413 | this.add( this.particleSystem ); 414 | 415 | }; 416 | 417 | this.update = function ( time ) { 418 | 419 | this.time = time; 420 | this.particleShaderMat.uniforms.uTime.value = time; 421 | 422 | this.geometryUpdate(); 423 | 424 | }; 425 | 426 | this.geometryUpdate = function () { 427 | 428 | if ( this.particleUpdate === true ) { 429 | 430 | this.particleUpdate = false; 431 | 432 | var positionStartAttribute = this.particleShaderGeo.getAttribute( 'positionStart' ); 433 | var startTimeAttribute = this.particleShaderGeo.getAttribute( 'startTime' ); 434 | var velocityAttribute = this.particleShaderGeo.getAttribute( 'velocity' ); 435 | var turbulenceAttribute = this.particleShaderGeo.getAttribute( 'turbulence' ); 436 | var colorAttribute = this.particleShaderGeo.getAttribute( 'color' ); 437 | var sizeAttribute = this.particleShaderGeo.getAttribute( 'size' ); 438 | var lifeTimeAttribute = this.particleShaderGeo.getAttribute( 'lifeTime' ); 439 | 440 | if ( this.offset + this.count < this.PARTICLE_COUNT ) { 441 | 442 | positionStartAttribute.updateRange.offset = this.offset * positionStartAttribute.itemSize; 443 | startTimeAttribute.updateRange.offset = this.offset * startTimeAttribute.itemSize; 444 | velocityAttribute.updateRange.offset = this.offset * velocityAttribute.itemSize; 445 | turbulenceAttribute.updateRange.offset = this.offset * turbulenceAttribute.itemSize; 446 | colorAttribute.updateRange.offset = this.offset * colorAttribute.itemSize; 447 | sizeAttribute.updateRange.offset = this.offset * sizeAttribute.itemSize; 448 | lifeTimeAttribute.updateRange.offset = this.offset * lifeTimeAttribute.itemSize; 449 | 450 | positionStartAttribute.updateRange.count = this.count * positionStartAttribute.itemSize; 451 | startTimeAttribute.updateRange.count = this.count * startTimeAttribute.itemSize; 452 | velocityAttribute.updateRange.count = this.count * velocityAttribute.itemSize; 453 | turbulenceAttribute.updateRange.count = this.count * turbulenceAttribute.itemSize; 454 | colorAttribute.updateRange.count = this.count * colorAttribute.itemSize; 455 | sizeAttribute.updateRange.count = this.count * sizeAttribute.itemSize; 456 | lifeTimeAttribute.updateRange.count = this.count * lifeTimeAttribute.itemSize; 457 | 458 | } else { 459 | 460 | positionStartAttribute.updateRange.offset = 0; 461 | startTimeAttribute.updateRange.offset = 0; 462 | velocityAttribute.updateRange.offset = 0; 463 | turbulenceAttribute.updateRange.offset = 0; 464 | colorAttribute.updateRange.offset = 0; 465 | sizeAttribute.updateRange.offset = 0; 466 | lifeTimeAttribute.updateRange.offset = 0; 467 | 468 | // Use -1 to update the entire buffer, see #11476 469 | positionStartAttribute.updateRange.count = - 1; 470 | startTimeAttribute.updateRange.count = - 1; 471 | velocityAttribute.updateRange.count = - 1; 472 | turbulenceAttribute.updateRange.count = - 1; 473 | colorAttribute.updateRange.count = - 1; 474 | sizeAttribute.updateRange.count = - 1; 475 | lifeTimeAttribute.updateRange.count = - 1; 476 | 477 | } 478 | 479 | positionStartAttribute.needsUpdate = true; 480 | startTimeAttribute.needsUpdate = true; 481 | velocityAttribute.needsUpdate = true; 482 | turbulenceAttribute.needsUpdate = true; 483 | colorAttribute.needsUpdate = true; 484 | sizeAttribute.needsUpdate = true; 485 | lifeTimeAttribute.needsUpdate = true; 486 | 487 | this.offset = 0; 488 | this.count = 0; 489 | 490 | } 491 | 492 | }; 493 | 494 | this.dispose = function () { 495 | 496 | this.particleShaderGeo.dispose(); 497 | 498 | }; 499 | 500 | this.init(); 501 | 502 | }; 503 | 504 | GPUParticleContainer.prototype = Object.create( THREE.Object3D.prototype ); 505 | GPUParticleContainer.prototype.constructor = GPUParticleContainer; 506 | 507 | export default GPUParticleSystem; -------------------------------------------------------------------------------- /src/game/graphics/passes/clear-mask-pass.js: -------------------------------------------------------------------------------- 1 | import Pass from "./pass"; 2 | 3 | /** 4 | * @author alteredq / http://alteredqualia.com/ 5 | */ 6 | 7 | const ClearMaskPass = function () { 8 | 9 | Pass.call( this ); 10 | 11 | this.needsSwap = false; 12 | 13 | }; 14 | 15 | ClearMaskPass.prototype = Object.create( Pass.prototype ); 16 | 17 | Object.assign( ClearMaskPass.prototype, { 18 | 19 | render: function ( renderer, writeBuffer, readBuffer, delta, maskActive ) { 20 | 21 | renderer.state.buffers.stencil.setTest( false ); 22 | 23 | } 24 | 25 | } ); 26 | 27 | export default ClearMaskPass; -------------------------------------------------------------------------------- /src/game/graphics/passes/mask-pass.js: -------------------------------------------------------------------------------- 1 | import Pass from "./pass"; 2 | 3 | /** 4 | * @author alteredq / http://alteredqualia.com/ 5 | */ 6 | 7 | const MaskPass = function ( scene, camera ) { 8 | 9 | Pass.call( this ); 10 | 11 | this.scene = scene; 12 | this.camera = camera; 13 | 14 | this.clear = true; 15 | this.needsSwap = false; 16 | 17 | this.inverse = false; 18 | 19 | }; 20 | 21 | MaskPass.prototype = Object.assign( Object.create( Pass.prototype ), { 22 | 23 | constructor: MaskPass, 24 | 25 | render: function ( renderer, writeBuffer, readBuffer, delta, maskActive ) { 26 | 27 | var context = renderer.context; 28 | var state = renderer.state; 29 | 30 | // don't update color or depth 31 | 32 | state.buffers.color.setMask( false ); 33 | state.buffers.depth.setMask( false ); 34 | 35 | // lock buffers 36 | 37 | state.buffers.color.setLocked( true ); 38 | state.buffers.depth.setLocked( true ); 39 | 40 | // set up stencil 41 | 42 | var writeValue, clearValue; 43 | 44 | if ( this.inverse ) { 45 | 46 | writeValue = 0; 47 | clearValue = 1; 48 | 49 | } else { 50 | 51 | writeValue = 1; 52 | clearValue = 0; 53 | 54 | } 55 | 56 | state.buffers.stencil.setTest( true ); 57 | state.buffers.stencil.setOp( context.REPLACE, context.REPLACE, context.REPLACE ); 58 | state.buffers.stencil.setFunc( context.ALWAYS, writeValue, 0xffffffff ); 59 | state.buffers.stencil.setClear( clearValue ); 60 | 61 | // draw into the stencil buffer 62 | 63 | renderer.render( this.scene, this.camera, readBuffer, this.clear ); 64 | renderer.render( this.scene, this.camera, writeBuffer, this.clear ); 65 | 66 | // unlock color and depth buffer for subsequent rendering 67 | 68 | state.buffers.color.setLocked( false ); 69 | state.buffers.depth.setLocked( false ); 70 | 71 | // only render where stencil is set to 1 72 | 73 | state.buffers.stencil.setFunc( context.EQUAL, 1, 0xffffffff ); // draw if == 1 74 | state.buffers.stencil.setOp( context.KEEP, context.KEEP, context.KEEP ); 75 | 76 | } 77 | 78 | } ); 79 | 80 | export default MaskPass; -------------------------------------------------------------------------------- /src/game/graphics/passes/pass.js: -------------------------------------------------------------------------------- 1 | import { OrthographicCamera, PlaneBufferGeometry, Mesh } from "three"; 2 | 3 | const Pass = function () { 4 | 5 | // if set to true, the pass is processed by the composer 6 | this.enabled = true; 7 | 8 | // if set to true, the pass indicates to swap read and write buffer after rendering 9 | this.needsSwap = true; 10 | 11 | // if set to true, the pass clears its buffer before rendering 12 | this.clear = false; 13 | 14 | // if set to true, the result of the pass is rendered to screen 15 | this.renderToScreen = false; 16 | 17 | }; 18 | 19 | Object.assign( Pass.prototype, { 20 | 21 | setSize: function ( width, height ) {}, 22 | 23 | render: function ( renderer, writeBuffer, readBuffer, delta, maskActive ) { 24 | 25 | console.error( 'THREE.Pass: .render() must be implemented in derived pass.' ); 26 | 27 | } 28 | 29 | } ); 30 | 31 | // Helper for passes that need to fill the viewport with a single quad. 32 | 33 | Pass.FullScreenQuad = ( function () { 34 | 35 | var camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 ); 36 | var geometry = new PlaneBufferGeometry( 2, 2 ); 37 | 38 | var FullScreenQuad = function ( material ) { 39 | 40 | this._mesh = new Mesh( geometry, material ); 41 | 42 | }; 43 | 44 | Object.defineProperty( FullScreenQuad.prototype, 'material', { 45 | 46 | get: function () { 47 | 48 | return this._mesh.material; 49 | 50 | }, 51 | 52 | set: function ( value ) { 53 | 54 | this._mesh.material = value; 55 | 56 | } 57 | 58 | } ); 59 | 60 | Object.assign( FullScreenQuad.prototype, { 61 | 62 | render: function ( renderer ) { 63 | 64 | renderer.render( this._mesh, camera ); 65 | 66 | } 67 | 68 | } ); 69 | 70 | return FullScreenQuad; 71 | 72 | } )(); 73 | 74 | export default Pass; -------------------------------------------------------------------------------- /src/game/graphics/passes/render-pass.js: -------------------------------------------------------------------------------- 1 | import Pass from "./pass"; 2 | 3 | /** 4 | * @author alteredq / http://alteredqualia.com/ 5 | */ 6 | 7 | const RenderPass = function ( scene, camera, overrideMaterial, clearColor, clearAlpha ) { 8 | 9 | Pass.call( this ); 10 | 11 | this.scene = scene; 12 | this.camera = camera; 13 | 14 | this.overrideMaterial = overrideMaterial; 15 | 16 | this.clearColor = clearColor; 17 | this.clearAlpha = ( clearAlpha !== undefined ) ? clearAlpha : 0; 18 | 19 | this.clear = true; 20 | this.clearDepth = false; 21 | this.needsSwap = false; 22 | 23 | }; 24 | 25 | RenderPass.prototype = Object.assign( Object.create( Pass.prototype ), { 26 | 27 | constructor: RenderPass, 28 | 29 | render: function ( renderer, writeBuffer, readBuffer, deltaTime, maskActive ) { 30 | 31 | var oldAutoClear = renderer.autoClear; 32 | renderer.autoClear = false; 33 | 34 | this.scene.overrideMaterial = this.overrideMaterial; 35 | 36 | var oldClearColor, oldClearAlpha; 37 | 38 | if ( this.clearColor ) { 39 | 40 | oldClearColor = renderer.getClearColor().getHex(); 41 | oldClearAlpha = renderer.getClearAlpha(); 42 | 43 | renderer.setClearColor( this.clearColor, this.clearAlpha ); 44 | 45 | } 46 | 47 | if ( this.clearDepth ) { 48 | 49 | renderer.clearDepth(); 50 | 51 | } 52 | 53 | renderer.setRenderTarget( this.renderToScreen ? null : readBuffer ); 54 | 55 | // TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600 56 | if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil ); 57 | renderer.render( this.scene, this.camera ); 58 | 59 | if ( this.clearColor ) { 60 | 61 | renderer.setClearColor( oldClearColor, oldClearAlpha ); 62 | 63 | } 64 | 65 | this.scene.overrideMaterial = null; 66 | renderer.autoClear = oldAutoClear; 67 | 68 | } 69 | 70 | } ); 71 | 72 | export default RenderPass; -------------------------------------------------------------------------------- /src/game/graphics/passes/shader-pass.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import Pass from "./pass"; 3 | 4 | /** 5 | * @author alteredq / http://alteredqualia.com/ 6 | */ 7 | 8 | const ShaderPass = function ( shader, textureID ) { 9 | 10 | Pass.call( this ); 11 | 12 | this.textureID = ( textureID !== undefined ) ? textureID : "tDiffuse"; 13 | 14 | if ( shader instanceof THREE.ShaderMaterial ) { 15 | 16 | this.uniforms = shader.uniforms; 17 | 18 | this.material = shader; 19 | 20 | } else if ( shader ) { 21 | 22 | this.uniforms = THREE.UniformsUtils.clone( shader.uniforms ); 23 | 24 | this.material = new THREE.ShaderMaterial( { 25 | 26 | defines: Object.assign( {}, shader.defines ), 27 | uniforms: this.uniforms, 28 | vertexShader: shader.vertexShader, 29 | fragmentShader: shader.fragmentShader 30 | 31 | } ); 32 | 33 | } 34 | 35 | this.camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 ); 36 | this.scene = new THREE.Scene(); 37 | 38 | this.quad = new THREE.Mesh( new THREE.PlaneBufferGeometry( 2, 2 ), null ); 39 | this.quad.frustumCulled = false; // Avoid getting clipped 40 | this.scene.add( this.quad ); 41 | 42 | }; 43 | 44 | ShaderPass.prototype = Object.assign( Object.create( Pass.prototype ), { 45 | 46 | constructor: ShaderPass, 47 | 48 | render: function ( renderer, writeBuffer, readBuffer, deltaTime, maskActive ) { 49 | 50 | if ( this.uniforms[ this.textureID ] ) { 51 | 52 | this.uniforms[ this.textureID ].value = readBuffer.texture; 53 | 54 | } 55 | 56 | this.quad.material = this.material; 57 | 58 | if ( this.renderToScreen ) { 59 | 60 | renderer.setRenderTarget( null ); 61 | renderer.render( this.scene, this.camera ); 62 | 63 | } else { 64 | 65 | renderer.setRenderTarget( writeBuffer ); 66 | // TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600 67 | if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil ); 68 | renderer.render( this.scene, this.camera ); 69 | 70 | } 71 | 72 | } 73 | 74 | } ); 75 | 76 | export default ShaderPass; -------------------------------------------------------------------------------- /src/game/graphics/passes/unreal-bloom-pass.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author spidersharma / http://eduperiment.com/ 3 | * 4 | * Inspired from Unreal Engine 5 | * https://docs.unrealengine.com/latest/INT/Engine/Rendering/PostProcessEffects/Bloom/ 6 | */ 7 | 8 | import { 9 | AdditiveBlending, 10 | Color, 11 | LinearFilter, 12 | MeshBasicMaterial, 13 | RGBAFormat, 14 | ShaderMaterial, 15 | UniformsUtils, 16 | Vector2, 17 | Vector3, 18 | WebGLRenderTarget 19 | } from "three"; 20 | import Pass from "./pass"; 21 | import CopyShader from "../shaders/copy-shader"; 22 | import LuminosityHighPassShader from "../shaders/luminosity-high-pass-shader"; 23 | import { screen } from "../../utils"; 24 | 25 | const UnrealBloomPass = function( 26 | resolution = new Vector2(screen.width, screen.height), 27 | strength = 2, 28 | radius = 0.75, 29 | threshold = 0 30 | ) { 31 | Pass.call(this); 32 | 33 | this.strength = strength !== undefined ? strength : 1; 34 | this.radius = radius; 35 | this.threshold = threshold; 36 | this.resolution = 37 | resolution !== undefined 38 | ? new Vector2(resolution.x, resolution.y) 39 | : new Vector2(256, 256); 40 | 41 | // create color only once here, reuse it later inside the render function 42 | this.clearColor = new Color(0, 0, 0); 43 | 44 | // render targets 45 | var pars = { 46 | minFilter: LinearFilter, 47 | magFilter: LinearFilter, 48 | format: RGBAFormat 49 | }; 50 | this.renderTargetsHorizontal = []; 51 | this.renderTargetsVertical = []; 52 | this.nMips = 5; 53 | var resx = Math.round(this.resolution.x / 2); 54 | var resy = Math.round(this.resolution.y / 2); 55 | 56 | this.renderTargetBright = new WebGLRenderTarget(resx, resy, pars); 57 | this.renderTargetBright.texture.name = "UnrealBloomPass.bright"; 58 | this.renderTargetBright.texture.generateMipmaps = false; 59 | 60 | for (var i = 0; i < this.nMips; i++) { 61 | var renderTargetHorizonal = new WebGLRenderTarget(resx, resy, pars); 62 | 63 | renderTargetHorizonal.texture.name = "UnrealBloomPass.h" + i; 64 | renderTargetHorizonal.texture.generateMipmaps = false; 65 | 66 | this.renderTargetsHorizontal.push(renderTargetHorizonal); 67 | 68 | var renderTargetVertical = new WebGLRenderTarget(resx, resy, pars); 69 | 70 | renderTargetVertical.texture.name = "UnrealBloomPass.v" + i; 71 | renderTargetVertical.texture.generateMipmaps = false; 72 | 73 | this.renderTargetsVertical.push(renderTargetVertical); 74 | 75 | resx = Math.round(resx / 2); 76 | 77 | resy = Math.round(resy / 2); 78 | } 79 | 80 | // luminosity high pass material 81 | 82 | if (LuminosityHighPassShader === undefined) 83 | console.error("UnrealBloomPass relies on LuminosityHighPassShader"); 84 | 85 | var highPassShader = LuminosityHighPassShader; 86 | this.highPassUniforms = UniformsUtils.clone(highPassShader.uniforms); 87 | 88 | this.highPassUniforms["luminosityThreshold"].value = threshold; 89 | this.highPassUniforms["smoothWidth"].value = 0.01; 90 | 91 | this.materialHighPassFilter = new ShaderMaterial({ 92 | uniforms: this.highPassUniforms, 93 | vertexShader: highPassShader.vertexShader, 94 | fragmentShader: highPassShader.fragmentShader, 95 | defines: {} 96 | }); 97 | 98 | // Gaussian Blur Materials 99 | this.separableBlurMaterials = []; 100 | var kernelSizeArray = [3, 5, 7, 9, 11]; 101 | resx = Math.round(this.resolution.x / 2); 102 | resy = Math.round(this.resolution.y / 2); 103 | 104 | for (let i = 0; i < this.nMips; i++) { 105 | this.separableBlurMaterials.push( 106 | this.getSeperableBlurMaterial(kernelSizeArray[i]) 107 | ); 108 | 109 | this.separableBlurMaterials[i].uniforms["texSize"].value = new Vector2( 110 | resx, 111 | resy 112 | ); 113 | 114 | resx = Math.round(resx / 2); 115 | 116 | resy = Math.round(resy / 2); 117 | } 118 | 119 | // Composite material 120 | this.compositeMaterial = this.getCompositeMaterial(this.nMips); 121 | this.compositeMaterial.uniforms[ 122 | "blurTexture1" 123 | ].value = this.renderTargetsVertical[0].texture; 124 | this.compositeMaterial.uniforms[ 125 | "blurTexture2" 126 | ].value = this.renderTargetsVertical[1].texture; 127 | this.compositeMaterial.uniforms[ 128 | "blurTexture3" 129 | ].value = this.renderTargetsVertical[2].texture; 130 | this.compositeMaterial.uniforms[ 131 | "blurTexture4" 132 | ].value = this.renderTargetsVertical[3].texture; 133 | this.compositeMaterial.uniforms[ 134 | "blurTexture5" 135 | ].value = this.renderTargetsVertical[4].texture; 136 | this.compositeMaterial.uniforms["bloomStrength"].value = strength; 137 | this.compositeMaterial.uniforms["bloomRadius"].value = 0.1; 138 | this.compositeMaterial.needsUpdate = true; 139 | 140 | var bloomFactors = [1.0, 0.8, 0.6, 0.4, 0.2]; 141 | this.compositeMaterial.uniforms["bloomFactors"].value = bloomFactors; 142 | this.bloomTintColors = [ 143 | new Vector3(1, 1, 1), 144 | new Vector3(1, 1, 1), 145 | new Vector3(1, 1, 1), 146 | new Vector3(1, 1, 1), 147 | new Vector3(1, 1, 1) 148 | ]; 149 | this.compositeMaterial.uniforms[ 150 | "bloomTintColors" 151 | ].value = this.bloomTintColors; 152 | 153 | // copy material 154 | if (CopyShader === undefined) { 155 | console.error("UnrealBloomPass relies on CopyShader"); 156 | } 157 | 158 | var copyShader = CopyShader; 159 | 160 | this.copyUniforms = UniformsUtils.clone(copyShader.uniforms); 161 | this.copyUniforms["opacity"].value = 1.0; 162 | 163 | this.materialCopy = new ShaderMaterial({ 164 | uniforms: this.copyUniforms, 165 | vertexShader: copyShader.vertexShader, 166 | fragmentShader: copyShader.fragmentShader, 167 | blending: AdditiveBlending, 168 | depthTest: false, 169 | depthWrite: false, 170 | transparent: true 171 | }); 172 | 173 | this.enabled = true; 174 | this.needsSwap = false; 175 | 176 | this.oldClearColor = new Color(); 177 | this.oldClearAlpha = 1; 178 | 179 | this.basic = new MeshBasicMaterial(); 180 | 181 | this.fsQuad = new Pass.FullScreenQuad(null); 182 | }; 183 | 184 | UnrealBloomPass.prototype = Object.assign(Object.create(Pass.prototype), { 185 | constructor: UnrealBloomPass, 186 | 187 | dispose: function() { 188 | for (var i = 0; i < this.renderTargetsHorizontal.length; i++) { 189 | this.renderTargetsHorizontal[i].dispose(); 190 | } 191 | 192 | for (let i = 0; i < this.renderTargetsVertical.length; i++) { 193 | this.renderTargetsVertical[i].dispose(); 194 | } 195 | 196 | this.renderTargetBright.dispose(); 197 | }, 198 | 199 | setSize: function(width, height) { 200 | var resx = Math.round(width / 2); 201 | var resy = Math.round(height / 2); 202 | 203 | this.renderTargetBright.setSize(resx, resy); 204 | 205 | for (var i = 0; i < this.nMips; i++) { 206 | this.renderTargetsHorizontal[i].setSize(resx, resy); 207 | this.renderTargetsVertical[i].setSize(resx, resy); 208 | 209 | this.separableBlurMaterials[i].uniforms[ 210 | "texSize" 211 | ].value = new Vector2(resx, resy); 212 | 213 | resx = Math.round(resx / 2); 214 | resy = Math.round(resy / 2); 215 | } 216 | }, 217 | 218 | render: function(renderer, writeBuffer, readBuffer, deltaTime, maskActive) { 219 | this.oldClearColor.copy(renderer.getClearColor()); 220 | this.oldClearAlpha = renderer.getClearAlpha(); 221 | var oldAutoClear = renderer.autoClear; 222 | renderer.autoClear = false; 223 | 224 | renderer.setClearColor(this.clearColor, 0); 225 | 226 | if (maskActive) renderer.context.disable(renderer.context.STENCIL_TEST); 227 | 228 | // Render input to screen 229 | 230 | if (this.renderToScreen) { 231 | this.fsQuad.material = this.basic; 232 | this.basic.map = readBuffer.texture; 233 | 234 | renderer.setRenderTarget(null); 235 | renderer.clear(); 236 | this.fsQuad.render(renderer); 237 | } 238 | 239 | // 1. Extract Bright Areas 240 | 241 | this.highPassUniforms["tDiffuse"].value = readBuffer.texture; 242 | this.highPassUniforms["luminosityThreshold"].value = this.threshold; 243 | this.fsQuad.material = this.materialHighPassFilter; 244 | 245 | renderer.setRenderTarget(this.renderTargetBright); 246 | renderer.clear(); 247 | this.fsQuad.render(renderer); 248 | 249 | // 2. Blur All the mips progressively 250 | 251 | var inputRenderTarget = this.renderTargetBright; 252 | 253 | for (var i = 0; i < this.nMips; i++) { 254 | this.fsQuad.material = this.separableBlurMaterials[i]; 255 | 256 | this.separableBlurMaterials[i].uniforms["colorTexture"].value = 257 | inputRenderTarget.texture; 258 | this.separableBlurMaterials[i].uniforms["direction"].value = 259 | UnrealBloomPass.BlurDirectionX; 260 | renderer.setRenderTarget(this.renderTargetsHorizontal[i]); 261 | renderer.clear(); 262 | this.fsQuad.render(renderer); 263 | 264 | this.separableBlurMaterials[i].uniforms[ 265 | "colorTexture" 266 | ].value = this.renderTargetsHorizontal[i].texture; 267 | this.separableBlurMaterials[i].uniforms["direction"].value = 268 | UnrealBloomPass.BlurDirectionY; 269 | renderer.setRenderTarget(this.renderTargetsVertical[i]); 270 | renderer.clear(); 271 | this.fsQuad.render(renderer); 272 | 273 | inputRenderTarget = this.renderTargetsVertical[i]; 274 | } 275 | 276 | // Composite All the mips 277 | 278 | this.fsQuad.material = this.compositeMaterial; 279 | this.compositeMaterial.uniforms["bloomStrength"].value = this.strength; 280 | this.compositeMaterial.uniforms["bloomRadius"].value = this.radius; 281 | this.compositeMaterial.uniforms[ 282 | "bloomTintColors" 283 | ].value = this.bloomTintColors; 284 | 285 | renderer.setRenderTarget(this.renderTargetsHorizontal[0]); 286 | renderer.clear(); 287 | this.fsQuad.render(renderer); 288 | 289 | // Blend it additively over the input texture 290 | 291 | this.fsQuad.material = this.materialCopy; 292 | this.copyUniforms[ 293 | "tDiffuse" 294 | ].value = this.renderTargetsHorizontal[0].texture; 295 | 296 | if (maskActive) renderer.context.enable(renderer.context.STENCIL_TEST); 297 | 298 | if (this.renderToScreen) { 299 | renderer.setRenderTarget(null); 300 | this.fsQuad.render(renderer); 301 | } else { 302 | renderer.setRenderTarget(readBuffer); 303 | this.fsQuad.render(renderer); 304 | } 305 | 306 | // Restore renderer settings 307 | 308 | renderer.setClearColor(this.oldClearColor, this.oldClearAlpha); 309 | renderer.autoClear = oldAutoClear; 310 | }, 311 | 312 | getSeperableBlurMaterial: function(kernelRadius) { 313 | return new ShaderMaterial({ 314 | defines: { 315 | KERNEL_RADIUS: kernelRadius, 316 | SIGMA: kernelRadius 317 | }, 318 | 319 | uniforms: { 320 | colorTexture: { value: null }, 321 | texSize: { value: new Vector2(0.5, 0.5) }, 322 | direction: { value: new Vector2(0.5, 0.5) } 323 | }, 324 | 325 | vertexShader: `varying vec2 vUv; 326 | void main() { 327 | vUv = uv; 328 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 329 | }`, 330 | 331 | fragmentShader: `#include 332 | varying vec2 vUv; 333 | uniform sampler2D colorTexture; 334 | uniform vec2 texSize; 335 | uniform vec2 direction; 336 | 337 | float gaussianPdf(in float x, in float sigma) { 338 | return 0.39894 * exp( -0.5 * x * x/( sigma * sigma))/sigma; 339 | } 340 | void main() { 341 | vec2 invSize = 1.0 / texSize; 342 | float fSigma = float(SIGMA); 343 | float weightSum = gaussianPdf(0.0, fSigma); 344 | vec3 diffuseSum = texture2D( colorTexture, vUv).rgb * weightSum; 345 | for( int i = 1; i < KERNEL_RADIUS; i ++ ) { 346 | float x = float(i); 347 | float w = gaussianPdf(x, fSigma); 348 | vec2 uvOffset = direction * invSize * x; 349 | vec3 sample1 = texture2D( colorTexture, vUv + uvOffset).rgb; 350 | vec3 sample2 = texture2D( colorTexture, vUv - uvOffset).rgb; 351 | diffuseSum += (sample1 + sample2) * w; 352 | weightSum += 2.0 * w; 353 | } 354 | gl_FragColor = vec4(diffuseSum/weightSum, 1.0); 355 | }` 356 | }); 357 | }, 358 | 359 | getCompositeMaterial: function(nMips) { 360 | return new ShaderMaterial({ 361 | defines: { 362 | NUM_MIPS: nMips 363 | }, 364 | 365 | uniforms: { 366 | blurTexture1: { value: null }, 367 | blurTexture2: { value: null }, 368 | blurTexture3: { value: null }, 369 | blurTexture4: { value: null }, 370 | blurTexture5: { value: null }, 371 | dirtTexture: { value: null }, 372 | bloomStrength: { value: 1.0 }, 373 | bloomFactors: { value: null }, 374 | bloomTintColors: { value: null }, 375 | bloomRadius: { value: 0.0 } 376 | }, 377 | 378 | vertexShader: `varying vec2 vUv; 379 | void main() { 380 | vUv = uv; 381 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 382 | }`, 383 | 384 | fragmentShader: `varying vec2 vUv; 385 | uniform sampler2D blurTexture1; 386 | uniform sampler2D blurTexture2; 387 | uniform sampler2D blurTexture3; 388 | uniform sampler2D blurTexture4; 389 | uniform sampler2D blurTexture5; 390 | uniform sampler2D dirtTexture; 391 | uniform float bloomStrength; 392 | uniform float bloomRadius; 393 | uniform float bloomFactors[NUM_MIPS]; 394 | uniform vec3 bloomTintColors[NUM_MIPS]; 395 | 396 | float lerpBloomFactor(const in float factor) { 397 | float mirrorFactor = 1.2 - factor; 398 | return mix(factor, mirrorFactor, bloomRadius); 399 | } 400 | 401 | void main() { 402 | gl_FragColor = bloomStrength * ( lerpBloomFactor(bloomFactors[0]) * vec4(bloomTintColors[0], 1.0) * texture2D(blurTexture1, vUv) + 403 | lerpBloomFactor(bloomFactors[1]) * vec4(bloomTintColors[1], 1.0) * texture2D(blurTexture2, vUv) + 404 | lerpBloomFactor(bloomFactors[2]) * vec4(bloomTintColors[2], 1.0) * texture2D(blurTexture3, vUv) + 405 | lerpBloomFactor(bloomFactors[3]) * vec4(bloomTintColors[3], 1.0) * texture2D(blurTexture4, vUv) + 406 | lerpBloomFactor(bloomFactors[4]) * vec4(bloomTintColors[4], 1.0) * texture2D(blurTexture5, vUv) ); 407 | }` 408 | }); 409 | } 410 | }); 411 | 412 | UnrealBloomPass.BlurDirectionX = new Vector2(1.0, 0.0); 413 | UnrealBloomPass.BlurDirectionY = new Vector2(0.0, 1.0); 414 | 415 | export default UnrealBloomPass; 416 | -------------------------------------------------------------------------------- /src/game/graphics/renderer.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import * as THREE from 'three'; 3 | import EffectComposer from "./effect-composer"; 4 | import RenderPass from "./passes/render-pass"; 5 | import _ from "lodash"; 6 | 7 | //-- https://medium.com/@colesayershapiro/using-three-js-in-react-6cb71e87bdf4 8 | //-- https://medium.com/@summerdeehan/a-beginners-guide-to-using-three-js-react-and-webgl-to-build-a-3d-application-with-interaction-5d7b2c7ca89a 9 | //-- https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Getting_started_with_WebGL 10 | 11 | class ThreeView extends PureComponent { 12 | 13 | componentDidMount() { 14 | const container = this.refs.container; 15 | const width = container.clientWidth; 16 | const height = container.clientHeight; 17 | const dpr = window.devicePixelRatio; 18 | 19 | this.props.camera.resize(width, height, dpr); 20 | this.renderer = new THREE.WebGLRenderer({ }); 21 | this.renderer.setPixelRatio(dpr); 22 | this.renderer.setSize(width, height); 23 | this.renderer.setClearColor(0x020202, 1.0); 24 | this.composer = new EffectComposer(this.renderer); 25 | 26 | const passes = [ 27 | new RenderPass(this.props.scene, this.props.camera), 28 | ...this.props.passes 29 | ] 30 | 31 | passes.forEach(p => this.composer.addPass(p)) 32 | passes[passes.length-1].renderToScreen = true; 33 | 34 | window.addEventListener("resize", this.onResize); 35 | 36 | container.appendChild(this.renderer.domElement) 37 | } 38 | 39 | componentWillUnmount() { 40 | window.removeEventListener("resize", this.onResize); 41 | 42 | const container = this.refs.container; 43 | container.removeChild(this.renderer.domElement) 44 | } 45 | 46 | onResize = () => { 47 | const container = this.refs.container; 48 | const width = container.clientWidth; 49 | const height = container.clientHeight; 50 | const dpr = window.devicePixelRatio; 51 | 52 | this.props.camera.resize(width, height, dpr); 53 | this.renderer.setPixelRatio(dpr); 54 | this.renderer.setSize(width, height); 55 | }; 56 | 57 | render() { 58 | if (this.composer) { 59 | this.composer.render(); 60 | } 61 | 62 | return ( 63 |
64 | ); 65 | } 66 | } 67 | 68 | const css = { 69 | container: { 70 | height: "100vh", 71 | width: "100vw", 72 | overflow: "hidden" 73 | } 74 | } 75 | 76 | const renderHUD = (entities, window) => { 77 | if (!entities.hud) return null; 78 | 79 | const hud = entities.hud; 80 | 81 | if (typeof hud.renderer === "object") 82 | return ; 83 | else if (typeof hud.renderer === "function") 84 | return ; 85 | }; 86 | 87 | const ThreeJSRenderer = (...passes) => (entities, window) => { 88 | if (!entities) return null; 89 | return [ 90 | , 96 | renderHUD(entities, window) 97 | ]; 98 | }; 99 | 100 | export default ThreeJSRenderer; 101 | -------------------------------------------------------------------------------- /src/game/graphics/shaders/copy-shader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author alteredq / http://alteredqualia.com/ 3 | * 4 | * Full-screen textured quad shader 5 | */ 6 | 7 | const CopyShader = { 8 | 9 | uniforms: { 10 | 11 | "tDiffuse": { value: null }, 12 | "opacity": { value: 1.0 } 13 | 14 | }, 15 | 16 | vertexShader: [ 17 | 18 | "varying vec2 vUv;", 19 | 20 | "void main() {", 21 | 22 | "vUv = uv;", 23 | "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", 24 | 25 | "}" 26 | 27 | ].join( "\n" ), 28 | 29 | fragmentShader: [ 30 | 31 | "uniform float opacity;", 32 | 33 | "uniform sampler2D tDiffuse;", 34 | 35 | "varying vec2 vUv;", 36 | 37 | "void main() {", 38 | 39 | "vec4 texel = texture2D( tDiffuse, vUv );", 40 | "gl_FragColor = opacity * texel;", 41 | 42 | "}" 43 | 44 | ].join( "\n" ) 45 | 46 | }; 47 | 48 | export default CopyShader; -------------------------------------------------------------------------------- /src/game/graphics/shaders/luminosity-high-pass-shader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author bhouston / http://clara.io/ 3 | * 4 | * Luminosity 5 | * http://en.wikipedia.org/wiki/Luminosity 6 | */ 7 | 8 | import { 9 | Color 10 | } from "three"; 11 | 12 | const LuminosityHighPassShader = { 13 | 14 | shaderID: "luminosityHighPass", 15 | 16 | uniforms: { 17 | 18 | "tDiffuse": { value: null }, 19 | "luminosityThreshold": { value: 1.0 }, 20 | "smoothWidth": { value: 1.0 }, 21 | "defaultColor": { value: new Color( 0x000000 ) }, 22 | "defaultOpacity": { value: 0.0 } 23 | 24 | }, 25 | 26 | vertexShader: [ 27 | 28 | "varying vec2 vUv;", 29 | 30 | "void main() {", 31 | 32 | "vUv = uv;", 33 | 34 | "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", 35 | 36 | "}" 37 | 38 | ].join("\n"), 39 | 40 | fragmentShader: [ 41 | 42 | "uniform sampler2D tDiffuse;", 43 | "uniform vec3 defaultColor;", 44 | "uniform float defaultOpacity;", 45 | "uniform float luminosityThreshold;", 46 | "uniform float smoothWidth;", 47 | 48 | "varying vec2 vUv;", 49 | 50 | "void main() {", 51 | 52 | "vec4 texel = texture2D( tDiffuse, vUv );", 53 | 54 | "vec3 luma = vec3( 0.299, 0.587, 0.114 );", 55 | 56 | "float v = dot( texel.xyz, luma );", 57 | 58 | "vec4 outputColor = vec4( defaultColor.rgb, defaultOpacity );", 59 | 60 | "float alpha = smoothstep( luminosityThreshold, luminosityThreshold + smoothWidth, v );", 61 | 62 | "gl_FragColor = mix( outputColor, texel, alpha );", 63 | 64 | "}" 65 | 66 | ].join("\n") 67 | 68 | }; 69 | 70 | export default LuminosityHighPassShader; -------------------------------------------------------------------------------- /src/game/graphics/shaders/pixel-shader.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | 3 | export default ({ 4 | pixelSize = 5, 5 | borderSize = 1, 6 | lightenFactor = 1.8, 7 | softenFactor = 0.75, 8 | darkenFactor = 0.5, 9 | resolution = new THREE.Vector2(window.innerWidth, window.innerHeight) 10 | } = {}) => { 11 | const pixelShader = { 12 | uniforms: { 13 | tDiffuse: { value: null }, 14 | pixelSize: { value: pixelSize }, 15 | borderFraction: { value: borderSize / pixelSize }, 16 | lightenFactor: { value: lightenFactor }, 17 | softenFactor: { value: softenFactor }, 18 | darkenFactor: { value: darkenFactor }, 19 | resolution: { value: resolution } 20 | }, 21 | 22 | vertexShader: ` 23 | varying highp vec2 vUv; 24 | 25 | void main() { 26 | vUv = uv; 27 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 28 | }`, 29 | 30 | fragmentShader: ` 31 | uniform sampler2D tDiffuse; 32 | uniform float pixelSize; 33 | uniform float borderFraction; 34 | uniform float lightenFactor; 35 | uniform float softenFactor; 36 | uniform float darkenFactor; 37 | uniform vec2 resolution; 38 | 39 | varying highp vec2 vUv; 40 | 41 | void main(){ 42 | vec2 dxy = pixelSize / resolution; 43 | vec2 pixel = vUv / dxy; 44 | vec2 fraction = fract(pixel); 45 | vec2 coord = dxy * floor(pixel); 46 | vec3 color = texture2D(tDiffuse, coord).xyz; 47 | 48 | if (fraction.y > (1.0 - borderFraction)) 49 | color = color * lightenFactor; 50 | 51 | if (fraction.x < borderFraction) 52 | color = color * softenFactor; 53 | 54 | if (fraction.y < borderFraction) 55 | color = color * darkenFactor; 56 | 57 | gl_FragColor = vec4(color, 1); 58 | }` 59 | }; 60 | 61 | return pixelShader; 62 | }; 63 | -------------------------------------------------------------------------------- /src/game/graphics/shaders/scanline-shader.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | 3 | export default (thickness = 5.0, color = new THREE.Vector4(0, 0, 0, 1)) => { 4 | const scanlineShader = { 5 | uniforms: { 6 | tDiffuse: { value: null }, 7 | thickness: { value: thickness }, 8 | color: { value: color } 9 | }, 10 | 11 | vertexShader: ` 12 | varying vec2 vUv; 13 | void main() { 14 | vUv = uv; 15 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 16 | } 17 | `, 18 | 19 | fragmentShader: ` 20 | const vec4 blank = vec4(0.0, 0.0, 0.0, 1.0); 21 | uniform sampler2D tDiffuse; 22 | uniform float thickness; 23 | uniform vec4 color; 24 | varying vec2 vUv; 25 | void main() { 26 | float result = floor(mod(gl_FragCoord.y, thickness)); 27 | gl_FragColor = result == 0.0 ? texture2D(tDiffuse, vUv) : color; 28 | } 29 | ` 30 | }; 31 | 32 | return scanlineShader; 33 | }; 34 | -------------------------------------------------------------------------------- /src/game/graphics/shaders/sepia-shader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author alteredq / http://alteredqualia.com/ 3 | * 4 | * Sepia tone shader 5 | * based on glfx.js sepia shader 6 | * https://github.com/evanw/glfx.js 7 | */ 8 | 9 | export default (amount = 1.0) => { 10 | const sepiaShader = { 11 | uniforms: { 12 | tDiffuse: { value: null }, 13 | amount: { value: amount } 14 | }, 15 | 16 | vertexShader: [ 17 | "varying vec2 vUv;", 18 | 19 | "void main() {", 20 | 21 | "vUv = uv;", 22 | "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", 23 | 24 | "}" 25 | ].join("\n"), 26 | 27 | fragmentShader: [ 28 | "uniform float amount;", 29 | 30 | "uniform sampler2D tDiffuse;", 31 | 32 | "varying vec2 vUv;", 33 | 34 | "void main() {", 35 | 36 | "vec4 color = texture2D( tDiffuse, vUv );", 37 | "vec3 c = color.rgb;", 38 | 39 | "color.r = dot( c, vec3( 1.0 - 0.607 * amount, 0.769 * amount, 0.189 * amount ) );", 40 | "color.g = dot( c, vec3( 0.349 * amount, 1.0 - 0.314 * amount, 0.168 * amount ) );", 41 | "color.b = dot( c, vec3( 0.272 * amount, 0.534 * amount, 1.0 - 0.869 * amount ) );", 42 | 43 | "gl_FragColor = vec4( min( vec3( 1.0 ), color.rgb ), color.a );", 44 | 45 | "}" 46 | ].join("\n") 47 | }; 48 | 49 | return sepiaShader; 50 | }; 51 | -------------------------------------------------------------------------------- /src/game/graphics/shaders/tri-color-shader.js: -------------------------------------------------------------------------------- 1 | import { Color, Vector4 } from "three"; 2 | 3 | export default ({ distance = 0.005, threshold = 0, colors = [new Color(0xFFFFFF), new Color(0x362928), new Color(0xFF3526)] } = {}) => { 4 | const triColorShader = { 5 | uniforms: { 6 | tDiffuse: { value: null }, 7 | distance: { value: distance }, 8 | threshold: { value: threshold }, 9 | colors: { 10 | value: colors.map(x => new Vector4(x.r, x.g, x.b, 1)) 11 | } 12 | }, 13 | 14 | vertexShader: ` 15 | varying vec2 vUv; 16 | void main() { 17 | vUv = uv; 18 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 19 | }`, 20 | 21 | fragmentShader: ` 22 | uniform sampler2D tDiffuse; 23 | uniform float distance; 24 | uniform float threshold; 25 | uniform vec4 colors[3]; 26 | varying vec2 vUv; 27 | 28 | void main() { 29 | vec4 tex = texture2D(tDiffuse, vUv); 30 | vec4 tex2 = texture2D(tDiffuse, vec2(vUv.x + distance, vUv.y)); 31 | 32 | float test = tex.r + tex.g + tex.b; 33 | float test2 = tex2.r + tex2.g + tex2.b; 34 | float diff = test2 - test; 35 | 36 | if(diff < -threshold) 37 | tex = colors[0]; 38 | else if (diff > threshold) 39 | tex = colors[1]; 40 | else 41 | tex = colors[2]; 42 | 43 | gl_FragColor = tex; 44 | }` 45 | }; 46 | 47 | return triColorShader; 48 | }; 49 | -------------------------------------------------------------------------------- /src/game/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { GameEngine } from "react-game-engine"; 3 | import Renderer from "./graphics/renderer"; 4 | import Systems from "./systems"; 5 | import Entities from "./entities"; 6 | 7 | import ShaderPass from "./graphics/passes/shader-pass"; 8 | import PixelShader from "./graphics/shaders/pixel-shader"; 9 | 10 | import "../index.css"; 11 | 12 | class Game extends React.Component { 13 | render() { 14 | return ( 15 | 23 | ); 24 | } 25 | } 26 | 27 | export default Game; 28 | -------------------------------------------------------------------------------- /src/game/systems/basic-physics.js: -------------------------------------------------------------------------------- 1 | import { all } from "../utils"; 2 | 3 | const Physics = entities => { 4 | const physicsEntities = all(entities, e => e.physics); 5 | 6 | physicsEntities.forEach(e => { 7 | const { 8 | mass, 9 | forces, 10 | acceleration, 11 | velocity, 12 | position, 13 | maxSpeed, 14 | damping 15 | } = e.physics; 16 | 17 | forces.divideScalar(mass); 18 | acceleration.add(forces); 19 | 20 | if (damping) velocity.multiplyScalar(1 - damping); 21 | 22 | velocity.add(acceleration); 23 | 24 | if (maxSpeed) velocity.clampLength(0, maxSpeed); 25 | 26 | position.add(velocity); 27 | 28 | forces.set(0, 0, 0); 29 | acceleration.set(0, 0, 0); 30 | }); 31 | 32 | return entities; 33 | }; 34 | 35 | export default Physics; 36 | -------------------------------------------------------------------------------- /src/game/systems/camera.js: -------------------------------------------------------------------------------- 1 | import { rotateAroundPoint } from "../utils/three"; 2 | 3 | const Camera = ({ 4 | yawSpeed = 0.01, 5 | pitchSpeed = 0.01, 6 | zoomSpeed = 0.02 7 | } = {}) => { 8 | return (entities, { keyboardController }) => { 9 | const camera = entities.camera; 10 | 11 | if (camera && keyboardController) { 12 | const { w, a, s, d, space, control } = keyboardController; 13 | 14 | //-- Yaw and pitch rotation 15 | if (w || a || s || d) { 16 | rotateAroundPoint(camera, camera.target, { 17 | y: (a ? 1 : d ? -1 : 0) * yawSpeed, 18 | x: (w ? 1 : s ? -1 : 0) * pitchSpeed 19 | }); 20 | camera.lookAt(camera.target); 21 | } 22 | 23 | //-- Zooming (pinching) 24 | if (space || control) { 25 | const zoomFactor = (space ? 1 : control ? -1 : 0) * zoomSpeed; 26 | 27 | camera.zoom += zoomFactor; 28 | camera.updateProjectionMatrix(); 29 | } 30 | } 31 | 32 | return entities; 33 | }; 34 | }; 35 | 36 | export default Camera; 37 | -------------------------------------------------------------------------------- /src/game/systems/collisions.js: -------------------------------------------------------------------------------- 1 | import { allKeys } from "../utils"; 2 | import { QuadTree, Box, Point, Circle } from "js-quadtree"; 3 | import _ from "lodash"; 4 | 5 | const createTree = (collideableKeys, entities, { x = -50, y = -50, width = 100, height = 100 } = {}) => { 6 | const tree = new QuadTree( 7 | new Box(x, y, width, height) 8 | ); 9 | 10 | for (let i = 0; i < collideableKeys.length; i++) { 11 | const key = collideableKeys[i]; 12 | const collideable = entities[key]; 13 | 14 | tree.insert( 15 | new Point( 16 | collideable.physics.position.x, 17 | collideable.physics.position.z, 18 | { entityId: key } 19 | ) 20 | ); 21 | } 22 | 23 | return tree; 24 | }; 25 | 26 | const queryTree = (tree, collideable) => { 27 | return tree.query( 28 | new Circle( 29 | collideable.physics.position.x, 30 | collideable.physics.position.z, 31 | collideable.collisions.sweepRadius 32 | ) 33 | ); 34 | }; 35 | 36 | const hitTests = [ 37 | ["isBox3", "isBox3", (b1, b2) => b1.intersectsBox(b2)], 38 | ["isBox3", "isSphere", (b1, b2) => b1.intersectsSphere(b2)], 39 | ["isBox3", "isPlane", (b1, b2) => b1.intersectsPlane(b2)], 40 | ["isSphere", "isBox3", (b1, b2) => b1.intersectsBox(b2)], 41 | ["isSphere", "isSphere", (b1, b2) => b1.intersectsSphere(b2)], 42 | ["isSphere", "isPlane", (b1, b2) => b1.intersectsPlane(b2)], 43 | ["isPlane", "isBox3", (b1, b2) => b1.intersectsBox(b2)], 44 | ["isPlane", "isSphere", (b1, b2) => b1.intersectsSphere(b2)] 45 | ]; 46 | 47 | const collided = (hitTest, bounds, otherBounds) => { 48 | //-- This could be extended to handle the case where bounds 49 | //-- and otherBounds are arrays (for complex models) 50 | return ( 51 | bounds[hitTest[0]] && 52 | otherBounds[hitTest[1]] && 53 | hitTest[2](bounds, otherBounds) 54 | ); 55 | }; 56 | 57 | const notify = (defer, key, otherKey, collideable, other, force) => { 58 | defer({ 59 | type: "collision", 60 | entities: [collideable, other], 61 | keys: [key, otherKey], 62 | force 63 | }); 64 | }; 65 | 66 | const Collisions = config => (entities, args) => { 67 | const collideableKeys = allKeys(entities, e => e.collisions && e.physics); 68 | 69 | if (collideableKeys.length) { 70 | //-- Populate tree 71 | 72 | const tree = createTree(collideableKeys, entities, config); 73 | 74 | //-- Query tree 75 | 76 | for (let i = 0; i < collideableKeys.length; i++) { 77 | const key = collideableKeys[i]; 78 | const entity = entities[key]; 79 | const entityCollisions = entity.collisions; 80 | 81 | //-- Continue if this entity is a hit target 82 | 83 | if (entityCollisions.predicate) { 84 | const results = queryTree(tree, entity); 85 | 86 | //-- Continue if another entity was found in the vicinity 87 | 88 | if (results.length > 1) { 89 | const bounds = _.isFunction(entityCollisions.bounds) 90 | ? entityCollisions.bounds() 91 | : entityCollisions.bounds; 92 | 93 | for (let j = 0; j < results.length; j++) { 94 | const otherKey = results[j].data.entityId; 95 | const other = entities[otherKey]; 96 | const otherCollisions = other.collisions; 97 | 98 | if (key === otherKey) continue; 99 | 100 | //-- Does the current entity care about the one he has collied with? 101 | 102 | if (entityCollisions.predicate(entity, other)) { 103 | const otherBounds = _.isFunction(otherCollisions.bounds) 104 | ? otherCollisions.bounds() 105 | : otherCollisions.bounds; 106 | 107 | for (let k = 0; k < hitTests.length; k++) { 108 | const test = hitTests[k]; 109 | 110 | //-- Check whether an actual collision occured using proper bounds 111 | 112 | if (collided(test, bounds, otherBounds)) { 113 | const force = entity.physics.velocity 114 | .clone() 115 | .multiplyScalar(entity.physics.mass) 116 | .add( 117 | other.physics.velocity 118 | .clone() 119 | .multiplyScalar(other.physics.mass) 120 | ); 121 | 122 | if (entityCollisions.hit) 123 | entityCollisions.hit(entity, other, force, args); 124 | 125 | if (entityCollisions.notify) 126 | notify(args.defer, key, otherKey, entity, other, force, args); 127 | 128 | break; 129 | } 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | } 137 | 138 | return entities; 139 | }; 140 | 141 | export default Collisions; 142 | -------------------------------------------------------------------------------- /src/game/systems/gamepad-controller.js: -------------------------------------------------------------------------------- 1 | const getGamepad = () => navigator.getGamepads()[0] || navigator.getGamepads()[1] || navigator.getGamepads()[2] || navigator.getGamepads()[3]; 2 | 3 | const vibrate = gp => { 4 | return effect => { 5 | if (gp && gp.vibrationActuator) 6 | gp.vibrationActuator.playEffect("dual-rumble", effect); 7 | }; 8 | }; 9 | 10 | const createGamepadButtonReader = (buttonIndices = []) => { 11 | return gp => { 12 | if (gp) { 13 | return buttonIndices.map(idx => gp.buttons[idx].pressed); 14 | } 15 | } 16 | }; 17 | 18 | const createGamepadButtonValueReader = (buttonIndices = [], threshold = 0.05) => { 19 | return gp => { 20 | if (gp) { 21 | return buttonIndices.map(idx => { 22 | const button = gp.buttons[idx]; 23 | 24 | return button.pressed && button.value > threshold ? button.value : 0; 25 | }); 26 | } 27 | } 28 | }; 29 | 30 | const createGamepadAxesReader = (axisIndices = [], mapper = x => x, threshold = 0.05) => { 31 | return gp => { 32 | if (gp) { 33 | return axisIndices.map(idx => { 34 | const val = gp.axes[idx]; 35 | 36 | return Math.abs(val) > threshold ? val : 0; 37 | }) 38 | } 39 | } 40 | }; 41 | 42 | const stick = (xIdx, yIdx) => { 43 | const reader = createGamepadAxesReader([xIdx, yIdx]); 44 | 45 | return gp => { 46 | const [x, y] = reader(gp) || [0, 0]; 47 | 48 | return { x, y, heading: (x + y) ? Math.atan2(y, x) : null }; 49 | } 50 | }; 51 | 52 | const button = (idx) => { 53 | const reader = createGamepadButtonReader([idx]); 54 | 55 | return gp => { 56 | const [val] = reader(gp) || [false]; 57 | 58 | return val; 59 | } 60 | }; 61 | 62 | const buttonValue = (idx) => { 63 | const reader = createGamepadButtonValueReader([idx]); 64 | 65 | return gp => { 66 | const [val] = reader(gp) || [0]; 67 | 68 | return val; 69 | } 70 | }; 71 | 72 | const leftStick = stick(0, 1); 73 | const rightStick = stick(2, 3); 74 | const button0 = button(0); 75 | const button1 = button(1); 76 | const leftTrigger = buttonValue(6); 77 | const rightTrigger = buttonValue(7); 78 | 79 | let previous = { }; 80 | 81 | const GamepadController = (Wrapped = x => x) => (entities, args) => { 82 | 83 | if (!args.gamepadController) { 84 | const gamepad = getGamepad(); 85 | 86 | const current = { 87 | leftStick: leftStick(gamepad), 88 | rightStick: rightStick(gamepad), 89 | button0: button0(gamepad), 90 | button1: button1(gamepad), 91 | leftTrigger: leftTrigger(gamepad), 92 | rightTrigger: rightTrigger(gamepad), 93 | vibrate: vibrate(gamepad) 94 | }; 95 | 96 | args.gamepadController = Object.assign({}, current, { previous }); 97 | 98 | previous = current; 99 | } 100 | 101 | return Wrapped(entities, args); 102 | }; 103 | 104 | export default GamepadController; -------------------------------------------------------------------------------- /src/game/systems/gravity.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { all } from "../utils"; 3 | 4 | const g = new THREE.Vector3(0, -0.08, 0); 5 | 6 | const Gravity = entities => { 7 | const gravityEntities = all(entities, e => e.gravity && e.physics); 8 | 9 | gravityEntities.forEach(e => { 10 | e.physics.forces.add(e.gravity.isVector3 ? e.gravity : g); 11 | }); 12 | 13 | return entities; 14 | }; 15 | 16 | export default Gravity; 17 | -------------------------------------------------------------------------------- /src/game/systems/hud.js: -------------------------------------------------------------------------------- 1 | const HUD = (entities, args) => { 2 | 3 | const hud = entities.hud; 4 | 5 | if (hud) { 6 | hud.gamepadController = args.gamepadController; 7 | hud.keyboardController = args.keyboardController; 8 | hud.mouseController = args.mouseController; 9 | } 10 | 11 | return entities; 12 | }; 13 | 14 | export default HUD; -------------------------------------------------------------------------------- /src/game/systems/index.js: -------------------------------------------------------------------------------- 1 | import Camera from "./camera"; 2 | import Particles from "./particles"; 3 | import Removal from "./removal"; 4 | import Rotation from "./rotation"; 5 | import Timeline from "./timeline"; 6 | import HUD from "./hud"; 7 | import GamepadController from "./gamepad-controller"; 8 | import KeyboardController from "./keyboard-controller"; 9 | import MouseController from "./mouse-controller"; 10 | import Physics from "./physics"; 11 | import Spawn from "./spawn"; 12 | 13 | export default [ 14 | GamepadController(), 15 | KeyboardController(), 16 | MouseController(), 17 | Camera({ pitchSpeed: -0.02, yawSpeed: 0.02 }), 18 | Particles, 19 | Removal, 20 | Rotation, 21 | Timeline, 22 | Spawn, 23 | Physics, 24 | HUD 25 | ]; 26 | -------------------------------------------------------------------------------- /src/game/systems/keyboard-controller.js: -------------------------------------------------------------------------------- 1 | const readKey = (input, keys, name) => input.find(x => x.name === name && keys.indexOf(x.payload.key) !== -1); 2 | 3 | const createKeyReader = keys => { 4 | let down = false; 5 | 6 | return input => { 7 | if (readKey(input, keys, "onKeyDown")) 8 | down = true; 9 | 10 | if (readKey(input, keys, "onKeyUp")) 11 | down = false; 12 | 13 | return down; 14 | } 15 | }; 16 | 17 | const w = createKeyReader(["w", "W", "ArrowUp"]); 18 | const a = createKeyReader(["a", "A", "ArrowLeft"]); 19 | const s = createKeyReader(["s", "S", "ArrowDown"]); 20 | const d = createKeyReader(["d", "D", "ArrowRight"]); 21 | const space = createKeyReader([" "]); 22 | const control = createKeyReader(["Control"]); 23 | 24 | let previous = { }; 25 | 26 | const KeyboardController = (Wrapped = x => x) => (entities, args) => { 27 | 28 | if (!args.keyboardController) { 29 | const input = args.input; 30 | 31 | const current = { 32 | w: w(input), 33 | a: a(input), 34 | s: s(input), 35 | d: d(input), 36 | space: space(input), 37 | control: control(input) 38 | }; 39 | 40 | args.keyboardController = Object.assign({}, current, { previous }); 41 | 42 | previous = current; 43 | } 44 | 45 | return Wrapped(entities, args); 46 | }; 47 | 48 | export default KeyboardController; -------------------------------------------------------------------------------- /src/game/systems/mouse-controller.js: -------------------------------------------------------------------------------- 1 | const readButton = (input, buttons, name) => input.find(x => x.name === name && buttons.indexOf(x.payload.button) !== -1); 2 | 3 | const createButtonReader = buttons => { 4 | let down = false; 5 | 6 | return input => { 7 | if (readButton(input, buttons, "onMouseDown")) 8 | down = true; 9 | 10 | if (readButton(input, buttons, "onMouseUp")) 11 | down = false; 12 | 13 | return down; 14 | } 15 | }; 16 | 17 | const createPositionReader = () => { 18 | let position = { x: 0, y: 0 }; 19 | 20 | return input => { 21 | const move = input.find(x => x.name === "onMouseMove"); 22 | 23 | if (move) { 24 | position = { 25 | x: move.payload.pageX, 26 | y: move.payload.pageY 27 | } 28 | } 29 | 30 | return position; 31 | } 32 | } 33 | 34 | const createWheelReader = () => { 35 | let value = 0; 36 | 37 | return input => { 38 | const wheel = input.find(x => x.name === "onWheel"); 39 | 40 | if (wheel) { 41 | if (wheel.payload.deltaY < 0) 42 | value--; 43 | else 44 | value++; 45 | } 46 | 47 | return value; 48 | } 49 | } 50 | 51 | const left = createButtonReader([0]); 52 | const middle = createButtonReader([1]); 53 | const right = createButtonReader([2]); 54 | const position = createPositionReader(); 55 | const wheel = createWheelReader(); 56 | 57 | let previous = { }; 58 | 59 | const MouseController = (Wrapped = x => x) => (entities, args) => { 60 | 61 | if (!args.mouseController) { 62 | const input = args.input; 63 | 64 | const current = { 65 | left: left(input), 66 | middle: middle(input), 67 | right: right(input), 68 | position: position(input), 69 | wheel: wheel(input) 70 | }; 71 | 72 | args.mouseController = Object.assign({}, current, { previous }); 73 | 74 | previous = current; 75 | } 76 | 77 | return Wrapped(entities, args); 78 | }; 79 | 80 | export default MouseController; -------------------------------------------------------------------------------- /src/game/systems/particles.js: -------------------------------------------------------------------------------- 1 | import { all } from "../utils"; 2 | 3 | const Particles = (entities, args) => { 4 | const { time } = args; 5 | const entitiesWithParticles = all(entities, e => e.particles); 6 | 7 | for (let i = 0; i < entitiesWithParticles.length; i++) { 8 | const entity = entitiesWithParticles[i]; 9 | const keys = Object.keys(entity.particles); 10 | 11 | for (let j = 0; j < keys.length; j++) { 12 | const ps = entity.particles[keys[j]]; 13 | const { spawnOptions, options, beforeSpawn } = ps; 14 | const delta = (time.delta / 1000) * spawnOptions.timeScale; 15 | 16 | ps.tick += delta; 17 | 18 | if (ps.tick < 0) ps.tick = 0; 19 | 20 | if (delta > 0) { 21 | beforeSpawn(entity, entities, ps, args); 22 | 23 | for (let x = 0; x < spawnOptions.spawnRate * delta; x++) { 24 | ps.emitter.spawnParticle(options); 25 | } 26 | } 27 | 28 | ps.emitter.update(ps.tick); 29 | } 30 | } 31 | 32 | return entities; 33 | }; 34 | 35 | export default Particles; 36 | -------------------------------------------------------------------------------- /src/game/systems/physics.js: -------------------------------------------------------------------------------- 1 | import { all } from "../utils"; 2 | 3 | const Physics = (entities, args) => { 4 | const world = entities.world; 5 | const entitiesWithBodies = all(entities, e => e.bodies && e.model); 6 | 7 | if (world) 8 | world.step(); 9 | 10 | for (let x = 0; x < entitiesWithBodies.length; x++) { 11 | const entity = entitiesWithBodies[x]; 12 | const model = entity.model; 13 | const body = entity.bodies[0]; 14 | const collision = entity.collision; 15 | 16 | if (!body.sleeping) { 17 | model.position.copy(body.getPosition()); 18 | model.quaternion.copy(body.getQuaternion()); 19 | } 20 | 21 | if (collision) { 22 | for (let y = 0; y < entitiesWithBodies.length; y++) { 23 | if (x === y) 24 | continue; 25 | 26 | const otherEntity = entitiesWithBodies[y]; 27 | const otherBody = otherEntity.bodies[0]; 28 | const contact = world.getContact(body, otherBody); 29 | 30 | if (contact) 31 | collision(entity, otherEntity, contact, entities, args); 32 | } 33 | } 34 | } 35 | 36 | return entities; 37 | }; 38 | 39 | export default Physics; 40 | -------------------------------------------------------------------------------- /src/game/systems/removal.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { remove } from "../utils"; 3 | import _ from "lodash"; 4 | 5 | //-- https://gist.github.com/zentrope/5022d89cfa995ac71978 6 | 7 | const frustum = new THREE.Frustum(); 8 | const cameraViewProjectionMatrix = new THREE.Matrix4(); 9 | 10 | const Removal = entities => { 11 | const camera = entities.camera; 12 | const removeableKeys = Object.keys(entities).filter( 13 | x => entities[x].removable 14 | ); 15 | 16 | camera.updateMatrixWorld(); 17 | camera.matrixWorldInverse.getInverse(camera.matrixWorld); 18 | cameraViewProjectionMatrix.multiplyMatrices( 19 | camera.projectionMatrix, 20 | camera.matrixWorldInverse 21 | ); 22 | frustum.setFromMatrix(cameraViewProjectionMatrix); 23 | 24 | removeableKeys.forEach(key => { 25 | const test = entities[key].removable; 26 | 27 | if (_.isFunction(test) ? test(frustum, entities[key], entities) : true) 28 | remove(entities, key); 29 | }); 30 | 31 | return entities; 32 | }; 33 | 34 | export default Removal; 35 | -------------------------------------------------------------------------------- /src/game/systems/rotation.js: -------------------------------------------------------------------------------- 1 | import { all } from "../utils"; 2 | import _ from "lodash"; 3 | 4 | const Rotation = (entities, args) => { 5 | const rotatables = all(entities, e => e.rotation, e => e.model); 6 | 7 | for (let i = 0; i < rotatables.length; i++) { 8 | const r = rotatables[i]; 9 | 10 | if (r.model.cellIndex !== undefined) { 11 | r.model.angle = _.isFunction(r.rotation) 12 | ? r.rotation(r, entities, args) 13 | : r.model.angle + r.rotation; 14 | } else { 15 | r.model.rotation.z = r.rotation.z 16 | ? _.isFunction(r.rotation.z) 17 | ? r.rotation.z(r, entities, args) 18 | : r.model.rotation.z + r.rotation.z 19 | : r.model.rotation.z; 20 | r.model.rotation.x = r.rotation.x 21 | ? _.isFunction(r.rotation.x) 22 | ? r.rotation.x(r, entities, args) 23 | : r.model.rotation.x + r.rotation.x 24 | : r.model.rotation.x; 25 | r.model.rotation.y = r.rotation.y 26 | ? _.isFunction(r.rotation.y) 27 | ? r.rotation.y(r, entities, args) 28 | : r.model.rotation.y + r.rotation.y 29 | : r.model.rotation.y; 30 | } 31 | } 32 | 33 | return entities; 34 | }; 35 | 36 | export default Rotation; 37 | -------------------------------------------------------------------------------- /src/game/systems/spawn.js: -------------------------------------------------------------------------------- 1 | import Box from "../components/box" 2 | import Cylinder from "../components/cylinder" 3 | import { id } from "../utils"; 4 | 5 | const boxId = (id => () => id("box"))(id(0)); 6 | const cylinderId = (id => () => id("cylinder"))(id(0)); 7 | 8 | const Spawn = (entities, { gamepadController, mouseController }) => { 9 | 10 | const world = entities.world; 11 | const scene = entities.scene; 12 | 13 | if ((gamepadController.button0 && !gamepadController.previous.button0) || (mouseController.left && !mouseController.previous.left)) 14 | entities[boxId()] = Box({ parent: scene, world, y: 5 }); 15 | 16 | if ((gamepadController.button1 && !gamepadController.previous.button1) || (mouseController.right && !mouseController.previous.right)) 17 | entities[cylinderId()] = Cylinder({ parent: scene, world, y: 5 }); 18 | 19 | return entities; 20 | }; 21 | 22 | export default Spawn; 23 | -------------------------------------------------------------------------------- /src/game/systems/spring.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { all } from "../utils"; 3 | 4 | const Spring = entities => { 5 | const spingEntities = all(entities, e => e.spring && e.physics); 6 | 7 | spingEntities.forEach(e => { 8 | const { 9 | spring: { k, length, anchor, subtract }, 10 | physics: { position, forces } 11 | } = e; 12 | 13 | const spring = subtract 14 | ? subtract(position, anchor, e.spring) 15 | : new THREE.Vector3().subVectors(position, anchor); 16 | const d = spring.length(); 17 | const stretch = d - length; 18 | 19 | spring.normalize(); 20 | spring.multiplyScalar(-1 * k * stretch); 21 | 22 | forces.add(spring); 23 | }); 24 | 25 | return entities; 26 | }; 27 | 28 | export default Spring; 29 | -------------------------------------------------------------------------------- /src/game/systems/timeline.js: -------------------------------------------------------------------------------- 1 | import { all } from "../utils"; 2 | import _ from "lodash"; 3 | 4 | const start = (timeline, args) => { 5 | if (!timeline.start) timeline.start = args.time.current; 6 | }; 7 | 8 | const update = (entity, entities, key, timeline, args) => { 9 | const time = args.time; 10 | 11 | if (timeline.duration) { 12 | let percent = (time.current - timeline.start) / timeline.duration; 13 | 14 | if (percent <= 1) { 15 | timeline.update(entity, entities, percent, timeline, args); 16 | } else { 17 | if (timeline.complete) 18 | timeline.complete(entity, entities, timeline, args); 19 | 20 | delete entity.timelines[key]; 21 | } 22 | } 23 | 24 | if (timeline.while) { 25 | if ( 26 | _.isFunction(timeline.while) 27 | ? timeline.while(entity, entities, timeline, args) 28 | : true 29 | ) { 30 | timeline.update(entity, entities, timeline, args); 31 | } else { 32 | if (timeline.complete) 33 | timeline.complete(entity, entities, timeline, args); 34 | 35 | delete entity.timelines[key]; 36 | } 37 | } 38 | }; 39 | 40 | const Timeline = (entities, args) => { 41 | const entitiesWithTimelines = all(entities, e => e.timelines); 42 | 43 | for (let i = 0; i < entitiesWithTimelines.length; i++) { 44 | const entity = entitiesWithTimelines[i]; 45 | const keys = Object.keys(entity.timelines); 46 | 47 | for (let j = 0; j < keys.length; j++) { 48 | const key = keys[j]; 49 | const timeline = entity.timelines[key]; 50 | 51 | if (timeline) { 52 | start(timeline, args); 53 | update(entity, entities, key, timeline, args); 54 | } 55 | } 56 | } 57 | 58 | return entities; 59 | }; 60 | 61 | export default Timeline; 62 | -------------------------------------------------------------------------------- /src/game/utils/index.js: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import { interpolate } from '@popmotion/popcorn'; 3 | import * as ThreeUtils from "./three"; 4 | 5 | const remove = (entities, key) => { 6 | const entity = entities[key]; 7 | 8 | if (!entity) 9 | return; 10 | 11 | if (entity.model) 12 | ThreeUtils.remove(entity.model.parent, entity.model); 13 | 14 | if (entity.light) 15 | ThreeUtils.remove(entity.light.parent, entity.light); 16 | 17 | if (entity.particles) { 18 | Object.keys(entity.particles).forEach(k => { 19 | const emitter = entity.particles[k].emitter 20 | if (emitter) 21 | ThreeUtils.remove(emitter.parent, emitter); 22 | }) 23 | } 24 | 25 | if (entity.bodies) 26 | entity.bodies.forEach(b => b.remove()) 27 | 28 | delete entities[key]; 29 | 30 | return entities; 31 | }; 32 | 33 | const any = (arr = [], b = "", c) => { 34 | if (c) { 35 | if (Array.isArray(c) === false) c = [c]; 36 | 37 | return _.isFunction(b) 38 | ? _.intersection(arr.map(b), c).length > 0 39 | : _.intersection(arr.map(x => x[b]), c).length > 0; 40 | } 41 | 42 | if (!b) return arr.length > 0; 43 | 44 | if (Array.isArray(b)) return _.intersection(arr, b).length > 0; 45 | 46 | if (_.isFunction(b)) return arr.find(b); 47 | 48 | return arr.indexOf(b) > -1; 49 | }; 50 | 51 | const first = (entities, ...predicates) => { 52 | if (!entities) return; 53 | if (!predicates || predicates.length < 1) return entities[0]; 54 | 55 | if (Array.isArray(entities)) 56 | return entities.find(e => _.every(predicates, p => p(e))) 57 | 58 | return entities[Object.keys(entities).find(key => _.every(predicates, p => p(entities[key])))] 59 | } 60 | 61 | const firstKey = (entities, ...predicates) => { 62 | if (!entities) return; 63 | if (!predicates || predicates.length < 1) return Object.keys(entities)[0]; 64 | 65 | return Object.keys(entities).find(key => _.every(predicates, p => p(entities[key]))) 66 | } 67 | 68 | const all = (entities, ...predicates) => { 69 | if (!entities) return; 70 | if (!predicates || predicates.length < 1) return entities; 71 | 72 | if (Array.isArray(entities)) 73 | return entities.filter(e => _.every(predicates, p => p(e))) 74 | 75 | return Object.keys(entities).filter(key => _.every(predicates, p => p(entities[key]))).map(key => entities[key]) 76 | } 77 | 78 | const allKeys = (entities, ...predicates) => { 79 | if (!entities) return; 80 | if (!predicates || predicates.length < 1) return Object.keys(entities); 81 | 82 | return Object.keys(entities).filter(key => _.every(predicates, p => p(entities[key]))); 83 | } 84 | 85 | //-- https://stackoverflow.com/a/7616484/138392 86 | const getHashCode = str => { 87 | var hash = 0, i, chr; 88 | if (str.length === 0) return hash; 89 | for (i = 0; i < str.length; i++) { 90 | chr = str.charCodeAt(i); 91 | hash = ((hash << 5) - hash) + chr; 92 | hash |= 0; // Convert to 32bit integer 93 | } 94 | return hash; 95 | }; 96 | 97 | const positive = val => Math.abs(val) 98 | 99 | const negative = val => { 100 | if (val > 0) return -val 101 | return val 102 | } 103 | 104 | const remap = (n, start1, stop1, start2, stop2) => { 105 | return (n - start1) / (stop1 - start1) * (stop2 - start2) + start2; 106 | } 107 | 108 | const constrain = (n, low, high) => { 109 | return Math.max(Math.min(n, high), low); 110 | } 111 | 112 | const between = (n, low, high) => { 113 | return n > low && n < high 114 | } 115 | 116 | const pipe = (...funcs) => _.flow(_.flatten(funcs || [])) 117 | 118 | const id = (seed = 0) => (prefix = "") => `${prefix}${++seed}` 119 | 120 | const cond = (condition, func) => { 121 | return (args) => { 122 | const test = _.isFunction(condition) ? condition(args) : condition 123 | return test ? func(args) : args 124 | } 125 | } 126 | 127 | const log = label => data => { 128 | console.log(label, data); 129 | return data; 130 | } 131 | 132 | const randomInt = (min = 0, max = 1) => Math.floor(Math.random() * (max - min + 1) + min); 133 | 134 | const throttle = (func, interval, defaultValue) => { 135 | let last = 0; 136 | return (...args) => { 137 | const current = performance.now(); 138 | if ((current - last) > interval) { 139 | last = current; 140 | return func(...args); 141 | } else { 142 | return _.isFunction(defaultValue) ? defaultValue(...args) : defaultValue; 143 | } 144 | } 145 | } 146 | 147 | const screen = window.screen; 148 | 149 | const createSound = (asset, throttleInterval = 0) => { 150 | const audio = new Audio(asset); 151 | const play = () => audio.play(); 152 | 153 | return throttleInterval ? throttle(play, throttleInterval) : play; 154 | }; 155 | 156 | const find = _.find; 157 | const filter = _.filter; 158 | const clamp = constrain; 159 | const once = _.once; 160 | const memoize = _.memoize; 161 | const sound = createSound; 162 | 163 | export { 164 | remove, 165 | any, 166 | find, 167 | filter, 168 | first, 169 | firstKey, 170 | all, 171 | allKeys, 172 | getHashCode, 173 | positive, 174 | negative, 175 | remap, 176 | constrain, 177 | clamp, 178 | between, 179 | pipe, 180 | id, 181 | cond, 182 | interpolate, 183 | log, 184 | randomInt, 185 | once, 186 | memoize, 187 | throttle, 188 | screen, 189 | createSound, 190 | sound 191 | } -------------------------------------------------------------------------------- /src/game/utils/perlin.js: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////// 2 | 3 | // http://mrl.nyu.edu/~perlin/noise/ 4 | // Adapting from PApplet.java 5 | // which was adapted from toxi 6 | // which was adapted from the german demo group farbrausch 7 | // as used in their demo "art": http://www.farb-rausch.de/fr010src.zip 8 | 9 | // someday we might consider using "improved noise" 10 | // http://mrl.nyu.edu/~perlin/paper445.pdf 11 | // See: https://github.com/shiffman/The-Nature-of-Code-Examples-p5.js/ 12 | // blob/master/introduction/Noise1D/noise.js 13 | 14 | /** 15 | * @module Math 16 | * @submodule Noise 17 | * @for p5 18 | * @requires core 19 | */ 20 | 21 | var PERLIN_YWRAPB = 4; 22 | var PERLIN_YWRAP = 1 << PERLIN_YWRAPB; 23 | var PERLIN_ZWRAPB = 8; 24 | var PERLIN_ZWRAP = 1 << PERLIN_ZWRAPB; 25 | var PERLIN_SIZE = 4095; 26 | 27 | var perlin_octaves = 4; // default to medium smooth 28 | var perlin_amp_falloff = 0.5; // 50% reduction/octave 29 | 30 | var scaled_cosine = function(i) { 31 | return 0.5 * (1.0 - Math.cos(i * Math.PI)); 32 | }; 33 | 34 | var perlin; // will be initialized lazily by noise() or noiseSeed() 35 | 36 | /** 37 | * Returns the Perlin noise value at specified coordinates. Perlin noise is 38 | * a random sequence generator producing a more natural ordered, harmonic 39 | * succession of numbers compared to the standard random() function. 40 | * It was invented by Ken Perlin in the 1980s and been used since in 41 | * graphical applications to produce procedural textures, natural motion, 42 | * shapes, terrains etc.

The main difference to the 43 | * random() function is that Perlin noise is defined in an infinite 44 | * n-dimensional space where each pair of coordinates corresponds to a 45 | * fixed semi-random value (fixed only for the lifespan of the program; see 46 | * the noiseSeed() function). p5.js can compute 1D, 2D and 3D noise, 47 | * depending on the number of coordinates given. The resulting value will 48 | * always be between 0.0 and 1.0. The noise value can be animated by moving 49 | * through the noise space as demonstrated in the example above. The 2nd 50 | * and 3rd dimension can also be interpreted as time.

The actual 51 | * noise is structured similar to an audio signal, in respect to the 52 | * function's use of frequencies. Similar to the concept of harmonics in 53 | * physics, perlin noise is computed over several octaves which are added 54 | * together for the final result.

Another way to adjust the 55 | * character of the resulting sequence is the scale of the input 56 | * coordinates. As the function works within an infinite space the value of 57 | * the coordinates doesn't matter as such, only the distance between 58 | * successive coordinates does (eg. when using noise() within a 59 | * loop). As a general rule the smaller the difference between coordinates, 60 | * the smoother the resulting noise sequence will be. Steps of 0.005-0.03 61 | * work best for most applications, but this will differ depending on use. 62 | * 63 | * 64 | * @method noise 65 | * @param {Number} x x-coordinate in noise space 66 | * @param {Number} [y] y-coordinate in noise space 67 | * @param {Number} [z] z-coordinate in noise space 68 | * @return {Number} Perlin noise value (between 0 and 1) at specified 69 | * coordinates 70 | * @example 71 | *
72 | * 73 | * var xoff = 0.0; 74 | * 75 | * function draw() { 76 | * background(204); 77 | * xoff = xoff + 0.01; 78 | * var n = noise(xoff) * width; 79 | * line(n, 0, n, height); 80 | * } 81 | * 82 | *
83 | *
84 | * var noiseScale=0.02; 85 | * 86 | * function draw() { 87 | * background(0); 88 | * for (var x=0; x < width; x++) { 89 | * var noiseVal = noise((mouseX+x)*noiseScale, mouseY*noiseScale); 90 | * stroke(noiseVal*255); 91 | * line(x, mouseY+noiseVal*80, x, height); 92 | * } 93 | * } 94 | * 95 | *
96 | * 97 | * @alt 98 | * vertical line moves left to right with updating noise values. 99 | * horizontal wave pattern effected by mouse x-position & updating noise values. 100 | * 101 | */ 102 | 103 | const noise = function(x, y, z) { 104 | y = y || 0; 105 | z = z || 0; 106 | 107 | if (perlin == null) { 108 | perlin = new Array(PERLIN_SIZE + 1); 109 | for (var i = 0; i < PERLIN_SIZE + 1; i++) { 110 | perlin[i] = Math.random(); 111 | } 112 | } 113 | 114 | if (x < 0) { 115 | x = -x; 116 | } 117 | if (y < 0) { 118 | y = -y; 119 | } 120 | if (z < 0) { 121 | z = -z; 122 | } 123 | 124 | var xi = Math.floor(x), 125 | yi = Math.floor(y), 126 | zi = Math.floor(z); 127 | var xf = x - xi; 128 | var yf = y - yi; 129 | var zf = z - zi; 130 | var rxf, ryf; 131 | 132 | var r = 0; 133 | var ampl = 0.5; 134 | 135 | var n1, n2, n3; 136 | 137 | for (var o = 0; o < perlin_octaves; o++) { 138 | var of = xi + (yi << PERLIN_YWRAPB) + (zi << PERLIN_ZWRAPB); 139 | 140 | rxf = scaled_cosine(xf); 141 | ryf = scaled_cosine(yf); 142 | 143 | n1 = perlin[of & PERLIN_SIZE]; 144 | n1 += rxf * (perlin[(of + 1) & PERLIN_SIZE] - n1); 145 | n2 = perlin[(of + PERLIN_YWRAP) & PERLIN_SIZE]; 146 | n2 += rxf * (perlin[(of + PERLIN_YWRAP + 1) & PERLIN_SIZE] - n2); 147 | n1 += ryf * (n2 - n1); 148 | 149 | of += PERLIN_ZWRAP; 150 | n2 = perlin[of & PERLIN_SIZE]; 151 | n2 += rxf * (perlin[(of + 1) & PERLIN_SIZE] - n2); 152 | n3 = perlin[(of + PERLIN_YWRAP) & PERLIN_SIZE]; 153 | n3 += rxf * (perlin[(of + PERLIN_YWRAP + 1) & PERLIN_SIZE] - n3); 154 | n2 += ryf * (n3 - n2); 155 | 156 | n1 += scaled_cosine(zf) * (n2 - n1); 157 | 158 | r += n1 * ampl; 159 | ampl *= perlin_amp_falloff; 160 | xi <<= 1; 161 | xf *= 2; 162 | yi <<= 1; 163 | yf *= 2; 164 | zi <<= 1; 165 | zf *= 2; 166 | 167 | if (xf >= 1.0) { 168 | xi++; 169 | xf--; 170 | } 171 | if (yf >= 1.0) { 172 | yi++; 173 | yf--; 174 | } 175 | if (zf >= 1.0) { 176 | zi++; 177 | zf--; 178 | } 179 | } 180 | return r; 181 | }; 182 | 183 | /** 184 | * 185 | * Adjusts the character and level of detail produced by the Perlin noise 186 | * function. Similar to harmonics in physics, noise is computed over 187 | * several octaves. Lower octaves contribute more to the output signal and 188 | * as such define the overall intensity of the noise, whereas higher octaves 189 | * create finer grained details in the noise sequence. 190 | *

191 | * By default, noise is computed over 4 octaves with each octave contributing 192 | * exactly half than its predecessor, starting at 50% strength for the 1st 193 | * octave. This falloff amount can be changed by adding an additional function 194 | * parameter. Eg. a falloff factor of 0.75 means each octave will now have 195 | * 75% impact (25% less) of the previous lower octave. Any value between 196 | * 0.0 and 1.0 is valid, however note that values greater than 0.5 might 197 | * result in greater than 1.0 values returned by noise(). 198 | *

199 | * By changing these parameters, the signal created by the noise() 200 | * function can be adapted to fit very specific needs and characteristics. 201 | * 202 | * @method noiseDetail 203 | * @param {Number} lod number of octaves to be used by the noise 204 | * @param {Number} falloff falloff factor for each octave 205 | * @example 206 | *
207 | * 208 | * var noiseVal; 209 | * var noiseScale = 0.02; 210 | * 211 | * function setup() { 212 | * createCanvas(100, 100); 213 | * } 214 | * 215 | * function draw() { 216 | * background(0); 217 | * for (var y = 0; y < height; y++) { 218 | * for (var x = 0; x < width / 2; x++) { 219 | * noiseDetail(2, 0.2); 220 | * noiseVal = noise((mouseX + x) * noiseScale, (mouseY + y) * noiseScale); 221 | * stroke(noiseVal * 255); 222 | * point(x, y); 223 | * noiseDetail(8, 0.65); 224 | * noiseVal = noise( 225 | * (mouseX + x + width / 2) * noiseScale, 226 | * (mouseY + y) * noiseScale 227 | * ); 228 | * stroke(noiseVal * 255); 229 | * point(x + width / 2, y); 230 | * } 231 | * } 232 | * } 233 | * 234 | *
235 | * 236 | * @alt 237 | * 2 vertical grey smokey patterns affected my mouse x-position and noise. 238 | * 239 | */ 240 | const noiseDetail = function(lod, falloff) { 241 | if (lod > 0) { 242 | perlin_octaves = lod; 243 | } 244 | if (falloff > 0) { 245 | perlin_amp_falloff = falloff; 246 | } 247 | }; 248 | 249 | /** 250 | * Sets the seed value for noise(). By default, noise() 251 | * produces different results each time the program is run. Set the 252 | * value parameter to a constant to return the same pseudo-random 253 | * numbers each time the software is run. 254 | * 255 | * @method noiseSeed 256 | * @param {Number} seed the seed value 257 | * @example 258 | *
259 | * var xoff = 0.0; 260 | * 261 | * function setup() { 262 | * noiseSeed(99); 263 | * stroke(0, 10); 264 | * } 265 | * 266 | * function draw() { 267 | * xoff = xoff + .01; 268 | * var n = noise(xoff) * width; 269 | * line(n, 0, n, height); 270 | * } 271 | * 272 | *
273 | * 274 | * @alt 275 | * vertical grey lines drawing in pattern affected by noise. 276 | * 277 | */ 278 | const noiseSeed = function(seed) { 279 | // Linear Congruential Generator 280 | // Variant of a Lehman Generator 281 | var lcg = (function() { 282 | // Set to values from http://en.wikipedia.org/wiki/Numerical_Recipes 283 | // m is basically chosen to be large (as it is the max period) 284 | // and for its relationships to a and c 285 | var m = 4294967296; 286 | // a - 1 should be divisible by m's prime factors 287 | var a = 1664525; 288 | // c and m should be co-prime 289 | var c = 1013904223; 290 | var seed, z; 291 | return { 292 | setSeed: function(val) { 293 | // pick a random seed if val is undefined or null 294 | // the >>> 0 casts the seed to an unsigned 32-bit integer 295 | z = seed = (val == null ? Math.random() * m : val) >>> 0; 296 | }, 297 | getSeed: function() { 298 | return seed; 299 | }, 300 | rand: function() { 301 | // define the recurrence relationship 302 | z = (a * z + c) % m; 303 | // return a float in [0, 1) 304 | // if z = m then z / m = 0 therefore (z % m) / m < 1 always 305 | return z / m; 306 | } 307 | }; 308 | })(); 309 | 310 | lcg.setSeed(seed); 311 | perlin = new Array(PERLIN_SIZE + 1); 312 | for (var i = 0; i < PERLIN_SIZE + 1; i++) { 313 | perlin[i] = lcg.rand(); 314 | } 315 | }; 316 | 317 | module.exports = { 318 | noise, 319 | noiseDetail, 320 | noiseSeed 321 | }; -------------------------------------------------------------------------------- /src/game/utils/three/dds-loader.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | /* 4 | * @author mrdoob / http://mrdoob.com/ 5 | */ 6 | 7 | const DDSLoader = function () { 8 | 9 | this._parser = DDSLoader.parse; 10 | 11 | }; 12 | 13 | DDSLoader.prototype = Object.create( THREE.CompressedTextureLoader.prototype ); 14 | DDSLoader.prototype.constructor = DDSLoader; 15 | 16 | DDSLoader.parse = function ( buffer, loadMipmaps ) { 17 | 18 | var dds = { mipmaps: [], width: 0, height: 0, format: null, mipmapCount: 1 }; 19 | 20 | // Adapted from @toji's DDS utils 21 | // https://github.com/toji/webgl-texture-utils/blob/master/texture-util/dds.js 22 | 23 | // All values and structures referenced from: 24 | // http://msdn.microsoft.com/en-us/library/bb943991.aspx/ 25 | 26 | var DDS_MAGIC = 0x20534444; 27 | 28 | var DDSD_MIPMAPCOUNT = 0x20000; 29 | 30 | var DDSCAPS2_CUBEMAP = 0x200, 31 | DDSCAPS2_CUBEMAP_POSITIVEX = 0x400, 32 | DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800, 33 | DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000, 34 | DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000, 35 | DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000, 36 | DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000; 37 | 38 | var DDPF_FOURCC = 0x4; 39 | 40 | function fourCCToInt32( value ) { 41 | 42 | return value.charCodeAt( 0 ) + 43 | ( value.charCodeAt( 1 ) << 8 ) + 44 | ( value.charCodeAt( 2 ) << 16 ) + 45 | ( value.charCodeAt( 3 ) << 24 ); 46 | 47 | } 48 | 49 | function int32ToFourCC( value ) { 50 | 51 | return String.fromCharCode( 52 | value & 0xff, 53 | ( value >> 8 ) & 0xff, 54 | ( value >> 16 ) & 0xff, 55 | ( value >> 24 ) & 0xff 56 | ); 57 | 58 | } 59 | 60 | function loadARGBMip( buffer, dataOffset, width, height ) { 61 | 62 | var dataLength = width * height * 4; 63 | var srcBuffer = new Uint8Array( buffer, dataOffset, dataLength ); 64 | var byteArray = new Uint8Array( dataLength ); 65 | var dst = 0; 66 | var src = 0; 67 | for ( var y = 0; y < height; y ++ ) { 68 | 69 | for ( var x = 0; x < width; x ++ ) { 70 | 71 | var b = srcBuffer[ src ]; src ++; 72 | var g = srcBuffer[ src ]; src ++; 73 | var r = srcBuffer[ src ]; src ++; 74 | var a = srcBuffer[ src ]; src ++; 75 | byteArray[ dst ] = r; dst ++; //r 76 | byteArray[ dst ] = g; dst ++; //g 77 | byteArray[ dst ] = b; dst ++; //b 78 | byteArray[ dst ] = a; dst ++; //a 79 | 80 | } 81 | 82 | } 83 | return byteArray; 84 | 85 | } 86 | 87 | var FOURCC_DXT1 = fourCCToInt32( "DXT1" ); 88 | var FOURCC_DXT3 = fourCCToInt32( "DXT3" ); 89 | var FOURCC_DXT5 = fourCCToInt32( "DXT5" ); 90 | var FOURCC_ETC1 = fourCCToInt32( "ETC1" ); 91 | 92 | var headerLengthInt = 31; // The header length in 32 bit ints 93 | 94 | // Offsets into the header array 95 | 96 | var off_magic = 0; 97 | 98 | var off_size = 1; 99 | var off_flags = 2; 100 | var off_height = 3; 101 | var off_width = 4; 102 | 103 | var off_mipmapCount = 7; 104 | 105 | var off_pfFlags = 20; 106 | var off_pfFourCC = 21; 107 | var off_RGBBitCount = 22; 108 | var off_RBitMask = 23; 109 | var off_GBitMask = 24; 110 | var off_BBitMask = 25; 111 | var off_ABitMask = 26; 112 | 113 | var off_caps2 = 28; 114 | 115 | // Parse header 116 | 117 | var header = new Int32Array( buffer, 0, headerLengthInt ); 118 | 119 | if ( header[ off_magic ] !== DDS_MAGIC ) { 120 | 121 | console.error( 'THREE.DDSLoader.parse: Invalid magic number in DDS header.' ); 122 | return dds; 123 | 124 | } 125 | 126 | if ( ! header[ off_pfFlags ] & DDPF_FOURCC ) { 127 | 128 | console.error( 'THREE.DDSLoader.parse: Unsupported format, must contain a FourCC code.' ); 129 | return dds; 130 | 131 | } 132 | 133 | var blockBytes; 134 | 135 | var fourCC = header[ off_pfFourCC ]; 136 | 137 | var isRGBAUncompressed = false; 138 | 139 | switch ( fourCC ) { 140 | 141 | case FOURCC_DXT1: 142 | 143 | blockBytes = 8; 144 | dds.format = THREE.RGB_S3TC_DXT1_Format; 145 | break; 146 | 147 | case FOURCC_DXT3: 148 | 149 | blockBytes = 16; 150 | dds.format = THREE.RGBA_S3TC_DXT3_Format; 151 | break; 152 | 153 | case FOURCC_DXT5: 154 | 155 | blockBytes = 16; 156 | dds.format = THREE.RGBA_S3TC_DXT5_Format; 157 | break; 158 | 159 | case FOURCC_ETC1: 160 | 161 | blockBytes = 8; 162 | dds.format = THREE.RGB_ETC1_Format; 163 | break; 164 | 165 | default: 166 | 167 | if ( header[ off_RGBBitCount ] === 32 168 | && header[ off_RBitMask ] & 0xff0000 169 | && header[ off_GBitMask ] & 0xff00 170 | && header[ off_BBitMask ] & 0xff 171 | && header[ off_ABitMask ] & 0xff000000 ) { 172 | 173 | isRGBAUncompressed = true; 174 | blockBytes = 64; 175 | dds.format = THREE.RGBAFormat; 176 | 177 | } else { 178 | 179 | console.error( 'THREE.DDSLoader.parse: Unsupported FourCC code ', int32ToFourCC( fourCC ) ); 180 | return dds; 181 | 182 | } 183 | 184 | } 185 | 186 | dds.mipmapCount = 1; 187 | 188 | if ( header[ off_flags ] & DDSD_MIPMAPCOUNT && loadMipmaps !== false ) { 189 | 190 | dds.mipmapCount = Math.max( 1, header[ off_mipmapCount ] ); 191 | 192 | } 193 | 194 | var caps2 = header[ off_caps2 ]; 195 | dds.isCubemap = caps2 & DDSCAPS2_CUBEMAP ? true : false; 196 | if ( dds.isCubemap && ( 197 | ! ( caps2 & DDSCAPS2_CUBEMAP_POSITIVEX ) || 198 | ! ( caps2 & DDSCAPS2_CUBEMAP_NEGATIVEX ) || 199 | ! ( caps2 & DDSCAPS2_CUBEMAP_POSITIVEY ) || 200 | ! ( caps2 & DDSCAPS2_CUBEMAP_NEGATIVEY ) || 201 | ! ( caps2 & DDSCAPS2_CUBEMAP_POSITIVEZ ) || 202 | ! ( caps2 & DDSCAPS2_CUBEMAP_NEGATIVEZ ) 203 | ) ) { 204 | 205 | console.error( 'THREE.DDSLoader.parse: Incomplete cubemap faces' ); 206 | return dds; 207 | 208 | } 209 | 210 | dds.width = header[ off_width ]; 211 | dds.height = header[ off_height ]; 212 | 213 | var dataOffset = header[ off_size ] + 4; 214 | 215 | // Extract mipmaps buffers 216 | 217 | var faces = dds.isCubemap ? 6 : 1; 218 | 219 | for ( var face = 0; face < faces; face ++ ) { 220 | 221 | var width = dds.width; 222 | var height = dds.height; 223 | var byteArray = null; 224 | var dataLength = null; 225 | 226 | for ( var i = 0; i < dds.mipmapCount; i ++ ) { 227 | 228 | if ( isRGBAUncompressed ) { 229 | 230 | byteArray = loadARGBMip( buffer, dataOffset, width, height ); 231 | dataLength = byteArray.length; 232 | 233 | } else { 234 | 235 | dataLength = Math.max( 4, width ) / 4 * Math.max( 4, height ) / 4 * blockBytes; 236 | byteArray = new Uint8Array( buffer, dataOffset, dataLength ); 237 | 238 | } 239 | 240 | var mipmap = { "data": byteArray, "width": width, "height": height }; 241 | dds.mipmaps.push( mipmap ); 242 | 243 | dataOffset += dataLength; 244 | 245 | width = Math.max( width >> 1, 1 ); 246 | height = Math.max( height >> 1, 1 ); 247 | 248 | } 249 | 250 | } 251 | 252 | return dds; 253 | 254 | }; 255 | 256 | export default DDSLoader; -------------------------------------------------------------------------------- /src/game/utils/three/fbx-loader.js: -------------------------------------------------------------------------------- 1 | import Zlib from "three/examples/js/libs/inflate.min"; 2 | import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader"; 3 | 4 | global.Zlib = Zlib.Zlib; 5 | 6 | export default FBXLoader; -------------------------------------------------------------------------------- /src/game/utils/three/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import SkeletonUtils from "./skeleton-utils"; 3 | 4 | export const clean = obj => { 5 | while (obj.children.length > 0) { 6 | clean(obj.children[0]); 7 | obj.remove(obj.children[0]); 8 | } 9 | 10 | if (obj.geometry && obj.geometry.dispose) obj.geometry.dispose(); 11 | if (obj.material && obj.material.dispose) obj.material.dispose(); 12 | if (obj.texture && obj.texture.dispose) obj.texture.dispose(); 13 | }; 14 | 15 | export const clear = clean; 16 | 17 | export const remove = (parent, child) => { 18 | if (child) 19 | clean(child); 20 | 21 | if (parent) 22 | parent.remove(child); 23 | }; 24 | 25 | export const direction = obj => { 26 | return obj.getWorldDirection(new THREE.Vector3()); 27 | }; 28 | 29 | export const rotateAroundPoint = ( 30 | obj, 31 | point, 32 | { x = 0, y = 0, z = 0 } 33 | ) => { 34 | //-- https://stackoverflow.com/a/42866733/138392 35 | //-- https://stackoverflow.com/a/44288885/138392 36 | 37 | const original = obj.position.clone(); 38 | const pivot = point.clone(); 39 | const diff = new THREE.Vector3().subVectors(original, pivot); 40 | 41 | obj.position.copy(pivot); 42 | 43 | obj.rotation.x += x; 44 | obj.rotation.y += y; 45 | obj.rotation.z += z; 46 | 47 | diff.applyAxisAngle(new THREE.Vector3(1, 0, 0), x); 48 | diff.applyAxisAngle(new THREE.Vector3(0, 1, 0), y); 49 | diff.applyAxisAngle(new THREE.Vector3(0, 0, 1), z); 50 | 51 | obj.position.add(diff); 52 | }; 53 | 54 | export const model = obj => { 55 | return obj.model ? obj.model : obj; 56 | }; 57 | 58 | export const add = (parent, child) => { 59 | if (!parent || !child) 60 | return; 61 | 62 | const p = parent.model ? parent.model : parent; 63 | const c = child.model ? child.model : child; 64 | 65 | model(p).add(model(c)) 66 | }; 67 | 68 | export const reparent = (subject, newParent) => { 69 | subject.matrix.copy(subject.matrixWorld); 70 | subject.applyMatrix(new THREE.Matrix4().getInverse(newParent.matrixWorld)); 71 | newParent.add(subject); 72 | }; 73 | 74 | export const size = model => { 75 | const currentSize = new THREE.Vector3(); 76 | const currentBox = new THREE.Box3().setFromObject(model); 77 | 78 | currentBox.getSize(currentSize); 79 | 80 | return currentSize; 81 | }; 82 | 83 | export const cloneTexture = texture => { 84 | const clone = texture.clone(); 85 | 86 | return clone; 87 | }; 88 | 89 | export const cloneMesh = SkeletonUtils.clone; 90 | 91 | export const firstMesh = obj => { 92 | if (!obj) 93 | return; 94 | 95 | if (obj.isMesh) 96 | return obj; 97 | 98 | if (obj.children && obj.children.length){ 99 | for (let i = 0; i < obj.children.length; i++) { 100 | const test = firstMesh(obj.children[i]); 101 | 102 | if (test && test.isMesh) 103 | return test; 104 | } 105 | } 106 | }; 107 | 108 | export const promisifyLoader = (loader, onProgress) => { 109 | 110 | const promiseLoader = url => { 111 | return new Promise( (resolve, reject) => { 112 | loader.load(url, resolve, onProgress, reject); 113 | }); 114 | } 115 | 116 | return { 117 | originalLoader: loader, 118 | load: promiseLoader, 119 | }; 120 | }; -------------------------------------------------------------------------------- /src/game/utils/three/skeleton-utils.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | 3 | /** 4 | * @author sunag / http://www.sunag.com.br 5 | */ 6 | 7 | const SkeletonUtils = { 8 | 9 | retarget: function () { 10 | 11 | var pos = new THREE.Vector3(), 12 | quat = new THREE.Quaternion(), 13 | scale = new THREE.Vector3(), 14 | bindBoneMatrix = new THREE.Matrix4(), 15 | relativeMatrix = new THREE.Matrix4(), 16 | globalMatrix = new THREE.Matrix4(); 17 | 18 | return function ( target, source, options ) { 19 | 20 | options = options || {}; 21 | options.preserveMatrix = options.preserveMatrix !== undefined ? options.preserveMatrix : true; 22 | options.preservePosition = options.preservePosition !== undefined ? options.preservePosition : true; 23 | options.preserveHipPosition = options.preserveHipPosition !== undefined ? options.preserveHipPosition : false; 24 | options.useTargetMatrix = options.useTargetMatrix !== undefined ? options.useTargetMatrix : false; 25 | options.hip = options.hip !== undefined ? options.hip : "hip"; 26 | options.names = options.names || {}; 27 | 28 | var sourceBones = source.isObject3D ? source.skeleton.bones : this.getBones( source ), 29 | bones = target.isObject3D ? target.skeleton.bones : this.getBones( target ), 30 | bindBones, 31 | bone, name, boneTo, 32 | bonesPosition, i; 33 | 34 | // reset bones 35 | 36 | if ( target.isObject3D ) { 37 | 38 | target.skeleton.pose(); 39 | 40 | } else { 41 | 42 | options.useTargetMatrix = true; 43 | options.preserveMatrix = false; 44 | 45 | } 46 | 47 | if ( options.preservePosition ) { 48 | 49 | bonesPosition = []; 50 | 51 | for ( i = 0; i < bones.length; i ++ ) { 52 | 53 | bonesPosition.push( bones[ i ].position.clone() ); 54 | 55 | } 56 | 57 | } 58 | 59 | if ( options.preserveMatrix ) { 60 | 61 | // reset matrix 62 | 63 | target.updateMatrixWorld(); 64 | 65 | target.matrixWorld.identity(); 66 | 67 | // reset children matrix 68 | 69 | for ( i = 0; i < target.children.length; ++ i ) { 70 | 71 | target.children[ i ].updateMatrixWorld( true ); 72 | 73 | } 74 | 75 | } 76 | 77 | if ( options.offsets ) { 78 | 79 | bindBones = []; 80 | 81 | for ( i = 0; i < bones.length; ++ i ) { 82 | 83 | bone = bones[ i ]; 84 | name = options.names[ bone.name ] || bone.name; 85 | 86 | if ( options.offsets && options.offsets[ name ] ) { 87 | 88 | bone.matrix.multiply( options.offsets[ name ] ); 89 | 90 | bone.matrix.decompose( bone.position, bone.quaternion, bone.scale ); 91 | 92 | bone.updateMatrixWorld(); 93 | 94 | } 95 | 96 | bindBones.push( bone.matrixWorld.clone() ); 97 | 98 | } 99 | 100 | } 101 | 102 | for ( i = 0; i < bones.length; ++ i ) { 103 | 104 | bone = bones[ i ]; 105 | name = options.names[ bone.name ] || bone.name; 106 | 107 | boneTo = this.getBoneByName( name, sourceBones ); 108 | 109 | globalMatrix.copy( bone.matrixWorld ); 110 | 111 | if ( boneTo ) { 112 | 113 | boneTo.updateMatrixWorld(); 114 | 115 | if ( options.useTargetMatrix ) { 116 | 117 | relativeMatrix.copy( boneTo.matrixWorld ); 118 | 119 | } else { 120 | 121 | relativeMatrix.getInverse( target.matrixWorld ); 122 | relativeMatrix.multiply( boneTo.matrixWorld ); 123 | 124 | } 125 | 126 | // ignore scale to extract rotation 127 | 128 | scale.setFromMatrixScale( relativeMatrix ); 129 | relativeMatrix.scale( scale.set( 1 / scale.x, 1 / scale.y, 1 / scale.z ) ); 130 | 131 | // apply to global matrix 132 | 133 | globalMatrix.makeRotationFromQuaternion( quat.setFromRotationMatrix( relativeMatrix ) ); 134 | 135 | if ( target.isObject3D ) { 136 | 137 | var boneIndex = bones.indexOf( bone ), 138 | wBindMatrix = bindBones ? bindBones[ boneIndex ] : bindBoneMatrix.getInverse( target.skeleton.boneInverses[ boneIndex ] ); 139 | 140 | globalMatrix.multiply( wBindMatrix ); 141 | 142 | } 143 | 144 | globalMatrix.copyPosition( relativeMatrix ); 145 | 146 | } 147 | 148 | if ( bone.parent && bone.parent.isBone ) { 149 | 150 | bone.matrix.getInverse( bone.parent.matrixWorld ); 151 | bone.matrix.multiply( globalMatrix ); 152 | 153 | } else { 154 | 155 | bone.matrix.copy( globalMatrix ); 156 | 157 | } 158 | 159 | if ( options.preserveHipPosition && name === options.hip ) { 160 | 161 | bone.matrix.setPosition( pos.set( 0, bone.position.y, 0 ) ); 162 | 163 | } 164 | 165 | bone.matrix.decompose( bone.position, bone.quaternion, bone.scale ); 166 | 167 | bone.updateMatrixWorld(); 168 | 169 | } 170 | 171 | if ( options.preservePosition ) { 172 | 173 | for ( i = 0; i < bones.length; ++ i ) { 174 | 175 | bone = bones[ i ]; 176 | name = options.names[ bone.name ] || bone.name; 177 | 178 | if ( name !== options.hip ) { 179 | 180 | bone.position.copy( bonesPosition[ i ] ); 181 | 182 | } 183 | 184 | } 185 | 186 | } 187 | 188 | if ( options.preserveMatrix ) { 189 | 190 | // restore matrix 191 | 192 | target.updateMatrixWorld( true ); 193 | 194 | } 195 | 196 | }; 197 | 198 | }(), 199 | 200 | retargetClip: function ( target, source, clip, options ) { 201 | 202 | options = options || {}; 203 | options.useFirstFramePosition = options.useFirstFramePosition !== undefined ? options.useFirstFramePosition : false; 204 | options.fps = options.fps !== undefined ? options.fps : 30; 205 | options.names = options.names || []; 206 | 207 | if ( ! source.isObject3D ) { 208 | 209 | source = this.getHelperFromSkeleton( source ); 210 | 211 | } 212 | 213 | var numFrames = Math.round( clip.duration * ( options.fps / 1000 ) * 1000 ), 214 | delta = 1 / options.fps, 215 | convertedTracks = [], 216 | mixer = new THREE.AnimationMixer( source ), 217 | bones = this.getBones( target.skeleton ), 218 | boneDatas = [], 219 | positionOffset, 220 | bone, boneTo, boneData, 221 | name, i, j; 222 | 223 | mixer.clipAction( clip ).play(); 224 | mixer.update( 0 ); 225 | 226 | source.updateMatrixWorld(); 227 | 228 | for ( i = 0; i < numFrames; ++ i ) { 229 | 230 | var time = i * delta; 231 | 232 | this.retarget( target, source, options ); 233 | 234 | for ( j = 0; j < bones.length; ++ j ) { 235 | 236 | name = options.names[ bones[ j ].name ] || bones[ j ].name; 237 | 238 | boneTo = this.getBoneByName( name, source.skeleton ); 239 | 240 | if ( boneTo ) { 241 | 242 | bone = bones[ j ]; 243 | boneData = boneDatas[ j ] = boneDatas[ j ] || { bone: bone }; 244 | 245 | if ( options.hip === name ) { 246 | 247 | if ( ! boneData.pos ) { 248 | 249 | boneData.pos = { 250 | times: new Float32Array( numFrames ), 251 | values: new Float32Array( numFrames * 3 ) 252 | }; 253 | 254 | } 255 | 256 | if ( options.useFirstFramePosition ) { 257 | 258 | if ( i === 0 ) { 259 | 260 | positionOffset = bone.position.clone(); 261 | 262 | } 263 | 264 | bone.position.sub( positionOffset ); 265 | 266 | } 267 | 268 | boneData.pos.times[ i ] = time; 269 | 270 | bone.position.toArray( boneData.pos.values, i * 3 ); 271 | 272 | } 273 | 274 | if ( ! boneData.quat ) { 275 | 276 | boneData.quat = { 277 | times: new Float32Array( numFrames ), 278 | values: new Float32Array( numFrames * 4 ) 279 | }; 280 | 281 | } 282 | 283 | boneData.quat.times[ i ] = time; 284 | 285 | bone.quaternion.toArray( boneData.quat.values, i * 4 ); 286 | 287 | } 288 | 289 | } 290 | 291 | mixer.update( delta ); 292 | 293 | source.updateMatrixWorld(); 294 | 295 | } 296 | 297 | for ( i = 0; i < boneDatas.length; ++ i ) { 298 | 299 | boneData = boneDatas[ i ]; 300 | 301 | if ( boneData ) { 302 | 303 | if ( boneData.pos ) { 304 | 305 | convertedTracks.push( new THREE.VectorKeyframeTrack( 306 | ".bones[" + boneData.bone.name + "].position", 307 | boneData.pos.times, 308 | boneData.pos.values 309 | ) ); 310 | 311 | } 312 | 313 | convertedTracks.push( new THREE.QuaternionKeyframeTrack( 314 | ".bones[" + boneData.bone.name + "].quaternion", 315 | boneData.quat.times, 316 | boneData.quat.values 317 | ) ); 318 | 319 | } 320 | 321 | } 322 | 323 | mixer.uncacheAction( clip ); 324 | 325 | return new THREE.AnimationClip( clip.name, - 1, convertedTracks ); 326 | 327 | }, 328 | 329 | getHelperFromSkeleton: function ( skeleton ) { 330 | 331 | var source = new THREE.SkeletonHelper( skeleton.bones[ 0 ] ); 332 | source.skeleton = skeleton; 333 | 334 | return source; 335 | 336 | }, 337 | 338 | getSkeletonOffsets: function () { 339 | 340 | var targetParentPos = new THREE.Vector3(), 341 | targetPos = new THREE.Vector3(), 342 | sourceParentPos = new THREE.Vector3(), 343 | sourcePos = new THREE.Vector3(), 344 | targetDir = new THREE.Vector2(), 345 | sourceDir = new THREE.Vector2(); 346 | 347 | return function ( target, source, options ) { 348 | 349 | options = options || {}; 350 | options.hip = options.hip !== undefined ? options.hip : "hip"; 351 | options.names = options.names || {}; 352 | 353 | if ( ! source.isObject3D ) { 354 | 355 | source = this.getHelperFromSkeleton( source ); 356 | 357 | } 358 | 359 | var nameKeys = Object.keys( options.names ), 360 | nameValues = Object.values( options.names ), 361 | sourceBones = source.isObject3D ? source.skeleton.bones : this.getBones( source ), 362 | bones = target.isObject3D ? target.skeleton.bones : this.getBones( target ), 363 | offsets = [], 364 | bone, boneTo, 365 | name, i; 366 | 367 | target.skeleton.pose(); 368 | 369 | for ( i = 0; i < bones.length; ++ i ) { 370 | 371 | bone = bones[ i ]; 372 | name = options.names[ bone.name ] || bone.name; 373 | 374 | boneTo = this.getBoneByName( name, sourceBones ); 375 | 376 | if ( boneTo && name !== options.hip ) { 377 | 378 | var boneParent = this.getNearestBone( bone.parent, nameKeys ), 379 | boneToParent = this.getNearestBone( boneTo.parent, nameValues ); 380 | 381 | boneParent.updateMatrixWorld(); 382 | boneToParent.updateMatrixWorld(); 383 | 384 | targetParentPos.setFromMatrixPosition( boneParent.matrixWorld ); 385 | targetPos.setFromMatrixPosition( bone.matrixWorld ); 386 | 387 | sourceParentPos.setFromMatrixPosition( boneToParent.matrixWorld ); 388 | sourcePos.setFromMatrixPosition( boneTo.matrixWorld ); 389 | 390 | targetDir.subVectors( 391 | new THREE.Vector2( targetPos.x, targetPos.y ), 392 | new THREE.Vector2( targetParentPos.x, targetParentPos.y ) 393 | ).normalize(); 394 | 395 | sourceDir.subVectors( 396 | new THREE.Vector2( sourcePos.x, sourcePos.y ), 397 | new THREE.Vector2( sourceParentPos.x, sourceParentPos.y ) 398 | ).normalize(); 399 | 400 | var laterialAngle = targetDir.angle() - sourceDir.angle(); 401 | 402 | var offset = new THREE.Matrix4().makeRotationFromEuler( 403 | new THREE.Euler( 404 | 0, 405 | 0, 406 | laterialAngle 407 | ) 408 | ); 409 | 410 | bone.matrix.multiply( offset ); 411 | 412 | bone.matrix.decompose( bone.position, bone.quaternion, bone.scale ); 413 | 414 | bone.updateMatrixWorld(); 415 | 416 | offsets[ name ] = offset; 417 | 418 | } 419 | 420 | } 421 | 422 | return offsets; 423 | 424 | }; 425 | 426 | }(), 427 | 428 | renameBones: function ( skeleton, names ) { 429 | 430 | var bones = this.getBones( skeleton ); 431 | 432 | for ( var i = 0; i < bones.length; ++ i ) { 433 | 434 | var bone = bones[ i ]; 435 | 436 | if ( names[ bone.name ] ) { 437 | 438 | bone.name = names[ bone.name ]; 439 | 440 | } 441 | 442 | } 443 | 444 | return this; 445 | 446 | }, 447 | 448 | getBones: function ( skeleton ) { 449 | 450 | return Array.isArray( skeleton ) ? skeleton : skeleton.bones; 451 | 452 | }, 453 | 454 | getBoneByName: function ( name, skeleton ) { 455 | 456 | for ( var i = 0, bones = this.getBones( skeleton ); i < bones.length; i ++ ) { 457 | 458 | if ( name === bones[ i ].name ) 459 | 460 | return bones[ i ]; 461 | 462 | } 463 | 464 | }, 465 | 466 | getNearestBone: function ( bone, names ) { 467 | 468 | while ( bone.isBone ) { 469 | 470 | if ( names.indexOf( bone.name ) !== - 1 ) { 471 | 472 | return bone; 473 | 474 | } 475 | 476 | bone = bone.parent; 477 | 478 | } 479 | 480 | }, 481 | 482 | findBoneTrackData: function ( name, tracks ) { 483 | 484 | var regexp = /\[(.*)\]\.(.*)/, 485 | result = { name: name }; 486 | 487 | for ( var i = 0; i < tracks.length; ++ i ) { 488 | 489 | // 1 is track name 490 | // 2 is track type 491 | var trackData = regexp.exec( tracks[ i ].name ); 492 | 493 | if ( trackData && name === trackData[ 1 ] ) { 494 | 495 | result[ trackData[ 2 ] ] = i; 496 | 497 | } 498 | 499 | } 500 | 501 | return result; 502 | 503 | }, 504 | 505 | getEqualsBonesNames: function ( skeleton, targetSkeleton ) { 506 | 507 | var sourceBones = this.getBones( skeleton ), 508 | targetBones = this.getBones( targetSkeleton ), 509 | bones = []; 510 | 511 | search : for ( var i = 0; i < sourceBones.length; i ++ ) { 512 | 513 | var boneName = sourceBones[ i ].name; 514 | 515 | for ( var j = 0; j < targetBones.length; j ++ ) { 516 | 517 | if ( boneName === targetBones[ j ].name ) { 518 | 519 | bones.push( boneName ); 520 | 521 | continue search; 522 | 523 | } 524 | 525 | } 526 | 527 | } 528 | 529 | return bones; 530 | 531 | }, 532 | 533 | clone: function ( source ) { 534 | 535 | var sourceLookup = new Map(); 536 | var cloneLookup = new Map(); 537 | 538 | var clone = source.clone(); 539 | 540 | parallelTraverse( source, clone, function ( sourceNode, clonedNode ) { 541 | 542 | sourceLookup.set( clonedNode, sourceNode ); 543 | cloneLookup.set( sourceNode, clonedNode ); 544 | 545 | } ); 546 | 547 | clone.traverse( function ( node ) { 548 | 549 | if ( ! node.isSkinnedMesh ) return; 550 | 551 | var clonedMesh = node; 552 | var sourceMesh = sourceLookup.get( node ); 553 | var sourceBones = sourceMesh.skeleton.bones; 554 | 555 | clonedMesh.skeleton = sourceMesh.skeleton.clone(); 556 | clonedMesh.bindMatrix.copy( sourceMesh.bindMatrix ); 557 | 558 | clonedMesh.skeleton.bones = sourceBones.map( function ( bone ) { 559 | 560 | return cloneLookup.get( bone ); 561 | 562 | } ); 563 | 564 | clonedMesh.bind( clonedMesh.skeleton, clonedMesh.bindMatrix ); 565 | 566 | } ); 567 | 568 | return clone; 569 | 570 | } 571 | 572 | }; 573 | 574 | 575 | function parallelTraverse( a, b, callback ) { 576 | 577 | callback( a, b ); 578 | 579 | for ( var i = 0; i < a.children.length; i ++ ) { 580 | 581 | parallelTraverse( a.children[ i ], b.children[ i ], callback ); 582 | 583 | } 584 | 585 | } 586 | 587 | export default SkeletonUtils; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | height: 100%; 5 | margin: 0px; 6 | } 7 | 8 | body { 9 | overflow: hidden; 10 | position: fixed; 11 | margin: 0; 12 | padding: 0; 13 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", 14 | "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", 15 | "Helvetica Neue", sans-serif; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | } 19 | 20 | code { 21 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 22 | } 23 | 24 | .game { 25 | display: flex; 26 | flex-direction: column; 27 | height: 100%; 28 | align-items: center; 29 | } 30 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Game from "./game"; 4 | import * as ServiceWorker from './service-worker'; 5 | import './index.css'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | ServiceWorker.register(); 13 | -------------------------------------------------------------------------------- /src/service-worker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | --------------------------------------------------------------------------------