├── .watchmanconfig
├── .gitignore
├── src
├── assets
│ ├── icon.png
│ ├── splash.png
│ ├── models
│ │ ├── jet.glb
│ │ └── droid.fbx
│ ├── textures
│ │ ├── grid.png
│ │ ├── perlin.png
│ │ └── particle.png
│ ├── audio
│ │ └── crash-01.wav
│ ├── aseprite
│ │ └── bullet-01.ase
│ └── sprite-sheets
│ │ ├── cuphead.png
│ │ ├── player.png
│ │ └── bullet-01.png
├── game
│ ├── systems
│ │ ├── hud.js
│ │ ├── gravity.js
│ │ ├── index.js
│ │ ├── spawn.js
│ │ ├── spring.js
│ │ ├── basic-physics.js
│ │ ├── particles.js
│ │ ├── removal.js
│ │ ├── camera.js
│ │ ├── physics.js
│ │ ├── rotation.js
│ │ ├── timeline.js
│ │ ├── touch-controller.js
│ │ ├── gamepad-controller.js
│ │ └── collisions.js
│ ├── components
│ │ ├── droid.js
│ │ ├── particles.js
│ │ ├── animated-model.js
│ │ ├── camera.js
│ │ ├── cylinder.js
│ │ ├── portal.js
│ │ ├── box.js
│ │ ├── jet.js
│ │ ├── turntable.js
│ │ ├── cuphead.js
│ │ ├── sprite.js
│ │ └── hud.js
│ ├── graphics
│ │ ├── passes
│ │ │ ├── clear-mask-pass.js
│ │ │ ├── pass.js
│ │ │ ├── render-pass.js
│ │ │ ├── mask-pass.js
│ │ │ ├── shader-pass.js
│ │ │ └── unreal-bloom-pass.js
│ │ ├── shaders
│ │ │ ├── scanline-shader.js
│ │ │ ├── copy-shader.js
│ │ │ ├── sepia-shader.js
│ │ │ ├── tri-color-shader.js
│ │ │ ├── luminosity-high-pass-shader.js
│ │ │ └── pixel-shader.js
│ │ ├── renderer.js
│ │ ├── effect-composer.js
│ │ └── gpu-particle-system.js
│ ├── index.js
│ ├── utils
│ │ ├── perf-timer.js
│ │ ├── three
│ │ │ ├── index.js
│ │ │ └── skeleton-utils.js
│ │ ├── index.js
│ │ └── perlin.js
│ └── entities.js
└── app.js
├── babel.config.js
├── metro.config.js
├── app.json
├── package.json
├── LICENSE
└── README.md
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/**/*
2 | .expo/*
3 | npm-debug.*
4 | *.jks
5 | *.p12
6 | *.key
7 | *.mobileprovision
8 |
--------------------------------------------------------------------------------
/src/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/react-native-game-engine-template/HEAD/src/assets/icon.png
--------------------------------------------------------------------------------
/src/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/react-native-game-engine-template/HEAD/src/assets/splash.png
--------------------------------------------------------------------------------
/src/assets/models/jet.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/react-native-game-engine-template/HEAD/src/assets/models/jet.glb
--------------------------------------------------------------------------------
/src/assets/models/droid.fbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/react-native-game-engine-template/HEAD/src/assets/models/droid.fbx
--------------------------------------------------------------------------------
/src/assets/textures/grid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/react-native-game-engine-template/HEAD/src/assets/textures/grid.png
--------------------------------------------------------------------------------
/src/assets/audio/crash-01.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/react-native-game-engine-template/HEAD/src/assets/audio/crash-01.wav
--------------------------------------------------------------------------------
/src/assets/textures/perlin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/react-native-game-engine-template/HEAD/src/assets/textures/perlin.png
--------------------------------------------------------------------------------
/src/assets/aseprite/bullet-01.ase:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/react-native-game-engine-template/HEAD/src/assets/aseprite/bullet-01.ase
--------------------------------------------------------------------------------
/src/assets/textures/particle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/react-native-game-engine-template/HEAD/src/assets/textures/particle.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/src/assets/sprite-sheets/cuphead.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/react-native-game-engine-template/HEAD/src/assets/sprite-sheets/cuphead.png
--------------------------------------------------------------------------------
/src/assets/sprite-sheets/player.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/react-native-game-engine-template/HEAD/src/assets/sprite-sheets/player.png
--------------------------------------------------------------------------------
/src/assets/sprite-sheets/bullet-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/react-native-game-engine-template/HEAD/src/assets/sprite-sheets/bullet-01.png
--------------------------------------------------------------------------------
/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;
--------------------------------------------------------------------------------
/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 | };
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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;
--------------------------------------------------------------------------------
/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;
--------------------------------------------------------------------------------