diff --git a/admin/package-lock.json b/admin/package-lock.json
index 704009ff96..5e1bd39f79 100644
--- a/admin/package-lock.json
+++ b/admin/package-lock.json
@@ -1504,7 +1504,7 @@
"dev": true
},
"aframe": {
- "version": "github:mozillareality/aframe#deba2d4eedda664bb7d87bf4b3c9fcf25f5298e1",
+ "version": "github:mozillareality/aframe#3301e4b276c8516b4e74afe45412c984ed6d6cac",
"from": "github:mozillareality/aframe#hubs/master",
"requires": {
"custom-event-polyfill": "^1.0.6",
@@ -18705,10 +18705,10 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
},
"networked-aframe": {
- "version": "github:mozillareality/networked-aframe#50184f495ff1f77d0e6fe3777d73092784e3aee3",
+ "version": "github:mozillareality/networked-aframe#25198c32d618adc5f887e70b1c4e8cb5f3d3e047",
"from": "github:mozillareality/networked-aframe#master",
"requires": {
- "buffered-interpolation": "^0.2.5"
+ "buffered-interpolation": "github:Infinitelee/buffered-interpolation#5bb18421ebf2bf11664645cdc7a15bd77ee2156b"
}
},
"new-array": {
diff --git a/package-lock.json b/package-lock.json
index a4dcc3472e..3422229275 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15829,7 +15829,7 @@
"dev": true
},
"aframe": {
- "version": "github:mozillareality/aframe#2cd12e184bbf7e1ff7d2100d66064f50403405c1",
+ "version": "github:mozillareality/aframe#3301e4b276c8516b4e74afe45412c984ed6d6cac",
"from": "github:mozillareality/aframe#hubs/master",
"requires": {
"custom-event-polyfill": "^1.0.6",
@@ -19021,6 +19021,11 @@
"integrity": "sha1-6C5D6OsXBkaB5D+cjbxzHjF9GJI=",
"dev": true
},
+ "bitecs": {
+ "version": "0.3.38",
+ "resolved": "https://registry.npmjs.org/bitecs/-/bitecs-0.3.38.tgz",
+ "integrity": "sha512-vfz6wjPElg/0O7c06Ttb/BeWMInBspSuekRvNoLqPyEZV87tLWipxqpjtGPKZBhin8jv4OuQkrkZZfZpaqOndQ=="
+ },
"bluebird": {
"version": "3.5.1",
"resolved": "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz",
@@ -27689,7 +27694,7 @@
"dev": true
},
"networked-aframe": {
- "version": "github:mozillareality/networked-aframe#2c3c394e3f12a7d2cc68b193c0f7c94d709a46e8",
+ "version": "github:mozillareality/networked-aframe#25198c32d618adc5f887e70b1c4e8cb5f3d3e047",
"from": "github:mozillareality/networked-aframe#master",
"requires": {
"buffered-interpolation": "github:Infinitelee/buffered-interpolation#5bb18421ebf2bf11664645cdc7a15bd77ee2156b"
diff --git a/package.json b/package.json
index be494e3494..dc6200c193 100644
--- a/package.json
+++ b/package.json
@@ -79,6 +79,7 @@
"ammo-debug-drawer": "github:infinitelee/ammo-debug-drawer",
"ammo.js": "github:mozillareality/ammo.js#hubs/master",
"animejs": "github:mozillareality/anime#hubs/master",
+ "bitecs": "^0.3.38",
"buffered-interpolation": "github:Infinitelee/buffered-interpolation",
"classnames": "^2.2.5",
"color": "^3.1.2",
diff --git a/src/App.js b/src/App.js
index de56893bcc..004a3ae2b6 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,7 +1,24 @@
-import Store from "./storage/store";
+import * as bitecs from "bitecs";
+import { addEntity, createWorld } from "bitecs";
+import "./aframe-to-bit-components";
+import { AEntity, Networked, Object3DTag, Owned } from "./bit-components";
import MediaSearchStore from "./storage/media-search-store";
+import Store from "./storage/store";
import qsTruthy from "./utils/qs_truthy";
+window.$B = bitecs;
+
+const timeSystem = world => {
+ const { time } = world;
+ const now = performance.now();
+ const delta = now - time.then;
+ time.delta = delta;
+ time.elapsed += delta;
+ time.then = now;
+ time.tick++;
+ return world;
+};
+
export class App {
constructor() {
this.scene = null;
@@ -23,6 +40,50 @@ export class App {
this.clippingState = new Set();
this.mutedState = new Set();
this.isAudioPaused = new Set();
+
+ this.world = createWorld();
+
+ // TODO: Create accessor / update methods for these maps / set
+ this.world.eid2obj = new Map(); // eid -> Object3D
+
+ this.world.nid2eid = new Map();
+ this.world.deletedNids = new Set();
+ this.world.ignoredNids = new Set();
+
+ this.str2sid = new Map([[null, 0]]);
+ this.sid2str = new Map([[0, null]]);
+ this.nextSid = 1;
+
+ window.$o = eid => {
+ this.world.eid2obj.get(eid);
+ };
+
+ // reserve entity 0 to avoid needing to check for undefined everywhere eid is checked for existance
+ addEntity(this.world);
+
+ // used in aframe and networked aframe to avoid imports
+ this.world.nameToComponent = {
+ object3d: Object3DTag,
+ networked: Networked,
+ owned: Owned,
+ AEntity
+ };
+ }
+
+ // TODO nothing ever cleans these up
+ getSid(str) {
+ if (!this.str2sid.has(str)) {
+ const sid = this.nextSid;
+ this.nextSid = this.nextSid + 1;
+ this.str2sid.set(str, sid);
+ this.sid2str.set(sid, str);
+ return sid;
+ }
+ return this.str2sid.get(str);
+ }
+
+ getString(sid) {
+ return this.sid2str.get(sid);
}
// This gets called by a-scene to setup the renderer, camera, and audio listener
@@ -66,6 +127,7 @@ export class App {
const camera = new THREE.PerspectiveCamera(80, window.innerWidth / window.innerHeight, 0.05, 10000);
const audioListener = new THREE.AudioListener();
+ APP.audioListener = audioListener;
camera.add(audioListener);
const renderClock = new THREE.Clock();
@@ -73,6 +135,16 @@ export class App {
// TODO NAF currently depends on this, it should not
sceneEl.clock = renderClock;
+ // TODO we should have 1 source of truth for time
+ APP.world.time = {
+ delta: 0,
+ elapsed: 0,
+ then: performance.now(),
+ tick: 0
+ };
+
+ APP.world.scene = sceneEl.object3D;
+
// Main RAF loop
function mainTick(_rafTime, xrFrame) {
// TODO we should probably be using time from the raf loop itself
@@ -82,6 +154,8 @@ export class App {
// TODO pass this into systems that care about it (like input) once they are moved into this loop
sceneEl.frame = xrFrame;
+ timeSystem(APP.world);
+
// Tick AFrame systems and components
if (sceneEl.isPlaying) {
sceneEl.tick(time, delta);
diff --git a/src/aframe-to-bit-components.js b/src/aframe-to-bit-components.js
new file mode 100644
index 0000000000..92ba10decb
--- /dev/null
+++ b/src/aframe-to-bit-components.js
@@ -0,0 +1,31 @@
+import { addComponent, removeComponent } from "bitecs";
+import {
+ RemoteRight,
+ RemoteLeft,
+ HandRight,
+ HandLeft,
+ RemoteHoverTarget,
+ NotRemoteHoverTarget,
+ RemoveNetworkedEntityButton,
+ DestroyAtExtremeDistance
+} from "./bit-components";
+
+[
+ ["remote-right", RemoteRight],
+ ["remote-left", RemoteLeft],
+ ["hand-right", HandRight],
+ ["hand-left", HandLeft],
+ ["is-remote-hover-target", RemoteHoverTarget],
+ ["is-not-remote-hover-target", NotRemoteHoverTarget],
+ ["remove-networked-object-button", RemoveNetworkedEntityButton],
+ ["destroy-at-extreme-distances", DestroyAtExtremeDistance]
+].forEach(([aframeComponentName, bitecsComponent]) => {
+ AFRAME.registerComponent(aframeComponentName, {
+ init: function() {
+ addComponent(APP.world, bitecsComponent, this.el.object3D.eid);
+ },
+ remove: function() {
+ removeComponent(APP.world, bitecsComponent, this.el.object3D.eid);
+ }
+ });
+});
diff --git a/src/bit-components.js b/src/bit-components.js
new file mode 100644
index 0000000000..30b703c039
--- /dev/null
+++ b/src/bit-components.js
@@ -0,0 +1,121 @@
+import { defineComponent, setDefaultSize, Types } from "bitecs";
+
+// TODO this has to happen before all components are defined. Is there a better spot to be doing this?
+setDefaultSize(10000);
+
+export const $isStringType = Symbol("isStringType");
+
+export const Networked = defineComponent({
+ id: Types.ui32,
+ creator: Types.ui32,
+ owner: Types.ui32,
+
+ lastOwnerTime: Types.ui32
+});
+Networked.id[$isStringType] = true;
+Networked.creator[$isStringType] = true;
+Networked.owner[$isStringType] = true;
+
+export const Owned = defineComponent();
+export const NetworkedMediaFrame = defineComponent({
+ capturedNid: Types.ui32,
+ scale: [Types.f32, 3]
+});
+NetworkedMediaFrame.capturedNid[$isStringType] = true;
+
+export const MediaFrame = defineComponent({
+ capturedNid: Types.ui32,
+ scale: [Types.f32, 3],
+ mediaType: Types.ui8,
+ bounds: [Types.f32, 3],
+ preview: Types.eid,
+ previewingNid: Types.eid
+});
+export const Text = defineComponent();
+export const Slice9 = defineComponent({
+ insets: [Types.ui32, 4],
+ size: [Types.f32, 2]
+});
+
+export const NetworkedTransform = defineComponent({
+ position: [Types.f32, 3],
+ rotation: [Types.f32, 4],
+ scale: [Types.f32, 3]
+});
+
+export const AEntity = defineComponent();
+export const Object3DTag = defineComponent();
+export const GLTFModel = defineComponent();
+export const CursorRaycastable = defineComponent();
+export const RemoteHoverTarget = defineComponent();
+export const NotRemoteHoverTarget = defineComponent();
+export const Holdable = defineComponent();
+export const RemoveNetworkedEntityButton = defineComponent();
+export const Interacted = defineComponent();
+
+export const HandRight = defineComponent();
+export const HandLeft = defineComponent();
+export const RemoteRight = defineComponent();
+export const RemoteLeft = defineComponent();
+export const HoveredHandRight = defineComponent();
+export const HoveredHandLeft = defineComponent();
+export const HoveredRemoteRight = defineComponent();
+export const HoveredRemoteLeft = defineComponent();
+export const HeldHandRight = defineComponent();
+export const HeldHandLeft = defineComponent();
+export const HeldRemoteRight = defineComponent();
+export const HeldRemoteLeft = defineComponent();
+export const Held = defineComponent();
+export const OffersRemoteConstraint = defineComponent();
+export const HandCollisionTarget = defineComponent();
+export const OffersHandConstraint = defineComponent();
+export const TogglesHoveredActionSet = defineComponent();
+
+export const HoverButton = defineComponent({ type: Types.ui8 });
+export const TextButton = defineComponent({ labelRef: Types.eid });
+export const HoldableButton = defineComponent();
+export const SingleActionButton = defineComponent();
+
+export const Pen = defineComponent();
+export const HoverMenuChild = defineComponent();
+export const Static = defineComponent();
+export const Inspectable = defineComponent();
+export const PreventAudioBoost = defineComponent();
+export const IgnoreSpaceBubble = defineComponent();
+export const Rigidbody = defineComponent({
+ bodyId: Types.ui16,
+ collisionGroup: Types.ui32,
+ collisionMask: Types.ui32,
+ flags: Types.ui8,
+ gravity: Types.f32
+});
+export const PhysicsShape = defineComponent({ bodyId: Types.ui16, shapeId: Types.ui16, halfExtents: [Types.f32, 3] });
+export const Pinnable = defineComponent();
+export const Pinned = defineComponent();
+export const DestroyAtExtremeDistance = defineComponent();
+
+export const MediaLoading = defineComponent();
+
+export const FloatyObject = defineComponent({ flags: Types.ui8, releaseGravity: Types.f32 });
+export const MakeKinematicOnRelease = defineComponent();
+
+export const CameraTool = defineComponent({
+ snapTime: Types.f32,
+ state: Types.ui8,
+ captureDurIdx: Types.ui8,
+ trackTarget: Types.eid,
+
+ snapMenuRef: Types.eid,
+ button_next: Types.eid,
+ button_prev: Types.eid,
+ snapRef: Types.eid,
+ cancelRef: Types.eid,
+ recVideoRef: Types.eid,
+ screenRef: Types.eid,
+ selfieScreenRef: Types.eid,
+ cameraRef: Types.eid,
+ countdownLblRef: Types.eid,
+ captureDurLblRef: Types.eid,
+ sndToggleRef: Types.eid
+});
+export const MyCameraTool = defineComponent();
diff --git a/src/bit-systems/camera-tool.js b/src/bit-systems/camera-tool.js
new file mode 100644
index 0000000000..2c8a84edf8
--- /dev/null
+++ b/src/bit-systems/camera-tool.js
@@ -0,0 +1,432 @@
+import { defineQuery, enterQuery, entityExists, exitQuery, hasComponent } from "bitecs";
+import {
+ CameraTool,
+ Held,
+ HeldHandLeft,
+ HeldHandRight,
+ HeldRemoteLeft,
+ HeldRemoteRight,
+ HoveredRemoteRight,
+ Interacted,
+ RemoteRight,
+ Rigidbody,
+ TextButton
+} from "../bit-components";
+import { addMedia } from "../utils/media-utils";
+import { pixelsToPNG, RenderTargetRecorder } from "../utils/render-target-recorder";
+import { isFacingCamera } from "../utils/three-utils";
+import { SOUND_CAMERA_TOOL_COUNTDOWN, SOUND_CAMERA_TOOL_TOOK_SNAPSHOT } from "../systems/sound-effects-system";
+import { paths } from "../systems/userinput/paths";
+import { ObjectTypes } from "../object-types";
+import { anyEntityWith } from "../utils/bit-utils";
+
+// Prefer h264 if available due to faster decoding speec on most platforms
+const videoCodec = ["h264", "vp9,opus", "vp8,opus", "vp9", "vp8"].find(
+ codec => window.MediaRecorder && MediaRecorder.isTypeSupported(`video/webm; codecs=${codec}`)
+);
+const videoMimeType = videoCodec ? `video/webm; codecs=${videoCodec}` : null;
+const hasWebGL2 = !!document.createElement("canvas").getContext("webgl2");
+const allowVideo = !!videoMimeType && hasWebGL2;
+
+const RENDER_WIDTH = 1280;
+const RENDER_HEIGHT = 720;
+
+const isMobileVR = AFRAME.utils.device.isMobileVR();
+const isOculusBrowser = navigator.userAgent.match(/Oculus/);
+// TODO ported from old camera system. Do we still want these restrictions?
+const CAPTURE_WIDTH = isMobileVR && !isOculusBrowser ? 640 : 1280;
+const CAPTURE_HEIGHT = isMobileVR && !isOculusBrowser ? 360 : 720;
+
+const VIDEO_FPS = 25;
+const VIEWFINDER_UPDATE_RATE = 1000 / 6;
+const VIDEO_UPDATE_RATE = 1000 / VIDEO_FPS;
+
+const CAMERA_STATE = {
+ IDLE: 0,
+ COUNTDOWN_PHOTO: 1,
+ COUNTDOWN_VIDEO: 2,
+ SNAP_PHOTO: 3,
+ RECORDING_VIDEO: 4
+};
+
+const CAPTURE_DURATIONS = [3, 7, 15, 30, 60];
+
+const renderTargets = new Map();
+const videoRecorders = new Map();
+let captureAudio = false;
+
+const tmpVec3 = new THREE.Vector3();
+
+function clicked(eid) {
+ return hasComponent(APP.world, Interacted, eid);
+}
+
+function grabberPressedSnapAction(world, camera) {
+ const userinput = AFRAME.scenes[0].systems.userinput;
+ return (
+ (hasComponent(world, HeldRemoteLeft, camera) && userinput.get(paths.actions.cursor.left.takeSnapshot)) ||
+ (hasComponent(world, HeldRemoteRight, camera) && userinput.get(paths.actions.cursor.right.takeSnapshot)) ||
+ (hasComponent(world, HeldHandLeft, camera) && userinput.get(paths.actions.leftHand.takeSnapshot)) ||
+ (hasComponent(world, HeldHandRight, camera) && userinput.get(paths.actions.rightHand.takeSnapshot))
+ // userinput.get(paths.actions.takeSnapshot)
+ );
+}
+
+function spawnCameraFile(cameraObj, file, type) {
+ const opts = type === "video" ? { videoPaused: true } : {};
+ const { entity } = addMedia(file, "#interactable-media", undefined, `${type}-camera`, false, false, true, opts);
+ entity.addEventListener(
+ "media_resolved",
+ () => {
+ AFRAME.scenes[0].systems["hubs-systems"].soundEffectsSystem.playSoundOneShot(SOUND_CAMERA_TOOL_TOOK_SNAPSHOT);
+ // TODO animate and orient around camera
+ entity.object3D.position.copy(cameraObj.localToWorld(new THREE.Vector3(0, -0.5, 0)));
+ entity.object3D.quaternion.copy(cameraObj.quaternion);
+ entity.object3D.matrixNeedsUpdate = true;
+ APP.hubChannel.sendMessage({ src: entity.components["media-loader"].data.src }, type);
+ APP.hubChannel.sendObjectSpawnedEvent(ObjectTypes.CAMERA);
+ },
+ { once: true }
+ );
+}
+
+function createRecorder(captureAudio) {
+ let srcAudioTrack;
+ // NOTE: if we don't have a self audio track, we can end up generating an empty video (browser bug?)
+ // if no audio comes through on the listener source. (Eg the room is otherwise silent.)
+ // So for now, if we don't have a track, just disable audio capture.
+ if (captureAudio && APP.dialog._micProducer?.track) {
+ const context = THREE.AudioContext.getContext();
+ const destination = context.createMediaStreamDestination();
+ if (APP.audioListener) {
+ // NOTE audio is not captured from camera vantage point for now.
+ APP.audioListener.getInput().connect(destination);
+ }
+ context.createMediaStreamSource(new MediaStream([APP.dialog._micProducer?.track])).connect(destination);
+ srcAudioTrack = destination.stream.getAudioTracks()[0];
+ }
+
+ return new RenderTargetRecorder(
+ AFRAME.scenes[0].renderer,
+ videoMimeType,
+ CAPTURE_WIDTH,
+ CAPTURE_HEIGHT,
+ VIDEO_FPS,
+ srcAudioTrack
+ );
+}
+
+function endRecording(world, camera, cancel) {
+ const recorder = videoRecorders.get(camera);
+ if (cancel) {
+ recorder.cancel();
+ } else {
+ recorder.save().then(file => {
+ spawnCameraFile(world.eid2obj.get(camera), file, "video");
+ });
+ }
+ videoRecorders.delete(camera);
+ APP.hubChannel.endRecording();
+}
+
+function updateRenderTarget(world, camera) {
+ const sceneEl = AFRAME.scenes[0];
+ const renderer = AFRAME.scenes[0].renderer;
+
+ const tmpVRFlag = renderer.xr.enabled;
+ renderer.xr.enabled = false;
+
+ // TODO we are doing this because aframe uses this hook for tock.
+ // Namely to capture what camera was rendering. We don't actually use that in any of our tocks.
+ // Also tock can likely go away as a concept since we can just direclty order things after render in raf if we want to.
+ const tmpOnAfterRender = sceneEl.object3D.onAfterRender;
+ delete sceneEl.object3D.onAfterRender;
+
+ // TODO this assumption is now not true since we are not running after render. We should probably just permentently turn of autoUpdate and run matrix updates at a point we wnat to.
+ // The entire scene graph matrices should already be updated
+ // in tick(). They don't need to be recomputed again in tock().
+ const tmpAutoUpdate = sceneEl.object3D.autoUpdate;
+ sceneEl.object3D.autoUpdate = false;
+
+ const bubbleSystem = AFRAME.scenes[0].systems["personal-space-bubble"];
+ const boneVisibilitySystem = AFRAME.scenes[0].systems["hubs-systems"].boneVisibilitySystem;
+
+ if (bubbleSystem) {
+ for (let i = 0, l = bubbleSystem.invaders.length; i < l; i++) {
+ bubbleSystem.invaders[i].disable();
+ }
+ // HACK, bone visibility typically takes a tick to update, but since we want to be able
+ // to have enable() and disable() be reflected this frame, we need to do it immediately.
+ boneVisibilitySystem.tick();
+ // scene.autoUpdate will be false so explicitly update the world matrices
+ boneVisibilitySystem.updateMatrices();
+ }
+
+ const renderTarget = renderTargets.get(camera);
+ renderTarget.needsUpdate = false;
+ renderTarget.lastUpdated = world.time.elapsed;
+
+ renderer.setRenderTarget(renderTarget);
+ renderer.render(sceneEl.object3D, world.eid2obj.get(CameraTool.cameraRef[camera]));
+ renderer.setRenderTarget(null);
+
+ renderer.xr.enabled = tmpVRFlag;
+ sceneEl.object3D.onAfterRender = tmpOnAfterRender;
+ sceneEl.object3D.autoUpdate = tmpAutoUpdate;
+
+ if (bubbleSystem) {
+ for (let i = 0, l = bubbleSystem.invaders.length; i < l; i++) {
+ bubbleSystem.invaders[i].enable();
+ }
+ // HACK, bone visibility typically takes a tick to update, but since we want to be able
+ // to have enable() and disable() be reflected this frame, we need to do it immediately.
+ boneVisibilitySystem.tick();
+ boneVisibilitySystem.updateMatrices();
+ }
+}
+
+function updateUI(world, camera) {
+ const snapMenuObj = world.eid2obj.get(CameraTool.snapMenuRef[camera]);
+ const snapBtnObj = world.eid2obj.get(CameraTool.snapRef[camera]);
+ const recBtnObj = world.eid2obj.get(CameraTool.recVideoRef[camera]);
+ const cancelBtnObj = world.eid2obj.get(CameraTool.cancelRef[camera]);
+ const nextBtnObj = world.eid2obj.get(CameraTool.button_next[camera]);
+ const prevBtnObj = world.eid2obj.get(CameraTool.button_prev[camera]);
+ const countdownLblObj = world.eid2obj.get(CameraTool.countdownLblRef[camera]);
+ const captureDurLblObj = world.eid2obj.get(CameraTool.captureDurLblRef[camera]);
+ const screenObj = world.eid2obj.get(CameraTool.screenRef[camera]);
+ const selfieScreenObj = world.eid2obj.get(CameraTool.selfieScreenRef[camera]);
+
+ const sndToggleBtnObj = world.eid2obj.get(CameraTool.sndToggleRef[camera]);
+ const sndToggleLblObj = world.eid2obj.get(TextButton.labelRef[CameraTool.sndToggleRef[camera]]);
+
+ const isIdle = CameraTool.state[camera] === CAMERA_STATE.IDLE;
+ const isCounting =
+ CameraTool.state[camera] === CAMERA_STATE.COUNTDOWN_PHOTO ||
+ CameraTool.state[camera] === CAMERA_STATE.COUNTDOWN_VIDEO ||
+ CameraTool.state[camera] === CAMERA_STATE.RECORDING_VIDEO;
+
+ const inVR = AFRAME.scenes[0].is("vr-mode");
+ const showViewfinder = !inVR || (hasComponent(world, Held, camera) || !isIdle);
+ screenObj.visible = showViewfinder;
+ selfieScreenObj.visible = showViewfinder;
+
+ const playerInFrontOfCamera = isFacingCamera(world.eid2obj.get(camera));
+
+ const yRot = playerInFrontOfCamera ? 0 : Math.PI;
+ if (snapMenuObj.rotation.y !== yRot) {
+ snapMenuObj.rotation.y = yRot;
+ snapMenuObj.matrixNeedsUpdate = true;
+ }
+
+ snapBtnObj.visible = isIdle;
+
+ recBtnObj.visible = allowVideo && isIdle;
+ captureDurLblObj.visible = allowVideo && isIdle;
+ nextBtnObj.visible = allowVideo && isIdle;
+ prevBtnObj.visible = allowVideo && isIdle;
+ sndToggleBtnObj.visible = allowVideo && isIdle;
+
+ cancelBtnObj.visible = isCounting;
+ countdownLblObj.visible = isCounting;
+
+ if (countdownLblObj.visible) {
+ const timeLeftSec = Math.ceil((CameraTool.snapTime[camera] - world.time.elapsed) / 1000);
+ countdownLblObj.text = timeLeftSec;
+ countdownLblObj.sync(); // TODO this should probably happen in 1 spot per frame for all Texts
+ }
+
+ if (captureDurLblObj.visible) {
+ captureDurLblObj.text = CAPTURE_DURATIONS[CameraTool.captureDurIdx[camera]];
+ captureDurLblObj.sync(); // TODO this should probably happen in 1 spot per frame for all Texts
+ }
+
+ if (sndToggleBtnObj.visible) {
+ sndToggleLblObj.text = captureAudio ? "Sound ON" : "Sound OFF";
+ sndToggleLblObj.sync();
+ }
+
+ // TODO HACK hidden objects are still not having their matricies updated correctly
+ // Seems like a regression of #5421
+ snapMenuObj.matrixNeedsUpdate = true;
+}
+
+let snapPixels;
+function captureSnapshot(world, camera) {
+ if (!snapPixels) {
+ snapPixels = new Uint8Array(RENDER_WIDTH * RENDER_HEIGHT * 4);
+ }
+ const renderer = AFRAME.scenes[0].renderer;
+ renderer.readRenderTargetPixels(renderTargets.get(camera), 0, 0, RENDER_WIDTH, RENDER_HEIGHT, snapPixels);
+ pixelsToPNG(snapPixels, RENDER_WIDTH, RENDER_HEIGHT).then(file => {
+ spawnCameraFile(world.eid2obj.get(camera), file, "photo");
+ });
+}
+
+// TODO this should be its own thing, not hardcoded to camera tools or mouse bindings
+function rotateWithRightClick(world, camera) {
+ const userinput = AFRAME.scenes[0].systems.userinput;
+ const transformSystem = AFRAME.scenes[0].systems["transform-selected-object"];
+ const physicsSystem = AFRAME.scenes[0].systems["hubs-systems"].physicsSystem;
+ if (
+ !transformSystem.transforming &&
+ hasComponent(world, HoveredRemoteRight, camera) &&
+ userinput.get(paths.device.mouse.buttonRight)
+ ) {
+ const rightCursor = anyEntityWith(world, RemoteRight);
+ physicsSystem.updateBodyOptions(Rigidbody.bodyId[camera], { type: "kinematic" });
+ transformSystem.startTransform(world.eid2obj.get(camera), world.eid2obj.get(rightCursor), {
+ mode: "cursor"
+ });
+ } else if (transformSystem.target?.eid === camera && !userinput.get(paths.device.mouse.buttonRight)) {
+ transformSystem.stopTransform();
+ }
+}
+
+const cameraToolQuery = defineQuery([CameraTool]);
+const cameraToolEnterQuery = enterQuery(cameraToolQuery);
+const cameraToolExitQuery = exitQuery(cameraToolQuery);
+
+export function cameraToolSystem(world) {
+ cameraToolEnterQuery(world).forEach(function(eid) {
+ const renderTarget = new THREE.WebGLRenderTarget(RENDER_WIDTH, RENDER_HEIGHT, {
+ format: THREE.RGBAFormat,
+ minFilter: THREE.LinearFilter,
+ magFilter: THREE.NearestFilter,
+ encoding: THREE.sRGBEncoding,
+ depth: false,
+ stencil: false
+ });
+ renderTarget.lastUpdated = 0;
+ renderTarget.needsUpdate = true;
+
+ // Only update the renderTarget when the screens are in view
+ function setRendertargetDirty() {
+ renderTarget.needsUpdate = true;
+ }
+
+ const screenObj = world.eid2obj.get(CameraTool.screenRef[eid]);
+ screenObj.material.map = renderTarget.texture;
+ screenObj.onBeforeRender = setRendertargetDirty;
+
+ const selfieScreenObj = world.eid2obj.get(CameraTool.selfieScreenRef[eid]);
+ selfieScreenObj.material.map = renderTarget.texture;
+ selfieScreenObj.onBeforeRender = setRendertargetDirty;
+
+ renderTargets.set(eid, renderTarget);
+ });
+
+ cameraToolExitQuery(world).forEach(function(eid) {
+ const renderTarget = renderTargets.get(eid);
+ renderTarget.dispose();
+ renderTargets.delete(eid);
+
+ const screenObj = world.eid2obj.get(CameraTool.screenRef[eid]);
+ screenObj.geometry.dispose();
+ screenObj.material.dispose();
+ });
+
+ cameraToolQuery(world).forEach((camera, i, allCameras) => {
+ rotateWithRightClick(world, camera);
+
+ if (CameraTool.trackTarget[camera]) {
+ if (entityExists(world, CameraTool.trackTarget[camera])) {
+ world.eid2obj.get(CameraTool.trackTarget[camera]).getWorldPosition(tmpVec3);
+ world.eid2obj.get(camera).lookAt(tmpVec3);
+ } else {
+ CameraTool.trackTarget[camera] = 0;
+ }
+ }
+
+ if (CameraTool.state[camera] === CAMERA_STATE.IDLE) {
+ if (clicked(CameraTool.snapRef[camera]) || grabberPressedSnapAction(world, camera)) {
+ CameraTool.state[camera] = CAMERA_STATE.COUNTDOWN_PHOTO;
+ CameraTool.snapTime[camera] = world.time.elapsed + 3000;
+ }
+
+ if (clicked(CameraTool.recVideoRef[camera])) {
+ CameraTool.state[camera] = CAMERA_STATE.COUNTDOWN_VIDEO;
+ CameraTool.snapTime[camera] = world.time.elapsed + 3000;
+ }
+
+ if (clicked(CameraTool.button_next[camera])) {
+ CameraTool.captureDurIdx[camera] = (CameraTool.captureDurIdx[camera] + 1) % CAPTURE_DURATIONS.length;
+ }
+
+ if (clicked(CameraTool.button_prev[camera])) {
+ CameraTool.captureDurIdx[camera] =
+ CameraTool.captureDurIdx[camera] === 0 ? CAPTURE_DURATIONS.length - 1 : CameraTool.captureDurIdx[camera] - 1;
+ }
+
+ if (clicked(CameraTool.sndToggleRef[camera])) {
+ captureAudio = !captureAudio;
+ }
+ }
+
+ if (
+ CameraTool.state[camera] === CAMERA_STATE.COUNTDOWN_PHOTO ||
+ CameraTool.state[camera] === CAMERA_STATE.COUNTDOWN_VIDEO
+ ) {
+ if (clicked(CameraTool.cancelRef[camera])) {
+ CameraTool.state[camera] = CAMERA_STATE.IDLE;
+ } else if (world.time.elapsed >= CameraTool.snapTime[camera]) {
+ if (CameraTool.state[camera] === CAMERA_STATE.COUNTDOWN_VIDEO) {
+ CameraTool.snapTime[camera] = world.time.elapsed + CAPTURE_DURATIONS[CameraTool.captureDurIdx[camera]] * 1000;
+ CameraTool.state[camera] = CAMERA_STATE.RECORDING_VIDEO;
+
+ const recorder = createRecorder(captureAudio);
+ videoRecorders.set(camera, recorder);
+ recorder.start();
+
+ APP.hubChannel.beginRecording();
+ } else {
+ CameraTool.state[camera] = CAMERA_STATE.SNAP_PHOTO;
+ }
+ } else {
+ // Floating point imprecision: Add epsilon to elapsed time so that the countdown sound always plays on the same frame that we click
+ const elapsed = world.time.elapsed + 0.01;
+ const msRemaining = CameraTool.snapTime[camera] - elapsed;
+ const msRemainingLastFrame = CameraTool.snapTime[camera] - (elapsed - world.time.delta);
+ if (Math.floor(msRemaining / 1000) !== Math.floor(msRemainingLastFrame / 1000)) {
+ AFRAME.scenes[0].systems["hubs-systems"].soundEffectsSystem.playSoundOneShot(SOUND_CAMERA_TOOL_COUNTDOWN);
+ }
+ }
+ }
+
+ // TODO we previously did this in tock() since we wanted to run it late in the frame
+ // We actually want to run this before the normal scene render otherwise the camera view is a frame behind.
+ // This is not really a big deal since we also run the camera at a lower FPS anyway
+ const renderTarget = renderTargets.get(camera);
+ const elapsed = world.time.elapsed;
+
+ // Update render target when taking a photo, recording a video, or round robbin for cameras in view
+ if (
+ CameraTool.state[camera] === CAMERA_STATE.SNAP_PHOTO ||
+ (CameraTool.state[camera] === CAMERA_STATE.RECORDING_VIDEO &&
+ elapsed > renderTarget.lastUpdated + VIDEO_UPDATE_RATE) ||
+ (renderTarget.needsUpdate &&
+ world.time.tick % allCameras.length === i &&
+ elapsed > renderTarget.lastUpdated + VIEWFINDER_UPDATE_RATE)
+ ) {
+ updateRenderTarget(world, camera);
+ if (CameraTool.state[camera] === CAMERA_STATE.RECORDING_VIDEO) {
+ videoRecorders.get(camera).captureFrame(renderTargets.get(camera));
+ }
+ }
+
+ if (CameraTool.state[camera] === CAMERA_STATE.SNAP_PHOTO) {
+ captureSnapshot(world, camera);
+ CameraTool.state[camera] = CAMERA_STATE.IDLE;
+ } else if (CameraTool.state[camera] === CAMERA_STATE.RECORDING_VIDEO) {
+ if (clicked(CameraTool.cancelRef[camera])) {
+ endRecording(world, camera, true);
+ CameraTool.state[camera] = CAMERA_STATE.IDLE;
+ } else if (world.time.elapsed >= CameraTool.snapTime[camera]) {
+ endRecording(world, camera, false);
+ CameraTool.state[camera] = CAMERA_STATE.IDLE;
+ }
+ }
+
+ updateUI(world, camera);
+ });
+}
diff --git a/src/components/avatar-inspect-collider.js b/src/components/avatar-inspect-collider.js
index 49728e8838..d24fe48fa6 100644
--- a/src/components/avatar-inspect-collider.js
+++ b/src/components/avatar-inspect-collider.js
@@ -5,11 +5,7 @@ AFRAME.registerComponent("avatar-inspect-collider", {
new THREE.Mesh(
new THREE.BoxGeometry(0.3, 0.8, 0.3),
new THREE.MeshBasicMaterial({
- depthWrite: false,
- opacity: 0,
- transparent: true,
- color: 0x020202,
- side: THREE.BackSide
+ visible: false
})
)
);
diff --git a/src/components/billboard.js b/src/components/billboard.js
index a03be5df36..fcfaee7787 100644
--- a/src/components/billboard.js
+++ b/src/components/billboard.js
@@ -1,6 +1,7 @@
const isMobileVR = AFRAME.utils.device.isMobileVR();
// Billboard component that only updates visible objects and only those in the camera view on mobile VR.
+// TODO billboarding assumes a single camera viewpoint but with video-texture-source, mirrors, and camera tools this is no longer valid
AFRAME.registerComponent("billboard", {
schema: {
onlyY: { type: "boolean" }
@@ -65,17 +66,6 @@ AFRAME.registerComponent("billboard", {
if (!this.playerCamera) return;
this.isInView = this.el.sceneEl.is("vr-mode") ? true : isInViewOfCamera(this.el.object3D, this.playerCamera);
-
- if (!this.isInView) {
- // Check in-game camera if rendering to viewfinder and owned
- const cameraTools = this.el.sceneEl.systems["camera-tools"];
-
- if (cameraTools) {
- cameraTools.ifMyCameraRenderingViewfinder(cameraTool => {
- this.isInView = this.isInView || isInViewOfCamera(this.el.object3D, cameraTool.camera);
- });
- }
- }
};
})(),
diff --git a/src/components/body-helper.js b/src/components/body-helper.js
index 50c988553d..c1c53f387e 100644
--- a/src/components/body-helper.js
+++ b/src/components/body-helper.js
@@ -1,8 +1,10 @@
+import { addComponent, removeComponent } from "bitecs";
import { CONSTANTS } from "three-ammo";
+import { Rigidbody } from "../bit-components";
const ACTIVATION_STATE = CONSTANTS.ACTIVATION_STATE,
TYPE = CONSTANTS.TYPE;
-const ACTIVATION_STATES = [
+export const ACTIVATION_STATES = [
ACTIVATION_STATE.ACTIVE_TAG,
ACTIVATION_STATE.ISLAND_SLEEPING,
ACTIVATION_STATE.WANTS_DEACTIVATION,
@@ -42,6 +44,9 @@ AFRAME.registerComponent("body-helper", {
init2: function() {
this.el.object3D.updateMatrices();
this.uuid = this.system.addBody(this.el.object3D, this.data);
+ const eid = this.el.object3D.eid;
+ addComponent(APP.world, Rigidbody, eid);
+ Rigidbody.bodyId[eid] = this.uuid; //uuid is a lie, it's actually an int
},
update: function(prevData) {
@@ -53,6 +58,8 @@ AFRAME.registerComponent("body-helper", {
remove: function() {
if (this.uuid !== -1) {
this.system.removeBody(this.uuid);
+ const eid = this.el.object3D.eid;
+ removeComponent(APP.world, Rigidbody, eid);
}
this.alive = false;
}
diff --git a/src/components/camera-focus-button.js b/src/components/camera-focus-button.js
index 1d0d3c9d33..3b58d63208 100644
--- a/src/components/camera-focus-button.js
+++ b/src/components/camera-focus-button.js
@@ -1,4 +1,8 @@
+import { CameraTool, MyCameraTool } from "../bit-components";
+import { anyEntityWith } from "../utils/bit-utils";
import { findComponentsInNearestAncestor } from "../utils/scene-graph";
+
+const tmpPos = new THREE.Vector3();
AFRAME.registerComponent("camera-focus-button", {
schema: {
track: { default: false },
@@ -6,8 +10,6 @@ AFRAME.registerComponent("camera-focus-button", {
},
init() {
- this.cameraSystem = this.el.sceneEl.systems["camera-tools"];
-
NAF.utils.getNetworkedEntity(this.el).then(networkedEl => {
if (this.data.selector) {
this.targetEl = networkedEl.querySelector(this.data.selector);
@@ -17,10 +19,16 @@ AFRAME.registerComponent("camera-focus-button", {
});
this.onClick = () => {
- const myCamera = this.cameraSystem.getMyCamera();
- if (!myCamera) return;
+ const myCam = anyEntityWith(APP.world, MyCameraTool);
+ if (!myCam) return;
- myCamera.components["camera-tool"].focus(this.targetEl, this.data.track);
+ if (this.data.track) {
+ const tracking = CameraTool.trackTarget[myCam];
+ CameraTool.trackTarget[myCam] = tracking === this.targetEl.eid ? 0 : this.targetEl.eid;
+ } else {
+ this.targetEl.object3D.getWorldPosition(tmpPos);
+ APP.world.eid2obj.get(myCam).lookAt(tmpPos);
+ }
};
this.menuPlacementRoots = findComponentsInNearestAncestor(this.el, "position-at-border");
@@ -28,10 +36,9 @@ AFRAME.registerComponent("camera-focus-button", {
tick() {
const isVisible = this.el.object3D.visible;
- const shouldBeVisible = !!(this.cameraSystem && this.cameraSystem.getMyCamera());
-
+ const shouldBeVisible = !!anyEntityWith(APP.world, MyCameraTool);
if (isVisible !== shouldBeVisible) {
- this.el.setAttribute("visible", shouldBeVisible);
+ this.el.object3D.visible = shouldBeVisible;
for (let i = 0; i < this.menuPlacementRoots.length; i++) {
this.menuPlacementRoots[i].markDirty();
}
diff --git a/src/components/camera-tool.js b/src/components/camera-tool.js
deleted file mode 100644
index 4a09727adb..0000000000
--- a/src/components/camera-tool.js
+++ /dev/null
@@ -1,819 +0,0 @@
-import { addAndArrangeMedia } from "../utils/media-utils";
-import { createImageBitmap } from "../utils/image-bitmap-utils";
-import { ObjectTypes } from "../object-types";
-import { paths } from "../systems/userinput/paths";
-import { SOUND_CAMERA_TOOL_TOOK_SNAPSHOT, SOUND_CAMERA_TOOL_COUNTDOWN } from "../systems/sound-effects-system";
-import { cloneObject3D } from "../utils/three-utils";
-import { loadModel } from "./gltf-model-plus";
-import { waitForDOMContentLoaded } from "../utils/async-utils";
-import cameraModelSrc from "../assets/camera_tool.glb";
-import anime from "animejs";
-import { Layers } from "./layers";
-const { detect } = require("detect-browser");
-
-const browser = detect();
-
-const isFirefox = browser.name === "firefox";
-
-const cameraModelPromise = waitForDOMContentLoaded().then(() => loadModel(cameraModelSrc));
-
-const pathsMap = {
- "player-right-controller": {
- takeSnapshot: paths.actions.rightHand.takeSnapshot
- },
- "player-left-controller": {
- takeSnapshot: paths.actions.leftHand.takeSnapshot
- },
- "right-cursor": {
- takeSnapshot: paths.actions.cursor.right.takeSnapshot
- },
- "left-cursor": {
- takeSnapshot: paths.actions.cursor.left.takeSnapshot
- }
-};
-
-const isMobileVR = AFRAME.utils.device.isMobileVR();
-
-const VIEWFINDER_FPS = 6;
-const VIDEO_FPS = 25;
-// Prefer h264 if available due to faster decoding speec on most platforms
-const videoCodec = ["h264", "vp9,opus", "vp8,opus", "vp9", "vp8"].find(
- codec => window.MediaRecorder && MediaRecorder.isTypeSupported(`video/webm; codecs=${codec}`)
-);
-const videoMimeType = videoCodec ? `video/webm; codecs=${videoCodec}` : null;
-const hasWebGL2 = !!document.createElement("canvas").getContext("webgl2");
-const allowVideo = !!videoMimeType && hasWebGL2;
-
-const isOculusBrowser = navigator.userAgent.match(/Oculus/);
-const CAPTURE_WIDTH = isMobileVR && !isOculusBrowser ? 640 : 1280;
-const CAPTURE_HEIGHT = isMobileVR && !isOculusBrowser ? 360 : 720;
-const RENDER_WIDTH = 1280;
-const RENDER_HEIGHT = 720;
-const CAPTURE_DURATIONS = [3, 7, 15, 30, 60, Infinity];
-const DEFAULT_CAPTURE_DURATION = 7;
-const COUNTDOWN_DURATION = 3;
-const VIDEO_LOOPS = 3; // Number of times to loop the videos we spawn before stopping them (for perf)
-const MAX_DURATION_TO_LIMIT_LOOPS = 31; // Max duration for which we limit loops (eg GIFs vs long form videos)
-
-const snapCanvas = document.createElement("canvas");
-
-async function pixelsToPNG(pixels, width, height) {
- snapCanvas.width = width;
- snapCanvas.height = height;
- const context = snapCanvas.getContext("2d");
-
- const imageData = context.createImageData(width, height);
- imageData.data.set(pixels);
- const bitmap = await createImageBitmap(imageData);
- context.scale(1, -1);
- context.drawImage(bitmap, 0, -height);
- const blob = await new Promise(resolve => snapCanvas.toBlob(resolve));
- return new File([blob], "snap.png", { type: "image/png" });
-}
-
-function blitFramebuffer(renderer, src, srcX0, srcY0, srcX1, srcY1, dest, dstX0, dstY0, dstX1, dstY1) {
- const gl = renderer.getContext();
-
- // Copies from one framebuffer to another. Note that at the end of this function, you need to restore
- // the original framebuffer via setRenderTarget
- const srcFramebuffer = renderer.properties.get(src).__webglFramebuffer;
- const destFramebuffer = renderer.properties.get(dest).__webglFramebuffer;
-
- if (srcFramebuffer && destFramebuffer) {
- gl.bindFramebuffer(gl.READ_FRAMEBUFFER, srcFramebuffer);
- gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, destFramebuffer);
-
- if (gl.checkFramebufferStatus(gl.READ_FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE) {
- gl.blitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, gl.COLOR_BUFFER_BIT, gl.LINEAR);
- }
- }
-}
-
-AFRAME.registerComponent("camera-tool", {
- schema: {
- captureDuration: { default: DEFAULT_CAPTURE_DURATION },
- captureAudio: { default: false },
- isSnapping: { default: false },
- isRecording: { default: false },
- label: { default: "" }
- },
-
- init() {
- this.el.object3D.visible = false; // Make invisible until model ready
- this.lastUpdate = performance.now();
- this.localSnapCount = 0; // Counter that is used to arrange photos/videos
-
- this.showCameraViewfinder = !isMobileVR;
-
- this.renderTarget = new THREE.WebGLRenderTarget(RENDER_WIDTH, RENDER_HEIGHT, {
- format: THREE.RGBAFormat,
- minFilter: THREE.LinearFilter,
- magFilter: THREE.NearestFilter,
- encoding: THREE.sRGBEncoding,
- depth: false,
- stencil: false
- });
-
- this.camera = new THREE.PerspectiveCamera(50, RENDER_WIDTH / RENDER_HEIGHT, 0.1, 30000);
- this.camera.layers.enable(Layers.CAMERA_LAYER_VIDEO_TEXTURE_TARGET);
- this.camera.layers.enable(Layers.CAMERA_LAYER_THIRD_PERSON_ONLY);
- this.camera.rotation.set(0, Math.PI, 0);
- this.camera.position.set(0, 0, 0.05);
- this.camera.matrixNeedsUpdate = true;
- this.el.setObject3D("camera", this.camera);
-
- const material = new THREE.MeshBasicMaterial({
- map: this.renderTarget.texture
- });
- material.toneMapped = false;
-
- // Only update the renderTarget when the screens are in view
- const onBeforeRender = () => {
- if (this.showCameraViewfinder) {
- this.viewfinderInViewThisFrame = true;
- }
- };
-
- this.el.sceneEl.addEventListener("stateadded", () => this.updateUI());
- this.el.sceneEl.addEventListener("stateremoved", () => this.updateUI());
-
- cameraModelPromise.then(model => {
- const mesh = cloneObject3D(model.scene);
- mesh.scale.set(2, 2, 2);
- mesh.matrixNeedsUpdate = true;
- this.el.setObject3D("mesh", mesh);
-
- this.el.object3D.visible = true;
- this.el.object3D.scale.set(0.5, 0.5, 0.5);
- this.el.object3D.matrixNeedsUpdate = true;
-
- const obj = this.el.object3D;
-
- const step = (function() {
- const lastValue = {};
- return function(anim) {
- const value = anim.animatables[0].target;
-
- value.x = Math.max(Number.MIN_VALUE, value.x);
- value.y = Math.max(Number.MIN_VALUE, value.y);
- value.z = Math.max(Number.MIN_VALUE, value.z);
-
- // For animation timeline.
- if (value.x === lastValue.x && value.y === lastValue.y && value.z === lastValue.z) {
- return;
- }
-
- lastValue.x = value.x;
- lastValue.y = value.y;
- lastValue.z = value.z;
-
- obj.scale.set(value.x, value.y, value.z);
- obj.matrixNeedsUpdate = true;
- };
- })();
-
- const config = {
- duration: 200,
- easing: "easeOutQuad",
- elasticity: 400,
- loop: 0,
- round: false,
- x: 1,
- y: 1,
- z: 1,
- targets: [{ x: 0.5, y: 0.5, z: 0.5 }],
- update: anim => step(anim),
- complete: anim => step(anim)
- };
-
- anime(config);
-
- const width = 0.28;
- const geometry = new THREE.PlaneBufferGeometry(width, width / this.camera.aspect);
-
- this.screen = new THREE.Mesh(geometry, material);
- this.screen.rotation.set(0, Math.PI, 0);
- this.screen.position.set(0, 0, -0.042);
- this.screen.onBeforeRender = onBeforeRender;
- this.screen.matrixNeedsUpdate = true;
- this.el.setObject3D("screen", this.screen);
-
- this.selfieScreen = new THREE.Mesh(geometry, material);
- this.selfieScreen.position.set(0, 0.4, 0);
- this.selfieScreen.scale.set(-2, 2, 2);
- this.selfieScreen.onBeforeRender = onBeforeRender;
- this.selfieScreen.matrixNeedsUpdate = true;
- this.el.setObject3D("selfieScreen", this.selfieScreen);
-
- this.label = this.el.querySelector(".label");
- this.labelActionBackground = this.el.querySelector(".label-action-background");
- this.labelBackground = this.el.querySelector(".label-background");
- this.durationLabel = this.el.querySelector(".duration");
-
- this.recordIcon = this.el.querySelector(".record-icon");
- this.recordAlphaIcon = this.el.querySelector(".record-alpha-icon");
-
- this.label.object3D.visible = false;
- this.durationLabel.object3D.visible = false;
-
- this.snapMenu = this.el.querySelector(".camera-snap-menu");
- this.snapButton = this.el.querySelector(".snap-button");
- this.recordButton = this.el.querySelector(".record-button");
-
- this.cancelButton = this.el.querySelector(".cancel-button");
- this.nextDurationButton = this.el.querySelector(".next-duration");
- this.prevDurationButton = this.el.querySelector(".prev-duration");
- this.snapButton.object3D.addEventListener("interact", () => this.snapClicked(true));
- this.recordButton.object3D.addEventListener("interact", () => this.snapClicked(false));
- this.cancelButton.object3D.addEventListener("interact", () => this.cancelSnapping());
- this.nextDurationButton.object3D.addEventListener("interact", () => this.changeDuration(1));
- this.prevDurationButton.object3D.addEventListener("interact", () => this.changeDuration(-1));
- this.stopButton = this.el.querySelector(".stop-button");
- this.stopButton.object3D.addEventListener("interact", () => this.stopRecording());
- this.captureAudioButton = this.el.querySelector(".capture-audio");
- this.captureAudioIcon = this.el.querySelector(".capture-audio-icon");
- this.captureAudioButton.object3D.addEventListener("interact", () =>
- this.el.setAttribute("camera-tool", "captureAudio", !this.data.captureAudio)
- );
-
- this.updateUI();
-
- this.updateRenderTargetNextTick = true;
-
- this.cameraSystem = this.el.sceneEl.systems["camera-tools"];
- this.cameraSystem.register(this.el);
-
- waitForDOMContentLoaded().then(() => {
- this.playerCamera = document.getElementById("viewing-camera").getObject3D("camera");
- });
- });
- },
-
- remove() {
- this.cameraSystem.deregister(this.el);
- this.stopRecording();
- },
-
- updateViewfinder() {
- this.updateRenderTargetNextTick = true;
- },
-
- changeDuration(delta) {
- const idx = CAPTURE_DURATIONS.findIndex(d => this.data.captureDuration == d);
- const newIdx = idx === 0 && delta === -1 ? CAPTURE_DURATIONS.length - 1 : (idx + delta) % CAPTURE_DURATIONS.length;
- this.el.setAttribute("camera-tool", "captureDuration", CAPTURE_DURATIONS[newIdx]);
- },
-
- focus(el, track) {
- if (track) {
- this.trackTarget = el;
- } else {
- this.trackTarget = null;
- }
-
- this.lookAt(el);
- },
-
- lookAt: (function() {
- const targetPos = new THREE.Vector3();
- return function(el) {
- targetPos.setFromMatrixPosition(el.object3D.matrixWorld);
- this.el.object3D.lookAt(targetPos);
- this.el.object3D.matrixNeedsUpdate = true;
- };
- })(),
-
- snapClicked(isPhoto) {
- if (this.data.isSnapping) return;
- if (!NAF.utils.isMine(this.el) && !NAF.utils.takeOwnership(this.el)) return;
-
- this.el.setAttribute("camera-tool", "isSnapping", true);
- this.el.sceneEl.systems["hubs-systems"].soundEffectsSystem.playSoundOneShot(SOUND_CAMERA_TOOL_COUNTDOWN);
-
- this.snapCountdown = COUNTDOWN_DURATION;
- this.el.setAttribute("camera-tool", "label", `${this.snapCountdown}`);
-
- this.countdownInterval = setInterval(async () => {
- if (!NAF.utils.isMine(this.el) && !NAF.utils.takeOwnership(this.el)) {
- clearInterval(this.countdownInterval);
- this.countdownInterval = null;
- return;
- }
-
- this.snapCountdown--;
-
- if (this.snapCountdown === 0) {
- clearInterval(this.countdownInterval);
- this.countdownInterval = null;
-
- if (isPhoto) {
- this.el.setAttribute("camera-tool", { label: "", isSnapping: false });
- this.takeSnapshotNextTick = true;
- } else {
- this.beginRecording(this.data.captureDuration);
- }
- } else {
- this.el.sceneEl.systems["hubs-systems"].soundEffectsSystem.playSoundOneShot(SOUND_CAMERA_TOOL_COUNTDOWN);
- this.el.setAttribute("camera-tool", "label", `${this.snapCountdown}`);
- }
- }, 1000);
- },
-
- cancelSnapping() {
- clearInterval(this.countdownInterval);
- this.stopRecording(true);
- this.updateUI();
- },
-
- update() {
- this.updateUI();
- },
-
- updateUI() {
- if (!this.label) return;
-
- const label = this.data.label;
- const isFrozen = this.el.sceneEl.is("frozen");
- const hasDuration = this.data.captureDuration !== Infinity;
-
- const isRecordingUnbound = !hasDuration && this.data.isRecording && this.videoRecorder;
- this.label.object3D.visible = !!label && !isRecordingUnbound;
- const showActionLabelBackground = this.label.object3D.visible && this.data.isRecording && !isRecordingUnbound;
-
- this.label.setAttribute("text", { value: label, color: showActionLabelBackground ? "#fafafa" : "#ff3464" });
-
- this.labelActionBackground.object3D.visible = showActionLabelBackground;
- this.labelBackground.object3D.visible = this.label.object3D.visible && !showActionLabelBackground;
-
- this.stopButton.object3D.visible = isRecordingUnbound;
-
- if (hasDuration) {
- this.durationLabel.setAttribute("text", "value", `${this.data.captureDuration}`);
- }
-
- this.durationLabel.object3D.visible = hasDuration && !this.data.isSnapping && !isFrozen && allowVideo;
- this.recordIcon.object3D.visible = !hasDuration && !isFrozen && allowVideo;
- this.recordAlphaIcon.object3D.visible = hasDuration && !isFrozen && allowVideo;
- this.snapButton.object3D.visible = !this.data.isSnapping && !isFrozen;
- this.recordButton.object3D.visible = !this.data.isSnapping && !isFrozen && allowVideo;
- this.cancelButton.object3D.visible = this.data.isSnapping && !isFrozen;
- this.prevDurationButton.object3D.visible = this.nextDurationButton.object3D.visible =
- !this.data.isSnapping && allowVideo && !isFrozen && allowVideo;
-
- this.captureAudioIcon.setAttribute("icon-button", "active", this.data.captureAudio);
- },
-
- async beginRecording(duration) {
- if (!this.videoContext) {
- this.videoCanvas = document.createElement("canvas");
- this.videoCanvas.width = CAPTURE_WIDTH;
- this.videoCanvas.height = CAPTURE_HEIGHT;
- this.videoContext = this.videoCanvas.getContext("2d");
- this.videoImageData = this.videoContext.createImageData(CAPTURE_WIDTH, CAPTURE_HEIGHT);
- this.videoPixels = new Uint8Array(CAPTURE_WIDTH * CAPTURE_HEIGHT * 4);
- this.videoImageData.data.set(this.videoPixels);
- }
-
- const stream = new MediaStream();
- const track = this.videoCanvas.captureStream(VIDEO_FPS).getVideoTracks()[0];
-
- // This adds hacks for current browser issues with media recordings when audio tracks are muted or missing.
- const attachBlankAudio = () => {
- // Chrome has issues when the audio tracks are silent so we only do this for FF.
- // https://bugs.chromium.org/p/chromium/issues/detail?id=1223382
- if (isFirefox) {
- // FF 73+ seems to fail to decode videos with no audio track, so we always include a silent track.
- const context = THREE.AudioContext.getContext();
- const oscillator = context.createOscillator();
- const gain = context.createGain();
- const destination = context.createMediaStreamDestination();
- gain.gain.setValueAtTime(0.0001, context.currentTime);
- oscillator.connect(destination);
- gain.connect(destination);
- stream.addTrack(destination.stream.getAudioTracks()[0]);
- }
- };
-
- if (this.data.captureAudio) {
- const selfAudio = await APP.dialog.getMediaStream(NAF.clientId, "audio");
-
- // NOTE: if we don't have a self audio track, we can end up generating an empty video (browser bug?)
- // if no audio comes through on the listener source. (Eg the room is otherwise silent.)
- // So for now, if we don't have a track, just disable audio capture.
- if (selfAudio && selfAudio.getAudioTracks().length > 0) {
- const context = THREE.AudioContext.getContext();
- const destination = context.createMediaStreamDestination();
-
- const listener = this.el.sceneEl.audioListener;
- if (listener) {
- // NOTE audio is not captured from camera vantage point for now.
- listener.getInput().connect(destination);
- }
- context.createMediaStreamSource(selfAudio).connect(destination);
-
- const audio = destination.stream.getAudioTracks()[0];
- stream.addTrack(audio);
- } else {
- attachBlankAudio();
- }
- } else {
- attachBlankAudio();
- }
-
- stream.addTrack(track);
- this.videoRecorder = new MediaRecorder(stream, { mimeType: videoMimeType });
- const chunks = [];
- const recordingStartTime = performance.now();
-
- this.videoRecorder.ondataavailable = e => chunks.push(e.data);
-
- this.updateRenderTargetNextTick = true;
-
- this.videoRecorder.start();
- this.el.setAttribute("camera-tool", { isRecording: true, label: " " });
- this.el.sceneEl.emit("action_camera_recording_started");
-
- if (duration !== Infinity) {
- this.videoCountdown = this.data.captureDuration;
- this.el.setAttribute("camera-tool", "label", `${this.videoCountdown}`);
-
- this.videoCountdownInterval = setInterval(() => {
- this.videoCountdown--;
-
- if (this.videoCountdown === 0) {
- this.stopRecording();
- this.videoCountdownInterval = null;
- } else {
- this.el.setAttribute("camera-tool", "label", `${this.videoCountdown}`);
- }
- }, 1000);
- }
-
- this.videoRecorder._free = () => (chunks.length = 0); // Used for cancelling
- this.videoRecorder.onstop = async () => {
- this.el.sceneEl.emit("action_camera_recording_ended");
-
- if (chunks.length === 0) return;
- const mimeType = chunks[0].type;
- const recordingDuration = performance.now() - recordingStartTime;
- const blob = new Blob(chunks, { type: mimeType });
-
- chunks.length = 0;
-
- const { entity, orientation } = addAndArrangeMedia(
- this.el,
- new File([blob], "capture", { type: mimeType.split(";")[0] }), // Drop codec
- "video-camera",
- this.localSnapCount,
- !!this.playerIsBehindCamera
- );
-
- entity.addEventListener(
- "video-loaded",
- () => {
- // If we were recording audio, then pause the video immediately after starting.
- //
- // Or, to limit the # of concurrent videos playing, if it was a short clip, let it loop
- // a few times and then pause it.
- if (this.data.captureAudio || recordingDuration <= MAX_DURATION_TO_LIMIT_LOOPS * 1000) {
- setTimeout(() => {
- if (!NAF.utils.isMine(entity) && !NAF.utils.takeOwnership(entity)) return;
- entity.components["media-video"].tryUpdateVideoPlaybackState(true);
- }, this.data.captureAudio ? 0 : recordingDuration * VIDEO_LOOPS + 100);
- }
- },
- { once: true }
- );
-
- this.localSnapCount++;
-
- orientation.then(() => this.el.sceneEl.emit("object_spawned", { objectType: ObjectTypes.CAMERA }));
- };
- },
-
- stopRecording(cancel) {
- if (this.videoRecorder) {
- if (cancel) {
- this.videoRecorder.onstop = () => {};
- this.videoRecorder._free();
- this.el.sceneEl.emit("action_camera_recording_ended");
- }
-
- this.videoRecorder.stop();
- this.videoRecorder = null;
-
- if (!cancel) {
- this.el.sceneEl.systems["hubs-systems"].soundEffectsSystem.playSoundOneShot(SOUND_CAMERA_TOOL_TOOK_SNAPSHOT);
- }
- }
- clearInterval(this.videoCountdownInterval);
-
- this.videoCountdownInterval = null;
-
- this.el.setAttribute("camera-tool", { label: " ", isRecording: false, isSnapping: false });
- },
-
- tick() {
- const interaction = AFRAME.scenes[0].systems.interaction;
- const userinput = AFRAME.scenes[0].systems.userinput;
- const heldLeftHand = interaction.state.leftHand.held === this.el;
- const heldRightHand = interaction.state.rightHand.held === this.el;
- const heldRightRemote = interaction.state.rightRemote.held === this.el;
-
- const heldThisFrame =
- (heldLeftHand && userinput.get(interaction.options.leftHand.grabPath)) ||
- (heldRightHand && userinput.get(interaction.options.rightHand.grabPath)) ||
- (heldRightRemote && userinput.get(interaction.options.rightRemote.grabPath));
-
- const isHolding = heldLeftHand || heldRightHand || heldRightRemote;
-
- this.updateSnapMenuOrientation();
-
- if (heldThisFrame) {
- this.localSnapCount = 0;
- }
-
- this.showCameraViewfinder = isHolding || !isMobileVR || this.data.isSnapping || this.videoRecorder;
-
- if (this.screen && this.selfieScreen) {
- this.screen.visible = this.selfieScreen.visible = !!this.showCameraViewfinder;
- }
-
- // Always draw held, snapping, or recording camera viewfinders with a decent framerate
- if (
- (isHolding || this.data.isSnapping || this.videoRecorder) &&
- performance.now() - this.lastUpdate >= 1000 / (this.videoRecorder ? VIDEO_FPS : VIEWFINDER_FPS)
- ) {
- this.updateRenderTargetNextTick = true;
- }
-
- const isHoldingTrigger = this.isHoldingSnapshotTrigger();
- const isPermittedToUse = window.APP.hubChannel.can("spawn_camera");
-
- if (isPermittedToUse) {
- // If the user lets go of the trigger before 500ms, take a picture, otherwise record until they let go.
- if (isHoldingTrigger && !this.data.isSnapping && !this.snapTriggerTimeout) {
- this.snapTriggerTimeout = setTimeout(() => {
- if (this.isHoldingSnapshotTrigger()) {
- this.el.setAttribute("camera-tool", "isSnapping", true);
- this.beginRecording(Infinity);
-
- const releaseInterval = setInterval(() => {
- if (this.isHoldingSnapshotTrigger()) return;
- this.stopRecording();
- clearInterval(releaseInterval);
- }, 500);
- }
-
- this.snapTriggerTimeout = null;
- }, 500);
- } else if (!isHoldingTrigger && !this.data.isSnapping && this.snapTriggerTimeout) {
- clearTimeout(this.snapTriggerTimeout);
- this.snapTriggerTimeout = null;
- this.takeSnapshotNextTick = true;
- }
- }
- },
-
- tock: (function() {
- return function tock() {
- const sceneEl = this.el.sceneEl;
- const renderer = this.renderer || sceneEl.renderer;
- const now = performance.now();
-
- // Perform lookAt in tock so it will re-orient after grabs, etc.
- if (this.trackTarget) {
- if (this.trackTarget.parentNode) {
- this.lookAt(this.trackTarget);
- // scene.autoUpdate will be false so explicitly update the world matrices
- this.object3D.updateMatrixWorld();
- } else {
- this.trackTarget = null; // Target removed
- }
- }
-
- if (!this.playerHud) {
- const hudEl = document.getElementById("player-hud");
- this.playerHud = hudEl && hudEl.object3D;
- }
-
- if (
- this.takeSnapshotNextTick ||
- (this.updateRenderTargetNextTick && (this.viewfinderInViewThisFrame || this.videoRecorder))
- ) {
- let playerHudWasVisible = false;
-
- if (this.playerHud) {
- playerHudWasVisible = this.playerHud.visible;
- this.playerHud.visible = false;
- if (this.el.sceneEl.systems["hubs-systems"]) {
- for (const mesh of Object.values(this.el.sceneEl.systems["hubs-systems"].spriteSystem.meshes)) {
- mesh.visible = false;
- }
- }
- }
-
- const bubbleSystem = this.el.sceneEl.systems["personal-space-bubble"];
- const boneVisibilitySystem = this.el.sceneEl.systems["hubs-systems"].boneVisibilitySystem;
-
- if (bubbleSystem) {
- for (let i = 0, l = bubbleSystem.invaders.length; i < l; i++) {
- bubbleSystem.invaders[i].disable();
- }
- // HACK, bone visibility typically takes a tick to update, but since we want to be able
- // to have enable() and disable() be reflected this frame, we need to do it immediately.
- boneVisibilitySystem.tick();
- // scene.autoUpdate will be false so explicitly update the world matrices
- boneVisibilitySystem.updateMatrices();
- }
-
- const tmpVRFlag = renderer.xr.enabled;
- const tmpOnAfterRender = sceneEl.object3D.onAfterRender;
- const tmpAutoUpdate = sceneEl.object3D.autoUpdate;
- delete sceneEl.object3D.onAfterRender;
- renderer.xr.enabled = false;
-
- // The entire scene graph matrices should already be updated
- // in tick(). They don't need to be recomputed again in tock().
- sceneEl.object3D.autoUpdate = false;
-
- if (allowVideo && this.videoRecorder && !this.videoRenderTarget) {
- // Create a separate render target for video because we need to flip and (sometimes) downscale it before
- // encoding it to video.
- this.videoRenderTarget = new THREE.WebGLRenderTarget(CAPTURE_WIDTH, CAPTURE_HEIGHT, {
- format: THREE.RGBAFormat,
- minFilter: THREE.LinearFilter,
- magFilter: THREE.NearestFilter,
- encoding: THREE.sRGBEncoding,
- depth: false,
- stencil: false
- });
-
- // Used to set up framebuffer in three.js as a side effect
- renderer.setRenderTarget(this.videoRenderTarget);
- }
-
- renderer.setRenderTarget(this.renderTarget);
- renderer.render(sceneEl.object3D, this.camera);
- renderer.setRenderTarget(null);
-
- renderer.xr.enabled = tmpVRFlag;
- sceneEl.object3D.onAfterRender = tmpOnAfterRender;
- sceneEl.object3D.autoUpdate = tmpAutoUpdate;
-
- if (this.playerHud) {
- this.playerHud.visible = playerHudWasVisible;
- if (this.el.sceneEl.systems["hubs-systems"]) {
- for (const mesh of Object.values(this.el.sceneEl.systems["hubs-systems"].spriteSystem.meshes)) {
- mesh.visible = true;
- }
- }
- }
-
- if (bubbleSystem) {
- for (let i = 0, l = bubbleSystem.invaders.length; i < l; i++) {
- bubbleSystem.invaders[i].enable();
- }
- // HACK, bone visibility typically takes a tick to update, but since we want to be able
- // to have enable() and disable() be reflected this frame, we need to do it immediately.
- boneVisibilitySystem.tick();
- boneVisibilitySystem.updateMatrices();
- }
-
- this.lastUpdate = now;
-
- if (this.videoRecorder) {
- // https://chromium.googlesource.com/chromium/src/gpu/+/master/command_buffer/service/gles2_cmd_decoder.cc#8899
- // We avoid using blitting and flip the render target pixels for OB.
- if (!isOculusBrowser) {
- // This blit operation will (if necessary) scale/resample the view finder render target and, importantly,
- // flip the texture on Y
- blitFramebuffer(
- renderer,
- this.renderTarget,
- 0,
- 0,
- RENDER_WIDTH,
- RENDER_HEIGHT,
- this.videoRenderTarget,
- 0,
- CAPTURE_HEIGHT,
- CAPTURE_WIDTH,
- 0
- );
- }
- renderer.readRenderTargetPixels(
- !isOculusBrowser ? this.videoRenderTarget : this.renderTarget,
- 0,
- 0,
- CAPTURE_WIDTH,
- CAPTURE_HEIGHT,
- this.videoPixels
- );
- if (isOculusBrowser) {
- this.flipPixelsY(this.videoPixels, CAPTURE_WIDTH, CAPTURE_HEIGHT);
- }
- this.videoImageData.data.set(this.videoPixels);
- this.videoContext.putImageData(this.videoImageData, 0, 0);
- }
-
- this.updateRenderTargetNextTick = false;
- this.viewfinderInViewThisFrame = false;
- }
-
- if (this.takeSnapshotNextTick) {
- if (!this.snapPixels) {
- this.snapPixels = new Uint8Array(RENDER_WIDTH * RENDER_HEIGHT * 4);
- }
- renderer.readRenderTargetPixels(this.renderTarget, 0, 0, RENDER_WIDTH, RENDER_HEIGHT, this.snapPixels);
-
- pixelsToPNG(this.snapPixels, RENDER_WIDTH, RENDER_HEIGHT).then(file => {
- const { orientation } = addAndArrangeMedia(
- this.el,
- file,
- "photo-camera",
- this.localSnapCount,
- !!this.playerIsBehindCamera
- );
-
- orientation.then(() => {
- this.el.sceneEl.emit("object_spawned", { objectType: ObjectTypes.CAMERA });
- });
- });
- sceneEl.systems["hubs-systems"].soundEffectsSystem.playSoundOneShot(SOUND_CAMERA_TOOL_TOOK_SNAPSHOT);
- this.takeSnapshotNextTick = false;
- this.localSnapCount++;
- }
- };
- })(),
-
- flipPixelsY(pixels, width, height) {
- const halfHeight = (height / 2) | 0;
- const bytesPerRow = width * 4;
-
- const temp = new Uint8Array(width * 4);
- for (let y = 0; y < halfHeight; ++y) {
- const topOffset = y * bytesPerRow;
- const bottomOffset = (height - y - 1) * bytesPerRow;
- temp.set(pixels.subarray(topOffset, topOffset + bytesPerRow));
- pixels.copyWithin(topOffset, bottomOffset, bottomOffset + bytesPerRow);
- pixels.set(temp, bottomOffset);
- }
- },
-
- isHoldingSnapshotTrigger: function() {
- const interaction = AFRAME.scenes[0].systems.interaction;
- const userinput = AFRAME.scenes[0].systems.userinput;
- const heldLeftHand = interaction.state.leftHand.held === this.el;
- const heldRightHand = interaction.state.rightHand.held === this.el;
- const heldRightRemote = interaction.state.rightRemote.held === this.el;
- const heldLeftRemote = interaction.state.leftRemote.held === this.el;
-
- let grabberId;
- if (heldRightHand) {
- grabberId = "player-right-controller";
- } else if (heldLeftHand) {
- grabberId = "player-left-controller";
- } else if (heldRightRemote) {
- grabberId = "right-cursor";
- } else if (heldLeftRemote) {
- grabberId = "left-cursor";
- }
-
- if (grabberId) {
- const grabberPaths = pathsMap[grabberId];
- if (userinput.get(grabberPaths.takeSnapshot)) {
- return true;
- }
- }
-
- return !!userinput.get(paths.actions.takeSnapshot);
- },
-
- updateSnapMenuOrientation: (function() {
- const playerWorld = new THREE.Vector3();
- const cameraWorld = new THREE.Vector3();
- const playerToCamera = new THREE.Vector3();
- const cameraForwardPoint = new THREE.Vector3();
- const cameraForwardWorld = new THREE.Vector3();
- return function() {
- if (!this.playerCamera) return;
- this.el.object3D.getWorldPosition(cameraWorld);
- this.playerCamera.getWorldPosition(playerWorld);
- playerToCamera.subVectors(playerWorld, cameraWorld);
- cameraForwardPoint.set(0, 0, 1);
- this.el.object3D.localToWorld(cameraForwardPoint);
- cameraForwardWorld.subVectors(cameraForwardPoint, cameraWorld);
- cameraForwardWorld.normalize();
- playerToCamera.normalize();
-
- const playerIsBehindCamera = cameraForwardWorld.dot(playerToCamera) < 0;
-
- if (this.playerIsBehindCamera !== playerIsBehindCamera) {
- this.playerIsBehindCamera = playerIsBehindCamera;
- this.snapMenu.object3D.rotation.set(0, this.playerIsBehindCamera ? Math.PI : 0, 0);
- this.snapMenu.object3D.matrixNeedsUpdate = true;
- }
- };
- })()
-});
diff --git a/src/components/cursor-controller.js b/src/components/cursor-controller.js
index ae8896b05c..3f33483d8f 100644
--- a/src/components/cursor-controller.js
+++ b/src/components/cursor-controller.js
@@ -1,6 +1,28 @@
+import { addComponent, defineQuery, hasComponent, removeComponent } from "bitecs";
+import { anyEntityWith } from "../utils/bit-utils";
+import {
+ HeldRemoteLeft,
+ HeldRemoteRight,
+ HoveredRemoteLeft,
+ HoveredRemoteRight,
+ NotRemoteHoverTarget,
+ RemoteHoverTarget
+} from "../bit-components";
import { paths } from "../systems/userinput/paths";
import { sets } from "../systems/userinput/sets";
import { getLastWorldPosition } from "../utils/three-utils";
+import { Layers } from "./layers";
+
+export function findRemoteHoverTarget(world, object3D) {
+ if (!object3D) return null;
+ if (!object3D.eid) return findRemoteHoverTarget(world, object3D.parent);
+ if (hasComponent(world, NotRemoteHoverTarget, object3D.eid)) return null;
+ if (hasComponent(world, RemoteHoverTarget, object3D.eid)) return object3D.eid;
+ return findRemoteHoverTarget(world, object3D.parent);
+}
+
+const hoveredRightRemoteQuery = defineQuery([HoveredRemoteRight]);
+const hoveredLeftRemoteQuery = defineQuery([HoveredRemoteLeft]);
const HIGHLIGHT = new THREE.Color(23 / 255, 64 / 255, 118 / 255);
const NO_HIGHLIGHT = new THREE.Color(190 / 255, 190 / 255, 190 / 255);
@@ -76,6 +98,7 @@ AFRAME.registerComponent("cursor-controller", {
this.cursorVisual.renderOrder = window.APP.RENDER_ORDER.CURSOR;
this.cursorVisual.material.transparent = true;
+ this.cursorVisual.layers.set(Layers.CAMERA_LAYER_UI);
this.data.cursor.object3D.add(this.cursorVisual);
this.intersection = null;
@@ -133,8 +156,8 @@ AFRAME.registerComponent("cursor-controller", {
this.raycaster.far = this.data.far * playerScale;
this.raycaster.near = this.data.near * playerScale;
- const interaction = AFRAME.scenes[0].systems.interaction;
- const isGrabbing = left ? !!interaction.state.leftRemote.held : !!interaction.state.rightRemote.held;
+ const isGrabbing = left ? anyEntityWith(APP.world, HeldRemoteLeft) : anyEntityWith(APP.world, HeldRemoteRight);
+ let isHoveringSomething = false;
if (!isGrabbing) {
rawIntersections.length = 0;
this.raycaster.ray.origin = cursorPose.position;
@@ -145,8 +168,20 @@ AFRAME.registerComponent("cursor-controller", {
rawIntersections
);
this.intersection = rawIntersections[0];
- this.intersectionIsValid = !!interaction.updateCursorIntersection(this.intersection, left);
- this.distance = this.intersectionIsValid ? this.intersection.distance : this.data.defaultDistance * playerScale;
+
+ const remoteHoverTarget = this.intersection && findRemoteHoverTarget(APP.world, this.intersection.object);
+ isHoveringSomething = !!remoteHoverTarget;
+ if (remoteHoverTarget) {
+ addComponent(APP.world, left ? HoveredRemoteLeft : HoveredRemoteRight, remoteHoverTarget);
+ }
+ const hovered = left ? hoveredLeftRemoteQuery(APP.world) : hoveredRightRemoteQuery(APP.world);
+ for (let i = 0; i < hovered.length; i++) {
+ // Unhover anything that should no longer be hovered
+ if (remoteHoverTarget !== hovered[i]) {
+ removeComponent(APP.world, left ? HoveredRemoteLeft : HoveredRemoteRight, hovered[i]);
+ }
+ }
+ this.distance = remoteHoverTarget ? this.intersection.distance : this.data.defaultDistance * playerScale;
}
const { cursor, minDistance, far, camera } = this.data;
@@ -171,7 +206,7 @@ AFRAME.registerComponent("cursor-controller", {
(!left && transformObjectSystem.hand.el.id === "player-right-controller"))
) {
this.color.copy(TRANSFORM_COLOR_1).lerpHSL(TRANSFORM_COLOR_2, 0.5 + 0.5 * Math.sin(t / 1000.0));
- } else if (this.intersectionIsValid || isGrabbing) {
+ } else if (isGrabbing || isHoveringSomething) {
this.color.copy(HIGHLIGHT);
} else {
this.color.copy(NO_HIGHLIGHT);
diff --git a/src/components/destroy-at-extreme-distances.js b/src/components/destroy-at-extreme-distances.js
deleted file mode 100644
index 34c53e4cac..0000000000
--- a/src/components/destroy-at-extreme-distances.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { getLastWorldPosition } from "../utils/three-utils";
-
-AFRAME.registerComponent("destroy-at-extreme-distances", {
- schema: {
- xMin: { default: -1000 },
- xMax: { default: 1000 },
- yMin: { default: -1000 },
- yMax: { default: 1000 },
- zMin: { default: -1000 },
- zMax: { default: 1000 }
- },
-
- init() {
- this._checkForDestroy = this._checkForDestroy.bind(this);
- this.el.sceneEl.systems["frame-scheduler"].schedule(this._checkForDestroy, "media-components");
- },
-
- remove() {
- this.el.sceneEl.systems["frame-scheduler"].unschedule(this._checkForDestroy, "media-components");
- },
-
- _checkForDestroy: (function() {
- const pos = new THREE.Vector3();
- return function() {
- const { xMin, xMax, yMin, yMax, zMin, zMax } = this.data;
- getLastWorldPosition(this.el.object3D, pos);
-
- if (pos.x < xMin || pos.x > xMax || pos.y < yMin || pos.y > yMax || pos.z < zMin || pos.z > zMax) {
- if (!this.el.components.networked || NAF.utils.isMine(this.el)) {
- this.el.parentNode.removeChild(this.el);
- }
- }
- };
- })()
-});
diff --git a/src/components/emoji-hud.js b/src/components/emoji-hud.js
index bca0b904b6..9d3a0d8f95 100644
--- a/src/components/emoji-hud.js
+++ b/src/components/emoji-hud.js
@@ -1,4 +1,6 @@
+import { addComponent, removeComponent } from "bitecs";
import { TYPE } from "three-ammo/constants";
+import { HandCollisionTarget } from "../bit-components";
import { emojis } from "./emoji";
const COLLISION_LAYERS = require("../constants").COLLISION_LAYERS;
@@ -114,7 +116,7 @@ AFRAME.registerComponent("emoji-hud", {
if (e.detail === "frozen") {
this._updateOffset();
for (let i = 0; i < this.spawnerEntities.length; i++) {
- this.spawnerEntities[i].components.tags.data.isHandCollisionTarget = true;
+ addComponent(APP.world, HandCollisionTarget, this.spawnerEntities[i].eid);
}
}
},
@@ -122,7 +124,7 @@ AFRAME.registerComponent("emoji-hud", {
_onThaw(e) {
if (e.detail === "frozen") {
for (let i = 0; i < this.spawnerEntities.length; i++) {
- this.spawnerEntities[i].components.tags.data.isHandCollisionTarget = false;
+ removeComponent(APP.world, HandCollisionTarget, this.spawnerEntities[i].eid);
}
}
},
diff --git a/src/components/floaty-object.js b/src/components/floaty-object.js
index f37f6afcec..54c8aa0e4f 100644
--- a/src/components/floaty-object.js
+++ b/src/components/floaty-object.js
@@ -1,14 +1,10 @@
+import { addComponent } from "bitecs";
+import { FloatyObject } from "../bit-components";
+import { FLOATY_OBJECT_FLAGS } from "../systems/floaty-object-system";
/* global AFRAME */
-const COLLISION_LAYERS = require("../constants").COLLISION_LAYERS;
AFRAME.registerComponent("floaty-object", {
schema: {
- // Make the object locked/kinematic upon load
- autoLockOnLoad: { default: false },
-
- // Make the object kinematic immediately upon release
- autoLockOnRelease: { default: false },
-
// On release, modify the gravity based upon gravitySpeedLimit. If less than this, let the object float
// otherwise apply releaseGravity.
modifyGravityOnRelease: { default: false },
@@ -19,124 +15,19 @@ AFRAME.registerComponent("floaty-object", {
// If true, the degree to which angular rotation is allowed when floating is reduced (useful for 2d media)
reduceAngularFloat: { default: false },
- // Velocity speed limit under which gravity will not be added if modifyGravityOnRelease is true
- gravitySpeedLimit: { default: 1.85 } // Set to 0 to never apply gravity
+ // If true, the object will behave the same regardless of how fast it was moving when released
+ unthrowable: { default: false }
},
init() {
- this.onGrab = this.onGrab.bind(this);
- this.onRelease = this.onRelease.bind(this);
- },
-
- tick() {
- if (!this.bodyHelper) {
- this.bodyHelper = this.el.components["body-helper"];
- }
-
- const interaction = AFRAME.scenes[0].systems.interaction;
- const isHeld = interaction && interaction.isHeld(this.el);
- if (isHeld && !this.wasHeld) {
- this.onGrab();
- }
- if (this.wasHeld && !isHeld) {
- this.onRelease();
- }
-
- if (!isHeld && this._makeStaticWhenAtRest) {
- const physicsSystem = this.el.sceneEl.systems["hubs-systems"].physicsSystem;
- const isMine = this.el.components.networked && NAF.utils.isMine(this.el);
- const linearThreshold = this.bodyHelper.data.linearSleepingThreshold;
- const angularThreshold = this.bodyHelper.data.angularSleepingThreshold;
- const uuid = this.bodyHelper.uuid;
- const isAtRest =
- physicsSystem.bodyInitialized(uuid) &&
- physicsSystem.getLinearVelocity(uuid) < linearThreshold &&
- physicsSystem.getAngularVelocity(uuid) < angularThreshold;
-
- if (isAtRest && isMine) {
- this.el.setAttribute("body-helper", { type: "kinematic" });
- }
-
- if (isAtRest || !isMine) {
- this._makeStaticWhenAtRest = false;
- }
- }
-
- this.wasHeld = isHeld;
- },
-
- play() {
- // We do this in play instead of in init because otherwise NAF.utils.isMine fails
- if (this.hasBeenHereBefore) return;
- this.hasBeenHereBefore = true;
- if (this.data.autoLockOnLoad) {
- this.el.setAttribute("body-helper", {
- gravity: { x: 0, y: 0, z: 0 }
- });
- this.setLocked(true);
- }
- },
-
- setLocked(locked) {
- if (this.el.components.networked && !NAF.utils.isMine(this.el)) return;
-
- this.locked = locked;
- this.el.setAttribute("body-helper", { type: locked ? "kinematic" : "dynamic" });
- },
-
- onRelease() {
- if (this.data.modifyGravityOnRelease) {
- const uuid = this.bodyHelper.uuid;
- const physicsSystem = this.el.sceneEl.systems["hubs-systems"].physicsSystem;
- if (
- this.data.gravitySpeedLimit === 0 ||
- (physicsSystem.bodyInitialized(uuid) && physicsSystem.getLinearVelocity(uuid) < this.data.gravitySpeedLimit)
- ) {
- this.el.setAttribute("body-helper", {
- gravity: { x: 0, y: 0, z: 0 },
- angularDamping: this.data.reduceAngularFloat ? 0.98 : 0.5,
- linearDamping: 0.95,
- linearSleepingThreshold: 0.1,
- angularSleepingThreshold: 0.1,
- collisionFilterMask: COLLISION_LAYERS.HANDS | COLLISION_LAYERS.MEDIA_FRAMES
- });
-
- this._makeStaticWhenAtRest = true;
- } else {
- this.el.setAttribute("body-helper", {
- gravity: { x: 0, y: this.data.releaseGravity, z: 0 },
- angularDamping: 0.01,
- linearDamping: 0.01,
- linearSleepingThreshold: 1.6,
- angularSleepingThreshold: 2.5,
- collisionFilterMask: COLLISION_LAYERS.DEFAULT_INTERACTABLE
- });
- }
- } else {
- this.el.setAttribute("body-helper", {
- collisionFilterMask: COLLISION_LAYERS.DEFAULT_INTERACTABLE,
- gravity: { x: 0, y: -9.8, z: 0 }
- });
- }
-
- if (this.data.autoLockOnRelease) {
- this.setLocked(true);
- }
- },
-
- onGrab() {
- this.el.setAttribute("body-helper", {
- gravity: { x: 0, y: 0, z: 0 },
- collisionFilterMask: COLLISION_LAYERS.HANDS | COLLISION_LAYERS.MEDIA_FRAMES
- });
- this.setLocked(false);
+ addComponent(APP.world, FloatyObject, this.el.object3D.eid);
},
- remove() {
- if (this.stuckTo) {
- const stuckTo = this.stuckTo;
- delete this.stuckTo;
- stuckTo._unstickObject();
- }
+ update() {
+ FloatyObject.flags[this.el.eid] =
+ (this.data.modifyGravityOnRelease && FLOATY_OBJECT_FLAGS.MODIFY_GRAVITY_ON_RELEASE) |
+ (this.data.reduceAngularFloat && FLOATY_OBJECT_FLAGS.REDUCE_ANGULAR_FLOAT) |
+ (this.data.unthrowable && FLOATY_OBJECT_FLAGS.UNTHROWABLE);
+ FloatyObject.releaseGravity[this.el.eid] = this.data.releaseGravity;
}
});
diff --git a/src/components/gltf-model-plus.js b/src/components/gltf-model-plus.js
index 5ef87c08be..9b968e609f 100644
--- a/src/components/gltf-model-plus.js
+++ b/src/components/gltf-model-plus.js
@@ -11,6 +11,11 @@ import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader";
import { BasisTextureLoader } from "three/examples/jsm/loaders/BasisTextureLoader";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
+// It seems we need to use require to import modules
+// under the three/examples/js to avoid tree shaking
+// in webpack production mode.
+require("three/examples/js/loaders/GLTFLoader");
+
THREE.Mesh.prototype.raycast = acceleratedRaycast;
class GLTFCache {
@@ -53,7 +58,7 @@ class GLTFCache {
}
}
}
-const gltfCache = new GLTFCache();
+export const gltfCache = new GLTFCache();
const inflightGltfs = new Map();
const extractZipFile = promisifyWorker(new SketchfabZipWorker());
diff --git a/src/components/ik-controller.js b/src/components/ik-controller.js
index d92d13f76b..fb8cf96f13 100644
--- a/src/components/ik-controller.js
+++ b/src/components/ik-controller.js
@@ -1,3 +1,5 @@
+import { defineQuery } from "bitecs";
+import { CameraTool } from "../bit-components";
import { waitForDOMContentLoaded } from "../utils/async-utils";
const { Vector3, Quaternion, Matrix4, Euler } = THREE;
@@ -15,6 +17,8 @@ function quaternionAlmostEquals(epsilon, u, v) {
);
}
+const cameraToolsQuery = defineQuery([CameraTool]);
+
/**
* Provides access to the end effectors for IK.
* @namespace avatar
@@ -321,7 +325,7 @@ AFRAME.registerComponent("ik-controller", {
_updateIsInView: (function() {
const frustum = new THREE.Frustum();
const frustumMatrix = new THREE.Matrix4();
- const cameraWorld = new THREE.Vector3();
+ const tmpPos = new THREE.Vector3();
const isInViewOfCamera = (screenCamera, pos) => {
frustumMatrix.multiplyMatrices(screenCamera.projectionMatrix, screenCamera.matrixWorldInverse);
frustum.setFromProjectionMatrix(frustumMatrix);
@@ -329,22 +333,23 @@ AFRAME.registerComponent("ik-controller", {
};
return function() {
- if (!this.playerCamera) return;
+ if (!this.playerCamera || this.data.alwaysUpdate) return;
const camera = this.ikRoot.camera.object3D;
- camera.getWorldPosition(cameraWorld);
+ camera.getWorldPosition(tmpPos);
// Check player camera
- this.isInView = isInViewOfCamera(this.playerCamera, cameraWorld);
+ this.isInView = isInViewOfCamera(this.playerCamera, tmpPos);
if (!this.isInView) {
- // Check in-game camera if rendering to viewfinder and owned
- const cameraTools = this.el.sceneEl.systems["camera-tools"];
-
- if (cameraTools) {
- cameraTools.ifMyCameraRenderingViewfinder(cameraTool => {
- this.isInView = this.isInView || isInViewOfCamera(cameraTool.camera, cameraWorld);
- });
+ const world = APP.world;
+ // Check camera tools if they are rendering to viewfinder
+ const cameraTools = cameraToolsQuery(world);
+ for (const eid of cameraTools) {
+ const screenObj = world.eid2obj.get(CameraTool.screenRef[eid]);
+ const cameraObj = world.eid2obj.get(CameraTool.cameraRef[eid]);
+ this.isInView = screenObj.visible && isInViewOfCamera(cameraObj, tmpPos);
+ if (this.isInView) break;
}
}
};
diff --git a/src/components/layers.js b/src/components/layers.js
index 27929fa946..f60434bee1 100644
--- a/src/components/layers.js
+++ b/src/components/layers.js
@@ -1,3 +1,4 @@
+// NOTE when changing be sure to check hardcoded mask values in hub.html
export const Layers = {
// These 3 layers are hardcoded in THREE
CAMERA_LAYER_DEFAULT: 0,
@@ -9,7 +10,8 @@ export const Layers = {
CAMERA_LAYER_VIDEO_TEXTURE_TARGET: 5,
CAMERA_LAYER_THIRD_PERSON_ONLY: 6,
- CAMERA_LAYER_FIRST_PERSON_ONLY: 7
+ CAMERA_LAYER_FIRST_PERSON_ONLY: 7,
+ CAMERA_LAYER_UI: 8
};
/**
@@ -19,35 +21,22 @@ export const Layers = {
*/
AFRAME.registerComponent("layers", {
schema: {
- reflection: { type: "boolean", default: false },
- inWorldHud: { type: "boolean", default: false },
- exclusive: { type: "boolean", default: false } // if true, only these layers will be set
+ mask: { default: Layers.CAMERA_LAYER_DEFAULT },
+ recursive: { default: false }
},
init() {
this.update = this.update.bind(this);
- this.el.addEventListener("model-loaded", this.update);
+ this.el.addEventListener("object3dset", this.update);
},
- update(oldData) {
+ update() {
const obj = this.el.object3D;
-
- if (this.data.exclusive) {
- obj.traverse(o => (o.layers.mask = 0));
- }
-
- for (const [name, layer] of Object.entries(Layers)) {
- const oldValue = oldData[name];
- const newValue = this.data[name];
-
- if (oldValue !== newValue) {
- if (newValue) {
- obj.traverse(o => o.layers.enable(layer));
- } else {
- obj.traverse(o => o.layers.disable(layer));
- }
- }
+ if (this.data.recursive) {
+ obj.traverse(o => (o.layers.mask = this.data.mask));
+ } else {
+ obj.layers.mask = this.data.mask;
}
},
remove() {
- this.el.removeEventListener("model-loaded", this.update);
+ this.el.removeEventListener("object3dset", this.update);
}
});
diff --git a/src/components/media-loader.js b/src/components/media-loader.js
index 21241befa7..73f3e4e962 100644
--- a/src/components/media-loader.js
+++ b/src/components/media-loader.js
@@ -23,6 +23,8 @@ import { cloneObject3D, setMatrixWorld } from "../utils/three-utils";
import { waitForDOMContentLoaded } from "../utils/async-utils";
import { SHAPE } from "three-ammo/constants";
+import { addComponent, entityExists, removeComponent } from "bitecs";
+import { MediaLoading } from "../bit-components";
let loadingObject;
@@ -283,7 +285,13 @@ AFRAME.registerComponent("media-loader", {
this.data.linkedEl.addEventListener("componentremoved", this.handleLinkedElRemoved);
}
+ // TODO this does duplicate work in some cases, but finish() is the only consistent place to do it
+ this.contentBounds = getBox(this.el, this.el.getObject3D("mesh")).getSize(new THREE.Vector3());
+
el.emit("media-loaded");
+ if (el.eid && entityExists(APP.world, el.eid)) {
+ removeComponent(APP.world, MediaLoading, el.eid);
+ }
};
if (this.data.animate) {
@@ -342,6 +350,7 @@ AFRAME.registerComponent("media-loader", {
try {
if ((forceLocalRefresh || srcChanged) && !this.showLoaderTimeout) {
this.showLoaderTimeout = setTimeout(this.showLoader, 100);
+ addComponent(APP.world, MediaLoading, this.el.eid);
}
//check if url is an anchor hash e.g. #Spawn_Point_1
diff --git a/src/components/pinnable.js b/src/components/pinnable.js
index ded6194643..aa1533a746 100644
--- a/src/components/pinnable.js
+++ b/src/components/pinnable.js
@@ -1,3 +1,6 @@
+import { addComponent, removeComponent } from "bitecs";
+import { Pinnable, Pinned } from "../bit-components";
+
AFRAME.registerComponent("pinnable", {
schema: {
pinned: { default: false }
@@ -13,10 +16,18 @@ AFRAME.registerComponent("pinnable", {
this.el.addEventListener("owned-pager-page-changed", this._persist);
this.el.addEventListener("owned-video-state-changed", this._persistAndAnimate);
+
+ addComponent(APP.world, Pinnable, this.el.object3D.eid);
},
update() {
this._animate();
+
+ if (this.data.pinned) {
+ addComponent(APP.world, Pinned, this.el.object3D.eid);
+ } else {
+ removeComponent(APP.world, Pinned, this.el.object3D.eid);
+ }
},
_persistAndAnimate() {
diff --git a/src/components/remove-networked-object-button.js b/src/components/remove-networked-object-button.js
deleted file mode 100644
index f4210fba8b..0000000000
--- a/src/components/remove-networked-object-button.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { removeNetworkedObject } from "../utils/removeNetworkedObject";
-
-AFRAME.registerComponent("remove-networked-object-button", {
- init() {
- this.onClick = () => {
- removeNetworkedObject(this.el.sceneEl, this.targetEl);
- this.el.parentNode.removeAttribute("visibility-while-frozen");
- this.el.parentNode.setAttribute("visible", false);
- };
-
- NAF.utils.getNetworkedEntity(this.el).then(networkedEl => {
- this.targetEl = networkedEl;
- });
- },
-
- play() {
- this.el.object3D.addEventListener("interact", this.onClick);
- },
-
- pause() {
- this.el.object3D.removeEventListener("interact", this.onClick);
- }
-});
diff --git a/src/components/set-unowned-body-kinematic.js b/src/components/set-unowned-body-kinematic.js
index e9640dc93d..6cfc00cc69 100644
--- a/src/components/set-unowned-body-kinematic.js
+++ b/src/components/set-unowned-body-kinematic.js
@@ -25,8 +25,5 @@ AFRAME.registerComponent("set-unowned-body-kinematic", {
type: "kinematic",
collisionFilterMask: COLLISION_LAYERS.UNOWNED_INTERACTABLE
});
- if (this.el.components["floaty-object"]) {
- this.el.components["floaty-object"].locked = true;
- }
}
});
diff --git a/src/components/shape-helper.js b/src/components/shape-helper.js
index 9cc4a11f43..d1c3d02ca0 100644
--- a/src/components/shape-helper.js
+++ b/src/components/shape-helper.js
@@ -68,7 +68,8 @@ AFRAME.registerComponent("shape-helper", {
},
remove: function() {
- if (this.uuid !== -1) {
+ // Removing a body already cleans up it's shapes
+ if (this.uuid !== -1 && this.bodyHelper.alive) {
this.system.removeShapes(this.bodyHelper.uuid, this.uuid);
}
this.alive = false;
diff --git a/src/components/slice9.js b/src/components/slice9.js
index ea6828bc0a..86f7d33e50 100644
--- a/src/components/slice9.js
+++ b/src/components/slice9.js
@@ -92,6 +92,7 @@ AFRAME.registerComponent("slice9", {
this.el.setObject3D("mesh", this.plane);
},
+ // TODO use updateSlice9Geometry
regenerateMesh: function() {
const data = this.data;
let height;
@@ -257,5 +258,9 @@ AFRAME.registerComponent("slice9", {
}
this.setMap(null);
+ },
+
+ remove() {
+ this.geometry?.dispose();
}
});
diff --git a/src/components/stats-plus.css b/src/components/stats-plus.css
index ec873cf8c1..2dbb12816d 100644
--- a/src/components/stats-plus.css
+++ b/src/components/stats-plus.css
@@ -25,6 +25,9 @@
-ms-user-select: none;
user-select: none;
}
+:global(.rs-counter-id) {
+ width: 80px;
+}
:global(.rs-base) {
right: 10px;
@@ -34,4 +37,5 @@
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
+ width: 350px;
}
diff --git a/src/components/stats-plus.js b/src/components/stats-plus.js
index e1c8e0c0bf..2aa710df02 100644
--- a/src/components/stats-plus.js
+++ b/src/components/stats-plus.js
@@ -1,5 +1,7 @@
import "./stats-plus.css";
import qsTruthy from "../utils/qs_truthy";
+import { defineQuery } from "bitecs";
+import { AEntity } from "../bit-components";
function ThreeStats(renderer) {
let _rS = null;
@@ -75,13 +77,21 @@ function createStats(scene) {
values: {
fps: { caption: "fps", below: 30 }
},
- groups: [{ caption: "Framerate", values: ["fps", "raf", "physics"] }],
+ groups: [
+ { caption: "Framerate", values: ["fps", "raf", "physics"] },
+ { caption: "BitECS", values: ["entities", "a-entities", "queries"] }
+ ],
plugins: plugins
});
}
const HIDDEN_CLASS = "a-hidden";
+let $queries;
+
+const allEntitiesQuery = defineQuery();
+const aEntityQuery = defineQuery([AEntity]);
+
AFRAME.registerComponent("stats-plus", {
// Whether or not the stats panel is expanded.
// Shows FPS counter when collapsed.
@@ -139,6 +149,8 @@ AFRAME.registerComponent("stats-plus", {
this.initVRStats();
}
this.lastUpdate = 0;
+
+ $queries = Object.getOwnPropertySymbols(APP.world).find(s => s.description == "queries");
},
initVRStats() {
this.vrPanel = document.createElement("a-entity");
@@ -198,6 +210,10 @@ AFRAME.registerComponent("stats-plus", {
stats("FPS").frame();
stats("physics").set(this.el.sceneEl.systems["hubs-systems"].physicsSystem.stepDuration);
+ stats("queries").set(APP.world[$queries].size);
+ stats("entities").set(allEntitiesQuery(APP.world).length);
+ stats("a-entities").set(aEntityQuery(APP.world).length);
+
stats().update();
} else if (!this.inVR) {
// Update the fps counter
diff --git a/src/components/super-spawner.js b/src/components/super-spawner.js
index af17427d0a..fe08de1fa1 100644
--- a/src/components/super-spawner.js
+++ b/src/components/super-spawner.js
@@ -151,15 +151,6 @@ AFRAME.registerComponent("super-spawner", {
const willAnimateFromCursor =
this.data.animateFromCursor &&
(userinput.get(paths.actions.rightHand.matrix) || userinput.get(paths.actions.leftHand.matrix));
- if (!willAnimateFromCursor) {
- if (left) {
- interaction.state.leftRemote.held = spawnedEntity;
- interaction.state.leftRemote.spawning = true;
- } else {
- interaction.state.rightRemote.held = spawnedEntity;
- interaction.state.rightRemote.spawning = true;
- }
- }
this.activateCooldown();
await waitForEvent("model-loaded", spawnedEntity);
@@ -181,12 +172,6 @@ AFRAME.registerComponent("super-spawner", {
to: { x: this.handPosition.x, y: this.handPosition.y, z: this.handPosition.z },
easing: "easeInOutBack"
});
- } else {
- if (left) {
- interaction.state.leftRemote.spawning = false;
- } else {
- interaction.state.rightRemote.spawning = false;
- }
}
// We skip this in the scene preview because
diff --git a/src/components/tags.js b/src/components/tags.js
index 5d9d14f7d8..7c133488ca 100644
--- a/src/components/tags.js
+++ b/src/components/tags.js
@@ -1,9 +1,40 @@
-export function isTagged(el, tag) {
- return el && el.components && el.components.tags && el.components.tags.data[tag];
-}
+import { addComponent, hasComponent } from "bitecs";
+import {
+ Holdable,
+ OffersRemoteConstraint,
+ HandCollisionTarget,
+ OffersHandConstraint,
+ TogglesHoveredActionSet,
+ SingleActionButton,
+ HoldableButton,
+ Pen,
+ HoverMenuChild,
+ Static,
+ Inspectable,
+ PreventAudioBoost,
+ IgnoreSpaceBubble
+} from "../bit-components";
+
+const tag2ecs = {
+ isHoldable: Holdable,
+ offersRemoteConstraint: OffersRemoteConstraint,
+ isHandCollisionTarget: HandCollisionTarget,
+ offersHandConstraint: OffersHandConstraint,
+ togglesHoveredActionSet: TogglesHoveredActionSet,
+ singleActionButton: SingleActionButton,
+ holdableButton: HoldableButton,
+ isPen: Pen,
+ isHoverMenuChild: HoverMenuChild,
+ isStatic: Static,
+ inspectable: Inspectable,
+ preventAudioBoost: PreventAudioBoost,
+ ignoreSpaceBubble: IgnoreSpaceBubble
+};
-export function setTag(el, tag, value = true) {
- return (el.components.tags.data[tag] = !!value);
+// TODO usages of this should be replaced with direct hasComponent calls
+// but this also has additional logic to check for non existant object
+export function isTagged(elOrObject3D, tag) {
+ return elOrObject3D && hasComponent(APP.world, tag2ecs[tag], elOrObject3D.eid);
}
AFRAME.registerComponent("tags", {
@@ -27,13 +58,8 @@ AFRAME.registerComponent("tags", {
console.warn("Do not edit tags with .setAttribute");
}
this.didUpdateOnce = true;
- },
-
- remove() {
- const interaction = this.el.sceneEl.systems.interaction;
- if (interaction.isHeld(this.el)) {
- interaction.release(this.el);
- this.el.sceneEl.systems["hubs-systems"].constraintsSystem.release(this.el);
- }
+ Object.entries(this.data).forEach(([tagName, isSet]) => {
+ if (isSet) addComponent(APP.world, tag2ecs[tagName], this.el.eid);
+ });
}
});
diff --git a/src/components/transform-object-button.js b/src/components/transform-object-button.js
index 320c740f98..08e11ded67 100644
--- a/src/components/transform-object-button.js
+++ b/src/components/transform-object-button.js
@@ -279,7 +279,7 @@ AFRAME.registerSystem("transform-selected-object", {
.applyQuaternion(q.copy(plane.quaternion).invert())
.multiplyScalar(SENSITIVITY / cameraToPlaneDistance);
if (this.mode === TRANSFORM_MODE.CURSOR) {
- const modify = AFRAME.scenes[0].systems.userinput.get(paths.actions.transformModifier);
+ const modify = !AFRAME.scenes[0].systems.userinput.get(paths.actions.transformModifier);
this.dyAll = this.dyStore + finalProjectedVec.y;
this.dyApplied = modify ? this.dyAll : Math.round(this.dyAll / STEP_LENGTH) * STEP_LENGTH;
diff --git a/src/constants.js b/src/constants.js
index 3e9f7da389..d4242db63c 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -13,7 +13,7 @@ const CL = {
// @TODO we should split these "sets" off into something other than COLLISION_LAYERS or at least name
// them differently to indicate they are a combination of multiple bits
CL.DEFAULT_INTERACTABLE = CL.INTERACTABLES | CL.ENVIRONMENT | CL.AVATAR | CL.HANDS | CL.MEDIA_FRAMES;
-CL.UNOWNED_INTERACTABLE = CL.INTERACTABLES | CL.HANDS;
+CL.UNOWNED_INTERACTABLE = CL.INTERACTABLES | CL.HANDS | CL.MEDIA_FRAMES;
CL.DEFAULT_SPAWNER = CL.INTERACTABLES | CL.HANDS;
const PRIVACY = "https://www.mozilla.org/en-US/privacy/hubs/";
diff --git a/src/gltf-component-mappings.js b/src/gltf-component-mappings.js
index 2cdda43587..5643fd46b3 100644
--- a/src/gltf-component-mappings.js
+++ b/src/gltf-component-mappings.js
@@ -5,6 +5,9 @@ import { TYPE, SHAPE, FIT } from "three-ammo/constants";
const COLLISION_LAYERS = require("./constants").COLLISION_LAYERS;
import { AudioType, DistanceModelType, SourceType } from "./components/audio-params";
import { updateAudioSettings } from "./update-audio-settings";
+import { renderAsEntity } from "./utils/jsx-entity";
+import { Networked } from "./bit-components";
+import { addComponent } from "bitecs";
AFRAME.GLTFModelPlus.registerComponent("duck", "duck", el => {
el.setAttribute("duck", "");
@@ -121,23 +124,21 @@ AFRAME.GLTFModelPlus.registerComponent("waypoint", "waypoint", (el, componentNam
el.setAttribute("waypoint", componentData);
});
-AFRAME.GLTFModelPlus.registerComponent("media-frame", "media-frame", (el, componentName, componentData, components) => {
- el.setAttribute("networked", {
- template: "#interactable-media-frame",
- owner: "scene",
- persistent: true,
- networkId: components.networked.id
- });
- el.setAttribute("shape-helper", {
- type: "box",
- fit: "manual",
- halfExtents: {
- x: componentData.bounds.x / 2,
- y: componentData.bounds.y / 2,
- z: componentData.bounds.z / 2
- }
- });
- el.setAttribute("media-frame", componentData);
+import { findAncestorWithComponent } from "./utils/scene-graph";
+import { createElementEntity } from "./utils/jsx-entity";
+/** @jsx createElementEntity */ createElementEntity;
+
+AFRAME.GLTFModelPlus.registerComponent("media-frame", "media-frame", (el, _componentName, componentData) => {
+ const eid = renderAsEntity(APP.world, );
+
+ addComponent(APP.world, Networked, eid);
+
+ const networkedEl = findAncestorWithComponent(el, "networked");
+ const rootNid = (networkedEl && networkedEl.components.networked.data.networkId) || "scene";
+ Networked.id[eid] = APP.getSid(`${rootNid}.${el.object3D.children[0].userData.gltfIndex}`);
+ APP.world.nid2eid.set(Networked.id[eid], eid);
+
+ el.object3D.add(APP.world.eid2obj.get(eid));
});
AFRAME.GLTFModelPlus.registerComponent("media", "media", (el, componentName, componentData) => {
diff --git a/src/hub.html b/src/hub.html
index d85b036b40..38217b1572 100644
--- a/src/hub.html
+++ b/src/hub.html
@@ -109,7 +109,7 @@
-
+
@@ -153,7 +153,7 @@
-
+