From bf62039e632909c76a931f0a4412d84efd0ac597 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Mon, 15 Jan 2024 16:17:23 +0100 Subject: [PATCH] BitECS hover visual effect --- src/bit-components.js | 7 ++ src/bit-systems/hoverable-visuals-system.ts | 124 ++++++++++++++++++++ src/bit-systems/media-loading.ts | 5 + src/prefabs/camera-tool.js | 1 + src/prefabs/duck.tsx | 1 + src/prefabs/media.tsx | 1 + src/systems/hubs-systems.ts | 3 + src/utils/jsx-entity.ts | 5 +- 8 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 src/bit-systems/hoverable-visuals-system.ts diff --git a/src/bit-components.js b/src/bit-components.js index 6959257411..4580877cdd 100644 --- a/src/bit-components.js +++ b/src/bit-components.js @@ -70,6 +70,13 @@ export const HoveredHandRight = defineComponent(); export const HoveredHandLeft = defineComponent(); export const HoveredRemoteRight = defineComponent(); export const HoveredRemoteLeft = defineComponent(); +export const HoverableVisuals = defineComponent({ + geometryRadius: Types.f32 +}); +/** + * @type {Map} + */ +export const HoverableVisualsUniforms = new Map(); export const HeldHandRight = defineComponent(); export const HeldHandLeft = defineComponent(); export const HeldRemoteRight = defineComponent(); diff --git a/src/bit-systems/hoverable-visuals-system.ts b/src/bit-systems/hoverable-visuals-system.ts new file mode 100644 index 0000000000..0658cb53b9 --- /dev/null +++ b/src/bit-systems/hoverable-visuals-system.ts @@ -0,0 +1,124 @@ +import { defineQuery, enterQuery, exitQuery, hasComponent } from "bitecs"; +import { HubsWorld } from "../app"; +import { + HandLeft, + HandRight, + Held, + HoverableVisuals, + HoverableVisualsUniforms, + HoveredHandLeft, + HoveredHandRight, + HoveredRemoteLeft, + HoveredRemoteRight, + RemoteLeft, + RemoteRight +} from "../bit-components"; +import { injectCustomShaderChunks } from "../utils/media-utils"; +import { anyEntityWith, findChildWithComponent } from "../utils/bit-utils"; +import { EntityID } from "../utils/networking-types"; + +export const updateHoverableVisuals = (function () { + const boundingBox = new THREE.Box3(); + const boundingSphere = new THREE.Sphere(); + return function (world: HubsWorld, eid: EntityID) { + const obj = world.eid2obj.get(eid)!; + + HoverableVisualsUniforms.set(eid, injectCustomShaderChunks(obj)); + + boundingBox.setFromObject(obj); + boundingBox.getBoundingSphere(boundingSphere); + HoverableVisuals.geometryRadius[eid] = boundingSphere.radius / obj.scale.y; + }; +})(); + +const sweepParams = [0, 0]; +const interactorOneTransform = new Array(); +const interactorTwoTransform = new Array(); +const hoverableVisualsQuery = defineQuery([HoverableVisuals]); +const hoverableVisualsEnterQuery = enterQuery(hoverableVisualsQuery); +const hoverableVisualsExitQuery = exitQuery(hoverableVisualsQuery); +const isMobile = AFRAME.utils.device.isMobile(); +const isMobileVR = AFRAME.utils.device.isMobileVR(); +const isTouchscreen = isMobile && !isMobileVR; +export function hoverableVisualsSystem(world: HubsWorld) { + hoverableVisualsExitQuery(world).forEach(eid => HoverableVisualsUniforms.delete(eid)); + hoverableVisualsEnterQuery(world).forEach(eid => updateHoverableVisuals(world, eid)); + hoverableVisualsQuery(world).forEach(eid => { + const uniforms = HoverableVisualsUniforms.get(eid); + if (uniforms && uniforms.length) { + const obj = world.eid2obj.get(eid)!; + const isFrozen = APP.scene!.is("frozen"); + + let interactorOne, interactorTwo; + + const hoveredHandLeft = findChildWithComponent(world, HoveredHandLeft, eid); + const leftHand = anyEntityWith(world, HandLeft); + if (leftHand) { + if (hoveredHandLeft && !hasComponent(world, Held, leftHand)) { + interactorOne = APP.world.eid2obj.get(leftHand); + } + } + const hoveredRemoteLeft = findChildWithComponent(world, HoveredRemoteLeft, eid); + const leftRemote = anyEntityWith(world, RemoteLeft); + if (leftRemote) { + if (hoveredRemoteLeft && !hasComponent(world, Held, hoveredRemoteLeft)) { + interactorOne = APP.world.eid2obj.get(leftRemote); + } + } + const hoveredHandRight = findChildWithComponent(world, HoveredHandRight, eid); + const rightHand = anyEntityWith(world, HandRight); + if (rightHand) { + if (hoveredHandRight && !hasComponent(world, Held, hoveredHandRight)) { + interactorTwo = APP.world.eid2obj.get(rightHand); + } + } + const hoveredRemoteRight = findChildWithComponent(world, HoveredRemoteRight, eid); + const rightRemote = anyEntityWith(world, RemoteRight); + if (rightRemote) { + if (hoveredRemoteRight && !hasComponent(world, Held, hoveredRemoteRight)) { + interactorTwo = APP.world.eid2obj.get(rightRemote); + } + } + + if (interactorOne) { + interactorOne.matrixWorld.toArray(interactorOneTransform); + } + if (interactorTwo) { + interactorTwo.matrixWorld.toArray(interactorTwoTransform); + } + + if (interactorOne || interactorTwo || isFrozen) { + const worldY = obj.matrixWorld.elements[13]; + const ms1 = obj.matrixWorld.elements[4]; + const ms2 = obj.matrixWorld.elements[5]; + const ms3 = obj.matrixWorld.elements[6]; + const worldScale = Math.sqrt(ms1 * ms1 + ms2 * ms2 + ms3 * ms3); + const scaledRadius = worldScale * HoverableVisuals.geometryRadius[eid]; + sweepParams[0] = worldY - scaledRadius; + sweepParams[1] = worldY + scaledRadius; + } + + const uniforms = HoverableVisualsUniforms.get(eid)! as any; + for (let i = 0, l = uniforms.length; i < l; i++) { + const uniform = uniforms[i]; + uniform.hubs_EnableSweepingEffect.value = true; + uniform.hubs_IsFrozen.value = isFrozen; + uniform.hubs_SweepParams.value = sweepParams; + + uniform.hubs_HighlightInteractorOne.value = !!interactorOne && !isTouchscreen; + uniform.hubs_InteractorOnePos.value[0] = interactorOneTransform[12]; + uniform.hubs_InteractorOnePos.value[1] = interactorOneTransform[13]; + uniform.hubs_InteractorOnePos.value[2] = interactorOneTransform[14]; + + uniform.hubs_HighlightInteractorTwo.value = !!interactorTwo && !isTouchscreen; + uniform.hubs_InteractorTwoPos.value[0] = interactorTwoTransform[12]; + uniform.hubs_InteractorTwoPos.value[1] = interactorTwoTransform[13]; + uniform.hubs_InteractorTwoPos.value[2] = interactorTwoTransform[14]; + + if (interactorOne || interactorTwo || isFrozen) { + uniform.hubs_Time.value = world.time.elapsed; + } + } + } + }); +} diff --git a/src/bit-systems/media-loading.ts b/src/bit-systems/media-loading.ts index 57c86b8bfb..6cb7052a96 100644 --- a/src/bit-systems/media-loading.ts +++ b/src/bit-systems/media-loading.ts @@ -13,6 +13,7 @@ import { Box3, Group, Matrix4, Quaternion, Vector3 } from "three"; import { HubsWorld } from "../app"; import { GLTFModel, + HoverableVisuals, LoadedByMediaLoader, MediaContentBounds, MediaImageLoaderData, @@ -47,6 +48,7 @@ import { inflateGrabbable } from "../inflators/grabbable"; import { findAncestorsWithComponent, findChildWithComponent } from "../utils/bit-utils"; import { setMatrixWorld } from "../utils/three-utils"; import { computeObjectAABB, getScaleCoefficient } from "../utils/auto-box-collider"; +import { updateHoverableVisuals } from "./hoverable-visuals-system"; export function* waitForMediaLoaded(world: HubsWorld, eid: EntityID) { while (hasComponent(world, MediaLoader, eid)) { @@ -170,6 +172,9 @@ function* finish(world: HubsWorld, mediaLoaderEid: EntityID) { type: Shape.HULL, minHalfExtent: 0.04 }); + if (hasComponent(world, HoverableVisuals, mediaLoaderEid)) { + updateHoverableVisuals(world, mediaLoaderEid); + } } } diff --git a/src/prefabs/camera-tool.js b/src/prefabs/camera-tool.js index 838e8a59e7..1bacbbbf0d 100644 --- a/src/prefabs/camera-tool.js +++ b/src/prefabs/camera-tool.js @@ -187,6 +187,7 @@ export function CubeMediaFramePrefab() { physicsShape={{ fit: Fit.MANUAL, type: Shape.BOX, halfExtents: [0.5, 0.5, 0.5] }} object3D={new THREE.Mesh(new THREE.BoxBufferGeometry(), new THREE.MeshStandardMaterial())} deletable + hoverableVisuals > diff --git a/src/prefabs/duck.tsx b/src/prefabs/duck.tsx index 8cc407b55d..380053c7d0 100644 --- a/src/prefabs/duck.tsx +++ b/src/prefabs/duck.tsx @@ -46,6 +46,7 @@ export function DuckPrefab(): EntityDef { scale={[1, 1, 1]} inspectable deletable + hoverableVisuals /> ); } diff --git a/src/prefabs/media.tsx b/src/prefabs/media.tsx index cacb88ef5c..9d30cc75ad 100644 --- a/src/prefabs/media.tsx +++ b/src/prefabs/media.tsx @@ -29,6 +29,7 @@ export function MediaPrefab(params: MediaLoaderParams): EntityDef { }} scale={[1, 1, 1]} inspectable + hoverableVisuals /> ); } diff --git a/src/systems/hubs-systems.ts b/src/systems/hubs-systems.ts index 3080057bcb..eb0a73b0e6 100644 --- a/src/systems/hubs-systems.ts +++ b/src/systems/hubs-systems.ts @@ -84,6 +84,7 @@ import { objectMenuTransformSystem } from "../bit-systems/object-menu-transform- import { bitPenCompatSystem } from "./bit-pen-system"; import { sfxButtonSystem } from "../bit-systems/sfx-button-system"; import { sfxMediaSystem } from "../bit-systems/sfx-media-system"; +import { hoverableVisualsSystem } from "../bit-systems/hoverable-visuals-system"; declare global { interface Window { @@ -209,6 +210,8 @@ export function mainTick(xrFrame: XRFrame, renderer: WebGLRenderer, scene: Scene constraintsSystem(world, hubsSystems.physicsSystem); floatyObjectSystem(world); + hoverableVisualsSystem(world); + // We run this earlier in the frame so things have a chance to override properties run by animations hubsSystems.animationMixerSystem.tick(dt); diff --git a/src/utils/jsx-entity.ts b/src/utils/jsx-entity.ts index 93a9f64fb7..2aad827d32 100644 --- a/src/utils/jsx-entity.ts +++ b/src/utils/jsx-entity.ts @@ -39,7 +39,8 @@ import { Quack, MixerAnimatableInitialize, Inspectable, - ObjectMenu + ObjectMenu, + HoverableVisuals } from "../bit-components"; import { inflateMediaLoader } from "../inflators/media-loader"; import { inflateMediaFrame } from "../inflators/media-frame"; @@ -306,6 +307,7 @@ export interface JSXComponentData extends ComponentData { networked?: any; textButton?: any; hoverButton?: any; + hoverableVisuals?: any; rigidbody?: OptionalParams; physicsShape?: OptionalParams; floatyObject?: any; @@ -444,6 +446,7 @@ const jsxInflators: Required<{ [K in keyof JSXComponentData]: InflatorFn }> = { holdableButton: createDefaultInflator(HoldableButton), textButton: createDefaultInflator(TextButton), hoverButton: createDefaultInflator(HoverButton), + hoverableVisuals: createDefaultInflator(HoverableVisuals), holdable: createDefaultInflator(Holdable), deletable: createDefaultInflator(Deletable), rigidbody: inflateRigidBody,