65 | Basic with events
66 | Example of listening to and logging NAF events
67 |
68 |
69 | Ownership Transfer
70 | Demonstrates transfering ownership of entities
71 |
72 |
73 | Adapter Test
74 | Use this page to test new network adapters
75 |
76 |
78 |
79 |
80 |
81 |
Want to help make better examples? That would be awesome! Contact us on Slack.
82 |
83 |
84 |
85 |
88 |
89 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/research/naf/examples/js/forward.component.js:
--------------------------------------------------------------------------------
1 | /* global AFRAME, THREE */
2 | AFRAME.registerComponent('forward', {
3 | schema: {
4 | speed: {default: 0.1},
5 | },
6 |
7 | init: function() {
8 | var worldDirection = new THREE.Vector3();
9 |
10 | this.el.object3D.getWorldDirection(worldDirection);
11 | worldDirection.multiplyScalar(-1);
12 |
13 | this.worldDirection = worldDirection;
14 | console.error(this.worldDirection);
15 | },
16 |
17 | tick: function() {
18 | var el = this.el;
19 |
20 | var currentPosition = el.getAttribute('position');
21 | var newPosition = this.worldDirection
22 | .clone()
23 | .multiplyScalar(this.data.speed)
24 | .add(currentPosition);
25 | el.setAttribute('position', newPosition);
26 | }
27 | });
--------------------------------------------------------------------------------
/research/naf/examples/js/gun.component.js:
--------------------------------------------------------------------------------
1 | /* global AFRAME, THREE */
2 | AFRAME.registerComponent('gun', {
3 | schema: {
4 | bulletTemplate: {default: '#bullet-template'},
5 | triggerKeyCode: {default: 32} // spacebar
6 | },
7 |
8 | init: function() {
9 | var that = this;
10 | document.body.onkeyup = function(e){
11 | if(e.keyCode == that.data.triggerKeyCode){
12 | that.shoot();
13 | }
14 | }
15 | },
16 |
17 | shoot: function() {
18 | this.createBullet();
19 | },
20 |
21 | createBullet: function() {
22 | var el = document.createElement('a-entity');
23 | el.setAttribute('networked', 'template:' + this.data.bulletTemplate);
24 | el.setAttribute('remove-in-seconds', 3);
25 | el.setAttribute('forward', 'speed:0.3');
26 |
27 | var tip = document.querySelector('#player');
28 | el.setAttribute('position', this.getInitialBulletPosition(tip));
29 | el.setAttribute('rotation', this.getInitialBulletRotation(tip));
30 |
31 | this.el.sceneEl.appendChild(el);
32 | },
33 |
34 | getInitialBulletPosition: function(spawnerEl) {
35 | var worldPos = new THREE.Vector3();
36 | worldPos.setFromMatrixPosition(spawnerEl.object3D.matrixWorld);
37 | return worldPos;
38 | },
39 |
40 | getInitialBulletRotation: function(spawnerEl) {
41 | var worldDirection = new THREE.Vector3();
42 |
43 | spawnerEl.object3D.getWorldDirection(worldDirection);
44 | worldDirection.multiplyScalar(-1);
45 | this.vec3RadToDeg(worldDirection);
46 |
47 | return worldDirection;
48 | },
49 |
50 | vec3RadToDeg: function(rad) {
51 | rad.set(rad.y * 90, -90 + (-THREE.Math.radToDeg(Math.atan2(rad.z, rad.x))), 0);
52 | }
53 | });
54 |
--------------------------------------------------------------------------------
/research/naf/examples/js/move-in-circle.component.js:
--------------------------------------------------------------------------------
1 | /* global AFRAME */
2 | AFRAME.registerComponent('move-in-circle', {
3 | schema: {
4 | speed: {default: 0.05}
5 | },
6 |
7 | init: function() {
8 | this.angle = 0;
9 | this.center = this.el.getAttribute('position');
10 | },
11 |
12 | tick: function() {
13 | this.angle = this.angle + this.data.speed;
14 |
15 | var circlePoint = this.getPointOnCircle(1, this.angle);
16 | var worldPoint = {x: circlePoint.x + this.center.x, y: this.center.y, z: circlePoint.y + this.center.z};
17 | this.el.setAttribute('position', worldPoint);
18 | // console.log(worldPoint);
19 | },
20 |
21 | getPointOnCircle: function (radius, angleRad) {
22 | var x = Math.cos(angleRad) * radius;
23 | var y = Math.sin(angleRad) * radius;
24 | return {x: x, y: y};
25 | }
26 | });
--------------------------------------------------------------------------------
/research/naf/examples/js/randomize-show-child.component.js:
--------------------------------------------------------------------------------
1 | /* global AFRAME */
2 | AFRAME.registerComponent('randomize-show-child', {
3 | schema: {},
4 |
5 | init: function() {
6 | var num = this.el.children.length
7 | var r = Math.floor(Math.random() * num);
8 | this.el.setAttribute('show-child', r);
9 | }
10 | });
--------------------------------------------------------------------------------
/research/naf/examples/js/remove-in-seconds.component.js:
--------------------------------------------------------------------------------
1 | /* global AFRAME */
2 | AFRAME.registerComponent('remove-in-seconds', {
3 | schema: {
4 | default: 1
5 | },
6 |
7 | init: function() {
8 | setTimeout(this.destroy.bind(this), this.data * 1000);
9 | },
10 |
11 | destroy: function() {
12 | var el = this.el;
13 | el.parentNode.removeChild(el);
14 | }
15 | });
--------------------------------------------------------------------------------
/research/naf/examples/js/show-child.component.js:
--------------------------------------------------------------------------------
1 | /* global AFRAME */
2 | AFRAME.registerComponent('show-child', {
3 | schema: {
4 | type: 'int',
5 | default: 0,
6 | },
7 |
8 | update: function() {
9 | this.show(this.data);
10 | },
11 |
12 | show: function(index) {
13 | if (index < this.el.children.length) {
14 | this.hideAll();
15 | this.el.children[index].setAttribute('visible', true);
16 | } else {
17 | console.error('show-child@show: invalid index: ', index);
18 | }
19 | },
20 |
21 | hideAll: function() {
22 | var el = this.el;
23 | for (var i = 0; i < el.children.length; i++) {
24 | el.children[i].setAttribute('visible', false);
25 | }
26 | }
27 | });
--------------------------------------------------------------------------------
/research/naf/examples/js/spawn-grid.component.js:
--------------------------------------------------------------------------------
1 | /* global AFRAME */
2 | /**
3 | * Creates a grid of entities based on the given template
4 | * Place at the center of the grid you wish to create
5 | * Requires aframe-template-component
6 | */
7 | AFRAME.registerComponent('spawn-grid', {
8 | schema: {
9 | clone: {type: 'string'},
10 | rows: {default: 2},
11 | rowSeparation: {default: 2},
12 | columns: {default: 2},
13 | columnSeparation: {default: 2}
14 | },
15 |
16 | init: function() {
17 | this.clone = document.querySelector(this.data.clone);
18 | var y = this.el.getAttribute('position').y;
19 | for (var i = 0; i < this.data.rows; i++) {
20 | var z = i * this.data.rowSeparation - (this.data.rowSeparation * this.data.rows / 2) + this.data.rowSeparation / 2;
21 | for (var j = 0; j < this.data.columns; j++) {
22 | var x = j * this.data.columnSeparation - (this.data.columnSeparation * this.data.columns / 2) + this.data.columnSeparation / 2;
23 | this.spawnEntity(i, j, x, y, z);
24 | }
25 | }
26 | },
27 |
28 | spawnEntity: function(row, col, x, y, z) {
29 | var cloned = this.clone.cloneNode(false);
30 | this.el.appendChild(cloned);
31 |
32 | var id = cloned.getAttribute('id');
33 | var newId = id + '-' + row + '-'+ col;
34 | cloned.setAttribute('id', newId);
35 |
36 | var position = x + ' ' + y + ' ' + z;
37 | cloned.setAttribute('position', position);
38 |
39 | cloned.setAttribute('visible', true);
40 | }
41 | });
--------------------------------------------------------------------------------
/research/naf/examples/js/spawn-in-circle.component.js:
--------------------------------------------------------------------------------
1 | /* global AFRAME, THREE */
2 | AFRAME.registerComponent('spawn-in-circle', {
3 | schema: {
4 | radius: {type: 'number', default: 1}
5 | },
6 |
7 | init: function() {
8 | var el = this.el;
9 | var center = el.getAttribute('position');
10 |
11 | var angleRad = this.getRandomAngleInRadians();
12 | var circlePoint = this.randomPointOnCircle(this.data.radius, angleRad);
13 | var worldPoint = {x: circlePoint.x + center.x, y: center.y, z: circlePoint.y + center.z};
14 | el.setAttribute('position', worldPoint);
15 | // console.log('world point', worldPoint);
16 |
17 | var angleDeg = angleRad * 180 / Math.PI;
18 | var angleToCenter = -1 * angleDeg + 90;
19 | angleRad = THREE.Math.degToRad(angleToCenter);
20 | el.object3D.rotation.set(0, angleRad, 0);
21 | // console.log('angle deg', angleDeg);
22 | },
23 |
24 | getRandomAngleInRadians: function() {
25 | return Math.random()*Math.PI*2;
26 | },
27 |
28 | randomPointOnCircle: function (radius, angleRad) {
29 | var x = Math.cos(angleRad)*radius;
30 | var y = Math.sin(angleRad)*radius;
31 | return {x: x, y: y};
32 | }
33 | });
--------------------------------------------------------------------------------
/research/naf/examples/js/spawner.component.js:
--------------------------------------------------------------------------------
1 | /* global AFRAME */
2 | AFRAME.registerComponent('spawner', {
3 | schema: {
4 | template: { default: '' },
5 | keyCode: { default: 32 }
6 | },
7 |
8 | init: function() {
9 | this.onKeyUp = this.onKeyUp.bind(this);
10 | document.addEventListener("keyup", this.onKeyUp);
11 | },
12 |
13 | onKeyUp: function(e) {
14 | if (this.data.keyCode === e.keyCode) {
15 | var el = document.createElement('a-entity');
16 | el.setAttribute('networked', 'template:' + this.data.template);
17 | el.setAttribute('position', this.el.getAttribute('position'));
18 | var scene = this.el.sceneEl;
19 | scene.appendChild(el);
20 | }
21 | }
22 | });
--------------------------------------------------------------------------------
/research/naf/examples/js/tests/show-child.component.test.js:
--------------------------------------------------------------------------------
1 | /* global assert, setup, suite, test */
2 | require('aframe');
3 | var helpers = require('./helpers');
4 | var naf = require('../../src/NafIndex');
5 |
6 | require('../../src/components/show-child.component');
7 |
8 | suite('show-child', function() {
9 | var scene;
10 | var entity;
11 | var comp;
12 |
13 | function initScene(done) {
14 | var opts = {};
15 | opts.entity = '';
16 | scene = helpers.sceneFactory(opts);
17 | naf.utils.whenEntityLoaded(scene, done);
18 | }
19 |
20 | setup(function(done) {
21 | initScene(function() {
22 | entity = document.querySelector('#test-entity');
23 | comp = entity.components['show-child'];
24 | done();
25 | });
26 | });
27 |
28 | suite('Setup', function() {
29 |
30 | test('creates entity', function() {
31 | assert.isOk(entity);
32 | });
33 |
34 | test('creates component', function() {
35 | assert.isOk(comp);
36 | });
37 | });
38 |
39 | suite('Schema', function() {
40 |
41 | test('Returns correct index', function() {
42 | var result = entity.getAttribute('show-child');
43 |
44 | assert.strictEqual(result, 2);
45 | });
46 | });
47 |
48 | suite('hideAll', function() {
49 |
50 | test('Hides correct children', function() {
51 | var child0 = document.querySelector('#zero');
52 | var child1 = document.querySelector('#one');
53 | var child2 = document.querySelector('#two');
54 |
55 | comp.hideAll();
56 |
57 | var result0 = child0.getAttribute('visible');
58 | var result1 = child1.getAttribute('visible');
59 | var result2 = child2.getAttribute('visible');
60 |
61 | assert.isFalse(result0);
62 | assert.isFalse(result1);
63 | assert.isFalse(result2);
64 | });
65 |
66 | test('Show correct child', function() {
67 | var child0 = document.querySelector('#zero');
68 | var child1 = document.querySelector('#one');
69 | var child2 = document.querySelector('#two');
70 |
71 | comp.hideAll();
72 | comp.show(1);
73 |
74 | var result0 = child0.getAttribute('visible');
75 | var result1 = child1.getAttribute('visible');
76 | var result2 = child2.getAttribute('visible');
77 |
78 | assert.isFalse(result0);
79 | assert.isTrue(result1);
80 | assert.isFalse(result2);
81 | });
82 | });
83 |
84 | suite('show', function() {
85 |
86 | test('Handles index too large', function() {
87 | var child0 = document.querySelector('#zero');
88 | var child1 = document.querySelector('#one');
89 | var child2 = document.querySelector('#two');
90 |
91 | comp.hideAll();
92 | comp.show(4);
93 |
94 | var result0 = child0.getAttribute('visible');
95 | var result1 = child1.getAttribute('visible');
96 | var result2 = child2.getAttribute('visible');
97 |
98 | assert.isFalse(result0);
99 | assert.isFalse(result1);
100 | assert.isFalse(result2);
101 | });
102 | });
103 | });
--------------------------------------------------------------------------------
/research/naf/examples/js/toggle-ownership.component.js:
--------------------------------------------------------------------------------
1 | /* global AFRAME, NAF, THREE */
2 | /**
3 | * Rotate the entity every frame if you are the owner.
4 | * When you press enter take ownership of the entity,
5 | * spin it in the opposite direction and change its color.
6 | */
7 | AFRAME.registerComponent('toggle-ownership', {
8 | schema: {
9 | speed: { default: 0.01 },
10 | direction: { default: 1 }
11 | },
12 |
13 | init() {
14 | var that = this;
15 | this.onKeyUp = this.onKeyUp.bind(this);
16 | document.addEventListener('keyup', this.onKeyUp);
17 |
18 | NAF.utils.getNetworkedEntity(this.el).then((el) => {
19 | if (NAF.utils.isMine(el)) {
20 | that.updateColor();
21 | } else {
22 | that.updateOpacity(0.5);
23 | }
24 |
25 | // Opacity is not a networked attribute, but change it based on ownership events
26 | let timeout;
27 |
28 | el.addEventListener('ownership-gained', e => {
29 | that.updateOpacity(1);
30 | });
31 |
32 | el.addEventListener('ownership-lost', e => {
33 | that.updateOpacity(0.5);
34 | });
35 |
36 | el.addEventListener('ownership-changed', e => {
37 | clearTimeout(timeout);
38 | console.log(e.detail)
39 | if (e.detail.newOwner == NAF.clientId) {
40 | //same as listening to 'ownership-gained'
41 | } else if (e.detail.oldOwner == NAF.clientId) {
42 | //same as listening to 'ownership-lost'
43 | } else {
44 | that.updateOpacity(0.8);
45 | timeout = setTimeout(() => {
46 | that.updateOpacity(0.5);
47 | }, 200)
48 | }
49 | });
50 | });
51 | },
52 |
53 | onKeyUp(e) {
54 | if (e.keyCode !== 13 /* enter */) {
55 | return;
56 | }
57 |
58 | if(NAF.utils.takeOwnership(this.el)) {
59 | this.el.setAttribute('toggle-ownership', { direction: this.data.direction * -1 });
60 | this.updateColor();
61 | }
62 | },
63 |
64 | updateColor() {
65 | const headColor = document.querySelector('#player .head').getAttribute('material').color;
66 | this.el.setAttribute('material', 'color', headColor);
67 | },
68 |
69 | updateOpacity(opacity) {
70 | this.el.setAttribute('material', 'opacity', opacity);
71 | },
72 |
73 | tick() {
74 | // Only update the component if you are the owner.
75 | if (!NAF.utils.isMine(this.el)) {
76 | return;
77 | }
78 |
79 | this.el.object3D.rotateY(this.data.speed * this.data.direction);
80 |
81 | const rotation = this.el.object3D.rotation;
82 | this.el.setAttribute('rotation', {
83 | x: THREE.Math.radToDeg(rotation.x),
84 | y: THREE.Math.radToDeg(rotation.y),
85 | z: THREE.Math.radToDeg(rotation.z),
86 | });
87 | }
88 | });
--------------------------------------------------------------------------------
/research/naf/examples/shooter-2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Iframe Dev Example — Networked-Aframe
6 |
7 |
8 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/research/naf/examples/shooter-ar.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Shooter AR Example — Networked-Aframe
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
25 |
26 |
27 |
28 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
43 |
46 |
51 |
56 |
57 |
62 |
67 |
68 |
69 |
70 |
75 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
104 |
105 |
107 |
108 |
122 |
123 |
--------------------------------------------------------------------------------
/research/naf/examples/shooter.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Shooter Example — Networked-Aframe
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
38 |
41 |
46 |
51 |
52 |
57 |
62 |
63 |
64 |
65 |
70 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
98 |
102 |
103 |
104 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
120 |
121 |
123 |
124 |
133 |
134 |
149 |
150 |
151 |
--------------------------------------------------------------------------------
/research/naf/examples/webrtc.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dev Example — Networked-Aframe
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
36 |
39 |
44 |
49 |
50 |
55 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
75 |
79 |
80 |
81 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
97 |
98 |
100 |
101 |
110 |
111 |
131 |
132 |
133 |
--------------------------------------------------------------------------------
/research/naf/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "naf-tutorial",
3 | "version": "1.0.0",
4 | "description": "My first multi-user virtual reality",
5 | "scripts": {
6 | "start": "node ./server/easyrtc-server.js"
7 | },
8 | "author": "YOUR_NAME",
9 | "dependencies": {
10 | "networked-aframe": "^0.8.0"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/research/naf/server/easyrtc-server.js:
--------------------------------------------------------------------------------
1 | // Load required modules
2 | const http = require("http"); // http server core module
3 | const path = require("path");
4 | const express = require("express"); // web framework external module
5 | const socketIo = require("socket.io"); // web socket external module
6 | const easyrtc = require("open-easyrtc"); // EasyRTC external module
7 |
8 | // Set process name
9 | process.title = "networked-aframe-server";
10 |
11 | // Get port or default to 8080
12 | const port = process.env.PORT || 8080;
13 |
14 | // Setup and configure Express http server.
15 | const app = express();
16 | app.use(express.static(path.resolve(__dirname, "..", "examples")));
17 |
18 | // Serve the example and build the bundle in development.
19 | if (process.env.NODE_ENV === "development") {
20 | const webpackMiddleware = require("webpack-dev-middleware");
21 | const webpack = require("webpack");
22 | const config = require("../webpack.config");
23 |
24 | app.use(
25 | webpackMiddleware(webpack(config), {
26 | publicPath: "/"
27 | })
28 | );
29 | }
30 |
31 | // Start Express http server
32 | const webServer = http.createServer(app);
33 |
34 | // Start Socket.io so it attaches itself to Express server
35 | const socketServer = socketIo.listen(webServer, {"log level": 1});
36 | const myIceServers = [
37 | {"urls":"stun:stun1.l.google.com:19302"},
38 | {"urls":"stun:stun2.l.google.com:19302"},
39 | // {
40 | // "urls":"turn:[ADDRESS]:[PORT]",
41 | // "username":"[USERNAME]",
42 | // "credential":"[CREDENTIAL]"
43 | // },
44 | // {
45 | // "urls":"turn:[ADDRESS]:[PORT][?transport=tcp]",
46 | // "username":"[USERNAME]",
47 | // "credential":"[CREDENTIAL]"
48 | // }
49 | ];
50 | easyrtc.setOption("appIceServers", myIceServers);
51 | easyrtc.setOption("logLevel", "debug");
52 | easyrtc.setOption("demosEnable", false);
53 |
54 | // Overriding the default easyrtcAuth listener, only so we can directly access its callback
55 | easyrtc.events.on("easyrtcAuth", (socket, easyrtcid, msg, socketCallback, callback) => {
56 | easyrtc.events.defaultListeners.easyrtcAuth(socket, easyrtcid, msg, socketCallback, (err, connectionObj) => {
57 | if (err || !msg.msgData || !msg.msgData.credential || !connectionObj) {
58 | callback(err, connectionObj);
59 | return;
60 | }
61 |
62 | connectionObj.setField("credential", msg.msgData.credential, {"isShared":false});
63 |
64 | console.log("["+easyrtcid+"] Credential saved!", connectionObj.getFieldValueSync("credential"));
65 |
66 | callback(err, connectionObj);
67 | });
68 | });
69 |
70 | // To test, lets print the credential to the console for every room join!
71 | easyrtc.events.on("roomJoin", (connectionObj, roomName, roomParameter, callback) => {
72 | console.log("["+connectionObj.getEasyrtcid()+"] Credential retrieved!", connectionObj.getFieldValueSync("credential"));
73 | easyrtc.events.defaultListeners.roomJoin(connectionObj, roomName, roomParameter, callback);
74 | });
75 |
76 | // Start EasyRTC server
77 | easyrtc.listen(app, socketServer, null, (err, rtcRef) => {
78 | console.log("Initiated");
79 |
80 | rtcRef.events.on("roomCreate", (appObj, creatorConnectionObj, roomName, roomOptions, callback) => {
81 | console.log("roomCreate fired! Trying to create: " + roomName);
82 |
83 | appObj.events.defaultListeners.roomCreate(appObj, creatorConnectionObj, roomName, roomOptions, callback);
84 | });
85 | });
86 |
87 | // Listen on port
88 | webServer.listen(port, () => {
89 | console.log("listening on http://localhost:" + port);
90 | });
91 |
--------------------------------------------------------------------------------
/research/naf/server/socketio-server.js:
--------------------------------------------------------------------------------
1 | // Load required modules
2 | const http = require("http"); // http server core module
3 | const path = require("path");
4 | const express = require("express"); // web framework external module
5 |
6 | // Set process name
7 | process.title = "networked-aframe-server";
8 |
9 | // Get port or default to 8080
10 | const port = process.env.PORT || 8080;
11 |
12 | // Setup and configure Express http server.
13 | const app = express();
14 | app.use(express.static(path.resolve(__dirname, "..", "examples")));
15 |
16 | // Serve the example and build the bundle in development.
17 | if (process.env.NODE_ENV === "development") {
18 | const webpackMiddleware = require("webpack-dev-middleware");
19 | const webpack = require("webpack");
20 | const config = require("../webpack.config");
21 |
22 | app.use(
23 | webpackMiddleware(webpack(config), {
24 | publicPath: "/"
25 | })
26 | );
27 | }
28 |
29 | // Start Express http server
30 | const webServer = http.createServer(app);
31 | const io = require("socket.io")(webServer);
32 |
33 | const rooms = {};
34 |
35 | io.on("connection", socket => {
36 | console.log("user connected", socket.id);
37 |
38 | let curRoom = null;
39 |
40 | socket.on("joinRoom", data => {
41 | const { room } = data;
42 |
43 | if (!rooms[room]) {
44 | rooms[room] = {
45 | name: room,
46 | occupants: {},
47 | };
48 | }
49 |
50 | const joinedTime = Date.now();
51 | rooms[room].occupants[socket.id] = joinedTime;
52 | curRoom = room;
53 |
54 | console.log(`${socket.id} joined room ${room}`);
55 | socket.join(room);
56 |
57 | socket.emit("connectSuccess", { joinedTime });
58 | const occupants = rooms[room].occupants;
59 | io.in(curRoom).emit("occupantsChanged", { occupants });
60 | });
61 |
62 | socket.on("send", data => {
63 | io.to(data.to).emit("send", data);
64 | });
65 |
66 | socket.on("broadcast", data => {
67 | socket.to(curRoom).broadcast.emit("broadcast", data);
68 | });
69 |
70 | socket.on("disconnect", () => {
71 | console.log('disconnected: ', socket.id, curRoom);
72 | if (rooms[curRoom]) {
73 | console.log("user disconnected", socket.id);
74 |
75 | delete rooms[curRoom].occupants[socket.id];
76 | const occupants = rooms[curRoom].occupants;
77 | socket.to(curRoom).broadcast.emit("occupantsChanged", { occupants });
78 |
79 | if (Object.keys(occupants).length === 0) {
80 | console.log("everybody left room");
81 | delete rooms[curRoom];
82 | }
83 | }
84 | });
85 | });
86 |
87 | webServer.listen(port, () => {
88 | console.log("listening on http://localhost:" + port);
89 | });
90 |
--------------------------------------------------------------------------------
/research/perf/physics-performance.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ### Physics performance in my "Winter Wonderland" Christmas Scene as of 20/12/2021
4 |
5 | Methodology:
6 |
7 | - Create modified HTML file with only selected scene elements
8 | - Start scene up, leave for ~20 seconds to "stabilize"
9 | - Logging records physics tick averaged over 100 ticks - observe these in Chrome debugger.
10 | - I eyeball 5-10 of these logs and pick out a rough average.
11 | - Might have been a good idea to record variance too, but I didn't. In general variance was higher on desktop and lower on Oculus Quest 2, which is unsurprising as the desktop was running a load of other applications and browser tabs at the same time.
12 |
13 | Rationale for measuring in idle state.
14 |
15 | - Although an idle scene is less demanding than a test involving dynamic physical interactions, it is much easier to reliably reproduce, and also decently representative of performance - the scene contains many isolated elements, so even when the user is being highly interactive, most of them will be idle most of the time.
16 |
17 | Data below shows the physics tick time in msecs for scnese comprising various subsets of objects.
18 |
19 | All scenes also include: Floor Plane: 1 x static box
20 |
21 | | | Contains | Desktop | Oculus Quest 2 |
22 | | ---------------------------- | ------------------------------------------------------------ | ------------ | -------------- |
23 | | Full scene (21/12) | 17 x static shapes (including floor), 34 kinematic convex shapes, 10 x dynamic convex hulls, 1 x HACD hollow hat. | 42 | 26 |
24 | | Snowman only | 2 x static spheres, 7 x kinematic spheres, 3 x kinematic convex hulls (carrot + 2 x arms) 1 x kinematic HACD hollow hat | 17 | 6.5 |
25 | | Tree and presents only | 3 x static hulls, 9 x kinematic spheres, 1 x kinematic convex hull (star) | 14 | 3.7 |
26 | | Xylophone | 5 x static boxes, 8 x kinematic convex hulls | 4 | 1.15 |
27 | | Marble run | 3 x static boxes, 6 x kinematic boxes | 2.5 | 0.5 |
28 | | Bowling Alley | 3 x static boxes, 10 x dynamic convex hulls | 18 | 5 |
29 | | SUM OF COMPONENTS | See "full scene" above. | 55.5 (vs 42) | 16.85 (vs. 26) |
30 | | Snowman without HACD hat | 2 x static spheres, 7 x kinematic spheres, 3 x kinematic hulls (carrot + 2 x arms) | 4 | 1.5 |
31 | | Implied cost of HACD hat | 1 x kinematic HACD hollow hat. HOWEVER, cost of HACD hat appears to be *higher* in the context of the whole scene.... see next row... | 13 | 5 |
32 | | Total scene without HACD hat | See "full scene" above. | 25 | 11.5 |
33 |
34 | Implied approx cost of each element:
35 |
36 | - HACD hollow hat: Up to 15msecs
37 | - Dynamic shapes: 0.5msecs
38 | - Kinetic shapes: 0.2 to 0.3 msecs
39 | - Static shapes: < 0.1msecs
40 |
41 |
42 |
43 | Target for 60fps is to use substantially less than 16 msecs (ideally < 10msecs, as we need CPU for other functions as well).
44 |
45 |
46 |
47 | Unknowns:
48 |
49 | - Non-linearity - why do values on desktop add up to more than the total, whereas on Oculus Quest 2 they add up to (substantially) less than the total?
50 | - Seems to be substantially influenced by non-linearity associated with the HACD hat.
51 | - Without this, numbers seem to roughly add up on Oculus Quest 2
52 | - Not clear why we don't see such a negative non-linear effect on desktop, but desktop numbers are less reliable as I'm testing with a bunch of other applications running, so it's a much less controlled environment.
53 |
54 |
55 |
56 | Implications:
57 |
58 | - To achieve 60fps on Oculus Quest, the hollow snowman's hat has to go. It uses almost our entire physics budget up just on its own!
59 | - Possible solution might be to make it square in shape (vs. currently a 10-sided approximation of a circle)
60 | - Plausibly that would cut the cost to 2/5ths...
61 | - The fact that most objects are "stuck" and hence kinetic, most of the time, is beneficial for physics performance. Maybe 40-50% less cost than equivalent dynamic objects.
62 | - Hooray for sticky snow!
63 | - Nothing else (other than reducing overall scene complexity) is likely to make much difference - e.g. modelling penguins as cylinders rather than convex hulls is unlikely to move the needle very far at all.
64 |
65 |
66 |
67 | ### Some more notes on HACD snowman's hat
68 |
69 | Looking at overall scene physics ttick time on Oculus Quest 2...
70 |
71 | - Reducing hat geometry from 10 sides to 4 sides, with HACD gets us from 26msecs to ~18msecs.
72 | - Making the hat 10 sided, non-hollow, with HACD also gets ups to ~18 msecs (since the brim sticks out, HACD still has a bunch of work to do for the outside of the hat)
73 | - Making the hat 10-sides non-hollow with a convex hull shape gets us down to ~12 msecs.
74 |
75 | I don't really care about the non-convex exterior of the hull. A convex wrap would be fine for that. But I would like the hat to still have a hollow interior.
76 |
77 | - Not sure whether that can be achieved... Segmenting the hat into a set of radial segments, each given a convex wrap, and kept rigidly in formation with parent-child relationships, would work as long as the object is kinematic, but things will fall apart badly as soon as all those objects become dynamic.
78 |
79 | - A hat without a brim might be a lot simpler...
80 |
81 | - Or maybe I could have the brim as a separate child object that is decorative only, with no collision physics on it (like I have done with the ribbons on the presents)...
82 |
83 |
84 |
85 |
86 |
87 | Further update on the HACD hat...
88 |
89 | - After adding 50 fence posts around the perimeter of the scene, the impact of the HACD hat has become much worse.
90 | - Without the HACD hat, physics tick processing is now ~9msecs (yes, it's gone down as a result of adding more physics objects, that doesn't make much sense but it seems to have hapened)
91 | - With the HACD hat, physics tick processing is now ~35msecs (way up vs. before).
92 | - So the HACD hat has to go.
93 | - One idea I have had is to have the hat track its orientation, and switch from hull shape to HACD shape only when upside down. If I can make the switchover clean, that might get me the best of both worlds...
--------------------------------------------------------------------------------
/research/physics-ammo/Object-types.md:
--------------------------------------------------------------------------------
1 | Current status:
2 |
3 | - Static objects OK
4 | - Stickable objects OK
5 | - Dynamic non-sticky objects OK
6 | - Sticky dynamic objects not OK - really need reparenting working with physics to manage these in a stable way...
7 | - New throwing physics TBC - not yet tested in VR.
8 |
9 |
10 |
11 | Net steps:
12 |
13 | - Maybe move to this codebase & API anyway. Works for everything in current scene & enables dynamic non-sticky objects.
14 | - Prototype re-parenting with Ammo.js physics *(why doesn't it work? Can it work?)
15 | - Test how new velocity-on-release throwing physics feels.
16 |
17 |
18 |
19 | Sticky static object
20 |
21 | - E.g. snowman's body & arms.
22 | - Just regular static objects with "sticky" attribute
23 |
24 |
25 |
26 | Stickable objects
27 |
28 | - E.g. eyes, carrot
29 | - Dynamic objects, moveable by hand
30 | - When positioned overlapping a sticky object , they stick to it.
31 | - They select one sticky parent object as the sticky-parent, and always move with this
32 | - (except when grabbed by a hand. Hand grabbing dominates over sticky-parent)
33 |
34 |
35 |
36 | Sticky dynamic objects
37 |
38 | - Eg. snowball
39 | - Act just like stickable objects...
40 | - But also: stackable objects can stick to them...
41 | - ... and they will stick to stickable objects.
42 |
43 |
44 |
45 | Dynamic objects
46 |
47 | - E.g. presents
48 | - Dynamic objects, moveable by hand
49 | - But they don't stick to objects, objects don't stick to them.
50 | -
51 |
52 |
53 |
54 | Attributes:
55 |
56 | - sticky
57 | - stickable
58 | - grabbable
59 | - static
60 |
61 |
62 |
63 | Marble run = static
64 |
65 | Snowman base = sticky, static
66 |
67 | Snowman carrot = stickable, grabbable
68 |
69 | Present = grabbable
70 |
71 | Snowball = sticky, grabbable
72 |
73 |
74 |
75 | So... objects are:
76 |
77 | - static, grabbable, or neither (if object doesn't interact with physics at all)
78 | - sticky, stickable, or neither.
79 |
80 |
81 |
82 | Wrapped up in a single component:
83 |
84 | movement="type: static | grabbable; stickiness: sticky | stickable | none"
85 |
86 | For easy access, "sticky" and "stickable" attributes are set directly on objects by the movement component.
87 |
88 | We'll call this rewrite "phase 3" - I want to preserve "phase 2" for demos etc.
89 |
90 |
91 |
92 |
93 | For simplicity & consistency, ammo-shape is always defined externally to this component.
94 |
95 |
96 |
97 |
98 |
99 | Migrating phase 2 to phase 3:
100 |
101 | sticky-object -> movement="type:static; stickiness:sticky" + define ammo shape externally.
102 |
103 | sticky object -> movement="type:static; stickiness:sticky" + define ammo shape externally.
104 |
105 |
106 |
107 | movable-object -> movement="type:grabbable; stickiness:stickable" + define ammo shape externally.
108 |
109 | *don't* define shape in mixins (unreliable, or maybe doesn't work at all?)
--------------------------------------------------------------------------------
/research/physics-ammo/advanced-physics-test-1.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Advanced Physics Test 1 - Hat & Coals
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | -->
18 |
19 |
22 |
23 |
26 |
27 |
30 |
31 |
35 |
36 |
37 |
38 |
39 |
40 |
42 |
43 |
45 |
46 |
47 |
48 |
49 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
80 |
81 |
82 |
83 |
84 |
85 | Hollow snowman's hat full of dynamic spheres.
86 | Key goal of this test is to reproduce stability problems (crashes) seen in VR.
87 | Keyboard Controls:
88 | IJKL (like WASD) to move the hat
89 | O & P to rotate CCW & CW
90 |
112 | Basic 2D simulation of function to switch between dynamic & kinematic ammo bodies.
113 | Keyboard Controls:
114 | Sphere begins in Kinematic Mode.
115 | 1 to set Dynamic Mode.
116 | 2 to set Kinematic Mode.
117 | In Kinematic Mode, IJKL (like WASD) to move the sphere
118 | Sphere changes color (cycling through the rainbow) eac time a collision begins or ends.
119 | This allows for easy confirmation that collisions are being detected (there are also console logs).
120 | Gravity is artificially low, to make behaviour easier to ovbserve.
121 |
46 | Basic 2D simulation of grabbing a snowman's nose, using "Phase 2" physics (as described in the Physics README).
47 | Keyboard Controls:
48 | IJKL (like WASD) to move the pink block (which represents a hand)
49 | Left Shift to grip (must be gripped to affect object). Pink block becomes more transparent on grip.
50 | O & P to rotate CCW & CW (must be gripped to affect object)
51 | To test conservation of velocity / rotation on release, hold the direction or rotation key down while releasing teh grip
52 | WASD and mouse also move view as usual.
53 | Gravity is artificially low, to make behaviour on release easier to ovbserve.
54 |
60 | Basic 2D simulation of grabbing a snowman's nose, using "Phase 3" physics (as described in the Physics README).
61 | Keyboard Controls:
62 | IJKL (like WASD) to move the pink block (which represents a hand)
63 | Left Shift to grip (must be gripped to affect object). Pink block becomes more transparent on grip.
64 | O & P to rotate CCW & CW (must be gripped to affect object)
65 | To test conservation of velocity / rotation on release, hold the direction or rotation key down while releasing teh grip
66 | WASD and mouse also move view as usual.
67 | Gravity is artificially low, to make behaviour on release easier to observe.
68 | Note that one side effect of low gravity is that objects my go to sleep (freeze) sooner than you would expect.
69 |
46 | Basic 2D simulation of grabbing a snowman's nose, using "Phase 1" physics (as described in the Physics README).
47 | Keyboard Controls:
48 | IJKL (like WASD) to move the pink block (which represents a hand)
49 | Left Shift to grip (must be gripped to affect object). Pink block becomes more transparent on grip.
50 | O & P to rotate CCW & CW (must be gripped to affect object)
51 | To test conservation of velocity / rotation on release, hold the direction or rotation key down while releasing teh grip
52 | WASD and mouse also move view as usual.
53 | Gravity is artificially low, to make behaviour on release easier to ovbserve.
54 |
144 | Basic 2D simulation of function to switch between dynamic & kinematic ammo bodies.
145 | Keyboard Controls:
146 | Sphere begins in Kinematic Mode.
147 | 1 to set Dynamic Mode.
148 | 2 to set Kinematic Mode.
149 | In Kinematic Mode, IJKL (like WASD) to move the sphere
150 | Sphere changes color (cycling through the rainbow) eac time a collision begins or ends.
151 | This allows for easy confirmation that collisions are being detected (there are also console logs).
152 | Gravity is artificially low, to make behaviour easier to ovbserve.
153 |
157 | Basic 2D simulation of function to switch between dynamic & kinematic ammo bodies.
158 | Keyboard Controls:
159 | Sphere begins in Kinematic Mode.
160 | 1 to set Dynamic Mode.
161 | 2 to set Kinematic Mode.
162 | 3 to parent to green sphere (goes blue)
163 | 4 to unparent from green sphere (goes green again)
164 | In Kinematic Mode, IJKL (like WASD) to move the sphere
165 | Sphere changes color (cycling through the rainbow) eac time a collision begins or ends.
166 | This allows for easy confirmation that collisions are being detected (there are also console logs).
167 | Gravity is artificially low, to make behaviour easier to ovbserve.
168 |
47 | Basic 2D simulation of grabbing a snowman's nose, using "Phase 2" physics (as described in the Physics README).
48 | Keyboard Controls:
49 | IJKL (like WASD) to move the pink block (which represents a hand)
50 | Left Shift to grip (must be gripped to affect object). Pink block becomes more transparent on grip.
51 | O & P to rotate CCW & CW (must be gripped to affect object)
52 | To test conservation of velocity / rotation on release, hold the direction or rotation key down while releasing teh grip
53 | WASD and mouse also move view as usual.
54 | Gravity is artificially low, to make behaviour on release easier to ovbserve.
55 |
55 | Basic 2D simulation of grabbing a snowman's nose, using "Phase 2" physics (as described in the Physics README).
56 | Keyboard Controls:
57 | IJKL (like WASD) to move the pink block (which represents a hand)
58 | Left Shift to grip (must be gripped to affect object). Pink block becomes more transparent on grip.
59 | O & P to rotate CCW & CW (must be gripped to affect object)
60 | To test conservation of velocity / rotation on release, hold the direction or rotation key down while releasing teh grip
61 | WASD and mouse also move view as usual.
62 | Gravity is artificially low, to make behaviour on release easier to ovbserve.
63 |
56 | Basic 2D simulation of pinting a snowball.
57 | Keyboard Controls:
58 | IJKL (like WASD) to move the pink block (which represents a hand)
59 | Left Shift to grip (must be gripped to affect object). Pink block becomes more transparent on grip.
60 | T to toggle trigger - used to change paintbrush color.
61 | O & P to rotate CCW & CW (must be gripped to affect object)
62 | Gravity is artificially low, to make behaviour on release easier to observe.
63 | Note that one side effect of low gravity is that objects my go to sleep (freeze) sooner than you would expect.
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/research/snowballs/snowball-generator.js:
--------------------------------------------------------------------------------
1 | AFRAME.registerComponent('snowball-generator', {
2 |
3 | schema: {
4 | // height level must go below to generate a snowball
5 | heightLevel: {type: 'number', default: 0.1},
6 | // how long must stay there with grip held down to generate a snowball.
7 | timer: {type: 'number', default: 1000},
8 | mixin: {type: 'string', default: 'snowball'},
9 | scale: {type: 'number', default: 0.15}
10 |
11 | },
12 |
13 | init() {
14 |
15 | this.timePassed = 0;
16 | this.el.addEventListener("gripdown", this.gripDown.bind(this));
17 | this.el.addEventListener("gripup", this.gripUp.bind(this));
18 | },
19 |
20 |
21 | tick(time, timeDelta) {
22 |
23 | // To create a snowball, certain conditions must be met continuously for
24 | // a set period of time.
25 | const emptyHand = (!this.el.components['hand'] ||
26 | !this.el.components['hand'].grabbedEl);
27 |
28 | if (emptyHand &&
29 | this.el.object3D.position.y <= this.data.heightLevel &&
30 | this.gripIsDown) {
31 |
32 | this.timePassed += timeDelta;
33 |
34 | if (this.timePassed > this.data.timer) {
35 | this.createSnowball();
36 | // for our purposes, consider grip up
37 | // i.e. don't make any more snowballs until user
38 | // explicitly grips down again.
39 | this.gripIsDown = false;
40 | }
41 | }
42 | else {
43 | // conditions not met - reset timer to zero.
44 | this.timePassed = 0;
45 | }
46 | },
47 |
48 | gripDown() {
49 | this.gripIsDown = true;
50 | },
51 |
52 | gripUp() {
53 | this.gripIsDown = false;
54 | },
55 |
56 | createSnowball() {
57 |
58 | snowball = document.createElement('a-entity');
59 | snowball.setAttribute('mixin', this.data.mixin)
60 | const radius = snowball.mixinEls[0].componentCache.geometry.radius;
61 | snowball.setAttribute('ammo-shape', `type:sphere; fit:manual; sphereRadius:${radius * this.data.scale}`)
62 | snowball.object3D.scale.set(this.data.scale, this.data.scale, this.data.scale);
63 | snowball.object3D.position.copy(this.el.object3D.position);
64 | snowball.setAttribute('snowball-grow-on-roll', "");
65 | this.el.sceneEl.appendChild(snowball);
66 | }
67 | });
68 |
69 |
70 | AFRAME.registerComponent('snowball-grow-on-roll', {
71 |
72 | schema: {
73 | // snowball center must be below radius + this height to grow.
74 | heightDelta: {type: 'number', default: 0.01},
75 | // Growth of radius per radius travelled.
76 | // This factor is added to "scale" for each radian of rotation.
77 | growthRate: {type: 'number', default: 0.01},
78 |
79 | // radius at which snowballs cease to be grabbable/stickable.
80 | // not working yet,
81 | //maxGrabbableRadius: {type: 'number', default: 0.2},
82 | },
83 |
84 | init() {
85 |
86 | this.lastWorldPosition = new THREE.Vector3()
87 | this.lastWorldQuaternion = new THREE.Quaternion()
88 | this.el.object3D.getWorldPosition(this.lastWorldPosition);
89 | this.el.object3D.getWorldQuaternion(this.lastWorldQuaternion);
90 |
91 | this.geometryRadius = this.el.mixinEls[0].componentCache.geometry.radius;
92 | this.setRadius();
93 |
94 | // used in calculations
95 | this.currentWorldPosition = new THREE.Vector3()
96 | this.currentWorldQuaternion = new THREE.Quaternion()
97 | this.distanceMoved = new THREE.Vector3()
98 |
99 | this.scaleVector = new THREE.Vector3();
100 | },
101 |
102 | setRadius() {
103 |
104 | this.el.object3D.getWorldScale(this.scaleVector)
105 | this.radius = this.geometryRadius * this.scaleVector.x;
106 | this.maxHeight = this.radius + this.data.heightDelta;
107 |
108 | /*if (this.radius > this.data.maxGrabbableRadius) {
109 | this.el.setAttribute('movement', "type:dynamic; stickiness:none");
110 | }*/
111 | },
112 |
113 | tick(time, timeDelta) {
114 |
115 | this.el.object3D.getWorldPosition(this.currentWorldPosition);
116 | this.el.object3D.getWorldQuaternion(this.currentWorldQuaternion);
117 |
118 | if (this.lastWorldPosition.y < this.maxHeight &&
119 | this.currentWorldPosition.y < this.maxHeight) {
120 | // snowball stayed below height limit.
121 | // grow it by horoizontal distance travelled.
122 |
123 | /* Distance-bsed - growth not using this */
124 | /*this.distanceMoved.subVectors(this.currentWorldPosition,
125 | this.lastWorldPosition);
126 | this.distanceMoved.y = 0;
127 |
128 | const distance = this.distanceMoved.length();*/
129 |
130 | const distance = this.lastWorldQuaternion.angleTo(this.currentWorldQuaternion);
131 |
132 | // require minium amount of turning speed before we grow.
133 | if (distance * 1000/timeDelta > 0.5) {
134 |
135 | const growthAmount = 0.1 * this.data.growthRate * distance / this.radius;
136 |
137 | const newScale = this.el.object3D.scale.x + growthAmount;
138 | this.el.object3D.scale.set(newScale, newScale, newScale);
139 | this.el.object3D.matrix.compose(this.el.object3D.position,
140 | this.el.object3D.quaternion,
141 | this.el.object3D.scale);
142 |
143 | this.setRadius()
144 | }
145 | }
146 | // update position for next tick.
147 | this.lastWorldPosition.copy(this.currentWorldPosition);
148 | this.lastWorldQuaternion.copy(this.currentWorldQuaternion);
149 | }
150 | });
151 |
--------------------------------------------------------------------------------
/research/snowballs/test-snowball-generation.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Snowballs and Other Objects with Ammo.js
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
18 |
19 |
23 |
24 |
25 |
26 |
34 |
35 |
37 |
43 |
44 |
45 |
46 |
47 |
48 | SNowball generation test.
49 | Keyboard Controls:
50 | IJKL (like WASD) to move the pink block (which represents a hand)
51 | Left Shift to grip (must be gripped to affect object). Pink block becomes more transparent on grip.
52 | O & P to rotate CCW & CW (must be gripped to affect object)
53 | To generate a snowball, hold down grip for 1 second at a height below 2m (will be 0.1m in production)
54 |