├── .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 | You need to enable JavaScript to run this app.
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 |
--------------------------------------------------------------------------------