Sfoglia il codice sorgente

New Basketball game

customisations
alemart 8 mesi fa
parent
commit
6877008ad3
41 ha cambiato i file con 4103 aggiunte e 0 eliminazioni
  1. 1254
    0
      demos/basketball/NOTICE.html
  2. BIN
      demos/basketball/assets/atlas.png
  3. BIN
      demos/basketball/assets/backboard.jpg
  4. BIN
      demos/basketball/assets/backboard.wav
  5. BIN
      demos/basketball/assets/ball.blend
  6. BIN
      demos/basketball/assets/ball.glb
  7. BIN
      demos/basketball/assets/bonus.wav
  8. BIN
      demos/basketball/assets/bounce.wav
  9. BIN
      demos/basketball/assets/bounce2.wav
  10. BIN
      demos/basketball/assets/button.wav
  11. BIN
      demos/basketball/assets/goal.blend
  12. BIN
      demos/basketball/assets/goal.glb
  13. BIN
      demos/basketball/assets/lose.wav
  14. BIN
      demos/basketball/assets/net.wav
  15. BIN
      demos/basketball/assets/score.wav
  16. BIN
      demos/basketball/assets/win.wav
  17. 27
    0
      demos/basketball/index.html
  18. 27
    0
      demos/basketball/poster.html
  19. BIN
      demos/basketball/qr-code.gif
  20. 32
    0
      demos/basketball/src/core/asset-list.js
  21. 44
    0
      demos/basketball/src/core/event-queue.js
  22. 24
    0
      demos/basketball/src/core/events.js
  23. 304
    0
      demos/basketball/src/core/game.js
  24. 22
    0
      demos/basketball/src/core/globals.js
  25. 437
    0
      demos/basketball/src/entities/ball.js
  26. 116
    0
      demos/basketball/src/entities/entity.js
  27. 93
    0
      demos/basketball/src/entities/game-controller.js
  28. 106
    0
      demos/basketball/src/entities/goal.js
  29. 65
    0
      demos/basketball/src/entities/gravity.js
  30. 112
    0
      demos/basketball/src/entities/gui/ball-counter.js
  31. 139
    0
      demos/basketball/src/entities/gui/gameover-overlay.js
  32. 59
    0
      demos/basketball/src/entities/gui/gui-control.js
  33. 75
    0
      demos/basketball/src/entities/gui/mute-button.js
  34. 130
    0
      demos/basketball/src/entities/gui/tutorial-overlay.js
  35. 131
    0
      demos/basketball/src/entities/jukebox.js
  36. 38
    0
      demos/basketball/src/entities/lights.js
  37. 450
    0
      demos/basketball/src/entities/net.js
  38. 175
    0
      demos/basketball/src/entities/score-text.js
  39. 184
    0
      demos/basketball/src/entities/scoreboard.js
  40. 28
    0
      demos/basketball/src/main.js
  41. 31
    0
      demos/basketball/video.html

+ 1254
- 0
demos/basketball/NOTICE.html
File diff soppresso perché troppo grande
Vedi File


BIN
demos/basketball/assets/atlas.png Vedi File


BIN
demos/basketball/assets/backboard.jpg Vedi File


BIN
demos/basketball/assets/backboard.wav Vedi File


BIN
demos/basketball/assets/ball.blend Vedi File


BIN
demos/basketball/assets/ball.glb Vedi File


BIN
demos/basketball/assets/bonus.wav Vedi File


BIN
demos/basketball/assets/bounce.wav Vedi File


BIN
demos/basketball/assets/bounce2.wav Vedi File


BIN
demos/basketball/assets/button.wav Vedi File


BIN
demos/basketball/assets/goal.blend Vedi File


BIN
demos/basketball/assets/goal.glb Vedi File


BIN
demos/basketball/assets/lose.wav Vedi File


BIN
demos/basketball/assets/net.wav Vedi File


BIN
demos/basketball/assets/score.wav Vedi File


BIN
demos/basketball/assets/win.wav Vedi File


+ 27
- 0
demos/basketball/index.html Vedi File

@@ -0,0 +1,27 @@
1
+<!doctype html>
2
+<html>
3
+    <head>
4
+        <meta charset="utf-8">
5
+        <meta name="viewport" content="width=device-width,initial-scale=1">
6
+        <title>Magic AR Basketball Game - encantar.js WebAR demo</title>
7
+        <link href="../assets/demo.css" rel="stylesheet">
8
+        <script src="../../dist/encantar.min.js"></script>
9
+        <script src="https://cdn.jsdelivr.net/npm/babylonjs@7.38.0/babylon.min.js"></script>
10
+        <script src="https://cdn.jsdelivr.net/npm/babylonjs-loaders@7.38.0/babylonjs.loaders.min.js"></script>
11
+        <script src="https://cdn.jsdelivr.net/npm/babylonjs-gui@7.38.0/babylon.gui.min.js"></script>
12
+        <script src="https://cdn.jsdelivr.net/npm/cannon@0.6.2/build/cannon.min.js"></script>
13
+        <script src="../../plugins/babylon-with-encantar.js"></script>
14
+        <script src="../../plugins/extras/asset-manager.js"></script>
15
+        <script type="module" src="./src/main.js"></script>
16
+    </head>
17
+    <body>
18
+        <div id="ar-viewport">
19
+            <div id="ar-hud" hidden>
20
+                <a id="info" href="NOTICE.html" draggable="false"></a>
21
+                <a id="like" href="../assets/promo.html" draggable="false"></a>
22
+                <img id="scan" src="../assets/scan.png" draggable="false">
23
+            </div>
24
+        </div>
25
+        <img id="mage" src="../assets/mage.webp" hidden>
26
+    </body>
27
+</html>

+ 27
- 0
demos/basketball/poster.html Vedi File

@@ -0,0 +1,27 @@
1
+<!doctype html>
2
+<html>
3
+    <head>
4
+        <meta charset="utf-8">
5
+        <meta name="description" content="A basketball game created with encantar.js, a GPU-accelerated Augmented Reality engine for the web">
6
+        <meta name="author" content="Alexandre Martins (alemart)">
7
+        <meta name="viewport" content="width=device-width,initial-scale=1">
8
+        <title>Magic AR Basketball Game - encantar.js WebAR demo</title>
9
+        <link href="../assets/poster.css" rel="stylesheet">
10
+        <style>body { background-color: #5a3636 !important; }</style>
11
+        <script data-goatcounter="https://encantar-js.goatcounter.com/count" async src="//gc.zgo.at/count.js"></script>
12
+        <script src="../assets/poster.js"></script>
13
+    </head>
14
+    <body data-ar-images="mage.webp">
15
+        <nav>
16
+            <a id="get" href="https://alemart.github.io/encantar-js/download" target="_blank" rel="external">Get encantar.js</a>
17
+            <a id="like" href="../assets/promo.html" target="_blank"><i></i></a>
18
+            <a id="prev" href="#"></a>
19
+            <a id="next" href="#"></a>
20
+        </nav>
21
+        <section id="qr-code-area">
22
+            <h4>QR code</h4>
23
+            <a id="video-preview" href="video.html" target="_blank"><img id="qr-code" src="qr-code.gif" alt="QR code"></a>
24
+            <small>Scan to begin!</small>
25
+        </section>
26
+    </body>
27
+</html>

BIN
demos/basketball/qr-code.gif Vedi File


+ 32
- 0
demos/basketball/src/core/asset-list.js Vedi File

@@ -0,0 +1,32 @@
1
+/**
2
+ * -------------------------------------------
3
+ * Magic AR Basketball
4
+ * A demo game of the encantar.js WebAR engine
5
+ * -------------------------------------------
6
+ * @fileoverview The list of assets used in the game
7
+ * @author Alexandre Martins <alemartf(at)gmail.com> (https://github.com/alemart/encantar-js)
8
+ */
9
+
10
+/**
11
+ * The list of assets used in the game
12
+ * (useful for preloading)
13
+ */
14
+export const ASSET_LIST = Object.freeze([
15
+
16
+    // 3D models
17
+    'ball.glb',
18
+    'goal.glb',
19
+
20
+    // 2D artwork
21
+    'atlas.png',
22
+
23
+    // sounds
24
+    'backboard.wav',
25
+    'bounce.wav',
26
+    'net.wav',
27
+    'bonus.wav',
28
+    'win.wav',
29
+    'lose.wav',
30
+    'button.wav',
31
+
32
+]);

+ 44
- 0
demos/basketball/src/core/event-queue.js Vedi File

@@ -0,0 +1,44 @@
1
+/**
2
+ * -------------------------------------------
3
+ * Magic AR Basketball
4
+ * A demo game of the encantar.js WebAR engine
5
+ * -------------------------------------------
6
+ * @fileoverview Event queue
7
+ * @author Alexandre Martins <alemartf(at)gmail.com> (https://github.com/alemart/encantar-js)
8
+ */
9
+
10
+import { GameEvent } from './events.js';
11
+
12
+/**
13
+ * Event queue
14
+ */
15
+export class EventQueue
16
+{
17
+    /**
18
+     * Constructor
19
+     */
20
+    constructor()
21
+    {
22
+        this._events = /** @type {GameEvent[]} */ ( [] );
23
+    }
24
+
25
+    /**
26
+     * Enqueue an event
27
+     * @param {GameEvent} event
28
+     * @returns {void}
29
+     */
30
+    enqueue(event)
31
+    {
32
+        this._events.push(event);
33
+    }
34
+
35
+    /**
36
+     * Removes and returns the first event from the queue
37
+     * If the queue is empty, null is returned instead
38
+     * @returns {GameEvent|null}
39
+     */
40
+    dequeue()
41
+    {
42
+        return this._events.shift() || null;
43
+    }
44
+}

+ 24
- 0
demos/basketball/src/core/events.js Vedi File

@@ -0,0 +1,24 @@
1
+/**
2
+ * -------------------------------------------
3
+ * Magic AR Basketball
4
+ * A demo game of the encantar.js WebAR engine
5
+ * -------------------------------------------
6
+ * @fileoverview A class for game events
7
+ * @author Alexandre Martins <alemartf(at)gmail.com> (https://github.com/alemart/encantar-js)
8
+ */
9
+
10
+/**
11
+ * Game Event
12
+ */
13
+export class GameEvent extends CustomEvent
14
+{
15
+    /**
16
+     * Constructor
17
+     * @param {string} type
18
+     * @param {any} [detail]
19
+     */
20
+    constructor(type, detail)
21
+    {
22
+        super(type, { detail });
23
+    }
24
+}

+ 304
- 0
demos/basketball/src/core/game.js Vedi File

@@ -0,0 +1,304 @@
1
+/**
2
+ * -------------------------------------------
3
+ * Magic AR Basketball
4
+ * A demo game of the encantar.js WebAR engine
5
+ * -------------------------------------------
6
+ * @fileoverview The main class of the game, which handles its lifecycle
7
+ * @author Alexandre Martins <alemartf(at)gmail.com> (https://github.com/alemart/encantar-js)
8
+ */
9
+
10
+import { ASSET_LIST } from './asset-list.js';
11
+import { GameEvent } from './events.js';
12
+import { EventQueue } from './event-queue.js';
13
+import { GameController } from '../entities/game-controller.js';
14
+import { Lights } from '../entities/lights.js';
15
+import { Gravity } from '../entities/gravity.js';
16
+import { Goal } from '../entities/goal.js';
17
+import { Ball } from '../entities/ball.js';
18
+import { BasketballNet } from '../entities/net.js';
19
+import { ScoreText } from '../entities/score-text.js';
20
+import { Scoreboard } from '../entities/scoreboard.js';
21
+import { Jukebox } from '../entities/jukebox.js';
22
+import { BallCounter } from '../entities/gui/ball-counter.js';
23
+import { MuteButton } from '../entities/gui/mute-button.js';
24
+import { TutorialOverlay } from '../entities/gui/tutorial-overlay.js';
25
+import { GameOverOverlay } from '../entities/gui/gameover-overlay.js';
26
+
27
+/**
28
+ * The main class of the game, which handles its lifecycle
29
+ */
30
+export class BasketballGame extends ARDemo
31
+{
32
+    /**
33
+     * Constructor
34
+     */
35
+    constructor()
36
+    {
37
+        super();
38
+
39
+        this._assetManager = new AssetManager();
40
+        this._eventQueue = new EventQueue();
41
+        this._entities = [];
42
+        this._gui = null;
43
+    }
44
+
45
+    /**
46
+     * Start the AR session
47
+     * @returns {Promise<Session>}
48
+     */
49
+    async startSession()
50
+    {
51
+        if(!AR.isSupported()) {
52
+            throw new Error(
53
+                'This device is not compatible with this AR experience.\n\n' +
54
+                'User agent: ' + navigator.userAgent
55
+            );
56
+        }
57
+
58
+        const imageTracker = AR.Tracker.Image();
59
+        await imageTracker.database.add([
60
+        {
61
+            name: 'mage',
62
+            image: document.getElementById('mage')
63
+        }
64
+        ]);
65
+
66
+        const viewport = AR.Viewport({
67
+            container: document.getElementById('ar-viewport'),
68
+            hudContainer: document.getElementById('ar-hud')
69
+        });
70
+
71
+        const video = document.getElementById('my-video');
72
+        const useWebcam = (video === null);
73
+        const videoSource = useWebcam ? AR.Source.Camera() : AR.Source.Video(video);
74
+
75
+        const pointerSource = AR.Source.Pointer();
76
+        const pointerTracker = AR.Tracker.Pointer({
77
+            space: 'adjusted'
78
+        });
79
+
80
+        const session = await AR.startSession({
81
+            mode: 'immersive',
82
+            viewport: viewport,
83
+            trackers: [ imageTracker, pointerTracker ],
84
+            sources: [ videoSource, pointerSource ],
85
+            stats: true,
86
+            gizmos: true,
87
+        });
88
+
89
+        const scan = document.getElementById('scan');
90
+
91
+        imageTracker.addEventListener('targetfound', event => {
92
+            session.gizmos.visible = false;
93
+            if(scan)
94
+                scan.hidden = true;
95
+
96
+            this.broadcast(new GameEvent('targetfound'));
97
+        });
98
+
99
+        imageTracker.addEventListener('targetlost', event => {
100
+            session.gizmos.visible = true;
101
+            if(scan)
102
+                scan.hidden = false;
103
+
104
+            this.broadcast(new GameEvent('targetlost'));
105
+        });
106
+
107
+        return session;
108
+    }
109
+
110
+    /**
111
+     * Preload resources before starting the AR session
112
+     * @returns {Promise<void>}
113
+     */
114
+    preload()
115
+    {
116
+        console.log('Preloading assets...');
117
+
118
+        return this._assetManager.preload(
119
+            ASSET_LIST.map(asset => 'assets/' + asset),
120
+            { timeout: 30 }
121
+        );
122
+    }
123
+
124
+    /**
125
+     * Initialization
126
+     * @returns {Promise<void>}
127
+     */
128
+    init()
129
+    {
130
+        return Promise.resolve()
131
+        .then(() => this._initPhysics())
132
+        .then(() => this._initGUI())
133
+        .then(() => this._spawnEntities())
134
+        .then(() => this._flushEventQueue());
135
+    }
136
+
137
+    /**
138
+     * Animation loop
139
+     * @returns {void}
140
+     */
141
+    update()
142
+    {
143
+        for(let i = 0; i < this._entities.length; i++)
144
+            this._entities[i].update();
145
+
146
+        this._flushEventQueue();
147
+    }
148
+
149
+    /**
150
+     * Release resources
151
+     * @returns {void}
152
+     */
153
+    release()
154
+    {
155
+        for(let i = 0; i < this._entities.length; i++)
156
+            this._entities[i].release();
157
+    }
158
+
159
+    /**
160
+     * Asset Manager
161
+     * @returns {AssetManager}
162
+     */
163
+    get assetManager()
164
+    {
165
+        return this._assetManager;
166
+    }
167
+
168
+    /**
169
+     * A texture that supports 2D GUI elements. It acts as the root of the GUI
170
+     * @returns {BABYLON.AdvancedDynamicTexture}
171
+     */
172
+    get gui()
173
+    {
174
+        return this._gui;
175
+    }
176
+
177
+    /**
178
+     * Broadcast an event to all entities
179
+     * @param {GameEvent} event
180
+     * @returns {void}
181
+     */
182
+    broadcast(event)
183
+    {
184
+        this._eventQueue.enqueue(event);
185
+    }
186
+
187
+    /**
188
+     * Instantiate an entity
189
+     * @template {Entity} T
190
+     * @param {new () => T} entityClass
191
+     * @returns {Promise<T>}
192
+     */
193
+    spawn(entityClass)
194
+    {
195
+        const entity = Reflect.construct(entityClass, [ this ]);
196
+        this._entities.push(entity);
197
+
198
+        return Promise.resolve()
199
+        .then(() => entity.init())
200
+        .then(() => entity);
201
+    }
202
+
203
+    /**
204
+     * Instantiate the entities of the game
205
+     * @returns {Promise<void>}
206
+     */
207
+    _spawnEntities()
208
+    {
209
+        const wantNet = (location.search.indexOf('disablenet=1') < 0);
210
+
211
+        return Promise.all([
212
+
213
+            // main game elements
214
+            this.spawn(GameController),
215
+            this.spawn(Jukebox),
216
+            this.spawn(Lights),
217
+            this.spawn(Gravity),
218
+            this.spawn(Ball),
219
+            this.spawn(Goal),
220
+            this.spawn(ScoreText).then(text => text.setScore(2)),
221
+            this.spawn(ScoreText).then(text => text.setScore(3)),
222
+            this.spawn(Scoreboard),
223
+            wantNet && this.spawn(BasketballNet),
224
+
225
+            // 2D GUI
226
+            this.spawn(BallCounter),
227
+            this.spawn(MuteButton),
228
+            this.spawn(TutorialOverlay),
229
+            this.spawn(GameOverOverlay),
230
+
231
+        ]);
232
+    }
233
+
234
+    /**
235
+     * Flush the event queue
236
+     * @returns {void}
237
+     */
238
+    _flushEventQueue()
239
+    {
240
+        let event;
241
+
242
+        // new events may be broadcasted during this loop
243
+        while((event = this._eventQueue.dequeue())) {
244
+            for(let i = 0; i < this._entities.length; i++)
245
+                this._entities[i].handleEvent(event);
246
+        }
247
+    }
248
+
249
+    /**
250
+     * Initialize the physics plugin
251
+     * @returns {void}
252
+     */
253
+    _initPhysics()
254
+    {
255
+        const physicsPlugin = new BABYLON.CannonJSPlugin();
256
+
257
+        if(!this.ar.scene.enablePhysics(undefined, physicsPlugin))
258
+            throw new Error(`Can't initialize the physics`);
259
+    }
260
+
261
+    /**
262
+     * Initializes the GUI
263
+     * @returns {void}
264
+     */
265
+    _initGUI()
266
+    {
267
+        this._gui = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI('UI');
268
+
269
+        this._scaleGUI = this._scaleGUI.bind(this);
270
+        this._scaleGUI();
271
+
272
+        const viewport = this.ar.session.viewport;
273
+        viewport.addEventListener('resize', this._scaleGUI);
274
+    }
275
+
276
+    /**
277
+     * Scale the GUI (fullscreen)
278
+     * @returns {void}
279
+     */
280
+    _scaleGUI()
281
+    {
282
+        const idealHeightInLandscapeMode = 600;
283
+        const viewport = this.ar.session.viewport;
284
+        const aspectRatio = viewport.aspectRatio;
285
+        let width, height;
286
+
287
+        if(aspectRatio >= 1) {
288
+            width = Math.round(idealHeightInLandscapeMode * aspectRatio);
289
+            height = idealHeightInLandscapeMode;
290
+        }
291
+        else {
292
+            width = idealHeightInLandscapeMode;
293
+            height = Math.round(idealHeightInLandscapeMode / aspectRatio);
294
+        }
295
+
296
+        width -= width % 2;
297
+        height -= height % 2;
298
+
299
+        this._gui.scaleTo(width, height);
300
+
301
+        const event = new GameEvent('guiresized');
302
+        this.broadcast(event);
303
+    }
304
+}

+ 22
- 0
demos/basketball/src/core/globals.js Vedi File

@@ -0,0 +1,22 @@
1
+/**
2
+ * -------------------------------------------
3
+ * Magic AR Basketball
4
+ * A demo game of the encantar.js WebAR engine
5
+ * -------------------------------------------
6
+ * @fileoverview Global definitions
7
+ * @author Alexandre Martins <alemartf(at)gmail.com> (https://github.com/alemart/encantar-js)
8
+ */
9
+
10
+/** Number of balls per match */
11
+export const NUMBER_OF_BALLS = 5;
12
+
13
+/** The minimum score for each rank */
14
+export const SCORE_TABLE = Object.freeze({
15
+    'S':  15,
16
+    'A+': 12,
17
+    'A':  11,
18
+    'B+': 8,
19
+    'B':  5,
20
+    'C':  2,
21
+    'F':  0
22
+});

+ 437
- 0
demos/basketball/src/entities/ball.js Vedi File

@@ -0,0 +1,437 @@
1
+/**
2
+ * -------------------------------------------
3
+ * Magic AR Basketball
4
+ * A demo game of the encantar.js WebAR engine
5
+ * -------------------------------------------
6
+ * @fileoverview The ball
7
+ * @author Alexandre Martins <alemartf(at)gmail.com> (https://github.com/alemart/encantar-js)
8
+ */
9
+
10
+import { Entity } from './entity.js';
11
+import { GameEvent } from '../core/events.js';
12
+
13
+/** Radius of the ball */
14
+const BALL_RADIUS = 0.27;
15
+
16
+/** Minimum distance for scoring 3 points */
17
+const THREE_POINT_THRESHOLD = 5.0;
18
+
19
+/** Shoot angle */
20
+const SHOOT_ANGLE = Math.PI / 4;
21
+
22
+/** Shoot sensitivity multiplier (y and z axes) */
23
+const SHOOT_SENSITIVITY = 1.65;
24
+
25
+/** Shoot sensitivity multiplier (x-axis) */
26
+const SHOOT_HORIZONTAL_SENSITIVITY = 0.5;
27
+
28
+/** Distance from the camera to the ball plane in the ready state */
29
+const PLANE_DISTANCE = 1.0;
30
+
31
+/** Y offset applied to the ball in the ready state (from the center of the view) */
32
+const PLANE_BALL_OFFSET = -0.35;
33
+
34
+/** Maximum distance from the camera to the ball, so that the ball is considered "lost" in the thrown state */
35
+const MAX_DISTANCE = 15;
36
+
37
+/** Maximum distance in the y-axis from the camera to the ball, so that the ball is considered "lost" in the thrown state */
38
+const MAX_Y_DISTANCE = 5;
39
+
40
+/** Collision flag for the backboard */
41
+const FLAG_BACKBOARD = 1;
42
+
43
+/** Collision flag for other materials */
44
+const FLAG_OTHER = 2;
45
+
46
+/**
47
+ * The ball
48
+ */
49
+export class Ball extends Entity
50
+{
51
+    /**
52
+     * Constructor
53
+     * @param {BasketballDemo} demo
54
+     */
55
+    constructor(demo)
56
+    {
57
+        super(demo);
58
+
59
+        this._state = 'ready';
60
+        this._runState = {
61
+            'ready': this._onReadyState,
62
+            'throwing': this._onThrowingState,
63
+            'thrown': this._onThrownState
64
+        };
65
+
66
+        this._plane = new BABYLON.Plane(0, 0, 1, 0);
67
+        this._planeOrigin = new BABYLON.Vector3();
68
+        this._positionWhenThrown = new BABYLON.Vector3();
69
+        this._mesh = null;
70
+        this._lastTrigger = '';
71
+        this._collisionFlags = 0;
72
+    }
73
+
74
+    /**
75
+     * Initialize the entity
76
+     * @returns {Promise<void>}
77
+     */
78
+    async init()
79
+    {
80
+        const file = this._demo.assetManager.file('ball.glb');
81
+        const gltf = await BABYLON.SceneLoader.ImportMeshAsync('', '', file);
82
+        const mesh = this._createPhysicsRoot(gltf.meshes[0]); // gltf.meshes[0] is __root__
83
+
84
+        mesh.actionManager = new BABYLON.ActionManager();
85
+
86
+        this._mesh = mesh;
87
+
88
+        // the ball is not a child of ar.root
89
+    }
90
+
91
+    /**
92
+     * Update the entity
93
+     * @returns {void}
94
+     */
95
+    update()
96
+    {
97
+        const ar = this.ar;
98
+
99
+        if(!ar.viewer) {
100
+            this._state = 'ready';
101
+            return;
102
+        }
103
+
104
+        this._updatePlane();
105
+
106
+        const fn = this._runState[this._state];
107
+        fn.call(this);
108
+    }
109
+
110
+    /**
111
+     * Update callback of the 'ready' state
112
+     * @returns {void}
113
+     */
114
+    _onReadyState()
115
+    {
116
+        const ar = this.ar;
117
+        const mesh = this._mesh;
118
+        const impostor = mesh.physicsImpostor;
119
+
120
+        this._lastTrigger = '';
121
+        this._collisionFlags = 0;
122
+
123
+        mesh.position.copyFrom(this._planeOrigin);
124
+        mesh.position.y += PLANE_BALL_OFFSET;
125
+
126
+        impostor.setLinearVelocity(BABYLON.Vector3.Zero());
127
+        impostor.mass = 0; // disable gravity
128
+
129
+        if(ar.pointers.length > 0) {
130
+            const pointer = ar.pointers[0];
131
+            const position = ar.session.viewport.convertToPixels(pointer.position, 'adjusted');
132
+
133
+            if(position.x > 60) {
134
+                impostor.mass = 1; // enable gravity
135
+                this._state = 'throwing';
136
+            }
137
+        }
138
+    }
139
+
140
+    /**
141
+     * Update callback of the 'throwing' state
142
+     * @returns {void}
143
+     */
144
+    _onThrowingState()
145
+    {
146
+        const ar = this.ar;
147
+
148
+        if(ar.pointers.length == 0) {
149
+            this._state = 'ready';
150
+            return;
151
+        }
152
+
153
+        const pointer = ar.pointers[0];
154
+
155
+        if(pointer.phase == 'ended')
156
+            this._throw(this._findVelocity(pointer));
157
+        else if(pointer.phase != 'canceled' && this._alignToPointer(pointer))
158
+            this._mesh.physicsImpostor.setLinearVelocity(BABYLON.Vector3.Zero());
159
+        else
160
+            this._state = 'ready';
161
+    }
162
+
163
+    /**
164
+     * Update callback of the 'thrown' state
165
+     * @returns {void}
166
+     */
167
+    _onThrownState()
168
+    {
169
+        const ar = this.ar;
170
+        const cameraPosition = ar.camera.globalPosition;
171
+        const ballPosition = this._mesh.absolutePosition;
172
+        const distance = BABYLON.Vector3.Distance(cameraPosition, ballPosition);
173
+
174
+        if(distance > MAX_DISTANCE || ballPosition.y < cameraPosition.y - MAX_Y_DISTANCE) {
175
+            this._broadcast(new GameEvent('lostball'));
176
+            this._state = 'ready';
177
+        }
178
+    }
179
+
180
+    /**
181
+     * Throw / Shoot the ball
182
+     * @param {Vector2} v velocity
183
+     * @returns {void}
184
+     */
185
+    _throw(v)
186
+    {
187
+        const magnitude = SHOOT_SENSITIVITY * v.y;
188
+        const angle = SHOOT_ANGLE;
189
+        const impulse = new BABYLON.Vector3(
190
+            v.x * SHOOT_HORIZONTAL_SENSITIVITY,
191
+            magnitude * Math.sin(angle),
192
+            -magnitude * Math.cos(angle)
193
+        );
194
+
195
+        this._positionWhenThrown.copyFrom(this._mesh.absolutePosition);
196
+        this._mesh.physicsImpostor.applyImpulse(impulse, this._mesh.absolutePosition);
197
+        this._state = 'thrown';
198
+    }
199
+
200
+    /**
201
+     * Find a velocity vector based on a trackable pointer
202
+     * @param {TrackablePointer} pointer
203
+     * @returns {Vector2}
204
+     */
205
+    _findVelocity(pointer)
206
+    {
207
+        // we could return pointer.velocity, but that's not always
208
+        // user-friendly when it comes to throwing the ball!
209
+        const currentSpeed = pointer.velocity.length();
210
+        const averageSpeed = pointer.totalDistance / pointer.elapsedTime;
211
+        const speed = Math.max(currentSpeed, averageSpeed);
212
+
213
+        let direction = pointer.initialPosition.directionTo(pointer.position);
214
+        if(direction.y < 0)
215
+            direction = pointer.deltaPosition.normalized();
216
+
217
+        return direction.times(speed);
218
+    }
219
+
220
+    /**
221
+     * This callback is invoked when the ball hits a collider
222
+     * @param {BABYLON.PhysicsImpostor} impostor
223
+     * @returns {void}
224
+     */
225
+    _onCollisionEnter(impostor)
226
+    {
227
+        if(this._state != 'thrown')
228
+            return;
229
+
230
+        const mesh = impostor.object;
231
+        const backboard = mesh.getChildMeshes(true, m => m.name == 'Collider_A')[0];
232
+        const collidedWithBackboard = backboard && this._mesh.intersectsMesh(backboard, true);
233
+        const material = collidedWithBackboard ? 'backboard' : 'other';
234
+        const flag = collidedWithBackboard ? FLAG_BACKBOARD : FLAG_OTHER;
235
+
236
+        if((this._collisionFlags & flag) == 0) {
237
+            const position = this._mesh.absolutePosition;
238
+            this._broadcast(new GameEvent('ballbounced', { position, material }));
239
+            this._collisionFlags |= flag;
240
+        }
241
+    }
242
+
243
+    /**
244
+     * This callback is invoked when the ball hits a trigger
245
+     * @param {BABYLON.Mesh} trigger
246
+     * @returns {void}
247
+     */
248
+    _onTriggerEnter(trigger)
249
+    {
250
+        if(this._state != 'thrown')
251
+            return;
252
+
253
+        if(trigger.name == 'Trigger_A') {
254
+            if(this._lastTrigger == '')
255
+                this._lastTrigger = 'A';
256
+        }
257
+        else if(trigger.name == 'Trigger_B') {
258
+            if(this._lastTrigger == 'A')
259
+                this._lastTrigger = 'B';
260
+            else if(this._mesh.physicsImpostor.getLinearVelocity().y > 0)
261
+                this._lastTrigger = 'X';
262
+        }
263
+        else if(trigger.name == 'Trigger_C') {
264
+            if(this._lastTrigger == 'B') {
265
+                this._lastTrigger = 'C';
266
+                this._score();
267
+            }
268
+        }
269
+    }
270
+
271
+    /**
272
+     * Score points
273
+     * @returns {void}
274
+     */
275
+    _score()
276
+    {
277
+        const pointA = this._mesh.absolutePosition;
278
+        const pointB = this._positionWhenThrown;
279
+
280
+        const plane = BABYLON.Plane.FromPositionAndNormal(this.ar.root.absolutePosition, this.ar.root.up);
281
+        const projectedPointA = this._orthogonalProjection(plane, pointA);
282
+        const projectedPointB = this._orthogonalProjection(plane, pointB);
283
+
284
+        const distance = BABYLON.Vector3.Distance(projectedPointA, projectedPointB);
285
+        const score = this._calculateScore(distance);
286
+        const position = pointA;
287
+
288
+        this._broadcast(new GameEvent('scored', { score, position }));
289
+    }
290
+
291
+    /**
292
+     * Calculate the score based on the distance traveled by the ball
293
+     * @param {number} distance
294
+     * @returns {number}
295
+     */
296
+    _calculateScore(distance)
297
+    {
298
+        return distance >= THREE_POINT_THRESHOLD ? 3 : 2;
299
+    }
300
+
301
+    /**
302
+     * Put this._plane in front of the camera
303
+     * @returns {void}
304
+     */
305
+    _updatePlane()
306
+    {
307
+        const ar = this.ar;
308
+        const forwardRay = ar.utils.convertRay(ar.viewer.forwardRay());
309
+        const origin = this._planeOrigin.copyFrom(forwardRay.origin);
310
+        const normal = forwardRay.direction;
311
+
312
+        normal.scaleAndAddToRef(PLANE_DISTANCE, origin);
313
+        BABYLON.Plane.FromPositionAndNormalToRef(origin, normal, this._plane);
314
+    }
315
+
316
+    /**
317
+     * Align the ball to a pointer
318
+     * @param {TrackablePointer} pointer
319
+     * @returns {boolean} true on success
320
+     */
321
+    _alignToPointer(pointer)
322
+    {
323
+        const ar = this.ar;
324
+        const ray = ar.utils.convertRay(ar.viewer.raycast(pointer.position));
325
+        this._mesh.physicsImpostor.setAngularVelocity(BABYLON.Vector3.Zero());
326
+        return this._intersectRayAndPlane(this._mesh.position, ray);
327
+    }
328
+
329
+    /**
330
+     * Intersect a ray with this._plane
331
+     * @param {BABYLON.Vector3} outputPoint
332
+     * @param {BABYLON.Ray} ray
333
+     * @returns {boolean} true if there is a single intersection
334
+     */
335
+    _intersectRayAndPlane(outputPoint, ray)
336
+    {
337
+        const normal = this._plane.normal;
338
+        const direction = ray.direction;
339
+
340
+        const dot = direction.dot(normal);
341
+        if(Math.abs(dot) < 1e-5)
342
+            return false;
343
+
344
+        const d = this._planeOrigin.subtract(ray.origin).dot(normal) / dot;
345
+        outputPoint.copyFrom(ray.origin);
346
+        ray.direction.scaleAndAddToRef(d, outputPoint);
347
+
348
+        return true;
349
+    }
350
+
351
+    /**
352
+     * Compute the orthogonal projection of a point onto a plane
353
+     * @param {BABYLON.Plane} plane
354
+     * @param {BABYLON.Vector3} point
355
+     * @returns {BABYLON.Vector3}
356
+     */
357
+    _orthogonalProjection(plane, point)
358
+    {
359
+        const n = plane.normal;
360
+        const p = new BABYLON.Vector3(0, -plane.d/n.y, 0);
361
+        const q = point;
362
+
363
+        const u = q.subtract(p);
364
+        const v = n.scale(u.dot(n));
365
+        return q.subtract(v);
366
+    }
367
+
368
+    /**
369
+     * Create a root node with a physics impostor
370
+     * @param {BABYLON.Mesh} mesh from gltf
371
+     * @param {number} radius radius of the ball
372
+     * @returns {BABYLON.Mesh}
373
+     */
374
+    _createPhysicsRoot(mesh)
375
+    {
376
+        const r = BALL_RADIUS;
377
+
378
+        // prepare the mesh
379
+        mesh.scaling.set(r, r, r); // original radius = 1
380
+        mesh.getChildMeshes().forEach(child => {
381
+            if(child.material)
382
+                child.material.specularIntensity = 0;
383
+        });
384
+
385
+        // create the root node
386
+        const physicsRoot = BABYLON.MeshBuilder.CreateSphere('Ball', { diameter: 2 * r });
387
+        physicsRoot.addChild(mesh);
388
+
389
+        physicsRoot.physicsImpostor = new BABYLON.PhysicsImpostor(physicsRoot, BABYLON.PhysicsImpostor.SphereImpostor, {
390
+            mass: 0.5,
391
+            restitution: 0.5
392
+        });
393
+
394
+        return physicsRoot;
395
+    }
396
+
397
+    /**
398
+     * Handle an event
399
+     * @param {GameEvent} event
400
+     * @returns {void}
401
+     */
402
+    handleEvent(event)
403
+    {
404
+        switch(event.type) {
405
+            case 'targetfound':
406
+                this._mesh.setEnabled(true);
407
+                break;
408
+
409
+            case 'targetlost':
410
+                this._mesh.setEnabled(false);
411
+                break;
412
+
413
+            case 'triggerready':
414
+                this._mesh.actionManager.registerAction(
415
+                    new BABYLON.ExecuteCodeAction({
416
+                        trigger: BABYLON.ActionManager.OnIntersectionEnterTrigger,
417
+                        parameter: {
418
+                            mesh: event.detail.mesh,
419
+                            usePreciseIntersection: true
420
+                        }
421
+                    }, () => this._onTriggerEnter(event.detail.mesh))
422
+                );
423
+                break;
424
+
425
+            case 'colliderready':
426
+                this._mesh.physicsImpostor.registerOnPhysicsCollide(
427
+                    event.detail.impostor,
428
+                    (_, collided) => this._onCollisionEnter(collided)
429
+                );
430
+                break;
431
+
432
+            case 'netready':
433
+                event.detail.entity.setBall(this._mesh);
434
+                break;
435
+        }
436
+    }
437
+}

+ 116
- 0
demos/basketball/src/entities/entity.js Vedi File

@@ -0,0 +1,116 @@
1
+/**
2
+ * -------------------------------------------
3
+ * Magic AR Basketball
4
+ * A demo game of the encantar.js WebAR engine
5
+ * -------------------------------------------
6
+ * @fileoverview Definition of game entities
7
+ * @author Alexandre Martins <alemartf(at)gmail.com> (https://github.com/alemart/encantar-js)
8
+ */
9
+
10
+/**
11
+ * Game Entity
12
+ */
13
+export class Entity
14
+{
15
+    /**
16
+     * Constructor
17
+     * @param {BasketballDemo} demo
18
+     */
19
+    constructor(demo)
20
+    {
21
+        this._demo = demo;
22
+    }
23
+
24
+    /**
25
+     * Initialize the entity
26
+     * @returns {Promise<void>|void}
27
+     */
28
+    init()
29
+    {
30
+        return Promise.resolve();
31
+    }
32
+
33
+    /**
34
+     * Update the entity
35
+     * @returns {void}
36
+     */
37
+    update()
38
+    {
39
+    }
40
+
41
+    /**
42
+     * Release resources
43
+     * @returns {void}
44
+     */
45
+    release()
46
+    {
47
+    }
48
+
49
+    /**
50
+     * Handle an event
51
+     * @param {GameEvent} event
52
+     * @returns {void}
53
+     */
54
+    handleEvent(event)
55
+    {
56
+    }
57
+
58
+    /**
59
+     * Broadcast an event
60
+     * @param {GameEvent} event
61
+     * @returns {void}
62
+     */
63
+    _broadcast(event)
64
+    {
65
+        this._demo.broadcast(event);
66
+    }
67
+
68
+    /**
69
+     * A reference to the ARSystem
70
+     * @returns {ARSystem | null}
71
+     */
72
+    get ar()
73
+    {
74
+        return this._demo.ar;
75
+    }
76
+}
77
+
78
+/**
79
+ * An entity with a physics root node displayed in AR
80
+ * @abstract
81
+ */
82
+export class PhysicsEntity extends Entity
83
+{
84
+    /**
85
+     * Constructor
86
+     * @param {BasketballDemo} demo
87
+     */
88
+    constructor(demo)
89
+    {
90
+        super(demo);
91
+        this._physicsAnchor = null;
92
+    }
93
+
94
+    /**
95
+     * The parent of the physics root node
96
+     * Parenting the physics root should be done after creating all the impostors
97
+     * @returns {BABYLON.TransformNode}
98
+     */
99
+    get physicsAnchor()
100
+    {
101
+        //return this.ar.root; // doesn't work as expected with multiple physics compounds (babylon.js 7.38)
102
+
103
+        if(!this._physicsAnchor) {
104
+            const ar = this.ar;
105
+            const id = PhysicsEntity._nextId++;
106
+
107
+            // physicsAnchor is an intermediate node
108
+            this._physicsAnchor = new BABYLON.TransformNode('physicsAnchor_' + id);
109
+            this._physicsAnchor.parent = ar.root;
110
+        }
111
+
112
+        return this._physicsAnchor;
113
+    }
114
+}
115
+
116
+PhysicsEntity._nextId = 1;

+ 93
- 0
demos/basketball/src/entities/game-controller.js Vedi File

@@ -0,0 +1,93 @@
1
+/**
2
+ * -------------------------------------------
3
+ * Magic AR Basketball
4
+ * A demo game of the encantar.js WebAR engine
5
+ * -------------------------------------------
6
+ * @fileoverview The Game Controller manages the state of the game
7
+ * @author Alexandre Martins <alemartf(at)gmail.com> (https://github.com/alemart/encantar-js)
8
+ */
9
+
10
+import { Entity } from './entity.js';
11
+import { GameEvent } from '../core/events.js';
12
+import { NUMBER_OF_BALLS, SCORE_TABLE } from '../core/globals.js';
13
+
14
+/**
15
+ * The Game Controller manages the state of the game
16
+ */
17
+export class GameController extends Entity
18
+{
19
+    /**
20
+     * Constructor
21
+     * @param {BasketballDemo} demo
22
+     */
23
+    constructor(demo)
24
+    {
25
+        super(demo);
26
+
27
+        this._ballsLeft = NUMBER_OF_BALLS;
28
+        this._score = 0;
29
+    }
30
+
31
+    /**
32
+     * Initialize the entity
33
+     * @returns {void}
34
+     */
35
+    init()
36
+    {
37
+        this._broadcast(new GameEvent('newball', { ballsLeft: this._ballsLeft }));
38
+        this._broadcast(new GameEvent('newscore', { score: this._score }));
39
+    }
40
+
41
+    /**
42
+     * Handle an event
43
+     * @param {GameEvent} event
44
+     * @returns {void}
45
+     */
46
+    handleEvent(event)
47
+    {
48
+        switch(event.type) {
49
+            case 'scored':
50
+                this._score += event.detail.score;
51
+                this._broadcast(new GameEvent('newscore', { score: this._score }));
52
+                break;
53
+
54
+            case 'lostball':
55
+                if(--this._ballsLeft <= 0) {
56
+                    const rank = this._computeRank(this._score);
57
+                    this._broadcast(new GameEvent('newball', { ballsLeft: 0 }));
58
+                    this._broadcast(new GameEvent('gameover', { rank }));
59
+                }
60
+                else
61
+                    this._broadcast(new GameEvent('newball', { ballsLeft: this._ballsLeft }));
62
+                break;
63
+
64
+            case 'restarted':
65
+                this._score = 0;
66
+                this._ballsLeft = NUMBER_OF_BALLS;
67
+                this._broadcast(new GameEvent('newscore', { score: this._score }));
68
+                this._broadcast(new GameEvent('newball', { ballsLeft: this._ballsLeft }));
69
+                break;
70
+
71
+            case 'targetlost':
72
+                this._broadcast(new GameEvent('restarted'));
73
+                break;
74
+        }
75
+    }
76
+
77
+    /**
78
+     * Compute the rank based on the score of the player
79
+     * @param {number} score
80
+     * @returns {string}
81
+     */
82
+    _computeRank(score)
83
+    {
84
+        const entries = Object.entries(SCORE_TABLE).sort((a, b) => b[1] - a[1]);
85
+
86
+        for(const [rank, minScore] of entries) {
87
+            if(score >= minScore)
88
+                return rank;
89
+        }
90
+
91
+        return '?';
92
+    }
93
+}

+ 106
- 0
demos/basketball/src/entities/goal.js Vedi File

@@ -0,0 +1,106 @@
1
+/**
2
+ * -------------------------------------------
3
+ * Magic AR Basketball
4
+ * A demo game of the encantar.js WebAR engine
5
+ * -------------------------------------------
6
+ * @fileoverview The Basketball Goal
7
+ * @author Alexandre Martins <alemartf(at)gmail.com> (https://github.com/alemart/encantar-js)
8
+ */
9
+
10
+import { PhysicsEntity } from './entity.js';
11
+import { GameEvent } from '../core/events.js';
12
+
13
+/** Offset from ar.root in the local y-axis */
14
+const Y_OFFSET = -0.35;
15
+
16
+/**
17
+ * The Basketball Goal
18
+ */
19
+export class Goal extends PhysicsEntity
20
+{
21
+    /**
22
+     * Constructor
23
+     * @param {BasketballDemo} demo
24
+     */
25
+    constructor(demo)
26
+    {
27
+        super(demo);
28
+        this._physicsRoot = null;
29
+    }
30
+
31
+    /**
32
+     * Initialize the entity
33
+     * @returns {Promise<void>}
34
+     */
35
+    async init()
36
+    {
37
+        const file = this._demo.assetManager.file('goal.glb');
38
+        const gltf = await BABYLON.SceneLoader.ImportMeshAsync('', '', file);
39
+        this._physicsRoot = this._createPhysicsRoot(gltf.meshes);
40
+        this._physicsRoot.position.y = Y_OFFSET;
41
+    }
42
+
43
+    /**
44
+     * Create a root node with a physics impostor
45
+     * @param {BABYLON.Mesh[]} meshes from gltf
46
+     * @returns {BABYLON.Mesh}
47
+     */
48
+    _createPhysicsRoot(meshes)
49
+    {
50
+        const physicsRoot = new BABYLON.Mesh('Goal');
51
+        const hooks = [];
52
+
53
+        meshes.forEach(mesh => {
54
+            if(mesh.name.startsWith('Collider_') || mesh.name.startsWith('Trigger_') || mesh.name.startsWith('Hook_')) {
55
+                mesh.isVisible = false;
56
+                physicsRoot.addChild(mesh);
57
+            }
58
+        });
59
+
60
+        meshes.forEach(mesh => {
61
+            if(mesh.parent == null)
62
+                physicsRoot.addChild(mesh);
63
+        });
64
+
65
+        physicsRoot.getChildMeshes().forEach(mesh => {
66
+            if(mesh.name.startsWith('Collider_')) {
67
+                mesh.scaling.x = Math.abs(mesh.scaling.x);
68
+                mesh.scaling.y = Math.abs(mesh.scaling.y);
69
+                mesh.scaling.z = Math.abs(mesh.scaling.z);
70
+
71
+                mesh.physicsImpostor = new BABYLON.PhysicsImpostor(mesh, BABYLON.PhysicsImpostor.BoxImpostor, {
72
+                    mass: 0,
73
+                });
74
+            }
75
+            else if(mesh.name.startsWith('Trigger_'))
76
+                this._broadcast(new GameEvent('triggerready', { mesh }));
77
+            else if(mesh.name.startsWith('Hook_'))
78
+                hooks.push(mesh);
79
+        });
80
+
81
+        hooks.sort((a, b) => a.name.localeCompare(b.name));
82
+        this._broadcast(new GameEvent('hooksready', { hooks }));
83
+
84
+        physicsRoot.physicsImpostor = new BABYLON.PhysicsImpostor(physicsRoot, BABYLON.PhysicsImpostor.NoImpostor, {
85
+            mass: 0
86
+        });
87
+
88
+        physicsRoot.parent = this.physicsAnchor;
89
+
90
+        this._broadcast(new GameEvent('colliderready', { impostor: physicsRoot.physicsImpostor }));
91
+        return physicsRoot;
92
+    }
93
+
94
+    /**
95
+     * Handle an event
96
+     * @param {GameEvent} event
97
+     * @returns {void}
98
+     */
99
+    handleEvent(event)
100
+    {
101
+        if(event.type == 'netready') {
102
+            const net = event.detail.entity;
103
+            net.moveBy(this._physicsRoot.position);
104
+        }
105
+    }
106
+}

+ 65
- 0
demos/basketball/src/entities/gravity.js Vedi File

@@ -0,0 +1,65 @@
1
+/**
2
+ * -------------------------------------------
3
+ * Magic AR Basketball
4
+ * A demo game of the encantar.js WebAR engine
5
+ * -------------------------------------------
6
+ * @fileoverview Gravity of the virtual scene
7
+ * @author Alexandre Martins <alemartf(at)gmail.com> (https://github.com/alemart/encantar-js)
8
+ */
9
+
10
+import { Entity } from './entity.js';
11
+
12
+/** The magnitude of the gravity vector in world units per second squared */
13
+const MAGNITUDE = 9.81;
14
+
15
+/**
16
+ * This entity updates the direction of gravity based on AR tracking results
17
+ */
18
+export class Gravity extends Entity
19
+{
20
+    /**
21
+     * Constructor
22
+     * @param {BasketballDemo} demo
23
+     */
24
+    constructor(demo)
25
+    {
26
+        super(demo);
27
+        this._gravity = new BABYLON.Vector3();
28
+        this._isTracking = false;
29
+    }
30
+
31
+    /**
32
+     * Update gravity
33
+     * @returns {void}
34
+     */
35
+    update()
36
+    {
37
+        const ar = this.ar;
38
+
39
+        // calculate the gravity vector in world space
40
+        if(this._isTracking) {
41
+            // we assume that the target image is parallel to a wall,
42
+            // because basketball boards are perpendicular to the ground
43
+            this._gravity.copyFrom(ar.root.up).scaleInPlace(-MAGNITUDE);
44
+        }
45
+        else {
46
+            // use a default gravity vector
47
+            this._gravity.set(0, -MAGNITUDE, 0);
48
+        }
49
+
50
+        ar.scene.getPhysicsEngine().setGravity(this._gravity);
51
+    }
52
+
53
+    /**
54
+     * Handle an event
55
+     * @param {GameEvent} event
56
+     * @returns {void}
57
+     */
58
+    handleEvent(event)
59
+    {
60
+        if(event.type == 'targetfound')
61
+            this._isTracking = true;
62
+        else if(event.type == 'targetlost')
63
+            this._isTracking = false;
64
+    }
65
+}

+ 112
- 0
demos/basketball/src/entities/gui/ball-counter.js Vedi File

@@ -0,0 +1,112 @@
1
+/**
2
+ * -------------------------------------------
3
+ * Magic AR Basketball
4
+ * A demo game of the encantar.js WebAR engine
5
+ * -------------------------------------------
6
+ * @fileoverview Ball Counter - 2D Graphical User Interface (GUI)
7
+ * @author Alexandre Martins <alemartf(at)gmail.com> (https://github.com/alemart/encantar-js)
8
+ */
9
+
10
+import { GUIControl } from './gui-control.js';
11
+import { Entity } from '../entity.js';
12
+import { NUMBER_OF_BALLS } from '../../core/globals.js';
13
+
14
+/**
15
+ * Ball Icon
16
+ */
17
+class BallIcon extends GUIControl
18
+{
19
+    /**
20
+     * Create the control
21
+     * @returns {BABYLON.GUI.Control}
22
+     */
23
+    _createControl()
24
+    {
25
+        const url = this._demo.assetManager.url('atlas.png');
26
+        const icon = new BABYLON.GUI.Image('ballIcon', url);
27
+
28
+        icon.sourceLeft = 896;
29
+        icon.sourceTop = 0;
30
+        icon.sourceWidth = 128;
31
+        icon.sourceHeight = 128;
32
+
33
+        icon.width = '48px';
34
+        icon.height = '48px';
35
+
36
+        icon.left = 8;
37
+        icon.top = 0;
38
+        icon.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
39
+        icon.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_CENTER;
40
+
41
+        icon.notRenderable = true;
42
+
43
+        return icon;
44
+    }
45
+
46
+    /**
47
+     * Reposition the icon
48
+     * @param {number} yOffset
49
+     * @returns {void}
50
+     */
51
+    _repositionBy(yOffset)
52
+    {
53
+        const icon = this.control;
54
+        const size = parseInt(icon.height);
55
+
56
+        icon.top = yOffset * size;
57
+    }
58
+
59
+    /**
60
+     * Set the ID of the icon
61
+     * @param {number} id
62
+     * @returns {this}
63
+     */
64
+    setId(id)
65
+    {
66
+        const j = (NUMBER_OF_BALLS / 2) | 0;
67
+
68
+        this._id = id;
69
+        this._repositionBy((id - j) * 1.125);
70
+
71
+        return this;
72
+    }
73
+
74
+    /**
75
+     * Handle an event
76
+     * @param {GameEvent} event
77
+     * @returns {void}
78
+     */
79
+    handleEvent(event)
80
+    {
81
+        if(event.type == 'newball') {
82
+            const ballsLeft = event.detail.ballsLeft;
83
+            this.control.notRenderable = !(this._id < ballsLeft);
84
+        }
85
+        else if(event.type == 'targetfound')
86
+            this.control.isVisible = true;
87
+        else if(event.type == 'targetlost')
88
+            this.control.isVisible = false;
89
+    }
90
+}
91
+
92
+/**
93
+ * Ball Counter
94
+ */
95
+export class BallCounter extends Entity
96
+{
97
+    /**
98
+     * Initialize the entity
99
+     * @returns {Promise<void>}
100
+     */
101
+    init()
102
+    {
103
+        const icons = [];
104
+
105
+        for(let i = 0; i < NUMBER_OF_BALLS; i++) {
106
+            const icon = this._demo.spawn(BallIcon).then(icon => icon.setId(i));
107
+            icons.push(icon);
108
+        }
109
+
110
+        return Promise.all(icons);
111
+    }
112
+}

+ 139
- 0
demos/basketball/src/entities/gui/gameover-overlay.js Vedi File

@@ -0,0 +1,139 @@
1
+/**
2
+ * -------------------------------------------
3
+ * Magic AR Basketball
4
+ * A demo game of the encantar.js WebAR engine
5
+ * -------------------------------------------
6
+ * @fileoverview An overlay displayed at the end of a match - 2D Graphical User Interface (GUI)
7
+ * @author Alexandre Martins <alemartf(at)gmail.com> (https://github.com/alemart/encantar-js)
8
+ */
9
+
10
+import { GUIControl } from './gui-control.js';
11
+import { GameEvent } from '../../core/events.js';
12
+
13
+/**
14
+ * An overlay displayed at the end of a match
15
+ */
16
+export class GameOverOverlay extends GUIControl
17
+{
18
+    /**
19
+     * Constructor
20
+     * @param {BasketballDemo} demo
21
+     */
22
+    constructor(demo)
23
+    {
24
+        super(demo);
25
+
26
+        this._messages = {
27
+            'S' : 'YOU ARE A\nLEGEND!!!!!',
28
+            'A+': 'Well done!\nYou\'re a Pro!',
29
+            'A' : 'Well done!\nYou\'re a Pro!',
30
+            'B+': 'Nice, but you\'re\nnot yet a Pro!',
31
+            'B' : 'You can do better!',
32
+            'C' : 'Try again!',
33
+            'F' : 'Try again!'
34
+        };
35
+    }
36
+
37
+    /**
38
+     * Create the control
39
+     * @returns {BABYLON.GUI.Control}
40
+     */
41
+    _createControl()
42
+    {
43
+        const container = new BABYLON.GUI.Container();
44
+        const title = new BABYLON.GUI.TextBlock();
45
+        const rank = new BABYLON.GUI.TextBlock('rank');
46
+        const message = new BABYLON.GUI.TextBlock('message');
47
+        const circle = new BABYLON.GUI.Ellipse();
48
+
49
+        container.background = 'rgba(51, 51, 76, 0.75)';
50
+        container.zIndex = 1;
51
+
52
+        title.textHorizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER;
53
+        title.textVerticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_CENTER;
54
+        title.text = 'Rank';
55
+        title.color = 'white';
56
+        title.fontFamily = 'sans-serif';
57
+        title.fontStyle = 'bold';
58
+        title.fontSize = 80;
59
+        title.top = '-192px';
60
+        title.left = '0px';
61
+        container.addControl(title);
62
+
63
+        rank.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER;
64
+        rank.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_CENTER;
65
+        rank.text = '';
66
+        rank.color = 'white';
67
+        rank.fontFamily = 'sans-serif';
68
+        rank.fontStyle = 'bold';
69
+        rank.fontSize = 112;
70
+        container.addControl(rank);
71
+
72
+        circle.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER;
73
+        circle.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_CENTER;
74
+        circle.width = '192px';
75
+        circle.height = circle.width;
76
+        circle.color = 'white';
77
+        circle.thickness = 16;
78
+        container.addControl(circle);
79
+
80
+        message.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER;
81
+        message.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_CENTER;
82
+        message.text = '';
83
+        message.textWrapping = true;
84
+        message.color = 'white';
85
+        message.fontFamily = 'sans-serif';
86
+        message.fontSize = 56;
87
+        message.top = '192px';
88
+        message.left = '0px';
89
+        message.paddingLeft = '10%';
90
+        message.paddingRight = '10%';
91
+        container.addControl(message);
92
+
93
+        return container;
94
+    }
95
+
96
+    /**
97
+     * Update the entity
98
+     * @returns {void}
99
+     */
100
+    update()
101
+    {
102
+        const container = this.control;
103
+        if(!container.isVisible)
104
+            return;
105
+
106
+        const ar = this.ar;
107
+        if(ar.pointers.length == 0)
108
+            return;
109
+
110
+        const pointer = ar.pointers[0];
111
+        if(pointer.phase != 'began')
112
+            return;
113
+
114
+        // hide the overlay when touching the screen
115
+        container.isVisible = false;
116
+        this._broadcast(new GameEvent('restarted'));
117
+    }
118
+
119
+    /**
120
+     * Handle an event
121
+     * @param {GameEvent} event
122
+     * @returns {void}
123
+     */
124
+    handleEvent(event)
125
+    {
126
+        if(event.type == 'gameover') {
127
+            const container = this.control;
128
+            const rank = container.getChildByName('rank');
129
+            const message = container.getChildByName('message');
130
+
131
+            rank.text = event.detail.rank;
132
+            message.text = this._messages[event.detail.rank] || '';
133
+
134
+            container.isVisible = true;
135
+        }
136
+        else if(event.type == 'targetlost')
137
+            this.control.isVisible = false;
138
+    }
139
+}

+ 59
- 0
demos/basketball/src/entities/gui/gui-control.js Vedi File

@@ -0,0 +1,59 @@
1
+/**
2
+ * -------------------------------------------
3
+ * Enchanted AR Basketball
4
+ * A demo game of the encantar.js WebAR engine
5
+ * -------------------------------------------
6
+ * @fileoverview an entity that wraps BABYLON.GUI.Control - 2D Graphical User Interface (GUI)
7
+ * @author Alexandre Martins <alemartf(at)gmail.com> (https://github.com/alemart/encantar-js)
8
+ */
9
+
10
+import { Entity } from '../entity.js';
11
+
12
+/**
13
+ * An entity that wraps BABYLON.GUI.Control
14
+ * @abstract
15
+ */
16
+export class GUIControl extends Entity
17
+{
18
+    /**
19
+     * Constructor
20
+     * @param {BasketballDemo} demo
21
+     */
22
+    constructor(demo)
23
+    {
24
+        super(demo);
25
+        this._parent = demo.gui;
26
+        this._control = null;
27
+    }
28
+
29
+    /**
30
+     * Create the control (template method)
31
+     * @returns {BABYLON.GUI.Control}
32
+     * @abstract
33
+     */
34
+    _createControl()
35
+    {
36
+        throw new Error('Abstract method');
37
+    }
38
+
39
+    /**
40
+     * Get the underlying control
41
+     * @returns {BABYLON.GUI.Control}
42
+     */
43
+    get control()
44
+    {
45
+        return this._control;
46
+    }
47
+
48
+    /**
49
+     * Initialize the entity
50
+     * @returns {void}
51
+     */
52
+    init()
53
+    {
54
+        this._control = this._createControl();
55
+        this._control.isVisible = false;
56
+
57
+        this._parent.addControl(this._control);
58
+    }
59
+}

+ 75
- 0
demos/basketball/src/entities/gui/mute-button.js Vedi File

@@ -0,0 +1,75 @@
1
+/**
2
+ * -------------------------------------------
3
+ * Magic AR Basketball
4
+ * A demo game of the encantar.js WebAR engine
5
+ * -------------------------------------------
6
+ * @fileoverview Mute/Unmute button - 2D Graphical User Interface (GUI)
7
+ * @author Alexandre Martins <alemartf(at)gmail.com> (https://github.com/alemart/encantar-js)
8
+ */
9
+
10
+import { GUIControl } from './gui-control.js';
11
+import { GameEvent } from '../../core/events.js';
12
+import { NUMBER_OF_BALLS } from '../../core/globals.js';
13
+
14
+/**
15
+ * Mute/Unmute button
16
+ */
17
+export class MuteButton extends GUIControl
18
+{
19
+    /**
20
+     * Create the control
21
+     * @returns {BABYLON.GUI.Control}
22
+     */
23
+    _createControl()
24
+    {
25
+        const url = this._demo.assetManager.url('atlas.png');
26
+        const button = BABYLON.GUI.Button.CreateImageOnlyButton('muteButton', url);
27
+        const offset = 1.5 + ((NUMBER_OF_BALLS / 2) | 0);
28
+
29
+        button.image.sourceLeft = 640;
30
+        button.image.sourceTop = 256;
31
+        button.image.sourceWidth = 128;
32
+        button.image.sourceHeight = 128;
33
+
34
+        button.width = '48px';
35
+        button.height = '48px';
36
+
37
+        button.left = 8;
38
+        button.top = -offset * 48 * 1.125;
39
+        button.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
40
+        button.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_CENTER;
41
+
42
+        button.onPointerClickObservable.add(this._onClick.bind(this));
43
+        button.alpha = 0.75;
44
+
45
+        return button;
46
+    }
47
+
48
+    /**
49
+     * Click handler
50
+     * @returns {void}
51
+     */
52
+    _onClick()
53
+    {
54
+        const button = this.control;
55
+        const wasMuted = (button.image.sourceTop != 256);
56
+        const isMuted = !wasMuted;
57
+
58
+        button.image.sourceTop = !isMuted ? 256 : 384;
59
+
60
+        this._broadcast(new GameEvent(isMuted ? 'muted' : 'unmuted'));
61
+    }
62
+
63
+    /**
64
+     * Handle an event
65
+     * @param {GameEvent} event
66
+     * @returns {void}
67
+     */
68
+    handleEvent(event)
69
+    {
70
+        if(event.type == 'targetfound')
71
+            this.control.isVisible = true;
72
+        else if(event.type == 'targetlost')
73
+            this.control.isVisible = false;
74
+    }
75
+}

+ 130
- 0
demos/basketball/src/entities/gui/tutorial-overlay.js Vedi File

@@ -0,0 +1,130 @@
1
+/**
2
+ * -------------------------------------------
3
+ * Magic AR Basketball
4
+ * A demo game of the encantar.js WebAR engine
5
+ * -------------------------------------------
6
+ * @fileoverview An overlay that shows how to play the game - 2D Graphical User Interface (GUI)
7
+ * @author Alexandre Martins <alemartf(at)gmail.com> (https://github.com/alemart/encantar-js)
8
+ */
9
+
10
+import { GUIControl } from './gui-control.js';
11
+
12
+/** Duration of the swipe animation, in seconds */
13
+const ANIMATION_DURATION = 1.5;
14
+
15
+/**
16
+ * An overlay that shows how to play the game
17
+ */
18
+export class TutorialOverlay extends GUIControl
19
+{
20
+    /**
21
+     * Constructor
22
+     * @param {BasketballDemo} demo
23
+     */
24
+    constructor(demo)
25
+    {
26
+        super(demo);
27
+        this._timer = 0;
28
+        this._enabled = true;
29
+    }
30
+
31
+    /**
32
+     * Create the control
33
+     * @returns {BABYLON.GUI.Control}
34
+     */
35
+    _createControl()
36
+    {
37
+        const url = this._demo.assetManager.url('atlas.png');
38
+        const container = new BABYLON.GUI.Container();
39
+        const text = new BABYLON.GUI.TextBlock();
40
+        const hand = new BABYLON.GUI.Image('hand', url);
41
+
42
+        container.background = 'rgba(51, 51, 76, 0.75)';
43
+        container.zIndex = 1;
44
+
45
+        text.textHorizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER;
46
+        text.textVerticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_CENTER;
47
+        text.text = 'Swipe to\nshoot';
48
+        text.color = 'white';
49
+        text.fontFamily = 'sans-serif';
50
+        text.fontStyle = 'bold';
51
+        text.fontSize = 96;
52
+        text.top = '0%';
53
+        text.left = 0;
54
+        text.zIndex = 1;
55
+        container.addControl(text);
56
+
57
+        hand.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_RIGHT;
58
+        hand.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM;
59
+        hand.stretch = BABYLON.GUI.Image.STRETCH_NONE;
60
+        hand.sourceLeft = 768;
61
+        hand.sourceTop = 256;
62
+        hand.sourceWidth = 256;
63
+        hand.sourceHeight = 256;
64
+        hand.width = '384px';
65
+        hand.height = '384px';
66
+        hand.top = '-50%';
67
+        container.addControl(hand);
68
+
69
+        return container;
70
+    }
71
+
72
+    /**
73
+     * Update the entity
74
+     * @returns {void}
75
+     */
76
+    update()
77
+    {
78
+        const container = this.control;
79
+
80
+        // check if the tutorial is enabled / being displayed
81
+        if(!this._enabled || !container.isVisible)
82
+            return;
83
+
84
+        // hide the overlay when touching the screen
85
+        const ar = this.ar;
86
+        if(ar.pointers.length > 0) {
87
+            const pointer = ar.pointers[0];
88
+            if(pointer.phase == 'began') {
89
+                container.isVisible = false;
90
+                this._enabled = false;
91
+                return;
92
+            }
93
+        }
94
+
95
+        // advance the timer
96
+        const dt = ar.session.time.delta;
97
+        this._timer += dt / ANIMATION_DURATION;
98
+        this._timer -= Math.floor(this._timer);
99
+
100
+        // tweening
101
+        const t = this._ease(this._timer);
102
+        const top = -40 * t - 10;
103
+        const hand = container.getChildByName('hand');
104
+        if(hand)
105
+            hand.top = top + '%';
106
+    }
107
+
108
+    /**
109
+     * Easing function
110
+     * @param {number} t in [0,1]
111
+     * @returns {number} f(t) in [0,1]
112
+     */
113
+    _ease(t)
114
+    {
115
+        return 0.5 - 0.5 * Math.cos(Math.PI * t);
116
+    }
117
+
118
+    /**
119
+     * Handle an event
120
+     * @param {GameEvent} event
121
+     * @returns {void}
122
+     */
123
+    handleEvent(event)
124
+    {
125
+        if(event.type == 'targetfound')
126
+            this.control.isVisible = this._enabled;
127
+        else if(event.type == 'targetlost' || event.type == 'guiresized')
128
+            this.control.isVisible = false;
129
+    }
130
+}

+ 131
- 0
demos/basketball/src/entities/jukebox.js Vedi File

@@ -0,0 +1,131 @@
1
+/**
2
+ * -------------------------------------------
3
+ * Magic AR Basketball
4
+ * A demo game of the encantar.js WebAR engine
5
+ * -------------------------------------------
6
+ * @fileoverview An entity that plays sounds
7
+ * @author Alexandre Martins <alemartf(at)gmail.com> (https://github.com/alemart/encantar-js)
8
+ */
9
+
10
+import { Entity } from './entity.js';
11
+import { GameEvent } from '../core/events.js';
12
+import { ASSET_LIST } from '../core/asset-list.js';
13
+
14
+/**
15
+ * An entity that plays sounds
16
+ */
17
+export class Jukebox extends Entity
18
+{
19
+     /**
20
+     * Constructor
21
+     * @param {BasketballDemo} demo
22
+     */
23
+    constructor(demo)
24
+    {
25
+        super(demo);
26
+        this._sound = new Map();
27
+    }
28
+
29
+    /**
30
+     * Initialize the entity
31
+     * @returns {void}
32
+     */
33
+    init()
34
+    {
35
+        const soundFiles = ASSET_LIST.filter(asset => asset.endsWith('.wav'));
36
+
37
+        for(const filepath of soundFiles) {
38
+            const url = this._demo.assetManager.url(filepath);
39
+            const soundName = filepath.substring(0, filepath.length - 4);
40
+            const sound = new BABYLON.Sound(soundName, url);
41
+
42
+            this._sound.set(soundName, sound);
43
+        }
44
+
45
+        BABYLON.Engine.audioEngine.useCustomUnlockedButton = true;
46
+    }
47
+
48
+    /**
49
+     * Handle an event
50
+     * @param {GameEvent} event
51
+     * @returns {void}
52
+     */
53
+    handleEvent(event)
54
+    {
55
+        switch(event.type) {
56
+            case 'scored':
57
+                this._play('net', event.detail.position);
58
+                if(event.detail.score == 3)
59
+                    this._play('bonus');
60
+                break;
61
+
62
+            case 'gameover':
63
+                if(/^[AS]/.test(event.detail.rank))
64
+                    this._play('win');
65
+                else if(event.detail.rank == 'B+')
66
+                    this._play('bonus');
67
+                else
68
+                    this._play('lose');
69
+                break;
70
+
71
+            case 'ballbounced':
72
+                if(event.detail.material == 'backboard')
73
+                    this._play('backboard', event.detail.position);
74
+                else
75
+                    this._play('bounce', event.detail.position);
76
+                break;
77
+
78
+            case 'unmuted':
79
+                this._unmute();
80
+                this._play('button');
81
+                break;
82
+
83
+            case 'muted':
84
+                this._mute();
85
+                break;
86
+        }
87
+    }
88
+
89
+    /**
90
+     * Play a sound
91
+     * @param {string} soundName
92
+     * @param {BABYLON.Vector3|null} [position]
93
+     * @returns {void}
94
+     */
95
+    _play(soundName, position = null)
96
+    {
97
+        const sfx = this._sound.get(soundName);
98
+        if(!sfx)
99
+            return;
100
+
101
+        if(!BABYLON.Engine.audioEngine.unlocked)
102
+            BABYLON.Engine.audioEngine.unlock();
103
+
104
+        if(position !== null) {
105
+            sfx.spatialSound = true;
106
+            sfx.setPosition(position);
107
+        }
108
+        else
109
+            sfx.spatialSound = false;
110
+
111
+        sfx.play();
112
+    }
113
+
114
+    /**
115
+     * Mute the game
116
+     * @returns {void}
117
+     */
118
+    _mute()
119
+    {
120
+        BABYLON.Engine.audioEngine.setGlobalVolume(0.0);
121
+    }
122
+
123
+    /**
124
+     * Unmute the game
125
+     * @returns {void}
126
+     */
127
+    _unmute()
128
+    {
129
+        BABYLON.Engine.audioEngine.setGlobalVolume(1.0);
130
+    }
131
+}

+ 38
- 0
demos/basketball/src/entities/lights.js Vedi File

@@ -0,0 +1,38 @@
1
+/**
2
+ * -------------------------------------------
3
+ * Magic AR Basketball
4
+ * A demo game of the encantar.js WebAR engine
5
+ * -------------------------------------------
6
+ * @fileoverview The lights of the virtual scene
7
+ * @author Alexandre Martins <alemartf(at)gmail.com> (https://github.com/alemart/encantar-js)
8
+ */
9
+
10
+import { Entity } from './entity.js';
11
+
12
+/**
13
+ * The lights of the virtual scene
14
+ */
15
+export class Lights extends Entity
16
+{
17
+    /**
18
+     * Initialize the entity
19
+     * @returns {void}
20
+     */
21
+    init()
22
+    {
23
+        const light = new BABYLON.HemisphericLight('light', BABYLON.Vector3.Up());
24
+        const dlight = new BABYLON.DirectionalLight('dlight', BABYLON.Vector3.Down());
25
+
26
+        light.intensity = 1.0;
27
+        light.diffuse.set(1, 1, 1);
28
+        light.groundColor.set(1, 1, 1);
29
+        light.specular.set(0, 0, 0);
30
+
31
+        dlight.intensity = 1.0;
32
+        dlight.diffuse.set(1, 1, 1);
33
+        dlight.specular.set(1, 1, 1);
34
+
35
+        const ar = this.ar;
36
+        dlight.parent = ar.root;
37
+    }
38
+}

+ 450
- 0
demos/basketball/src/entities/net.js Vedi File

@@ -0,0 +1,450 @@
1
+/**
2
+ * -------------------------------------------
3
+ * Magic AR Basketball
4
+ * A demo game of the encantar.js WebAR engine
5
+ * -------------------------------------------
6
+ * @fileoverview Basketball Net with cloth physics
7
+ * @author Alexandre Martins <alemartf(at)gmail.com> (https://github.com/alemart/encantar-js)
8
+ */
9
+
10
+import { PhysicsEntity } from './entity.js';
11
+import { GameEvent } from '../core/events.js';
12
+
13
+/** Number of rings / segments parallel to the XZ plane (in model space) */
14
+const NUMBER_OF_RINGS = 4;
15
+
16
+/** Height of the mesh */
17
+const HEIGHT = 0.67;
18
+
19
+/** XZ scale of the bottom ring */
20
+const XZ_FALLOFF_SCALE = 0.6;
21
+
22
+/** XZ scale of the particles */
23
+const XZ_PARTICLE_SCALE = 0.8; //1;
24
+
25
+/**
26
+ * Basketball Net with cloth physics
27
+ */
28
+export class BasketballNet extends PhysicsEntity
29
+{
30
+    /**
31
+     * Constructor
32
+     * @param {BasketballDemo} demo
33
+     */
34
+    constructor(demo)
35
+    {
36
+        super(demo);
37
+
38
+        this._physicsRoot = null;
39
+        this._mesh = null;
40
+        this._mirrorMesh = null;
41
+
42
+        this._vertices = [];
43
+        this._indices = [];
44
+        this._particles = [];
45
+
46
+        this._ball = null;
47
+        this._freezeTime = 0;
48
+        this._xzParticleScaleVector = new BABYLON.Vector3(XZ_PARTICLE_SCALE, 1, XZ_PARTICLE_SCALE);
49
+    }
50
+
51
+    /**
52
+     * Initialize the entity
53
+     * @returns {void}
54
+     */
55
+    init()
56
+    {
57
+        this._physicsRoot = new BABYLON.Mesh('BasketballNet');
58
+
59
+        this._mesh = new BABYLON.Mesh('BasketballNetMesh');
60
+        this._mesh.material = this._createMaterial();
61
+        this._mesh.parent = this._physicsRoot;
62
+        this._mesh.alphaIndex = 1;
63
+
64
+        this._mirrorMesh = new BABYLON.Mesh('BasketballNetMirrorMesh');
65
+        this._mirrorMesh.material = this._mesh.material;
66
+        this._mirrorMesh.parent = this._physicsRoot;
67
+        this._mirrorMesh.alphaIndex = 0;
68
+    }
69
+
70
+    /**
71
+     * Update the entity
72
+     * @returns {void}
73
+     */
74
+    update()
75
+    {
76
+        const maxSpeed = 2.5, minSpeed = 0.1, maxTime = 1.5;
77
+        const deceleration = maxSpeed / maxTime;
78
+        const elapsedTime = this.ar.session.time.elapsed;
79
+
80
+        // we only move the particles if the ball is or was just nearby
81
+        if(this._mesh.intersectsMesh(this._ball))
82
+            this._freezeTime = elapsedTime + maxTime;
83
+
84
+        if(elapsedTime < this._freezeTime) {
85
+            this._sleepParticles(false);
86
+            let avgSpeed = this._decelerateParticles(deceleration, maxSpeed);
87
+
88
+            // stop early
89
+            if(avgSpeed < minSpeed)
90
+                this._freezeTime = elapsedTime;
91
+        }
92
+        else {
93
+            this._resetParticles();
94
+            this._sleepParticles(true);
95
+        }
96
+
97
+        this._updateMesh();
98
+    }
99
+
100
+    /**
101
+     * Move the net by an offset
102
+     * @param {BABYLON.Vector3} offset
103
+     * @returns {void}
104
+     */
105
+    moveBy(offset)
106
+    {
107
+        this._physicsRoot.position.addInPlace(offset);
108
+    }
109
+
110
+    /**
111
+     * Set a reference to the mesh of the ball
112
+     * @param {BABYLON.Mesh} ball
113
+     * @returns {void}
114
+     */
115
+    setBall(ball)
116
+    {
117
+        this._ball = ball;
118
+    }
119
+
120
+    /**
121
+     * Handle an event
122
+     * @param {GameEvent} event
123
+     * @returns {void}
124
+     */
125
+    handleEvent(event)
126
+    {
127
+        if(event.type == 'hooksready') {
128
+            const hooks = event.detail.hooks;
129
+            this._createNet(hooks);
130
+        }
131
+        else if(event.type == 'targetfound') {
132
+            if(this._particles.length > 0) {
133
+                this._resetParticles();
134
+                this._updateMesh();
135
+                this._freezeTime = 0;
136
+            }
137
+        }
138
+    }
139
+
140
+    /**
141
+     * Create the material of the basketball net
142
+     * @returns {BABYLON.StandardMaterial}
143
+     */
144
+    _createMaterial()
145
+    {
146
+        const material = new BABYLON.StandardMaterial('BasketballNetMaterial');
147
+        const url = this._demo.assetManager.url('atlas.png');
148
+
149
+        material.diffuseTexture = new BABYLON.Texture(url);
150
+        material.diffuseTexture.hasAlpha = true;
151
+        material.useAlphaFromDiffuseTexture = true;
152
+        material.backFaceCulling = true;
153
+        material.unlit = true;
154
+        //material.wireframe = true;
155
+
156
+        return material;
157
+    }
158
+
159
+    /**
160
+     * Create the basketball net
161
+     * @param {BABYLON.Mesh[]} hooks
162
+     * @returns {void}
163
+     */
164
+    _createNet(hooks)
165
+    {
166
+        this._createMesh(hooks);
167
+        this._createParticles();
168
+        this._broadcast(new GameEvent('netready', { entity: this }));
169
+    }
170
+
171
+    /**
172
+     * Create a custom mesh of a basketball net
173
+     * @param {BABYLON.Mesh[]} hooks
174
+     * @returns {void}
175
+     */
176
+    _createMesh(hooks)
177
+    {
178
+        const n = hooks.length;
179
+        const r = NUMBER_OF_RINGS - 1;
180
+        const h = HEIGHT / r;
181
+        const tmp = new BABYLON.Vector3();
182
+        const vertexData = new BABYLON.VertexData();
183
+        const positions = [], indices = [], uvs = [], normals = [];
184
+        const alpha = 1 - XZ_FALLOFF_SCALE;
185
+        const falloff = x => 1 - alpha * x; // x in [0,1]
186
+
187
+        // validate
188
+        if(n == 0 || NUMBER_OF_RINGS < 2)
189
+            throw new Error();
190
+
191
+        // setup the vertices
192
+        const vertexCount = n * NUMBER_OF_RINGS;
193
+        this._vertices = Array.from({ length: vertexCount }, () => new BABYLON.Vector3());
194
+        this._indices.length = 0;
195
+
196
+        // the origin of the mesh will be positioned at the geometrical center of the hooks
197
+        const center = this._findCenter(hooks);
198
+        this._physicsRoot.position.copyFrom(center);
199
+
200
+        // setup the top ring
201
+        // the local space of the mesh is centered at the origin of the XZ plane
202
+        for(let i = 0; i < n; i++) {
203
+            const vertex = this._vertices[i];
204
+            vertex.copyFrom(hooks[i].position).subtractInPlace(center);
205
+        }
206
+
207
+        // setup the other rings
208
+        for(let k = 1; k <= r; k++) {
209
+            for(let i = 0; i < n; i++) {
210
+                const f = falloff(k/r);
211
+                const s = tmp.set(f, 1, f);
212
+
213
+                // find the position of the next vertex
214
+                const prevVertex = this._vertices[i+(k-1)*n];
215
+                const nextVertex = this._vertices[i+k*n];
216
+                nextVertex.copyFrom(prevVertex)
217
+                .addInPlaceFromFloats(0, -h, 0)
218
+                .multiplyInPlace(s);
219
+
220
+                // for each vertex of the mesh, we create two triangles with
221
+                // nearby vertices. Here we store the indices of the vertices
222
+                const j = (i + n-1) % n;
223
+                this._indices.push(
224
+                    i+(k-1)*n, j+(k-1)*n, j+k*n,
225
+                    j+k*n, i+k*n, i+(k-1)*n
226
+                );
227
+            }
228
+        }
229
+
230
+        // setup the mesh
231
+        for(let j = 0; j < this._indices.length; j++) {
232
+            const index = this._indices[j];
233
+            const vertex = this._vertices[index];
234
+            const i = index % n, k = (index / n) | 0;
235
+
236
+            const u = 2*i < n ? 2*i/n : 2*(1-i/n);
237
+            const v = 0.5*(1-k/r);
238
+
239
+            positions.push(vertex.x, vertex.y, vertex.z);
240
+            uvs.push(u, v);
241
+            indices.push(j);
242
+        }
243
+
244
+        // setup vertex data
245
+        BABYLON.VertexData.ComputeNormals(positions, indices, normals);
246
+        vertexData.positions = positions;
247
+        vertexData.indices = indices;
248
+        vertexData.normals = normals;
249
+        vertexData.uvs = uvs;
250
+        vertexData.applyToMesh(this._mesh, true);
251
+
252
+        /*
253
+
254
+        We would like to see the inside and the outside of the basketball
255
+        net simultaneously. Although disabling backface culling seems like
256
+        a reasonable line of thought, the mesh is alpha blended. Disabling
257
+        backface culling in this case makes the front and the back faces
258
+        appear garbled (refer to the Transparent Rendering section of the
259
+        babylon.js documentation). We get around this issue by enabling
260
+        backface culling and by creating a mirror of the mesh. The mirror
261
+        has the same triangles as the original mesh, but we reverse their
262
+        orientation. The back faces of the mesh are the front faces of the
263
+        mirror. We can then see the inside and the outside of the net!
264
+
265
+        */
266
+        vertexData.indices = indices.reverse();
267
+        vertexData.applyToMesh(this._mirrorMesh, true);
268
+    }
269
+
270
+    /**
271
+     * Create the physics particles
272
+     * @returns {void}
273
+     */
274
+    _createParticles()
275
+    {
276
+        const physicsRoot = this._physicsRoot;
277
+        const n = this._vertices.length / NUMBER_OF_RINGS;
278
+        const r = NUMBER_OF_RINGS - 1;
279
+        const zero = BABYLON.Vector3.Zero();
280
+        const xz = this._xzParticleScaleVector;
281
+
282
+        // setup the particles and the impostors
283
+        this._particles = Array.from({ length: this._vertices.length }, (_, i) => {
284
+            const particle = BABYLON.MeshBuilder.CreateSphere('BasketballNetParticle_' + i, {
285
+                diameter: 1/32
286
+            });
287
+
288
+            const vertex = this._vertices[i];
289
+            particle.isVisible = false;
290
+            particle.position.copyFrom(vertex).multiplyInPlace(xz);
291
+
292
+            const TOTAL_MASS = 0.16;
293
+            particle.physicsImpostor = new BABYLON.PhysicsImpostor(particle, BABYLON.PhysicsImpostor.ParticleImpostor, {
294
+                mass: (i == 0) ? 0 : TOTAL_MASS / this._vertices.length, // actually, mass / (|v|-1)
295
+            });
296
+            particle.physicsImpostor.setLinearVelocity(zero);
297
+
298
+            return particle;
299
+        });
300
+
301
+        // lock particles of the top ring
302
+        for(let i = 1; i < n; i++) {
303
+            const p = this._particles[0];
304
+            const q = this._particles[i];
305
+
306
+            const jointPQ = new BABYLON.PhysicsJoint(BABYLON.PhysicsJoint.LockJoint, {});
307
+
308
+            p.physicsImpostor.addJoint(q.physicsImpostor, jointPQ);
309
+        }
310
+
311
+        // create cloth pattern
312
+        for(let k = 1; k <= r; k++) {
313
+            for(let i = 0; i < n; i++) {
314
+                const j = (i + n-1) % n;
315
+                const p = this._particles[i+k*n];
316
+                const q = this._particles[j+k*n];
317
+                const r = this._particles[i+(k-1)*n];
318
+
319
+                const distPQ = BABYLON.Vector3.Distance(p.position, q.position);
320
+                const distPR = BABYLON.Vector3.Distance(p.position, r.position);
321
+
322
+                const jointPQ = new BABYLON.PhysicsJoint(BABYLON.PhysicsJoint.DistanceJoint, { maxDistance: distPQ });
323
+                const jointPR = new BABYLON.PhysicsJoint(BABYLON.PhysicsJoint.DistanceJoint, { maxDistance: distPR });
324
+
325
+                p.physicsImpostor.addJoint(q.physicsImpostor, jointPQ);
326
+                p.physicsImpostor.addJoint(r.physicsImpostor, jointPR);
327
+            }
328
+        }
329
+
330
+        // set parent objects (at the end)
331
+        for(let i = 0; i < this._particles.length; i++)
332
+            this._particles[i].parent = physicsRoot;
333
+
334
+        physicsRoot.parent = this.physicsAnchor;
335
+    }
336
+
337
+    /**
338
+     * Put particles to sleep or wake them up
339
+     * @param {boolean} sleep
340
+     * @returns {void}
341
+     */
342
+    _sleepParticles(sleep)
343
+    {
344
+         for(let i = 0; i < this._particles.length; i++) {
345
+            const particle = this._particles[i];       
346
+            const impostor = particle.physicsImpostor;
347
+
348
+            if(sleep)
349
+                impostor.sleep();
350
+            else
351
+                impostor.wakeUp();
352
+         }
353
+    }
354
+
355
+    /**
356
+     * Reset the particles to their initial position and velocity
357
+     * @returns {void}
358
+     */
359
+    _resetParticles()
360
+    {
361
+        const zero = this._particles[0].physicsImpostor.getLinearVelocity();
362
+        const xz = this._xzParticleScaleVector;
363
+
364
+        for(let i = 0; i < this._particles.length; i++) {
365
+            const particle = this._particles[i];
366
+            const vertex = this._vertices[i];
367
+
368
+            particle.position.copyFrom(vertex).multiplyInPlace(xz);
369
+            particle.physicsImpostor.setLinearVelocity(zero);
370
+        }
371
+    }
372
+
373
+    /**
374
+     * Reduce the linear velocity of the particles
375
+     * @param {number} rate in velocity units per second
376
+     * @param {number} maxSpeed
377
+     * @returns {number} the average speed of the particles
378
+     */
379
+    _decelerateParticles(rate, maxSpeed)
380
+    {
381
+        const dt = this.ar.scene.getPhysicsEngine().getTimeStep();
382
+        const xz = this._xzParticleScaleVector;
383
+        let sumOfSpeeds = 0;
384
+
385
+        for(let i = 1; i < this._particles.length; i++) {
386
+            const vertex = this._vertices[i];
387
+            const particle = this._particles[i];
388
+            const impostor = particle.physicsImpostor;
389
+            const velocity = impostor.getLinearVelocity();
390
+            let speed = velocity.length();
391
+
392
+            if(vertex.y == 0) { // if hook
393
+                particle.position.copyFrom(vertex).multiplyInPlace(xz);
394
+                speed = 0;
395
+            }
396
+
397
+            speed -= rate * dt;
398
+            speed = Math.max(0, Math.min(speed, maxSpeed));
399
+            sumOfSpeeds += speed;
400
+
401
+            velocity.normalize().scaleInPlace(speed);
402
+            impostor.setLinearVelocity(velocity);
403
+        }
404
+
405
+        return sumOfSpeeds / this._particles.length;
406
+    }
407
+
408
+    /**
409
+     * Update the mesh by linking its vertices to the particles
410
+     * @returns {void}
411
+     */
412
+    _updateMesh()
413
+    {
414
+        const positions = this._mesh.getVerticesData(BABYLON.VertexBuffer.PositionKind);
415
+        const xz = 1 / XZ_PARTICLE_SCALE;
416
+
417
+        for(let i = 0, j = 0; i < this._indices.length; i++, j += 3) {
418
+            const particleIndex = this._indices[i];
419
+            const particle = this._particles[particleIndex];
420
+
421
+            positions[j+0] = particle.position.x * xz;
422
+            positions[j+1] = particle.position.y;
423
+            positions[j+2] = particle.position.z * xz;
424
+        }
425
+
426
+        this._mesh.updateVerticesData(BABYLON.VertexBuffer.PositionKind, positions);
427
+        this._mirrorMesh.updateVerticesData(BABYLON.VertexBuffer.PositionKind, positions);
428
+
429
+        // recompute the normals? the material is unlit
430
+    }
431
+
432
+    /**
433
+     * Find the geometrical center of a set of hooks
434
+     * @param {BABYLON.Mesh[]} hooks non-empty array
435
+     * @returns {BABYLON.Vector3}
436
+     */
437
+    _findCenter(hooks)
438
+    {
439
+        const center = new BABYLON.Vector3(0, 0, 0);
440
+
441
+        for(const hook of hooks) {
442
+            center.x += hook.position.x;
443
+            center.y += hook.position.y;
444
+            center.z += hook.position.z;
445
+        }
446
+
447
+        return center.scaleInPlace(1 / hooks.length);
448
+    }
449
+}
450
+

+ 175
- 0
demos/basketball/src/entities/score-text.js Vedi File

@@ -0,0 +1,175 @@
1
+/**
2
+ * -------------------------------------------
3
+ * Magic AR Basketball
4
+ * A demo game of the encantar.js WebAR engine
5
+ * -------------------------------------------
6
+ * @fileoverview Score Text
7
+ * @author Alexandre Martins <alemartf(at)gmail.com> (https://github.com/alemart/encantar-js)
8
+ */
9
+
10
+import { Entity } from './entity.js';
11
+import { GameEvent } from '../core/events.js';
12
+
13
+/** Movement length, in world units */
14
+const MOVEMENT_LENGTH = 0.5;
15
+
16
+/** Duration of the animation, in seconds */
17
+const ANIMATION_DURATION = 3.0;
18
+
19
+/** Duration of the fade-out effect, in seconds */
20
+const FADE_OUT_DURATION = 1.0;
21
+
22
+/**
23
+ * Score Text
24
+ */
25
+export class ScoreText extends Entity
26
+{
27
+     /**
28
+     * Constructor
29
+     * @param {BasketballDemo} demo
30
+     */
31
+    constructor(demo)
32
+    {
33
+        super(demo);
34
+        this._score = 0;
35
+        this._mesh = null;
36
+        this._initialPosition = new BABYLON.Vector3();
37
+        this._timer = 0;
38
+    }
39
+
40
+    /**
41
+     * Initialize the entity
42
+     * @returns {void}
43
+     */
44
+    init()
45
+    {
46
+        const mesh = BABYLON.MeshBuilder.CreatePlane('ScoreText', {
47
+            width: 0.5,
48
+            height: 0.5,
49
+        });
50
+
51
+        const url = this._demo.assetManager.url('atlas.png');
52
+        const material = new BABYLON.StandardMaterial('ScoreTextMaterial');
53
+
54
+        material.diffuseTexture = new BABYLON.Texture(url);
55
+        material.diffuseTexture.hasAlpha = true;
56
+        material.useAlphaFromDiffuseTexture = true;
57
+        material.frontUVs = BABYLON.Vector4.Zero();
58
+        material.alpha = 0;
59
+        material.unlit = true;
60
+
61
+        const ar = this.ar;
62
+        mesh.parent = ar.root;
63
+        mesh.material = material;
64
+        mesh.billboardMode = BABYLON.Mesh.BILLBOARDMODE_ALL;
65
+
66
+        this._mesh = mesh;
67
+    }
68
+
69
+    /**
70
+     * Update the entity
71
+     * @returns {void}
72
+     */
73
+    update()
74
+    {
75
+        const mesh = this._mesh;
76
+
77
+        if(this._timer <= 0) {
78
+            mesh.material.alpha = 0;
79
+            return;
80
+        }
81
+
82
+        const ar = this.ar;
83
+        const dt = ar.session.time.delta;
84
+        this._timer -= dt;
85
+
86
+        const speed = MOVEMENT_LENGTH / ANIMATION_DURATION;
87
+        mesh.position.y += speed * dt;
88
+
89
+        if(this._timer < FADE_OUT_DURATION) {
90
+            const rate = 1.0 / FADE_OUT_DURATION;
91
+            mesh.material.alpha -= rate * dt;
92
+        }
93
+    }
94
+
95
+    /**
96
+     * Set the score to be displayed
97
+     * @param {number} score
98
+     * @returns {this}
99
+     */
100
+    setScore(score)
101
+    {
102
+        const uvs = new Array(8).fill(0);
103
+        const d = 1 / 1024;
104
+
105
+        if(score == 2) {
106
+            uvs[0] = 7/8;
107
+            uvs[1] = 6/8;
108
+            uvs[2] = 6/8 + d;
109
+            uvs[3] = 6/8;
110
+            uvs[4] = 6/8 + d;
111
+            uvs[5] = 7/8 - d;
112
+            uvs[6] = 7/8;
113
+            uvs[7] = 7/8 - d;
114
+        }
115
+        else if(score == 3) {
116
+            uvs[0] = 8/8 - d;
117
+            uvs[1] = 6/8;
118
+            uvs[2] = 7/8;
119
+            uvs[3] = 6/8;
120
+            uvs[4] = 7/8;
121
+            uvs[5] = 7/8 - d;
122
+            uvs[6] = 8/8 - d;
123
+            uvs[7] = 7/8 - d;
124
+        }
125
+
126
+        this._mesh.setVerticesData(BABYLON.VertexBuffer.UVKind, uvs);
127
+        this._score = score;
128
+        return this;
129
+    }
130
+
131
+    /**
132
+     * Find the geometrical center, in world space, of a set of hooks
133
+     * @param {BABYLON.Mesh[]} hooks non-empty array
134
+     * @returns {BABYLON.Vector3}
135
+     */
136
+    _findCenter(hooks)
137
+    {
138
+        const center = new BABYLON.Vector3(0, 0, 0);
139
+
140
+        for(const hook of hooks) {
141
+            center.x += hook.absolutePosition.x;
142
+            center.y += hook.absolutePosition.y;
143
+            center.z += hook.absolutePosition.z;
144
+        }
145
+
146
+        return center.scaleInPlace(1 / hooks.length);
147
+    }
148
+
149
+    /**
150
+     * Handle an event
151
+     * @param {GameEvent} event
152
+     * @returns {void}
153
+     */
154
+    handleEvent(event)
155
+    {
156
+        if(event.type == 'hooksready') {
157
+            const hooks = event.detail.hooks;
158
+            const center = this._findCenter(hooks);
159
+            const parent = this._mesh.parent;
160
+            const offset = center.subtract(parent.absolutePosition);
161
+
162
+            this._initialPosition.copyFrom(offset);
163
+            this._mesh.position.copyFrom(this._initialPosition);
164
+        }
165
+        else if(event.type == 'scored') {
166
+            const score = event.detail.score;
167
+
168
+            if(score == this._score) {
169
+                this._mesh.material.alpha = 1;
170
+                this._mesh.position.copyFrom(this._initialPosition);
171
+                this._timer = ANIMATION_DURATION;
172
+            }
173
+        }
174
+    }
175
+}

+ 184
- 0
demos/basketball/src/entities/scoreboard.js Vedi File

@@ -0,0 +1,184 @@
1
+/**
2
+ * -------------------------------------------
3
+ * Magic AR Basketball
4
+ * A demo game of the encantar.js WebAR engine
5
+ * -------------------------------------------
6
+ * @fileoverview The Scoreboard
7
+ * @author Alexandre Martins <alemartf(at)gmail.com> (https://github.com/alemart/encantar-js)
8
+ */
9
+
10
+import { Entity, PhysicsEntity } from './entity.js';
11
+import { GameEvent } from '../core/events.js';
12
+
13
+/**
14
+ * A digit of a Scoreboard
15
+ */
16
+class ScoreboardDigit extends Entity
17
+{
18
+     /**
19
+     * Constructor
20
+     * @param {BasketballDemo} demo
21
+     */
22
+    constructor(demo)
23
+    {
24
+        super(demo);
25
+        this._mesh = null;
26
+    }
27
+
28
+    /**
29
+     * Initialize the entity
30
+     * @returns {void}
31
+     */
32
+    init()
33
+    {
34
+        this._mesh = this._createMesh();
35
+        this.setDigit(Number.NaN);
36
+    }
37
+
38
+    /**
39
+     * The mesh
40
+     * @returns {BABYLON.Mesh}
41
+     */
42
+    get mesh()
43
+    {
44
+        return this._mesh;
45
+    }
46
+
47
+    /**
48
+     * Set the digit to be displayed
49
+     * @param {number} digit 0-9 or NaN
50
+     * @returns {void}
51
+     */
52
+    setDigit(digit)
53
+    {
54
+        const uvs = new Array(8);
55
+
56
+        uvs[0] = 1/8;
57
+        uvs[1] = 6/8;
58
+        uvs[2] = 0/8;
59
+        uvs[3] = 6/8;
60
+        uvs[4] = 0/8;
61
+        uvs[5] = 8/8;
62
+        uvs[6] = 1/8;
63
+        uvs[7] = 8/8;
64
+
65
+        if(digit == 0) {
66
+            for(let i = 0; i < 8; i += 2) {
67
+                uvs[i+0] += 4/8;
68
+                uvs[i+1] -= 2/8;
69
+            }
70
+        }
71
+        else if(digit >= 1 && digit <= 5) {
72
+            for(let i = 0, d = digit; i < 8; i += 2) {
73
+                uvs[i] += d/8;
74
+            }
75
+        }
76
+        else if(digit >= 6 && digit <= 9) {
77
+            for(let i = 0, d = digit-6; i < 8; i += 2) {
78
+                uvs[i+0] += d/8;
79
+                uvs[i+1] -= 2/8;
80
+            }
81
+        }
82
+
83
+        this._mesh.setVerticesData(BABYLON.VertexBuffer.UVKind, uvs);
84
+    }
85
+
86
+    /**
87
+     * Create the mesh
88
+     * @returns {BABYLON.Mesh}
89
+     */
90
+    _createMesh()
91
+    {
92
+        const mesh = BABYLON.MeshBuilder.CreatePlane('ScoreboardDigit', {
93
+            width: 0.5,
94
+            height: 1.0,
95
+        });
96
+
97
+        const url = this._demo.assetManager.url('atlas.png');
98
+        const material = new BABYLON.StandardMaterial('ScoreboardDigitMaterial');
99
+
100
+        material.diffuseTexture = new BABYLON.Texture(url);
101
+        material.diffuseTexture.hasAlpha = true;
102
+        material.useAlphaFromDiffuseTexture = true;
103
+        material.unlit = true;
104
+        //material.wireframe = true;
105
+
106
+        mesh.material = material;
107
+        mesh.rotate(BABYLON.Axis.Y, Math.PI);
108
+        mesh.parent = this.ar.root;
109
+
110
+        return mesh;
111
+    }
112
+}
113
+
114
+/**
115
+ * Scoreboard
116
+ */
117
+export class Scoreboard extends PhysicsEntity
118
+{
119
+     /**
120
+     * Constructor
121
+     * @param {BasketballDemo} demo
122
+     */
123
+    constructor(demo)
124
+    {
125
+        super(demo);
126
+        this._mesh = null;
127
+        this._units = null;
128
+        this._tens = null;
129
+    }
130
+
131
+    /**
132
+     * Initialize the entity
133
+     * @returns {Promise<void>}
134
+     */
135
+    async init()
136
+    {
137
+        const x = 0.245, y = 0, z = 0.501;
138
+
139
+        this._mesh = BABYLON.MeshBuilder.CreateBox('Scoreboard', {
140
+            width: 1.3125,
141
+            height: 1.0,
142
+            depth: 1.0,
143
+        });
144
+
145
+        this._mesh.position = new BABYLON.Vector3(2.5, 0.5, 0);
146
+        this._mesh.rotate(BABYLON.Axis.Y, -Math.PI / 6);
147
+
148
+        this._mesh.material = new BABYLON.StandardMaterial('ScoreboardMaterial');
149
+        this._mesh.material.diffuseColor = BABYLON.Color3.FromHexString('#110d7c');
150
+
151
+        this._units = await this._demo.spawn(ScoreboardDigit);
152
+        this._units.mesh.position = new BABYLON.Vector3(x, y, z);
153
+        this._units.mesh.parent = this._mesh;
154
+
155
+        this._tens = await this._demo.spawn(ScoreboardDigit);
156
+        this._tens.mesh.position = new BABYLON.Vector3(-x, y, z);
157
+        this._tens.mesh.parent = this._mesh;
158
+
159
+        this._mesh.physicsImpostor = new BABYLON.PhysicsImpostor(this._mesh, BABYLON.PhysicsImpostor.BoxImpostor, {
160
+            mass: 0,
161
+        });
162
+
163
+        this._mesh.parent = this.physicsAnchor;
164
+
165
+        this._broadcast(new GameEvent('colliderready', { impostor: this._mesh.physicsImpostor }));
166
+    }
167
+
168
+    /**
169
+     * Handle an event
170
+     * @param {GameEvent} event
171
+     * @returns {void}
172
+     */
173
+    handleEvent(event)
174
+    {
175
+        if(event.type == 'newscore') {
176
+            const score = event.detail.score;
177
+            const units = score % 10;
178
+            const tens = (score / 10) | 0;
179
+
180
+            this._units.setDigit(units);
181
+            this._tens.setDigit(tens || Number.NaN);
182
+        }
183
+    }
184
+}

+ 28
- 0
demos/basketball/src/main.js Vedi File

@@ -0,0 +1,28 @@
1
+/**
2
+ * -------------------------------------------
3
+ * Magic AR Basketball
4
+ * A demo game of the encantar.js WebAR engine
5
+ * -------------------------------------------
6
+ * @fileoverview Main function
7
+ * @author Alexandre Martins <alemartf(at)gmail.com> (https://github.com/alemart/encantar-js)
8
+ */
9
+
10
+import { BasketballGame } from './core/game.js';
11
+
12
+/**
13
+ * Start the game
14
+ * @returns {void}
15
+ */
16
+function main()
17
+{
18
+    const game = new BasketballGame();
19
+
20
+    if(typeof encantar === 'undefined')
21
+        throw new Error(`Can't find the babylon.js plugin for encantar.js`);
22
+
23
+    encantar(game).catch(error => {
24
+        alert(error.message);
25
+    });
26
+}
27
+
28
+document.addEventListener('DOMContentLoaded', main);

+ 31
- 0
demos/basketball/video.html Vedi File

@@ -0,0 +1,31 @@
1
+<!doctype html>
2
+<html>
3
+    <head>
4
+        <meta charset="utf-8">
5
+        <meta name="viewport" content="width=device-width,initial-scale=1">
6
+        <title>Magic AR Basketball Game - encantar.js WebAR demo</title>
7
+        <link href="../assets/demo.css" rel="stylesheet">
8
+        <script src="../../dist/encantar.min.js"></script>
9
+        <script src="https://cdn.jsdelivr.net/npm/babylonjs@7.38.0/babylon.min.js"></script>
10
+        <script src="https://cdn.jsdelivr.net/npm/babylonjs-loaders@7.38.0/babylonjs.loaders.min.js"></script>
11
+        <script src="https://cdn.jsdelivr.net/npm/babylonjs-gui@7.38.0/babylon.gui.min.js"></script>
12
+        <script src="https://cdn.jsdelivr.net/npm/cannon@0.6.2/build/cannon.min.js"></script>
13
+        <script src="../../plugins/babylon-with-encantar.js"></script>
14
+        <script src="../../plugins/extras/asset-manager.js"></script>
15
+        <script type="module" src="./src/main.js"></script>
16
+    </head>
17
+    <body>
18
+        <div id="ar-viewport">
19
+            <div id="ar-hud" hidden>
20
+                <a id="info" href="NOTICE.html" draggable="false"></a>
21
+                <a id="like" href="../assets/promo.html" draggable="false"></a>
22
+                <img id="scan" src="../assets/scan.png" draggable="false">
23
+            </div>
24
+        </div>
25
+        <img id="mage" src="../assets/mage.webp" hidden>
26
+        <video id="my-video" hidden muted loop playsinline autoplay>
27
+            <source src="../assets/my-video.webm" type="video/webm" />
28
+            <source src="../assets/my-video.mp4" type="video/mp4" />
29
+        </video>
30
+    </body>
31
+</html>

Loading…
Annulla
Salva