From 048cf983c48f2a7fe64b9e162182938b441f3e97 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Fri, 26 Apr 2024 12:55:57 +0200 Subject: [PATCH] wip --- addons.json | 5 +- src/addons.ts | 22 +- src/app.ts | 3 + src/bit-components.js | 57 ++- src/bit-systems/audio-emitter-system.ts | 8 + src/bit-systems/camera-tool.js | 2 +- src/bit-systems/loop-animation.ts | 8 +- src/bit-systems/media-loading.ts | 9 +- src/bit-systems/object-menu.ts | 4 +- src/bit-systems/scene-loading.ts | 1 - src/bit-systems/text.ts | 112 +++++- src/bit-systems/video-system.ts | 135 ++++++- src/components/body-helper.js | 2 + src/components/gltf-model-plus.js | 14 + src/components/media-loader.js | 6 +- src/constants.ts | 5 +- src/hub.html | 5 +- src/hubs.js | 14 + src/inflators/capturable.js | 7 + src/inflators/grabbable.ts | 9 + src/inflators/loop-animation.ts | 6 - src/inflators/media-frame.js | 26 +- src/inflators/model.tsx | 14 +- src/inflators/rigid-body.ts | 229 ++++++++++-- src/inflators/spawner.ts | 1 - src/inflators/text.ts | 333 +++++++++++++++++- src/inflators/video.ts | 5 +- src/load-media-on-paste-or-drop.ts | 1 + src/prefabs/media.tsx | 2 +- .../debug-panel/ECSSidebar.js | 42 ++- src/react-components/room/ChatSidebar.js | 2 + .../room/contexts/ChatContext.tsx | 1 + src/systems/bit-constraints-system.js | 15 +- src/systems/bit-media-frames.js | 129 ++++--- src/systems/bit-physics.ts | 47 ++- src/systems/floaty-object-system.js | 18 +- src/systems/hubs-systems.ts | 30 +- src/systems/networked-transform.js | 14 +- src/systems/on-ownership-lost.js | 16 +- src/systems/physics-system.js | 29 +- src/systems/remove-object3D-system.js | 8 + src/utils/assign-network-ids.ts | 11 + src/utils/bit-utils.ts | 13 +- src/utils/jsx-entity.ts | 33 +- src/utils/load-model.tsx | 1 - src/utils/media-utils.js | 15 +- src/utils/network-schemas.ts | 6 + src/utils/networked-media-frame-schema.ts | 35 +- src/utils/networked-rigid-body.ts | 31 ++ src/utils/networked-text-schema.ts | 136 +++++++ src/utils/networked-video-schema.ts | 26 +- src/utils/projection-mode.ts | 9 + src/utils/three-utils.js | 70 +++- types/aframe.d.ts | 9 +- types/three.d.ts | 8 +- types/troika-three-text.d.ts | 22 +- webpack.config.js | 3 - 57 files changed, 1580 insertions(+), 244 deletions(-) create mode 100644 src/inflators/capturable.js create mode 100644 src/utils/networked-rigid-body.ts create mode 100644 src/utils/networked-text-schema.ts diff --git a/addons.json b/addons.json index 8d9496e075..b793052ac8 100644 --- a/addons.json +++ b/addons.json @@ -1,6 +1,3 @@ { - "addons": [ - "hubs-duck-addon", - "hubs-portals-addon" - ] + "addons": [] } \ No newline at end of file diff --git a/src/addons.ts b/src/addons.ts index 9be4e7ba04..8614e7a15f 100644 --- a/src/addons.ts +++ b/src/addons.ts @@ -1,4 +1,5 @@ -import { App } from "./app"; +import { GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader"; +import { App, HubsWorld } from "./app"; import { prefabs } from "./prefabs/prefabs"; import { @@ -12,6 +13,10 @@ import { import configs from "./utils/configs"; import { commonInflators, gltfInflators, jsxInflators } from "./utils/jsx-entity"; import { networkableComponents, schemas } from "./utils/network-schemas"; +import { gltfPluginsExtra } from "./components/gltf-model-plus"; +import { GLTFLinkResolverFn, gltfLinkResolvers } from "./inflators/model"; +import { Object3D } from "three"; +import { extraSections } from "./react-components/debug-panel/ECSSidebar"; function getNextIdx(slot: Array, system: SystemConfigT) { return slot.findIndex(item => { @@ -25,8 +30,10 @@ function registerSystem(system: SystemConfigT) { slot = APP.addon_systems.setup; } else if (system.order < SystemOrderE.PostPhysics) { slot = APP.addon_systems.prePhysics; - } else if (system.order < SystemOrderE.MatricesUpdate) { + } else if (system.order < SystemOrderE.PostPhysics) { slot = APP.addon_systems.postPhysics; + } else if (system.order < SystemOrderE.MatricesUpdate) { + slot = APP.addon_systems.matricesUpdate; } else if (system.order < SystemOrderE.BeforeRender) { slot = APP.addon_systems.beforeRender; } else if (system.order < SystemOrderE.AfterRender) { @@ -101,6 +108,17 @@ export function registerAddon(id: AddonIdT, config: AddonConfigT) { pendingAddons.set(id, config); } +export type GLTFParserCallbackFn = (parser: GLTFParser) => GLTFLoaderPlugin; +export function registerGLTFLoaderPlugin(callback: GLTFParserCallbackFn): void { + gltfPluginsExtra.push(callback); +} +export function registerGLTFLinkResolver(resolver: GLTFLinkResolverFn): void { + gltfLinkResolvers.push(resolver); +} +export function registerECSSidebarSection(section: (world: HubsWorld, selectedObj: Object3D) => React.JSX.Element) { + extraSections.push(section); +} + export function onAddonsInit(app: App) { app.scene?.addEventListener("hub_updated", () => { for (const [id, addon] of pendingAddons) { diff --git a/src/app.ts b/src/app.ts index afe0198704..c86fb43944 100644 --- a/src/app.ts +++ b/src/app.ts @@ -19,6 +19,7 @@ import { PositionalAudio, Scene, sRGBEncoding, + Texture, WebGLRenderer } from "three"; import { AudioSettings, SourceType } from "./components/audio-params"; @@ -54,6 +55,7 @@ export interface HubsWorld extends IWorld { nid2eid: Map; eid2obj: Map; eid2mat: Map; + eid2tex: Map; time: { delta: number; elapsed: number; tick: number }; } @@ -135,6 +137,7 @@ export class App { // TODO: Create accessor / update methods for these maps / set this.world.eid2obj = new Map(); this.world.eid2mat = new Map(); + this.world.eid2tex = new Map(); this.world.nid2eid = new Map(); this.world.deletedNids = new Set(); diff --git a/src/bit-components.js b/src/bit-components.js index e0d50332be..e3721d1672 100644 --- a/src/bit-components.js +++ b/src/bit-components.js @@ -21,7 +21,9 @@ export const Owned = defineComponent(); export const EntityStateDirty = defineComponent(); export const NetworkedMediaFrame = defineComponent({ capturedNid: Types.ui32, - scale: [Types.f32, 3] + scale: [Types.f32, 3], + flags: Types.ui8, + mediaType: Types.ui8 }); NetworkedMediaFrame.capturedNid[$isStringType] = true; @@ -36,6 +38,45 @@ export const MediaFrame = defineComponent({ previewingNid: Types.eid, flags: Types.ui8 }); +export const MediaRoot = defineComponent(); +export const Capturable = defineComponent(); +export const CapturableObject = defineComponent(); +export const NetworkedText = defineComponent({ + text: Types.ui8, + anchorX: Types.ui8, + anchorY: Types.ui8, + color: Types.ui32, + curveRadius: Types.f32, + direction: Types.ui8, + fillOpacity: Types.f32, + fontUrl: Types.ui8, + fontSize: Types.f32, + letterSpacing: Types.f32, + lineHeight: Types.ui8, + textAlign: Types.ui8, + outlineWidth: Types.ui8, + outlineColor: Types.ui32, + outlineBlur: Types.ui8, + outlineOffsetX: Types.ui8, + outlineOffsetY: Types.ui8, + outlineOpacity: Types.f32, + strokeWidth: Types.ui8, + strokeColor: Types.ui32, + strokeOpacity: Types.ui32, + textIndent: Types.ui32, + whiteSpace: Types.ui8, + overflowWrap: Types.ui8, + opacity: Types.f32, + side: Types.ui8, + maxWidth: Types.f32 +}); +NetworkedText.text[$isStringType] = true; +NetworkedText.lineHeight[$isStringType] = true; +NetworkedText.outlineWidth[$isStringType] = true; +NetworkedText.outlineBlur[$isStringType] = true; +NetworkedText.outlineOffsetX[$isStringType] = true; +NetworkedText.outlineOffsetY[$isStringType] = true; +NetworkedText.strokeWidth[$isStringType] = true; export const TextTag = defineComponent(); export const ReflectionProbe = defineComponent(); export const Slice9 = defineComponent({ @@ -119,7 +160,11 @@ export const Rigidbody = defineComponent({ activationState: Types.ui8, collisionFilterGroup: Types.ui32, collisionFilterMask: Types.ui32, - flags: Types.ui8 + flags: Types.ui8, + prevType: Types.ui8 +}); +export const NetworkedRigidBody = defineComponent({ + prevType: Types.ui8 }); export const PhysicsShape = defineComponent({ bodyId: Types.ui16, @@ -216,6 +261,7 @@ export const SceneRoot = defineComponent(); export const NavMesh = defineComponent(); export const SceneLoader = defineComponent({ src: Types.ui32 }); SceneLoader.src[$isStringType] = true; +export const SceneLoaded = defineComponent(); export const MediaImage = defineComponent({ cacheKey: Types.ui32, @@ -270,9 +316,12 @@ export const LoopAnimation = defineComponent(); */ export const LoopAnimationData = new Map(); export const NetworkedVideo = defineComponent({ + src: Types.ui8, time: Types.f32, - flags: Types.ui8 + flags: Types.ui8, + projection: Types.ui8 }); +NetworkedVideo.src[$isStringType] = true; export const VideoMenuItem = defineComponent(); export const VideoMenu = defineComponent({ videoRef: Types.eid, @@ -387,6 +436,7 @@ export const Billboard = defineComponent({ onlyY: Types.ui8 }); export const MaterialTag = defineComponent(); +export const TextureTag = defineComponent(); export const UVScroll = defineComponent({ speed: [Types.f32, 2], increment: [Types.f32, 2], @@ -454,3 +504,4 @@ export const ObjectMenuTransform = defineComponent({ prevObjectRef: Types.eid, flags: Types.ui8 }); +export const InteractableObject = defineComponent(); diff --git a/src/bit-systems/audio-emitter-system.ts b/src/bit-systems/audio-emitter-system.ts index a525fc3e02..43f8cd7dd3 100644 --- a/src/bit-systems/audio-emitter-system.ts +++ b/src/bit-systems/audio-emitter-system.ts @@ -6,6 +6,7 @@ import { AudioType, SourceType } from "../components/audio-params"; import { AudioSystem } from "../systems/audio-system"; import { applySettings, getCurrentAudioSettings, updateAudioSettings } from "../update-audio-settings"; import { addObject3DComponent, swapObject3DComponent } from "../utils/jsx-entity"; +import { EntityID } from "../utils/networking-types"; export type AudioObject3D = StereoAudio | PositionalAudio; type AudioConstructor = new (listener: ThreeAudioListener) => T; @@ -55,6 +56,13 @@ function swapAudioType( swapObject3DComponent(world, eid, newAudio); } +export function swapAudioSrc(world: HubsWorld, videoEid: EntityID, audioEid: EntityID) { + const audio = world.eid2obj.get(audioEid)! as AudioObject3D; + const video = MediaVideoData.get(videoEid)!; + audio.setMediaElementSource(video); + video.volume = 1; +} + export function makeAudioEntity(world: HubsWorld, source: number, sourceType: SourceType, audioSystem: AudioSystem) { const eid = addEntity(world); APP.sourceType.set(eid, sourceType); diff --git a/src/bit-systems/camera-tool.js b/src/bit-systems/camera-tool.js index 0ce7cf8411..f403300cf4 100644 --- a/src/bit-systems/camera-tool.js +++ b/src/bit-systems/camera-tool.js @@ -211,7 +211,7 @@ function rotateWithRightClick(world, camera) { userinput.get(paths.device.mouse.buttonRight) ) { const rightCursor = anyEntityWith(world, RemoteRight); - physicsSystem.updateRigidBodyOptions(camera, { type: "kinematic" }); + physicsSystem.updateRigidBody(camera, { type: "kinematic" }); transformSystem.startTransform(world.eid2obj.get(camera), world.eid2obj.get(rightCursor), { mode: "cursor" }); diff --git a/src/bit-systems/loop-animation.ts b/src/bit-systems/loop-animation.ts index 4bc7844ccd..bd791ddad8 100644 --- a/src/bit-systems/loop-animation.ts +++ b/src/bit-systems/loop-animation.ts @@ -1,5 +1,5 @@ import { addComponent, defineQuery, enterQuery, exitQuery, hasComponent, removeComponent } from "bitecs"; -import { AnimationAction, AnimationClip, AnimationMixer, LoopRepeat } from "three"; +import { AnimationClip, LoopRepeat } from "three"; import { MixerAnimatable, MixerAnimatableData, @@ -47,12 +47,16 @@ const getActiveClips = ( export function loopAnimationSystem(world: HubsWorld): void { loopAnimationInitializeEnterQuery(world).forEach((eid: number): void => { + const params = LoopAnimationInitializeData.get(eid)!; + if (!params.length) { + return; + } + const object = world.eid2obj.get(eid)!; const mixer = MixerAnimatableData.get(eid)!; addComponent(world, LoopAnimation, eid); - const params = LoopAnimationInitializeData.get(eid)!; const activeAnimations = []; for (let i = 0; i < params.length; i++) { diff --git a/src/bit-systems/media-loading.ts b/src/bit-systems/media-loading.ts index 98832a80dd..07eb7aaa1c 100644 --- a/src/bit-systems/media-loading.ts +++ b/src/bit-systems/media-loading.ts @@ -30,7 +30,9 @@ import { Rigidbody, MediaLoaderOffset, MediaVideo, - NetworkedTransform + NetworkedTransform, + MediaRoot, + Capturable } from "../bit-components"; import { inflatePhysicsShape, Shape } from "../inflators/physics-shape"; import { ErrorObject } from "../prefabs/error-object"; @@ -204,7 +206,7 @@ class UnsupportedMediaTypeError extends Error { } } -type MediaInfo = { +export type MediaInfo = { accessibleUrl: string; canonicalUrl: string; canonicalAudioUrl: string | null; @@ -282,7 +284,10 @@ function* loadMedia(world: HubsWorld, eid: EntityID) { let media: EntityID; try { const urlData = (yield resolveMediaInfo(src)) as MediaInfo; + APP.getSid(urlData.accessibleUrl); // Register the sid as this is what we will get over the network for media. media = yield* loadByMediaType(world, eid, urlData); + addComponent(world, MediaRoot, media); + addComponent(world, Capturable, media); addComponent(world, MediaLoaded, media); addComponent(world, MediaInfo, media); MediaInfo.accessibleUrl[media] = APP.getSid(urlData.accessibleUrl); diff --git a/src/bit-systems/object-menu.ts b/src/bit-systems/object-menu.ts index 51c6702a73..e7545c9936 100644 --- a/src/bit-systems/object-menu.ts +++ b/src/bit-systems/object-menu.ts @@ -105,7 +105,7 @@ function startRotation(world: HubsWorld, menuEid: EntityID, targetEid: EntityID) } const transformSystem = APP.scene!.systems["transform-selected-object"]; const physicsSystem = AFRAME.scenes[0].systems["hubs-systems"].physicsSystem; - physicsSystem.updateRigidBodyOptions(Rigidbody.bodyId[targetEid], { type: "kinematic" }); + physicsSystem.updateRigidBody(Rigidbody.bodyId[targetEid], { type: "kinematic" }); const rightCursorEid = anyEntityWith(world, RemoteRight)!; transformSystem.startTransform(world.eid2obj.get(targetEid)!, world.eid2obj.get(rightCursorEid)!, { mode: TRANSFORM_MODE.CURSOR @@ -137,7 +137,7 @@ function startScaling(world: HubsWorld, menuEid: EntityID, targetEid: EntityID) // TODO: Remove the dependency with AFRAME const transformSystem = (AFRAME as any).scenes[0].systems["transform-selected-object"]; const physicsSystem = AFRAME.scenes[0].systems["hubs-systems"].physicsSystem; - physicsSystem.updateRigidBodyOptions(Rigidbody.bodyId[targetEid], { type: "kinematic" }); + physicsSystem.updateRigidBody(Rigidbody.bodyId[targetEid], { type: "kinematic" }); const rightCursorEid = anyEntityWith(world, RemoteRight)!; scalingHandler = new ScalingHandler(world.eid2obj.get(targetEid), transformSystem); scalingHandler!.objectToScale = world.eid2obj.get(targetEid); diff --git a/src/bit-systems/scene-loading.ts b/src/bit-systems/scene-loading.ts index c782c001b6..5f5a176d91 100644 --- a/src/bit-systems/scene-loading.ts +++ b/src/bit-systems/scene-loading.ts @@ -7,7 +7,6 @@ import { HeightFieldTag, NavMesh, Networked, - PhysicsShape, SceneLoader, ScenePreviewCamera, SceneRoot, diff --git a/src/bit-systems/text.ts b/src/bit-systems/text.ts index 5f6a93a7e3..717c2fc49e 100644 --- a/src/bit-systems/text.ts +++ b/src/bit-systems/text.ts @@ -1,9 +1,23 @@ import { defineQuery } from "bitecs"; import { Text as TroikaText } from "troika-three-text"; import { HubsWorld } from "../app"; -import { TextTag } from "../bit-components"; +import { NetworkedText, TextTag } from "../bit-components"; +import { + NumberOrNormalT, + NumberOrPctT, + THREE_SIDES, + flagToAnchorX, + flagToAnchorY, + flagToDirection, + flagToOverflowWrap, + flagToSide, + flagToTextAlign, + flagToWhiteSpace, + stringToNumberOrString +} from "../inflators/text"; const textQuery = defineQuery([TextTag]); +const networkedTextQuery = defineQuery([TextTag, NetworkedText]); export function textSystem(world: HubsWorld) { textQuery(world).forEach(eid => { @@ -30,4 +44,100 @@ export function textSystem(world: HubsWorld) { // because TroikaText properly handles text.sync(); }); + networkedTextQuery(world).forEach(eid => { + const text = world.eid2obj.get(eid)! as TroikaText; + const newText = APP.getString(NetworkedText.text[eid]); + if (text.text !== newText) { + text.text = newText!; + } + if (text.fontSize !== NetworkedText.fontSize[eid]) { + text.fontSize = NetworkedText.fontSize[eid]; + } + const textAlign = flagToTextAlign(NetworkedText.textAlign[eid]); + if (text.textAlign !== textAlign) { + text.textAlign = textAlign; + } + const anchorX = flagToAnchorX(NetworkedText.anchorX[eid]); + if (text.anchorX !== anchorX) { + text.anchorX = anchorX; + } + const anchorY = flagToAnchorY(NetworkedText.anchorY[eid]); + if (text.anchorY !== anchorY) { + text.anchorY = anchorY; + } + if (text.color !== NetworkedText.color[eid]) { + text.color = NetworkedText.color[eid]; + } + if (text.letterSpacing !== NetworkedText.letterSpacing[eid]) { + text.letterSpacing = NetworkedText.letterSpacing[eid]; + } + const lineHeight = stringToNumberOrString(APP.getString(NetworkedText.lineHeight[eid])!) as NumberOrNormalT; + if (text.lineHeight !== lineHeight) { + text.lineHeight = lineHeight; + } + const outlineWidth = stringToNumberOrString(APP.getString(NetworkedText.outlineWidth[eid])!) as NumberOrPctT; + if (text.outlineWidth !== outlineWidth) { + text.outlineWidth = outlineWidth; + } + if (text.outlineColor !== NetworkedText.outlineColor[eid]) { + text.outlineColor = NetworkedText.outlineColor[eid]; + } + const outlineBlur = stringToNumberOrString(APP.getString(NetworkedText.outlineBlur[eid])!) as NumberOrPctT; + if (text.outlineBlur !== outlineBlur) { + text.outlineBlur = outlineBlur; + } + const outlineOffsetX = stringToNumberOrString(APP.getString(NetworkedText.outlineOffsetX[eid])!) as NumberOrPctT; + if (text.outlineOffsetX !== outlineOffsetX) { + text.outlineOffsetX = outlineOffsetX; + } + const outlineOffsetY = stringToNumberOrString(APP.getString(NetworkedText.outlineOffsetY[eid])!) as NumberOrPctT; + if (text.outlineOffsetY !== outlineOffsetY) { + text.outlineOffsetY = outlineOffsetY; + } + if (text.outlineOpacity !== NetworkedText.outlineOpacity[eid]) { + text.outlineOpacity = NetworkedText.outlineOpacity[eid]; + } + if (text.fillOpacity !== NetworkedText.fillOpacity[eid]) { + text.fillOpacity = NetworkedText.fillOpacity[eid]; + } + const strokeWidth = stringToNumberOrString(APP.getString(NetworkedText.strokeWidth[eid])!) as NumberOrPctT; + if (text.strokeWidth !== strokeWidth) { + text.strokeWidth = strokeWidth; + } + if (text.strokeColor !== NetworkedText.strokeColor[eid]) { + text.strokeColor = NetworkedText.strokeColor[eid]; + } + if (text.strokeOpacity !== NetworkedText.strokeOpacity[eid]) { + text.strokeOpacity = NetworkedText.strokeOpacity[eid]; + } + if (text.textIndent !== NetworkedText.textIndent[eid]) { + text.textIndent = NetworkedText.textIndent[eid]; + } + const whiteSpace = flagToWhiteSpace(NetworkedText.whiteSpace[eid]); + if (text.whiteSpace !== whiteSpace) { + text.whiteSpace = whiteSpace; + } + const overflowWrap = flagToOverflowWrap(NetworkedText.overflowWrap[eid]); + if (text.overflowWrap !== overflowWrap) { + text.overflowWrap = overflowWrap; + } + if (text.material!.opacity !== NetworkedText.opacity[eid]) { + text.material!.opacity = NetworkedText.opacity[eid]; + } + const side = THREE_SIDES[flagToSide(NetworkedText.side[eid])]; + if (text.material!.side !== side) { + text.material!.side = side; + } + if (text.maxWidth !== NetworkedText.maxWidth[eid]) { + text.maxWidth = NetworkedText.maxWidth[eid]; + } + if (text.curveRadius !== NetworkedText.curveRadius[eid]) { + text.curveRadius = NetworkedText.curveRadius[eid]; + } + const direction = flagToDirection(NetworkedText.direction[eid]); + if (text.direction !== direction) { + text.direction = direction; + } + text.sync(); + }); } diff --git a/src/bit-systems/video-system.ts b/src/bit-systems/video-system.ts index 6fe23202c4..cfd60b5f91 100644 --- a/src/bit-systems/video-system.ts +++ b/src/bit-systems/video-system.ts @@ -1,10 +1,21 @@ -import { addComponent, defineQuery, enterQuery, entityExists, exitQuery, hasComponent, removeComponent } from "bitecs"; +import { + addComponent, + defineComponent, + defineQuery, + enterQuery, + entityExists, + exitQuery, + hasComponent, + removeComponent +} from "bitecs"; import { Mesh } from "three"; import { HubsWorld } from "../app"; import { + AudioEmitter, AudioParams, AudioSettingsChanged, MediaLoaded, + MediaRoot, MediaVideo, MediaVideoData, MediaVideoUpdated, @@ -14,20 +25,97 @@ import { } from "../bit-components"; import { SourceType } from "../components/audio-params"; import { AudioSystem } from "../systems/audio-system"; -import { findAncestorWithComponent } from "../utils/bit-utils"; -import { Emitter2Audio, Emitter2Params, makeAudioEntity } from "./audio-emitter-system"; +import { findAncestorWithComponent, findChildWithComponent } from "../utils/bit-utils"; +import { Emitter2Audio, Emitter2Params, makeAudioEntity, swapAudioSrc } from "./audio-emitter-system"; import { takeSoftOwnership } from "../utils/take-soft-ownership"; import { crNextFrame } from "../utils/coroutine"; +import { ClearFunction, JobRunner, swapObject3DComponent } from "../hubs"; +import { VIDEO_FLAGS } from "../inflators/video"; +import { HubsVideoTexture } from "../textures/HubsVideoTexture"; +import { create360ImageMesh, createImageMesh } from "../utils/create-image-mesh"; +import { loadAudioTexture } from "../utils/load-audio-texture"; +import { loadVideoTexture } from "../utils/load-video-texture"; +import { resolveMediaInfo, MediaType } from "../utils/media-utils"; +import { EntityID } from "../utils/networking-types"; +import { ProjectionModeName, getProjectionNameFromProjection } from "../utils/projection-mode"; +import { disposeNode } from "../utils/three-utils"; +import { MediaInfo } from "./media-loading"; + +export const MediaVideoUpdateSrcEvent = defineComponent(); + +function* loadSrc( + world: HubsWorld, + eid: EntityID, + src: string, + oldVideo: HTMLVideoElement, + clearRollbacks: ClearFunction +) { + const projection = getProjectionNameFromProjection(NetworkedVideo.projection[eid]); + const autoPlay = NetworkedVideo.flags[eid] & VIDEO_FLAGS.AUTO_PLAY ? true : false; + const loop = NetworkedVideo.flags[eid] & VIDEO_FLAGS.LOOP ? true : false; + const { accessibleUrl, contentType, mediaType } = (yield resolveMediaInfo(src)) as MediaInfo; + let data: any; + if (mediaType === MediaType.VIDEO) { + data = (yield loadVideoTexture(accessibleUrl, contentType, loop, autoPlay)) as unknown; + } else if (mediaType === MediaType.AUDIO) { + data = (yield loadAudioTexture(accessibleUrl, loop, autoPlay)) as unknown; + } else { + return; + } + + const { texture, ratio, video }: { texture: HubsVideoTexture; ratio: number; video: HTMLVideoElement } = data; + + clearRollbacks(); // After this point, normal entity cleanup will take care of things + + let videoObj; + if (projection === ProjectionModeName.SPHERE_EQUIRECTANGULAR) { + videoObj = create360ImageMesh(texture, ratio); + } else { + videoObj = createImageMesh(texture, ratio); + } + MediaVideo.ratio[eid] = ratio; + MediaVideoData.set(eid, video); + oldVideo.pause(); + const mediaRoot = findAncestorWithComponent(world, MediaRoot, eid)!; + const mediaRootObj = world.eid2obj.get(mediaRoot)!; + mediaRootObj.add(videoObj); + + const audioEmitter = findChildWithComponent(world, AudioEmitter, eid)!; + swapAudioSrc(world, eid, audioEmitter); + const audioObj = APP.world.eid2obj.get(audioEmitter)!; + videoObj.add(audioObj); + + const oldVideoObj = APP.world.eid2obj.get(eid)! as Mesh; + mediaRootObj.remove(oldVideoObj); + disposeNode(oldVideoObj); + + swapObject3DComponent(world, eid, videoObj); + + if ((NetworkedVideo.flags[eid] & VIDEO_FLAGS.PAUSED) === 0 || autoPlay) { + video.play(); + } + + removeComponent(world, MediaVideoUpdateSrcEvent, eid); +} enum Flags { PAUSED = 1 << 0 } +export function updateVideoSrc(world: HubsWorld, eid: EntityID, src: string, video: HTMLVideoElement) { + addComponent(world, MediaVideoUpdateSrcEvent, eid); + + jobs.stop(eid); + jobs.add(eid, clearRollbacks => loadSrc(world, eid, src, video, clearRollbacks)); +} + +const jobs = new JobRunner(); export const OUT_OF_SYNC_SEC = 5; const networkedVideoQuery = defineQuery([Networked, NetworkedVideo]); const networkedVideoEnterQuery = enterQuery(networkedVideoQuery); const mediaVideoQuery = defineQuery([MediaVideo]); const mediaVideoEnterQuery = enterQuery(mediaVideoQuery); +const networkedVideoExitQuery = exitQuery(networkedVideoQuery); const mediaVideoExitQuery = exitQuery(mediaVideoQuery); const mediaLoadStatusQuery = defineQuery([MediaVideo, MediaLoaded]); const mediaLoadedQuery = enterQuery(mediaLoadStatusQuery); @@ -71,6 +159,9 @@ export function videoSystem(world: HubsWorld, audioSystem: AudioSystem) { takeSoftOwnership(world, eid); } }); + networkedVideoExitQuery(world).forEach(eid => { + jobs.stop(eid); + }); networkedVideoQuery(world).forEach(function (eid) { const video = MediaVideoData.get(eid)!; @@ -80,11 +171,41 @@ export function videoSystem(world: HubsWorld, audioSystem: AudioSystem) { NetworkedVideo.time[eid] = video.currentTime; MediaVideo.lastUpdate[eid] = now; } - let flags = 0; - flags |= video.paused ? Flags.PAUSED : 0; + let flags = NetworkedVideo.flags[eid]; + if (video.paused) { + flags |= VIDEO_FLAGS.PAUSED; + } else { + flags &= ~VIDEO_FLAGS.PAUSED; + } + if (video.loop) { + flags |= VIDEO_FLAGS.LOOP; + } else { + flags &= ~VIDEO_FLAGS.LOOP; + } + if (video.autoplay) { + flags |= VIDEO_FLAGS.AUTO_PLAY; + } else { + flags &= ~VIDEO_FLAGS.AUTO_PLAY; + } NetworkedVideo.flags[eid] = flags; + NetworkedVideo.src[eid] = APP.getSid(video.src); } else { - const networkedPauseState = !!(NetworkedVideo.flags[eid] & Flags.PAUSED); + let shouldUpdateVideo = false; + const autoPlay = NetworkedVideo.flags[eid] & VIDEO_FLAGS.AUTO_PLAY ? true : false; + const loop = NetworkedVideo.flags[eid] & VIDEO_FLAGS.AUTO_PLAY ? true : false; + if (MediaVideo.flags[eid] !== NetworkedVideo.flags[eid]) { + MediaVideo.flags[eid] = NetworkedVideo.flags[eid]; + } + if (MediaVideo.projection[eid] !== NetworkedVideo.projection[eid]) { + MediaVideo.projection[eid] = NetworkedVideo.projection[eid]; + shouldUpdateVideo ||= true; + } + const src = APP.getString(NetworkedVideo.src[eid])!; + shouldUpdateVideo ||= src !== video.src || autoPlay !== video.autoplay || loop !== video.loop; + if (shouldUpdateVideo && !hasComponent(world, MediaVideoUpdateSrcEvent, eid)) { + updateVideoSrc(world, eid, src, video); + } + const networkedPauseState = !!(NetworkedVideo.flags[eid] & VIDEO_FLAGS.PAUSED); if (networkedPauseState !== video.paused) { video.paused ? video.play().catch(() => { @@ -108,4 +229,6 @@ export function videoSystem(world: HubsWorld, audioSystem: AudioSystem) { } }); }); + + jobs.tick(); } diff --git a/src/components/body-helper.js b/src/components/body-helper.js index e7b17d63d7..96496da1e0 100644 --- a/src/components/body-helper.js +++ b/src/components/body-helper.js @@ -1,6 +1,7 @@ import { addComponent, removeComponent } from "bitecs"; import { CONSTANTS } from "three-ammo"; import { Rigidbody } from "../bit-components"; +import { updateBodyParams } from "../inflators/rigid-body"; const ACTIVATION_STATE = CONSTANTS.ACTIVATION_STATE, TYPE = CONSTANTS.TYPE; @@ -41,6 +42,7 @@ AFRAME.registerComponent("body-helper", { this.uuid = this.system.addBody(this.el.object3D, this.data); const eid = this.el.object3D.eid; addComponent(APP.world, Rigidbody, eid); + updateBodyParams(eid, this.data); Rigidbody.bodyId[eid] = this.uuid; //uuid is a lie, it's actually an int }, diff --git a/src/components/gltf-model-plus.js b/src/components/gltf-model-plus.js index 12090f11b9..2b778a400f 100644 --- a/src/components/gltf-model-plus.js +++ b/src/components/gltf-model-plus.js @@ -508,6 +508,18 @@ class GLTFHubsPlugin { } } } + const materials = parser.json.materials; + if (materials) { + for (let i = 0; i < materials.length; i++) { + const mat = materials[i]; + + if (!mat.extras) { + mat.extras = {}; + } + + mat.extras.gltfIndex = i; + } + } } afterRoot(gltf) { @@ -858,6 +870,7 @@ class GLTFHubsLoopAnimationComponent { } } +export const gltfPluginsExtra = []; export async function loadGLTF(src, contentType, onProgress, jsonPreprocessor) { let gltfUrl = src; let fileMap; @@ -933,6 +946,7 @@ export async function loadGLTF(src, contentType, onProgress, jsonPreprocessor) { } }) ); + gltfPluginsExtra.forEach(ext => gltfLoader.register(parser => ext(parser))); // TODO some models are loaded before the renderer exists. This is likely things like the camera tool and loading cube. // They don't currently use KTX textures but if they did this would be an issue. Fixing this is hard but is part of diff --git a/src/components/media-loader.js b/src/components/media-loader.js index 9c89bbddcf..94961d74f6 100644 --- a/src/components/media-loader.js +++ b/src/components/media-loader.js @@ -284,8 +284,10 @@ AFRAME.registerComponent("media-loader", { // TODO this does duplicate work in some cases, but finish() is the only consistent place to do it const contentBounds = getBox(this.el.object3D, this.el.getObject3D("mesh")).getSize(new THREE.Vector3()); - addComponent(APP.world, MediaContentBounds, el.eid); - MediaContentBounds.bounds[el.eid].set(contentBounds.toArray()); + if (el.eid) { + addComponent(APP.world, MediaContentBounds, el.eid); + MediaContentBounds.bounds[el.eid].set(contentBounds.toArray()); + } el.emit("media-loaded"); }; diff --git a/src/constants.ts b/src/constants.ts index 7dadf4f4ec..b69ca21272 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -6,12 +6,13 @@ export enum COLLISION_LAYERS { AVATAR = 1 << 2, HANDS = 1 << 3, MEDIA_FRAMES = 1 << 4, + TRIGGERS = 1 << 5, // @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 - DEFAULT_INTERACTABLE = INTERACTABLES | ENVIRONMENT | AVATAR | HANDS | MEDIA_FRAMES, + DEFAULT_INTERACTABLE = INTERACTABLES | ENVIRONMENT | AVATAR | HANDS | MEDIA_FRAMES | TRIGGERS, UNOWNED_INTERACTABLE = INTERACTABLES | HANDS | MEDIA_FRAMES, DEFAULT_SPAWNER = INTERACTABLES | HANDS -}; +} export enum AAModes { NONE = "NONE", diff --git a/src/hub.html b/src/hub.html index 623027ad19..a6d733bd6f 100644 --- a/src/hub.html +++ b/src/hub.html @@ -211,7 +211,7 @@ @@ -821,7 +821,8 @@ diff --git a/src/hubs.js b/src/hubs.js index a335c5af2e..965e5b907d 100644 --- a/src/hubs.js +++ b/src/hubs.js @@ -1,8 +1,10 @@ export * from "./bit-components"; +export * as bitComponents from "./bit-components"; export * from "./addons"; export * from "./types"; export * from "./camera-layers"; export * from "./constants"; +export * from "./change-hub"; export * from "./utils/bit-utils"; export * from "./utils/jsx-entity"; export * from "./utils/media-url-utils"; @@ -15,10 +17,20 @@ export * from "./utils/animate"; export * from "./utils/easing"; export * from "./utils/coroutine"; export * from "./utils/coroutine-utils"; +export * from "./utils/take-ownership"; +export * from "./utils/take-soft-ownership"; +export * from "./utils/component-utils"; +export * from "./utils/projection-mode"; +export * from "./utils/assign-network-ids"; export * from "./components/gltf-model-plus"; +export * from "./inflators/model"; export * from "./inflators/physics-shape"; export * from "./inflators/media-frame"; +export * from "./inflators/rigid-body"; +export * from "./inflators/text"; +export * from "./inflators/video"; export * from "./bit-systems/delete-entity-system"; +export * from "./bit-systems/video-system"; export * from "./systems/floaty-object-system"; export * from "./systems/userinput/paths"; export * from "./systems/userinput/sets"; @@ -26,3 +38,5 @@ export * from "./systems/userinput/userinput"; export * from "./systems/userinput/bindings/xforms"; export * from "./systems/userinput/bindings/keyboard-mouse-user"; export * from "./systems/userinput/devices/keyboard"; +export * from "./systems/bit-physics"; +export * from "./react-components/debug-panel/ECSSidebar"; diff --git a/src/inflators/capturable.js b/src/inflators/capturable.js new file mode 100644 index 0000000000..7567339b21 --- /dev/null +++ b/src/inflators/capturable.js @@ -0,0 +1,7 @@ +import { Capturable, CapturableObject } from "../bit-components"; +import { addComponent } from "bitecs"; + +export function inflateCapturable(world, eid) { + addComponent(world, Capturable, eid, true); + addComponent(world, CapturableObject, eid, true); +} diff --git a/src/inflators/grabbable.ts b/src/inflators/grabbable.ts index 2c8f89405e..ce1a8d1b38 100644 --- a/src/inflators/grabbable.ts +++ b/src/inflators/grabbable.ts @@ -4,6 +4,9 @@ import { CursorRaycastable, HandCollisionTarget, Holdable, + InteractableObject, + Networked, + NetworkedFloatyObject, OffersHandConstraint, OffersRemoteConstraint, RemoteHoverTarget @@ -24,3 +27,9 @@ export function inflateGrabbable(world: HubsWorld, eid: number, props: Grabbable } addComponent(world, Holdable, eid); } + +export function inflateGLTFGrabbable(world: HubsWorld, eid: number, props: GrabbableParams) { + inflateGrabbable(world, eid, props); + addComponent(world, InteractableObject, eid, true); + addComponent(world, Networked, eid, true); +} diff --git a/src/inflators/loop-animation.ts b/src/inflators/loop-animation.ts index d21665bec7..5c7c1da595 100644 --- a/src/inflators/loop-animation.ts +++ b/src/inflators/loop-animation.ts @@ -29,17 +29,11 @@ const ELEMENT_DEFAULTS: Required = { timeScale: 1.0 }; -const DEFAULTS: Required = [ELEMENT_DEFAULTS]; - export function inflateLoopAnimationInitialize( world: HubsWorld, eid: number, params: LoopAnimationParams = [] ): number { - if (params.length === 0) { - params = DEFAULTS; - } - const componentParams = []; for (let i = 0; i < params.length; i++) { const requiredParams = Object.assign({}, ELEMENT_DEFAULTS, params[i]) as Required; diff --git a/src/inflators/media-frame.js b/src/inflators/media-frame.js index f5e50292de..11738e31c9 100644 --- a/src/inflators/media-frame.js +++ b/src/inflators/media-frame.js @@ -33,7 +33,9 @@ const DEFAULTS = { bounds: { x: 1, y: 1, z: 1 }, mediaType: "all", scaleToBounds: true, - align: { x: "center", y: "center", z: "center" } + align: { x: "center", y: "center", z: "center" }, + active: true, + locked: false }; export function inflateMediaFrame(world, eid, componentProps) { componentProps = Object.assign({}, DEFAULTS, componentProps); @@ -79,17 +81,16 @@ export function inflateMediaFrame(world, eid, componentProps) { addComponent(world, MediaFrame, eid, true); addComponent(world, NetworkedMediaFrame, eid, true); + NetworkedMediaFrame.flags[eid] |= MEDIA_FRAME_FLAGS.ACTIVE; + if (componentProps.snapToCenter) { + NetworkedMediaFrame.flags[eid] |= MEDIA_FRAME_FLAGS.SNAP_TO_CENTER; + } + if (!hasComponent(world, Networked, eid)) addComponent(world, Networked, eid); // Media types accepted - MediaFrame.mediaType[eid] = { - all: MediaType.ALL, - "all-2d": MediaType.ALL_2D, - model: MediaType.MODEL, - image: MediaType.IMAGE, - video: MediaType.VIDEO, - pdf: MediaType.PDF - }[componentProps.mediaType]; + MediaFrame.mediaType[eid] = MediaTypes[componentProps.mediaType]; + NetworkedMediaFrame.mediaType[eid] = MediaFrame.mediaType[eid]; // Bounds MediaFrame.bounds[eid].set([componentProps.bounds.x, componentProps.bounds.y, componentProps.bounds.z]); // Axis alignment @@ -112,6 +113,13 @@ export function inflateMediaFrame(world, eid, componentProps) { if (componentProps.scaleToBounds) flags |= MEDIA_FRAME_FLAGS.SCALE_TO_BOUNDS; MediaFrame.flags[eid] = flags; + if (componentProps.active) { + NetworkedMediaFrame.flags[eid] |= MEDIA_FRAME_FLAGS.ACTIVE; + } + if (componentProps.locked) { + NetworkedMediaFrame.flags[eid] |= MEDIA_FRAME_FLAGS.LOCKED; + } + inflateRigidBody(world, eid, { type: Type.KINEMATIC, collisionGroup: COLLISION_LAYERS.MEDIA_FRAMES, diff --git a/src/inflators/model.tsx b/src/inflators/model.tsx index 5feefedd72..8e8a1bf3b2 100644 --- a/src/inflators/model.tsx +++ b/src/inflators/model.tsx @@ -7,12 +7,19 @@ import { mapMaterials } from "../utils/material-utils"; import { EntityID } from "../utils/networking-types"; import { inflateLoopAnimationInitialize, LoopAnimationParams } from "./loop-animation"; -function camelCase(s: string) { +export function camelCase(s: string) { return s.replace(/-(\w)/g, (_, m) => m.toUpperCase()); } export type ModelParams = { model: Object3D }; +export type GLTFLinkResolverFn = ( + world: HubsWorld, + model: Object3D, + rootEid: EntityID, + idx2eid: Map +) => void; + // These components are all handled in some special way, not through inflators const ignoredComponents = [ "visible", @@ -24,7 +31,7 @@ const ignoredComponents = [ "loop-animation" ]; -function inflateComponents( +export function inflateComponents( world: HubsWorld, eid: number, components: { [componentName: string]: any }, @@ -59,6 +66,7 @@ function inflateComponents( }); } +export const gltfLinkResolvers = new Array(); export function inflateModel(world: HubsWorld, rootEid: number, { model }: ModelParams) { const swap: [old: Object3D, replacement: Object3D][] = []; const idx2eid = new Map(); @@ -166,5 +174,7 @@ export function inflateModel(world: HubsWorld, rootEid: number, { model }: Model inflateLoopAnimationInitialize(world, rootEid, loopAnimationParams); } + gltfLinkResolvers.forEach(resolved => resolved(world, model, rootEid, idx2eid)); + addComponent(world, GLTFModel, rootEid); } diff --git a/src/inflators/rigid-body.ts b/src/inflators/rigid-body.ts index 54f2cf55e5..b380c37517 100644 --- a/src/inflators/rigid-body.ts +++ b/src/inflators/rigid-body.ts @@ -1,7 +1,8 @@ import { addComponent } from "bitecs"; -import { Rigidbody } from "../bit-components"; +import { NetworkedRigidBody, Rigidbody } from "../bit-components"; import { HubsWorld } from "../app"; import { CONSTANTS } from "three-ammo"; +import { COLLISION_LAYERS } from "../constants"; export enum Type { STATIC = 0, @@ -9,6 +10,13 @@ export enum Type { KINEMATIC } +export enum CollisionGroup { + OBJECTS = "objects", + ENVIRONMENT = "environment", + TRIGGERS = "triggers", + AVATARS = "avatars" +} + export enum ActivationState { ACTIVE_TAG = 0, ISLAND_SLEEPING = 1, @@ -17,6 +25,23 @@ export enum ActivationState { DISABLE_SIMULATION = 4 } +export type BodyParams = { + type: string; + mass: number; + gravity: { x: number; y: number; z: number }; + linearDamping: number; + angularDamping: number; + linearSleepingThreshold: number; + angularSleepingThreshold: number; + angularFactor: { x: number; y: number; z: number }; + activationState: string; + emitCollisionEvents: boolean; + disableCollision: boolean; + collisionFilterGroup: number; + collisionFilterMask: number; + scaleAutoUpdate: boolean; +}; + export type RigidBodyParams = { type: Type; mass: number; @@ -61,10 +86,50 @@ export const getTypeString = (eid: number) => { return Object.values(CONSTANTS.TYPE)[Rigidbody.type[eid]]; }; -export const getActivationStateString = (eid: number) => { +export const getStringFromActivationState = (eid: number) => { return Object.values(CONSTANTS.ACTIVATION_STATE)[Rigidbody.activationState[eid]]; }; +export const getActivationStateFromString = (activationState: string) => { + switch (activationState) { + case CONSTANTS.ACTIVATION_STATE.ACTIVE_TAG: + return ActivationState.ACTIVE_TAG; + case CONSTANTS.ACTIVATION_STATE.DISABLE_DEACTIVATION: + return ActivationState.DISABLE_DEACTIVATION; + case CONSTANTS.ACTIVATION_STATE.DISABLE_SIMULATION: + return ActivationState.DISABLE_SIMULATION; + case CONSTANTS.ACTIVATION_STATE.ISLAND_SLEEPING: + return ActivationState.ISLAND_SLEEPING; + case CONSTANTS.ACTIVATION_STATE.WANTS_DEACTIVATION: + return ActivationState.WANTS_DEACTIVATION; + } + return ActivationState.ACTIVE_TAG; +}; + +export const getTypeFromBodyType = (type: string) => { + switch (type) { + case "static": + return Type.STATIC; + case "dynamic": + return Type.DYNAMIC; + case "kinematic": + return Type.KINEMATIC; + } + return Type.KINEMATIC; +}; + +export const getBodyTypeFromType = (type: Type) => { + switch (type) { + case Type.STATIC: + return "static"; + case Type.DYNAMIC: + return "dynamic"; + case Type.KINEMATIC: + return "kinematic"; + } + return "kinematic"; +}; + export const getBodyFromRigidBody = (eid: number) => { return { mass: Rigidbody.mass[eid], @@ -78,44 +143,154 @@ export const getBodyFromRigidBody = (eid: number) => { y: Rigidbody.angularFactor[eid][1], z: Rigidbody.angularFactor[eid][2] }, - activationState: getActivationStateString(eid), - emitCollisionEvents: Rigidbody.flags[eid] & RIGID_BODY_FLAGS.EMIT_COLLISION_EVENTS, - scaleAutoUpdate: Rigidbody.flags[eid] & RIGID_BODY_FLAGS.SCALE_AUTO_UPDATE, + activationState: getStringFromActivationState(eid), + emitCollisionEvents: (Rigidbody.flags[eid] & RIGID_BODY_FLAGS.EMIT_COLLISION_EVENTS) !== 0, + scaleAutoUpdate: (Rigidbody.flags[eid] & RIGID_BODY_FLAGS.SCALE_AUTO_UPDATE) !== 0, type: getTypeString(eid), - disableCollision: Rigidbody.flags[eid] & RIGID_BODY_FLAGS.DISABLE_COLLISION, + disableCollision: (Rigidbody.flags[eid] & RIGID_BODY_FLAGS.DISABLE_COLLISION) !== 0, collisionFilterGroup: Rigidbody.collisionFilterGroup[eid], collisionFilterMask: Rigidbody.collisionFilterMask[eid] }; }; -const updateRigidBody = (eid: number, params: RigidBodyParams) => { - Rigidbody.type[eid] = params.type; - Rigidbody.mass[eid] = params.mass; - Rigidbody.gravity[eid].set(params.gravity); - Rigidbody.linearDamping[eid] = params.linearDamping; - Rigidbody.angularDamping[eid] = params.angularDamping; - Rigidbody.linearSleepingThreshold[eid] = params.linearSleepingThreshold; - Rigidbody.angularSleepingThreshold[eid] = params.angularSleepingThreshold; - Rigidbody.angularFactor[eid].set(params.angularFactor); - Rigidbody.activationState[eid] = params.activationState; - params.emitCollisionEvents && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.EMIT_COLLISION_EVENTS); - params.disableCollision && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.DISABLE_COLLISION); - Rigidbody.collisionFilterGroup[eid] = params.collisionGroup; - Rigidbody.collisionFilterMask[eid] = params.collisionMask; - params.scaleAutoUpdate && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.SCALE_AUTO_UPDATE); +export const updateBodyParams = (eid: number, params: Partial) => { + const currentParams = getBodyFromRigidBody(eid); + const bodyParams = Object.assign({}, currentParams, params) as BodyParams; + + Rigidbody.type[eid] = getTypeFromBodyType(bodyParams.type); + Rigidbody.mass[eid] = bodyParams.mass; + Rigidbody.gravity[eid].set([bodyParams.gravity.x, bodyParams.gravity.y, bodyParams.gravity.z]); + Rigidbody.linearDamping[eid] = bodyParams.linearDamping; + Rigidbody.angularDamping[eid] = bodyParams.angularDamping; + Rigidbody.linearSleepingThreshold[eid] = bodyParams.linearSleepingThreshold; + Rigidbody.angularSleepingThreshold[eid] = bodyParams.angularSleepingThreshold; + Rigidbody.angularFactor[eid].set([ + bodyParams.angularFactor.x, + bodyParams.angularFactor.y, + bodyParams.angularFactor.z + ]); + Rigidbody.activationState[eid] = getActivationStateFromString(params.activationState!); + bodyParams.emitCollisionEvents && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.EMIT_COLLISION_EVENTS); + bodyParams.disableCollision && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.DISABLE_COLLISION); + Rigidbody.collisionFilterGroup[eid] = bodyParams.collisionFilterGroup; + Rigidbody.collisionFilterMask[eid] = bodyParams.collisionFilterMask; + bodyParams.scaleAutoUpdate && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.SCALE_AUTO_UPDATE); }; -export const updateRigiBodyParams = (eid: number, params: Partial) => { - const currentParams = getBodyFromRigidBody(eid); - const bodyParams = Object.assign({}, currentParams, params); - updateRigidBody(eid, bodyParams); +export const updateRigidBodyParams = (eid: number, params: Partial) => { + if (params.type !== undefined) { + Rigidbody.type[eid] = params.type; + } + if (params.mass !== undefined) { + Rigidbody.mass[eid] = params.mass; + } + if (params.gravity !== undefined) { + Rigidbody.gravity[eid].set(params.gravity); + } + if (params.linearDamping !== undefined) { + Rigidbody.linearDamping[eid] = params.linearDamping; + } + if (params.angularDamping !== undefined) { + Rigidbody.angularDamping[eid] = params.angularDamping; + } + if (params.linearSleepingThreshold !== undefined) { + Rigidbody.linearSleepingThreshold[eid] = params.linearSleepingThreshold; + } + if (params.angularSleepingThreshold !== undefined) { + Rigidbody.angularSleepingThreshold[eid] = params.angularSleepingThreshold; + } + if (params.angularFactor !== undefined) { + Rigidbody.angularFactor[eid].set(params.angularFactor); + } + if (params.activationState !== undefined) { + Rigidbody.activationState[eid] = params.activationState; + } + if (params.emitCollisionEvents !== undefined) { + Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.EMIT_COLLISION_EVENTS; + } + if (params.disableCollision !== undefined) { + Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.DISABLE_COLLISION; + } + if (params.scaleAutoUpdate !== undefined) { + Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.SCALE_AUTO_UPDATE; + } + if (params.collisionGroup !== undefined) { + Rigidbody.collisionFilterGroup[eid] = params.collisionGroup; + } + if (params.collisionMask !== undefined) { + Rigidbody.collisionFilterMask[eid] = params.collisionMask; + } }; export function inflateRigidBody(world: HubsWorld, eid: number, params: Partial) { - const bodyParams = Object.assign({}, DEFAULTS, params); + const bodyParams = Object.assign({}, DEFAULTS, params) as RigidBodyParams; addComponent(world, Rigidbody, eid); - updateRigidBody(eid, bodyParams); + addComponent(world, NetworkedRigidBody, eid); + + Rigidbody.type[eid] = bodyParams.type; + Rigidbody.mass[eid] = bodyParams.mass; + Rigidbody.gravity[eid].set(bodyParams.gravity); + Rigidbody.linearDamping[eid] = bodyParams.linearDamping; + Rigidbody.angularDamping[eid] = bodyParams.angularDamping; + Rigidbody.linearSleepingThreshold[eid] = bodyParams.linearSleepingThreshold; + Rigidbody.angularSleepingThreshold[eid] = bodyParams.angularSleepingThreshold; + Rigidbody.angularFactor[eid].set(bodyParams.angularFactor); + Rigidbody.activationState[eid] = bodyParams.activationState; + bodyParams.emitCollisionEvents && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.EMIT_COLLISION_EVENTS); + bodyParams.disableCollision && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.DISABLE_COLLISION); + Rigidbody.collisionFilterGroup[eid] = bodyParams.collisionGroup; + Rigidbody.collisionFilterMask[eid] = bodyParams.collisionMask; + bodyParams.scaleAutoUpdate && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.SCALE_AUTO_UPDATE); + + return eid; +} + +export enum GLTFRigidBodyType { + STATIC = "static", + DYNAMIC = "dynamic", + KINEMATIC = "kinematic" +} + +export enum GLTFRigidBodyCollisionGroup { + OBJECTS = "objects", + ENVIRONMENT = "environment", + TRIGGERS = "triggers", + AVATARS = "avatars", + MEDIA_FRAMES = "media-frames" +} + +const GLTF_DEFAULTS = { + ...DEFAULTS, + type: GLTFRigidBodyType.DYNAMIC, + collisionGroup: GLTFRigidBodyCollisionGroup.OBJECTS, + collisionMask: [GLTFRigidBodyCollisionGroup.AVATARS] +}; + +const gltfGroupToLayer = { + [GLTFRigidBodyCollisionGroup.OBJECTS]: COLLISION_LAYERS.INTERACTABLES, + [GLTFRigidBodyCollisionGroup.ENVIRONMENT]: COLLISION_LAYERS.ENVIRONMENT, + [GLTFRigidBodyCollisionGroup.TRIGGERS]: COLLISION_LAYERS.TRIGGERS, + [GLTFRigidBodyCollisionGroup.AVATARS]: COLLISION_LAYERS.AVATAR, + [GLTFRigidBodyCollisionGroup.MEDIA_FRAMES]: COLLISION_LAYERS.MEDIA_FRAMES +} as const; + +export interface GLTFRigidBodyParams + extends Partial> { + type?: GLTFRigidBodyType; + collisionGroup?: GLTFRigidBodyCollisionGroup; + collisionMask?: GLTFRigidBodyCollisionGroup[]; +} + +export function inflateGLTFRigidBody(world: HubsWorld, eid: number, params: GLTFRigidBodyParams) { + const bodyParams = Object.assign({}, GLTF_DEFAULTS, params); + + inflateRigidBody(world, eid, { + ...bodyParams, + type: Object.values(GLTFRigidBodyType).indexOf(bodyParams.type), + collisionGroup: gltfGroupToLayer[bodyParams.collisionGroup], + collisionMask: bodyParams.collisionMask.reduce((acc, m) => acc | gltfGroupToLayer[m], 0) + }); return eid; } diff --git a/src/inflators/spawner.ts b/src/inflators/spawner.ts index 78c6be0310..1587fadb39 100644 --- a/src/inflators/spawner.ts +++ b/src/inflators/spawner.ts @@ -42,7 +42,6 @@ export function inflateSpawner(world: HubsWorld, eid: number, props: SpawnerPara inflateRigidBody(world, eid, { mass: 0, - type: Type.STATIC, collisionGroup: COLLISION_LAYERS.INTERACTABLES, collisionMask: COLLISION_LAYERS.DEFAULT_SPAWNER }); diff --git a/src/inflators/text.ts b/src/inflators/text.ts index 418d7a0e43..cc7f38480c 100644 --- a/src/inflators/text.ts +++ b/src/inflators/text.ts @@ -1,10 +1,255 @@ import { addComponent } from "bitecs"; -import { BackSide, DoubleSide, FrontSide } from "three"; +import { BackSide, Color, DoubleSide, FrontSide, Side } from "three"; import { Text as TroikaText } from "troika-three-text"; import { HubsWorld } from "../app"; -import { TextTag } from "../bit-components"; +import { Networked, NetworkedText, TextTag } from "../bit-components"; import { addObject3DComponent } from "../utils/jsx-entity"; +export const ANCHOR_X = { + LEFT: 1 << 0, + CENTER: 1 << 1, + RIGHT: 1 << 2 +}; + +export function anchorXToFlag(anchorX: string) { + switch (anchorX) { + case "center": + return ANCHOR_X.CENTER; + case "left": + return ANCHOR_X.LEFT; + case "right": + return ANCHOR_X.RIGHT; + } + return ANCHOR_X.CENTER; +} + +export function flagToAnchorX(flag: number) { + switch (flag) { + case ANCHOR_X.CENTER: + return "center"; + case ANCHOR_X.LEFT: + return "left"; + case ANCHOR_X.RIGHT: + return "right"; + } + return "center"; +} + +export const ANCHOR_Y = { + TOP: 1 << 0, + TOP_BASELINE: 1 << 1, + TOP_CAP: 1 << 2, + TOP_EX: 1 << 3, + MIDDLE: 1 << 4, + BOTTOM_BASELINE: 1 << 5, + BOTTOM: 1 << 6 +}; + +export function anchorYToFlag(anchorY: string) { + switch (anchorY) { + case "top": + return ANCHOR_Y.TOP; + case "top-baseline": + return ANCHOR_Y.TOP_BASELINE; + case "top-cap": + return ANCHOR_Y.TOP_CAP; + case "top-ex": + return ANCHOR_Y.TOP_EX; + case "middle": + return ANCHOR_Y.MIDDLE; + case "bottom-baseline": + return ANCHOR_Y.BOTTOM_BASELINE; + case "bottom": + return ANCHOR_Y.BOTTOM; + } + return ANCHOR_Y.MIDDLE; +} + +export function flagToAnchorY(flag: number) { + switch (flag) { + case ANCHOR_Y.TOP: + return "top"; + case ANCHOR_Y.TOP_BASELINE: + return "top-baseline"; + case ANCHOR_Y.TOP_CAP: + return "top-cap"; + case ANCHOR_Y.TOP_EX: + return "top-ex"; + case ANCHOR_Y.MIDDLE: + return "middle"; + case ANCHOR_Y.BOTTOM_BASELINE: + return "bottom-baseline"; + case ANCHOR_Y.BOTTOM: + return "bottom"; + } + return "middle"; +} + +export const DIRECTION = { + AUTO: 1 << 0, + LTR: 1 << 1, + RTL: 1 << 2 +}; + +export function directionToFlag(direction: string) { + switch (direction) { + case "auto": + return DIRECTION.AUTO; + case "ltr": + return DIRECTION.LTR; + case "rtl": + return DIRECTION.RTL; + } + return DIRECTION.AUTO; +} + +export function flagToDirection(flag: number) { + switch (flag) { + case DIRECTION.AUTO: + return "auto"; + case DIRECTION.LTR: + return "ltr"; + case DIRECTION.RTL: + return "rtl"; + } + return "auto"; +} + +export const OVERFLOW_WRAP = { + NORMAL: 1 << 0, + BREAK_WORD: 1 << 1 +}; + +export function overflowWrapToFlag(overflowWrap: string) { + switch (overflowWrap) { + case "normal": + return OVERFLOW_WRAP.NORMAL; + case "break-word": + return OVERFLOW_WRAP.BREAK_WORD; + } + return OVERFLOW_WRAP.NORMAL; +} + +export function flagToOverflowWrap(flag: number) { + switch (flag) { + case OVERFLOW_WRAP.NORMAL: + return "normal"; + case OVERFLOW_WRAP.BREAK_WORD: + return "break-word"; + } + return "normal"; +} + +export const SIDE = { + FRONT: 1 << 0, + BACK: 1 << 1, + DOUBLE: 1 << 2 +}; + +export function sideToFlag(side: string) { + switch (side) { + case "front": + return SIDE.FRONT; + case "back": + return SIDE.BACK; + case "double": + return SIDE.DOUBLE; + } + return SIDE.FRONT; +} + +export function flagToSide(flag: number) { + switch (flag) { + case SIDE.FRONT: + return "front"; + case SIDE.BACK: + return "back"; + case SIDE.DOUBLE: + return "double"; + } + return "front"; +} + +export const TEXT_ALIGN = { + LEFT: 1 << 0, + RIGHT: 1 << 1, + CENTER: 1 << 2, + JUSTIFY: 1 << 2 +}; + +export function textAlignToFlag(textAlign: string) { + switch (textAlign) { + case "left": + return TEXT_ALIGN.LEFT; + case "right": + return TEXT_ALIGN.RIGHT; + case "center": + return TEXT_ALIGN.CENTER; + case "justify": + return TEXT_ALIGN.JUSTIFY; + } + return TEXT_ALIGN.CENTER; +} + +export function flagToTextAlign(flag: number) { + switch (flag) { + case TEXT_ALIGN.LEFT: + return "left"; + case TEXT_ALIGN.RIGHT: + return "right"; + case TEXT_ALIGN.CENTER: + return "center"; + case TEXT_ALIGN.JUSTIFY: + return "justify"; + } + return "center"; +} + +export const WHITESPACE = { + NORMAL: 1 << 0, + NO_WRAP: 1 << 1 +}; + +export function whiteSpaceToFlag(whiteSpace: string) { + switch (whiteSpace) { + case "normal": + return WHITESPACE.NORMAL; + case "nowrap": + return WHITESPACE.NO_WRAP; + } + return WHITESPACE.NORMAL; +} + +export function flagToWhiteSpace(flag: number) { + switch (flag) { + case WHITESPACE.NORMAL: + return "normal"; + case WHITESPACE.NO_WRAP: + return "nowrap"; + } + return "normal"; +} + +export function numberOrStringToString(value: number | string) { + if (isNaN(Number(value))) { + return APP.getSid(value as string); + } else { + return APP.getSid(`${value as number}`); + } +} + +export function stringToNumberOrString(value: string): number | string { + if (!value) return 0; + if (value.indexOf("%") !== -1) { + return value; + } else { + return Number(value); + } +} + +export type NumberOrNormalT = number | "normal"; +export type NumberOrPctT = number | `${number}%`; + export type TextParams = { value: string; anchorX?: "left" | "center" | "right"; @@ -13,39 +258,53 @@ export type TextParams = { color?: string; curveRadius?: number; depthOffset?: number; - direction?: "auto" | "ltr" | "trl"; + direction?: "auto" | "ltr" | "rtl"; fillOpacity?: number; fontUrl?: string | null; fontSize?: number; glyphGeometryDetail?: number; gpuAccelerateSDF?: boolean; letterSpacing?: number; - lineHeight?: number | "normal"; + lineHeight?: NumberOrNormalT; maxWidth?: number; opacity?: number; - outlineBlur?: number | `${number}%`; + outlineBlur?: NumberOrPctT; outlineColor?: string; - outlineOffsetX?: number | `${number}%`; - outlineOffsetY?: number | `${number}%`; + outlineOffsetX?: NumberOrPctT; + outlineOffsetY?: NumberOrPctT; outlineOpacity?: number; - outlineWidth?: number | `${number}%`; + outlineWidth?: NumberOrPctT; overflowWrap?: "normal" | "break-word"; sdfGlyphSize?: number | null; side?: "front" | "back" | "double"; strokeColor?: string; strokeOpacity?: number; - strokeWidth?: number | `${number}%`; + strokeWidth?: NumberOrPctT; textAlign?: "left" | "right" | "center" | "justify"; textIndent?: number; whiteSpace?: "normal" | "nowrap"; }; -const THREE_SIDES = { +export const THREE_SIDES = { front: FrontSide, back: BackSide, double: DoubleSide }; +export function sideToThree(side: "front" | "back" | "double"): Side { + return THREE_SIDES[side]; +} + +export const THREE_TO_SIDE = { + [FrontSide]: "front", + [BackSide]: "back", + [DoubleSide]: "double" +}; + +export function threeToSide(side: Side) { + return THREE_TO_SIDE[side]; +} + const DEFAULTS: Required = { anchorX: "center", anchorY: "middle", @@ -92,7 +351,7 @@ const DEFAULTS: Required = { // glTF. If we notice problems with other parameters, we may add // casting of other parameters. // TODO: Add a generic mechanism to cast and validate inflator params. -const cast = (params: Required): Required => { +export const cast = (params: Required): Required => { const keys: Array = [ "curveRadius", "depthOffset", @@ -132,8 +391,8 @@ const cast = (params: Required): Required => { return params; }; -export function inflateText(world: HubsWorld, eid: number, params: TextParams) { - const requiredParams = cast(Object.assign({}, DEFAULTS, params) as Required); +const tmpColor = new Color(); +function createText(requiredParams: Required) { const text = new TroikaText(); text.material!.toneMapped = false; @@ -145,7 +404,7 @@ export function inflateText(world: HubsWorld, eid: number, params: TextParams) { text.anchorX = requiredParams.anchorX; text.anchorY = requiredParams.anchorY; text.clipRect = requiredParams.clipRect; - text.color = requiredParams.color; + text.color = tmpColor.set(requiredParams.color).getHex(); text.curveRadius = requiredParams.curveRadius; text.depthOffset = requiredParams.depthOffset; text.direction = requiredParams.direction; @@ -173,6 +432,52 @@ export function inflateText(world: HubsWorld, eid: number, params: TextParams) { text.sync(); + return text; +} + +export function inflateText(world: HubsWorld, eid: number, params: TextParams) { + const requiredParams = Object.assign({}, DEFAULTS, params) as Required; + const text = createText(requiredParams); + + addComponent(world, TextTag, eid); + addObject3DComponent(world, eid, text); +} + +export function inflateGLTFText(world: HubsWorld, eid: number, params: TextParams) { + const requiredParams = Object.assign({}, DEFAULTS, params) as Required; + const text = createText(requiredParams); + addComponent(world, TextTag, eid); + addComponent(world, Networked, eid); + addComponent(world, NetworkedText, eid); addObject3DComponent(world, eid, text); + + NetworkedText.text[eid] = APP.getSid(requiredParams.value); + NetworkedText.fontSize[eid] = requiredParams.fontSize; + NetworkedText.textAlign[eid] = textAlignToFlag(requiredParams.textAlign); + NetworkedText.anchorX[eid] = anchorXToFlag(requiredParams.anchorX); + NetworkedText.anchorY[eid] = anchorYToFlag(requiredParams.anchorY); + NetworkedText.color[eid] = tmpColor.set(requiredParams.color).getHex(); + NetworkedText.letterSpacing[eid] = requiredParams.letterSpacing; + NetworkedText.lineHeight[eid] = numberOrStringToString(requiredParams.lineHeight); + NetworkedText.outlineWidth[eid] = numberOrStringToString(requiredParams.outlineWidth); + NetworkedText.outlineColor[eid] = tmpColor.set(requiredParams.outlineColor).getHex(); + NetworkedText.outlineBlur[eid] = numberOrStringToString(requiredParams.outlineBlur); + NetworkedText.outlineOffsetX[eid] = numberOrStringToString(requiredParams.outlineOffsetX); + NetworkedText.outlineOffsetY[eid] = numberOrStringToString(requiredParams.outlineOffsetY); + NetworkedText.outlineOpacity[eid] = requiredParams.outlineOpacity; + NetworkedText.fillOpacity[eid] = requiredParams.fillOpacity; + NetworkedText.strokeWidth[eid] = numberOrStringToString(requiredParams.strokeWidth); + NetworkedText.strokeColor[eid] = tmpColor.set(requiredParams.strokeColor).getHex(); + NetworkedText.strokeOpacity[eid] = requiredParams.strokeOpacity; + NetworkedText.textIndent[eid] = requiredParams.textIndent; + NetworkedText.whiteSpace[eid] = whiteSpaceToFlag(requiredParams.whiteSpace); + NetworkedText.overflowWrap[eid] = overflowWrapToFlag(requiredParams.overflowWrap); + NetworkedText.opacity[eid] = requiredParams.opacity; + NetworkedText.side[eid] = sideToFlag(requiredParams.side); + NetworkedText.maxWidth[eid] = requiredParams.maxWidth; + NetworkedText.curveRadius[eid] = requiredParams.curveRadius; + NetworkedText.direction[eid] = directionToFlag(requiredParams.direction); + + return eid; } diff --git a/src/inflators/video.ts b/src/inflators/video.ts index 8e087390bb..34dc7e07c3 100644 --- a/src/inflators/video.ts +++ b/src/inflators/video.ts @@ -8,7 +8,10 @@ import { EntityID } from "../utils/networking-types"; import { Texture } from "three"; export const VIDEO_FLAGS = { - CONTROLS: 1 << 0 + CONTROLS: 1 << 0, + AUTO_PLAY: 1 << 1, + LOOP: 1 << 2, + PAUSED: 1 << 3 }; export interface VideoParams { diff --git a/src/load-media-on-paste-or-drop.ts b/src/load-media-on-paste-or-drop.ts index 1d41d0fafe..17b48448fe 100644 --- a/src/load-media-on-paste-or-drop.ts +++ b/src/load-media-on-paste-or-drop.ts @@ -107,6 +107,7 @@ function onDrop(e: DragEvent) { if (qsTruthy("debugLocalScene")) { URL.revokeObjectURL(lastDebugScene); if (!e.dataTransfer?.files.length) return; + e.preventDefault(); const url = URL.createObjectURL(e.dataTransfer.files[0]); APP.hubChannel!.updateScene(url); lastDebugScene = url; diff --git a/src/prefabs/media.tsx b/src/prefabs/media.tsx index 9d30cc75ad..cee802a0c2 100644 --- a/src/prefabs/media.tsx +++ b/src/prefabs/media.tsx @@ -25,7 +25,7 @@ export function MediaPrefab(params: MediaLoaderParams): EntityDef { rigidbody={{ type: Type.KINEMATIC, collisionGroup: COLLISION_LAYERS.INTERACTABLES, - collisionMask: COLLISION_LAYERS.HANDS + collisionMask: COLLISION_LAYERS.DEFAULT_INTERACTABLE }} scale={[1, 1, 1]} inspectable diff --git a/src/react-components/debug-panel/ECSSidebar.js b/src/react-components/debug-panel/ECSSidebar.js index 1793990ad8..d04b97e714 100644 --- a/src/react-components/debug-panel/ECSSidebar.js +++ b/src/react-components/debug-panel/ECSSidebar.js @@ -70,9 +70,28 @@ function MaterialItem(props) { ); } +function TextureItem(props) { + const { tex, setSelectedObj } = props; + const displayName = formatObjectName(tex); + return ( +
+
{ + e.preventDefault(); + setSelectedObj(tex); + }} + > + {displayName} + {` [${tex.eid}]`} +
+
+ ); +} + export function formatComponentProps(eid, component) { const formatted = Object.keys(component).reduce((str, k, i, arr) => { - const val = component[k][eid]; + const val = component[k] instanceof Map ? component[k].get(eid) : component[k][eid]; const isStr = component[k][bitComponents.$isStringType]; str += ` ${k}: `; if (ArrayBuffer.isView(val)) { @@ -144,8 +163,10 @@ function RefreshButton({ onClick }) { ); } +export const extraSections = new Array(); const object3dQuery = defineQuery([bitComponents.Object3DTag]); const materialQuery = defineQuery([bitComponents.MaterialTag]); +const textureQuery = defineQuery([bitComponents.TextureTag]); function ECSDebugSidebar({ onClose, toggleObjExpand, @@ -159,6 +180,8 @@ function ECSDebugSidebar({ .map(eid => APP.world.eid2obj.get(eid)) .filter(o => !o.parent); const materials = materialQuery(APP.world).map(eid => APP.world.eid2mat.get(eid)); + const textures = textureQuery(APP.world).map(eid => APP.world.eid2tex.get(eid)); + const envRoot = document.getElementById("environment-root").object3D; return ( +
+ +
{orphaned.map(o => (
- {materials.map(m => ( - - ))} + {materials.map(m => m && )} +
+
+ {textures.map(t => t && )}
+ {extraSections.map(section => section(APP.world, setSelectedObj))}
{selectedObj && }
diff --git a/src/react-components/room/ChatSidebar.js b/src/react-components/room/ChatSidebar.js index c7dab949fa..bd411ea078 100644 --- a/src/react-components/room/ChatSidebar.js +++ b/src/react-components/room/ChatSidebar.js @@ -324,6 +324,8 @@ export function formatSystemMessage(entry, intl) { values={{ hubName: {entry.hubName} }} /> ); + case "script_message": + return "script: " + entry.msg; case "log": return intl.formatMessage(logMessages[entry.messageType], entry.props); default: diff --git a/src/react-components/room/contexts/ChatContext.tsx b/src/react-components/room/contexts/ChatContext.tsx index bb56c0f22e..6ba664e28b 100644 --- a/src/react-components/room/contexts/ChatContext.tsx +++ b/src/react-components/room/contexts/ChatContext.tsx @@ -81,6 +81,7 @@ function updateMessageGroups(messageGroups: any[], newMessage: NewMessageT) { case "scene_changed": case "hub_name_changed": case "hub_changed": + case "script_message": case "log": return [ ...messageGroups, diff --git a/src/systems/bit-constraints-system.js b/src/systems/bit-constraints-system.js index 1fa79aea1a..18b02cce8f 100644 --- a/src/systems/bit-constraints-system.js +++ b/src/systems/bit-constraints-system.js @@ -21,6 +21,8 @@ import { ConstraintRemoteLeft, ConstraintRemoteRight } from "../bit-components"; +import { Type, getBodyFromRigidBody, getBodyTypeFromType } from "../inflators/rigid-body"; +import { updatePrevBodyType } from "./bit-physics"; const queryRemoteRight = defineQuery([HeldRemoteRight, OffersRemoteConstraint]); const queryEnterRemoteRight = enterQuery(queryRemoteRight); @@ -45,7 +47,8 @@ function add(world, physicsSystem, interactor, constraintComponent, entities) { for (let i = 0; i < entities.length; i++) { const eid = findAncestorEntity(world, entities[i], ancestor => hasComponent(world, Rigidbody, ancestor)); if (!entityExists(world, eid)) continue; - physicsSystem.updateRigidBodyOptions(eid, grabBodyOptions); + updatePrevBodyType(world, eid); + physicsSystem.updateRigidBody(eid, grabBodyOptions); physicsSystem.addConstraint(interactor, Rigidbody.bodyId[eid], Rigidbody.bodyId[interactor], {}); addComponent(world, Constraint, eid); addComponent(world, constraintComponent, eid); @@ -57,8 +60,16 @@ function remove(world, offersConstraint, constraintComponent, physicsSystem, int const eid = findAncestorEntity(world, entities[i], ancestor => hasComponent(world, Rigidbody, ancestor)); if (!entityExists(world, eid)) continue; if (hasComponent(world, offersConstraint, entities[i]) && hasComponent(world, Rigidbody, eid)) { - physicsSystem.updateRigidBodyOptions(eid, releaseBodyOptions); + physicsSystem.updateRigidBody(eid, { + type: getBodyTypeFromType(Rigidbody.prevType[eid]), + ...releaseBodyOptions + }); physicsSystem.removeConstraint(interactor); + if (Rigidbody.type[eid] === Type.DYNAMIC) { + physicsSystem.activateBody(Rigidbody.bodyId[eid]); + // This shouldn't be necessary but for some reason it doesn't activate the body if we don't update the body afterwards + physicsSystem.updateRigidBody(eid, getBodyFromRigidBody(eid)); + } removeComponent(world, constraintComponent, eid); if ( !hasComponent(world, ConstraintHandLeft, eid) && diff --git a/src/systems/bit-media-frames.js b/src/systems/bit-media-frames.js index f56ed3ad79..fd07c18b1c 100644 --- a/src/systems/bit-media-frames.js +++ b/src/systems/bit-media-frames.js @@ -9,7 +9,8 @@ import { hasComponent, addEntity, removeEntity, - addComponent + addComponent, + removeComponent } from "bitecs"; import { AEntity, @@ -19,13 +20,15 @@ import { MediaContentBounds, MediaFrame, MediaImage, - MediaLoaded, MediaPDF, MediaVideo, Networked, NetworkedMediaFrame, Owned, - Rigidbody + Rigidbody, + Capturable, + CapturableObject, + Holdable } from "../bit-components"; import { MediaType } from "../utils/media-utils"; import { cloneObject3D, disposeNode, setMatrixWorld } from "../utils/three-utils"; @@ -36,12 +39,13 @@ import { addObject3DComponent } from "../utils/jsx-entity"; import { updateMaterials } from "../utils/material-utils"; import { MEDIA_FRAME_FLAGS, AxisAlignType } from "../inflators/media-frame"; import { Matrix4, NormalBlending, Quaternion, RGBAFormat, Vector3 } from "three"; +import { getBox } from "../utils/auto-box-collider"; const EMPTY_COLOR = 0x6fc0fd; const HOVER_COLOR = 0x2f80ed; const FULL_COLOR = 0x808080; -const mediaFramesQuery = defineQuery([MediaFrame]); +const mediaFramesQuery = defineQuery([MediaFrame, NetworkedMediaFrame]); const enteredMediaFramesQuery = enterQuery(mediaFramesQuery); const exitedMediaFramesQuery = exitQuery(mediaFramesQuery); @@ -54,11 +58,12 @@ function mediaTypeMaskFor(world, eid) { mediaTypeMask |= el.components["media-image"] && MediaType.IMAGE; mediaTypeMask |= el.components["media-pdf"] && MediaType.PDF; } else { - const mediaEid = findChildWithComponent(world, MediaLoaded, eid); - mediaTypeMask |= hasComponent(world, GLTFModel, mediaEid) && MediaType.MODEL; - mediaTypeMask |= hasComponent(world, MediaVideo, mediaEid) && MediaType.VIDEO; - mediaTypeMask |= hasComponent(world, MediaImage, mediaEid) && MediaType.IMAGE; - mediaTypeMask |= hasComponent(world, MediaPDF, mediaEid) && MediaType.PDF; + const capturable = findChildWithComponent(world, Capturable, eid); + mediaTypeMask |= hasComponent(world, GLTFModel, capturable) && MediaType.MODEL; + mediaTypeMask |= hasComponent(world, MediaVideo, capturable) && MediaType.VIDEO; + mediaTypeMask |= hasComponent(world, MediaImage, capturable) && MediaType.IMAGE; + mediaTypeMask |= hasComponent(world, MediaPDF, capturable) && MediaType.PDF; + mediaTypeMask |= hasComponent(world, CapturableObject, capturable) && MediaType.OBJECT; } return mediaTypeMask; } @@ -276,10 +281,21 @@ export function cleanupMediaFrame(obj) { }); } +const tmpVector = new Vector3(); const takeOwnershipOnTimeout = new Map(); const heldQuery = defineQuery([Held]); +const capturableQuery = defineQuery([Capturable, CapturableObject]); +const capturableEnterQuery = enterQuery(capturableQuery); // const droppedQuery = exitQuery(heldQuery); export function mediaFramesSystem(world, physicsSystem) { + // Spawned media handles this in the media-loading system. We should probably unify this in a different system. + capturableEnterQuery(world).forEach(eid => { + const obj = world.eid2obj.get(eid); + const box = getBox(obj, obj); + box.getSize(tmpVector); + addComponent(world, MediaContentBounds, eid); + MediaContentBounds.bounds[eid].set(tmpVector.toArray()); + }); enteredMediaFramesQuery(world).forEach(eid => { if (Networked.owner[eid] === APP.getSid("reticulum")) { takeOwnershipOnTimeout.set( @@ -314,46 +330,71 @@ export function mediaFramesSystem(world, physicsSystem) { const frame = mediaFrames[i]; const capturedEid = world.nid2eid.get(MediaFrame.capturedNid[frame]) || 0; + + if (MediaFrame.flags[frame] !== NetworkedMediaFrame.flags[frame]) { + MediaFrame.flags[frame] = NetworkedMediaFrame.flags[frame]; + } + + if (MediaFrame.mediaType[frame] !== NetworkedMediaFrame.mediaType[frame]) { + MediaFrame.mediaType[frame] = NetworkedMediaFrame.mediaType[frame]; + } + + // This currently only works for Capturables (not spawned media). Do we want to support that? + if (capturedEid) { + if (MediaFrame.flags[frame] & MEDIA_FRAME_FLAGS.LOCKED) { + removeComponent(world, Holdable, capturedEid); + } else { + addComponent(world, Holdable, capturedEid); + } + } + + if ((MediaFrame.flags[frame] & MEDIA_FRAME_FLAGS.ACTIVE) === 0) { + NetworkedMediaFrame.capturedNid[frame] = 0; + NetworkedMediaFrame.scale[frame].set(zero); + } + const isCapturedOwned = hasComponent(world, Owned, capturedEid); const isCapturedHeld = findChildWithComponent(world, Held, capturedEid); const isCapturedColliding = capturedEid && isEntityColliding(physicsSystem, frame, capturedEid); const isFrameDeleting = findAncestorWithComponent(world, Deleting, frame); const isFrameOwned = hasComponent(world, Owned, frame); - if (capturedEid && isCapturedOwned && !isCapturedHeld && !isFrameDeleting && isCapturedColliding) { - snapToFrame(world, frame, capturedEid); - physicsSystem.updateRigidBodyOptions(capturedEid, { type: "kinematic" }); - } else if ( - (isFrameOwned && MediaFrame.capturedNid[frame] && world.deletedNids.has(MediaFrame.capturedNid[frame])) || - (capturedEid && isCapturedOwned && !isCapturedColliding) || - isFrameDeleting - ) { - takeOwnership(world, frame); - NetworkedMediaFrame.capturedNid[frame] = 0; - NetworkedMediaFrame.scale[frame].set(zero); - // TODO BUG: If an entity I do not own is capturedEid by the media frame, - // and then I take ownership of the entity (by grabbing it), - // the physics system does not immediately notice the entity isCapturedColliding with the frame, - // so I immediately think the frame should be emptied. - } else if (isFrameOwned && MediaFrame.capturedNid[frame] && !capturedEid) { - NetworkedMediaFrame.capturedNid[frame] = 0; - NetworkedMediaFrame.scale[frame].set(zero); - } else if (!NetworkedMediaFrame.capturedNid[frame]) { - const capturable = getCapturableEntity(world, physicsSystem, frame); - if ( - capturable && - (hasComponent(world, Owned, capturable) || (isOwnedByRet(world, capturable) && isFrameOwned)) && - !findChildWithComponent(world, Held, capturable) && - !inOtherFrame(world, frame, capturable) + if (MediaFrame.flags[frame] & MEDIA_FRAME_FLAGS.ACTIVE) { + if (capturedEid && isCapturedOwned && !isCapturedHeld && !isFrameDeleting && isCapturedColliding) { + snapToFrame(world, frame, capturedEid); + physicsSystem.updateRigidBody(capturedEid, { type: "kinematic" }); + } else if ( + (isFrameOwned && MediaFrame.capturedNid[frame] && world.deletedNids.has(MediaFrame.capturedNid[frame])) || + (capturedEid && isCapturedOwned && !isCapturedColliding) || + isFrameDeleting ) { takeOwnership(world, frame); - takeOwnership(world, capturable); - NetworkedMediaFrame.capturedNid[frame] = Networked.id[capturable]; - const obj = world.eid2obj.get(capturable); - obj.updateMatrices(); - tmpVec3.setFromMatrixScale(obj.matrixWorld).toArray(NetworkedMediaFrame.scale[frame]); - snapToFrame(world, frame, capturable); - physicsSystem.updateRigidBodyOptions(capturable, { type: "kinematic" }); + NetworkedMediaFrame.capturedNid[frame] = 0; + NetworkedMediaFrame.scale[frame].set(zero); + // TODO BUG: If an entity I do not own is capturedEid by the media frame, + // and then I take ownership of the entity (by grabbing it), + // the physics system does not immediately notice the entity isCapturedColliding with the frame, + // so I immediately think the frame should be emptied. + } else if (isFrameOwned && MediaFrame.capturedNid[frame] && !capturedEid) { + NetworkedMediaFrame.capturedNid[frame] = 0; + NetworkedMediaFrame.scale[frame].set(zero); + } else if (!NetworkedMediaFrame.capturedNid[frame]) { + const capturable = getCapturableEntity(world, physicsSystem, frame); + if ( + capturable && + (hasComponent(world, Owned, capturable) || (isOwnedByRet(world, capturable) && isFrameOwned)) && + !findChildWithComponent(world, Held, capturable) && + !inOtherFrame(world, frame, capturable) + ) { + takeOwnership(world, frame); + takeOwnership(world, capturable); + NetworkedMediaFrame.capturedNid[frame] = Networked.id[capturable]; + const obj = world.eid2obj.get(capturable); + obj.updateMatrices(); + tmpVec3.setFromMatrixScale(obj.matrixWorld).toArray(NetworkedMediaFrame.scale[frame]); + snapToFrame(world, frame, capturable); + physicsSystem.updateRigidBody(capturable, { type: "kinematic" }); + } } } @@ -366,12 +407,14 @@ export function mediaFramesSystem(world, physicsSystem) { // TODO: If you are resetting scale because you lost a race for the frame, // you should probably also move the object away from the frame. setMatrixScale(world.eid2obj.get(capturedEid), MediaFrame.scale[frame]); - physicsSystem.updateRigidBodyOptions(capturedEid, { type: "dynamic" }); + physicsSystem.updateRigidBody(capturedEid, { type: "dynamic" }); } MediaFrame.capturedNid[frame] = NetworkedMediaFrame.capturedNid[frame]; MediaFrame.scale[frame].set(NetworkedMediaFrame.scale[frame]); - display(world, physicsSystem, frame, capturedEid, heldMediaTypes); + if (MediaFrame.flags[frame] & MEDIA_FRAME_FLAGS.ACTIVE) { + display(world, physicsSystem, frame, capturedEid, heldMediaTypes); + } } } diff --git a/src/systems/bit-physics.ts b/src/systems/bit-physics.ts index a07c325956..3666464b75 100644 --- a/src/systems/bit-physics.ts +++ b/src/systems/bit-physics.ts @@ -1,10 +1,12 @@ import { defineQuery, enterQuery, entityExists, exitQuery, hasComponent, Not } from "bitecs"; -import { Object3DTag, Rigidbody, PhysicsShape, AEntity } from "../bit-components"; +import { Object3DTag, Rigidbody, PhysicsShape, AEntity, Networked, NetworkedRigidBody, Owned } from "../bit-components"; import { getShapeFromPhysicsShape } from "../inflators/physics-shape"; import { findAncestorWithComponent } from "../utils/bit-utils"; import { getBodyFromRigidBody } from "../inflators/rigid-body"; import { HubsWorld } from "../app"; import { PhysicsSystem } from "./physics-system"; +import { EntityID } from "../utils/networking-types"; +import { takeSoftOwnership } from "../utils/take-soft-ownership"; const rigidbodyQuery = defineQuery([Rigidbody, Object3DTag, Not(AEntity)]); const rigidbodyEnteredQuery = enterQuery(rigidbodyQuery); @@ -16,20 +18,45 @@ const shapeExitQuery = exitQuery(shapeQuery); function addPhysicsShapes(world: HubsWorld, physicsSystem: PhysicsSystem, eid: number) { const bodyId = PhysicsShape.bodyId[eid]; const obj = world.eid2obj.get(eid)!; + const hidden = new Array(); + obj.traverse(child => { + if (child.eid! !== eid && hasComponent(world, PhysicsShape, child.eid!)) { + hidden.push(child.eid!); + child.visible = false; + } + }); const shape = getShapeFromPhysicsShape(eid); const shapeId = physicsSystem.addShapes(bodyId, obj, shape); PhysicsShape.shapeId[eid] = shapeId; + + hidden.forEach(eid => { + const obj = world.eid2obj.get(eid)!; + obj.visible = true; + }); +} + +export function updatePrevBodyType(world: HubsWorld, eid: EntityID) { + // If someone else owned the entity we want to preserve the prevType state + if (hasComponent(world, Owned, eid) || Networked.owner[eid] === APP.getSid("reticulum")) { + Rigidbody.prevType[eid] = Rigidbody.type[eid]; + } else { + Rigidbody.prevType[eid] = NetworkedRigidBody.prevType[eid]; + } } +const networkedRigidBodyQuery = defineQuery([Rigidbody, NetworkedRigidBody]); export const physicsCompatSystem = (world: HubsWorld, physicsSystem: PhysicsSystem) => { - rigidbodyEnteredQuery(world).forEach(eid => { + rigidbodyEnteredQuery(world).forEach((eid: EntityID) => { const obj = world.eid2obj.get(eid); const body = getBodyFromRigidBody(eid); const bodyId = physicsSystem.addBody(obj, body); Rigidbody.bodyId[eid] = bodyId; + if (hasComponent(world, Networked, eid)) { + takeSoftOwnership(world, eid); + } }); - shapeEnterQuery(world).forEach(eid => { + shapeEnterQuery(world).forEach((eid: EntityID) => { const bodyEid = findAncestorWithComponent(world, Rigidbody, eid); if (bodyEid) { PhysicsShape.bodyId[eid] = Rigidbody.bodyId[bodyEid]; @@ -39,13 +66,23 @@ export const physicsCompatSystem = (world: HubsWorld, physicsSystem: PhysicsSyst } }); - shapeExitQuery(world).forEach(eid => physicsSystem.removeShapes(PhysicsShape.bodyId[eid], PhysicsShape.shapeId[eid])); + shapeExitQuery(world).forEach((eid: EntityID) => + physicsSystem.removeShapes(PhysicsShape.bodyId[eid], PhysicsShape.shapeId[eid]) + ); - rigidbodyExitedQuery(world).forEach(eid => { + rigidbodyExitedQuery(world).forEach((eid: EntityID) => { if (entityExists(world, eid) && hasComponent(world, PhysicsShape, eid)) { physicsSystem.removeShapes(PhysicsShape.bodyId[eid], PhysicsShape.shapeId[eid]); // The PhysicsShape is still on this entity! } physicsSystem.removeBody(Rigidbody.bodyId[eid]); }); + + networkedRigidBodyQuery(world).forEach((eid: EntityID) => { + if (hasComponent(world, Owned, eid)) { + NetworkedRigidBody.prevType[eid] = Rigidbody.prevType[eid]; + } else { + Rigidbody.prevType[eid] = NetworkedRigidBody.prevType[eid]; + } + }); }; diff --git a/src/systems/floaty-object-system.js b/src/systems/floaty-object-system.js index f857fa8e70..82db35efc8 100644 --- a/src/systems/floaty-object-system.js +++ b/src/systems/floaty-object-system.js @@ -53,7 +53,7 @@ function makeKinematicOnRelease(world) { const physicsSystem = AFRAME.scenes[0].systems["hubs-systems"].physicsSystem; makeKinematicOnReleaseExitQuery(world).forEach(eid => { if (!entityExists(world, eid) || !hasComponent(world, Owned, eid)) return; - physicsSystem.updateRigidBodyOptions(eid, { type: "kinematic" }); + physicsSystem.updateRigidBody(eid, { type: "kinematic" }); }); } @@ -73,17 +73,17 @@ export const floatyObjectSystem = world => { const physicsSystem = AFRAME.scenes[0].systems["hubs-systems"].physicsSystem; enteredFloatyObjectsQuery(world).forEach(eid => { - physicsSystem.updateRigidBodyOptions(eid, { + physicsSystem.updateRigidBody(eid, { type: "kinematic", gravity: { x: 0, y: 0, z: 0 } }); }); enterHeldFloatyObjectsQuery(world).forEach(eid => { - physicsSystem.updateRigidBodyOptions(eid, { + physicsSystem.updateRigidBody(eid, { gravity: { x: 0, y: 0, z: 0 }, type: "dynamic", - collisionFilterMask: COLLISION_LAYERS.HANDS | COLLISION_LAYERS.MEDIA_FRAMES + collisionFilterMask: COLLISION_LAYERS.HANDS | COLLISION_LAYERS.MEDIA_FRAMES | COLLISION_LAYERS.TRIGGERS }); }); @@ -95,17 +95,17 @@ export const floatyObjectSystem = world => { const bodyData = physicsSystem.bodyUuidToData.get(bodyId); if (FloatyObject.flags[eid] & FLOATY_OBJECT_FLAGS.MODIFY_GRAVITY_ON_RELEASE) { if (bodyData.linearVelocity < 1.85) { - physicsSystem.updateRigidBodyOptions(eid, { + physicsSystem.updateRigidBody(eid, { gravity: { x: 0, y: 0, z: 0 }, angularDamping: FloatyObject.flags[eid] & FLOATY_OBJECT_FLAGS.REDUCE_ANGULAR_FLOAT ? 0.89 : 0.5, linearDamping: 0.95, linearSleepingThreshold: 0.1, angularSleepingThreshold: 0.1, - collisionFilterMask: COLLISION_LAYERS.HANDS | COLLISION_LAYERS.MEDIA_FRAMES + collisionFilterMask: COLLISION_LAYERS.HANDS | COLLISION_LAYERS.MEDIA_FRAMES | COLLISION_LAYERS.TRIGGERS }); addComponent(world, MakeStaticWhenAtRest, eid); } else { - physicsSystem.updateRigidBodyOptions(eid, { + physicsSystem.updateRigidBody(eid, { gravity: { x: 0, y: FloatyObject.releaseGravity[eid], z: 0 }, angularDamping: 0.01, linearDamping: 0.01, @@ -131,7 +131,7 @@ export const floatyObjectSystem = world => { const angle = Math.random() * Math.PI * 2; const x = Math.cos(angle); const z = Math.sin(angle); - physicsSystem.updateRigidBodyOptions(eid, { + physicsSystem.updateRigidBody(eid, { gravity: { x, y: force, z }, angularDamping: 0.01, linearDamping: 0.01, @@ -142,7 +142,7 @@ export const floatyObjectSystem = world => { removeComponent(world, MakeStaticWhenAtRest, eid); } } else { - physicsSystem.updateRigidBodyOptions(eid, { + physicsSystem.updateRigidBody(eid, { collisionFilterMask: COLLISION_LAYERS.DEFAULT_INTERACTABLE, gravity: { x: 0, y: -9.8, z: 0 } }); diff --git a/src/systems/hubs-systems.ts b/src/systems/hubs-systems.ts index 06496c475f..cac2e6c59a 100644 --- a/src/systems/hubs-systems.ts +++ b/src/systems/hubs-systems.ts @@ -224,11 +224,6 @@ export function mainTick(xrFrame: XRFrame, renderer: WebGLRenderer, scene: Scene physicsCompatSystem(world, hubsSystems.physicsSystem); hubsSystems.physicsSystem.tick(dt); constraintsSystem(world, hubsSystems.physicsSystem); - floatyObjectSystem(world); - - APP.addon_systems.postPhysics.forEach((systemConfig: SystemConfigT) => { - systemConfig.system(APP); - }); hoverableVisualsSystem(world); @@ -253,6 +248,8 @@ export function mainTick(xrFrame: XRFrame, renderer: WebGLRenderer, scene: Scene hubsSystems.positionAtBorderSystem.tick(); hubsSystems.twoPointStretchingSystem.tick(); + floatyObjectSystem(world); + hubsSystems.holdableButtonSystem.tick(); hubsSystems.hoverButtonSystem.tick(); hubsSystems.drawingMenuSystem.tick(); @@ -314,6 +311,10 @@ export function mainTick(xrFrame: XRFrame, renderer: WebGLRenderer, scene: Scene bitPenCompatSystem(world, aframeSystems["pen-tools"]); snapMediaSystem(world, aframeSystems["hubs-systems"].soundEffectsSystem); + APP.addon_systems.postPhysics.forEach((systemConfig: SystemConfigT) => { + systemConfig.system(APP); + }); + deleteEntitySystem(world, aframeSystems.userinput); destroyAtExtremeDistanceSystem(world); removeNetworkedObjectButtonSystem(world); @@ -329,28 +330,31 @@ export function mainTick(xrFrame: XRFrame, renderer: WebGLRenderer, scene: Scene networkDebugSystem(world, scene); } - scene.updateMatrixWorld(); - APP.addon_systems.matricesUpdate.forEach((systemConfig: SystemConfigT) => { systemConfig.system(APP); }); + scene.updateMatrixWorld(); + renderer.info.reset(); + + APP.addon_systems.beforeRender.forEach((systemConfig: SystemConfigT) => { + systemConfig.system(APP); + }); + if (APP.fx.composer) { APP.addon_systems.postProcessing.forEach((systemConfig: SystemConfigT) => { systemConfig.system(APP); }); APP.fx.composer.render(); } else { - APP.addon_systems.beforeRender.forEach((systemConfig: SystemConfigT) => { - systemConfig.system(APP); - }); renderer.render(scene, camera); - APP.addon_systems.afterRender.forEach((systemConfig: SystemConfigT) => { - systemConfig.system(APP); - }); } + APP.addon_systems.afterRender.forEach((systemConfig: SystemConfigT) => { + systemConfig.system(APP); + }); + // tock()s on components and system will fire here. (As well as any other time render() is called without unbinding onAfterRender) // TODO inline invoking tocks instead of using onAfterRender registered in a-scene diff --git a/src/systems/networked-transform.js b/src/systems/networked-transform.js index 9a501c25d7..e834be8c42 100644 --- a/src/systems/networked-transform.js +++ b/src/systems/networked-transform.js @@ -1,12 +1,22 @@ -import { addComponent, defineQuery, hasComponent } from "bitecs"; -import { LinearRotate, LinearScale, LinearTranslate, NetworkedTransform, Owned } from "../bit-components"; +import { addComponent, defineQuery, hasComponent, enterQuery } from "bitecs"; +import { LinearRotate, LinearScale, LinearTranslate, NetworkedTransform, Owned, Networked } from "../bit-components"; import { millisecondsBetweenTicks } from "../bit-systems/networking"; const query = defineQuery([NetworkedTransform]); const tmpVec = new THREE.Vector3(); const tmpQuat = new THREE.Quaternion(); + +const networkedTransformEnterQuery = enterQuery(query); export function networkedTransformSystem(world) { + networkedTransformEnterQuery(world).forEach(eid => { + if (Networked.owner[eid] === APP.getSid("reticulum")) { + const obj = world.eid2obj.get(eid); + NetworkedTransform.position[eid].set(obj.position.toArray()); + NetworkedTransform.rotation[eid].set(obj.quaternion.toArray()); + NetworkedTransform.scale[eid].set(obj.scale.toArray()); + } + }); const ents = query(world); for (let i = 0; i < ents.length; i++) { const eid = ents[i]; diff --git a/src/systems/on-ownership-lost.js b/src/systems/on-ownership-lost.js index ae055891c6..a6e61b15cb 100644 --- a/src/systems/on-ownership-lost.js +++ b/src/systems/on-ownership-lost.js @@ -7,11 +7,13 @@ import { HeldRemoteLeft, HeldRemoteRight, Owned, - Rigidbody + Rigidbody, + OffersRemoteConstraint, + InteractableObject } from "../bit-components"; // TODO this seems wrong, nothing sets it back unless its a floaty object -const exitOwned = exitQuery(defineQuery([Owned])); +const exitOwned = exitQuery(defineQuery([Owned, OffersRemoteConstraint])); const componentsToRemove = [Held, HeldHandRight, HeldHandLeft, HeldRemoteRight, HeldRemoteLeft]; const kinematicOptions = { type: "kinematic", collisionFilterMask: COLLISION_LAYERS.UNOWNED_INTERACTABLE }; export function onOwnershipLost(world) { @@ -27,7 +29,15 @@ export function onOwnershipLost(world) { } if (hasComponent(world, Rigidbody, eid)) { - physicsSystem.updateRigidBodyOptions(eid, kinematicOptions); + // TODO: If this system should only act on FloatyObjects we should express that + // in the query instead of doing this. + if (hasComponent(world, InteractableObject, eid)) { + physicsSystem.updateRigidBody(eid, { + type: "kinematic" + }); + } else { + physicsSystem.updateRigidBody(eid, kinematicOptions); + } } } } diff --git a/src/systems/physics-system.js b/src/systems/physics-system.js index 1489cafad9..3d1f1a3069 100644 --- a/src/systems/physics-system.js +++ b/src/systems/physics-system.js @@ -3,7 +3,7 @@ import { AmmoDebugConstants, DefaultBufferSize } from "ammo-debug-drawer"; import configs from "../utils/configs"; import ammoWasmUrl from "ammo.js/builds/ammo.wasm.wasm"; import { Rigidbody } from "../bit-components"; -import { updateRigiBodyParams } from "../inflators/rigid-body"; +import { updateBodyParams } from "../inflators/rigid-body"; const MESSAGE_TYPES = CONSTANTS.MESSAGE_TYPES, TYPE = CONSTANTS.TYPE, @@ -129,7 +129,10 @@ export class PhysicsSystem { const transform = new THREE.Matrix4(); const inverse = new THREE.Matrix4(); const matrix = new THREE.Matrix4(); + const loc = new THREE.Vector3(); + const rot = new THREE.Quaternion(); const scale = new THREE.Vector3(); + const UNIT_V = new THREE.Vector3(1, 1, 1); return function () { if (this.ready) { if (this.debugRequested !== this.debugEnabled) { @@ -173,8 +176,10 @@ export class PhysicsSystem { } object3D.updateMatrices(); + object3D.matrixWorld.decompose(loc, rot, scale); + matrix.compose(loc, rot, UNIT_V); this.objectMatricesFloatArray.set( - object3D.matrixWorld.elements, + matrix.elements, index * BUFFER_CONFIG.BODY_DATA_SIZE + BUFFER_CONFIG.MATRIX_OFFSET ); @@ -250,7 +255,7 @@ export class PhysicsSystem { updateRigidBody(eid, options) { const bodyId = Rigidbody.bodyId[eid]; - updateRigiBodyParams(eid, options); + updateBodyParams(eid, options); if (this.bodyUuidToData.has(bodyId)) { this.bodyUuidToData.get(bodyId).options = options; this.workerHelpers.updateBody(bodyId, options); @@ -259,18 +264,6 @@ export class PhysicsSystem { } } - updateRigidBodyOptions(eid, options) { - const bodyId = Rigidbody.bodyId[eid]; - updateRigiBodyParams(eid, options); - const bodyData = this.bodyUuidToData.get(bodyId); - if (!bodyData) { - // TODO: Fix me. - console.warn("updateBodyOptions called for invalid bodyId"); - return; - } - this.workerHelpers.updateBody(bodyId, Object.assign(this.bodyUuidToData.get(bodyId).options, options)); - } - removeBody(uuid) { const bodyData = this.bodyUuidToData.get(uuid); if (!bodyData) { @@ -285,8 +278,10 @@ export class PhysicsSystem { if (bodyData.isInitialized) { delete this.indexToUuid[bodyData.index]; bodyData.collisions.forEach(otherId => { - const otherData = this.bodyUuidToData.get(otherId).collisions; - otherData.splice(otherData.indexOf(uuid), 1); + const collisions = this.bodyUuidToData.get(otherId)?.collisions; + // This can happen when removing multiple bodies in a frame + if (!collisions) return; + collisions.splice(collisions.indexOf(uuid), 1); }); this.bodyUuids.splice(this.bodyUuids.indexOf(uuid), 1); this.bodyUuidToData.delete(uuid); diff --git a/src/systems/remove-object3D-system.js b/src/systems/remove-object3D-system.js index a163834c5f..d7cc9dc04a 100644 --- a/src/systems/remove-object3D-system.js +++ b/src/systems/remove-object3D-system.js @@ -5,6 +5,7 @@ import { GLTFModel, LightTag, MaterialTag, + TextureTag, MediaFrame, MediaImage, MediaVideo, @@ -93,6 +94,7 @@ const cleanupAudioDebugSystem = cleanupOnExit(NavMesh, eid => cleanupAudioDebugN // which means we will remove each descendent from its parent. const exitedObject3DQuery = exitQuery(defineQuery([Object3DTag])); const exitedMaterialQuery = exitQuery(defineQuery([MaterialTag])); +const exitedTextureQuery = exitQuery(defineQuery([TextureTag])); export function removeObject3DSystem(world) { function removeObjFromMap(eid) { const o = world.eid2obj.get(eid); @@ -104,6 +106,11 @@ export function removeObject3DSystem(world) { world.eid2mat.delete(eid); m.eid = 0; } + function removeFromTexMap(eid) { + const m = world.eid2tex.get(eid); + world.eid2tex.delete(eid); + m.eid = 0; + } // TODO write removeObject3DEntity to do this work up-front, // keeping the scene graph consistent and avoiding the second exitedObject3DQuery in this system. @@ -145,4 +152,5 @@ export function removeObject3DSystem(world) { entities.forEach(removeObjFromMap); exitedObject3DQuery(world).forEach(removeObjFromMap); exitedMaterialQuery(world).forEach(removeFromMatMap); + exitedTextureQuery(world).forEach(removeFromTexMap); } diff --git a/src/utils/assign-network-ids.ts b/src/utils/assign-network-ids.ts index ae5399bc8d..1c4aaba4ed 100644 --- a/src/utils/assign-network-ids.ts +++ b/src/utils/assign-network-ids.ts @@ -2,6 +2,7 @@ import { hasComponent } from "bitecs"; import { HubsWorld } from "../app"; import { Networked } from "../bit-components"; import { ClientID, EntityID, NetworkID } from "./networking-types"; +import { Material, Texture } from "three"; export function setNetworkedDataWithRoot(world: HubsWorld, rootNid: NetworkID, eid: EntityID, creator: ClientID) { let i = 0; @@ -12,6 +13,16 @@ export function setNetworkedDataWithRoot(world: HubsWorld, rootNid: NetworkID, e i += 1; } }); + i = 0; + world.eid2mat.forEach((mat: Material, matEid: EntityID) => { + setInitialNetworkedData(matEid, `${rootNid}.mat.${i}`, rootNid); + i += 1; + }); + i = 0; + world.eid2tex.forEach((tex: Texture, texEid: EntityID) => { + setInitialNetworkedData(texEid, `${rootNid}.tex.${i}`, rootNid); + i += 1; + }); } export function setNetworkedDataWithoutRoot(world: HubsWorld, rootNid: NetworkID, childEid: EntityID) { diff --git a/src/utils/bit-utils.ts b/src/utils/bit-utils.ts index 009fb52882..bfc954b308 100644 --- a/src/utils/bit-utils.ts +++ b/src/utils/bit-utils.ts @@ -25,8 +25,15 @@ export function hasAnyComponent(world: HubsWorld, components: Component[], eid: return false; } -export function findAncestorEntity(world: HubsWorld, eid: number, predicate: (eid: number) => boolean) { - const obj = findAncestor(world.eid2obj.get(eid)!, (o: Object3D) => !!(o.eid && predicate(o.eid))) as Object3D | null; +export function findAncestorEntity( + world: HubsWorld, + eid: number, + predicate: (eid: number, world: HubsWorld) => boolean +) { + const obj = findAncestor( + world.eid2obj.get(eid)!, + (o: Object3D) => !!(o.eid && predicate(o.eid, world)) + ) as Object3D | null; return obj && obj.eid!; } @@ -36,7 +43,7 @@ export function findAncestorEntities(world: HubsWorld, eid: number, predicate: ( } export function findAncestorWithComponent(world: HubsWorld, component: Component, eid: number) { - return findAncestorEntity(world, eid, otherId => hasComponent(world, component, otherId)); + return findAncestorEntity(world, eid, (otherId, world) => hasComponent(world, component, otherId)); } export function findAncestorsWithComponent(world: HubsWorld, component: Component, eid: number): EntityID[] { diff --git a/src/utils/jsx-entity.ts b/src/utils/jsx-entity.ts index 65ed903429..7e885532d3 100644 --- a/src/utils/jsx-entity.ts +++ b/src/utils/jsx-entity.ts @@ -44,7 +44,7 @@ import { } from "../bit-components"; import { inflateMediaLoader } from "../inflators/media-loader"; import { inflateMediaFrame } from "../inflators/media-frame"; -import { GrabbableParams, inflateGrabbable } from "../inflators/grabbable"; +import { GrabbableParams, inflateGLTFGrabbable, inflateGrabbable } from "../inflators/grabbable"; import { ImageParams, inflateImage } from "../inflators/image"; import { inflateVideo, VideoParams } from "../inflators/video"; import { inflateModel, ModelParams } from "../inflators/model"; @@ -56,7 +56,7 @@ import { inflateLink, LinkParams } from "../inflators/link"; import { inflateLinkLoader, LinkLoaderParams } from "../inflators/link-loader"; import { inflateLoopAnimationInitialize, LoopAnimationParams } from "../inflators/loop-animation"; import { inflateSlice9 } from "../inflators/slice9"; -import { TextParams, inflateText } from "../inflators/text"; +import { TextParams, inflateGLTFText, inflateText } from "../inflators/text"; import { BackgroundParams, EnvironmentSettingsParams, @@ -92,19 +92,19 @@ import { inflateAudioParams } from "../inflators/audio-params"; import { AudioSourceParams, inflateAudioSource } from "../inflators/audio-source"; import { AudioTargetParams, inflateAudioTarget } from "../inflators/audio-target"; import { PhysicsShapeParams, inflatePhysicsShape } from "../inflators/physics-shape"; -import { inflateRigidBody, RigidBodyParams } from "../inflators/rigid-body"; +import { inflateGLTFRigidBody, inflateRigidBody, RigidBodyParams } from "../inflators/rigid-body"; import { AmmoShapeParams, inflateAmmoShape } from "../inflators/ammo-shape"; import { BoxColliderParams, inflateBoxCollider } from "../inflators/box-collider"; import { inflateTrimesh } from "../inflators/trimesh"; import { HeightFieldParams, inflateHeightField } from "../inflators/heightfield"; import { inflateAudioSettings } from "../inflators/audio-settings"; -import { HubsVideoTexture } from "../textures/HubsVideoTexture"; import { inflateMediaLink, MediaLinkParams } from "../inflators/media-link"; import { inflateObjectMenuTarget, ObjectMenuTargetParams } from "../inflators/object-menu-target"; import { inflateObjectMenuTransform, ObjectMenuTransformParams } from "../inflators/object-menu-transform"; import { inflatePlane, PlaneParams } from "../inflators/plane"; import { FollowInFovParams, inflateFollowInFov } from "../inflators/follow-in-fov"; import { ComponentDataT } from "../types"; +import { inflateCapturable } from "../inflators/capturable"; preload( new Promise(resolve => { @@ -258,13 +258,11 @@ export interface ComponentData { hemisphereLight?: HemisphereLightParams; pointLight?: PointLightParams; spotLight?: SpotLightParams; - grabbable?: GrabbableParams; billboard?: { onlyY: boolean }; mirror?: MirrorParams; audioZone?: AudioZoneParams; audioParams?: AudioSettings; mediaFrame?: any; - text?: TextParams; } type OptionalParams = Partial | true; @@ -306,6 +304,7 @@ export interface JSXComponentData extends ComponentData { deletable?: true; makeKinematicOnRelease?: true; destroyAtExtremeDistance?: true; + grabbable?: GrabbableParams; // @TODO Define all the anys networked?: any; @@ -377,6 +376,7 @@ export interface JSXComponentData extends ComponentData { objectMenuTransform?: OptionalParams; objectMenuTarget?: OptionalParams; plane?: PlaneParams; + text?: TextParams; } export interface GLTFComponentData extends ComponentData { @@ -398,6 +398,13 @@ export interface GLTFComponentData extends ComponentData { audioTarget: AudioTargetParams; audioSettings: SceneAudioSettings; mediaLink: MediaLinkParams; + interactable: true; + rigidbody?: OptionalParams; + // TODO GLTFPhysicsShapeParams + physicsShape?: AmmoShapeParams; + text?: TextParams; + grabbable?: GrabbableParams; + capturable?: true; // deprecated spawnPoint?: true; @@ -428,7 +435,6 @@ declare global { } export const commonInflators: Required<{ [K in keyof ComponentDataT]: InflatorFn }> = { - grabbable: inflateGrabbable, billboard: createDefaultInflator(Billboard), // inflators that create Object3Ds @@ -440,12 +446,12 @@ export const commonInflators: Required<{ [K in keyof ComponentDataT]: InflatorFn mirror: inflateMirror, audioZone: inflateAudioZone, audioParams: inflateAudioParams, - mediaFrame: inflateMediaFrame, - text: inflateText + mediaFrame: inflateMediaFrame }; export const jsxInflators: Required<{ [K in keyof ComponentDataT]: InflatorFn }> = { ...commonInflators, + grabbable: inflateGrabbable, cursorRaycastable: createDefaultInflator(CursorRaycastable), remoteHoverTarget: createDefaultInflator(RemoteHoverTarget), isNotRemoteHoverTarget: createDefaultInflator(NotRemoteHoverTarget), @@ -485,6 +491,7 @@ export const jsxInflators: Required<{ [K in keyof ComponentDataT]: InflatorFn }> mediaLoader: inflateMediaLoader, mixerAnimatable: createDefaultInflator(MixerAnimatableInitialize), loopAnimation: inflateLoopAnimationInitialize, + text: inflateText, inspectable: createDefaultInflator(Inspectable), // inflators that create Object3Ds object3D: addObject3DComponent, @@ -500,6 +507,7 @@ export const jsxInflators: Required<{ [K in keyof ComponentDataT]: InflatorFn }> export const gltfInflators: Required<{ [K in keyof ComponentDataT]: InflatorFn }> = { ...commonInflators, + grabbable: inflateGLTFGrabbable, pdf: inflatePDFLoader, // Temporarily reuse video loader for audio because of // their processings are similar. @@ -531,7 +539,12 @@ export const gltfInflators: Required<{ [K in keyof ComponentDataT]: InflatorFn } trimesh: inflateTrimesh, heightfield: inflateHeightField, audioSettings: inflateAudioSettings, - mediaLink: inflateMediaLink + mediaLink: inflateMediaLink, + interactable: createDefaultInflator(SingleActionButton), + rigidbody: inflateGLTFRigidBody, + physicsShape: inflateAmmoShape, + text: inflateGLTFText, + capturable: inflateCapturable }; function jsxInflatorExists(name: string) { diff --git a/src/utils/load-model.tsx b/src/utils/load-model.tsx index 2b5313e0a7..4edb0b7213 100644 --- a/src/utils/load-model.tsx +++ b/src/utils/load-model.tsx @@ -9,7 +9,6 @@ export function* loadModel(world: HubsWorld, src: string, contentType: string, u const { scene, animations } = yield loadGLTFModel(src, contentType, useCache, null); scene.animations = animations; - scene.mixer = new THREE.AnimationMixer(scene); return renderAsEntity(world, ); } diff --git a/src/utils/media-utils.js b/src/utils/media-utils.js index 0f391dacff..5a9dbd9859 100644 --- a/src/utils/media-utils.js +++ b/src/utils/media-utils.js @@ -24,9 +24,17 @@ export const MediaType = { VIDEO: 1 << 2, PDF: 1 << 3, HTML: 1 << 4, - AUDIO: 1 << 5 + AUDIO: 1 << 5, + OBJECT: 1 << 6 }; -MediaType.ALL = MediaType.MODEL | MediaType.IMAGE | MediaType.VIDEO | MediaType.PDF | MediaType.HTML | MediaType.AUDIO; +MediaType.ALL = + MediaType.MODEL | + MediaType.IMAGE | + MediaType.VIDEO | + MediaType.PDF | + MediaType.HTML | + MediaType.AUDIO | + MediaType.OBJECT; MediaType.ALL_2D = MediaType.IMAGE | MediaType.VIDEO | MediaType.PDF | MediaType.HTML; const MediaTypeName = new Map([ [MediaType.MODEL, "model"], @@ -34,7 +42,8 @@ const MediaTypeName = new Map([ [MediaType.VIDEO, "video"], [MediaType.PDF, "pdf"], [MediaType.HTML, "html"], - [MediaType.AUDIO, "audio"] + [MediaType.AUDIO, "audio"], + [MediaType.OBJECT, "object"] ]); export function mediaTypeName(type) { return MediaTypeName.get(type) || "unknown"; diff --git a/src/utils/network-schemas.ts b/src/utils/network-schemas.ts index b97517af7e..87ee038ee8 100644 --- a/src/utils/network-schemas.ts +++ b/src/utils/network-schemas.ts @@ -5,6 +5,8 @@ import { NetworkedFloatyObject, NetworkedMediaFrame, NetworkedPDF, + NetworkedRigidBody, + NetworkedText, NetworkedTransform, NetworkedVideo, NetworkedWaypoint @@ -16,6 +18,8 @@ import { NetworkedTransformSchema } from "./networked-transform-schema"; import { NetworkedVideoSchema } from "./networked-video-schema"; import { NetworkedWaypointSchema } from "./networked-waypoint-schema"; import type { CursorBuffer, EntityID } from "./networking-types"; +import { NetworkedTextSchema } from "./networked-text-schema"; +import { NetworkedRigidBodySchema } from "./networked-rigid-body"; export interface StoredComponent { version: number; @@ -46,6 +50,8 @@ schemas.set(NetworkedFloatyObject, { ...defineNetworkSchema(NetworkedFloatyObject) }); schemas.set(NetworkedPDF, NetworkedPDFSchema); +schemas.set(NetworkedText, NetworkedTextSchema); +schemas.set(NetworkedRigidBody, NetworkedRigidBodySchema); export const networkableComponents = Array.from(schemas.keys()); diff --git a/src/utils/networked-media-frame-schema.ts b/src/utils/networked-media-frame-schema.ts index 175b022af6..4948df2313 100644 --- a/src/utils/networked-media-frame-schema.ts +++ b/src/utils/networked-media-frame-schema.ts @@ -8,16 +8,31 @@ const runtimeSerde = defineNetworkSchema(NetworkedMediaFrame); const migrations = new Map(); migrations.set(0, ({ data }: StoredComponent) => { - return { version: 1, data }; + return { version: 2, data }; }); function apply(eid: EntityID, { version, data }: StoredComponent) { - if (version !== 1) return false; - - const { capturedNid, scale }: { capturedNid: string; scale: ArrayVec3 } = data; - write(NetworkedMediaFrame.capturedNid, eid, capturedNid); - write(NetworkedMediaFrame.scale, eid, scale); - return true; + if (version === 1) { + const { capturedNid, scale }: { capturedNid: string; scale: ArrayVec3 } = data; + write(NetworkedMediaFrame.capturedNid, eid, capturedNid); + write(NetworkedMediaFrame.scale, eid, scale); + write(NetworkedMediaFrame.flags, eid, 0); + return true; + } else if (version === 2) { + const { + capturedNid, + scale, + flags, + mediaType, + align + }: { capturedNid: string; scale: ArrayVec3; flags: number; mediaType: number; align: ArrayVec3 } = data; + write(NetworkedMediaFrame.capturedNid, eid, capturedNid); + write(NetworkedMediaFrame.scale, eid, scale); + write(NetworkedMediaFrame.flags, eid, flags); + write(NetworkedMediaFrame.mediaType, eid, mediaType); + return true; + } + return false; } export const NetworkedMediaFrameSchema: NetworkSchema = { @@ -26,10 +41,12 @@ export const NetworkedMediaFrameSchema: NetworkSchema = { deserialize: runtimeSerde.deserialize, serializeForStorage: function serializeForStorage(eid: EntityID) { return { - version: 1, + version: 2, data: { capturedNid: read(NetworkedMediaFrame.capturedNid, eid), - scale: read(NetworkedMediaFrame.scale, eid) + scale: read(NetworkedMediaFrame.scale, eid), + flags: read(NetworkedMediaFrame.flags, eid), + mediaType: read(NetworkedMediaFrame.mediaType, eid) } }; }, diff --git a/src/utils/networked-rigid-body.ts b/src/utils/networked-rigid-body.ts new file mode 100644 index 0000000000..41b9cf0aaa --- /dev/null +++ b/src/utils/networked-rigid-body.ts @@ -0,0 +1,31 @@ +import { NetworkedRigidBody } from "../bit-components"; +import { defineNetworkSchema } from "./define-network-schema"; +import { deserializerWithMigrations, Migration, NetworkSchema, read, StoredComponent, write } from "./network-schemas"; +import type { EntityID } from "./networking-types"; + +const runtimeSerde = defineNetworkSchema(NetworkedRigidBody); + +const migrations = new Map(); + +function apply(eid: EntityID, { version, data }: StoredComponent) { + if (version !== 1) return false; + + const { prevType }: { prevType: number } = data; + write(NetworkedRigidBody.prevType, eid, prevType); + return true; +} + +export const NetworkedRigidBodySchema: NetworkSchema = { + componentName: "networked-rigid-body", + serialize: runtimeSerde.serialize, + deserialize: runtimeSerde.deserialize, + serializeForStorage: function serializeForStorage(eid: EntityID) { + return { + version: 1, + data: { + prevType: read(NetworkedRigidBody.prevType, eid) + } + }; + }, + deserializeFromStorage: deserializerWithMigrations(migrations, apply) +}; diff --git a/src/utils/networked-text-schema.ts b/src/utils/networked-text-schema.ts new file mode 100644 index 0000000000..1f9b272f6d --- /dev/null +++ b/src/utils/networked-text-schema.ts @@ -0,0 +1,136 @@ +import { NetworkedText } from "../bit-components"; +import { defineNetworkSchema } from "./define-network-schema"; +import { deserializerWithMigrations, Migration, NetworkSchema, read, StoredComponent, write } from "./network-schemas"; +import type { EntityID } from "./networking-types"; + +const migrations = new Map(); + +function apply(eid: EntityID, { version, data }: StoredComponent) { + if (version !== 1) return false; + + const { + text, + fontSize, + color, + fillOpacity, + anchorX, + anchorY, + curveRadius, + direction, + letterSpacing, + lineHeight, + maxWidth, + opacity, + outlineBlur, + outlineColor, + outlineOffsetX, + outlineOffsetY, + outlineOpacity, + outlineWidth, + overflowWrap, + side, + strokeColor, + strokeOpacity, + strokeWidth, + textAlign, + textIndent, + whiteSpace + }: { + text: string; + fontSize: number; + color: number; + fillOpacity: string; + anchorX: number; + anchorY: number; + curveRadius: number; + direction: number; + letterSpacing: number; + lineHeight: string; + maxWidth: number; + opacity: number; + outlineBlur: string; + outlineColor: number; + outlineOffsetX: string; + outlineOffsetY: string; + outlineOpacity: number; + outlineWidth: string; + overflowWrap: number; + side: number; + strokeColor: number; + strokeOpacity: number; + strokeWidth: string; + textAlign: number; + textIndent: number; + whiteSpace: number; + } = data; + write(NetworkedText.text, eid, APP.getSid(text)); + write(NetworkedText.fontSize, eid, fontSize); + write(NetworkedText.color, eid, color); + write(NetworkedText.fillOpacity, eid, APP.getSid(fillOpacity)); + write(NetworkedText.anchorX, eid, anchorX); + write(NetworkedText.anchorY, eid, anchorY); + write(NetworkedText.curveRadius, eid, curveRadius); + write(NetworkedText.direction, eid, direction); + write(NetworkedText.letterSpacing, eid, letterSpacing); + write(NetworkedText.lineHeight, eid, APP.getSid(lineHeight)); + write(NetworkedText.maxWidth, eid, maxWidth); + write(NetworkedText.opacity, eid, opacity); + write(NetworkedText.outlineBlur, eid, APP.getSid(outlineBlur)); + write(NetworkedText.outlineColor, eid, outlineColor); + write(NetworkedText.outlineOffsetX, eid, APP.getSid(outlineOffsetX)); + write(NetworkedText.outlineOffsetY, eid, APP.getSid(outlineOffsetY)); + write(NetworkedText.outlineOpacity, eid, outlineOpacity); + write(NetworkedText.outlineWidth, eid, outlineWidth); + write(NetworkedText.overflowWrap, eid, overflowWrap); + write(NetworkedText.side, eid, side); + write(NetworkedText.strokeColor, eid, strokeColor); + write(NetworkedText.strokeOpacity, eid, strokeOpacity); + write(NetworkedText.strokeWidth, eid, APP.getSid(strokeWidth)); + write(NetworkedText.textAlign, eid, textAlign); + write(NetworkedText.textIndent, eid, textIndent); + write(NetworkedText.text, eid, text); + write(NetworkedText.whiteSpace, eid, whiteSpace); + return true; +} + +const runtimeSerde = defineNetworkSchema(NetworkedText); +export const NetworkedTextSchema: NetworkSchema = { + componentName: "networked-text", + serialize: runtimeSerde.serialize, + deserialize: runtimeSerde.deserialize, + serializeForStorage: function serializeForStorage(eid: EntityID) { + return { + version: 1, + data: { + text: APP.getString(read(NetworkedText.text, eid)), + fontSize: read(NetworkedText.fontSize, eid), + color: read(NetworkedText.color, eid), + fillOpacity: APP.getString(read(NetworkedText.fillOpacity, eid)), + anchorX: read(NetworkedText.anchorX, eid), + anchorY: read(NetworkedText.anchorY, eid), + curveRadius: read(NetworkedText.curveRadius, eid), + direction: read(NetworkedText.direction, eid), + letterSpacing: read(NetworkedText.letterSpacing, eid), + lineHeight: APP.getString(read(NetworkedText.lineHeight, eid)), + maxWidth: read(NetworkedText.maxWidth, eid), + opacity: read(NetworkedText.opacity, eid), + outlineBlur: APP.getString(read(NetworkedText.outlineBlur, eid)), + outlineColor: read(NetworkedText.outlineColor, eid), + outlineOffsetX: APP.getString(read(NetworkedText.outlineOffsetX, eid)), + outlineOffsetY: APP.getString(read(NetworkedText.outlineOffsetY, eid)), + outlineOpacity: read(NetworkedText.outlineOpacity, eid), + outlineWidth: APP.getString(read(NetworkedText.outlineWidth, eid)), + overflowWrap: read(NetworkedText.overflowWrap, eid), + side: read(NetworkedText.side, eid), + strokeColor: read(NetworkedText.strokeColor, eid), + strokeOpacity: read(NetworkedText.strokeOpacity, eid), + strokeWidth: APP.getString(read(NetworkedText.strokeWidth, eid)), + textAlign: read(NetworkedText.textAlign, eid), + textIndent: read(NetworkedText.textIndent, eid), + value: read(NetworkedText.text, eid), + whiteSpace: read(NetworkedText.whiteSpace, eid) + } + }; + }, + deserializeFromStorage: deserializerWithMigrations(migrations, apply) +}; diff --git a/src/utils/networked-video-schema.ts b/src/utils/networked-video-schema.ts index c1accc7e5d..695119658d 100644 --- a/src/utils/networked-video-schema.ts +++ b/src/utils/networked-video-schema.ts @@ -8,12 +8,20 @@ const runtimeSerde = defineNetworkSchema(NetworkedVideo); const migrations = new Map(); function apply(eid: EntityID, { version, data }: StoredComponent) { - if (version !== 1) return false; - - const { time, flags }: { time: number; flags: number } = data; - write(NetworkedVideo.time, eid, time); - write(NetworkedVideo.flags, eid, flags); - return true; + if (version === 1) { + const { time, flags }: { time: number; flags: number } = data; + write(NetworkedVideo.time, eid, time); + write(NetworkedVideo.flags, eid, flags); + return true; + } else if (version === 2) { + const { time, flags, projection, src }: { time: number; flags: number; src: string; projection: number } = data; + write(NetworkedVideo.time, eid, time); + write(NetworkedVideo.flags, eid, flags); + write(NetworkedVideo.projection, eid, projection); + write(NetworkedVideo.src, eid, APP.getSid(src)); + return true; + } + return false; } export const NetworkedVideoSchema: NetworkSchema = { @@ -22,10 +30,12 @@ export const NetworkedVideoSchema: NetworkSchema = { deserialize: runtimeSerde.deserialize, serializeForStorage: function serializeForStorage(eid: EntityID) { return { - version: 1, + version: 2, data: { time: read(NetworkedVideo.time, eid), - flags: read(NetworkedVideo.flags, eid) + flags: read(NetworkedVideo.flags, eid), + projection: read(NetworkedVideo.projection, eid), + src: APP.getString(read(NetworkedVideo.src, eid)) } }; }, diff --git a/src/utils/projection-mode.ts b/src/utils/projection-mode.ts index 073ed99eef..e9731045b5 100644 --- a/src/utils/projection-mode.ts +++ b/src/utils/projection-mode.ts @@ -15,3 +15,12 @@ export function getProjectionFromProjectionName(projectionName: ProjectionModeNa } return ProjectionMode.FLAT; } + +export function getProjectionNameFromProjection(projection: ProjectionMode): ProjectionModeName { + if (projection === ProjectionMode.FLAT) { + return ProjectionModeName.FLAT; + } else if (projection === ProjectionMode.SPHERE_EQUIRECTANGULAR) { + return ProjectionModeName.SPHERE_EQUIRECTANGULAR; + } + return ProjectionModeName.FLAT; +} diff --git a/src/utils/three-utils.js b/src/utils/three-utils.js index d9808919b5..b2b9238298 100644 --- a/src/utils/three-utils.js +++ b/src/utils/three-utils.js @@ -23,16 +23,66 @@ export function getLastWorldScale(src, target) { } export function disposeMaterial(mtrl) { - if (mtrl.map) mtrl.map.dispose(); - if (mtrl.lightMap) mtrl.lightMap.dispose(); - if (mtrl.bumpMap) mtrl.bumpMap.dispose(); - if (mtrl.normalMap) mtrl.normalMap.dispose(); - if (mtrl.specularMap) mtrl.specularMap.dispose(); - if (mtrl.envMap) mtrl.envMap.dispose(); - if (mtrl.aoMap) mtrl.aoMap.dispose(); - if (mtrl.metalnessMap) mtrl.metalnessMap.dispose(); - if (mtrl.roughnessMap) mtrl.roughnessMap.dispose(); - if (mtrl.emissiveMap) mtrl.emissiveMap.dispose(); + if (mtrl.map) { + mtrl.map.dispose(); + if (mtrl.map.eid) { + removeEntity(APP.world, mtrl.map.eid); + } + } + if (mtrl.lightMap) { + mtrl.lightMap.dispose(); + if (mtrl.lightMap.eid) { + removeEntity(APP.world, mtrl.lightMap.eid); + } + } + if (mtrl.bumpMap) { + mtrl.bumpMap.dispose(); + if (mtrl.bumpMap.eid) { + removeEntity(APP.world, mtrl.bumpMap.eid); + } + } + if (mtrl.normalMap) { + mtrl.normalMap.dispose(); + if (mtrl.normalMap.eid) { + removeEntity(APP.world, mtrl.normalMap.eid); + } + } + if (mtrl.specularMap) { + mtrl.specularMap.dispose(); + if (mtrl.specularMap.eid) { + removeEntity(APP.world, mtrl.specularMap.eid); + } + } + if (mtrl.envMap) { + mtrl.envMap.dispose(); + if (mtrl.envMap.eid) { + removeEntity(APP.world, mtrl.envMap.eid); + } + } + if (mtrl.aoMap) { + mtrl.aoMap.dispose(); + if (mtrl.aoMap.eid) { + removeEntity(APP.world, mtrl.aoMap.eid); + } + } + if (mtrl.metalnessMap) { + mtrl.metalnessMap.dispose(); + if (mtrl.metalnessMap.eid) { + removeEntity(APP.world, mtrl.metalnessMap.eid); + } + } + if (mtrl.roughnessMap) { + mtrl.roughnessMap.dispose(); + if (mtrl.roughnessMap.eid) { + removeEntity(APP.world, mtrl.roughnessMap.eid); + } + } + if (mtrl.emissiveMap) { + mtrl.emissiveMap.dispose(); + if (mtrl.emissiveMap.eid) { + removeEntity(APP.world, mtrl.emissiveMap.eid); + } + } mtrl.dispose(); if (mtrl.eid) { removeEntity(APP.world, mtrl.eid); diff --git a/types/aframe.d.ts b/types/aframe.d.ts index dbad160219..5d91b871d3 100644 --- a/types/aframe.d.ts +++ b/types/aframe.d.ts @@ -8,7 +8,10 @@ declare module "aframe" { [name: string]: Object3D; }; getObject3D(string): Object3D?; - components: { [s: string]: AComponent }; + components: { + [s: string]: AComponent; + "player-info": PlayerInfo; + }; eid: number; isPlaying: boolean; } @@ -92,6 +95,10 @@ declare module "aframe" { disable(): void; } + interface PlayerInfo extends AComponent { + playerSessionId: string; + } + interface PersonalSpaceBubbleSystem extends ASystem { invaders: PersonalSpaceInvader[]; } diff --git a/types/three.d.ts b/types/three.d.ts index 5ef7d9bbe8..a019ee3379 100644 --- a/types/three.d.ts +++ b/types/three.d.ts @@ -21,10 +21,16 @@ declare module "three" { group: GeometryGroup ) => void; } + + interface Texture { + eid?: number; + } interface Mesh { reflectionProbeMode: "static" | "dynamic" | false; } - + interface AnimationAction { + eid?: number; + } interface Vector3 { near: Function; } diff --git a/types/troika-three-text.d.ts b/types/troika-three-text.d.ts index e099337d2e..9dbe86a730 100644 --- a/types/troika-three-text.d.ts +++ b/types/troika-three-text.d.ts @@ -3,8 +3,17 @@ declare module "troika-three-text" { export class Text extends Mesh { text: string; - anchorX: number | `${number}%` | 'left' | 'center' | 'right'; - anchorY: number | `${number}%` | 'top' | 'top-baseline' | 'top-cap' | 'top-ex' | 'middle' | 'bottom-baseline' | 'bottom'; + anchorX: number | `${number}%` | "left" | "center" | "right"; + anchorY: + | number + | `${number}%` + | "top" + | "top-baseline" + | "top-cap" + | "top-ex" + | "middle" + | "bottom-baseline" + | "bottom"; clipRect: [number, number, number, number] | null; color: string | number | Color | null; curveRadius: number; @@ -16,7 +25,7 @@ declare module "troika-three-text" { glyphGeometryDetail: number; gpuAccelerateSDF: boolean; letterSpacing: number; - lineHeight: number | 'normal'; + lineHeight: number | "normal"; material: Material | null; maxWidth: number; outlineBlur: number | `${number}%`; @@ -25,15 +34,16 @@ declare module "troika-three-text" { outlineOffsetY: number | `${number}%`; outlineOpacity: number; outlineWidth: number | `${number}%`; - overflowWrap: 'normal' | 'break-word'; + overflowWrap: "normal" | "break-word"; sdfGlyphSize: number | null; strokeColor: string | number | Color; strokeOpacity: number; strokeWidth: number | `${number}%`; - textAlign: 'left' | 'right' | 'center' | 'justify'; + textAlign: "left" | "right" | "center" | "justify"; textIndent: number; - whiteSpace: 'normal' | 'nowrap'; + whiteSpace: "normal" | "nowrap"; sync(callback?: () => void): void; + isTroikaText: true; } export const preloadFont: ( diff --git a/webpack.config.js b/webpack.config.js index c52b924df0..ad9831a8d5 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -290,9 +290,6 @@ module.exports = async (env, argv) => { const internalHostname = process.env.INTERNAL_HOSTNAME || "hubs.local"; return { - cache: { - type: "filesystem" - }, resolve: { alias: { // aframe and networked-aframe are still using commonjs modules. three and bitecs are peer dependanciees