├── .gitignore ├── .watchmanconfig ├── LICENSE ├── README.md ├── app.json ├── babel.config.js ├── metro.config.js ├── package-lock.json ├── package.json └── src ├── app.js ├── 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 ├── particles.js ├── physics.js ├── removal.js ├── rotation.js ├── spawn.js ├── spring.js ├── timeline.js └── touch-controller.js └── utils ├── index.js ├── perf-timer.js ├── perlin.js └── three ├── index.js └── skeleton-utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p12 6 | *.key 7 | *.mobileprovision 8 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /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 Native 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 Native Game Engine](https://github.com/bberak/react-native-game-engine). 4 | 5 | The underlying renderer is [ThreeJS](https://github.com/mrdoob/three.js) which has been extended with [Expo-Three](https://github.com/expo/expo-three). 6 | 7 | The template will contain both 3D and 2D game entities (sprites) and potentially some particles. 8 | 9 | This project uses [Expo](https://expo.io) because quite frankly, it is the easiest and most stable way to get up and running with a React Native project for both iOS and Android with all the OpenGL/WebGL goodness ready to go out of the box. 10 | 11 | ## How to start 12 | 13 | Firstly, clone the repo and configure git tracking: 14 | 15 | ``` 16 | git clone https://github.com/bberak/react-native-game-engine-template.git [new-game] 17 | 18 | cd [new-game] 19 | 20 | rm -rf .git # Windows: rmdir /S .git 21 | 22 | git init 23 | 24 | git add . 25 | 26 | git commit -m "First commit" 27 | 28 | git remote add origin https://github.com/[you]/[new-game].git 29 | 30 | git push -u origin master 31 | 32 | ``` 33 | 34 | Then, install the dependencies and start the app: 35 | 36 | ``` 37 | npm install 38 | 39 | npm install -g expo-cli 40 | 41 | npm run start 42 | ``` 43 | 44 | This template contains the following: 45 | 46 | - Stick (Gamepad) controllers 47 | - A simple HUD 48 | - Particle systems 49 | - Sound support 50 | - Physics implementation powered by [Oimo](https://github.com/lo-th/Oimo.js/) 51 | - [ThreeJS](https://github.com/mrdoob/three.js) rendering 52 | - Post-processing effects 53 | - Sprite support with animations 54 | 55 | > All of the above systems and components are hackable and extensible - which *should* allow for quick[er] prototyping. 56 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "React Native Game Engine Template", 4 | "slug": "react-native-game-engine-template", 5 | "privacy": "public", 6 | "platforms": ["ios", "android"], 7 | "version": "1.0.0", 8 | "orientation": "landscape", 9 | "icon": "./src/assets/icon.png", 10 | "splash": { 11 | "image": "./src/assets/splash.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "updates": { 16 | "fallbackToCacheTimeout": 0 17 | }, 18 | "assetBundlePatterns": ["assets/**/*"], 19 | "ios": { 20 | "supportsTablet": true 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | const defaultAssetExts = require("metro-config/src/defaults/defaults").assetExts; 2 | 3 | module.exports = { 4 | resolver: { 5 | assetExts: [ 6 | ...defaultAssetExts, 7 | "glb", 8 | "fbx", 9 | "wav", 10 | "mp3" 11 | ] 12 | } 13 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "src/app.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "eject": "expo eject" 8 | }, 9 | "dependencies": { 10 | "@popmotion/popcorn": "^0.4.0", 11 | "expo": "^37.0.0", 12 | "expo-graphics-rnge": "^1.2.0", 13 | "expo-three": "^4.0.5", 14 | "lodash": "^4.17.11", 15 | "oimo": "^1.0.9", 16 | "react": "16.9.0", 17 | "react-native": "https://github.com/expo/react-native/archive/sdk-37.0.1.tar.gz", 18 | "react-native-game-engine": "^1.0.0", 19 | "expo-av": "~8.1.0", 20 | "expo-gl": "~8.1.0" 21 | }, 22 | "devDependencies": { 23 | "babel-preset-expo": "^8.1.0", 24 | "cross-env": "^5.2.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Text, View } from 'react-native'; 3 | import { registerRootComponent } from 'expo'; 4 | import Game from "./game"; 5 | 6 | class App extends React.Component { 7 | render() { 8 | return ( 9 | 10 | ); 11 | } 12 | } 13 | 14 | registerRootComponent(App); 15 | -------------------------------------------------------------------------------- /src/assets/aseprite/bullet-01.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-native-game-engine-template/cbbbeb69b68358f7225267ee1daf02c57fd42362/src/assets/aseprite/bullet-01.ase -------------------------------------------------------------------------------- /src/assets/audio/crash-01.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-native-game-engine-template/cbbbeb69b68358f7225267ee1daf02c57fd42362/src/assets/audio/crash-01.wav -------------------------------------------------------------------------------- /src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-native-game-engine-template/cbbbeb69b68358f7225267ee1daf02c57fd42362/src/assets/icon.png -------------------------------------------------------------------------------- /src/assets/models/droid.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-native-game-engine-template/cbbbeb69b68358f7225267ee1daf02c57fd42362/src/assets/models/droid.fbx -------------------------------------------------------------------------------- /src/assets/models/jet.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-native-game-engine-template/cbbbeb69b68358f7225267ee1daf02c57fd42362/src/assets/models/jet.glb -------------------------------------------------------------------------------- /src/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-native-game-engine-template/cbbbeb69b68358f7225267ee1daf02c57fd42362/src/assets/splash.png -------------------------------------------------------------------------------- /src/assets/sprite-sheets/bullet-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-native-game-engine-template/cbbbeb69b68358f7225267ee1daf02c57fd42362/src/assets/sprite-sheets/bullet-01.png -------------------------------------------------------------------------------- /src/assets/sprite-sheets/cuphead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-native-game-engine-template/cbbbeb69b68358f7225267ee1daf02c57fd42362/src/assets/sprite-sheets/cuphead.png -------------------------------------------------------------------------------- /src/assets/sprite-sheets/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-native-game-engine-template/cbbbeb69b68358f7225267ee1daf02c57fd42362/src/assets/sprite-sheets/player.png -------------------------------------------------------------------------------- /src/assets/textures/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-native-game-engine-template/cbbbeb69b68358f7225267ee1daf02c57fd42362/src/assets/textures/grid.png -------------------------------------------------------------------------------- /src/assets/textures/particle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-native-game-engine-template/cbbbeb69b68358f7225267ee1daf02c57fd42362/src/assets/textures/particle.png -------------------------------------------------------------------------------- /src/assets/textures/perlin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bberak/react-native-game-engine-template/cbbbeb69b68358f7225267ee1daf02c57fd42362/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 { THREE } from "expo-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(); 61 | } 62 | }, 63 | removable: (frustum, self) => !frustum.intersectsObject(self.model) 64 | }; 65 | }; 66 | -------------------------------------------------------------------------------- /src/game/components/camera.js: -------------------------------------------------------------------------------- 1 | import { THREE } from "expo-three"; 2 | import { screen, remap } from "../utils"; 3 | import { noise } from "../utils/perlin"; 4 | 5 | export default () => { 6 | const camera = new THREE.PerspectiveCamera( 7 | 90, 8 | screen.width / screen.height, 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 ExpoTHREE, { THREE } from "expo-three"; 2 | import Sprite from "./sprite"; 3 | import { between } from "../utils"; 4 | import CupheadFile from "../../assets/sprite-sheets/cuphead.png"; 5 | 6 | const spriteSheet = ExpoTHREE.loadAsync(CupheadFile); 7 | 8 | export default async ({ parent, x = 0, y = 0, z = 0}) => { 9 | 10 | const sprite = await Sprite({ 11 | parent, 12 | x, 13 | y, 14 | z, 15 | spriteSheet, 16 | columns: 16, 17 | rows: 8, 18 | actions: { 19 | idle: { 20 | start: { row: 2, column: 0 } 21 | }, 22 | jump: { 23 | start: { row: 0, column: 0 }, 24 | end: { row: 0, column: 9 }, 25 | loop: false 26 | }, 27 | s: { 28 | start: { row: 1, column: 0 }, 29 | end: { row: 1, column: 12 } 30 | }, 31 | se: { 32 | start: { row: 3, column: 0 }, 33 | end: { row: 3, column: 15 } 34 | }, 35 | e: { 36 | start: { row: 4, column: 0 }, 37 | end: { row: 4, column: 13 } 38 | }, 39 | ne: { 40 | start: { row: 6, column: 0 }, 41 | end: { row: 6, column: 14 } 42 | }, 43 | n: { 44 | start: { row: 7, column: 1 }, 45 | end: { row: 7, column: 15 } 46 | }, 47 | nw: { 48 | start: { row: 6, column: 0 }, 49 | end: { row: 6, column: 14 }, 50 | flipX: true 51 | }, 52 | w: { 53 | start: { row: 4, column: 0 }, 54 | end: { row: 4, column: 13 }, 55 | flipX: true 56 | }, 57 | sw: { 58 | start: { row: 3, column: 0 }, 59 | end: { row: 3, column: 15 }, 60 | flipX: true 61 | } 62 | } 63 | }); 64 | 65 | sprite.timelines.controls = { 66 | while: true, 67 | directions: [ 68 | { heading: 0, action: "e" }, 69 | { heading: -45, action: "ne" }, 70 | { heading: -90, action: "n" }, 71 | { heading: -135, action: "nw" }, 72 | { heading: -180, action: "w" }, 73 | { heading: 45, action: "se" }, 74 | { heading: 90, action: "s" }, 75 | { heading: 135, action: "sw" }, 76 | { heading: 180, action: "w" } 77 | ], 78 | update(self, entities, { directions }, { gamepadController }) { 79 | if (gamepadController.heading !== null ) { 80 | const degrees = THREE.Math.radToDeg(gamepadController.heading) 81 | const direction = directions.find(x => between(degrees, x.heading - 25, x.heading + 25)) 82 | 83 | self.actions[direction.action]() 84 | } else self.actions.idle(); 85 | } 86 | }; 87 | 88 | return sprite; 89 | }; 90 | -------------------------------------------------------------------------------- /src/game/components/cylinder.js: -------------------------------------------------------------------------------- 1 | import { THREE } from "expo-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 ExpoTHREE from "expo-three"; 2 | import AnimatedModel from "./animated-model"; 3 | import DroidFile from "../../assets/models/droid.fbx" 4 | 5 | const mesh = ExpoTHREE.loadAsync(DroidFile); 6 | 7 | export default async ({ parent, x = 0, y = 0, z = 0}) => { 8 | 9 | const animated = await AnimatedModel({ parent, x, y, z, mesh, scale: 0.0035 }) 10 | 11 | return animated; 12 | }; 13 | -------------------------------------------------------------------------------- /src/game/components/hud.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { StyleSheet, View } from "react-native"; 3 | 4 | class HUDRenderer extends React.Component { 5 | shouldComponentUpdate(nextProps) { 6 | const g1 = this.props.gamepadController || {}; 7 | const g2 = nextProps.gamepadController || {}; 8 | 9 | return Boolean(g1.x || g1.y) !== Boolean(g2.x || g2.y) || g1.a !== g2.a || g1.b !== g2.b; 10 | } 11 | 12 | render() { 13 | const { 14 | stickRadius = 0, 15 | stickPosition = { x: 0, y: 0 }, 16 | aRadius = 0, 17 | aPosition = { x: 0, y: 0 }, 18 | bRadius = 0, 19 | bPosition = { x: 0, y: 0 }, 20 | a = false, 21 | b = false, 22 | x = 0, 23 | y = 0 24 | } = this.props.gamepadController || {}; 25 | 26 | const usingStick = x || y; 27 | 28 | return [ 29 | , 43 | , 57 | , 71 | 85 | ]; 86 | } 87 | } 88 | 89 | const styles = StyleSheet.create({ 90 | container: { 91 | position: "absolute", 92 | backgroundColor: "transparent", 93 | borderWidth: 5, 94 | borderColor: "white", 95 | opacity: 0.25 96 | } 97 | }); 98 | 99 | export default () => { 100 | return { renderer: }; 101 | }; 102 | -------------------------------------------------------------------------------- /src/game/components/jet.js: -------------------------------------------------------------------------------- 1 | import ExpoTHREE, { THREE } from "expo-three"; 2 | import AnimatedModel from "./animated-model"; 3 | import { firstMesh } from "../utils/three"; 4 | import { between } from "../utils"; 5 | import JetFile from "../../assets/models/jet.glb"; 6 | 7 | const mesh = ExpoTHREE.loadAsync(JetFile).then(gltf => firstMesh(gltf.scene)); 8 | 9 | export default async ({ parent, x = 0, y = 0, z = 0}) => { 10 | 11 | const animated = await AnimatedModel({ 12 | parent, 13 | x, 14 | y, 15 | z, 16 | mesh, 17 | morphTargets: { 18 | rudderLeft: 0, 19 | rudderRight: 1, 20 | leftFlapUp: 2, 21 | leftFlapDown: 3, 22 | rightFlapUp: 4, 23 | rightFlapDown: 5 24 | } 25 | }); 26 | 27 | const timelines = {}; 28 | 29 | timelines.controls = { 30 | while: true, 31 | directions: [ 32 | { heading: 0, pose: "rudderRight" }, 33 | { heading: -60, pose: "leftFlapDown" }, 34 | { heading: -120, pose: "leftFlapUp" }, 35 | { heading: -180, pose: "rudderLeft" }, 36 | { heading: 60, pose: "rightFlapUp" }, 37 | { heading: 120, pose: "rightFlapDown" }, 38 | { heading: 180, pose: "rudderLeft" } 39 | ], 40 | update(self, entities, { directions }, { gamepadController }) { 41 | let target = null; 42 | 43 | if (gamepadController.heading !== null ) { 44 | const degrees = THREE.Math.radToDeg(gamepadController.heading) 45 | const direction = directions.find(x => between(degrees, x.heading - 30, x.heading + 30)) 46 | 47 | if (direction) 48 | target = direction.pose; 49 | } 50 | 51 | directions.forEach(x => { 52 | const pose = self.poses[x.pose]; 53 | const val = pose(); 54 | 55 | pose(val + (x.pose === target ? 0.01 : -0.01)) 56 | }); 57 | } 58 | }; 59 | 60 | return { ...animated, ...{ timelines }}; 61 | }; 62 | -------------------------------------------------------------------------------- /src/game/components/particles.js: -------------------------------------------------------------------------------- 1 | import ExpoTHREE from "expo-three"; 2 | import GPUParticleSystem from "../graphics/gpu-particle-system"; 3 | import { add } from "../utils/three"; 4 | import NoiseFile from "../../assets/textures/perlin.png"; 5 | 6 | const _noiseTexture = ExpoTHREE.loadAsync(NoiseFile); 7 | 8 | export default async ({ 9 | maxParticles = 250, 10 | noiseTexture, 11 | particleTexture, 12 | parent, 13 | options = {}, 14 | spawnOptions = {}, 15 | beforeSpawn = () => {} 16 | }) => { 17 | const emitter = new GPUParticleSystem({ 18 | maxParticles, 19 | particleNoiseTex: await Promise.resolve(noiseTexture || _noiseTexture), 20 | particleSpriteTex: await Promise.resolve(particleTexture) 21 | }); 22 | 23 | add(parent, emitter); 24 | 25 | return { 26 | emitter, 27 | options, 28 | spawnOptions, 29 | beforeSpawn, 30 | tick: 0 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /src/game/components/portal.js: -------------------------------------------------------------------------------- 1 | import ExpoTHREE, { THREE } from "expo-three"; 2 | import Particles from "./particles"; 3 | import ParticleFile from "../../assets/textures/particle.png"; 4 | 5 | const particleTexture = ExpoTHREE.loadAsync(ParticleFile); 6 | 7 | export default async ({ 8 | parent, 9 | x = 0, 10 | y = 0, 11 | z = 0, 12 | height = 0.5, 13 | radius = 0.5, 14 | verticalSpeed = 0.01, 15 | horizontalSpeed = 0.3, 16 | color = 0xffffff 17 | }) => { 18 | 19 | const swirl = await Particles({ 20 | parent, 21 | particleTexture, 22 | maxParticles: 250, 23 | options: { 24 | position: new THREE.Vector3(x, y, z), 25 | positionRandomness: 0, 26 | velocity: new THREE.Vector3(), 27 | velocityRandomness: 0, 28 | color, 29 | colorRandomness: 0, 30 | turbulence: 0, 31 | lifetime: 12, 32 | size: 10, 33 | sizeRandomness: 0, 34 | verticalSpeed, 35 | theta: 0 36 | }, 37 | spawnOptions: { 38 | spawnRate: 20, 39 | timeScale: 1 40 | }, 41 | beforeSpawn(self, entities, { options }) { 42 | options.theta += horizontalSpeed; 43 | options.position.x = x + Math.cos(options.theta) * radius; 44 | options.position.y += options.verticalSpeed; 45 | options.position.z = z + Math.sin(options.theta) * radius; 46 | 47 | if (Math.abs(options.position.y - y) > height) 48 | options.verticalSpeed *= -1; 49 | } 50 | }); 51 | 52 | return { 53 | model: swirl.emitter, 54 | particles: { 55 | swirl 56 | } 57 | }; 58 | }; 59 | -------------------------------------------------------------------------------- /src/game/components/sprite.js: -------------------------------------------------------------------------------- 1 | import { THREE } from "expo-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 { THREE } from 'expo-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, { touchController, gamepadController }) { 57 | if (gamepadController.heading !== null || gamepadController.a || gamepadController.b) 58 | return; 59 | 60 | if (touchController.singleFingerMovement.x) 61 | self.bodies[0].angularVelocity.set(0, touchController.singleFingerMovement.x * 0.1, 0) 62 | else if (touchController.start) 63 | self.bodies[0].angularVelocity.set(0, 0, 0) 64 | } 65 | } 66 | } 67 | } 68 | }; -------------------------------------------------------------------------------- /src/game/entities.js: -------------------------------------------------------------------------------- 1 | import { THREE } from 'expo-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 | }; -------------------------------------------------------------------------------- /src/game/graphics/effect-composer.js: -------------------------------------------------------------------------------- 1 | import { THREE } from 'expo-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 | THREE.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 ( THREE.CopyShader === undefined ) { 43 | 44 | console.error( 'THREE.EffectComposer relies on THREE.CopyShader' ); 45 | 46 | } 47 | 48 | if ( THREE.ShaderPass === undefined ) { 49 | 50 | console.error( 'THREE.EffectComposer relies on THREE.ShaderPass' ); 51 | 52 | } 53 | 54 | this.copyPass = new THREE.ShaderPass( THREE.CopyShader ); 55 | 56 | }; 57 | 58 | Object.assign( THREE.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 ( THREE.MaskPass !== undefined ) { 117 | 118 | if ( pass instanceof THREE.MaskPass ) { 119 | 120 | maskActive = true; 121 | 122 | } else if ( pass instanceof THREE.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 THREE.EffectComposer; -------------------------------------------------------------------------------- /src/game/graphics/gpu-particle-system.js: -------------------------------------------------------------------------------- 1 | import { THREE } from 'expo-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 | THREE.Object3D.apply(this, arguments); 24 | 25 | options = options || {}; 26 | 27 | // parse options and use defaults 28 | 29 | this.PARTICLE_COUNT = options.maxParticles || 1000000; 30 | this.PARTICLE_CONTAINERS = options.containerCount || 1; 31 | 32 | this.PARTICLE_NOISE_TEXTURE = options.particleNoiseTex || null; 33 | this.PARTICLE_SPRITE_TEXTURE = options.particleSpriteTex || null; 34 | 35 | this.PARTICLES_PER_CONTAINER = Math.ceil( 36 | this.PARTICLE_COUNT / this.PARTICLE_CONTAINERS 37 | ); 38 | this.PARTICLE_CURSOR = 0; 39 | this.time = 0; 40 | this.particleContainers = []; 41 | this.rand = []; 42 | 43 | // custom vertex and fragement shader 44 | 45 | var GPUParticleShader = { 46 | vertexShader: ` 47 | uniform float uTime; 48 | uniform float uScale; 49 | uniform sampler2D tNoise; 50 | 51 | attribute vec3 positionStart; 52 | attribute float startTime; 53 | attribute vec3 velocity; 54 | attribute float turbulence; 55 | attribute vec3 color; 56 | attribute float size; 57 | attribute float lifeTime; 58 | 59 | varying vec4 vColor; 60 | varying float lifeLeft; 61 | 62 | void main() { 63 | // unpack things from our attributes' 64 | 65 | vColor = vec4( color, 1.0 ); 66 | 67 | // convert our velocity back into a value we can use' 68 | 69 | vec3 newPosition; 70 | vec3 v; 71 | 72 | float timeElapsed = uTime - startTime; 73 | 74 | lifeLeft = 1.0 - ( timeElapsed / lifeTime ); 75 | 76 | gl_PointSize = ( uScale * size ) * lifeLeft; 77 | 78 | v.x = ( velocity.x - 0.5 ) * 3.0; 79 | v.y = ( velocity.y - 0.5 ) * 3.0; 80 | v.z = ( velocity.z - 0.5 ) * 3.0; 81 | 82 | newPosition = positionStart + ( v * 10.0 ) * timeElapsed; 83 | 84 | vec3 noise = texture2D( tNoise, vec2( newPosition.x * 0.015 + ( uTime * 0.05 ), newPosition.y * 0.02 + ( uTime * 0.015 ) ) ).rgb; 85 | vec3 noiseVel = ( noise.rgb - 0.5 ) * 30.0; 86 | 87 | newPosition = mix( newPosition, newPosition + vec3( noiseVel * ( turbulence * 5.0 ) ), ( timeElapsed / lifeTime ) ); 88 | 89 | if( v.y > 0. && v.y < .05 ) { 90 | lifeLeft = 0.0; 91 | } 92 | 93 | if( v.x < - 1.45 ) { 94 | lifeLeft = 0.0; 95 | } 96 | 97 | if( timeElapsed > 0.0 ) { 98 | gl_Position = projectionMatrix * modelViewMatrix * vec4( newPosition, 1.0 ); 99 | } else { 100 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 101 | lifeLeft = 0.0; 102 | gl_PointSize = 0.; 103 | } 104 | }`, 105 | 106 | fragmentShader: ` 107 | float scaleLinear( float value, vec2 valueDomain ) { 108 | return ( value - valueDomain.x ) / ( valueDomain.y - valueDomain.x ); 109 | } 110 | 111 | float scaleLinear( float value, vec2 valueDomain, vec2 valueRange ) { 112 | return mix( valueRange.x, valueRange.y, scaleLinear( value, valueDomain ) ); 113 | } 114 | 115 | varying vec4 vColor; 116 | varying float lifeLeft; 117 | 118 | uniform sampler2D tSprite; 119 | 120 | void main() { 121 | float alpha = 0.; 122 | 123 | if( lifeLeft > 0.995 ) { 124 | alpha = scaleLinear( lifeLeft, vec2( 1.0, 0.995 ), vec2( 0.0, 1.0 ) ); 125 | } else { 126 | alpha = lifeLeft * 0.75; 127 | } 128 | 129 | vec4 tex = texture2D( tSprite, gl_PointCoord ); 130 | gl_FragColor = vec4( vColor.rgb * tex.a, alpha * tex.a ); 131 | } 132 | ` 133 | }; 134 | 135 | // preload a million random numbers 136 | 137 | var i; 138 | 139 | for (i = 1e5; i > 0; i--) { 140 | this.rand.push(Math.random() - 0.5); 141 | } 142 | 143 | this.random = function() { 144 | return ++i >= this.rand.length ? this.rand[(i = 1)] : this.rand[i]; 145 | }; 146 | 147 | this.particleNoiseTex = this.PARTICLE_NOISE_TEXTURE; 148 | this.particleNoiseTex.wrapS = this.particleNoiseTex.wrapT = THREE.RepeatWrapping; 149 | 150 | this.particleSpriteTex = this.PARTICLE_SPRITE_TEXTURE; 151 | this.particleSpriteTex.wrapS = this.particleSpriteTex.wrapT = THREE.RepeatWrapping; 152 | 153 | this.particleShaderMat = new THREE.ShaderMaterial({ 154 | transparent: true, 155 | depthWrite: false, 156 | uniforms: { 157 | uTime: { 158 | value: 0.0 159 | }, 160 | uScale: { 161 | value: 1.0 162 | }, 163 | tNoise: { 164 | value: this.particleNoiseTex 165 | }, 166 | tSprite: { 167 | value: this.particleSpriteTex 168 | } 169 | }, 170 | blending: THREE.AdditiveBlending, 171 | vertexShader: GPUParticleShader.vertexShader, 172 | fragmentShader: GPUParticleShader.fragmentShader 173 | }); 174 | 175 | // define defaults for all values 176 | 177 | this.particleShaderMat.defaultAttributeValues.particlePositionsStartTime = [ 178 | 0, 179 | 0, 180 | 0, 181 | 0 182 | ]; 183 | this.particleShaderMat.defaultAttributeValues.particleVelColSizeLife = [ 184 | 0, 185 | 0, 186 | 0, 187 | 0 188 | ]; 189 | 190 | this.init = function() { 191 | for (var i = 0; i < this.PARTICLE_CONTAINERS; i++) { 192 | var c = new GPUParticleContainer( 193 | this.PARTICLES_PER_CONTAINER, 194 | this 195 | ); 196 | this.particleContainers.push(c); 197 | this.add(c); 198 | } 199 | }; 200 | 201 | this.spawnParticle = function(options) { 202 | this.PARTICLE_CURSOR++; 203 | 204 | if (this.PARTICLE_CURSOR >= this.PARTICLE_COUNT) { 205 | this.PARTICLE_CURSOR = 1; 206 | } 207 | 208 | var currentContainer = this.particleContainers[ 209 | Math.floor(this.PARTICLE_CURSOR / this.PARTICLES_PER_CONTAINER) 210 | ]; 211 | 212 | currentContainer.spawnParticle(options); 213 | }; 214 | 215 | this.update = function(time) { 216 | for (var i = 0; i < this.PARTICLE_CONTAINERS; i++) { 217 | this.particleContainers[i].update(time); 218 | } 219 | }; 220 | 221 | this.dispose = function() { 222 | this.particleShaderMat.dispose(); 223 | this.particleNoiseTex.dispose(); 224 | this.particleSpriteTex.dispose(); 225 | 226 | for (var i = 0; i < this.PARTICLE_CONTAINERS; i++) { 227 | this.particleContainers[i].dispose(); 228 | } 229 | }; 230 | 231 | this.init(); 232 | }; 233 | 234 | GPUParticleSystem.prototype = Object.create( THREE.Object3D.prototype ); 235 | GPUParticleSystem.prototype.constructor = GPUParticleSystem; 236 | 237 | // Subclass for particle containers, allows for very large arrays to be spread out 238 | 239 | const GPUParticleContainer = function ( maxParticles, particleSystem ) { 240 | THREE.Object3D.apply( this, arguments ); 241 | 242 | this.PARTICLE_COUNT = maxParticles || 100000; 243 | this.PARTICLE_CURSOR = 0; 244 | this.time = 0; 245 | this.offset = 0; 246 | this.count = 0; 247 | this.DPR = window.devicePixelRatio; 248 | this.GPUParticleSystem = particleSystem; 249 | this.particleUpdate = false; 250 | 251 | // geometry 252 | 253 | this.particleShaderGeo = new THREE.BufferGeometry(); 254 | 255 | this.particleShaderGeo.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT * 3 ), 3 ).setDynamic( true ) ); 256 | this.particleShaderGeo.addAttribute( 'positionStart', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT * 3 ), 3 ).setDynamic( true ) ); 257 | this.particleShaderGeo.addAttribute( 'startTime', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT ), 1 ).setDynamic( true ) ); 258 | this.particleShaderGeo.addAttribute( 'velocity', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT * 3 ), 3 ).setDynamic( true ) ); 259 | this.particleShaderGeo.addAttribute( 'turbulence', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT ), 1 ).setDynamic( true ) ); 260 | this.particleShaderGeo.addAttribute( 'color', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT * 3 ), 3 ).setDynamic( true ) ); 261 | this.particleShaderGeo.addAttribute( 'size', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT ), 1 ).setDynamic( true ) ); 262 | this.particleShaderGeo.addAttribute( 'lifeTime', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT ), 1 ).setDynamic( true ) ); 263 | 264 | // material 265 | 266 | this.particleShaderMat = this.GPUParticleSystem.particleShaderMat; 267 | 268 | var position = new THREE.Vector3(); 269 | var velocity = new THREE.Vector3(); 270 | var color = new THREE.Color(); 271 | 272 | this.spawnParticle = function ( options ) { 273 | var positionStartAttribute = this.particleShaderGeo.getAttribute( 'positionStart' ); 274 | var startTimeAttribute = this.particleShaderGeo.getAttribute( 'startTime' ); 275 | var velocityAttribute = this.particleShaderGeo.getAttribute( 'velocity' ); 276 | var turbulenceAttribute = this.particleShaderGeo.getAttribute( 'turbulence' ); 277 | var colorAttribute = this.particleShaderGeo.getAttribute( 'color' ); 278 | var sizeAttribute = this.particleShaderGeo.getAttribute( 'size' ); 279 | var lifeTimeAttribute = this.particleShaderGeo.getAttribute( 'lifeTime' ); 280 | 281 | options = options || {}; 282 | 283 | // setup reasonable default values for all arguments 284 | 285 | position = options.position !== undefined ? position.copy( options.position ) : position.set( 0, 0, 0 ); 286 | velocity = options.velocity !== undefined ? velocity.copy( options.velocity ) : velocity.set( 0, 0, 0 ); 287 | color = options.color !== undefined ? color.set( options.color ) : color.set( 0xffffff ); 288 | 289 | var positionRandomness = options.positionRandomness !== undefined ? options.positionRandomness : 0; 290 | var velocityRandomness = options.velocityRandomness !== undefined ? options.velocityRandomness : 0; 291 | var colorRandomness = options.colorRandomness !== undefined ? options.colorRandomness : 1; 292 | var turbulence = options.turbulence !== undefined ? options.turbulence : 1; 293 | var lifetime = options.lifetime !== undefined ? options.lifetime : 5; 294 | var size = options.size !== undefined ? options.size : 10; 295 | var sizeRandomness = options.sizeRandomness !== undefined ? options.sizeRandomness : 0; 296 | var smoothPosition = options.smoothPosition !== undefined ? options.smoothPosition : false; 297 | 298 | if ( this.DPR !== undefined ) size *= this.DPR; 299 | 300 | var i = this.PARTICLE_CURSOR; 301 | 302 | // position 303 | 304 | positionStartAttribute.array[ i * 3 + 0 ] = position.x + ( particleSystem.random() * positionRandomness ); 305 | positionStartAttribute.array[ i * 3 + 1 ] = position.y + ( particleSystem.random() * positionRandomness ); 306 | positionStartAttribute.array[ i * 3 + 2 ] = position.z + ( particleSystem.random() * positionRandomness ); 307 | 308 | if ( smoothPosition === true ) { 309 | positionStartAttribute.array[ i * 3 + 0 ] += - ( velocity.x * particleSystem.random() ); 310 | positionStartAttribute.array[ i * 3 + 1 ] += - ( velocity.y * particleSystem.random() ); 311 | positionStartAttribute.array[ i * 3 + 2 ] += - ( velocity.z * particleSystem.random() ); 312 | } 313 | 314 | // velocity 315 | 316 | var maxVel = 2; 317 | 318 | var velX = velocity.x + particleSystem.random() * velocityRandomness; 319 | var velY = velocity.y + particleSystem.random() * velocityRandomness; 320 | var velZ = velocity.z + particleSystem.random() * velocityRandomness; 321 | 322 | velX = THREE.Math.clamp( ( velX - ( - maxVel ) ) / ( maxVel - ( - maxVel ) ), 0, 1 ); 323 | velY = THREE.Math.clamp( ( velY - ( - maxVel ) ) / ( maxVel - ( - maxVel ) ), 0, 1 ); 324 | velZ = THREE.Math.clamp( ( velZ - ( - maxVel ) ) / ( maxVel - ( - maxVel ) ), 0, 1 ); 325 | 326 | velocityAttribute.array[ i * 3 + 0 ] = velX; 327 | velocityAttribute.array[ i * 3 + 1 ] = velY; 328 | velocityAttribute.array[ i * 3 + 2 ] = velZ; 329 | 330 | // color 331 | 332 | color.r = THREE.Math.clamp( color.r + particleSystem.random() * colorRandomness, 0, 1 ); 333 | color.g = THREE.Math.clamp( color.g + particleSystem.random() * colorRandomness, 0, 1 ); 334 | color.b = THREE.Math.clamp( color.b + particleSystem.random() * colorRandomness, 0, 1 ); 335 | 336 | colorAttribute.array[ i * 3 + 0 ] = color.r; 337 | colorAttribute.array[ i * 3 + 1 ] = color.g; 338 | colorAttribute.array[ i * 3 + 2 ] = color.b; 339 | 340 | // turbulence, size, lifetime and starttime 341 | 342 | turbulenceAttribute.array[ i ] = turbulence; 343 | sizeAttribute.array[ i ] = size + particleSystem.random() * sizeRandomness; 344 | lifeTimeAttribute.array[ i ] = lifetime; 345 | startTimeAttribute.array[ i ] = this.time + particleSystem.random() * 2e-2; 346 | 347 | // offset 348 | 349 | if ( this.offset === 0 ) { 350 | this.offset = this.PARTICLE_CURSOR; 351 | } 352 | 353 | // counter and cursor 354 | 355 | this.count ++; 356 | this.PARTICLE_CURSOR ++; 357 | 358 | if ( this.PARTICLE_CURSOR >= this.PARTICLE_COUNT ) { 359 | this.PARTICLE_CURSOR = 0; 360 | } 361 | 362 | this.particleUpdate = true; 363 | }; 364 | 365 | this.init = function () { 366 | this.particleSystem = new THREE.Points( this.particleShaderGeo, this.particleShaderMat ); 367 | this.particleSystem.frustumCulled = false; 368 | this.add( this.particleSystem ); 369 | }; 370 | 371 | this.update = function ( time ) { 372 | this.time = time; 373 | this.particleShaderMat.uniforms.uTime.value = time; 374 | 375 | this.geometryUpdate(); 376 | }; 377 | 378 | this.geometryUpdate = function () { 379 | if (this.particleUpdate === true) { 380 | 381 | this.particleUpdate = false; 382 | 383 | var positionStartAttribute = this.particleShaderGeo.getAttribute( 'positionStart' ); 384 | var startTimeAttribute = this.particleShaderGeo.getAttribute( 'startTime' ); 385 | var velocityAttribute = this.particleShaderGeo.getAttribute( 'velocity' ); 386 | var turbulenceAttribute = this.particleShaderGeo.getAttribute( 'turbulence' ); 387 | var colorAttribute = this.particleShaderGeo.getAttribute( 'color' ); 388 | var sizeAttribute = this.particleShaderGeo.getAttribute( 'size' ); 389 | var lifeTimeAttribute = this.particleShaderGeo.getAttribute( 'lifeTime' ); 390 | 391 | if ( this.offset + this.count < this.PARTICLE_COUNT ) { 392 | 393 | positionStartAttribute.updateRange.offset = this.offset * positionStartAttribute.itemSize; 394 | startTimeAttribute.updateRange.offset = this.offset * startTimeAttribute.itemSize; 395 | velocityAttribute.updateRange.offset = this.offset * velocityAttribute.itemSize; 396 | turbulenceAttribute.updateRange.offset = this.offset * turbulenceAttribute.itemSize; 397 | colorAttribute.updateRange.offset = this.offset * colorAttribute.itemSize; 398 | sizeAttribute.updateRange.offset = this.offset * sizeAttribute.itemSize; 399 | lifeTimeAttribute.updateRange.offset = this.offset * lifeTimeAttribute.itemSize; 400 | 401 | positionStartAttribute.updateRange.count = this.count * positionStartAttribute.itemSize; 402 | startTimeAttribute.updateRange.count = this.count * startTimeAttribute.itemSize; 403 | velocityAttribute.updateRange.count = this.count * velocityAttribute.itemSize; 404 | turbulenceAttribute.updateRange.count = this.count * turbulenceAttribute.itemSize; 405 | colorAttribute.updateRange.count = this.count * colorAttribute.itemSize; 406 | sizeAttribute.updateRange.count = this.count * sizeAttribute.itemSize; 407 | lifeTimeAttribute.updateRange.count = this.count * lifeTimeAttribute.itemSize; 408 | 409 | } else { 410 | 411 | positionStartAttribute.updateRange.offset = 0; 412 | startTimeAttribute.updateRange.offset = 0; 413 | velocityAttribute.updateRange.offset = 0; 414 | turbulenceAttribute.updateRange.offset = 0; 415 | colorAttribute.updateRange.offset = 0; 416 | sizeAttribute.updateRange.offset = 0; 417 | lifeTimeAttribute.updateRange.offset = 0; 418 | 419 | // Use -1 to update the entire buffer, see #11476 420 | positionStartAttribute.updateRange.count = - 1; 421 | startTimeAttribute.updateRange.count = - 1; 422 | velocityAttribute.updateRange.count = - 1; 423 | turbulenceAttribute.updateRange.count = - 1; 424 | colorAttribute.updateRange.count = - 1; 425 | sizeAttribute.updateRange.count = - 1; 426 | lifeTimeAttribute.updateRange.count = - 1; 427 | 428 | } 429 | 430 | positionStartAttribute.needsUpdate = true; 431 | startTimeAttribute.needsUpdate = true; 432 | velocityAttribute.needsUpdate = true; 433 | turbulenceAttribute.needsUpdate = true; 434 | colorAttribute.needsUpdate = true; 435 | sizeAttribute.needsUpdate = true; 436 | lifeTimeAttribute.needsUpdate = true; 437 | 438 | this.offset = 0; 439 | this.count = 0; 440 | } 441 | }; 442 | 443 | this.dispose = function () { 444 | this.particleShaderGeo.dispose(); 445 | }; 446 | 447 | this.init(); 448 | 449 | }; 450 | 451 | GPUParticleContainer.prototype = Object.create( THREE.Object3D.prototype ); 452 | GPUParticleContainer.prototype.constructor = GPUParticleContainer; 453 | 454 | export default GPUParticleSystem; -------------------------------------------------------------------------------- /src/game/graphics/passes/clear-mask-pass.js: -------------------------------------------------------------------------------- 1 | import { THREE } from 'expo-three'; 2 | import Pass from "./pass"; 3 | 4 | /** 5 | * @author alteredq / http://alteredqualia.com/ 6 | */ 7 | 8 | THREE.ClearMaskPass = function () { 9 | 10 | Pass.call( this ); 11 | 12 | this.needsSwap = false; 13 | 14 | }; 15 | 16 | THREE.ClearMaskPass.prototype = Object.create( THREE.Pass.prototype ); 17 | 18 | Object.assign( THREE.ClearMaskPass.prototype, { 19 | 20 | render: function ( renderer, writeBuffer, readBuffer, delta, maskActive ) { 21 | 22 | renderer.state.buffers.stencil.setTest( false ); 23 | 24 | } 25 | 26 | } ); 27 | 28 | export default THREE.ClearMaskPass; -------------------------------------------------------------------------------- /src/game/graphics/passes/mask-pass.js: -------------------------------------------------------------------------------- 1 | import { THREE } from 'expo-three'; 2 | import Pass from "./pass"; 3 | 4 | /** 5 | * @author alteredq / http://alteredqualia.com/ 6 | */ 7 | 8 | THREE.MaskPass = function ( scene, camera ) { 9 | 10 | Pass.call( this ); 11 | 12 | this.scene = scene; 13 | this.camera = camera; 14 | 15 | this.clear = true; 16 | this.needsSwap = false; 17 | 18 | this.inverse = false; 19 | 20 | }; 21 | 22 | THREE.MaskPass.prototype = Object.assign( Object.create( THREE.Pass.prototype ), { 23 | 24 | constructor: THREE.MaskPass, 25 | 26 | render: function ( renderer, writeBuffer, readBuffer, delta, maskActive ) { 27 | 28 | var context = renderer.context; 29 | var state = renderer.state; 30 | 31 | // don't update color or depth 32 | 33 | state.buffers.color.setMask( false ); 34 | state.buffers.depth.setMask( false ); 35 | 36 | // lock buffers 37 | 38 | state.buffers.color.setLocked( true ); 39 | state.buffers.depth.setLocked( true ); 40 | 41 | // set up stencil 42 | 43 | var writeValue, clearValue; 44 | 45 | if ( this.inverse ) { 46 | 47 | writeValue = 0; 48 | clearValue = 1; 49 | 50 | } else { 51 | 52 | writeValue = 1; 53 | clearValue = 0; 54 | 55 | } 56 | 57 | state.buffers.stencil.setTest( true ); 58 | state.buffers.stencil.setOp( context.REPLACE, context.REPLACE, context.REPLACE ); 59 | state.buffers.stencil.setFunc( context.ALWAYS, writeValue, 0xffffffff ); 60 | state.buffers.stencil.setClear( clearValue ); 61 | 62 | // draw into the stencil buffer 63 | 64 | renderer.render( this.scene, this.camera, readBuffer, this.clear ); 65 | renderer.render( this.scene, this.camera, writeBuffer, this.clear ); 66 | 67 | // unlock color and depth buffer for subsequent rendering 68 | 69 | state.buffers.color.setLocked( false ); 70 | state.buffers.depth.setLocked( false ); 71 | 72 | // only render where stencil is set to 1 73 | 74 | state.buffers.stencil.setFunc( context.EQUAL, 1, 0xffffffff ); // draw if == 1 75 | state.buffers.stencil.setOp( context.KEEP, context.KEEP, context.KEEP ); 76 | 77 | } 78 | 79 | } ); 80 | 81 | export default THREE.MaskPass; -------------------------------------------------------------------------------- /src/game/graphics/passes/pass.js: -------------------------------------------------------------------------------- 1 | import { THREE } from 'expo-three'; 2 | 3 | THREE.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( THREE.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 | THREE.Pass.FullScreenQuad = ( function () { 32 | 33 | var camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 ); 34 | var geometry = new THREE.PlaneBufferGeometry( 2, 2 ); 35 | 36 | var FullScreenQuad = function ( material ) { 37 | 38 | this._mesh = new THREE.Mesh( geometry, material ); 39 | 40 | }; 41 | 42 | Object.defineProperty( FullScreenQuad.prototype, 'material', { 43 | 44 | get: function () { 45 | 46 | return this._mesh.material; 47 | 48 | }, 49 | 50 | set: function ( value ) { 51 | 52 | this._mesh.material = value; 53 | 54 | } 55 | 56 | } ); 57 | 58 | Object.assign( FullScreenQuad.prototype, { 59 | 60 | render: function ( renderer ) { 61 | 62 | renderer.render( this._mesh, camera ); 63 | 64 | } 65 | 66 | } ); 67 | 68 | return FullScreenQuad; 69 | 70 | } )(); 71 | 72 | export default THREE.Pass; -------------------------------------------------------------------------------- /src/game/graphics/passes/render-pass.js: -------------------------------------------------------------------------------- 1 | import { THREE } from 'expo-three'; 2 | import Pass from "./pass"; 3 | 4 | /** 5 | * @author alteredq / http://alteredqualia.com/ 6 | */ 7 | 8 | THREE.RenderPass = function ( scene, camera, overrideMaterial, clearColor, clearAlpha ) { 9 | 10 | THREE.Pass.call( this ); 11 | 12 | this.scene = scene; 13 | this.camera = camera; 14 | 15 | this.overrideMaterial = overrideMaterial; 16 | 17 | this.clearColor = clearColor; 18 | this.clearAlpha = ( clearAlpha !== undefined ) ? clearAlpha : 0; 19 | 20 | this.clear = true; 21 | this.clearDepth = false; 22 | this.needsSwap = false; 23 | 24 | }; 25 | 26 | THREE.RenderPass.prototype = Object.assign( Object.create( THREE.Pass.prototype ), { 27 | 28 | constructor: THREE.RenderPass, 29 | 30 | render: function ( renderer, writeBuffer, readBuffer, deltaTime, maskActive ) { 31 | 32 | var oldAutoClear = renderer.autoClear; 33 | renderer.autoClear = false; 34 | 35 | this.scene.overrideMaterial = this.overrideMaterial; 36 | 37 | var oldClearColor, oldClearAlpha; 38 | 39 | if ( this.clearColor ) { 40 | 41 | oldClearColor = renderer.getClearColor().getHex(); 42 | oldClearAlpha = renderer.getClearAlpha(); 43 | 44 | renderer.setClearColor( this.clearColor, this.clearAlpha ); 45 | 46 | } 47 | 48 | if ( this.clearDepth ) { 49 | 50 | renderer.clearDepth(); 51 | 52 | } 53 | 54 | renderer.setRenderTarget( this.renderToScreen ? null : readBuffer ); 55 | 56 | // TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600 57 | if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil ); 58 | renderer.render( this.scene, this.camera ); 59 | 60 | if ( this.clearColor ) { 61 | 62 | renderer.setClearColor( oldClearColor, oldClearAlpha ); 63 | 64 | } 65 | 66 | this.scene.overrideMaterial = null; 67 | renderer.autoClear = oldAutoClear; 68 | 69 | } 70 | 71 | } ); 72 | 73 | export default THREE.RenderPass; -------------------------------------------------------------------------------- /src/game/graphics/passes/shader-pass.js: -------------------------------------------------------------------------------- 1 | import { THREE } from 'expo-three'; 2 | import Pass from "./pass"; 3 | 4 | /** 5 | * @author alteredq / http://alteredqualia.com/ 6 | */ 7 | 8 | THREE.ShaderPass = function ( shader, textureID ) { 9 | 10 | THREE.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 | THREE.ShaderPass.prototype = Object.assign( Object.create( THREE.Pass.prototype ), { 45 | 46 | constructor: THREE.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 THREE.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 | import { THREE } from 'expo-three'; 8 | const { 9 | AdditiveBlending, 10 | Color, 11 | LinearFilter, 12 | MeshBasicMaterial, 13 | RGBAFormat, 14 | ShaderMaterial, 15 | UniformsUtils, 16 | Vector2, 17 | Vector3, 18 | WebGLRenderTarget 19 | } = 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 { Platform } from "react-native"; 3 | import ExpoGraphics from "expo-graphics-rnge"; 4 | import ExpoTHREE, { THREE } from "expo-three"; 5 | import EffectComposer from "./effect-composer"; 6 | import RenderPass from "./passes/render-pass"; 7 | import _ from "lodash"; 8 | 9 | global.THREE = THREE; 10 | 11 | THREE.suppressExpoWarnings(); 12 | 13 | class ThreeView extends PureComponent { 14 | 15 | onShouldReloadContext = () => { 16 | return Platform.OS === "android"; 17 | }; 18 | 19 | onContextCreate = async ({ gl, canvas, width, height, scale: pixelRatio }) => { 20 | this.props.camera.resize(width, height, pixelRatio); 21 | this.renderer = new ExpoTHREE.Renderer({ 22 | gl, 23 | pixelRatio, 24 | width, 25 | height, 26 | }); 27 | this.renderer.setClearColor(0x020202, 1.0); 28 | this.gl = gl; 29 | this.composer = new EffectComposer(this.renderer); 30 | 31 | //-- Toggle line below if you have issues with shadows and/or post-processing effects 32 | this.gl.createRenderbuffer = () => {}; 33 | 34 | const passes = [ 35 | new RenderPass(this.props.scene, this.props.camera), 36 | ...this.props.passes 37 | ]; 38 | 39 | passes.forEach(p => this.composer.addPass(p)) 40 | passes[passes.length-1].renderToScreen = true; 41 | }; 42 | 43 | onResize = ({ width, height, scale: pixelRatio }) => { 44 | this.props.camera.resize(width, height, pixelRatio); 45 | this.renderer.setSize(width, height); 46 | this.renderer.setPixelRatio(pixelRatio); 47 | }; 48 | 49 | render() { 50 | if (this.composer && this.gl) { 51 | this.composer.render(); 52 | this.gl.endFrameEXP(); 53 | } 54 | return ( 55 | 62 | ); 63 | } 64 | } 65 | 66 | const renderHUD = (entities, screen) => { 67 | if (!entities.hud) return null; 68 | 69 | const hud = entities.hud; 70 | 71 | if (typeof hud.renderer === "object") 72 | return ; 73 | else if (typeof hud.renderer === "function") 74 | return ; 75 | }; 76 | 77 | const ThreeJSRenderer = (...passes) => (entities, screen) => { 78 | if (!entities) return null; 79 | return [ 80 | , 86 | renderHUD(entities, screen) 87 | ]; 88 | }; 89 | 90 | export default ThreeJSRenderer; -------------------------------------------------------------------------------- /src/game/graphics/shaders/copy-shader.js: -------------------------------------------------------------------------------- 1 | import { THREE } from 'expo-three'; 2 | 3 | /** 4 | * @author alteredq / http://alteredqualia.com/ 5 | * 6 | * Full-screen textured quad shader 7 | */ 8 | 9 | THREE.CopyShader = { 10 | 11 | uniforms: { 12 | 13 | "tDiffuse": { value: null }, 14 | "opacity": { value: 1.0 } 15 | 16 | }, 17 | 18 | vertexShader: [ 19 | 20 | "varying vec2 vUv;", 21 | 22 | "void main() {", 23 | 24 | "vUv = uv;", 25 | "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", 26 | 27 | "}" 28 | 29 | ].join( "\n" ), 30 | 31 | fragmentShader: [ 32 | 33 | "uniform float opacity;", 34 | 35 | "uniform sampler2D tDiffuse;", 36 | 37 | "varying vec2 vUv;", 38 | 39 | "void main() {", 40 | 41 | "vec4 texel = texture2D( tDiffuse, vUv );", 42 | "gl_FragColor = opacity * texel;", 43 | 44 | "}" 45 | 46 | ].join( "\n" ) 47 | 48 | }; 49 | 50 | export default THREE.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 { THREE } from "expo-three"; 9 | 10 | const LuminosityHighPassShader = { 11 | 12 | shaderID: "luminosityHighPass", 13 | 14 | uniforms: { 15 | 16 | "tDiffuse": { value: null }, 17 | "luminosityThreshold": { value: 1.0 }, 18 | "smoothWidth": { value: 1.0 }, 19 | "defaultColor": { value: new THREE.Color( 0x000000 ) }, 20 | "defaultOpacity": { value: 0.0 } 21 | 22 | }, 23 | 24 | vertexShader: [ 25 | 26 | "varying vec2 vUv;", 27 | 28 | "void main() {", 29 | 30 | "vUv = uv;", 31 | 32 | "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", 33 | 34 | "}" 35 | 36 | ].join("\n"), 37 | 38 | fragmentShader: [ 39 | 40 | "uniform sampler2D tDiffuse;", 41 | "uniform vec3 defaultColor;", 42 | "uniform float defaultOpacity;", 43 | "uniform float luminosityThreshold;", 44 | "uniform float smoothWidth;", 45 | 46 | "varying vec2 vUv;", 47 | 48 | "void main() {", 49 | 50 | "vec4 texel = texture2D( tDiffuse, vUv );", 51 | 52 | "vec3 luma = vec3( 0.299, 0.587, 0.114 );", 53 | 54 | "float v = dot( texel.xyz, luma );", 55 | 56 | "vec4 outputColor = vec4( defaultColor.rgb, defaultOpacity );", 57 | 58 | "float alpha = smoothstep( luminosityThreshold, luminosityThreshold + smoothWidth, v );", 59 | 60 | "gl_FragColor = mix( outputColor, texel, alpha );", 61 | 62 | "}" 63 | 64 | ].join("\n") 65 | 66 | }; 67 | 68 | export default LuminosityHighPassShader; -------------------------------------------------------------------------------- /src/game/graphics/shaders/pixel-shader.js: -------------------------------------------------------------------------------- 1 | import { THREE } from "expo-three"; 2 | import { screen } from "../../utils"; 3 | 4 | export default ({ 5 | pixelSize = 5, 6 | borderSize = 1, 7 | lightenFactor = 1.8, 8 | softenFactor = 0.75, 9 | darkenFactor = 0.5, 10 | resolution = new THREE.Vector2(screen.width, screen.height) 11 | } = {}) => { 12 | const pixelShader = { 13 | uniforms: { 14 | tDiffuse: { value: null }, 15 | pixelSize: { value: pixelSize }, 16 | borderFraction: { value: borderSize / pixelSize }, 17 | lightenFactor: { value: lightenFactor }, 18 | softenFactor: { value: softenFactor }, 19 | darkenFactor: { value: darkenFactor }, 20 | resolution: { value: resolution } 21 | }, 22 | 23 | vertexShader: ` 24 | varying highp vec2 vUv; 25 | 26 | void main() { 27 | vUv = uv; 28 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 29 | }`, 30 | 31 | fragmentShader: ` 32 | uniform sampler2D tDiffuse; 33 | uniform float pixelSize; 34 | uniform float borderFraction; 35 | uniform float lightenFactor; 36 | uniform float softenFactor; 37 | uniform float darkenFactor; 38 | uniform vec2 resolution; 39 | 40 | varying highp vec2 vUv; 41 | 42 | void main(){ 43 | vec2 dxy = pixelSize / resolution; 44 | vec2 pixel = vUv / dxy; 45 | vec2 fraction = fract(pixel); 46 | vec2 coord = dxy * floor(pixel); 47 | vec3 color = texture2D(tDiffuse, coord).xyz; 48 | 49 | if (fraction.y > (1.0 - borderFraction)) 50 | color = color * lightenFactor; 51 | 52 | if (fraction.x < borderFraction) 53 | color = color * softenFactor; 54 | 55 | if (fraction.y < borderFraction) 56 | color = color * darkenFactor; 57 | 58 | gl_FragColor = vec4(color, 1); 59 | }` 60 | }; 61 | 62 | return pixelShader; 63 | }; 64 | -------------------------------------------------------------------------------- /src/game/graphics/shaders/scanline-shader.js: -------------------------------------------------------------------------------- 1 | import { THREE } from "expo-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 | fragmentShader: ` 19 | uniform sampler2D tDiffuse; 20 | uniform float thickness; 21 | uniform vec4 color; 22 | varying vec2 vUv; 23 | void main() { 24 | float result = floor(mod(gl_FragCoord.y, thickness)); 25 | gl_FragColor = result == 0.0 ? texture2D(tDiffuse, vUv) : color; 26 | }` 27 | }; 28 | 29 | return scanlineShader; 30 | }; 31 | -------------------------------------------------------------------------------- /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 { THREE } from "expo-three"; 2 | 3 | export default ({ distance = 0.005, threshold = 0, colors = [new THREE.Color(0xFFFFFF), new THREE.Color(0x362928), new THREE.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 THREE.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-native-game-engine"; 3 | import Renderer from "./graphics/renderer"; 4 | import Systems from "./systems"; 5 | import Entities from "./entities"; 6 | import Timer from "./utils/perf-timer"; 7 | import ShaderPass from "./graphics/passes/shader-pass"; 8 | import PixelShader from "./graphics/shaders/pixel-shader"; 9 | 10 | class Game extends React.Component { 11 | render() { 12 | return ( 13 | 22 | ); 23 | } 24 | } 25 | 26 | export default Game; 27 | -------------------------------------------------------------------------------- /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, { touchController }) => { 9 | const camera = entities.camera; 10 | 11 | if (camera && touchController) { 12 | //-- Yaw and pitch rotation 13 | if (touchController.multiFingerMovement.x || touchController.multiFingerMovement.y) { 14 | rotateAroundPoint(camera, camera.target, { 15 | y: touchController.multiFingerMovement.x * yawSpeed, 16 | x: touchController.multiFingerMovement.y * pitchSpeed 17 | }); 18 | camera.lookAt(camera.target); 19 | } 20 | 21 | //-- Zooming (pinching) 22 | if (touchController.pinch) { 23 | const zoomFactor = touchController.pinch * zoomSpeed; 24 | 25 | camera.zoom += zoomFactor; 26 | camera.updateProjectionMatrix(); 27 | } 28 | } 29 | 30 | return entities; 31 | }; 32 | }; 33 | 34 | export default Camera; 35 | -------------------------------------------------------------------------------- /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 | import { screen } from "../utils/index"; 2 | import { Vibration } from 'react-native'; 3 | 4 | const padding = 10; 5 | 6 | const stickRadius = 50; 7 | const stickPosition = { 8 | x: stickRadius + padding, 9 | y: screen.height - stickRadius - padding 10 | }; 11 | 12 | const aRadius = 25; 13 | const aPosition = { 14 | x: screen.width - aRadius * 2.75 - padding, 15 | y: screen.height - aRadius - padding 16 | }; 17 | 18 | const bRadius = 25; 19 | const bPosition = { 20 | x: screen.width - bRadius - padding, 21 | y: screen.height - bRadius * 2.75 - padding 22 | }; 23 | 24 | const distance = (touch, pos) => { 25 | return Math.hypot(touch.event.pageX - pos.x, touch.event.pageY - pos.y); 26 | }; 27 | 28 | const subtract = (touch, pos) => { 29 | return { x: touch.event.pageX - pos.x, y: touch.event.pageY - pos.y }; 30 | }; 31 | 32 | const clamp = (vec, radius) => { 33 | const dist = Math.hypot(vec.x, vec.y); 34 | 35 | if (dist < radius) 36 | return vec; 37 | 38 | return { 39 | x: vec.x * (radius / dist), 40 | y: vec.y * (radius / dist) 41 | } 42 | }; 43 | 44 | const normalize = (vec, radius) => { 45 | return { 46 | x: vec.x / radius, 47 | y: vec.y / radius, 48 | heading: Math.atan2(vec.y, vec.x) 49 | } 50 | } 51 | 52 | const isTouchingPosition = (pos, proxmity) => { 53 | let touching = false; 54 | let id = null; 55 | 56 | return touches => { 57 | if (!touching) { 58 | const down = touches.find( 59 | t => 60 | (t.type === "start" || t.type === "move") && 61 | distance(t, pos) < proxmity 62 | ); 63 | 64 | if (down) { 65 | touching = true; 66 | id = down.event.identifier; 67 | } 68 | } else { 69 | const up = 70 | touches.find(t => t.type === "end" && t.event.identifier == id) || 71 | touches.find( 72 | t => 73 | t.type === "move" && 74 | t.event.identifier === id && 75 | distance(t, pos) > proxmity 76 | ); 77 | 78 | if (up) { 79 | touching = false; 80 | id = null; 81 | } 82 | } 83 | 84 | return touching; 85 | }; 86 | }; 87 | 88 | const neutral = { x: 0, y: 0, heading: null }; 89 | 90 | const trackNormalFromPosition = (pos, radius, proxmity) => { 91 | let normal = null; 92 | let id = null; 93 | 94 | return touches => { 95 | if (!normal) { 96 | const down = touches.find( 97 | t => 98 | (t.type === "start" || t.type === "move") && 99 | distance(t, pos) < proxmity 100 | ); 101 | 102 | if (down) { 103 | const vec = subtract(down, pos); 104 | const clamped = clamp(vec, radius); 105 | 106 | normal = normalize(clamped, radius); 107 | id = down.event.identifier; 108 | } 109 | } else { 110 | const move = touches.find( 111 | t => 112 | t.type === "move" && 113 | t.event.identifier === id && 114 | distance(t, pos) < proxmity 115 | ); 116 | 117 | if (move) { 118 | const vec = subtract(move, pos); 119 | const clamped = clamp(vec, radius); 120 | 121 | normal = normalize(clamped, radius); 122 | } else { 123 | const up = 124 | touches.find(t => t.type === "end" && t.event.identifier === id) || 125 | touches.find( 126 | t => 127 | t.type === "move" && 128 | t.event.identifier === id && 129 | distance(t, pos) > proxmity 130 | ); 131 | 132 | if (up) { 133 | normal = null; 134 | id = null; 135 | } 136 | } 137 | } 138 | 139 | return normal || neutral; 140 | }; 141 | }; 142 | 143 | const vibrate = (patternOrDuration, repeat) => { 144 | Vibration.vibrate(patternOrDuration, repeat); 145 | }; 146 | 147 | const isTouchingA = isTouchingPosition(aPosition, aRadius + 20); 148 | const isTouchingB = isTouchingPosition(bPosition, bRadius + 20); 149 | const trackNormalFromStick = trackNormalFromPosition(stickPosition, stickRadius, stickRadius + 40) 150 | 151 | let previous = {}; 152 | 153 | const GamepadController = (Wrapped = x => x) => (entities, args) => { 154 | if (!args.gamepadController) { 155 | const current = { 156 | ...trackNormalFromStick(args.touches), 157 | a: isTouchingA(args.touches), 158 | b: isTouchingB(args.touches), 159 | vibrate 160 | }; 161 | 162 | args.gamepadController = Object.assign( 163 | { stickRadius, stickPosition, aRadius, aPosition, bRadius, bPosition }, 164 | current, 165 | { previous } 166 | ); 167 | 168 | previous = current; 169 | } 170 | 171 | return Wrapped(entities, args); 172 | }; 173 | 174 | export default GamepadController; 175 | -------------------------------------------------------------------------------- /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 | } 8 | 9 | return entities; 10 | }; 11 | 12 | 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 TouchController from "./touch-controller"; 9 | import Physics from "./physics"; 10 | import Spawn from "./spawn"; 11 | 12 | export default [ 13 | GamepadController(), 14 | TouchController()(), 15 | Camera({ pitchSpeed: -0.01, yawSpeed: 0.01 }), 16 | Particles, 17 | Removal, 18 | Rotation, 19 | Timeline, 20 | Spawn, 21 | Physics, 22 | HUD 23 | ]; 24 | -------------------------------------------------------------------------------- /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 { THREE } from "expo-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 }) => { 9 | 10 | const world = entities.world; 11 | const scene = entities.scene; 12 | 13 | if (gamepadController.a && !gamepadController.previous.a) 14 | entities[boxId()] = Box({ parent: scene, world, y: 5 }); 15 | 16 | if (gamepadController.b && !gamepadController.previous.b) 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/systems/touch-controller.js: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | 3 | const neutral = { x: 0, y: 0 }; 4 | 5 | const singleFingerMovement = moves => { 6 | if (moves.length === 1) { 7 | const f1 = moves[0]; 8 | 9 | return { 10 | x: f1.delta.locationX, 11 | y: f1.delta.locationY 12 | }; 13 | } 14 | 15 | return neutral; 16 | }; 17 | 18 | const multiFingerMovement = moves => { 19 | if (moves.length > 1) { 20 | const f1 = moves[0]; 21 | const f2 = moves[1]; 22 | 23 | return { 24 | x: (f1.delta.locationX + f2.delta.locationX) / 2, 25 | y: (f1.delta.locationY + f2.delta.locationY) / 2 26 | }; 27 | } 28 | 29 | return neutral; 30 | }; 31 | 32 | const pinch = (moves, pinchThreshold) => { 33 | if (moves.length === 2) { 34 | 35 | const f1 = moves[0]; 36 | const f2 = moves[1]; 37 | 38 | const f1Pos = { x: f1.event.pageX, y: f1.event.pageY }; 39 | const f1PosPrev = { x: f1Pos.x - f1.delta.pageX, y: f1Pos.y - f1.delta.pageY }; 40 | 41 | const f2Pos = { x: f2.event.pageX, y: f2.event.pageY }; 42 | const f2PosPrev = { x: f2Pos.x - f2.delta.pageX, y: f2Pos.y - f2.delta.pageY }; 43 | 44 | const currentDistance = Math.hypot(f1Pos.x - f2Pos.x, f1Pos.y - f2Pos.y); 45 | const previousDistance = Math.hypot(f1PosPrev.x - f2PosPrev.x, f1PosPrev.y - f2PosPrev.y) 46 | 47 | if (currentDistance > pinchThreshold) 48 | return currentDistance - previousDistance; 49 | } 50 | 51 | return 0; 52 | }; 53 | 54 | const find = type => touches => { 55 | const found = touches.find(x => x.type === type) 56 | 57 | if (found) 58 | return found.event; 59 | } 60 | 61 | const press = find("press"); 62 | 63 | const start = find("start") 64 | 65 | let previous = {}; 66 | 67 | const TouchController = ({ pinchThreshold = 150 } = {}) => (Wrapped = x => x) => (entities, args) => { 68 | if (!args.touchController) { 69 | const touches = args.touches; 70 | const moves = _.uniqBy(touches.filter(x => x.type === "move"), x => x.event.identifier); 71 | 72 | const current = { 73 | singleFingerMovement: singleFingerMovement(moves), 74 | multiFingerMovement: multiFingerMovement(moves), 75 | pinch: pinch(moves, pinchThreshold), 76 | press: press(touches), 77 | start: start(touches) 78 | }; 79 | 80 | args.touchController = Object.assign( 81 | {}, 82 | current, 83 | { previous } 84 | ); 85 | 86 | previous = current; 87 | } 88 | 89 | return Wrapped(entities, args); 90 | }; 91 | 92 | export default TouchController; 93 | -------------------------------------------------------------------------------- /src/game/utils/index.js: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import { interpolate } from '@popmotion/popcorn'; 3 | import { Dimensions } from "react-native"; 4 | import * as ThreeUtils from "./three"; 5 | import { Audio } from "expo-av"; 6 | 7 | const remove = (entities, key) => { 8 | const entity = entities[key]; 9 | 10 | if (!entity) 11 | return; 12 | 13 | if (entity.model) 14 | ThreeUtils.remove(entity.model.parent, entity.model); 15 | 16 | if (entity.light) 17 | ThreeUtils.remove(entity.light.parent, entity.light); 18 | 19 | if (entity.particles) { 20 | Object.keys(entity.particles).forEach(k => { 21 | const emitter = entity.particles[k].emitter 22 | if (emitter) 23 | ThreeUtils.remove(emitter.parent, emitter); 24 | }) 25 | } 26 | 27 | if (entity.bodies) 28 | entity.bodies.forEach(b => b.remove()) 29 | 30 | delete entities[key]; 31 | 32 | return entities; 33 | }; 34 | 35 | const any = (arr = [], b = "", c) => { 36 | if (c) { 37 | if (Array.isArray(c) === false) c = [c]; 38 | 39 | return _.isFunction(b) 40 | ? _.intersection(arr.map(b), c).length > 0 41 | : _.intersection(arr.map(x => x[b]), c).length > 0; 42 | } 43 | 44 | if (!b) return arr.length > 0; 45 | 46 | if (Array.isArray(b)) return _.intersection(arr, b).length > 0; 47 | 48 | if (_.isFunction(b)) return arr.find(b); 49 | 50 | return arr.indexOf(b) > -1; 51 | }; 52 | 53 | const first = (entities, ...predicates) => { 54 | if (!entities) return; 55 | if (!predicates || predicates.length < 1) return entities[0]; 56 | 57 | if (Array.isArray(entities)) 58 | return entities.find(e => _.every(predicates, p => p(e))) 59 | 60 | return entities[Object.keys(entities).find(key => _.every(predicates, p => p(entities[key])))] 61 | } 62 | 63 | const firstKey = (entities, ...predicates) => { 64 | if (!entities) return; 65 | if (!predicates || predicates.length < 1) return Object.keys(entities)[0]; 66 | 67 | return Object.keys(entities).find(key => _.every(predicates, p => p(entities[key]))) 68 | } 69 | 70 | const all = (entities, ...predicates) => { 71 | if (!entities) return; 72 | if (!predicates || predicates.length < 1) return entities; 73 | 74 | if (Array.isArray(entities)) 75 | return entities.filter(e => _.every(predicates, p => p(e))) 76 | 77 | return Object.keys(entities).filter(key => _.every(predicates, p => p(entities[key]))).map(key => entities[key]) 78 | } 79 | 80 | const allKeys = (entities, ...predicates) => { 81 | if (!entities) return; 82 | if (!predicates || predicates.length < 1) return Object.keys(entities); 83 | 84 | return Object.keys(entities).filter(key => _.every(predicates, p => p(entities[key]))); 85 | } 86 | 87 | //-- https://stackoverflow.com/a/7616484/138392 88 | const getHashCode = str => { 89 | var hash = 0, i, chr; 90 | if (str.length === 0) return hash; 91 | for (i = 0; i < str.length; i++) { 92 | chr = str.charCodeAt(i); 93 | hash = ((hash << 5) - hash) + chr; 94 | hash |= 0; // Convert to 32bit integer 95 | } 96 | return hash; 97 | }; 98 | 99 | const positive = val => Math.abs(val) 100 | 101 | const negative = val => { 102 | if (val > 0) return -val 103 | return val 104 | } 105 | 106 | const remap = (n, start1, stop1, start2, stop2) => { 107 | return (n - start1) / (stop1 - start1) * (stop2 - start2) + start2; 108 | } 109 | 110 | const constrain = (n, low, high) => { 111 | return Math.max(Math.min(n, high), low); 112 | } 113 | 114 | const between = (n, low, high) => { 115 | return n > low && n < high 116 | } 117 | 118 | const pipe = (...funcs) => _.flow(_.flatten(funcs || [])) 119 | 120 | const id = (seed = 0) => (prefix = "") => `${prefix}${++seed}` 121 | 122 | const cond = (condition, func) => { 123 | return (args) => { 124 | const test = _.isFunction(condition) ? condition(args) : condition 125 | return test ? func(args) : args 126 | } 127 | } 128 | 129 | const log = label => data => { 130 | console.log(label, data); 131 | return data; 132 | } 133 | 134 | const randomInt = (min = 0, max = 1) => Math.floor(Math.random() * (max - min + 1) + min); 135 | 136 | const throttle = (func, interval, defaultValue) => { 137 | let last = 0; 138 | return (...args) => { 139 | const current = performance.now(); 140 | if ((current - last) > interval) { 141 | last = current; 142 | return func(...args); 143 | } else { 144 | return _.isFunction(defaultValue) ? defaultValue(...args) : defaultValue; 145 | } 146 | } 147 | } 148 | 149 | const screen = Dimensions.get("window"); 150 | 151 | const createSound = (asset, throttleInterval = 0) => { 152 | const task = Audio.Sound.createAsync(asset); 153 | 154 | const play = () => { 155 | Promise.resolve(task).then(({ sound, status }) => { 156 | if (!status.isPlaying) 157 | sound.playFromPositionAsync(0) 158 | }); 159 | }; 160 | 161 | return throttleInterval ? throttle(play, throttleInterval) : play; 162 | } 163 | 164 | module.exports = { 165 | remove, 166 | any, 167 | find: _.find, 168 | filter: _.filter, 169 | first, 170 | firstKey, 171 | all, 172 | allKeys, 173 | getHashCode, 174 | positive, 175 | negative, 176 | remap, 177 | constrain, 178 | clamp: constrain, 179 | between, 180 | pipe, 181 | id, 182 | cond, 183 | interpolate, 184 | log, 185 | randomInt, 186 | once: _.once, 187 | memoize: _.memoize, 188 | throttle, 189 | screen, 190 | createSound, 191 | sound: createSound 192 | } -------------------------------------------------------------------------------- /src/game/utils/perf-timer.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet, Platform } from "react-native"; 2 | import { DefaultTimer } from "react-native-game-engine"; 3 | 4 | const ideal = 1000 / 60; 5 | 6 | class PerfTimer { 7 | constructor() { 8 | this.subscribers = []; 9 | this.loopId = null; 10 | this.last = 0; 11 | } 12 | 13 | loop = time => { 14 | if (this.loopId) { 15 | this.subscribers.forEach(callback => { 16 | callback(time); 17 | }); 18 | } 19 | 20 | const now = new Date().getTime(); 21 | const delay = ideal - (now - this.last) 22 | 23 | this.loopId = setTimeout(this.loop, delay > 0 ? delay : 0, now); 24 | this.last = now; 25 | }; 26 | 27 | start() { 28 | if (!this.loopId) { 29 | this.loop(); 30 | } 31 | } 32 | 33 | stop() { 34 | if (this.loopId) { 35 | clearTimeout(this.loopId); 36 | this.loopId = null; 37 | } 38 | } 39 | 40 | subscribe(callback) { 41 | if (this.subscribers.indexOf(callback) === -1) 42 | this.subscribers.push(callback); 43 | } 44 | 45 | unsubscribe(callback) { 46 | this.subscribers = this.subscribers.filter(s => s !== callback) 47 | } 48 | } 49 | 50 | export default (Platform.OS === "android" ? PerfTimer : DefaultTimer) -------------------------------------------------------------------------------- /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/index.js: -------------------------------------------------------------------------------- 1 | import { THREE } from "expo-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 | //-- Forces passing to `gl.texImage2D(...)` verbatim 87 | clone.isDataTexture = true; 88 | 89 | return clone; 90 | }; 91 | 92 | export const cloneMesh = SkeletonUtils.clone; 93 | 94 | export const firstMesh = obj => { 95 | if (!obj) 96 | return; 97 | 98 | if (obj.isMesh) 99 | return obj; 100 | 101 | if (obj.children && obj.children.length){ 102 | for (let i = 0; i < obj.children.length; i++) { 103 | const test = firstMesh(obj.children[i]); 104 | 105 | if (test && test.isMesh) 106 | return test; 107 | } 108 | } 109 | }; -------------------------------------------------------------------------------- /src/game/utils/three/skeleton-utils.js: -------------------------------------------------------------------------------- 1 | import { THREE } from "expo-three"; 2 | 3 | /** 4 | * @author sunag / http://www.sunag.com.br 5 | */ 6 | 7 | THREE.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 THREE.SkeletonUtils; --------------------------------------------------------------------------------