332 | varying vec2 vUv;
333 | uniform sampler2D colorTexture;
334 | uniform vec2 texSize;
335 | uniform vec2 direction;
336 |
337 | float gaussianPdf(in float x, in float sigma) {
338 | return 0.39894 * exp( -0.5 * x * x/( sigma * sigma))/sigma;
339 | }
340 | void main() {
341 | vec2 invSize = 1.0 / texSize;
342 | float fSigma = float(SIGMA);
343 | float weightSum = gaussianPdf(0.0, fSigma);
344 | vec3 diffuseSum = texture2D( colorTexture, vUv).rgb * weightSum;
345 | for( int i = 1; i < KERNEL_RADIUS; i ++ ) {
346 | float x = float(i);
347 | float w = gaussianPdf(x, fSigma);
348 | vec2 uvOffset = direction * invSize * x;
349 | vec3 sample1 = texture2D( colorTexture, vUv + uvOffset).rgb;
350 | vec3 sample2 = texture2D( colorTexture, vUv - uvOffset).rgb;
351 | diffuseSum += (sample1 + sample2) * w;
352 | weightSum += 2.0 * w;
353 | }
354 | gl_FragColor = vec4(diffuseSum/weightSum, 1.0);
355 | }`
356 | });
357 | },
358 |
359 | getCompositeMaterial: function(nMips) {
360 | return new ShaderMaterial({
361 | defines: {
362 | NUM_MIPS: nMips
363 | },
364 |
365 | uniforms: {
366 | blurTexture1: { value: null },
367 | blurTexture2: { value: null },
368 | blurTexture3: { value: null },
369 | blurTexture4: { value: null },
370 | blurTexture5: { value: null },
371 | dirtTexture: { value: null },
372 | bloomStrength: { value: 1.0 },
373 | bloomFactors: { value: null },
374 | bloomTintColors: { value: null },
375 | bloomRadius: { value: 0.0 }
376 | },
377 |
378 | vertexShader: `varying vec2 vUv;
379 | void main() {
380 | vUv = uv;
381 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
382 | }`,
383 |
384 | fragmentShader: `varying vec2 vUv;
385 | uniform sampler2D blurTexture1;
386 | uniform sampler2D blurTexture2;
387 | uniform sampler2D blurTexture3;
388 | uniform sampler2D blurTexture4;
389 | uniform sampler2D blurTexture5;
390 | uniform sampler2D dirtTexture;
391 | uniform float bloomStrength;
392 | uniform float bloomRadius;
393 | uniform float bloomFactors[NUM_MIPS];
394 | uniform vec3 bloomTintColors[NUM_MIPS];
395 |
396 | float lerpBloomFactor(const in float factor) {
397 | float mirrorFactor = 1.2 - factor;
398 | return mix(factor, mirrorFactor, bloomRadius);
399 | }
400 |
401 | void main() {
402 | gl_FragColor = bloomStrength * ( lerpBloomFactor(bloomFactors[0]) * vec4(bloomTintColors[0], 1.0) * texture2D(blurTexture1, vUv) +
403 | lerpBloomFactor(bloomFactors[1]) * vec4(bloomTintColors[1], 1.0) * texture2D(blurTexture2, vUv) +
404 | lerpBloomFactor(bloomFactors[2]) * vec4(bloomTintColors[2], 1.0) * texture2D(blurTexture3, vUv) +
405 | lerpBloomFactor(bloomFactors[3]) * vec4(bloomTintColors[3], 1.0) * texture2D(blurTexture4, vUv) +
406 | lerpBloomFactor(bloomFactors[4]) * vec4(bloomTintColors[4], 1.0) * texture2D(blurTexture5, vUv) );
407 | }`
408 | });
409 | }
410 | });
411 |
412 | UnrealBloomPass.BlurDirectionX = new Vector2(1.0, 0.0);
413 | UnrealBloomPass.BlurDirectionY = new Vector2(0.0, 1.0);
414 |
415 | export default UnrealBloomPass;
416 |
--------------------------------------------------------------------------------
/src/game/graphics/renderer.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | import { Platform } from "react-native";
3 | import ExpoGraphics from "expo-graphics-rnge";
4 | import ExpoTHREE, { THREE } from "expo-three";
5 | import EffectComposer from "./effect-composer";
6 | import RenderPass from "./passes/render-pass";
7 | import _ from "lodash";
8 |
9 | global.THREE = THREE;
10 |
11 | THREE.suppressExpoWarnings();
12 |
13 | class ThreeView extends PureComponent {
14 |
15 | onShouldReloadContext = () => {
16 | return Platform.OS === "android";
17 | };
18 |
19 | onContextCreate = async ({ gl, canvas, width, height, scale: pixelRatio }) => {
20 | this.props.camera.resize(width, height, pixelRatio);
21 | this.renderer = new ExpoTHREE.Renderer({
22 | gl,
23 | pixelRatio,
24 | width,
25 | height,
26 | });
27 | this.renderer.setClearColor(0x020202, 1.0);
28 | this.gl = gl;
29 | this.composer = new EffectComposer(this.renderer);
30 |
31 | //-- Toggle line below if you have issues with shadows and/or post-processing effects
32 | this.gl.createRenderbuffer = () => {};
33 |
34 | const passes = [
35 | new RenderPass(this.props.scene, this.props.camera),
36 | ...this.props.passes
37 | ];
38 |
39 | passes.forEach(p => this.composer.addPass(p))
40 | passes[passes.length-1].renderToScreen = true;
41 | };
42 |
43 | onResize = ({ width, height, scale: pixelRatio }) => {
44 | this.props.camera.resize(width, height, pixelRatio);
45 | this.renderer.setSize(width, height);
46 | this.renderer.setPixelRatio(pixelRatio);
47 | };
48 |
49 | render() {
50 | if (this.composer && this.gl) {
51 | this.composer.render();
52 | this.gl.endFrameEXP();
53 | }
54 | return (
55 |
62 | );
63 | }
64 | }
65 |
66 | const renderHUD = (entities, screen) => {
67 | if (!entities.hud) return null;
68 |
69 | const hud = entities.hud;
70 |
71 | if (typeof hud.renderer === "object")
72 | return ;
73 | else if (typeof hud.renderer === "function")
74 | return ;
75 | };
76 |
77 | const ThreeJSRenderer = (...passes) => (entities, screen) => {
78 | if (!entities) return null;
79 | return [
80 | ,
86 | renderHUD(entities, screen)
87 | ];
88 | };
89 |
90 | export default ThreeJSRenderer;
--------------------------------------------------------------------------------
/src/game/graphics/shaders/copy-shader.js:
--------------------------------------------------------------------------------
1 | import { THREE } from 'expo-three';
2 |
3 | /**
4 | * @author alteredq / http://alteredqualia.com/
5 | *
6 | * Full-screen textured quad shader
7 | */
8 |
9 | THREE.CopyShader = {
10 |
11 | uniforms: {
12 |
13 | "tDiffuse": { value: null },
14 | "opacity": { value: 1.0 }
15 |
16 | },
17 |
18 | vertexShader: [
19 |
20 | "varying vec2 vUv;",
21 |
22 | "void main() {",
23 |
24 | "vUv = uv;",
25 | "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
26 |
27 | "}"
28 |
29 | ].join( "\n" ),
30 |
31 | fragmentShader: [
32 |
33 | "uniform float opacity;",
34 |
35 | "uniform sampler2D tDiffuse;",
36 |
37 | "varying vec2 vUv;",
38 |
39 | "void main() {",
40 |
41 | "vec4 texel = texture2D( tDiffuse, vUv );",
42 | "gl_FragColor = opacity * texel;",
43 |
44 | "}"
45 |
46 | ].join( "\n" )
47 |
48 | };
49 |
50 | export default THREE.CopyShader;
--------------------------------------------------------------------------------
/src/game/graphics/shaders/luminosity-high-pass-shader.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author bhouston / http://clara.io/
3 | *
4 | * Luminosity
5 | * http://en.wikipedia.org/wiki/Luminosity
6 | */
7 |
8 | import { THREE } from "expo-three";
9 |
10 | const LuminosityHighPassShader = {
11 |
12 | shaderID: "luminosityHighPass",
13 |
14 | uniforms: {
15 |
16 | "tDiffuse": { value: null },
17 | "luminosityThreshold": { value: 1.0 },
18 | "smoothWidth": { value: 1.0 },
19 | "defaultColor": { value: new THREE.Color( 0x000000 ) },
20 | "defaultOpacity": { value: 0.0 }
21 |
22 | },
23 |
24 | vertexShader: [
25 |
26 | "varying vec2 vUv;",
27 |
28 | "void main() {",
29 |
30 | "vUv = uv;",
31 |
32 | "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
33 |
34 | "}"
35 |
36 | ].join("\n"),
37 |
38 | fragmentShader: [
39 |
40 | "uniform sampler2D tDiffuse;",
41 | "uniform vec3 defaultColor;",
42 | "uniform float defaultOpacity;",
43 | "uniform float luminosityThreshold;",
44 | "uniform float smoothWidth;",
45 |
46 | "varying vec2 vUv;",
47 |
48 | "void main() {",
49 |
50 | "vec4 texel = texture2D( tDiffuse, vUv );",
51 |
52 | "vec3 luma = vec3( 0.299, 0.587, 0.114 );",
53 |
54 | "float v = dot( texel.xyz, luma );",
55 |
56 | "vec4 outputColor = vec4( defaultColor.rgb, defaultOpacity );",
57 |
58 | "float alpha = smoothstep( luminosityThreshold, luminosityThreshold + smoothWidth, v );",
59 |
60 | "gl_FragColor = mix( outputColor, texel, alpha );",
61 |
62 | "}"
63 |
64 | ].join("\n")
65 |
66 | };
67 |
68 | export default LuminosityHighPassShader;
--------------------------------------------------------------------------------
/src/game/graphics/shaders/pixel-shader.js:
--------------------------------------------------------------------------------
1 | import { THREE } from "expo-three";
2 | import { screen } from "../../utils";
3 |
4 | export default ({
5 | pixelSize = 5,
6 | borderSize = 1,
7 | lightenFactor = 1.8,
8 | softenFactor = 0.75,
9 | darkenFactor = 0.5,
10 | resolution = new THREE.Vector2(screen.width, screen.height)
11 | } = {}) => {
12 | const pixelShader = {
13 | uniforms: {
14 | tDiffuse: { value: null },
15 | pixelSize: { value: pixelSize },
16 | borderFraction: { value: borderSize / pixelSize },
17 | lightenFactor: { value: lightenFactor },
18 | softenFactor: { value: softenFactor },
19 | darkenFactor: { value: darkenFactor },
20 | resolution: { value: resolution }
21 | },
22 |
23 | vertexShader: `
24 | varying highp vec2 vUv;
25 |
26 | void main() {
27 | vUv = uv;
28 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
29 | }`,
30 |
31 | fragmentShader: `
32 | uniform sampler2D tDiffuse;
33 | uniform float pixelSize;
34 | uniform float borderFraction;
35 | uniform float lightenFactor;
36 | uniform float softenFactor;
37 | uniform float darkenFactor;
38 | uniform vec2 resolution;
39 |
40 | varying highp vec2 vUv;
41 |
42 | void main(){
43 | vec2 dxy = pixelSize / resolution;
44 | vec2 pixel = vUv / dxy;
45 | vec2 fraction = fract(pixel);
46 | vec2 coord = dxy * floor(pixel);
47 | vec3 color = texture2D(tDiffuse, coord).xyz;
48 |
49 | if (fraction.y > (1.0 - borderFraction))
50 | color = color * lightenFactor;
51 |
52 | if (fraction.x < borderFraction)
53 | color = color * softenFactor;
54 |
55 | if (fraction.y < borderFraction)
56 | color = color * darkenFactor;
57 |
58 | gl_FragColor = vec4(color, 1);
59 | }`
60 | };
61 |
62 | return pixelShader;
63 | };
64 |
--------------------------------------------------------------------------------
/src/game/graphics/shaders/scanline-shader.js:
--------------------------------------------------------------------------------
1 | import { THREE } from "expo-three";
2 |
3 | export default (thickness = 5.0, color = new THREE.Vector4(0, 0, 0, 1)) => {
4 | const scanlineShader = {
5 | uniforms: {
6 | tDiffuse: { value: null },
7 | thickness: { value: thickness },
8 | color: { value: color }
9 | },
10 |
11 | vertexShader: `
12 | varying vec2 vUv;
13 | void main() {
14 | vUv = uv;
15 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
16 | }`,
17 |
18 | fragmentShader: `
19 | uniform sampler2D tDiffuse;
20 | uniform float thickness;
21 | uniform vec4 color;
22 | varying vec2 vUv;
23 | void main() {
24 | float result = floor(mod(gl_FragCoord.y, thickness));
25 | gl_FragColor = result == 0.0 ? texture2D(tDiffuse, vUv) : color;
26 | }`
27 | };
28 |
29 | return scanlineShader;
30 | };
31 |
--------------------------------------------------------------------------------
/src/game/graphics/shaders/sepia-shader.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author alteredq / http://alteredqualia.com/
3 | *
4 | * Sepia tone shader
5 | * based on glfx.js sepia shader
6 | * https://github.com/evanw/glfx.js
7 | */
8 |
9 | export default (amount = 1.0) => {
10 | const sepiaShader = {
11 | uniforms: {
12 | tDiffuse: { value: null },
13 | amount: { value: amount }
14 | },
15 |
16 | vertexShader: [
17 | "varying vec2 vUv;",
18 |
19 | "void main() {",
20 |
21 | "vUv = uv;",
22 | "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
23 |
24 | "}"
25 | ].join("\n"),
26 |
27 | fragmentShader: [
28 | "uniform float amount;",
29 |
30 | "uniform sampler2D tDiffuse;",
31 |
32 | "varying vec2 vUv;",
33 |
34 | "void main() {",
35 |
36 | "vec4 color = texture2D( tDiffuse, vUv );",
37 | "vec3 c = color.rgb;",
38 |
39 | "color.r = dot( c, vec3( 1.0 - 0.607 * amount, 0.769 * amount, 0.189 * amount ) );",
40 | "color.g = dot( c, vec3( 0.349 * amount, 1.0 - 0.314 * amount, 0.168 * amount ) );",
41 | "color.b = dot( c, vec3( 0.272 * amount, 0.534 * amount, 1.0 - 0.869 * amount ) );",
42 |
43 | "gl_FragColor = vec4( min( vec3( 1.0 ), color.rgb ), color.a );",
44 |
45 | "}"
46 | ].join("\n")
47 | };
48 |
49 | return sepiaShader;
50 | };
51 |
--------------------------------------------------------------------------------
/src/game/graphics/shaders/tri-color-shader.js:
--------------------------------------------------------------------------------
1 | import { THREE } from "expo-three";
2 |
3 | export default ({ distance = 0.005, threshold = 0, colors = [new THREE.Color(0xFFFFFF), new THREE.Color(0x362928), new THREE.Color(0xFF3526)] } = {}) => {
4 | const triColorShader = {
5 | uniforms: {
6 | tDiffuse: { value: null },
7 | distance: { value: distance },
8 | threshold: { value: threshold },
9 | colors: {
10 | value: colors.map(x => new THREE.Vector4(x.r, x.g, x.b, 1))
11 | }
12 | },
13 |
14 | vertexShader: `
15 | varying vec2 vUv;
16 | void main() {
17 | vUv = uv;
18 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
19 | }`,
20 |
21 | fragmentShader: `
22 | uniform sampler2D tDiffuse;
23 | uniform float distance;
24 | uniform float threshold;
25 | uniform vec4 colors[3];
26 | varying vec2 vUv;
27 |
28 | void main() {
29 | vec4 tex = texture2D(tDiffuse, vUv);
30 | vec4 tex2 = texture2D(tDiffuse, vec2(vUv.x + distance, vUv.y));
31 |
32 | float test = tex.r + tex.g + tex.b;
33 | float test2 = tex2.r + tex2.g + tex2.b;
34 | float diff = test2 - test;
35 |
36 | if(diff < -threshold)
37 | tex = colors[0];
38 | else if (diff > threshold)
39 | tex = colors[1];
40 | else
41 | tex = colors[2];
42 |
43 | gl_FragColor = tex;
44 | }`
45 | };
46 |
47 | return triColorShader;
48 | };
49 |
--------------------------------------------------------------------------------
/src/game/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { GameEngine } from "react-native-game-engine";
3 | import Renderer from "./graphics/renderer";
4 | import Systems from "./systems";
5 | import Entities from "./entities";
6 | import Timer from "./utils/perf-timer";
7 | import ShaderPass from "./graphics/passes/shader-pass";
8 | import PixelShader from "./graphics/shaders/pixel-shader";
9 |
10 | class Game extends React.Component {
11 | render() {
12 | return (
13 |
22 | );
23 | }
24 | }
25 |
26 | export default Game;
27 |
--------------------------------------------------------------------------------
/src/game/systems/basic-physics.js:
--------------------------------------------------------------------------------
1 | import { all } from "../utils";
2 |
3 | const Physics = entities => {
4 | const physicsEntities = all(entities, e => e.physics);
5 |
6 | physicsEntities.forEach(e => {
7 | const {
8 | mass,
9 | forces,
10 | acceleration,
11 | velocity,
12 | position,
13 | maxSpeed,
14 | damping
15 | } = e.physics;
16 |
17 | forces.divideScalar(mass);
18 | acceleration.add(forces);
19 |
20 | if (damping) velocity.multiplyScalar(1 - damping);
21 |
22 | velocity.add(acceleration);
23 |
24 | if (maxSpeed) velocity.clampLength(0, maxSpeed);
25 |
26 | position.add(velocity);
27 |
28 | forces.set(0, 0, 0);
29 | acceleration.set(0, 0, 0);
30 | });
31 |
32 | return entities;
33 | };
34 |
35 | export default Physics;
36 |
--------------------------------------------------------------------------------
/src/game/systems/camera.js:
--------------------------------------------------------------------------------
1 | import { rotateAroundPoint } from "../utils/three";
2 |
3 | const Camera = ({
4 | yawSpeed = 0.01,
5 | pitchSpeed = 0.01,
6 | zoomSpeed = 0.02
7 | } = {}) => {
8 | return (entities, { touchController }) => {
9 | const camera = entities.camera;
10 |
11 | if (camera && touchController) {
12 | //-- Yaw and pitch rotation
13 | if (touchController.multiFingerMovement.x || touchController.multiFingerMovement.y) {
14 | rotateAroundPoint(camera, camera.target, {
15 | y: touchController.multiFingerMovement.x * yawSpeed,
16 | x: touchController.multiFingerMovement.y * pitchSpeed
17 | });
18 | camera.lookAt(camera.target);
19 | }
20 |
21 | //-- Zooming (pinching)
22 | if (touchController.pinch) {
23 | const zoomFactor = touchController.pinch * zoomSpeed;
24 |
25 | camera.zoom += zoomFactor;
26 | camera.updateProjectionMatrix();
27 | }
28 | }
29 |
30 | return entities;
31 | };
32 | };
33 |
34 | export default Camera;
35 |
--------------------------------------------------------------------------------
/src/game/systems/collisions.js:
--------------------------------------------------------------------------------
1 | import { allKeys } from "../utils";
2 | import { QuadTree, Box, Point, Circle } from "js-quadtree";
3 | import _ from "lodash";
4 |
5 | const createTree = (collideableKeys, entities, { x = -50, y = -50, width = 100, height = 100 } = {}) => {
6 | const tree = new QuadTree(
7 | new Box(x, y, width, height)
8 | );
9 |
10 | for (let i = 0; i < collideableKeys.length; i++) {
11 | const key = collideableKeys[i];
12 | const collideable = entities[key];
13 |
14 | tree.insert(
15 | new Point(
16 | collideable.physics.position.x,
17 | collideable.physics.position.z,
18 | { entityId: key }
19 | )
20 | );
21 | }
22 |
23 | return tree;
24 | };
25 |
26 | const queryTree = (tree, collideable) => {
27 | return tree.query(
28 | new Circle(
29 | collideable.physics.position.x,
30 | collideable.physics.position.z,
31 | collideable.collisions.sweepRadius
32 | )
33 | );
34 | };
35 |
36 | const hitTests = [
37 | ["isBox3", "isBox3", (b1, b2) => b1.intersectsBox(b2)],
38 | ["isBox3", "isSphere", (b1, b2) => b1.intersectsSphere(b2)],
39 | ["isBox3", "isPlane", (b1, b2) => b1.intersectsPlane(b2)],
40 | ["isSphere", "isBox3", (b1, b2) => b1.intersectsBox(b2)],
41 | ["isSphere", "isSphere", (b1, b2) => b1.intersectsSphere(b2)],
42 | ["isSphere", "isPlane", (b1, b2) => b1.intersectsPlane(b2)],
43 | ["isPlane", "isBox3", (b1, b2) => b1.intersectsBox(b2)],
44 | ["isPlane", "isSphere", (b1, b2) => b1.intersectsSphere(b2)]
45 | ];
46 |
47 | const collided = (hitTest, bounds, otherBounds) => {
48 | //-- This could be extended to handle the case where bounds
49 | //-- and otherBounds are arrays (for complex models)
50 | return (
51 | bounds[hitTest[0]] &&
52 | otherBounds[hitTest[1]] &&
53 | hitTest[2](bounds, otherBounds)
54 | );
55 | };
56 |
57 | const notify = (defer, key, otherKey, collideable, other, force) => {
58 | defer({
59 | type: "collision",
60 | entities: [collideable, other],
61 | keys: [key, otherKey],
62 | force
63 | });
64 | };
65 |
66 | const Collisions = config => (entities, args) => {
67 | const collideableKeys = allKeys(entities, e => e.collisions && e.physics);
68 |
69 | if (collideableKeys.length) {
70 | //-- Populate tree
71 |
72 | const tree = createTree(collideableKeys, entities, config);
73 |
74 | //-- Query tree
75 |
76 | for (let i = 0; i < collideableKeys.length; i++) {
77 | const key = collideableKeys[i];
78 | const entity = entities[key];
79 | const entityCollisions = entity.collisions;
80 |
81 | //-- Continue if this entity is a hit target
82 |
83 | if (entityCollisions.predicate) {
84 | const results = queryTree(tree, entity);
85 |
86 | //-- Continue if another entity was found in the vicinity
87 |
88 | if (results.length > 1) {
89 | const bounds = _.isFunction(entityCollisions.bounds)
90 | ? entityCollisions.bounds()
91 | : entityCollisions.bounds;
92 |
93 | for (let j = 0; j < results.length; j++) {
94 | const otherKey = results[j].data.entityId;
95 | const other = entities[otherKey];
96 | const otherCollisions = other.collisions;
97 |
98 | if (key === otherKey) continue;
99 |
100 | //-- Does the current entity care about the one he has collied with?
101 |
102 | if (entityCollisions.predicate(entity, other)) {
103 | const otherBounds = _.isFunction(otherCollisions.bounds)
104 | ? otherCollisions.bounds()
105 | : otherCollisions.bounds;
106 |
107 | for (let k = 0; k < hitTests.length; k++) {
108 | const test = hitTests[k];
109 |
110 | //-- Check whether an actual collision occured using proper bounds
111 |
112 | if (collided(test, bounds, otherBounds)) {
113 | const force = entity.physics.velocity
114 | .clone()
115 | .multiplyScalar(entity.physics.mass)
116 | .add(
117 | other.physics.velocity
118 | .clone()
119 | .multiplyScalar(other.physics.mass)
120 | );
121 |
122 | if (entityCollisions.hit)
123 | entityCollisions.hit(entity, other, force, args);
124 |
125 | if (entityCollisions.notify)
126 | notify(args.defer, key, otherKey, entity, other, force, args);
127 |
128 | break;
129 | }
130 | }
131 | }
132 | }
133 | }
134 | }
135 | }
136 | }
137 |
138 | return entities;
139 | };
140 |
141 | export default Collisions;
142 |
--------------------------------------------------------------------------------
/src/game/systems/gamepad-controller.js:
--------------------------------------------------------------------------------
1 | import { screen } from "../utils/index";
2 | import { Vibration } from 'react-native';
3 |
4 | const padding = 10;
5 |
6 | const stickRadius = 50;
7 | const stickPosition = {
8 | x: stickRadius + padding,
9 | y: screen.height - stickRadius - padding
10 | };
11 |
12 | const aRadius = 25;
13 | const aPosition = {
14 | x: screen.width - aRadius * 2.75 - padding,
15 | y: screen.height - aRadius - padding
16 | };
17 |
18 | const bRadius = 25;
19 | const bPosition = {
20 | x: screen.width - bRadius - padding,
21 | y: screen.height - bRadius * 2.75 - padding
22 | };
23 |
24 | const distance = (touch, pos) => {
25 | return Math.hypot(touch.event.pageX - pos.x, touch.event.pageY - pos.y);
26 | };
27 |
28 | const subtract = (touch, pos) => {
29 | return { x: touch.event.pageX - pos.x, y: touch.event.pageY - pos.y };
30 | };
31 |
32 | const clamp = (vec, radius) => {
33 | const dist = Math.hypot(vec.x, vec.y);
34 |
35 | if (dist < radius)
36 | return vec;
37 |
38 | return {
39 | x: vec.x * (radius / dist),
40 | y: vec.y * (radius / dist)
41 | }
42 | };
43 |
44 | const normalize = (vec, radius) => {
45 | return {
46 | x: vec.x / radius,
47 | y: vec.y / radius,
48 | heading: Math.atan2(vec.y, vec.x)
49 | }
50 | }
51 |
52 | const isTouchingPosition = (pos, proxmity) => {
53 | let touching = false;
54 | let id = null;
55 |
56 | return touches => {
57 | if (!touching) {
58 | const down = touches.find(
59 | t =>
60 | (t.type === "start" || t.type === "move") &&
61 | distance(t, pos) < proxmity
62 | );
63 |
64 | if (down) {
65 | touching = true;
66 | id = down.event.identifier;
67 | }
68 | } else {
69 | const up =
70 | touches.find(t => t.type === "end" && t.event.identifier == id) ||
71 | touches.find(
72 | t =>
73 | t.type === "move" &&
74 | t.event.identifier === id &&
75 | distance(t, pos) > proxmity
76 | );
77 |
78 | if (up) {
79 | touching = false;
80 | id = null;
81 | }
82 | }
83 |
84 | return touching;
85 | };
86 | };
87 |
88 | const neutral = { x: 0, y: 0, heading: null };
89 |
90 | const trackNormalFromPosition = (pos, radius, proxmity) => {
91 | let normal = null;
92 | let id = null;
93 |
94 | return touches => {
95 | if (!normal) {
96 | const down = touches.find(
97 | t =>
98 | (t.type === "start" || t.type === "move") &&
99 | distance(t, pos) < proxmity
100 | );
101 |
102 | if (down) {
103 | const vec = subtract(down, pos);
104 | const clamped = clamp(vec, radius);
105 |
106 | normal = normalize(clamped, radius);
107 | id = down.event.identifier;
108 | }
109 | } else {
110 | const move = touches.find(
111 | t =>
112 | t.type === "move" &&
113 | t.event.identifier === id &&
114 | distance(t, pos) < proxmity
115 | );
116 |
117 | if (move) {
118 | const vec = subtract(move, pos);
119 | const clamped = clamp(vec, radius);
120 |
121 | normal = normalize(clamped, radius);
122 | } else {
123 | const up =
124 | touches.find(t => t.type === "end" && t.event.identifier === id) ||
125 | touches.find(
126 | t =>
127 | t.type === "move" &&
128 | t.event.identifier === id &&
129 | distance(t, pos) > proxmity
130 | );
131 |
132 | if (up) {
133 | normal = null;
134 | id = null;
135 | }
136 | }
137 | }
138 |
139 | return normal || neutral;
140 | };
141 | };
142 |
143 | const vibrate = (patternOrDuration, repeat) => {
144 | Vibration.vibrate(patternOrDuration, repeat);
145 | };
146 |
147 | const isTouchingA = isTouchingPosition(aPosition, aRadius + 20);
148 | const isTouchingB = isTouchingPosition(bPosition, bRadius + 20);
149 | const trackNormalFromStick = trackNormalFromPosition(stickPosition, stickRadius, stickRadius + 40)
150 |
151 | let previous = {};
152 |
153 | const GamepadController = (Wrapped = x => x) => (entities, args) => {
154 | if (!args.gamepadController) {
155 | const current = {
156 | ...trackNormalFromStick(args.touches),
157 | a: isTouchingA(args.touches),
158 | b: isTouchingB(args.touches),
159 | vibrate
160 | };
161 |
162 | args.gamepadController = Object.assign(
163 | { stickRadius, stickPosition, aRadius, aPosition, bRadius, bPosition },
164 | current,
165 | { previous }
166 | );
167 |
168 | previous = current;
169 | }
170 |
171 | return Wrapped(entities, args);
172 | };
173 |
174 | export default GamepadController;
175 |
--------------------------------------------------------------------------------
/src/game/systems/gravity.js:
--------------------------------------------------------------------------------
1 | import * as THREE from "three";
2 | import { all } from "../utils";
3 |
4 | const g = new THREE.Vector3(0, -0.08, 0);
5 |
6 | const Gravity = entities => {
7 | const gravityEntities = all(entities, e => e.gravity && e.physics);
8 |
9 | gravityEntities.forEach(e => {
10 | e.physics.forces.add(e.gravity.isVector3 ? e.gravity : g);
11 | });
12 |
13 | return entities;
14 | };
15 |
16 | export default Gravity;
17 |
--------------------------------------------------------------------------------
/src/game/systems/hud.js:
--------------------------------------------------------------------------------
1 | const HUD = (entities, args) => {
2 |
3 | const hud = entities.hud;
4 |
5 | if (hud) {
6 | hud.gamepadController = args.gamepadController;
7 | }
8 |
9 | return entities;
10 | };
11 |
12 | export default HUD;
--------------------------------------------------------------------------------
/src/game/systems/index.js:
--------------------------------------------------------------------------------
1 | import Camera from "./camera";
2 | import Particles from "./particles";
3 | import Removal from "./removal";
4 | import Rotation from "./rotation";
5 | import Timeline from "./timeline";
6 | import HUD from "./hud";
7 | import GamepadController from "./gamepad-controller";
8 | import TouchController from "./touch-controller";
9 | import Physics from "./physics";
10 | import Spawn from "./spawn";
11 |
12 | export default [
13 | GamepadController(),
14 | TouchController()(),
15 | Camera({ pitchSpeed: -0.01, yawSpeed: 0.01 }),
16 | Particles,
17 | Removal,
18 | Rotation,
19 | Timeline,
20 | Spawn,
21 | Physics,
22 | HUD
23 | ];
24 |
--------------------------------------------------------------------------------
/src/game/systems/particles.js:
--------------------------------------------------------------------------------
1 | import { all } from "../utils";
2 |
3 | const Particles = (entities, args) => {
4 | const { time } = args;
5 | const entitiesWithParticles = all(entities, e => e.particles);
6 |
7 | for (let i = 0; i < entitiesWithParticles.length; i++) {
8 | const entity = entitiesWithParticles[i];
9 | const keys = Object.keys(entity.particles);
10 |
11 | for (let j = 0; j < keys.length; j++) {
12 | const ps = entity.particles[keys[j]];
13 | const { spawnOptions, options, beforeSpawn } = ps;
14 | const delta = (time.delta / 1000) * spawnOptions.timeScale;
15 |
16 | ps.tick += delta;
17 |
18 | if (ps.tick < 0) ps.tick = 0;
19 |
20 | if (delta > 0) {
21 | beforeSpawn(entity, entities, ps, args);
22 |
23 | for (let x = 0; x < spawnOptions.spawnRate * delta; x++) {
24 | ps.emitter.spawnParticle(options);
25 | }
26 | }
27 |
28 | ps.emitter.update(ps.tick);
29 | }
30 | }
31 |
32 | return entities;
33 | };
34 |
35 | export default Particles;
36 |
--------------------------------------------------------------------------------
/src/game/systems/physics.js:
--------------------------------------------------------------------------------
1 | import { all } from "../utils";
2 |
3 | const Physics = (entities, args) => {
4 | const world = entities.world;
5 | const entitiesWithBodies = all(entities, e => e.bodies && e.model);
6 |
7 | if (world)
8 | world.step();
9 |
10 | for (let x = 0; x < entitiesWithBodies.length; x++) {
11 | const entity = entitiesWithBodies[x];
12 | const model = entity.model;
13 | const body = entity.bodies[0];
14 | const collision = entity.collision;
15 |
16 | if (!body.sleeping) {
17 | model.position.copy(body.getPosition());
18 | model.quaternion.copy(body.getQuaternion());
19 | }
20 |
21 | if (collision) {
22 | for (let y = 0; y < entitiesWithBodies.length; y++) {
23 | if (x === y)
24 | continue;
25 |
26 | const otherEntity = entitiesWithBodies[y];
27 | const otherBody = otherEntity.bodies[0];
28 | const contact = world.getContact(body, otherBody);
29 |
30 | if (contact)
31 | collision(entity, otherEntity, contact, entities, args);
32 | }
33 | }
34 | }
35 |
36 | return entities;
37 | };
38 |
39 | export default Physics;
40 |
--------------------------------------------------------------------------------
/src/game/systems/removal.js:
--------------------------------------------------------------------------------
1 | import { THREE } from "expo-three";
2 | import { remove } from "../utils";
3 | import _ from "lodash";
4 |
5 | //-- https://gist.github.com/zentrope/5022d89cfa995ac71978
6 |
7 | const frustum = new THREE.Frustum();
8 | const cameraViewProjectionMatrix = new THREE.Matrix4();
9 |
10 | const Removal = entities => {
11 | const camera = entities.camera;
12 | const removeableKeys = Object.keys(entities).filter(
13 | x => entities[x].removable
14 | );
15 |
16 | camera.updateMatrixWorld();
17 | camera.matrixWorldInverse.getInverse(camera.matrixWorld);
18 | cameraViewProjectionMatrix.multiplyMatrices(
19 | camera.projectionMatrix,
20 | camera.matrixWorldInverse
21 | );
22 | frustum.setFromMatrix(cameraViewProjectionMatrix);
23 |
24 | removeableKeys.forEach(key => {
25 | const test = entities[key].removable;
26 |
27 | if (_.isFunction(test) ? test(frustum, entities[key], entities) : true)
28 | remove(entities, key);
29 | });
30 |
31 | return entities;
32 | };
33 |
34 | export default Removal;
35 |
--------------------------------------------------------------------------------
/src/game/systems/rotation.js:
--------------------------------------------------------------------------------
1 | import { all } from "../utils";
2 | import _ from "lodash";
3 |
4 | const Rotation = (entities, args) => {
5 | const rotatables = all(entities, e => e.rotation, e => e.model);
6 |
7 | for (let i = 0; i < rotatables.length; i++) {
8 | const r = rotatables[i];
9 |
10 | if (r.model.cellIndex !== undefined) {
11 | r.model.angle = _.isFunction(r.rotation)
12 | ? r.rotation(r, entities, args)
13 | : r.model.angle + r.rotation;
14 | } else {
15 | r.model.rotation.z = r.rotation.z
16 | ? _.isFunction(r.rotation.z)
17 | ? r.rotation.z(r, entities, args)
18 | : r.model.rotation.z + r.rotation.z
19 | : r.model.rotation.z;
20 | r.model.rotation.x = r.rotation.x
21 | ? _.isFunction(r.rotation.x)
22 | ? r.rotation.x(r, entities, args)
23 | : r.model.rotation.x + r.rotation.x
24 | : r.model.rotation.x;
25 | r.model.rotation.y = r.rotation.y
26 | ? _.isFunction(r.rotation.y)
27 | ? r.rotation.y(r, entities, args)
28 | : r.model.rotation.y + r.rotation.y
29 | : r.model.rotation.y;
30 | }
31 | }
32 |
33 | return entities;
34 | };
35 |
36 | export default Rotation;
37 |
--------------------------------------------------------------------------------
/src/game/systems/spawn.js:
--------------------------------------------------------------------------------
1 | import Box from "../components/box"
2 | import Cylinder from "../components/cylinder"
3 | import { id } from "../utils";
4 |
5 | const boxId = (id => () => id("box"))(id(0));
6 | const cylinderId = (id => () => id("cylinder"))(id(0));
7 |
8 | const Spawn = (entities, { gamepadController }) => {
9 |
10 | const world = entities.world;
11 | const scene = entities.scene;
12 |
13 | if (gamepadController.a && !gamepadController.previous.a)
14 | entities[boxId()] = Box({ parent: scene, world, y: 5 });
15 |
16 | if (gamepadController.b && !gamepadController.previous.b)
17 | entities[cylinderId()] = Cylinder({ parent: scene, world, y: 5 });
18 |
19 | return entities;
20 | };
21 |
22 | export default Spawn;
23 |
--------------------------------------------------------------------------------
/src/game/systems/spring.js:
--------------------------------------------------------------------------------
1 | import * as THREE from "three";
2 | import { all } from "../utils";
3 |
4 | const Spring = entities => {
5 | const spingEntities = all(entities, e => e.spring && e.physics);
6 |
7 | spingEntities.forEach(e => {
8 | const {
9 | spring: { k, length, anchor, subtract },
10 | physics: { position, forces }
11 | } = e;
12 |
13 | const spring = subtract
14 | ? subtract(position, anchor, e.spring)
15 | : new THREE.Vector3().subVectors(position, anchor);
16 | const d = spring.length();
17 | const stretch = d - length;
18 |
19 | spring.normalize();
20 | spring.multiplyScalar(-1 * k * stretch);
21 |
22 | forces.add(spring);
23 | });
24 |
25 | return entities;
26 | };
27 |
28 | export default Spring;
29 |
--------------------------------------------------------------------------------
/src/game/systems/timeline.js:
--------------------------------------------------------------------------------
1 | import { all } from "../utils";
2 | import _ from "lodash";
3 |
4 | const start = (timeline, args) => {
5 | if (!timeline.start) timeline.start = args.time.current;
6 | };
7 |
8 | const update = (entity, entities, key, timeline, args) => {
9 | const time = args.time;
10 |
11 | if (timeline.duration) {
12 | let percent = (time.current - timeline.start) / timeline.duration;
13 |
14 | if (percent <= 1) {
15 | timeline.update(entity, entities, percent, timeline, args);
16 | } else {
17 | if (timeline.complete)
18 | timeline.complete(entity, entities, timeline, args);
19 |
20 | delete entity.timelines[key];
21 | }
22 | }
23 |
24 | if (timeline.while) {
25 | if (
26 | _.isFunction(timeline.while)
27 | ? timeline.while(entity, entities, timeline, args)
28 | : true
29 | ) {
30 | timeline.update(entity, entities, timeline, args);
31 | } else {
32 | if (timeline.complete)
33 | timeline.complete(entity, entities, timeline, args);
34 |
35 | delete entity.timelines[key];
36 | }
37 | }
38 | };
39 |
40 | const Timeline = (entities, args) => {
41 | const entitiesWithTimelines = all(entities, e => e.timelines);
42 |
43 | for (let i = 0; i < entitiesWithTimelines.length; i++) {
44 | const entity = entitiesWithTimelines[i];
45 | const keys = Object.keys(entity.timelines);
46 |
47 | for (let j = 0; j < keys.length; j++) {
48 | const key = keys[j];
49 | const timeline = entity.timelines[key];
50 |
51 | if (timeline) {
52 | start(timeline, args);
53 | update(entity, entities, key, timeline, args);
54 | }
55 | }
56 | }
57 |
58 | return entities;
59 | };
60 |
61 | export default Timeline;
62 |
--------------------------------------------------------------------------------
/src/game/systems/touch-controller.js:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 |
3 | const neutral = { x: 0, y: 0 };
4 |
5 | const singleFingerMovement = moves => {
6 | if (moves.length === 1) {
7 | const f1 = moves[0];
8 |
9 | return {
10 | x: f1.delta.locationX,
11 | y: f1.delta.locationY
12 | };
13 | }
14 |
15 | return neutral;
16 | };
17 |
18 | const multiFingerMovement = moves => {
19 | if (moves.length > 1) {
20 | const f1 = moves[0];
21 | const f2 = moves[1];
22 |
23 | return {
24 | x: (f1.delta.locationX + f2.delta.locationX) / 2,
25 | y: (f1.delta.locationY + f2.delta.locationY) / 2
26 | };
27 | }
28 |
29 | return neutral;
30 | };
31 |
32 | const pinch = (moves, pinchThreshold) => {
33 | if (moves.length === 2) {
34 |
35 | const f1 = moves[0];
36 | const f2 = moves[1];
37 |
38 | const f1Pos = { x: f1.event.pageX, y: f1.event.pageY };
39 | const f1PosPrev = { x: f1Pos.x - f1.delta.pageX, y: f1Pos.y - f1.delta.pageY };
40 |
41 | const f2Pos = { x: f2.event.pageX, y: f2.event.pageY };
42 | const f2PosPrev = { x: f2Pos.x - f2.delta.pageX, y: f2Pos.y - f2.delta.pageY };
43 |
44 | const currentDistance = Math.hypot(f1Pos.x - f2Pos.x, f1Pos.y - f2Pos.y);
45 | const previousDistance = Math.hypot(f1PosPrev.x - f2PosPrev.x, f1PosPrev.y - f2PosPrev.y)
46 |
47 | if (currentDistance > pinchThreshold)
48 | return currentDistance - previousDistance;
49 | }
50 |
51 | return 0;
52 | };
53 |
54 | const find = type => touches => {
55 | const found = touches.find(x => x.type === type)
56 |
57 | if (found)
58 | return found.event;
59 | }
60 |
61 | const press = find("press");
62 |
63 | const start = find("start")
64 |
65 | let previous = {};
66 |
67 | const TouchController = ({ pinchThreshold = 150 } = {}) => (Wrapped = x => x) => (entities, args) => {
68 | if (!args.touchController) {
69 | const touches = args.touches;
70 | const moves = _.uniqBy(touches.filter(x => x.type === "move"), x => x.event.identifier);
71 |
72 | const current = {
73 | singleFingerMovement: singleFingerMovement(moves),
74 | multiFingerMovement: multiFingerMovement(moves),
75 | pinch: pinch(moves, pinchThreshold),
76 | press: press(touches),
77 | start: start(touches)
78 | };
79 |
80 | args.touchController = Object.assign(
81 | {},
82 | current,
83 | { previous }
84 | );
85 |
86 | previous = current;
87 | }
88 |
89 | return Wrapped(entities, args);
90 | };
91 |
92 | export default TouchController;
93 |
--------------------------------------------------------------------------------
/src/game/utils/index.js:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 | import { interpolate } from '@popmotion/popcorn';
3 | import { Dimensions } from "react-native";
4 | import * as ThreeUtils from "./three";
5 | import { Audio } from "expo-av";
6 |
7 | const remove = (entities, key) => {
8 | const entity = entities[key];
9 |
10 | if (!entity)
11 | return;
12 |
13 | if (entity.model)
14 | ThreeUtils.remove(entity.model.parent, entity.model);
15 |
16 | if (entity.light)
17 | ThreeUtils.remove(entity.light.parent, entity.light);
18 |
19 | if (entity.particles) {
20 | Object.keys(entity.particles).forEach(k => {
21 | const emitter = entity.particles[k].emitter
22 | if (emitter)
23 | ThreeUtils.remove(emitter.parent, emitter);
24 | })
25 | }
26 |
27 | if (entity.bodies)
28 | entity.bodies.forEach(b => b.remove())
29 |
30 | delete entities[key];
31 |
32 | return entities;
33 | };
34 |
35 | const any = (arr = [], b = "", c) => {
36 | if (c) {
37 | if (Array.isArray(c) === false) c = [c];
38 |
39 | return _.isFunction(b)
40 | ? _.intersection(arr.map(b), c).length > 0
41 | : _.intersection(arr.map(x => x[b]), c).length > 0;
42 | }
43 |
44 | if (!b) return arr.length > 0;
45 |
46 | if (Array.isArray(b)) return _.intersection(arr, b).length > 0;
47 |
48 | if (_.isFunction(b)) return arr.find(b);
49 |
50 | return arr.indexOf(b) > -1;
51 | };
52 |
53 | const first = (entities, ...predicates) => {
54 | if (!entities) return;
55 | if (!predicates || predicates.length < 1) return entities[0];
56 |
57 | if (Array.isArray(entities))
58 | return entities.find(e => _.every(predicates, p => p(e)))
59 |
60 | return entities[Object.keys(entities).find(key => _.every(predicates, p => p(entities[key])))]
61 | }
62 |
63 | const firstKey = (entities, ...predicates) => {
64 | if (!entities) return;
65 | if (!predicates || predicates.length < 1) return Object.keys(entities)[0];
66 |
67 | return Object.keys(entities).find(key => _.every(predicates, p => p(entities[key])))
68 | }
69 |
70 | const all = (entities, ...predicates) => {
71 | if (!entities) return;
72 | if (!predicates || predicates.length < 1) return entities;
73 |
74 | if (Array.isArray(entities))
75 | return entities.filter(e => _.every(predicates, p => p(e)))
76 |
77 | return Object.keys(entities).filter(key => _.every(predicates, p => p(entities[key]))).map(key => entities[key])
78 | }
79 |
80 | const allKeys = (entities, ...predicates) => {
81 | if (!entities) return;
82 | if (!predicates || predicates.length < 1) return Object.keys(entities);
83 |
84 | return Object.keys(entities).filter(key => _.every(predicates, p => p(entities[key])));
85 | }
86 |
87 | //-- https://stackoverflow.com/a/7616484/138392
88 | const getHashCode = str => {
89 | var hash = 0, i, chr;
90 | if (str.length === 0) return hash;
91 | for (i = 0; i < str.length; i++) {
92 | chr = str.charCodeAt(i);
93 | hash = ((hash << 5) - hash) + chr;
94 | hash |= 0; // Convert to 32bit integer
95 | }
96 | return hash;
97 | };
98 |
99 | const positive = val => Math.abs(val)
100 |
101 | const negative = val => {
102 | if (val > 0) return -val
103 | return val
104 | }
105 |
106 | const remap = (n, start1, stop1, start2, stop2) => {
107 | return (n - start1) / (stop1 - start1) * (stop2 - start2) + start2;
108 | }
109 |
110 | const constrain = (n, low, high) => {
111 | return Math.max(Math.min(n, high), low);
112 | }
113 |
114 | const between = (n, low, high) => {
115 | return n > low && n < high
116 | }
117 |
118 | const pipe = (...funcs) => _.flow(_.flatten(funcs || []))
119 |
120 | const id = (seed = 0) => (prefix = "") => `${prefix}${++seed}`
121 |
122 | const cond = (condition, func) => {
123 | return (args) => {
124 | const test = _.isFunction(condition) ? condition(args) : condition
125 | return test ? func(args) : args
126 | }
127 | }
128 |
129 | const log = label => data => {
130 | console.log(label, data);
131 | return data;
132 | }
133 |
134 | const randomInt = (min = 0, max = 1) => Math.floor(Math.random() * (max - min + 1) + min);
135 |
136 | const throttle = (func, interval, defaultValue) => {
137 | let last = 0;
138 | return (...args) => {
139 | const current = performance.now();
140 | if ((current - last) > interval) {
141 | last = current;
142 | return func(...args);
143 | } else {
144 | return _.isFunction(defaultValue) ? defaultValue(...args) : defaultValue;
145 | }
146 | }
147 | }
148 |
149 | const screen = Dimensions.get("window");
150 |
151 | const createSound = (asset, throttleInterval = 0) => {
152 | const task = Audio.Sound.createAsync(asset);
153 |
154 | const play = () => {
155 | Promise.resolve(task).then(({ sound, status }) => {
156 | if (!status.isPlaying)
157 | sound.playFromPositionAsync(0)
158 | });
159 | };
160 |
161 | return throttleInterval ? throttle(play, throttleInterval) : play;
162 | }
163 |
164 | module.exports = {
165 | remove,
166 | any,
167 | find: _.find,
168 | filter: _.filter,
169 | first,
170 | firstKey,
171 | all,
172 | allKeys,
173 | getHashCode,
174 | positive,
175 | negative,
176 | remap,
177 | constrain,
178 | clamp: constrain,
179 | between,
180 | pipe,
181 | id,
182 | cond,
183 | interpolate,
184 | log,
185 | randomInt,
186 | once: _.once,
187 | memoize: _.memoize,
188 | throttle,
189 | screen,
190 | createSound,
191 | sound: createSound
192 | }
--------------------------------------------------------------------------------
/src/game/utils/perf-timer.js:
--------------------------------------------------------------------------------
1 | import { StyleSheet, Platform } from "react-native";
2 | import { DefaultTimer } from "react-native-game-engine";
3 |
4 | const ideal = 1000 / 60;
5 |
6 | class PerfTimer {
7 | constructor() {
8 | this.subscribers = [];
9 | this.loopId = null;
10 | this.last = 0;
11 | }
12 |
13 | loop = time => {
14 | if (this.loopId) {
15 | this.subscribers.forEach(callback => {
16 | callback(time);
17 | });
18 | }
19 |
20 | const now = new Date().getTime();
21 | const delay = ideal - (now - this.last)
22 |
23 | this.loopId = setTimeout(this.loop, delay > 0 ? delay : 0, now);
24 | this.last = now;
25 | };
26 |
27 | start() {
28 | if (!this.loopId) {
29 | this.loop();
30 | }
31 | }
32 |
33 | stop() {
34 | if (this.loopId) {
35 | clearTimeout(this.loopId);
36 | this.loopId = null;
37 | }
38 | }
39 |
40 | subscribe(callback) {
41 | if (this.subscribers.indexOf(callback) === -1)
42 | this.subscribers.push(callback);
43 | }
44 |
45 | unsubscribe(callback) {
46 | this.subscribers = this.subscribers.filter(s => s !== callback)
47 | }
48 | }
49 |
50 | export default (Platform.OS === "android" ? PerfTimer : DefaultTimer)
--------------------------------------------------------------------------------
/src/game/utils/perlin.js:
--------------------------------------------------------------------------------
1 | //////////////////////////////////////////////////////////////
2 |
3 | // http://mrl.nyu.edu/~perlin/noise/
4 | // Adapting from PApplet.java
5 | // which was adapted from toxi
6 | // which was adapted from the german demo group farbrausch
7 | // as used in their demo "art": http://www.farb-rausch.de/fr010src.zip
8 |
9 | // someday we might consider using "improved noise"
10 | // http://mrl.nyu.edu/~perlin/paper445.pdf
11 | // See: https://github.com/shiffman/The-Nature-of-Code-Examples-p5.js/
12 | // blob/master/introduction/Noise1D/noise.js
13 |
14 | /**
15 | * @module Math
16 | * @submodule Noise
17 | * @for p5
18 | * @requires core
19 | */
20 |
21 | var PERLIN_YWRAPB = 4;
22 | var PERLIN_YWRAP = 1 << PERLIN_YWRAPB;
23 | var PERLIN_ZWRAPB = 8;
24 | var PERLIN_ZWRAP = 1 << PERLIN_ZWRAPB;
25 | var PERLIN_SIZE = 4095;
26 |
27 | var perlin_octaves = 4; // default to medium smooth
28 | var perlin_amp_falloff = 0.5; // 50% reduction/octave
29 |
30 | var scaled_cosine = function(i) {
31 | return 0.5 * (1.0 - Math.cos(i * Math.PI));
32 | };
33 |
34 | var perlin; // will be initialized lazily by noise() or noiseSeed()
35 |
36 | /**
37 | * Returns the Perlin noise value at specified coordinates. Perlin noise is
38 | * a random sequence generator producing a more natural ordered, harmonic
39 | * succession of numbers compared to the standard random() function.
40 | * It was invented by Ken Perlin in the 1980s and been used since in
41 | * graphical applications to produce procedural textures, natural motion,
42 | * shapes, terrains etc.
The main difference to the
43 | * random() function is that Perlin noise is defined in an infinite
44 | * n-dimensional space where each pair of coordinates corresponds to a
45 | * fixed semi-random value (fixed only for the lifespan of the program; see
46 | * the noiseSeed() function). p5.js can compute 1D, 2D and 3D noise,
47 | * depending on the number of coordinates given. The resulting value will
48 | * always be between 0.0 and 1.0. The noise value can be animated by moving
49 | * through the noise space as demonstrated in the example above. The 2nd
50 | * and 3rd dimension can also be interpreted as time.
The actual
51 | * noise is structured similar to an audio signal, in respect to the
52 | * function's use of frequencies. Similar to the concept of harmonics in
53 | * physics, perlin noise is computed over several octaves which are added
54 | * together for the final result.
Another way to adjust the
55 | * character of the resulting sequence is the scale of the input
56 | * coordinates. As the function works within an infinite space the value of
57 | * the coordinates doesn't matter as such, only the distance between
58 | * successive coordinates does (eg. when using noise() within a
59 | * loop). As a general rule the smaller the difference between coordinates,
60 | * the smoother the resulting noise sequence will be. Steps of 0.005-0.03
61 | * work best for most applications, but this will differ depending on use.
62 | *
63 | *
64 | * @method noise
65 | * @param {Number} x x-coordinate in noise space
66 | * @param {Number} [y] y-coordinate in noise space
67 | * @param {Number} [z] z-coordinate in noise space
68 | * @return {Number} Perlin noise value (between 0 and 1) at specified
69 | * coordinates
70 | * @example
71 | *
72 | *
73 | * var xoff = 0.0;
74 | *
75 | * function draw() {
76 | * background(204);
77 | * xoff = xoff + 0.01;
78 | * var n = noise(xoff) * width;
79 | * line(n, 0, n, height);
80 | * }
81 | *
82 | *
83 | *
84 | * var noiseScale=0.02;
85 | *
86 | * function draw() {
87 | * background(0);
88 | * for (var x=0; x < width; x++) {
89 | * var noiseVal = noise((mouseX+x)*noiseScale, mouseY*noiseScale);
90 | * stroke(noiseVal*255);
91 | * line(x, mouseY+noiseVal*80, x, height);
92 | * }
93 | * }
94 | *
95 | *
96 | *
97 | * @alt
98 | * vertical line moves left to right with updating noise values.
99 | * horizontal wave pattern effected by mouse x-position & updating noise values.
100 | *
101 | */
102 |
103 | const noise = function(x, y, z) {
104 | y = y || 0;
105 | z = z || 0;
106 |
107 | if (perlin == null) {
108 | perlin = new Array(PERLIN_SIZE + 1);
109 | for (var i = 0; i < PERLIN_SIZE + 1; i++) {
110 | perlin[i] = Math.random();
111 | }
112 | }
113 |
114 | if (x < 0) {
115 | x = -x;
116 | }
117 | if (y < 0) {
118 | y = -y;
119 | }
120 | if (z < 0) {
121 | z = -z;
122 | }
123 |
124 | var xi = Math.floor(x),
125 | yi = Math.floor(y),
126 | zi = Math.floor(z);
127 | var xf = x - xi;
128 | var yf = y - yi;
129 | var zf = z - zi;
130 | var rxf, ryf;
131 |
132 | var r = 0;
133 | var ampl = 0.5;
134 |
135 | var n1, n2, n3;
136 |
137 | for (var o = 0; o < perlin_octaves; o++) {
138 | var of = xi + (yi << PERLIN_YWRAPB) + (zi << PERLIN_ZWRAPB);
139 |
140 | rxf = scaled_cosine(xf);
141 | ryf = scaled_cosine(yf);
142 |
143 | n1 = perlin[of & PERLIN_SIZE];
144 | n1 += rxf * (perlin[(of + 1) & PERLIN_SIZE] - n1);
145 | n2 = perlin[(of + PERLIN_YWRAP) & PERLIN_SIZE];
146 | n2 += rxf * (perlin[(of + PERLIN_YWRAP + 1) & PERLIN_SIZE] - n2);
147 | n1 += ryf * (n2 - n1);
148 |
149 | of += PERLIN_ZWRAP;
150 | n2 = perlin[of & PERLIN_SIZE];
151 | n2 += rxf * (perlin[(of + 1) & PERLIN_SIZE] - n2);
152 | n3 = perlin[(of + PERLIN_YWRAP) & PERLIN_SIZE];
153 | n3 += rxf * (perlin[(of + PERLIN_YWRAP + 1) & PERLIN_SIZE] - n3);
154 | n2 += ryf * (n3 - n2);
155 |
156 | n1 += scaled_cosine(zf) * (n2 - n1);
157 |
158 | r += n1 * ampl;
159 | ampl *= perlin_amp_falloff;
160 | xi <<= 1;
161 | xf *= 2;
162 | yi <<= 1;
163 | yf *= 2;
164 | zi <<= 1;
165 | zf *= 2;
166 |
167 | if (xf >= 1.0) {
168 | xi++;
169 | xf--;
170 | }
171 | if (yf >= 1.0) {
172 | yi++;
173 | yf--;
174 | }
175 | if (zf >= 1.0) {
176 | zi++;
177 | zf--;
178 | }
179 | }
180 | return r;
181 | };
182 |
183 | /**
184 | *
185 | * Adjusts the character and level of detail produced by the Perlin noise
186 | * function. Similar to harmonics in physics, noise is computed over
187 | * several octaves. Lower octaves contribute more to the output signal and
188 | * as such define the overall intensity of the noise, whereas higher octaves
189 | * create finer grained details in the noise sequence.
190 | *
191 | * By default, noise is computed over 4 octaves with each octave contributing
192 | * exactly half than its predecessor, starting at 50% strength for the 1st
193 | * octave. This falloff amount can be changed by adding an additional function
194 | * parameter. Eg. a falloff factor of 0.75 means each octave will now have
195 | * 75% impact (25% less) of the previous lower octave. Any value between
196 | * 0.0 and 1.0 is valid, however note that values greater than 0.5 might
197 | * result in greater than 1.0 values returned by noise().
198 | *
199 | * By changing these parameters, the signal created by the noise()
200 | * function can be adapted to fit very specific needs and characteristics.
201 | *
202 | * @method noiseDetail
203 | * @param {Number} lod number of octaves to be used by the noise
204 | * @param {Number} falloff falloff factor for each octave
205 | * @example
206 | *
207 | *
208 | * var noiseVal;
209 | * var noiseScale = 0.02;
210 | *
211 | * function setup() {
212 | * createCanvas(100, 100);
213 | * }
214 | *
215 | * function draw() {
216 | * background(0);
217 | * for (var y = 0; y < height; y++) {
218 | * for (var x = 0; x < width / 2; x++) {
219 | * noiseDetail(2, 0.2);
220 | * noiseVal = noise((mouseX + x) * noiseScale, (mouseY + y) * noiseScale);
221 | * stroke(noiseVal * 255);
222 | * point(x, y);
223 | * noiseDetail(8, 0.65);
224 | * noiseVal = noise(
225 | * (mouseX + x + width / 2) * noiseScale,
226 | * (mouseY + y) * noiseScale
227 | * );
228 | * stroke(noiseVal * 255);
229 | * point(x + width / 2, y);
230 | * }
231 | * }
232 | * }
233 | *
234 | *
235 | *
236 | * @alt
237 | * 2 vertical grey smokey patterns affected my mouse x-position and noise.
238 | *
239 | */
240 | const noiseDetail = function(lod, falloff) {
241 | if (lod > 0) {
242 | perlin_octaves = lod;
243 | }
244 | if (falloff > 0) {
245 | perlin_amp_falloff = falloff;
246 | }
247 | };
248 |
249 | /**
250 | * Sets the seed value for noise(). By default, noise()
251 | * produces different results each time the program is run. Set the
252 | * value parameter to a constant to return the same pseudo-random
253 | * numbers each time the software is run.
254 | *
255 | * @method noiseSeed
256 | * @param {Number} seed the seed value
257 | * @example
258 | *
259 | * var xoff = 0.0;
260 | *
261 | * function setup() {
262 | * noiseSeed(99);
263 | * stroke(0, 10);
264 | * }
265 | *
266 | * function draw() {
267 | * xoff = xoff + .01;
268 | * var n = noise(xoff) * width;
269 | * line(n, 0, n, height);
270 | * }
271 | *
272 | *
273 | *
274 | * @alt
275 | * vertical grey lines drawing in pattern affected by noise.
276 | *
277 | */
278 | const noiseSeed = function(seed) {
279 | // Linear Congruential Generator
280 | // Variant of a Lehman Generator
281 | var lcg = (function() {
282 | // Set to values from http://en.wikipedia.org/wiki/Numerical_Recipes
283 | // m is basically chosen to be large (as it is the max period)
284 | // and for its relationships to a and c
285 | var m = 4294967296;
286 | // a - 1 should be divisible by m's prime factors
287 | var a = 1664525;
288 | // c and m should be co-prime
289 | var c = 1013904223;
290 | var seed, z;
291 | return {
292 | setSeed: function(val) {
293 | // pick a random seed if val is undefined or null
294 | // the >>> 0 casts the seed to an unsigned 32-bit integer
295 | z = seed = (val == null ? Math.random() * m : val) >>> 0;
296 | },
297 | getSeed: function() {
298 | return seed;
299 | },
300 | rand: function() {
301 | // define the recurrence relationship
302 | z = (a * z + c) % m;
303 | // return a float in [0, 1)
304 | // if z = m then z / m = 0 therefore (z % m) / m < 1 always
305 | return z / m;
306 | }
307 | };
308 | })();
309 |
310 | lcg.setSeed(seed);
311 | perlin = new Array(PERLIN_SIZE + 1);
312 | for (var i = 0; i < PERLIN_SIZE + 1; i++) {
313 | perlin[i] = lcg.rand();
314 | }
315 | };
316 |
317 | module.exports = {
318 | noise,
319 | noiseDetail,
320 | noiseSeed
321 | };
--------------------------------------------------------------------------------
/src/game/utils/three/index.js:
--------------------------------------------------------------------------------
1 | import { THREE } from "expo-three";
2 | import SkeletonUtils from "./skeleton-utils";
3 |
4 | export const clean = obj => {
5 | while (obj.children.length > 0) {
6 | clean(obj.children[0]);
7 | obj.remove(obj.children[0]);
8 | }
9 |
10 | if (obj.geometry && obj.geometry.dispose) obj.geometry.dispose();
11 | if (obj.material && obj.material.dispose) obj.material.dispose();
12 | if (obj.texture && obj.texture.dispose) obj.texture.dispose();
13 | };
14 |
15 | export const clear = clean;
16 |
17 | export const remove = (parent, child) => {
18 | if (child)
19 | clean(child);
20 |
21 | if (parent)
22 | parent.remove(child);
23 | };
24 |
25 | export const direction = obj => {
26 | return obj.getWorldDirection(new THREE.Vector3());
27 | };
28 |
29 | export const rotateAroundPoint = (
30 | obj,
31 | point,
32 | { x = 0, y = 0, z = 0 }
33 | ) => {
34 | //-- https://stackoverflow.com/a/42866733/138392
35 | //-- https://stackoverflow.com/a/44288885/138392
36 |
37 | const original = obj.position.clone();
38 | const pivot = point.clone();
39 | const diff = new THREE.Vector3().subVectors(original, pivot);
40 |
41 | obj.position.copy(pivot);
42 |
43 | obj.rotation.x += x;
44 | obj.rotation.y += y;
45 | obj.rotation.z += z;
46 |
47 | diff.applyAxisAngle(new THREE.Vector3(1, 0, 0), x);
48 | diff.applyAxisAngle(new THREE.Vector3(0, 1, 0), y);
49 | diff.applyAxisAngle(new THREE.Vector3(0, 0, 1), z);
50 |
51 | obj.position.add(diff);
52 | };
53 |
54 | export const model = obj => {
55 | return obj.model ? obj.model : obj;
56 | };
57 |
58 | export const add = (parent, child) => {
59 | if (!parent || !child)
60 | return;
61 |
62 | const p = parent.model ? parent.model : parent;
63 | const c = child.model ? child.model : child;
64 |
65 | model(p).add(model(c))
66 | };
67 |
68 | export const reparent = (subject, newParent) => {
69 | subject.matrix.copy(subject.matrixWorld);
70 | subject.applyMatrix(new THREE.Matrix4().getInverse(newParent.matrixWorld));
71 | newParent.add(subject);
72 | };
73 |
74 | export const size = model => {
75 | const currentSize = new THREE.Vector3();
76 | const currentBox = new THREE.Box3().setFromObject(model);
77 |
78 | currentBox.getSize(currentSize);
79 |
80 | return currentSize;
81 | };
82 |
83 | export const cloneTexture = texture => {
84 | const clone = texture.clone();
85 |
86 | //-- Forces passing to `gl.texImage2D(...)` verbatim
87 | clone.isDataTexture = true;
88 |
89 | return clone;
90 | };
91 |
92 | export const cloneMesh = SkeletonUtils.clone;
93 |
94 | export const firstMesh = obj => {
95 | if (!obj)
96 | return;
97 |
98 | if (obj.isMesh)
99 | return obj;
100 |
101 | if (obj.children && obj.children.length){
102 | for (let i = 0; i < obj.children.length; i++) {
103 | const test = firstMesh(obj.children[i]);
104 |
105 | if (test && test.isMesh)
106 | return test;
107 | }
108 | }
109 | };
--------------------------------------------------------------------------------
/src/game/utils/three/skeleton-utils.js:
--------------------------------------------------------------------------------
1 | import { THREE } from "expo-three";
2 |
3 | /**
4 | * @author sunag / http://www.sunag.com.br
5 | */
6 |
7 | THREE.SkeletonUtils = {
8 |
9 | retarget: function () {
10 |
11 | var pos = new THREE.Vector3(),
12 | quat = new THREE.Quaternion(),
13 | scale = new THREE.Vector3(),
14 | bindBoneMatrix = new THREE.Matrix4(),
15 | relativeMatrix = new THREE.Matrix4(),
16 | globalMatrix = new THREE.Matrix4();
17 |
18 | return function ( target, source, options ) {
19 |
20 | options = options || {};
21 | options.preserveMatrix = options.preserveMatrix !== undefined ? options.preserveMatrix : true;
22 | options.preservePosition = options.preservePosition !== undefined ? options.preservePosition : true;
23 | options.preserveHipPosition = options.preserveHipPosition !== undefined ? options.preserveHipPosition : false;
24 | options.useTargetMatrix = options.useTargetMatrix !== undefined ? options.useTargetMatrix : false;
25 | options.hip = options.hip !== undefined ? options.hip : "hip";
26 | options.names = options.names || {};
27 |
28 | var sourceBones = source.isObject3D ? source.skeleton.bones : this.getBones( source ),
29 | bones = target.isObject3D ? target.skeleton.bones : this.getBones( target ),
30 | bindBones,
31 | bone, name, boneTo,
32 | bonesPosition, i;
33 |
34 | // reset bones
35 |
36 | if ( target.isObject3D ) {
37 |
38 | target.skeleton.pose();
39 |
40 | } else {
41 |
42 | options.useTargetMatrix = true;
43 | options.preserveMatrix = false;
44 |
45 | }
46 |
47 | if ( options.preservePosition ) {
48 |
49 | bonesPosition = [];
50 |
51 | for ( i = 0; i < bones.length; i ++ ) {
52 |
53 | bonesPosition.push( bones[ i ].position.clone() );
54 |
55 | }
56 |
57 | }
58 |
59 | if ( options.preserveMatrix ) {
60 |
61 | // reset matrix
62 |
63 | target.updateMatrixWorld();
64 |
65 | target.matrixWorld.identity();
66 |
67 | // reset children matrix
68 |
69 | for ( i = 0; i < target.children.length; ++ i ) {
70 |
71 | target.children[ i ].updateMatrixWorld( true );
72 |
73 | }
74 |
75 | }
76 |
77 | if ( options.offsets ) {
78 |
79 | bindBones = [];
80 |
81 | for ( i = 0; i < bones.length; ++ i ) {
82 |
83 | bone = bones[ i ];
84 | name = options.names[ bone.name ] || bone.name;
85 |
86 | if ( options.offsets && options.offsets[ name ] ) {
87 |
88 | bone.matrix.multiply( options.offsets[ name ] );
89 |
90 | bone.matrix.decompose( bone.position, bone.quaternion, bone.scale );
91 |
92 | bone.updateMatrixWorld();
93 |
94 | }
95 |
96 | bindBones.push( bone.matrixWorld.clone() );
97 |
98 | }
99 |
100 | }
101 |
102 | for ( i = 0; i < bones.length; ++ i ) {
103 |
104 | bone = bones[ i ];
105 | name = options.names[ bone.name ] || bone.name;
106 |
107 | boneTo = this.getBoneByName( name, sourceBones );
108 |
109 | globalMatrix.copy( bone.matrixWorld );
110 |
111 | if ( boneTo ) {
112 |
113 | boneTo.updateMatrixWorld();
114 |
115 | if ( options.useTargetMatrix ) {
116 |
117 | relativeMatrix.copy( boneTo.matrixWorld );
118 |
119 | } else {
120 |
121 | relativeMatrix.getInverse( target.matrixWorld );
122 | relativeMatrix.multiply( boneTo.matrixWorld );
123 |
124 | }
125 |
126 | // ignore scale to extract rotation
127 |
128 | scale.setFromMatrixScale( relativeMatrix );
129 | relativeMatrix.scale( scale.set( 1 / scale.x, 1 / scale.y, 1 / scale.z ) );
130 |
131 | // apply to global matrix
132 |
133 | globalMatrix.makeRotationFromQuaternion( quat.setFromRotationMatrix( relativeMatrix ) );
134 |
135 | if ( target.isObject3D ) {
136 |
137 | var boneIndex = bones.indexOf( bone ),
138 | wBindMatrix = bindBones ? bindBones[ boneIndex ] : bindBoneMatrix.getInverse( target.skeleton.boneInverses[ boneIndex ] );
139 |
140 | globalMatrix.multiply( wBindMatrix );
141 |
142 | }
143 |
144 | globalMatrix.copyPosition( relativeMatrix );
145 |
146 | }
147 |
148 | if ( bone.parent && bone.parent.isBone ) {
149 |
150 | bone.matrix.getInverse( bone.parent.matrixWorld );
151 | bone.matrix.multiply( globalMatrix );
152 |
153 | } else {
154 |
155 | bone.matrix.copy( globalMatrix );
156 |
157 | }
158 |
159 | if ( options.preserveHipPosition && name === options.hip ) {
160 |
161 | bone.matrix.setPosition( pos.set( 0, bone.position.y, 0 ) );
162 |
163 | }
164 |
165 | bone.matrix.decompose( bone.position, bone.quaternion, bone.scale );
166 |
167 | bone.updateMatrixWorld();
168 |
169 | }
170 |
171 | if ( options.preservePosition ) {
172 |
173 | for ( i = 0; i < bones.length; ++ i ) {
174 |
175 | bone = bones[ i ];
176 | name = options.names[ bone.name ] || bone.name;
177 |
178 | if ( name !== options.hip ) {
179 |
180 | bone.position.copy( bonesPosition[ i ] );
181 |
182 | }
183 |
184 | }
185 |
186 | }
187 |
188 | if ( options.preserveMatrix ) {
189 |
190 | // restore matrix
191 |
192 | target.updateMatrixWorld( true );
193 |
194 | }
195 |
196 | };
197 |
198 | }(),
199 |
200 | retargetClip: function ( target, source, clip, options ) {
201 |
202 | options = options || {};
203 | options.useFirstFramePosition = options.useFirstFramePosition !== undefined ? options.useFirstFramePosition : false;
204 | options.fps = options.fps !== undefined ? options.fps : 30;
205 | options.names = options.names || [];
206 |
207 | if ( ! source.isObject3D ) {
208 |
209 | source = this.getHelperFromSkeleton( source );
210 |
211 | }
212 |
213 | var numFrames = Math.round( clip.duration * ( options.fps / 1000 ) * 1000 ),
214 | delta = 1 / options.fps,
215 | convertedTracks = [],
216 | mixer = new THREE.AnimationMixer( source ),
217 | bones = this.getBones( target.skeleton ),
218 | boneDatas = [],
219 | positionOffset,
220 | bone, boneTo, boneData,
221 | name, i, j;
222 |
223 | mixer.clipAction( clip ).play();
224 | mixer.update( 0 );
225 |
226 | source.updateMatrixWorld();
227 |
228 | for ( i = 0; i < numFrames; ++ i ) {
229 |
230 | var time = i * delta;
231 |
232 | this.retarget( target, source, options );
233 |
234 | for ( j = 0; j < bones.length; ++ j ) {
235 |
236 | name = options.names[ bones[ j ].name ] || bones[ j ].name;
237 |
238 | boneTo = this.getBoneByName( name, source.skeleton );
239 |
240 | if ( boneTo ) {
241 |
242 | bone = bones[ j ];
243 | boneData = boneDatas[ j ] = boneDatas[ j ] || { bone: bone };
244 |
245 | if ( options.hip === name ) {
246 |
247 | if ( ! boneData.pos ) {
248 |
249 | boneData.pos = {
250 | times: new Float32Array( numFrames ),
251 | values: new Float32Array( numFrames * 3 )
252 | };
253 |
254 | }
255 |
256 | if ( options.useFirstFramePosition ) {
257 |
258 | if ( i === 0 ) {
259 |
260 | positionOffset = bone.position.clone();
261 |
262 | }
263 |
264 | bone.position.sub( positionOffset );
265 |
266 | }
267 |
268 | boneData.pos.times[ i ] = time;
269 |
270 | bone.position.toArray( boneData.pos.values, i * 3 );
271 |
272 | }
273 |
274 | if ( ! boneData.quat ) {
275 |
276 | boneData.quat = {
277 | times: new Float32Array( numFrames ),
278 | values: new Float32Array( numFrames * 4 )
279 | };
280 |
281 | }
282 |
283 | boneData.quat.times[ i ] = time;
284 |
285 | bone.quaternion.toArray( boneData.quat.values, i * 4 );
286 |
287 | }
288 |
289 | }
290 |
291 | mixer.update( delta );
292 |
293 | source.updateMatrixWorld();
294 |
295 | }
296 |
297 | for ( i = 0; i < boneDatas.length; ++ i ) {
298 |
299 | boneData = boneDatas[ i ];
300 |
301 | if ( boneData ) {
302 |
303 | if ( boneData.pos ) {
304 |
305 | convertedTracks.push( new THREE.VectorKeyframeTrack(
306 | ".bones[" + boneData.bone.name + "].position",
307 | boneData.pos.times,
308 | boneData.pos.values
309 | ) );
310 |
311 | }
312 |
313 | convertedTracks.push( new THREE.QuaternionKeyframeTrack(
314 | ".bones[" + boneData.bone.name + "].quaternion",
315 | boneData.quat.times,
316 | boneData.quat.values
317 | ) );
318 |
319 | }
320 |
321 | }
322 |
323 | mixer.uncacheAction( clip );
324 |
325 | return new THREE.AnimationClip( clip.name, - 1, convertedTracks );
326 |
327 | },
328 |
329 | getHelperFromSkeleton: function ( skeleton ) {
330 |
331 | var source = new THREE.SkeletonHelper( skeleton.bones[ 0 ] );
332 | source.skeleton = skeleton;
333 |
334 | return source;
335 |
336 | },
337 |
338 | getSkeletonOffsets: function () {
339 |
340 | var targetParentPos = new THREE.Vector3(),
341 | targetPos = new THREE.Vector3(),
342 | sourceParentPos = new THREE.Vector3(),
343 | sourcePos = new THREE.Vector3(),
344 | targetDir = new THREE.Vector2(),
345 | sourceDir = new THREE.Vector2();
346 |
347 | return function ( target, source, options ) {
348 |
349 | options = options || {};
350 | options.hip = options.hip !== undefined ? options.hip : "hip";
351 | options.names = options.names || {};
352 |
353 | if ( ! source.isObject3D ) {
354 |
355 | source = this.getHelperFromSkeleton( source );
356 |
357 | }
358 |
359 | var nameKeys = Object.keys( options.names ),
360 | nameValues = Object.values( options.names ),
361 | sourceBones = source.isObject3D ? source.skeleton.bones : this.getBones( source ),
362 | bones = target.isObject3D ? target.skeleton.bones : this.getBones( target ),
363 | offsets = [],
364 | bone, boneTo,
365 | name, i;
366 |
367 | target.skeleton.pose();
368 |
369 | for ( i = 0; i < bones.length; ++ i ) {
370 |
371 | bone = bones[ i ];
372 | name = options.names[ bone.name ] || bone.name;
373 |
374 | boneTo = this.getBoneByName( name, sourceBones );
375 |
376 | if ( boneTo && name !== options.hip ) {
377 |
378 | var boneParent = this.getNearestBone( bone.parent, nameKeys ),
379 | boneToParent = this.getNearestBone( boneTo.parent, nameValues );
380 |
381 | boneParent.updateMatrixWorld();
382 | boneToParent.updateMatrixWorld();
383 |
384 | targetParentPos.setFromMatrixPosition( boneParent.matrixWorld );
385 | targetPos.setFromMatrixPosition( bone.matrixWorld );
386 |
387 | sourceParentPos.setFromMatrixPosition( boneToParent.matrixWorld );
388 | sourcePos.setFromMatrixPosition( boneTo.matrixWorld );
389 |
390 | targetDir.subVectors(
391 | new THREE.Vector2( targetPos.x, targetPos.y ),
392 | new THREE.Vector2( targetParentPos.x, targetParentPos.y )
393 | ).normalize();
394 |
395 | sourceDir.subVectors(
396 | new THREE.Vector2( sourcePos.x, sourcePos.y ),
397 | new THREE.Vector2( sourceParentPos.x, sourceParentPos.y )
398 | ).normalize();
399 |
400 | var laterialAngle = targetDir.angle() - sourceDir.angle();
401 |
402 | var offset = new THREE.Matrix4().makeRotationFromEuler(
403 | new THREE.Euler(
404 | 0,
405 | 0,
406 | laterialAngle
407 | )
408 | );
409 |
410 | bone.matrix.multiply( offset );
411 |
412 | bone.matrix.decompose( bone.position, bone.quaternion, bone.scale );
413 |
414 | bone.updateMatrixWorld();
415 |
416 | offsets[ name ] = offset;
417 |
418 | }
419 |
420 | }
421 |
422 | return offsets;
423 |
424 | };
425 |
426 | }(),
427 |
428 | renameBones: function ( skeleton, names ) {
429 |
430 | var bones = this.getBones( skeleton );
431 |
432 | for ( var i = 0; i < bones.length; ++ i ) {
433 |
434 | var bone = bones[ i ];
435 |
436 | if ( names[ bone.name ] ) {
437 |
438 | bone.name = names[ bone.name ];
439 |
440 | }
441 |
442 | }
443 |
444 | return this;
445 |
446 | },
447 |
448 | getBones: function ( skeleton ) {
449 |
450 | return Array.isArray( skeleton ) ? skeleton : skeleton.bones;
451 |
452 | },
453 |
454 | getBoneByName: function ( name, skeleton ) {
455 |
456 | for ( var i = 0, bones = this.getBones( skeleton ); i < bones.length; i ++ ) {
457 |
458 | if ( name === bones[ i ].name )
459 |
460 | return bones[ i ];
461 |
462 | }
463 |
464 | },
465 |
466 | getNearestBone: function ( bone, names ) {
467 |
468 | while ( bone.isBone ) {
469 |
470 | if ( names.indexOf( bone.name ) !== - 1 ) {
471 |
472 | return bone;
473 |
474 | }
475 |
476 | bone = bone.parent;
477 |
478 | }
479 |
480 | },
481 |
482 | findBoneTrackData: function ( name, tracks ) {
483 |
484 | var regexp = /\[(.*)\]\.(.*)/,
485 | result = { name: name };
486 |
487 | for ( var i = 0; i < tracks.length; ++ i ) {
488 |
489 | // 1 is track name
490 | // 2 is track type
491 | var trackData = regexp.exec( tracks[ i ].name );
492 |
493 | if ( trackData && name === trackData[ 1 ] ) {
494 |
495 | result[ trackData[ 2 ] ] = i;
496 |
497 | }
498 |
499 | }
500 |
501 | return result;
502 |
503 | },
504 |
505 | getEqualsBonesNames: function ( skeleton, targetSkeleton ) {
506 |
507 | var sourceBones = this.getBones( skeleton ),
508 | targetBones = this.getBones( targetSkeleton ),
509 | bones = [];
510 |
511 | search : for ( var i = 0; i < sourceBones.length; i ++ ) {
512 |
513 | var boneName = sourceBones[ i ].name;
514 |
515 | for ( var j = 0; j < targetBones.length; j ++ ) {
516 |
517 | if ( boneName === targetBones[ j ].name ) {
518 |
519 | bones.push( boneName );
520 |
521 | continue search;
522 |
523 | }
524 |
525 | }
526 |
527 | }
528 |
529 | return bones;
530 |
531 | },
532 |
533 | clone: function ( source ) {
534 |
535 | var sourceLookup = new Map();
536 | var cloneLookup = new Map();
537 |
538 | var clone = source.clone();
539 |
540 | parallelTraverse( source, clone, function ( sourceNode, clonedNode ) {
541 |
542 | sourceLookup.set( clonedNode, sourceNode );
543 | cloneLookup.set( sourceNode, clonedNode );
544 |
545 | } );
546 |
547 | clone.traverse( function ( node ) {
548 |
549 | if ( ! node.isSkinnedMesh ) return;
550 |
551 | var clonedMesh = node;
552 | var sourceMesh = sourceLookup.get( node );
553 | var sourceBones = sourceMesh.skeleton.bones;
554 |
555 | clonedMesh.skeleton = sourceMesh.skeleton.clone();
556 | clonedMesh.bindMatrix.copy( sourceMesh.bindMatrix );
557 |
558 | clonedMesh.skeleton.bones = sourceBones.map( function ( bone ) {
559 |
560 | return cloneLookup.get( bone );
561 |
562 | } );
563 |
564 | clonedMesh.bind( clonedMesh.skeleton, clonedMesh.bindMatrix );
565 |
566 | } );
567 |
568 | return clone;
569 |
570 | }
571 |
572 | };
573 |
574 |
575 | function parallelTraverse( a, b, callback ) {
576 |
577 | callback( a, b );
578 |
579 | for ( var i = 0; i < a.children.length; i ++ ) {
580 |
581 | parallelTraverse( a.children[ i ], b.children[ i ], callback );
582 |
583 | }
584 |
585 | }
586 |
587 | export default THREE.SkeletonUtils;
--------------------------------------------------------------------------------