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
}
]