diff --git a/packages/client-core/src/common/components/TouchGamepad/index.tsx b/packages/client-core/src/common/components/TouchGamepad/index.tsx index 41221e1490..bcdb2ad938 100755 --- a/packages/client-core/src/common/components/TouchGamepad/index.tsx +++ b/packages/client-core/src/common/components/TouchGamepad/index.tsx @@ -62,7 +62,7 @@ const handleStop = () => { const buttonsConfig: Array<{ button: AnyButton; label: React.ReactElement }> = [ { - button: XRStandardGamepadButton.Trigger, + button: XRStandardGamepadButton.XRStandardGamepadTrigger, label: } ] diff --git a/packages/client-core/src/systems/AvatarUISystem.tsx b/packages/client-core/src/systems/AvatarUISystem.tsx index 6faf49d821..b0fbfec87b 100644 --- a/packages/client-core/src/systems/AvatarUISystem.tsx +++ b/packages/client-core/src/systems/AvatarUISystem.tsx @@ -133,7 +133,7 @@ const raycastComponentData = { const onSecondaryClick = () => { const { physicsWorld } = getState(PhysicsState) - const inputPointerEntity = InputPointerComponent.getPointerForCanvas(Engine.instance.viewerEntity) + const inputPointerEntity = InputPointerComponent.getPointersForCamera(Engine.instance.viewerEntity)[0] if (!inputPointerEntity) return const pointerPosition = getComponent(inputPointerEntity, InputPointerComponent).position const hits = Physics.castRayFromCamera( diff --git a/packages/client-core/src/systems/WidgetUISystem.tsx b/packages/client-core/src/systems/WidgetUISystem.tsx index 75f83f2b15..c96e8a9fc3 100644 --- a/packages/client-core/src/systems/WidgetUISystem.tsx +++ b/packages/client-core/src/systems/WidgetUISystem.tsx @@ -125,7 +125,7 @@ const execute = () => { const inputSource = getComponent(inputSourceEntity, InputSourceComponent) const keys = inputSource.buttons if (inputSource.source.gamepad?.mapping === 'xr-standard') { - if (keys[XRStandardGamepadButton.ButtonA]?.down) + if (keys[XRStandardGamepadButton.XRStandardGamepadButtonA]?.down) toggleWidgetsMenu(inputSource.source.handedness === 'left' ? 'right' : 'left') } /** @todo allow non HMDs to access the widget menu too */ diff --git a/packages/client-core/src/systems/createAnchorWidget.tsx b/packages/client-core/src/systems/createAnchorWidget.tsx index 81d89d7b45..ed5526297f 100644 --- a/packages/client-core/src/systems/createAnchorWidget.tsx +++ b/packages/client-core/src/systems/createAnchorWidget.tsx @@ -77,7 +77,7 @@ export function createAnchorWidget() { if (inputComponent.source.gamepad?.mapping !== 'xr-standard') continue if (inputComponent.source.handedness !== preferredHand) continue - const buttonInputPressed = inputComponent.buttons[XRStandardGamepadButton.Trigger]?.down + const buttonInputPressed = inputComponent.buttons[XRStandardGamepadButton.XRStandardGamepadTrigger]?.down if (buttonInputPressed) { xrState.scenePlacementMode.set('placed') @@ -86,8 +86,8 @@ export function createAnchorWidget() { const { deltaSeconds } = getState(ECSState) - const xAxisInput = inputComponent.source.gamepad.axes[XRStandardGamepadAxes.ThumbstickX] - const yAxisInput = inputComponent.source.gamepad.axes[XRStandardGamepadAxes.ThumbstickY] + const xAxisInput = inputComponent.source.gamepad.axes[XRStandardGamepadAxes.XRStandardGamepadThumbstickX] + const yAxisInput = inputComponent.source.gamepad.axes[XRStandardGamepadAxes.XRStandardGamepadThumbstickY] const xDelta = xAxisInput * Math.PI * deltaSeconds getMutableState(XRState).sceneRotationOffset.set((currentValue) => currentValue + xDelta) @@ -97,7 +97,7 @@ export function createAnchorWidget() { xrState.sceneScaleTarget.set((currentValue) => MathUtils.clamp(currentValue + yDelta, 0.01, 0.2)) } - const triggerButtonPressed = inputComponent.buttons[XRStandardGamepadButton.Stick]?.down + const triggerButtonPressed = inputComponent.buttons[XRStandardGamepadButton.XRStandardGamepadStick]?.down if (triggerButtonPressed) { xrState.sceneScaleAutoMode.set(!xrState.sceneScaleAutoMode.value) diff --git a/packages/ecs/src/ComponentFunctions.ts b/packages/ecs/src/ComponentFunctions.ts index cb6fd970e6..e3c9f000a2 100755 --- a/packages/ecs/src/ComponentFunctions.ts +++ b/packages/ecs/src/ComponentFunctions.ts @@ -508,6 +508,7 @@ export function _use(promise) { * Use a component in a reactive context (a React component) */ export function useComponent>(entity: Entity, Component: C) { + if (entity === UndefinedEntity) throw new Error('InvalidUsage: useComponent called with UndefinedEntity') if (!Component.stateMap[entity]) Component.stateMap[entity] = hookstate(none) const componentState = Component.stateMap[entity]! // use() will suspend the component (by throwing a promise) and resume when the promise is resolved diff --git a/packages/editor/src/components/properties/CameraPropertiesNodeEditor.tsx b/packages/editor/src/components/properties/CameraPropertiesNodeEditor.tsx index e64e262849..0b49e06785 100644 --- a/packages/editor/src/components/properties/CameraPropertiesNodeEditor.tsx +++ b/packages/editor/src/components/properties/CameraPropertiesNodeEditor.tsx @@ -31,7 +31,7 @@ import { getOptionalComponent, useComponent } from '@etherealengine/ecs/src/Comp import { defineQuery } from '@etherealengine/ecs/src/QueryFunctions' import { CameraSettingsComponent } from '@etherealengine/engine/src/scene/components/CameraSettingsComponent' import { ModelComponent } from '@etherealengine/engine/src/scene/components/ModelComponent' -import { CameraMode } from '@etherealengine/spatial/src/camera/types/CameraMode' +import { FollowCameraMode } from '@etherealengine/spatial/src/camera/types/FollowCameraMode' import { MeshComponent } from '@etherealengine/spatial/src/renderer/components/MeshComponent' import { iterateEntityNode } from '@etherealengine/spatial/src/transform/components/EntityTree' @@ -46,28 +46,29 @@ import { commitProperties, commitProperty, EditorComponentType, updateProperty } const cameraModeSelect = [ { label: 'First Person', - value: CameraMode.FirstPerson + value: FollowCameraMode.FirstPerson }, { label: 'Shoulder Cam', - value: CameraMode.ShoulderCam + value: FollowCameraMode.ShoulderCam }, { label: 'Third Person', - value: CameraMode.ThirdPerson + value: FollowCameraMode.ThirdPerson }, { label: 'Top Down', - value: CameraMode.TopDown - }, - { - label: 'Strategic', - value: CameraMode.Strategic - }, - { - label: 'Dynamic', - value: CameraMode.Dynamic + value: FollowCameraMode.TopDown } + // These are not currently defined or implemented: + // { + // label: 'Strategic', + // value: FollowCameraMode.Strategic + // }, + // { + // label: 'Dynamic', + // value: FollowCameraMode.Dynamic + // } ] /** Types copied from Camera Modes of engine. */ diff --git a/packages/editor/src/functions/gizmoHelper.ts b/packages/editor/src/functions/gizmoHelper.ts index 4445ab86cb..7e4a1511a6 100644 --- a/packages/editor/src/functions/gizmoHelper.ts +++ b/packages/editor/src/functions/gizmoHelper.ts @@ -483,7 +483,7 @@ export function controlUpdate(gizmoEntity: Entity) { function pointerHover(gizmoEntity) { // TODO support gizmos in multiple viewports - const inputPointerEntity = InputPointerComponent.getPointerForCanvas(Engine.instance.viewerEntity) + const inputPointerEntity = InputPointerComponent.getPointersForCamera(Engine.instance.viewerEntity)[0] if (!inputPointerEntity) return const pointerPosition = getComponent(inputPointerEntity, InputPointerComponent).position const gizmoControlComponent = getMutableComponent(gizmoEntity, TransformGizmoControlComponent) @@ -509,7 +509,7 @@ function pointerHover(gizmoEntity) { function pointerDown(gizmoEntity) { // TODO support gizmos in multiple viewports - const inputPointerEntity = InputPointerComponent.getPointerForCanvas(Engine.instance.viewerEntity) + const inputPointerEntity = InputPointerComponent.getPointersForCamera(Engine.instance.viewerEntity)[0] if (!inputPointerEntity) return const pointer = getComponent(inputPointerEntity, InputPointerComponent) const gizmoControlComponent = getMutableComponent(gizmoEntity, TransformGizmoControlComponent) @@ -768,7 +768,7 @@ function applyPivotRotation(entity, pivotToOriginMatrix, originToPivotMatrix, ro function pointerMove(gizmoEntity) { // TODO support gizmos in multiple viewports - const inputPointerEntity = InputPointerComponent.getPointerForCanvas(Engine.instance.viewerEntity) + const inputPointerEntity = InputPointerComponent.getPointersForCamera(Engine.instance.viewerEntity)[0] if (!inputPointerEntity) return const pointer = getComponent(inputPointerEntity, InputPointerComponent) const gizmoControlComponent = getMutableComponent(gizmoEntity, TransformGizmoControlComponent) @@ -901,7 +901,7 @@ function pointerMove(gizmoEntity) { function pointerUp(gizmoEntity) { // TODO support gizmos in multiple viewports - const inputPointerEntity = InputPointerComponent.getPointerForCanvas(Engine.instance.viewerEntity) + const inputPointerEntity = InputPointerComponent.getPointersForCamera(Engine.instance.viewerEntity)[0] if (!inputPointerEntity) return const pointer = getComponent(inputPointerEntity, InputPointerComponent) diff --git a/packages/editor/src/systems/ClickPlacementSystem.tsx b/packages/editor/src/systems/ClickPlacementSystem.tsx index b2334a3011..3404d1d9d4 100644 --- a/packages/editor/src/systems/ClickPlacementSystem.tsx +++ b/packages/editor/src/systems/ClickPlacementSystem.tsx @@ -241,7 +241,7 @@ export const ClickPlacementSystem = defineSystem({ execute: () => { const editorHelperState = getState(EditorHelperState) if (editorHelperState.placementMode !== PlacementMode.CLICK) return - const clickState = getState(ClickPlacementState) + const clickState = getMutableState(ClickPlacementState) const placementEntity = clickState.placementEntity if (!placementEntity) return @@ -262,7 +262,7 @@ export const ClickPlacementSystem = defineSystem({ let targetIntersection: { point: Vector3; normal: Vector3 } | null = null const viewerEntity = Engine.instance.viewerEntity - const mouseEntity = InputPointerComponent.getPointerForCanvas(viewerEntity) + const mouseEntity = InputPointerComponent.getPointersForCamera(viewerEntity)[0] if (!mouseEntity) return const buttons = InputComponent.getMergedButtons(viewerEntity) @@ -271,14 +271,14 @@ export const ClickPlacementSystem = defineSystem({ const zoom = axes[MouseScroll.VerticalScroll] if (buttons.SecondaryClick?.pressed) { - clickState.maxDistance -= zoom + clickState.maxDistance.set(clickState.maxDistance.value - zoom) } if (buttons.KeyE?.up) { - clickState.yawOffset += Math.PI / 4 + clickState.yawOffset.set(clickState.yawOffset.value + Math.PI / 4) } if (buttons.KeyQ?.up) { - clickState.yawOffset -= Math.PI / 4 + clickState.yawOffset.set(clickState.yawOffset.value - Math.PI / 4) } if (buttons.PrimaryClick?.up) { clickListener() @@ -292,7 +292,7 @@ export const ClickPlacementSystem = defineSystem({ const cameraPosition = pointerScreenRaycaster.ray.origin const cameraDirection = pointerScreenRaycaster.ray.direction const physicsIntersection = physicsWorld.castRayAndGetNormal(new Ray(cameraPosition, cameraDirection), 1000, false) - if (physicsIntersection && physicsIntersection.toi < clickState.maxDistance) { + if (physicsIntersection && physicsIntersection.toi < clickState.maxDistance.value) { const intersectPosition = cameraPosition .clone() .add(cameraDirection.clone().multiplyScalar(physicsIntersection.toi)) @@ -311,7 +311,7 @@ export const ClickPlacementSystem = defineSystem({ //if (intersect.length === 0 && !targetIntersection) return for (let i = 0; i < intersect.length; i++) { const intersected = intersect[i] - if (intersected.distance > clickState.maxDistance) continue + if (intersected.distance > clickState.maxDistance.value) continue if (isPlacementDescendant(intersected.object.entity)) continue targetIntersection = { point: intersected.point, @@ -321,16 +321,16 @@ export const ClickPlacementSystem = defineSystem({ } if (!targetIntersection) { - const point = cameraPosition.clone().add(cameraDirection.clone().multiplyScalar(clickState.maxDistance)) + const point = cameraPosition.clone().add(cameraDirection.clone().multiplyScalar(clickState.maxDistance.value)) targetIntersection = { point, normal: new Vector3(0, 1, 0) } } const position = targetIntersection.point let rotation = new Quaternion().setFromUnitVectors(new Vector3(), targetIntersection.normal ?? new Vector3(0, 1, 0)) const offset = new Quaternion().setFromEuler( - new Euler(clickState.pitchOffset, clickState.yawOffset, clickState.rollOffset) + new Euler(clickState.pitchOffset.value, clickState.yawOffset.value, clickState.rollOffset.value) ) rotation = offset.multiply(rotation) - setComponent(placementEntity, TransformComponent, { position, rotation }) + setComponent(placementEntity.value, TransformComponent, { position, rotation }) } }) diff --git a/packages/engine/src/avatar/AvatarModule.ts b/packages/engine/src/avatar/AvatarModule.ts index 9a7299a4cf..9c64b60e4a 100644 --- a/packages/engine/src/avatar/AvatarModule.ts +++ b/packages/engine/src/avatar/AvatarModule.ts @@ -28,7 +28,6 @@ import { AvatarState } from './state/AvatarNetworkState' import { AnimationSystem } from './systems/AnimationSystem' import { AvatarAnimationSystem } from './systems/AvatarAnimationSystem' import { AvatarAutopilotSystem } from './systems/AvatarAutopilotSystem' -import { AvatarCameraInputSystem } from './systems/AvatarCameraInputSystem' import { AvatarControllerSystem } from './systems/AvatarControllerSystem' import { AvatarInputSystem } from './systems/AvatarInputSystem' import { AvatarLoadingSystem } from './systems/AvatarLoadingSystem' @@ -42,7 +41,6 @@ export default { AnimationSystem, AvatarAnimationSystem, AvatarAutopilotSystem, - AvatarCameraInputSystem, AvatarControllerSystem, AvatarIKTargetState, AvatarInputSystem, diff --git a/packages/engine/src/avatar/components/AvatarControllerComponent.ts b/packages/engine/src/avatar/components/AvatarControllerComponent.ts index e4662de174..f95a58fc3a 100755 --- a/packages/engine/src/avatar/components/AvatarControllerComponent.ts +++ b/packages/engine/src/avatar/components/AvatarControllerComponent.ts @@ -48,6 +48,8 @@ import { CameraComponent } from '../../../../spatial/src/camera/components/Camer import { setAvatarColliderTransform } from '../functions/spawnAvatarReceptor' import { AvatarComponent } from './AvatarComponent' +export const eyeOffset = 0.25 + export const AvatarControllerComponent = defineComponent({ name: 'AvatarControllerComponent', @@ -109,7 +111,8 @@ export const AvatarControllerComponent = defineComponent({ const cameraEntity = avatarControllerComponent.cameraEntity.value if (cameraEntity && entityExists(cameraEntity) && hasComponent(cameraEntity, FollowCameraComponent)) { const cameraComponent = getComponent(cameraEntity, FollowCameraComponent) - cameraComponent.offset.set(0, avatarComponent.eyeHeight.value, 0) + cameraComponent.firstPersonOffset.set(0, avatarComponent.eyeHeight.value, eyeOffset) + cameraComponent.thirdPersonOffset.set(0, avatarComponent.eyeHeight.value, 0) } }, [avatarComponent.avatarHeight, camera.near]) diff --git a/packages/engine/src/avatar/functions/autopilotFunctions.ts b/packages/engine/src/avatar/functions/autopilotFunctions.ts index 78d2a40e2c..b463afbbc1 100644 --- a/packages/engine/src/avatar/functions/autopilotFunctions.ts +++ b/packages/engine/src/avatar/functions/autopilotFunctions.ts @@ -65,7 +65,7 @@ export const autopilotSetPosition = (entity: Entity) => { const { physicsWorld } = getState(PhysicsState) - const inputPointerEntity = InputPointerComponent.getPointerForCanvas(Engine.instance.viewerEntity) + const inputPointerEntity = InputPointerComponent.getPointersForCamera(Engine.instance.viewerEntity)[0] if (!inputPointerEntity) return const pointerPosition = getComponent(inputPointerEntity, InputPointerComponent).position diff --git a/packages/engine/src/avatar/functions/spawnAvatarReceptor.ts b/packages/engine/src/avatar/functions/spawnAvatarReceptor.ts index 67d1f187b4..0e2a2b15b8 100644 --- a/packages/engine/src/avatar/functions/spawnAvatarReceptor.ts +++ b/packages/engine/src/avatar/functions/spawnAvatarReceptor.ts @@ -63,8 +63,7 @@ import { proxifyParentChildRelationships } from '../../scene/functions/loadGLTFM import { AnimationComponent } from '../components/AnimationComponent' import { AvatarAnimationComponent, AvatarRigComponent } from '../components/AvatarAnimationComponent' import { AvatarComponent } from '../components/AvatarComponent' -import { AvatarColliderComponent, AvatarControllerComponent } from '../components/AvatarControllerComponent' -import { eyeOffset } from '../systems/AvatarTransparencySystem' +import { AvatarColliderComponent, AvatarControllerComponent, eyeOffset } from '../components/AvatarControllerComponent' export const spawnAvatarReceptor = (entityUUID: EntityUUID) => { const entity = UUIDComponent.getEntityByUUID(entityUUID) diff --git a/packages/engine/src/avatar/systems/AvatarCameraInputSystem.ts b/packages/engine/src/avatar/systems/AvatarCameraInputSystem.ts deleted file mode 100644 index 5718fb6010..0000000000 --- a/packages/engine/src/avatar/systems/AvatarCameraInputSystem.ts +++ /dev/null @@ -1,199 +0,0 @@ -/* -CPAL-1.0 License - -The contents of this file are subject to the Common Public Attribution License -Version 1.0. (the "License"); you may not use this file except in compliance -with the License. You may obtain a copy of the License at -https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. -The License is based on the Mozilla Public License Version 1.1, but Sections 14 -and 15 have been added to cover use of software over a computer network and -provide for limited attribution for the Original Developer. In addition, -Exhibit A has been modified to be consistent with Exhibit B. - -Software distributed under the License is distributed on an "AS IS" basis, -WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the -specific language governing rights and limitations under the License. - -The Original Code is Ethereal Engine. - -The Original Developer is the Initial Developer. The Initial Developer of the -Original Code is the Ethereal Engine team. - -All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 -Ethereal Engine. All Rights Reserved. -*/ - -import { Vector2 } from 'three' - -import { Engine } from '@etherealengine/ecs' -import { getComponent, getOptionalComponent } from '@etherealengine/ecs/src/ComponentFunctions' -import { ECSState } from '@etherealengine/ecs/src/ECSState' -import { defineQuery } from '@etherealengine/ecs/src/QueryFunctions' -import { defineSystem } from '@etherealengine/ecs/src/SystemFunctions' -import { InputSystemGroup } from '@etherealengine/ecs/src/SystemGroups' -import { getMutableState, getState } from '@etherealengine/hyperflux' -import { CameraSettings } from '@etherealengine/spatial/src/camera/CameraState' -import { FollowCameraComponent } from '@etherealengine/spatial/src/camera/components/FollowCameraComponent' -import { TargetCameraRotationComponent } from '@etherealengine/spatial/src/camera/components/TargetCameraRotationComponent' -import { handleCameraZoom, setTargetCameraRotation } from '@etherealengine/spatial/src/camera/functions/CameraFunctions' -import { switchCameraMode } from '@etherealengine/spatial/src/camera/functions/switchCameraMode' -import { CameraMode } from '@etherealengine/spatial/src/camera/types/CameraMode' -import { throttle } from '@etherealengine/spatial/src/common/functions/FunctionHelpers' -import { isMobile } from '@etherealengine/spatial/src/common/functions/isMobile' -import { InputComponent } from '@etherealengine/spatial/src/input/components/InputComponent' -import { InputPointerComponent } from '@etherealengine/spatial/src/input/components/InputPointerComponent' -import { InputSourceComponent } from '@etherealengine/spatial/src/input/components/InputSourceComponent' -import { MouseScroll } from '@etherealengine/spatial/src/input/state/ButtonState' -import { InputState } from '@etherealengine/spatial/src/input/state/InputState' -import { XRState } from '@etherealengine/spatial/src/xr/XRState' - -import { TransformComponent } from '@etherealengine/spatial' -import { AvatarControllerComponent } from '../../avatar/components/AvatarControllerComponent' -import { getThumbstickOrThumbpadAxes } from '../../avatar/systems/AvatarInputSystem' -import { AvatarComponent } from '../components/AvatarComponent' - -const avatarControllerQuery = defineQuery([AvatarControllerComponent]) - -const onKeyV = () => { - for (const entity of avatarControllerQuery()) { - const avatarController = getComponent(entity, AvatarControllerComponent) - const cameraEntity = avatarController.cameraEntity - const followComponent = getOptionalComponent(cameraEntity, FollowCameraComponent) - if (followComponent) - switch (followComponent.mode) { - case CameraMode.FirstPerson: - switchCameraMode(entity, { cameraMode: CameraMode.ShoulderCam }) - break - case CameraMode.ShoulderCam: - switchCameraMode(entity, { cameraMode: CameraMode.ThirdPerson }) - followComponent.distance = followComponent.minDistance + 1 - break - case CameraMode.ThirdPerson: - switchCameraMode(entity, { cameraMode: CameraMode.TopDown }) - break - case CameraMode.TopDown: - switchCameraMode(entity, { cameraMode: CameraMode.FirstPerson }) - break - default: - break - } - } -} - -const onKeyF = () => { - for (const entity of avatarControllerQuery()) { - const avatarController = getComponent(entity, AvatarControllerComponent) - const cameraEntity = avatarController.cameraEntity - const followComponent = getOptionalComponent(cameraEntity, FollowCameraComponent) - if (followComponent && followComponent.mode !== CameraMode.FirstPerson) { - followComponent.locked = !followComponent.locked - } - } -} - -const onKeyC = () => { - for (const entity of avatarControllerQuery()) { - const avatarController = getComponent(entity, AvatarControllerComponent) - const cameraEntity = avatarController.cameraEntity - const followComponent = getOptionalComponent(cameraEntity, FollowCameraComponent) - if (followComponent) followComponent.shoulderSide = !followComponent.shoulderSide - } -} - -const lastLookDelta = new Vector2() -let lastMouseMoved = false -const INPUT_CAPTURE_DELAY = 0.1 - -const throttleHandleCameraZoom = throttle(handleCameraZoom, 30, { leading: true, trailing: false }) - -const lastPointerPosition = new Vector2() -const pointerMovement = new Vector2() - -const pointerQuery = defineQuery([InputSourceComponent, TransformComponent]) - -const capturedByView = (): boolean => { - return getState(InputState).capturingEntity === Engine.instance.viewerEntity -} - -const execute = () => { - if (getState(XRState).xrFrame) return - - const deltaSeconds = getState(ECSState).deltaSeconds - - const selfAvatarEntity = AvatarComponent.getSelfAvatarEntity() - if (!selfAvatarEntity) return - - const cameraSettings = getState(CameraSettings) - - const avatarControllerEntities = avatarControllerQuery() - - const viewerEntity = Engine.instance.viewerEntity - - const inputPointerEntity = InputPointerComponent.getPointerForCanvas(viewerEntity) - if (!inputPointerEntity) return - - const buttons = InputComponent.getMergedButtons(viewerEntity) - const axes = InputComponent.getMergedAxes(viewerEntity) - const inputSource = getOptionalComponent(inputPointerEntity, InputSourceComponent) - const inputPointer = getOptionalComponent(inputPointerEntity, InputPointerComponent) - - if (buttons?.KeyV?.down) onKeyV() - if (buttons?.KeyF?.down) onKeyF() - if (buttons?.KeyC?.down) onKeyC() - - if (!inputPointer || (getMutableState(InputState).capturingEntity && !capturedByView)) return - - const inputState = getState(InputState) - pointerMovement.subVectors(inputPointer.position, lastPointerPosition) - lastPointerPosition.copy(inputPointer.position) - - const mouseMoved = isMobile - ? pointerMovement.lengthSq() > 0 && buttons?.PrimaryClick?.pressed - : buttons?.PrimaryClick?.pressed - - for (const entity of avatarControllerEntities) { - if (!inputSource) continue - - const avatarController = getComponent(entity, AvatarControllerComponent) - const cameraEntity = avatarController.cameraEntity - const target = - getOptionalComponent(cameraEntity, TargetCameraRotationComponent) ?? - getOptionalComponent(cameraEntity, FollowCameraComponent) - if (!target) continue - - if (!lastMouseMoved && mouseMoved) lastLookDelta.set(inputPointer.position.x, inputPointer.position.y) - - const [x, z] = getThumbstickOrThumbpadAxes(inputSource.source, inputState.preferredHand) - target.theta -= x * 2 - target.phi += z * 2 - - const keyDelta = (buttons?.ArrowLeft ? 1 : 0) + (buttons?.ArrowRight ? -1 : 0) - target.theta += 100 * deltaSeconds * keyDelta - setTargetCameraRotation(cameraEntity, target.phi, target.theta) - - if (mouseMoved) { - setTargetCameraRotation( - cameraEntity, - target.phi - (inputPointer.position.y - lastLookDelta.y) * cameraSettings.cameraRotationSpeed, - target.theta - (inputPointer.position.x - lastLookDelta.x) * cameraSettings.cameraRotationSpeed, - 0.1 - ) - } - - if (buttons?.PrimaryClick?.pressed) { - InputState.setCapturingEntity(cameraEntity) - } - const zoom = axes[MouseScroll.VerticalScroll] - throttleHandleCameraZoom(cameraEntity, zoom) - } - - lastLookDelta.set(inputPointer.position.x, inputPointer.position.y) - - lastMouseMoved = !!mouseMoved -} - -export const AvatarCameraInputSystem = defineSystem({ - uuid: 'ee.engine.AvatarCameraInputSystem', - insert: { after: InputSystemGroup }, - execute -}) diff --git a/packages/engine/src/avatar/systems/AvatarInputSystem.ts b/packages/engine/src/avatar/systems/AvatarInputSystem.ts index e554cb0acf..42c41ec8d4 100755 --- a/packages/engine/src/avatar/systems/AvatarInputSystem.ts +++ b/packages/engine/src/avatar/systems/AvatarInputSystem.ts @@ -62,20 +62,11 @@ import { AvatarComponent } from '../components/AvatarComponent' import { applyInputSourcePoseToIKTargets } from '../functions/applyInputSourcePoseToIKTargets' import { setIkFootTarget } from '../functions/avatarFootHeuristics' -const _quat = new Quaternion() +import { FollowCameraComponent } from '@etherealengine/spatial/src/camera/components/FollowCameraComponent' +import { FollowCameraMode } from '@etherealengine/spatial/src/camera/types/FollowCameraMode' +import { getThumbstickOrThumbpadAxes } from '@etherealengine/spatial/src/input/functions/getThumbstickOrThumbpadAxes' -/** - * On 'xr-standard' mapping, get thumbstick input [2,3], fallback to thumbpad input [0,1] - * On 'standard' mapping, get thumbstick input [0,1] - */ -export function getThumbstickOrThumbpadAxes(inputSource: XRInputSource, handedness: XRHandedness, deadZone = 0.05) { - const gamepad = inputSource.gamepad - const axes = gamepad!.axes - const axesIndex = inputSource.gamepad?.mapping === 'xr-standard' || handedness === 'right' ? 2 : 0 - const xAxis = Math.abs(axes[axesIndex]) > deadZone ? axes[axesIndex] : 0 - const zAxis = Math.abs(axes[axesIndex + 1]) > deadZone ? axes[axesIndex + 1] : 0 - return [xAxis, zAxis] as [number, number] -} +const _quat = new Quaternion() export const InputSourceAxesDidReset = new WeakMap() @@ -244,14 +235,16 @@ const execute = () => { controller.gamepadLocalInput.set(0, 0, 0) const viewerEntity = Engine.instance.viewerEntity - const inputPointerEntity = InputPointerComponent.getPointerForCanvas(viewerEntity) + + const inputPointerEntity = InputPointerComponent.getPointersForCamera(viewerEntity)[0] + if (!inputPointerEntity && !xrState.session) return const buttons = InputComponent.getMergedButtons(viewerEntity) if (buttons.ShiftLeft?.down) onShiftLeft() - const gamepadJump = buttons[StandardGamepadButton.ButtonA]?.down + const gamepadJump = buttons[StandardGamepadButton.StandardGamepadButtonA]?.down //** touch input (only for avatar jump)*/ const doubleClicked = isCameraAttachedToAvatar ? false : getAvatarDoubleClick(buttons) @@ -259,15 +252,22 @@ const execute = () => { const keyDeltaX = (buttons.KeyA?.pressed ? -1 : 0) + (buttons.KeyD?.pressed ? 1 : 0) + - (buttons[StandardGamepadButton.DPadLeft]?.pressed ? -1 : 0) + - (buttons[StandardGamepadButton.DPadRight]?.pressed ? 1 : 0) + (buttons[StandardGamepadButton.StandardGamepadDPadLeft]?.pressed ? -1 : 0) + + (buttons[StandardGamepadButton.StandardGamepadDPadRight]?.pressed ? 1 : 0) const keyDeltaZ = (buttons.KeyW?.pressed ? -1 : 0) + (buttons.KeyS?.pressed ? 1 : 0) + (buttons.ArrowUp?.pressed ? -1 : 0) + (buttons.ArrowDown?.pressed ? 1 : 0) + - (buttons[StandardGamepadButton.DPadUp]?.pressed ? -1 : 0) + - (buttons[StandardGamepadButton.DPadDown]?.pressed ? -1 : 0) + (buttons[StandardGamepadButton.StandardGamepadDPadUp]?.pressed ? -1 : 0) + + (buttons[StandardGamepadButton.StandardGamepadDPadDown]?.pressed ? -1 : 0) + + if (keyDeltaZ === 1) { + // todo: auto-adjust target distance in follow camera system based on target velocity + const follow = getOptionalComponent(controller.cameraEntity, FollowCameraComponent) + if (follow?.mode === FollowCameraMode.ThirdPerson || follow?.mode === FollowCameraMode.ShoulderCam) + follow.targetDistance = Math.max(follow.targetDistance, follow.effectiveMaxDistance * 0.5) + } controller.gamepadLocalInput.set(keyDeltaX, 0, keyDeltaZ).normalize() diff --git a/packages/engine/src/avatar/systems/AvatarTransparencySystem.tsx b/packages/engine/src/avatar/systems/AvatarTransparencySystem.tsx index 81202402fb..8f388ae826 100644 --- a/packages/engine/src/avatar/systems/AvatarTransparencySystem.tsx +++ b/packages/engine/src/avatar/systems/AvatarTransparencySystem.tsx @@ -50,7 +50,6 @@ import React, { useEffect } from 'react' import { SourceComponent } from '../../scene/components/SourceComponent' import { useModelSceneID } from '../../scene/functions/loaders/ModelFunctions' import { AvatarComponent } from '../components/AvatarComponent' -import { AvatarHeadDecapComponent } from '../components/AvatarIKComponents' const headDithering = 0 const cameraDithering = 1 @@ -85,25 +84,12 @@ const execute = () => { } } -export const eyeOffset = 0.25 - export const AvatarTransparencySystem = defineSystem({ uuid: 'AvatarTransparencySystem', execute, insert: { with: PresentationSystemGroup }, reactor: () => { const selfEid = AvatarComponent.useSelfAvatarEntity() - const hasDecapComponent = !!useOptionalComponent(selfEid, AvatarHeadDecapComponent) - const hasFollowCamera = !!useOptionalComponent(Engine.instance.viewerEntity, FollowCameraComponent) - useEffect(() => { - const followCamera = getOptionalComponent(Engine.instance.viewerEntity, FollowCameraComponent) - if (!followCamera) return - const prevOffsetZ = followCamera.offset.z - followCamera.offset.setZ(eyeOffset) - return () => { - followCamera.offset.setZ(prevOffsetZ) - } - }, [hasFollowCamera, hasDecapComponent, selfEid]) const sceneInstanceID = useModelSceneID(selfEid) const childEntities = useHookstate(SourceComponent.entitiesBySourceState[sceneInstanceID]) diff --git a/packages/engine/src/interaction/functions/createUI.ts b/packages/engine/src/interaction/functions/createUI.ts index ca9f169a69..0148babac1 100755 --- a/packages/engine/src/interaction/functions/createUI.ts +++ b/packages/engine/src/interaction/functions/createUI.ts @@ -52,7 +52,7 @@ export function createUI(entity: Entity, uiMessage: string, isInteractable = tru color: new Color('#B9B9B9'), transmission: 1, roughness: 0.5, - opacity: 0.95, + opacity: 1, transparent: true, side: DoubleSide }) diff --git a/packages/engine/src/scene/components/CameraSettingsComponent.ts b/packages/engine/src/scene/components/CameraSettingsComponent.ts index b77fbd6d57..03f1ebdfa5 100755 --- a/packages/engine/src/scene/components/CameraSettingsComponent.ts +++ b/packages/engine/src/scene/components/CameraSettingsComponent.ts @@ -47,7 +47,8 @@ export const CameraSettingsComponent = defineComponent({ if (typeof json.cameraNearClip === 'number') component.cameraNearClip.set(json.cameraNearClip) if (typeof json.cameraFarClip === 'number') component.cameraFarClip.set(json.cameraFarClip) if (typeof json.projectionType === 'number') component.projectionType.set(json.projectionType) - if (typeof json.minCameraDistance === 'number') component.minCameraDistance.set(json.minCameraDistance) + if (typeof json.minCameraDistance === 'number') + component.minCameraDistance.set(Math.max(json.minCameraDistance, 1.5)) if (typeof json.maxCameraDistance === 'number') component.maxCameraDistance.set(json.maxCameraDistance) if (typeof json.startCameraDistance === 'number') component.startCameraDistance.set(json.startCameraDistance) if (typeof json.cameraMode === 'number') component.cameraMode.set(json.cameraMode) diff --git a/packages/engine/src/scene/components/LinkComponent.ts b/packages/engine/src/scene/components/LinkComponent.ts index adc2bbb82d..eea99f3ce7 100755 --- a/packages/engine/src/scene/components/LinkComponent.ts +++ b/packages/engine/src/scene/components/LinkComponent.ts @@ -32,7 +32,6 @@ import { Entity } from '@etherealengine/ecs/src/Entity' import { useEntityContext } from '@etherealengine/ecs/src/EntityFunctions' import { defineState, getMutableState, getState, matches } from '@etherealengine/hyperflux' import { setCallback } from '@etherealengine/spatial/src/common/CallbackComponent' -import { XRStandardGamepadButton } from '@etherealengine/spatial/src/input/state/ButtonState' import { XRState } from '@etherealengine/spatial/src/xr/XRState' import { InputComponent } from '@etherealengine/spatial/src/input/components/InputComponent' @@ -49,7 +48,7 @@ const linkLogic = (linkComponent, xrState) => { const linkCallback = (linkEntity: Entity) => { const linkComponent = getComponent(linkEntity, LinkComponent) const buttons = InputComponent.getMergedButtons(linkEntity) - if (buttons[XRStandardGamepadButton.Trigger]?.down) { + if (buttons.XRStandardGamepadTrigger?.down) { const xrState = getState(XRState) linkLogic(linkComponent, xrState) } else { diff --git a/packages/engine/src/scene/systems/PortalSystem.ts b/packages/engine/src/scene/systems/PortalSystem.ts index 157c648875..2ce40391fd 100644 --- a/packages/engine/src/scene/systems/PortalSystem.ts +++ b/packages/engine/src/scene/systems/PortalSystem.ts @@ -26,15 +26,15 @@ Ethereal Engine. All Rights Reserved. import { useEffect } from 'react' import { UUIDComponent } from '@etherealengine/ecs' -import { getComponent } from '@etherealengine/ecs/src/ComponentFunctions' +import { getComponent, getMutableComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { Engine } from '@etherealengine/ecs/src/Engine' import { defineSystem } from '@etherealengine/ecs/src/SystemFunctions' import { PresentationSystemGroup } from '@etherealengine/ecs/src/SystemGroups' import { getMutableState, getState, useHookstate } from '@etherealengine/hyperflux' import { SpawnPoseState } from '@etherealengine/spatial' -import { switchCameraMode } from '@etherealengine/spatial/src/camera/functions/switchCameraMode' -import { CameraMode } from '@etherealengine/spatial/src/camera/types/CameraMode' +import { FollowCameraMode } from '@etherealengine/spatial/src/camera/types/FollowCameraMode' +import { FollowCameraComponent } from '@etherealengine/spatial/src/camera/components/FollowCameraComponent' import { AvatarComponent } from '../../avatar/components/AvatarComponent' import { AvatarControllerComponent } from '../../avatar/components/AvatarControllerComponent' import { PortalComponent, PortalState } from '../components/PortalComponent' @@ -46,7 +46,7 @@ const reactor = () => { const activePortalEntity = activePortalEntityState.value if (!activePortalEntity) return const activePortal = getComponent(activePortalEntity, PortalComponent) - switchCameraMode(Engine.instance.cameraEntity, { cameraMode: CameraMode.ShoulderCam }) + getMutableComponent(Engine.instance.cameraEntity, FollowCameraComponent).mode.set(FollowCameraMode.ShoulderCam) const selfAvatarEntity = AvatarComponent.getSelfAvatarEntity() AvatarControllerComponent.captureMovement(selfAvatarEntity, activePortalEntity) diff --git a/packages/engine/src/visualscript/nodes/profiles/engine/values/CustomNodes.ts b/packages/engine/src/visualscript/nodes/profiles/engine/values/CustomNodes.ts index 08cb4eedac..06cf08cfbe 100644 --- a/packages/engine/src/visualscript/nodes/profiles/engine/values/CustomNodes.ts +++ b/packages/engine/src/visualscript/nodes/profiles/engine/values/CustomNodes.ts @@ -416,7 +416,7 @@ export const setCameraZoom = makeFlowNodeDefinition({ triggered: ({ read, commit }) => { const entity = Engine.instance.cameraEntity const zoom = read('zoom') - setComponent(entity, FollowCameraComponent, { zoomLevel: zoom }) + setComponent(entity, FollowCameraComponent, { targetDistance: zoom }) commit('flow') } }) diff --git a/packages/engine/tsconfig.json b/packages/engine/tsconfig.json index 097f7ff881..c1db9208f9 100755 --- a/packages/engine/tsconfig.json +++ b/packages/engine/tsconfig.json @@ -35,4 +35,4 @@ "./**/*.ts", "./**/*.tsx" ] -} \ No newline at end of file +} diff --git a/packages/spatial/src/camera/CameraModule.ts b/packages/spatial/src/camera/CameraModule.ts index 3eccc6495b..12d04679de 100644 --- a/packages/spatial/src/camera/CameraModule.ts +++ b/packages/spatial/src/camera/CameraModule.ts @@ -26,5 +26,6 @@ Ethereal Engine. All Rights Reserved. import { CameraFadeBlackEffectSystem } from './systems/CameraFadeBlackEffectSystem' import { CameraOrbitSystem } from './systems/CameraOrbitSystem' import { CameraSystem } from './systems/CameraSystem' +import { FollowCameraInputSystem } from './systems/FollowCameraInputSystem' -export default { CameraFadeBlackEffectSystem, CameraSystem, CameraOrbitSystem } +export default { CameraFadeBlackEffectSystem, CameraSystem, CameraOrbitSystem, FollowCameraInputSystem } diff --git a/packages/spatial/src/camera/CameraSceneMetadata.ts b/packages/spatial/src/camera/CameraSceneMetadata.ts index 223f0cc586..fcb1936c43 100644 --- a/packages/spatial/src/camera/CameraSceneMetadata.ts +++ b/packages/spatial/src/camera/CameraSceneMetadata.ts @@ -25,9 +25,10 @@ Ethereal Engine. All Rights Reserved. import { defineState } from '@etherealengine/hyperflux' -import { CameraMode } from './types/CameraMode' +import { FollowCameraMode } from './types/FollowCameraMode' import { ProjectionType } from './types/ProjectionType' +// TODO: don't mix camera settings and follow camera settings export const CameraSettingsState = defineState({ name: 'CameraSettingsState', initial: { @@ -35,11 +36,11 @@ export const CameraSettingsState = defineState({ cameraNearClip: 0.1, cameraFarClip: 1000, projectionType: ProjectionType.Perspective, - minCameraDistance: 1, + minCameraDistance: 1.5, maxCameraDistance: 50, startCameraDistance: 3, - cameraMode: CameraMode.Dynamic, - cameraModeDefault: CameraMode.ThirdPerson, + cameraMode: FollowCameraMode.Dynamic, + cameraModeDefault: FollowCameraMode.ThirdPerson, minPhi: -70, maxPhi: 85 } diff --git a/packages/spatial/src/camera/CameraState.ts b/packages/spatial/src/camera/CameraState.ts index fe12c4d109..a7a84af22f 100644 --- a/packages/spatial/src/camera/CameraState.ts +++ b/packages/spatial/src/camera/CameraState.ts @@ -32,7 +32,7 @@ import { SpawnObjectActions } from '../transform/SpawnObjectActions' export const CameraSettings = defineState({ name: 'xre.engine.CameraSettings', initial: () => ({ - cameraRotationSpeed: 100 + cameraRotationSpeed: 200 }) }) diff --git a/packages/spatial/src/camera/components/FollowCameraComponent.ts b/packages/spatial/src/camera/components/FollowCameraComponent.ts index 131db17efa..94f0a20c38 100755 --- a/packages/spatial/src/camera/components/FollowCameraComponent.ts +++ b/packages/spatial/src/camera/components/FollowCameraComponent.ts @@ -30,12 +30,14 @@ import { defineQuery, ECSState, Engine, useEntityContext } from '@etherealengine import { defineComponent, getComponent, + getMutableComponent, getOptionalComponent, removeComponent, - setComponent + setComponent, + useComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { Entity, UndefinedEntity } from '@etherealengine/ecs/src/Entity' -import { getState } from '@etherealengine/hyperflux' +import { getState, matches, useImmediateEffect } from '@etherealengine/hyperflux' import { createConeOfVectors } from '../../common/functions/MathFunctions' import { smoothDamp, smootheLerpAlpha } from '../../common/functions/MathLerpFunctions' @@ -46,7 +48,8 @@ import { ObjectLayers } from '../../renderer/constants/ObjectLayers' import { ComputedTransformComponent } from '../../transform/components/ComputedTransformComponent' import { TransformComponent } from '../../transform/components/TransformComponent' import { CameraSettingsState } from '../CameraSceneMetadata' -import { CameraMode } from '../types/CameraMode' +import { setTargetCameraRotation } from '../functions/CameraFunctions' +import { FollowCameraMode, FollowCameraShoulderSide } from '../types/FollowCameraMode' import { TargetCameraRotationComponent } from './TargetCameraRotationComponent' export const coneDebugHelpers: ArrowHelper[] = [] @@ -71,7 +74,7 @@ export const FollowCameraComponent = defineComponent({ // } const cameraRays = [] as Vector3[] - const rayConeAngle = Math.PI / 6 + const rayConeAngle = Math.PI / 12 const camRayCastClock = new Clock() const camRayCastCache = { maxDistance: -1, @@ -94,47 +97,63 @@ export const FollowCameraComponent = defineComponent({ } return { - offset: new Vector3(), + firstPersonOffset: new Vector3(), + thirdPersonOffset: new Vector3(), + currentOffset: new Vector3(), + offsetSmoothness: 0.1, targetEntity: UndefinedEntity, currentTargetPosition: new Vector3(), targetPositionSmoothness: 0, - mode: CameraMode.ThirdPerson, + mode: FollowCameraMode.ThirdPerson, + allowedModes: [ + FollowCameraMode.ThirdPerson, + FollowCameraMode.FirstPerson, + FollowCameraMode.TopDown, + FollowCameraMode.ShoulderCam + ], distance: cameraSettings.startCameraDistance, - zoomLevel: 5, + targetDistance: 5, zoomVelocity: { value: 0 }, - minDistance: cameraSettings.minCameraDistance, - maxDistance: cameraSettings.maxCameraDistance, + thirdPersonMinDistance: cameraSettings.minCameraDistance, + thirdPersonMaxDistance: cameraSettings.maxCameraDistance, + effectiveMinDistance: cameraSettings.minCameraDistance, + effectiveMaxDistance: cameraSettings.maxCameraDistance, theta: 180, phi: 10, minPhi: cameraSettings.minPhi, maxPhi: cameraSettings.maxPhi, - shoulderSide: true, - locked: true, - raycastProps + shoulderSide: FollowCameraShoulderSide.Left, + raycastProps, + accumulatedZoomTriggerDebounceTime: -1, + lastZoomStartDistance: (cameraSettings.minCameraDistance + cameraSettings.minCameraDistance) / 2 } }, onSet: (entity, component, json) => { if (!json) return - if (typeof json.offset !== 'undefined') component.offset.set(json.offset) + if (typeof json.firstPersonOffset !== 'undefined') component.firstPersonOffset.set(json.firstPersonOffset) + if (typeof json.thirdPersonOffset !== 'undefined') component.thirdPersonOffset.set(json.thirdPersonOffset) if (typeof json.targetEntity !== 'undefined') component.targetEntity.set(json.targetEntity) - if (typeof json.mode !== 'undefined') component.mode.set(json.mode) + if (typeof json.mode === 'string') component.mode.set(json.mode) + if (matches.arrayOf(matches.string).test(json.allowedModes)) component.allowedModes.set(json.allowedModes) if (typeof json.distance !== 'undefined') component.distance.set(json.distance) - if (typeof json.zoomLevel !== 'undefined') component.zoomLevel.set(json.zoomLevel) + if (typeof json.targetDistance !== 'undefined') component.targetDistance.set(json.targetDistance) if (typeof json.zoomVelocity !== 'undefined') component.zoomVelocity.set(json.zoomVelocity) - if (typeof json.minDistance !== 'undefined') component.minDistance.set(json.minDistance) - if (typeof json.maxDistance !== 'undefined') component.maxDistance.set(json.maxDistance) + if (typeof json.thirdPersonMinDistance !== 'undefined') + component.thirdPersonMinDistance.set(json.thirdPersonMinDistance) + if (typeof json.thirdPersonMaxDistance !== 'undefined') + component.thirdPersonMaxDistance.set(json.thirdPersonMaxDistance) if (typeof json.theta !== 'undefined') component.theta.set(json.theta) if (typeof json.phi !== 'undefined') component.phi.set(json.phi) if (typeof json.minPhi !== 'undefined') component.minPhi.set(json.minPhi) if (typeof json.maxPhi !== 'undefined') component.maxPhi.set(json.maxPhi) if (typeof json.shoulderSide !== 'undefined') component.shoulderSide.set(json.shoulderSide) - if (typeof json.locked !== 'undefined') component.locked.set(json.locked) }, reactor: () => { const entity = useEntityContext() + const follow = useComponent(entity, FollowCameraComponent) useEffect(() => { const followCamera = getComponent(entity, FollowCameraComponent) @@ -148,6 +167,12 @@ export const FollowCameraComponent = defineComponent({ } }, []) + useImmediateEffect(() => { + if (follow.mode.value === FollowCameraMode.FirstPerson) { + follow.targetDistance.set(0) + } + }, [follow.mode]) + return null } }) @@ -160,61 +185,186 @@ const mx = new Matrix4() const tempVec1 = new Vector3() const raycaster = new Raycaster() +const MODE_SWITCH_DEBOUNCE = 0.03 + const computeCameraFollow = (cameraEntity: Entity, referenceEntity: Entity) => { - const followCamera = getComponent(cameraEntity, FollowCameraComponent) + const follow = getComponent(cameraEntity, FollowCameraComponent) + const followState = getMutableComponent(cameraEntity, FollowCameraComponent) const cameraTransform = getComponent(cameraEntity, TransformComponent) const targetTransform = getComponent(referenceEntity, TransformComponent) - if (!targetTransform || !followCamera) return + if (!targetTransform || !follow) return // Limit the pitch - followCamera.phi = Math.min(followCamera.maxPhi, Math.max(followCamera.minPhi, followCamera.phi)) + follow.phi = Math.min(follow.maxPhi, Math.max(follow.minPhi, follow.phi)) - let maxDistance = followCamera.zoomLevel let isInsideWall = false + const offsetAlpha = smootheLerpAlpha(follow.offsetSmoothness, getState(ECSState).deltaSeconds) + const targetOffset = + follow.mode === FollowCameraMode.FirstPerson ? follow.firstPersonOffset : follow.thirdPersonOffset + follow.currentOffset.lerp(targetOffset, offsetAlpha) + targetPosition - .copy(followCamera.offset) + .copy(follow.currentOffset) .applyQuaternion(TransformComponent.getWorldRotation(referenceEntity, targetTransform.rotation)) .add(TransformComponent.getWorldPosition(referenceEntity, new Vector3())) - const alpha = smootheLerpAlpha(followCamera.targetPositionSmoothness, getState(ECSState).deltaSeconds) - followCamera.currentTargetPosition.lerp(targetPosition, alpha) + const targetPositionAlpha = smootheLerpAlpha(follow.targetPositionSmoothness, getState(ECSState).deltaSeconds) + follow.currentTargetPosition.lerp(targetPosition, targetPositionAlpha) // Run only if not in first person mode - if (followCamera.raycastProps.enabled && followCamera.zoomLevel >= followCamera.minDistance) { - const distanceResults = getMaxCamDistance(cameraEntity, followCamera.currentTargetPosition) - maxDistance = distanceResults.maxDistance + let obstacleDistance = Infinity + if (follow.raycastProps.enabled && follow.mode !== FollowCameraMode.FirstPerson) { + const distanceResults = getMaxCamDistance(cameraEntity, follow.currentTargetPosition) + obstacleDistance = distanceResults.maxDistance isInsideWall = distanceResults.targetHit } - const newZoomDistance = Math.min(followCamera.zoomLevel, maxDistance) + if (follow.mode === FollowCameraMode.FirstPerson) { + follow.effectiveMinDistance = follow.effectiveMaxDistance = 0 + } else if (follow.mode === FollowCameraMode.ThirdPerson || follow.mode === FollowCameraMode.ShoulderCam) { + follow.effectiveMaxDistance = Math.min(obstacleDistance * 0.8, follow.thirdPersonMaxDistance) + follow.effectiveMinDistance = Math.min(follow.thirdPersonMinDistance, follow.effectiveMaxDistance) + } else if (follow.mode === FollowCameraMode.TopDown) { + follow.effectiveMinDistance = follow.effectiveMaxDistance = Math.min( + obstacleDistance * 0.9, + follow.thirdPersonMaxDistance + ) + } + + let newZoomDistance = Math.max( + Math.min(follow.targetDistance, follow.effectiveMaxDistance), + follow.effectiveMinDistance + ) + + const constrainTargetDistance = follow.accumulatedZoomTriggerDebounceTime === -1 + + if (constrainTargetDistance) { + follow.targetDistance = newZoomDistance + } + + const triggerZoomShift = follow.accumulatedZoomTriggerDebounceTime > MODE_SWITCH_DEBOUNCE + + const minSpringFactor = + Math.min( + Math.sqrt(Math.abs(follow.targetDistance - follow.effectiveMinDistance)) * + Math.sign(follow.targetDistance - follow.effectiveMinDistance), + 0 + ) * 0.5 + + const maxSpringFactor = + Math.max( + Math.sqrt(Math.abs(follow.targetDistance - follow.effectiveMaxDistance)) * + Math.sign(follow.targetDistance - follow.effectiveMaxDistance), + 0 + ) * 0.5 + + if (follow.mode === FollowCameraMode.FirstPerson) { + newZoomDistance = Math.sqrt(follow.targetDistance) * 0.5 + // Move from first person mode to third person mode + if (triggerZoomShift) { + follow.accumulatedZoomTriggerDebounceTime = -1 + if ( + follow.allowedModes.includes(FollowCameraMode.ThirdPerson) && + newZoomDistance > 0.1 * follow.thirdPersonMinDistance + ) { + // setup third person mode + setTargetCameraRotation(cameraEntity, 0, follow.theta) + followState.mode.set(FollowCameraMode.ThirdPerson) + follow.targetDistance = newZoomDistance = follow.thirdPersonMinDistance + } else { + // reset first person mode + follow.targetDistance = newZoomDistance = 0 + } + } + } else if (follow.mode === FollowCameraMode.ThirdPerson) { + newZoomDistance = newZoomDistance + minSpringFactor + maxSpringFactor + if (triggerZoomShift) { + follow.accumulatedZoomTriggerDebounceTime = -1 + if ( + // Move from third person mode to first person mode + follow.allowedModes.includes(FollowCameraMode.FirstPerson) && + follow.targetDistance < follow.effectiveMinDistance - follow.effectiveMaxDistance * 0.05 && + Math.abs(follow.lastZoomStartDistance - follow.effectiveMinDistance) < follow.effectiveMaxDistance * 0.05 + ) { + setTargetCameraRotation(cameraEntity, 0, follow.theta) + followState.mode.set(FollowCameraMode.FirstPerson) + follow.targetDistance = newZoomDistance = 0 + } else if ( + // Move from third person mode to top down mode + follow.allowedModes.includes(FollowCameraMode.TopDown) && + follow.targetDistance > follow.effectiveMaxDistance + follow.effectiveMaxDistance * 0.02 && + Math.abs(follow.lastZoomStartDistance - follow.effectiveMaxDistance) < follow.effectiveMaxDistance * 0.02 + ) { + setTargetCameraRotation(cameraEntity, 85, follow.theta) + followState.mode.set(FollowCameraMode.TopDown) + follow.targetDistance = newZoomDistance = follow.effectiveMaxDistance + } else { + follow.targetDistance = newZoomDistance = Math.max( + Math.min(follow.targetDistance, follow.effectiveMaxDistance), + follow.effectiveMinDistance + ) + } + } + } else if (follow.mode === FollowCameraMode.TopDown) { + newZoomDistance += minSpringFactor + maxSpringFactor * 0.1 + // Move from top down mode to third person mode + if (triggerZoomShift) { + follow.accumulatedZoomTriggerDebounceTime = -1 + if ( + follow.allowedModes.includes(FollowCameraMode.ThirdPerson) && + newZoomDistance < follow.effectiveMaxDistance * 0.98 && + Math.abs(follow.lastZoomStartDistance - follow.effectiveMaxDistance) < 0.05 * follow.effectiveMaxDistance + ) { + setTargetCameraRotation(cameraEntity, 0, follow.theta) + followState.mode.set(FollowCameraMode.ThirdPerson) + } + follow.targetDistance = newZoomDistance = follow.effectiveMaxDistance + } + } + + // // Move from third person mode to top down mode + // if (allowModeShift && follow.mode === FollowCameraMode.ThirdPerson && + // follow.allowedModes.includes(FollowCameraMode.TopDown) && + // follow.targetDistance >= 1.1 * follow.thirdPersonMaxDistance) { + // setTargetCameraRotation(cameraEntity, 90, follow.theta) + // followState.mode.set(FollowCameraMode.TopDown) + // } + + // // Rotate camera to the top but let the player rotate if he/she desires + // if (Math.abs(follow.thirdPersonMaxDistance - nextTargetDistance) <= 1.0 && scrollDelta > 0 && follow) { + // setTargetCameraRotation(cameraEntity, 85, follow.theta) + // } + + // // Rotate from top + // if (Math.abs(follow.thirdPersonMaxDistance - follow.targetDistance) <= 1.0 && scrollDelta < 0 && follow.phi >= 80) { + // setTargetCameraRotation(cameraEntity, 45, follow.theta) + // } + + // if (Math.abs(follow.targetDistance - nextTargetDistance) > epsilon) { + // follow.targetDistance = nextTargetDistance + // } // Zoom smoothing const smoothingSpeed = isInsideWall ? 0.1 : 0.3 const deltaSeconds = getState(ECSState).deltaSeconds - followCamera.distance = smoothDamp( - followCamera.distance, - newZoomDistance, - followCamera.zoomVelocity, - smoothingSpeed, - deltaSeconds - ) + follow.distance = smoothDamp(follow.distance, newZoomDistance, follow.zoomVelocity, smoothingSpeed, deltaSeconds) - const theta = followCamera.theta + const theta = follow.theta const thetaRad = MathUtils.degToRad(theta) - const phiRad = MathUtils.degToRad(followCamera.phi) + const phiRad = MathUtils.degToRad(follow.phi) + + direction.set(Math.sin(thetaRad) * Math.cos(phiRad), Math.sin(phiRad), Math.cos(thetaRad) * Math.cos(phiRad)) cameraTransform.position.set( - followCamera.currentTargetPosition.x + followCamera.distance * Math.sin(thetaRad) * Math.cos(phiRad), - followCamera.currentTargetPosition.y + followCamera.distance * Math.sin(phiRad), - followCamera.currentTargetPosition.z + followCamera.distance * Math.cos(thetaRad) * Math.cos(phiRad) + follow.currentTargetPosition.x + follow.distance * direction.x, + follow.currentTargetPosition.y + follow.distance * direction.y, + follow.currentTargetPosition.z + follow.distance * direction.z ) - direction.copy(cameraTransform.position).sub(followCamera.currentTargetPosition).normalize() mx.lookAt(direction, empty, upVector) - cameraTransform.rotation.setFromRotationMatrix(mx) updateCameraTargetRotation(cameraEntity) @@ -263,13 +413,13 @@ const getMaxCamDistance = (cameraEntity: Entity, target: Vector3) => { createConeOfVectors(targetToCamVec, cameraRays, rayConeAngle) - let maxDistance = Math.min(followCamera.maxDistance, raycastProps.rayLength) + let maxDistance = Math.min(followCamera.thirdPersonMaxDistance, raycastProps.rayLength) // Check hit with mid ray raycaster.layers.set(ObjectLayers.Camera) // Ignore avatars // @ts-ignore - todo figure out why typescript freaks out at this raycaster.firstHitOnly = true // three-mesh-bvh setting - raycaster.far = followCamera.maxDistance + raycaster.far = followCamera.thirdPersonMaxDistance raycaster.set(target, targetToCamVec.normalize()) const hits = raycaster.intersectObjects(sceneObjects, false) diff --git a/packages/spatial/src/camera/functions/CameraFunctions.ts b/packages/spatial/src/camera/functions/CameraFunctions.ts index 82abeb4a13..7c389a9d17 100644 --- a/packages/spatial/src/camera/functions/CameraFunctions.ts +++ b/packages/spatial/src/camera/functions/CameraFunctions.ts @@ -23,12 +23,9 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { clamp } from 'lodash' - import { ComponentType, getOptionalComponent, setComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { Entity } from '@etherealengine/ecs/src/Entity' -import { FollowCameraComponent } from '../components/FollowCameraComponent' import { TargetCameraRotationComponent } from '../components/TargetCameraRotationComponent' export const setTargetCameraRotation = (entity: Entity, phi: number, theta: number, time = 0.3) => { @@ -49,53 +46,3 @@ export const setTargetCameraRotation = (entity: Entity, phi: number, theta: numb cameraRotationTransition.time = time } } - -/** - * Change camera distance. - * @param cameraEntity Entity holding camera and input component. - */ -export const handleCameraZoom = (cameraEntity: Entity, scrollDelta: number): void => { - if (scrollDelta === 0) { - return - } - - const followComponent = getOptionalComponent(cameraEntity, FollowCameraComponent) as - | ComponentType - | undefined - - if (!followComponent) { - return - } - - const epsilon = 0.001 - const nextZoomLevel = clamp(followComponent.zoomLevel + scrollDelta, epsilon, followComponent.maxDistance) - - // Move out of first person mode - if (followComponent.zoomLevel <= epsilon && scrollDelta > 0) { - followComponent.zoomLevel = followComponent.minDistance - return - } - - // Move to first person mode - if (nextZoomLevel < followComponent.minDistance) { - followComponent.zoomLevel = epsilon - setTargetCameraRotation(cameraEntity, 0, followComponent.theta) - return - } - - // Rotate camera to the top but let the player rotate if he/she desires - if (Math.abs(followComponent.maxDistance - nextZoomLevel) <= 1.0 && scrollDelta > 0) { - setTargetCameraRotation(cameraEntity, 85, followComponent.theta) - } - - // Rotate from top - if ( - Math.abs(followComponent.maxDistance - followComponent.zoomLevel) <= 1.0 && - scrollDelta < 0 && - followComponent.phi >= 80 - ) { - setTargetCameraRotation(cameraEntity, 45, followComponent.theta) - } - - followComponent.zoomLevel = nextZoomLevel -} diff --git a/packages/spatial/src/camera/functions/switchCameraMode.ts b/packages/spatial/src/camera/functions/switchCameraMode.ts deleted file mode 100644 index 324623ee78..0000000000 --- a/packages/spatial/src/camera/functions/switchCameraMode.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* -CPAL-1.0 License - -The contents of this file are subject to the Common Public Attribution License -Version 1.0. (the "License"); you may not use this file except in compliance -with the License. You may obtain a copy of the License at -https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. -The License is based on the Mozilla Public License Version 1.1, but Sections 14 -and 15 have been added to cover use of software over a computer network and -provide for limited attribution for the Original Developer. In addition, -Exhibit A has been modified to be consistent with Exhibit B. - -Software distributed under the License is distributed on an "AS IS" basis, -WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the -specific language governing rights and limitations under the License. - -The Original Code is Ethereal Engine. - -The Original Developer is the Initial Developer. The Initial Developer of the -Original Code is the Ethereal Engine team. - -All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 -Ethereal Engine. All Rights Reserved. -*/ - -import { getOptionalComponent } from '@etherealengine/ecs/src/ComponentFunctions' -import { Entity } from '@etherealengine/ecs/src/Entity' - -import { FollowCameraComponent } from '../components/FollowCameraComponent' -import { CameraMode } from '../types/CameraMode' - -type SwitchCameraModeProps = { - cameraMode: CameraMode - pointerLock?: boolean -} - -let changeTimeout: any = undefined -export const switchCameraMode = ( - cameraEntity: Entity, - args: SwitchCameraModeProps = { pointerLock: false, cameraMode: CameraMode.ThirdPerson }, - force = false -): void => { - if (!force) { - if (changeTimeout !== undefined) return - changeTimeout = setTimeout(() => { - clearTimeout(changeTimeout) - changeTimeout = undefined - }, 250) - } - - const cameraFollow = getOptionalComponent(cameraEntity, FollowCameraComponent) - if (!cameraFollow) return - cameraFollow.mode = args.cameraMode - - if (cameraFollow.mode === CameraMode.FirstPerson) { - cameraFollow.phi = 0 - cameraFollow.locked = true - } -} diff --git a/packages/spatial/src/camera/systems/CameraOrbitSystem.tsx b/packages/spatial/src/camera/systems/CameraOrbitSystem.tsx index 72a26977ef..04471145f2 100644 --- a/packages/spatial/src/camera/systems/CameraOrbitSystem.tsx +++ b/packages/spatial/src/camera/systems/CameraOrbitSystem.tsx @@ -81,7 +81,7 @@ const execute = () => { * assign active orbit camera based on which input source registers input */ for (const cameraEid of orbitCameraQuery()) { - const inputPointerEntity = InputPointerComponent.getPointerForCanvas(cameraEid) + const inputPointerEntity = InputPointerComponent.getPointersForCamera(cameraEid)[0] const cameraOrbit = getMutableComponent(cameraEid, CameraOrbitComponent) diff --git a/packages/spatial/src/camera/systems/CameraSystem.tsx b/packages/spatial/src/camera/systems/CameraSystem.tsx index 3c507c009c..fa84058594 100755 --- a/packages/spatial/src/camera/systems/CameraSystem.tsx +++ b/packages/spatial/src/camera/systems/CameraSystem.tsx @@ -33,6 +33,7 @@ import { Engine, EntityUUID, getComponent, + getOptionalMutableComponent, setComponent, UUIDComponent } from '@etherealengine/ecs' @@ -45,7 +46,7 @@ import { TransformComponent } from '../../transform/components/TransformComponen import { CameraSettingsState } from '../CameraSceneMetadata' import { CameraActions } from '../CameraState' import { CameraComponent } from '../components/CameraComponent' -import { switchCameraMode } from '../functions/switchCameraMode' +import { FollowCameraComponent } from '../components/FollowCameraComponent' export const CameraEntityState = defineState({ name: 'CameraEntityState', @@ -92,12 +93,23 @@ function CameraReactor() { if (!cameraSettings?.cameraNearClip) return const camera = getComponent(Engine.instance.cameraEntity, CameraComponent) as PerspectiveCamera if (camera?.isPerspectiveCamera) { + camera.fov = cameraSettings.fov.value camera.near = cameraSettings.cameraNearClip.value camera.far = cameraSettings.cameraFarClip.value camera.updateProjectionMatrix() } - switchCameraMode(Engine.instance.cameraEntity, cameraSettings.value) - }, [cameraSettings.cameraNearClip, cameraSettings.cameraFarClip]) + }, [cameraSettings.fov, cameraSettings.cameraNearClip, cameraSettings.cameraFarClip]) + + // TODO: this is messy and not properly reactive; we need a better way to handle camera settings + useEffect(() => { + if (!cameraSettings?.fov) return + const follow = getOptionalMutableComponent(Engine.instance.cameraEntity, FollowCameraComponent) + if (follow) { + follow.thirdPersonMinDistance.set(cameraSettings.minCameraDistance.value) + follow.thirdPersonMaxDistance.set(cameraSettings.maxCameraDistance.value) + follow.distance.set(cameraSettings.startCameraDistance.value) + } + }, [cameraSettings]) return null } diff --git a/packages/spatial/src/camera/systems/FollowCameraInputSystem.ts b/packages/spatial/src/camera/systems/FollowCameraInputSystem.ts new file mode 100644 index 0000000000..3b36b56c5e --- /dev/null +++ b/packages/spatial/src/camera/systems/FollowCameraInputSystem.ts @@ -0,0 +1,170 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +import { Vector2 } from 'three' + +import { Entity } from '@etherealengine/ecs' +import { getComponent, getMutableComponent, getOptionalComponent } from '@etherealengine/ecs/src/ComponentFunctions' +import { ECSState } from '@etherealengine/ecs/src/ECSState' +import { defineQuery } from '@etherealengine/ecs/src/QueryFunctions' +import { defineSystem } from '@etherealengine/ecs/src/SystemFunctions' +import { InputSystemGroup } from '@etherealengine/ecs/src/SystemGroups' +import { getState } from '@etherealengine/hyperflux' +import { CameraSettings } from '@etherealengine/spatial/src/camera/CameraState' +import { FollowCameraComponent } from '@etherealengine/spatial/src/camera/components/FollowCameraComponent' +import { TargetCameraRotationComponent } from '@etherealengine/spatial/src/camera/components/TargetCameraRotationComponent' +import { setTargetCameraRotation } from '@etherealengine/spatial/src/camera/functions/CameraFunctions' +import { FollowCameraMode } from '@etherealengine/spatial/src/camera/types/FollowCameraMode' +import { DefaultAxisAlias, InputComponent } from '@etherealengine/spatial/src/input/components/InputComponent' +import { InputPointerComponent } from '@etherealengine/spatial/src/input/components/InputPointerComponent' +import { InputSourceComponent } from '@etherealengine/spatial/src/input/components/InputSourceComponent' +import { getThumbstickOrThumbpadAxes } from '@etherealengine/spatial/src/input/functions/getThumbstickOrThumbpadAxes' +import { AxisValueMap } from '@etherealengine/spatial/src/input/state/ButtonState' +import { InputState } from '@etherealengine/spatial/src/input/state/InputState' +import { XRState } from '@etherealengine/spatial/src/xr/XRState' +import { RendererComponent } from '../../renderer/WebGLRendererSystem' + +// const throttleHandleCameraZoom = throttle(handleFollowCameraZoom, 30, { leading: true, trailing: false }) + +const pointerPositionDelta = new Vector2() +const rendererQuery = defineQuery([RendererComponent]) +const epsilon = 0.001 + +const followCameraModeCycle = [ + FollowCameraMode.FirstPerson, + FollowCameraMode.ShoulderCam, + FollowCameraMode.ThirdPerson, + FollowCameraMode.TopDown +] as FollowCameraMode[] + +const onFollowCameraModeCycle = (cameraEntity: Entity) => { + const follow = getMutableComponent(cameraEntity, FollowCameraComponent) + const mode = follow.mode.value + const currentModeIdx = followCameraModeCycle.includes(mode) ? followCameraModeCycle.indexOf(mode) : 0 + const nextModeIdx = (currentModeIdx + 1) % followCameraModeCycle.length + const nextMode = followCameraModeCycle[nextModeIdx] + follow.mode.set(nextMode) +} + +const onFollowCameraFirstPerson = (cameraEntity: Entity) => { + const followComponent = getMutableComponent(cameraEntity, FollowCameraComponent) + followComponent.mode.set(FollowCameraMode.FirstPerson) +} + +const onFollowCameraShoulderCam = (cameraEntity: Entity) => { + const follow = getMutableComponent(cameraEntity, FollowCameraComponent) + follow.mode.set(FollowCameraMode.ShoulderCam) +} + +/** + * Change camera distance. + * @param cameraEntity Entity holding camera and input component. + */ +export const handleFollowCameraScroll = ( + cameraEntity: Entity, + axes: AxisValueMap, + deltaTime: number +): void => { + const follow = getComponent(cameraEntity, FollowCameraComponent) + + const zoomDelta = axes.FollowCameraZoomScroll ?? 0 + const shoulderDelta = axes.FollowCameraShoulderCamScroll ?? 0 + + follow.targetDistance = Math.max(follow.targetDistance + zoomDelta, 0) + + // Math.min( + // Math.max(follow.targetDistance + zoomDelta, follow.effectiveMinDistance * 0.8), + // follow.effectiveMaxDistance * 1.2 + // ) + + const outsideMinMaxRange = + follow.targetDistance < follow.effectiveMinDistance || follow.targetDistance > follow.effectiveMaxDistance + + if (zoomDelta === 0 && shoulderDelta === 0 && follow.accumulatedZoomTriggerDebounceTime >= 0 && outsideMinMaxRange) { + follow.accumulatedZoomTriggerDebounceTime += deltaTime + } else if (Math.abs(zoomDelta) > 0 || Math.abs(shoulderDelta) > 0) { + if (follow.accumulatedZoomTriggerDebounceTime === -1) { + follow.lastZoomStartDistance = follow.distance + } + follow.accumulatedZoomTriggerDebounceTime = 0 + } +} + +const execute = () => { + if (getState(XRState).xrFrame) return + + const deltaSeconds = getState(ECSState).deltaSeconds + const cameraSettings = getState(CameraSettings) + + for (const cameraEntity of rendererQuery()) { + const buttons = InputComponent.getMergedButtons(cameraEntity) + const axes = InputComponent.getMergedAxes(cameraEntity) + + const inputPointerEntities = InputPointerComponent.getPointersForCamera(cameraEntity) + const inputState = getState(InputState) + + const follow = getOptionalComponent(cameraEntity, FollowCameraComponent) + if (!follow) continue + + let { theta, phi } = getOptionalComponent(cameraEntity, TargetCameraRotationComponent) ?? follow + let time = 0.3 + + if (buttons?.PrimaryClick?.pressed && buttons?.PrimaryClick?.dragging) { + InputState.setCapturingEntity(cameraEntity) + } + if (buttons?.FollowCameraModeCycle?.down) onFollowCameraModeCycle(cameraEntity) + if (buttons?.FollowCameraFirstPerson?.down) onFollowCameraFirstPerson(cameraEntity) + if (buttons?.FollowCameraShoulderCam?.down) onFollowCameraShoulderCam(cameraEntity) + + const keyDelta = (buttons?.ArrowLeft ? 1 : 0) + (buttons?.ArrowRight ? -1 : 0) + theta += 100 * deltaSeconds * keyDelta + + for (const inputPointerEid of inputPointerEntities) { + const inputSource = getComponent(inputPointerEid, InputSourceComponent) + const [x, y] = getThumbstickOrThumbpadAxes(inputSource.source, inputState.preferredHand) + theta -= x * 2 + phi += y * 2 + const pointerDragging = inputSource.buttons?.PrimaryClick?.dragging + if (pointerDragging) { + const inputPointer = getComponent(inputPointerEid, InputPointerComponent) + pointerPositionDelta.copy(inputPointer.movement) + phi -= pointerPositionDelta.y * cameraSettings.cameraRotationSpeed + theta -= pointerPositionDelta.x * cameraSettings.cameraRotationSpeed + time = 0.1 + } + } + + if (getState(InputState).capturingEntity === cameraEntity) { + setTargetCameraRotation(cameraEntity, phi, theta, time) + } + handleFollowCameraScroll(cameraEntity, axes, deltaSeconds) + } +} + +export const FollowCameraInputSystem = defineSystem({ + uuid: 'ee.engine.FollowCameraInputSystem', + insert: { after: InputSystemGroup }, + execute +}) diff --git a/packages/spatial/src/camera/types/CameraMode.ts b/packages/spatial/src/camera/types/FollowCameraMode.ts similarity index 80% rename from packages/spatial/src/camera/types/CameraMode.ts rename to packages/spatial/src/camera/types/FollowCameraMode.ts index 86f2a19323..6d04ea1477 100755 --- a/packages/spatial/src/camera/types/CameraMode.ts +++ b/packages/spatial/src/camera/types/FollowCameraMode.ts @@ -24,11 +24,16 @@ Ethereal Engine. All Rights Reserved. */ /** Camera Modes. */ -export enum CameraMode { - FirstPerson, - ShoulderCam, - ThirdPerson, - TopDown, - Strategic, - Dynamic +export enum FollowCameraMode { + FirstPerson = 'FirstPerson', + ShoulderCam = 'ShoulderCam', + ThirdPerson = 'ThirdPerson', + TopDown = 'TopDown', + Strategic = 'Strategic', + Dynamic = 'Dynamic' +} + +export enum FollowCameraShoulderSide { + Left = 'Left', + Right = 'Right' } diff --git a/packages/spatial/src/common/functions/FeathersHooks.tsx b/packages/spatial/src/common/functions/FeathersHooks.tsx index 7927828263..db3e7e64dc 100644 --- a/packages/spatial/src/common/functions/FeathersHooks.tsx +++ b/packages/spatial/src/common/functions/FeathersHooks.tsx @@ -76,6 +76,7 @@ export const FeathersState = defineState({ QueryHash, { fetch: () => void + query: any response: unknown status: 'pending' | 'success' | 'error' error: string @@ -118,11 +119,13 @@ export const useService = ( const service = Engine.instance.api.service(serviceName) const state = useMutableState(FeathersState) - const queryId = `${method.substring(0, 1)}:${hashObject({ + const queryParams = { serviceName, method, args - })}` as QueryHash + } + + const queryId = `${method.substring(0, 1)}:${hashObject(queryParams)}` as QueryHash const fetch = () => { if (method === 'get' && !args) { @@ -159,6 +162,7 @@ export const useService = ( state[serviceName].merge({ [queryId]: { fetch, + query: queryParams, response: null, status: 'pending', error: '' diff --git a/packages/spatial/src/input/components/InputComponent.ts b/packages/spatial/src/input/components/InputComponent.ts index ce3d2ffdf0..ee0209b1c8 100644 --- a/packages/spatial/src/input/components/InputComponent.ts +++ b/packages/spatial/src/input/components/InputComponent.ts @@ -46,16 +46,39 @@ import { EngineState } from '../../EngineState' import { HighlightComponent } from '../../renderer/components/HighlightComponent' import { getAncestorWithComponent, isAncestor } from '../../transform/components/EntityTree' -import { ButtonState, ButtonStateMap, KeyboardButton, MouseButton, XRStandardGamepadButton } from '../state/ButtonState' +import { + AnyAxis, + AnyButton, + AxisMapping, + AxisValueMap, + ButtonStateMap, + KeyboardButton, + MouseButton, + MouseScroll, + XRStandardGamepadAxes, + XRStandardGamepadButton +} from '../state/ButtonState' import { InputState } from '../state/InputState' import { InputSinkComponent } from './InputSinkComponent' import { InputSourceComponent } from './InputSourceComponent' export type InputAlias = Record -export const DefaultInputAlias = { - Interact: [MouseButton.PrimaryClick, XRStandardGamepadButton.Trigger, KeyboardButton.KeyE] -} +export const DefaultButtonAlias = { + Interact: [MouseButton.PrimaryClick, XRStandardGamepadButton.XRStandardGamepadTrigger, KeyboardButton.KeyE], + FollowCameraModeCycle: [KeyboardButton.KeyV], + FollowCameraFirstPerson: [KeyboardButton.KeyF], + FollowCameraShoulderCam: [KeyboardButton.KeyC] +} satisfies Record> + +export const DefaultAxisAlias = { + FollowCameraZoomScroll: [ + MouseScroll.VerticalScroll, + XRStandardGamepadAxes.XRStandardGamepadThumbstickY, + XRStandardGamepadAxes.XRStandardGamepadTouchpadY + ], + FollowCameraShoulderCamScroll: [MouseScroll.HorizontalScroll] +} satisfies Record> export const InputComponent = defineComponent({ name: 'InputComponent', @@ -76,7 +99,7 @@ export const InputComponent = defineComponent({ onSet(entity, component, json) { if (!json) return - if (typeof json.inputSinks === 'object') component.inputSinks.set(json.inputSinks) + if (Array.isArray(json.inputSinks)) component.inputSinks.set(json.inputSinks) if (typeof json.highlight === 'boolean') component.highlight.set(json.highlight) if (json.activationDistance) component.activationDistance.set(json.activationDistance) if (typeof json.grow === 'boolean') component.grow.set(json.grow) @@ -124,32 +147,32 @@ export const InputComponent = defineComponent({ }, []) }, - getMergedButtons( + getMergedButtons( entityContext: Entity, - inputAlias: AliasType = DefaultInputAlias as unknown as AliasType + inputAlias: AliasType = DefaultButtonAlias as unknown as AliasType ) { const inputSourceEntities = InputComponent.getInputSourceEntities(entityContext) return InputComponent.getMergedButtonsForInputSources(inputSourceEntities, inputAlias) }, - getMergedAxes( + getMergedAxes( entityContext: Entity, - inputAlias: AliasType = DefaultInputAlias as unknown as AliasType + inputAlias: AliasType = DefaultAxisAlias as unknown as AliasType ) { const inputSourceEntities = InputComponent.getInputSourceEntities(entityContext) return InputComponent.getMergedAxesForInputSources(inputSourceEntities, inputAlias) }, - getMergedButtonsForInputSources( + getMergedButtonsForInputSources( inputSourceEntities: Entity[], - inputAlias: AliasType = DefaultInputAlias as unknown as AliasType + inputAlias: AliasType = DefaultButtonAlias as unknown as AliasType ) { const buttons = Object.assign( - {} as ButtonStateMap, + {}, ...inputSourceEntities.map((eid) => { return getComponent(eid, InputSourceComponent).buttons }) - ) as ButtonStateMap & Partial> + ) as ButtonStateMap for (const key of Object.keys(inputAlias)) { const k = key as keyof AliasType @@ -159,34 +182,36 @@ export const InputComponent = defineComponent({ return buttons }, - getMergedAxesForInputSources( + getMergedAxesForInputSources( inputSourceEntities: Entity[], - inputAlias: AliasType = DefaultInputAlias as unknown as AliasType + inputAlias: AliasType = DefaultAxisAlias as unknown as AliasType ) { const axes = { 0: 0, 1: 0, 2: 0, 3: 0 - } as Record + } as any for (const eid of inputSourceEntities) { const inputSource = getComponent(eid, InputSourceComponent) if (inputSource.source.gamepad?.axes) { + const mapping = AxisMapping[inputSource.source.gamepad.mapping] for (let i = 0; i < 4; i++) { const newAxis = inputSource.source.gamepad.axes[i] ?? 0 - axes[i] = getLargestMagnitudeNumber(axes[i], newAxis) + axes[i] = getLargestMagnitudeNumber(axes[i] ?? 0, newAxis) + axes[mapping[i]] = axes[i] } } } for (const key of Object.keys(inputAlias)) { - axes[key] = inputAlias[key].reduce((prev, alias) => { - return getLargestMagnitudeNumber(prev, axes[alias]) + axes[key as any] = inputAlias[key].reduce((prev, alias) => { + return getLargestMagnitudeNumber(prev, axes[alias] ?? 0) }, 0) } - return axes + return axes as AxisValueMap }, useHasFocus() { diff --git a/packages/spatial/src/input/components/InputPointerComponent.ts b/packages/spatial/src/input/components/InputPointerComponent.ts index ef03416ef2..ac4f528439 100644 --- a/packages/spatial/src/input/components/InputPointerComponent.ts +++ b/packages/spatial/src/input/components/InputPointerComponent.ts @@ -26,6 +26,16 @@ Ethereal Engine. All Rights Reserved. import { Vector2 } from 'three' import { defineComponent, defineQuery, Entity, getComponent, UndefinedEntity } from '@etherealengine/ecs' +import { defineState, getState } from '@etherealengine/hyperflux' + +export const InputPointerState = defineState({ + name: 'InputPointerState', + initial() { + return { + pointers: new Map() + } + } +}) export const InputPointerComponent = defineComponent({ name: 'InputPointerComponent', @@ -36,17 +46,29 @@ export const InputPointerComponent = defineComponent({ position: new Vector2(), lastPosition: new Vector2(), movement: new Vector2(), - canvasEntity: UndefinedEntity + cameraEntity: UndefinedEntity } }, - onSet(entity, component, args: { pointerId: number; canvasEntity: Entity }) { + onSet(entity, component, args: { pointerId: number; cameraEntity: Entity }) { component.pointerId.set(args.pointerId) - component.canvasEntity.set(args.canvasEntity) + component.cameraEntity.set(args.cameraEntity) + const pointerHash = `canvas-${args.cameraEntity}.pointer-${args.pointerId}` + getState(InputPointerState).pointers.set(pointerHash, entity) + }, + + onRemove(entity, component) { + const pointerHash = `canvas-${component.cameraEntity}.pointer-${component.pointerId}` + getState(InputPointerState).pointers.delete(pointerHash) + }, + + getPointersForCamera(cameraEntity: Entity) { + return pointerQuery().filter((entity) => getComponent(entity, InputPointerComponent).cameraEntity === cameraEntity) }, - getPointerForCanvas(canvasEntity: Entity) { - return pointerQuery().find((entity) => getComponent(entity, InputPointerComponent).canvasEntity === canvasEntity) + getPointerByID(cameraEntity: Entity, pointerId: number) { + const pointerHash = `canvas-${cameraEntity}.pointer-${pointerId}` + return getState(InputPointerState).pointers.get(pointerHash) ?? UndefinedEntity } }) diff --git a/packages/spatial/src/input/components/InputSourceComponent.tsx b/packages/spatial/src/input/components/InputSourceComponent.tsx index 2e229f3945..5df66226c7 100644 --- a/packages/spatial/src/input/components/InputSourceComponent.tsx +++ b/packages/spatial/src/input/components/InputSourceComponent.tsx @@ -34,6 +34,7 @@ import { XRHandComponent, XRSpaceComponent } from '../../xr/XRComponents' import { ReferenceSpace, XRState } from '../../xr/XRState' import { ButtonStateMap } from '../state/ButtonState' import { InputState } from '../state/InputState' +import { DefaultButtonAlias } from './InputComponent' export const InputSourceComponent = defineComponent({ name: 'InputSourceComponent', @@ -41,7 +42,7 @@ export const InputSourceComponent = defineComponent({ onInit: () => { return { source: {} as XRInputSource, - buttons: {} as Readonly, + buttons: {} as Readonly>, raycaster: new Raycaster(), intersections: [] as Array<{ entity: Entity @@ -67,7 +68,7 @@ export const InputSourceComponent = defineComponent({ hapticActuators: [], id: 'emulated-gamepad-' + entity, index: 0, - mapping: 'standard', + mapping: '', timestamp: performance.now(), vibrationActuator: null } as Gamepad), diff --git a/packages/spatial/src/xrui/XRUIState.ts b/packages/spatial/src/input/functions/getThumbstickOrThumbpadAxes.ts similarity index 63% rename from packages/spatial/src/xrui/XRUIState.ts rename to packages/spatial/src/input/functions/getThumbstickOrThumbpadAxes.ts index 9eff38fe78..34f3a72aad 100644 --- a/packages/spatial/src/xrui/XRUIState.ts +++ b/packages/spatial/src/input/functions/getThumbstickOrThumbpadAxes.ts @@ -23,13 +23,15 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { Object3D, Ray } from 'three' - -import { defineState } from '@etherealengine/hyperflux' - -export const XRUIState = defineState({ - name: 'XRUIState', - initial: () => ({ - interactionRays: [] as Array - }) -}) +/** + * On 'xr-standard' mapping, get thumbstick input [2,3], fallback to thumbpad input [0,1] + * On 'standard' mapping, get thumbstick input [0,1] + */ +export function getThumbstickOrThumbpadAxes(inputSource: XRInputSource, handedness: XRHandedness, deadZone = 0.05) { + const gamepad = inputSource.gamepad + const axes = gamepad!.axes + const axesIndex = inputSource.gamepad?.mapping === 'xr-standard' || handedness === 'right' ? 2 : 0 + const xAxis = Math.abs(axes[axesIndex]) > deadZone ? axes[axesIndex] : 0 + const zAxis = Math.abs(axes[axesIndex + 1]) > deadZone ? axes[axesIndex + 1] : 0 + return [xAxis, zAxis] as [number, number] +} diff --git a/packages/spatial/src/input/state/ButtonState.ts b/packages/spatial/src/input/state/ButtonState.ts index 2d98fecdb8..5eeb0b9518 100644 --- a/packages/spatial/src/input/state/ButtonState.ts +++ b/packages/spatial/src/input/state/ButtonState.ts @@ -196,30 +196,30 @@ export enum KeyboardButton { * https://www.w3.org/TR/gamepad/#dfn-standard-gamepad */ export enum StandardGamepadButton { - 'ButtonA' = 0, // X - 'ButtonB' = 1, // Circle - 'ButtonX' = 2, // Square - 'ButtonY' = 3, // Triangle - 'Left1' = 4, - 'Right1' = 5, - 'Left2' = 6, - 'Right2' = 7, - 'ButtonBack' = 8, - 'ButtonStart' = 9, - 'LeftStick' = 10, - 'RightStick' = 11, - 'DPadUp' = 12, - 'DPadDown' = 13, - 'DPadLeft' = 14, - 'DPadRight' = 15, - 'ButtonHome' = 16 + 'StandardGamepadButtonA' = 0, // X + 'StandardGamepadButtonB' = 1, // Circle + 'StandardGamepadButtonX' = 2, // Square + 'StandardGamepadButtonY' = 3, // Triangle + 'StandardGamepadLeft1' = 4, + 'StandardGamepadRight1' = 5, + 'StandardGamepadLeft2' = 6, + 'StandardGamepadRight2' = 7, + 'StandardGamepadButtonBack' = 8, + 'StandardGamepadButtonStart' = 9, + 'StandardGamepadLeftStick' = 10, + 'StandardGamepadRightStick' = 11, + 'StandardGamepadDPadUp' = 12, + 'StandardGamepadDPadDown' = 13, + 'StandardGamepadDPadLeft' = 14, + 'StandardGamepadDPadRight' = 15, + 'StandardGamepadButtonHome' = 16 } export enum StandardGamepadAxes { - 'LeftStickX' = 0, - 'LeftStickY' = 1, - 'RightStickX' = 2, - 'RightStickY' = 3 + 'StandardGamepadLeftStickX' = 0, + 'StandardGamepadLeftStickY' = 1, + 'StandardGamepadRightStickX' = 2, + 'StandardGamepadRightStickY' = 3 } /** @@ -227,24 +227,53 @@ export enum StandardGamepadAxes { * https://www.w3.org/TR/webxr-gamepads-module-1/#xr-standard-gamepad-mapping */ export enum XRStandardGamepadButton { - 'Trigger' = 0, - 'Squeeze' = 1, - 'Pad' = 2, - 'Stick' = 3, - 'ButtonA' = 4, - 'ButtonB' = 5 + 'XRStandardGamepadTrigger' = 0, + 'XRStandardGamepadSqueeze' = 1, + 'XRStandardGamepadPad' = 2, + 'XRStandardGamepadStick' = 3, + 'XRStandardGamepadButtonA' = 4, + 'XRStandardGamepadButtonB' = 5 } export enum XRStandardGamepadAxes { - 'TouchpadX' = 0, - 'TouchpadY' = 1, - 'ThumbstickX' = 2, - 'ThumbstickY' = 3 + 'XRStandardGamepadTouchpadX' = 0, + 'XRStandardGamepadTouchpadY' = 1, + 'XRStandardGamepadThumbstickX' = 2, + 'XRStandardGamepadThumbstickY' = 3 } -export type AnyButton = MouseButton | KeyboardButton | StandardGamepadButton | XRStandardGamepadButton +export type AnyButton = + | keyof typeof MouseButton + | keyof typeof KeyboardButton + | keyof typeof StandardGamepadButton + | keyof typeof XRStandardGamepadButton + | StandardGamepadButton + | XRStandardGamepadButton +export type AnyAxis = + | keyof typeof MouseScroll + | keyof typeof StandardGamepadAxes + | keyof typeof XRStandardGamepadAxes + | MouseScroll + | StandardGamepadAxes + | XRStandardGamepadAxes -export type ButtonStateMap = Partial> +export type ButtonStateMap> = Partial< + Record +> +export type AxisValueMap> = Partial> + +export const ButtonMapping = { + '': MouseButton, + keyboard: KeyboardButton, + standard: StandardGamepadButton, + 'xr-standard': XRStandardGamepadButton +} satisfies Record> + +export const AxisMapping = { + '': MouseScroll, + 'xr-standard': XRStandardGamepadAxes, + standard: StandardGamepadAxes +} satisfies Record> export const DefaultBooleanButtonState = Object.freeze({ down: true, diff --git a/packages/spatial/src/input/systems/ClientInputSystem.tsx b/packages/spatial/src/input/systems/ClientInputSystem.tsx index 9afdfc0adc..0ca3304eef 100755 --- a/packages/spatial/src/input/systems/ClientInputSystem.tsx +++ b/packages/spatial/src/input/systems/ClientInputSystem.tsx @@ -34,7 +34,6 @@ import { getMutableComponent, getOptionalComponent, hasComponent, - removeComponent, setComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { Engine } from '@etherealengine/ecs/src/Engine' @@ -52,6 +51,7 @@ import { } from '@etherealengine/spatial/src/transform/components/EntityTree' import { UUIDComponent } from '@etherealengine/ecs' +import { InteractableComponent } from '@etherealengine/engine/src/interaction/components/InteractableComponent' import { CameraComponent } from '../../camera/components/CameraComponent' import { ObjectDirection, PI, Q_IDENTITY, Vector3_Zero } from '../../common/constants/MathConstants' import { NameComponent } from '../../common/NameComponent' @@ -71,11 +71,11 @@ import { XRSpaceComponent } from '../../xr/XRComponents' import { XRScenePlacementComponent } from '../../xr/XRScenePlacementComponent' import { XRControlsState, XRState } from '../../xr/XRState' import { XRUIComponent } from '../../xrui/components/XRUIComponent' -import { InputComponent } from '../components/InputComponent' +import { DefaultButtonAlias, InputComponent } from '../components/InputComponent' import { InputPointerComponent } from '../components/InputPointerComponent' import { InputSourceComponent } from '../components/InputSourceComponent' import normalizeWheel from '../functions/normalizeWheel' -import { ButtonStateMap, createInitialButtonState, MouseButton } from '../state/ButtonState' +import { AnyButton, ButtonState, ButtonStateMap, createInitialButtonState, MouseButton } from '../state/ButtonState' import { InputState } from '../state/InputState' /** squared distance threshold for dragging state */ @@ -101,7 +101,7 @@ const preventDefaultKeyDown = (evt) => { export function updateGamepadInput(eid: Entity) { const inputSource = getComponent(eid, InputSourceComponent) const gamepad = inputSource.source.gamepad - const buttons = inputSource.buttons as ButtonStateMap + const buttons = inputSource.buttons // const buttonDownPos = inputSource.buttonDownPositions as WeakMap // log buttons // if (source.gamepad) { @@ -180,7 +180,7 @@ const inputs = defineQuery([InputComponent]) const worldPosInputSourceComponent = new Vector3() const worldPosInputComponent = new Vector3() -const inputXRUIs = defineQuery([InputComponent, VisibleComponent, XRUIComponent]) +const xruiQuery = defineQuery([VisibleComponent, XRUIComponent]) const boundingBoxesQuery = defineQuery([VisibleComponent, BoundingBoxComponent]) const meshesQuery = defineQuery([VisibleComponent, MeshComponent]) @@ -227,8 +227,7 @@ const execute = () => { for (const eid of pointers()) { const pointer = getComponent(eid, InputPointerComponent) const inputSource = getComponent(eid, InputSourceComponent) - const viewerEntity = pointer.canvasEntity - const camera = getComponent(viewerEntity, CameraComponent) + const camera = getComponent(pointer.cameraEntity, CameraComponent) pointer.movement.copy(pointer.position).sub(pointer.lastPosition) pointer.lastPosition.copy(pointer.position) inputSource.raycaster.setFromCamera(pointer.position, camera) @@ -263,6 +262,11 @@ const execute = () => { } } + const interactionRays = inputSourceQuery().map((eid) => getComponent(eid, InputSourceComponent).raycaster.ray) + for (const xrui of xruiQuery()) { + getComponent(xrui, XRUIComponent).interactionRays = interactionRays + } + // assign input sources (InputSourceComponent) to input sinks (InputComponent), foreach on InputSourceComponents for (const sourceEid of inputSourceQuery()) { const isSpatialInput = hasComponent(sourceEid, TransformComponent) @@ -305,7 +309,7 @@ const execute = () => { } } else { // 1st heuristic is XRUI - for (const entity of inputXRUIs()) { + for (const entity of xruiQuery()) { const xrui = getComponent(entity, XRUIComponent) const layerHit = xrui.hitTest(inputRay) if ( @@ -330,7 +334,8 @@ const execute = () => { // 3rd heuristic is bboxes for (const entity of inputState.inputBoundingBoxes) { - const boundingBox = getComponent(entity, BoundingBoxComponent) + const boundingBox = getOptionalComponent(entity, BoundingBoxComponent) + if (!boundingBox) continue const hit = inputRay.intersectBox(boundingBox.box, bboxHitTarget) if (hit) { intersectionData.add({ entity, distance: inputRay.origin.distanceTo(bboxHitTarget) }) @@ -371,9 +376,6 @@ const execute = () => { sortedIntersections.length === 0 && !hasComponent(sourceEid, InputPointerComponent) ) { - let closestEntity = UndefinedEntity - let closestDistanceSquared = Infinity - //use sourceEid if controller (one InputSource per controller), otherwise use avatar rather than InputSource-emulated-pointer const selfAvatarEntity = UUIDComponent.getEntityByUUID((Engine.instance.userID + '_avatar') as EntityUUID) //would prefer a better way to do this const inputSourceEntity = @@ -388,27 +390,42 @@ const execute = () => { const inputComponent = getComponent(inputEntity, InputComponent) TransformComponent.getWorldPosition(inputEntity, worldPosInputComponent) - const distSquared = worldPosInputSourceComponent.distanceToSquared(worldPosInputComponent) //closer than our current closest AND within inputSource's activation distance - if ( - distSquared < closestDistanceSquared && - inputComponent.activationDistance * inputComponent.activationDistance > distSquared - ) { - closestDistanceSquared = distSquared - closestEntity = inputEntity + if (inputComponent.activationDistance * inputComponent.activationDistance > distSquared) { + //using this object type out of convenience (intersectionsData is also guaranteed empty in this flow) + intersectionData.add({ entity: inputEntity, distance: distSquared }) //keeping it as distSquared for now to avoid extra square root calls } } - if (closestEntity !== UndefinedEntity) { - sortedIntersections.push({ entity: closestEntity, distance: Math.sqrt(closestDistanceSquared) }) + const closestEntities = Array.from(intersectionData) + if (closestEntities.length > 0) { + if (closestEntities.length === 1) { + sortedIntersections.push({ + entity: closestEntities[0].entity, + distance: Math.sqrt(closestEntities[0].distance) + }) + } else { + //sort if more than 1 entry + closestEntities.sort((a, b) => { + //prioritize anything with an InteractableComponent if otherwise equal + const aNum = hasComponent(a.entity, InteractableComponent) ? -1 : 0 + const bNum = hasComponent(b.entity, InteractableComponent) ? -1 : 0 + //aNum - bNum : 0 if equal, -1 if a has tag and b doesn't, 1 if a doesnt have tag and b does + return Math.sign(a.distance - b.distance) + (aNum - bNum) + }) + sortedIntersections.push({ + entity: closestEntities[0].entity, + distance: Math.sqrt(closestEntities[0].distance) + }) + } } } } const inputPointerComponent = getOptionalComponent(sourceEid, InputPointerComponent) if (inputPointerComponent) { - sortedIntersections.push({ entity: inputPointerComponent.canvasEntity, distance: 0 }) + sortedIntersections.push({ entity: inputPointerComponent.cameraEntity, distance: 0 }) } sourceState.intersections.set(sortedIntersections) @@ -463,7 +480,7 @@ const useNonSpatialInputSources = () => { const code = event.code const down = event.type === 'keydown' - const buttonState = inputSourceComponent.buttons as ButtonStateMap + const buttonState = inputSourceComponent.buttons if (down) buttonState[code] = createInitialButtonState(eid) else if (buttonState[code]) buttonState[code].up = true } @@ -481,30 +498,21 @@ const useNonSpatialInputSources = () => { document.addEventListener('touchstickmove', handleTouchDirectionalPad) document.addEventListener('touchgamepadbuttondown', (event: CustomEvent) => { - const buttonState = inputSourceComponent.buttons as ButtonStateMap + const buttonState = inputSourceComponent.buttons buttonState[event.detail.button] = createInitialButtonState(eid) }) document.addEventListener('touchgamepadbuttonup', (event: CustomEvent) => { - const buttonState = inputSourceComponent.buttons as ButtonStateMap + const buttonState = inputSourceComponent.buttons if (buttonState[event.detail.button]) buttonState[event.detail.button].up = true }) - const onWheelEvent = (event: WheelEvent) => { - const normalizedValues = normalizeWheel(event) - const axes = inputSourceComponent.source.gamepad!.axes as number[] - axes[0] = normalizedValues.spinX - axes[1] = normalizedValues.spinY - } - document.addEventListener('wheel', onWheelEvent, { passive: true, capture: true }) - return () => { document.removeEventListener('DOMMouseScroll', preventDefault, false) document.removeEventListener('gesturestart', preventDefault) document.removeEventListener('keyup', onKeyEvent) document.removeEventListener('keydown', onKeyEvent) document.removeEventListener('touchstickmove', handleTouchDirectionalPad) - document.removeEventListener('wheel', onWheelEvent) removeEntity(eid) } }, []) @@ -532,75 +540,67 @@ const useGamepadInputSources = () => { } const CanvasInputReactor = () => { - const canvasEntity = useEntityContext() + const cameraEntity = useEntityContext() const xrState = useMutableState(XRState) useEffect(() => { if (xrState.session.value) return // pointer input sources are automatically handled by webxr - const rendererComponent = getComponent(canvasEntity, RendererComponent) + const rendererComponent = getComponent(cameraEntity, RendererComponent) const canvas = rendererComponent.canvas - canvas.addEventListener('dragstart', preventDefault, false) - canvas.addEventListener('contextmenu', preventDefault) - - // TODO: follow this spec more closely https://immersive-web.github.io/webxr/#transient-input - // const pointerEntities = new Map() - - const emulatedInputSourceEntity = createEntity() - setComponent(emulatedInputSourceEntity, NameComponent, 'InputSource-emulated-pointer') - setComponent(emulatedInputSourceEntity, TransformComponent) - setComponent(emulatedInputSourceEntity, InputSourceComponent) - const inputSourceComponent = getComponent(emulatedInputSourceEntity, InputSourceComponent) - /** Clear mouse events */ - const pointerButtons = ['PrimaryClick', 'AuxiliaryClick', 'SecondaryClick'] - const clearPointerState = () => { - const state = inputSourceComponent.buttons as ButtonStateMap + const pointerButtons = ['PrimaryClick', 'AuxiliaryClick', 'SecondaryClick'] as AnyButton[] + const clearPointerState = (entity: Entity) => { + const inputSourceComponent = getComponent(entity, InputSourceComponent) + const state = inputSourceComponent.buttons for (const button of pointerButtons) { - const val = state[button] - if (!val?.up && val?.pressed) state[button].up = true + const val = state[button] as ButtonState + if (!val?.up && val?.pressed) (state[button] as ButtonState).up = true } } - const pointerEnter = (event: PointerEvent) => { - setComponent(emulatedInputSourceEntity, InputPointerComponent, { + const onPointerEnter = (event: PointerEvent) => { + const pointerEntity = createEntity() + setComponent(pointerEntity, NameComponent, 'InputSource-emulated-pointer') + setComponent(pointerEntity, TransformComponent) + setComponent(pointerEntity, InputSourceComponent) + setComponent(pointerEntity, InputPointerComponent, { pointerId: event.pointerId, - canvasEntity: canvasEntity + cameraEntity }) + redirectPointerEventsToXRUI(cameraEntity, event) } - const pointerLeave = (event: PointerEvent) => { - const pointerComponent = getOptionalComponent(emulatedInputSourceEntity, InputPointerComponent) - if (!pointerComponent || pointerComponent?.pointerId !== event.pointerId) return - clearPointerState() - removeComponent(emulatedInputSourceEntity, InputPointerComponent) + const onPointerOver = (event: PointerEvent) => { + redirectPointerEventsToXRUI(cameraEntity, event) } - canvas.addEventListener('pointerenter', pointerEnter) - canvas.addEventListener('pointerleave', pointerLeave) + const onPointerOut = (event: PointerEvent) => { + redirectPointerEventsToXRUI(cameraEntity, event) + } - canvas.addEventListener('blur', clearPointerState) - canvas.addEventListener('mouseleave', clearPointerState) - const handleVisibilityChange = (event: Event) => { - if (document.visibilityState === 'hidden') clearPointerState() + const onPointerLeave = (event: PointerEvent) => { + const pointerEntity = InputPointerComponent.getPointerByID(cameraEntity, event.pointerId) + redirectPointerEventsToXRUI(cameraEntity, event) + removeEntity(pointerEntity) } - canvas.addEventListener('visibilitychange', handleVisibilityChange) - const handleMouseClick = (event: MouseEvent) => { - const down = event.type === 'mousedown' || event.type === 'touchstart' + const onPointerClick = (event: PointerEvent) => { + const pointerEntity = InputPointerComponent.getPointerByID(cameraEntity, event.pointerId) + const inputSourceComponent = getOptionalComponent(pointerEntity, InputSourceComponent) + if (!inputSourceComponent) return + + const down = event.type === 'pointerdown' let button = MouseButton.PrimaryClick if (event.button === 1) button = MouseButton.AuxiliaryClick else if (event.button === 2) button = MouseButton.SecondaryClick - const inputSourceComponent = getOptionalComponent(emulatedInputSourceEntity, InputSourceComponent) - if (!inputSourceComponent) return - - const state = inputSourceComponent.buttons as ButtonStateMap + const state = inputSourceComponent.buttons as ButtonStateMap if (down) { - state[button] = createInitialButtonState(emulatedInputSourceEntity) //down, pressed, touched = true + state[button] = createInitialButtonState(pointerEntity) //down, pressed, touched = true - const pointer = getOptionalComponent(emulatedInputSourceEntity, InputPointerComponent) + const pointer = getOptionalComponent(pointerEntity, InputPointerComponent) if (pointer) { state[button]!.downPosition = new Vector3(pointer.position.x, pointer.position.y, 0) //rotation will never be defined for the mouse or touch @@ -608,50 +608,78 @@ const CanvasInputReactor = () => { } else if (state[button]) { state[button]!.up = true } - } - - const handleMouseMove = (event: MouseEvent) => { - handleMouseOrTouchMovement(event.clientX, event.clientY, event) - } - const handleTouchMove = (event: TouchEvent) => { - const touch = event.touches[0] - handleMouseOrTouchMovement(touch.clientX, touch.clientY, event) + redirectPointerEventsToXRUI(cameraEntity, event) } - const handleMouseOrTouchMovement = (clientX: number, clientY: number, event: MouseEvent | TouchEvent) => { - const pointerComponent = getOptionalComponent(emulatedInputSourceEntity, InputPointerComponent) + const onPointerMove = (event: PointerEvent) => { + const pointerEntity = InputPointerComponent.getPointerByID(cameraEntity, event.pointerId) + const pointerComponent = getOptionalComponent(pointerEntity, InputPointerComponent) if (!pointerComponent) return + pointerComponent.position.set( - ((clientX - canvas.getBoundingClientRect().x) / canvas.clientWidth) * 2 - 1, - ((clientY - canvas.getBoundingClientRect().y) / canvas.clientHeight) * -2 + 1 + ((event.clientX - canvas.getBoundingClientRect().x) / canvas.clientWidth) * 2 - 1, + ((event.clientY - canvas.getBoundingClientRect().y) / canvas.clientHeight) * -2 + 1 ) - updateMouseOrTouchDragging(emulatedInputSourceEntity, event) + updatePointerDragging(pointerEntity, event) + redirectPointerEventsToXRUI(cameraEntity, event) + } + + const onVisibilityChange = (event: Event) => { + if ( + document.visibilityState === 'hidden' || + !canvas.checkVisibility({ + checkOpacity: true, + checkVisibilityCSS: true + }) + ) { + InputPointerComponent.getPointersForCamera(cameraEntity).forEach(clearPointerState) + } + } + + const onClick = (evt: PointerEvent) => { + redirectPointerEventsToXRUI(cameraEntity, evt) + } + + const onWheelEvent = (event: WheelEvent) => { + const pointer = InputPointerComponent.getPointersForCamera(cameraEntity)[0] + if (!pointer) return + const inputSourceComponent = getComponent(pointer, InputSourceComponent) + const normalizedValues = normalizeWheel(event) + const axes = inputSourceComponent.source.gamepad!.axes as number[] + axes[0] = normalizedValues.spinX + axes[1] = normalizedValues.spinY } - canvas.addEventListener('touchmove', handleTouchMove, { passive: true, capture: true }) - canvas.addEventListener('mousemove', handleMouseMove, { passive: true, capture: true }) - canvas.addEventListener('mouseup', handleMouseClick) - canvas.addEventListener('mousedown', handleMouseClick) - canvas.addEventListener('touchstart', handleMouseClick) - canvas.addEventListener('touchend', handleMouseClick) + canvas.addEventListener('dragstart', preventDefault, false) + canvas.addEventListener('contextmenu', preventDefault) + canvas.addEventListener('pointerenter', onPointerEnter) + canvas.addEventListener('pointerover', onPointerOver) + canvas.addEventListener('pointerout', onPointerOut) + canvas.addEventListener('pointerleave', onPointerLeave) + canvas.addEventListener('pointermove', onPointerMove, { passive: true, capture: true }) + canvas.addEventListener('pointerup', onPointerClick) + canvas.addEventListener('pointerdown', onPointerClick) + canvas.addEventListener('blur', onVisibilityChange) + canvas.addEventListener('visibilitychange', onVisibilityChange) + canvas.addEventListener('click', onClick) + canvas.addEventListener('wheel', onWheelEvent, { passive: true, capture: true }) return () => { canvas.removeEventListener('dragstart', preventDefault, false) canvas.removeEventListener('contextmenu', preventDefault) - canvas.removeEventListener('pointerenter', pointerEnter) - canvas.removeEventListener('pointerleave', pointerLeave) - canvas.removeEventListener('blur', clearPointerState) - canvas.removeEventListener('mouseleave', clearPointerState) - canvas.removeEventListener('visibilitychange', handleVisibilityChange) - canvas.removeEventListener('touchmove', handleTouchMove) - canvas.removeEventListener('mousemove', handleMouseMove) - canvas.removeEventListener('mouseup', handleMouseClick) - canvas.removeEventListener('mousedown', handleMouseClick) - canvas.removeEventListener('touchstart', handleMouseClick) - canvas.removeEventListener('touchend', handleMouseClick) - removeEntity(emulatedInputSourceEntity) + canvas.removeEventListener('pointerenter', onPointerEnter) + canvas.removeEventListener('pointerover', onPointerOver) + canvas.removeEventListener('pointerout', onPointerOut) + canvas.removeEventListener('pointerleave', onPointerLeave) + canvas.removeEventListener('pointermove', onPointerMove) + canvas.removeEventListener('pointerup', onPointerClick) + canvas.removeEventListener('pointerdown', onPointerClick) + canvas.removeEventListener('blur', onVisibilityChange) + canvas.removeEventListener('visibilitychange', onVisibilityChange) + canvas.removeEventListener('click', onClick) + canvas.removeEventListener('wheel', onWheelEvent) } }, [xrState.session]) @@ -695,7 +723,7 @@ const useXRInputSources = () => { if (!eid) return const inputSourceComponent = getComponent(eid, InputSourceComponent) if (!inputSourceComponent) return - const state = inputSourceComponent.buttons as ButtonStateMap + const state = inputSourceComponent.buttons as ButtonStateMap state.PrimaryClick = createInitialButtonState(eid) } const onXRSelectEnd = (event: XRInputSourceEvent) => { @@ -703,7 +731,7 @@ const useXRInputSources = () => { if (!eid) return const inputSourceComponent = getComponent(eid, InputSourceComponent) if (!inputSourceComponent) return - const state = inputSourceComponent.buttons as ButtonStateMap + const state = inputSourceComponent.buttons as ButtonStateMap if (!state.PrimaryClick) return state.PrimaryClick.up = true } @@ -766,20 +794,20 @@ export const ClientInputSystem = defineSystem({ reactor }) -function updateMouseOrTouchDragging(emulatedInputSourceEntity: Entity, event: MouseEvent | TouchEvent) { - const inputSourceComponent = getOptionalComponent(emulatedInputSourceEntity, InputSourceComponent) +function updatePointerDragging(pointerEntity: Entity, event: PointerEvent) { + const inputSourceComponent = getOptionalComponent(pointerEntity, InputSourceComponent) if (!inputSourceComponent) return - const state = inputSourceComponent.buttons as ButtonStateMap + const state = inputSourceComponent.buttons as ButtonStateMap let button = MouseButton.PrimaryClick - if (event.type === 'mousemove') { + if (event.type === 'pointermove') { if ((event as MouseEvent).button === 1) button = MouseButton.AuxiliaryClick else if ((event as MouseEvent).button === 2) button = MouseButton.SecondaryClick } const btn = state[button] if (btn && !btn.dragging) { - const pointer = getOptionalComponent(emulatedInputSourceEntity, InputPointerComponent) + const pointer = getOptionalComponent(pointerEntity, InputPointerComponent) if (btn.pressed && btn.downPosition) { //if not yet dragging, compare distance to drag threshold and begin if appropriate @@ -797,7 +825,11 @@ function updateMouseOrTouchDragging(emulatedInputSourceEntity: Entity, event: Mo } } -function cleanupButton(key: string, buttons: ButtonStateMap, hasFocus: boolean) { +function cleanupButton( + key: string, + buttons: ButtonStateMap>>, + hasFocus: boolean +) { const button = buttons[key] if (button?.down) button.down = false if (button?.up || !hasFocus) delete buttons[key] @@ -825,3 +857,22 @@ export const ClientInputCleanupSystem = defineSystem({ insert: { after: PresentationSystemGroup }, execute: cleanupInputs }) + +const redirectPointerEventsToXRUI = (cameraEntity: Entity, evt: PointerEvent) => { + const pointerEntity = InputPointerComponent.getPointerByID(cameraEntity, evt.pointerId) + const inputSource = getOptionalComponent(pointerEntity, InputSourceComponent) + if (!inputSource) return + for (const i of inputSource.intersections) { + const entity = i.entity + const xrui = getOptionalComponent(entity, XRUIComponent) + if (!xrui) continue + xrui.updateWorldMatrix(true, true) + const raycaster = inputSource.raycaster + const hit = xrui.hitTest(raycaster.ray) + if (hit && hit.intersection.object.visible) { + hit.target.dispatchEvent(new (evt.constructor as any)(evt.type, evt)) + hit.target.focus() + return + } + } +} diff --git a/packages/spatial/src/input/systems/FlyControlSystem.ts b/packages/spatial/src/input/systems/FlyControlSystem.ts index 206d62f5fd..c7fb925245 100644 --- a/packages/spatial/src/input/systems/FlyControlSystem.ts +++ b/packages/spatial/src/input/systems/FlyControlSystem.ts @@ -87,7 +87,7 @@ const execute = () => { /** Since we have nothing that specifies whether we should use orbit/fly controls or not, just tie it to the camera orbit component for the studio */ for (const entity of cameraQuery()) { - const inputPointerEntity = InputPointerComponent.getPointerForCanvas(entity) + const inputPointerEntity = InputPointerComponent.getPointersForCamera(entity) if (!inputPointerEntity) continue if (hasComponent(entity, CameraOrbitComponent)) { if (buttons.SecondaryClick?.down) onSecondaryClick(entity) diff --git a/packages/spatial/src/transform/components/BoundingBoxComponents.ts b/packages/spatial/src/transform/components/BoundingBoxComponents.ts index 64a4dd5268..ac49effc9d 100755 --- a/packages/spatial/src/transform/components/BoundingBoxComponents.ts +++ b/packages/spatial/src/transform/components/BoundingBoxComponents.ts @@ -98,8 +98,14 @@ export const BoundingBoxComponent = defineComponent({ }) export const updateBoundingBox = (entity: Entity) => { - const box = getComponent(entity, BoundingBoxComponent).box + const boxComponent = getOptionalComponent(entity, BoundingBoxComponent) + if (!boxComponent) { + console.error('BoundingBoxComponent not found in updateBoundingBox') + return + } + + const box = boxComponent.box box.makeEmpty() const callback = (child: Entity) => { diff --git a/packages/spatial/src/xr/XRHapticsSystem.ts b/packages/spatial/src/xr/XRHapticsSystem.ts index 20347617bd..ed246388ea 100644 --- a/packages/spatial/src/xr/XRHapticsSystem.ts +++ b/packages/spatial/src/xr/XRHapticsSystem.ts @@ -35,15 +35,13 @@ import { XRAction } from './XRState' /** haptic typings are currently incomplete */ declare global { - interface Gamepad { - /** @deprecated - old meta quest API */ - hapticActuators?: Array<{ - /** - * @param value A double representing the intensity of the pulse. This can vary depending on the hardware type, but generally takes a value between 0.0 (no intensity) and 1.0 (full intensity). - * @param duration A double representing the duration of the pulse, in milliseconds. - */ - pulse: (value: number, duration: number) => void - }> + interface GamepadHapticActuator { + /** + * @deprecated - old meta quest API + * @param value A double representing the intensity of the pulse. This can vary depending on the hardware type, but generally takes a value between 0.0 (no intensity) and 1.0 (full intensity). + * @param duration A double representing the duration of the pulse, in milliseconds. + */ + pulse: (value: number, duration: number) => void } } diff --git a/packages/spatial/src/xrui/components/XRUIComponent.ts b/packages/spatial/src/xrui/components/XRUIComponent.ts index 4e398afed5..1895877889 100644 --- a/packages/spatial/src/xrui/components/XRUIComponent.ts +++ b/packages/spatial/src/xrui/components/XRUIComponent.ts @@ -24,11 +24,8 @@ Ethereal Engine. All Rights Reserved. */ import { defineComponent } from '@etherealengine/ecs/src/ComponentFunctions' -import { getState } from '@etherealengine/hyperflux' import type { WebContainer3D } from '@etherealengine/xrui' -import { XRUIState } from '../XRUIState' - export const XRUIComponent = defineComponent({ name: 'XRUIComponent', @@ -39,7 +36,6 @@ export const XRUIComponent = defineComponent({ onSet: (entity, component, json: WebContainer3D) => { if (typeof json !== 'undefined') { component.set(json) - json.interactionRays = getState(XRUIState).interactionRays } }, diff --git a/packages/spatial/src/xrui/systems/XRUISystem.ts b/packages/spatial/src/xrui/systems/XRUISystem.ts index f7155bee88..13fe616662 100644 --- a/packages/spatial/src/xrui/systems/XRUISystem.ts +++ b/packages/spatial/src/xrui/systems/XRUISystem.ts @@ -32,19 +32,14 @@ import { Entity } from '@etherealengine/ecs/src/Entity' import { removeEntity } from '@etherealengine/ecs/src/EntityFunctions' import { defineQuery } from '@etherealengine/ecs/src/QueryFunctions' import { defineSystem } from '@etherealengine/ecs/src/SystemFunctions' -import { getMutableState, getState } from '@etherealengine/hyperflux' import { WebContainer3D } from '@etherealengine/xrui' import { InputComponent } from '../../input/components/InputComponent' import { InputSourceComponent } from '../../input/components/InputSourceComponent' -import { XRStandardGamepadButton } from '../../input/state/ButtonState' -import { InputState } from '../../input/state/InputState' import { VisibleComponent } from '../../renderer/components/VisibleComponent' import { TransformSystem } from '../../transform/systems/TransformSystem' -import { XRState } from '../../xr/XRState' import { PointerComponent, PointerObject } from '../components/PointerComponent' import { XRUIComponent } from '../components/XRUIComponent' -import { XRUIState } from '../XRUIState' const hitColor = new Color(0x00e6e6) const normalColor = new Color(0xffffff) @@ -56,10 +51,11 @@ const inputSourceQuery = defineQuery([InputSourceComponent]) // redirect DOM events from the canvas, to the 3D scene, // to the appropriate child Web3DLayer, and finally (back) to the // DOM to dispatch an event on the intended DOM target -const redirectDOMEvent = (evt) => { +const redirectDOMEvent = (evt: PointerEvent) => { for (const entity of visibleInteractableXRUIQuery()) { const layer = getComponent(entity, XRUIComponent) - const inputSources = getComponent(entity, InputComponent).inputSources + const inputSources = InputComponent.getInputSourceEntities(entity) + // const inputSources = getComponent(entity, InputComponent).inputSources if (!inputSources.length) continue const inputSource = getComponent(inputSources[0], InputSourceComponent) // assume only one input source per XRUI if (inputSource.intersections.length && inputSource.intersections[0].entity !== entity) continue // only handle events for the first intersection @@ -67,7 +63,7 @@ const redirectDOMEvent = (evt) => { const raycaster = inputSource.raycaster const hit = layer.hitTest(raycaster.ray) if (hit && hit.intersection.object.visible) { - hit.target.dispatchEvent(new evt.constructor(evt.type, evt)) + hit.target.dispatchEvent(new (evt.constructor as any)(evt.type, evt)) hit.target.focus() return } @@ -133,17 +129,6 @@ const updateClickEventsForController = (entity: Entity) => { const execute = () => { if (!isClient) return - const xruiState = getState(XRUIState) - const xrFrame = getState(XRState).xrFrame - - /** Update the objects to use for intersection tests */ - const pointerScreenRaycaster = getState(InputState).pointerScreenRaycaster - if (xrFrame && xruiState.interactionRays[0] === pointerScreenRaycaster.ray) - xruiState.interactionRays = [...PointerComponent.getPointers(), pointerScreenRaycaster.ray] // todo, replace pointerScreenRaycaster with input sources - - if (!xrFrame && xruiState.interactionRays[0] !== pointerScreenRaycaster.ray) - xruiState.interactionRays = [pointerScreenRaycaster.ray] - const interactableXRUIEntities = visibleInteractableXRUIQuery() const inputSourceEntities = inputSourceQuery() @@ -166,7 +151,7 @@ const execute = () => { if (!pointer) continue if ( - buttons[XRStandardGamepadButton.Trigger]?.down && + buttons.XRStandardGamepadTrigger?.down && (inputSource.handedness === 'left' || inputSource.handedness === 'right') ) updateClickEventsForController(pointerEntity) @@ -222,10 +207,6 @@ const reactor = () => { document.body.addEventListener('contextmenu', redirectDOMEvent) document.body.addEventListener('dblclick', redirectDOMEvent) - const pointerScreenRaycaster = getState(InputState).pointerScreenRaycaster - - getMutableState(XRUIState).interactionRays.set([pointerScreenRaycaster.ray]) - return () => { document.body.removeEventListener('pointerdown', redirectDOMEvent) document.body.removeEventListener('click', redirectDOMEvent) diff --git a/packages/ui/src/components/editor/properties/camera/index.tsx b/packages/ui/src/components/editor/properties/camera/index.tsx index 6a5cb16e2b..0c328608c6 100644 --- a/packages/ui/src/components/editor/properties/camera/index.tsx +++ b/packages/ui/src/components/editor/properties/camera/index.tsx @@ -28,7 +28,7 @@ import React from 'react' import { getOptionalComponent, useComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { CameraSettingsComponent } from '@etherealengine/engine/src/scene/components/CameraSettingsComponent' -import { CameraMode } from '@etherealengine/spatial/src/camera/types/CameraMode' +import { FollowCameraMode } from '@etherealengine/spatial/src/camera/types/FollowCameraMode' import { defineQuery } from '@etherealengine/ecs/src/QueryFunctions' import { @@ -52,27 +52,27 @@ import PropertyGroup from '../group' const cameraModeSelect = [ { label: 'First Person', - value: CameraMode.FirstPerson + value: FollowCameraMode.FirstPerson }, { label: 'Shoulder Cam', - value: CameraMode.ShoulderCam + value: FollowCameraMode.ShoulderCam }, { label: 'Third Person', - value: CameraMode.ThirdPerson + value: FollowCameraMode.ThirdPerson }, { label: 'Top Down', - value: CameraMode.TopDown + value: FollowCameraMode.TopDown }, { label: 'Strategic', - value: CameraMode.Strategic + value: FollowCameraMode.Strategic }, { label: 'Dynamic', - value: CameraMode.Dynamic + value: FollowCameraMode.Dynamic } ]