From 2f6e4e12a48af907423220a0685c875f5248811e Mon Sep 17 00:00:00 2001 From: TheCodeTherapy Date: Tue, 26 Nov 2024 16:20:49 +0000 Subject: [PATCH 1/2] implements the toggleable orbital camera --- .../client/src/LocalAvatarClient.ts | 2 +- .../src/camera/CameraManager.ts | 68 +++++++++++++++++-- .../src/character/LocalController.ts | 8 +-- packages/3d-web-client-core/src/index.ts | 2 +- .../src/input/KeyInputManager.ts | 15 +++- .../src/rendering/composer.ts | 43 +++++++----- .../src/Networked3dWebExperienceClient.ts | 8 ++- 7 files changed, 116 insertions(+), 30 deletions(-) diff --git a/example/local-only-multi-user-3d-web-experience/client/src/LocalAvatarClient.ts b/example/local-only-multi-user-3d-web-experience/client/src/LocalAvatarClient.ts index 5011ac9e..b9fa98db 100644 --- a/example/local-only-multi-user-3d-web-experience/client/src/LocalAvatarClient.ts +++ b/example/local-only-multi-user-3d-web-experience/client/src/LocalAvatarClient.ts @@ -100,7 +100,7 @@ export class LocalAvatarClient { this.composer = new Composer({ scene: this.scene, - camera: this.cameraManager.camera, + cameraManager: this.cameraManager, spawnSun: true, }); this.composer.useHDRJPG(hdrJpgUrl); diff --git a/packages/3d-web-client-core/src/camera/CameraManager.ts b/packages/3d-web-client-core/src/camera/CameraManager.ts index 24129a65..217f3bd5 100644 --- a/packages/3d-web-client-core/src/camera/CameraManager.ts +++ b/packages/3d-web-client-core/src/camera/CameraManager.ts @@ -1,4 +1,5 @@ import { PerspectiveCamera, Raycaster, Vector3 } from "three"; +import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import { CollisionsManager } from "../collisions/CollisionsManager"; import { remap } from "../helpers/math-helpers"; @@ -13,6 +14,9 @@ const pinchZoomSensitivity = 0.025; export class CameraManager { public readonly camera: PerspectiveCamera; + private flyCamera: PerspectiveCamera; + private orbitControls: OrbitControls; + private isMainCameraActive: boolean = true; public initialDistance: number = camValues.initialDistance; public minDistance: number = camValues.minDistance; @@ -66,21 +70,44 @@ export class CameraManager { this.targetPhi = this.phi; this.theta = initialTheta; this.targetTheta = this.theta; - this.camera = new PerspectiveCamera(this.fov, window.innerWidth / window.innerHeight, 0.1, 400); + + const aspect = window.innerWidth / window.innerHeight; + + this.camera = new PerspectiveCamera(this.fov, aspect, 0.1, 400); this.camera.position.set(0, 1.4, -this.initialDistance); + this.camera.name = "MainCamera"; + this.flyCamera = new PerspectiveCamera(this.initialFOV, aspect, 0.1, 400); + this.flyCamera.name = "FlyCamera"; + this.flyCamera.position.copy(this.camera.position); + this.flyCamera.name = "FlyCamera"; + + this.orbitControls = new OrbitControls(this.flyCamera, this.targetElement); + this.orbitControls.enableDamping = true; + this.orbitControls.dampingFactor = 0.05; + this.orbitControls.enablePan = true; + this.orbitControls.enabled = false; + this.rayCaster = new Raycaster(); + this.createEventHandlers(); + } + + private createEventHandlers(): void { this.eventHandlerCollection = EventHandlerCollection.create([ - [targetElement, "pointerdown", this.onPointerDown.bind(this)], - [targetElement, "gesturestart", this.preventDefaultAndStopPropagation.bind(this)], + [this.targetElement, "pointerdown", this.onPointerDown.bind(this)], + [this.targetElement, "gesturestart", this.preventDefaultAndStopPropagation.bind(this)], + [this.targetElement, "wheel", this.onMouseWheel.bind(this)], + [this.targetElement, "contextmenu", this.onContextMenu.bind(this)], [document, "pointerup", this.onPointerUp.bind(this)], [document, "pointercancel", this.onPointerUp.bind(this)], [document, "pointermove", this.onPointerMove.bind(this)], - [targetElement, "wheel", this.onMouseWheel.bind(this)], - [targetElement, "contextmenu", this.onContextMenu.bind(this)], ]); } + private disposeEventHandlers(): void { + this.eventHandlerCollection.clear(); + } + private preventDefaultAndStopPropagation(evt: PointerEvent): void { evt.preventDefault(); evt.stopPropagation(); @@ -238,7 +265,8 @@ export class CameraManager { } public dispose() { - this.eventHandlerCollection.clear(); + this.disposeEventHandlers(); + this.orbitControls.dispose(); document.body.style.cursor = ""; } @@ -248,6 +276,7 @@ export class CameraManager { public updateAspect(aspect: number): void { this.camera.aspect = aspect; + this.flyCamera.aspect = aspect; } public recomputeFoV(immediately: boolean = false): void { @@ -263,7 +292,34 @@ export class CameraManager { } } + public toggleFlyCamera(): void { + this.isMainCameraActive = !this.isMainCameraActive; + this.orbitControls.enabled = !this.isMainCameraActive; + + if (!this.isMainCameraActive) { + this.updateAspect(window.innerWidth / window.innerHeight); + this.flyCamera.position.copy(this.camera.position); + this.flyCamera.rotation.copy(this.camera.rotation); + const target = new Vector3(); + this.camera.getWorldDirection(target); + target.multiplyScalar(this.targetDistance).add(this.camera.position); + this.orbitControls.target.copy(target); + this.orbitControls.update(); + this.disposeEventHandlers(); + } else { + this.createEventHandlers(); + } + } + + get activeCamera(): PerspectiveCamera { + return this.isMainCameraActive ? this.camera : this.flyCamera; + } + public update(): void { + if (!this.isMainCameraActive) { + this.orbitControls.update(); + return; + } if (this.isLerping && this.lerpFactor < 1) { this.lerpFactor += 0.01 / this.lerpDuration; this.lerpFactor = Math.min(1, this.lerpFactor); diff --git a/packages/3d-web-client-core/src/character/LocalController.ts b/packages/3d-web-client-core/src/character/LocalController.ts index 1f7398c0..1edde5dc 100644 --- a/packages/3d-web-client-core/src/character/LocalController.ts +++ b/packages/3d-web-client-core/src/character/LocalController.ts @@ -172,19 +172,19 @@ export class LocalController { } private updateAzimuthalAngle(): void { - const camToModelDistance = this.config.cameraManager.camera.position.distanceTo( + const camToModelDistance = this.config.cameraManager.activeCamera.position.distanceTo( this.config.character.position, ); const isCameraFirstPerson = camToModelDistance < 2; if (isCameraFirstPerson) { const cameraForward = this.tempVector .set(0, 0, 1) - .applyQuaternion(this.config.cameraManager.camera.quaternion); + .applyQuaternion(this.config.cameraManager.activeCamera.quaternion); this.azimuthalAngle = Math.atan2(cameraForward.x, cameraForward.z); } else { this.azimuthalAngle = Math.atan2( - this.config.cameraManager.camera.position.x - this.config.character.position.x, - this.config.cameraManager.camera.position.z - this.config.character.position.z, + this.config.cameraManager.activeCamera.position.x - this.config.character.position.x, + this.config.cameraManager.activeCamera.position.z - this.config.character.position.z, ); } } diff --git a/packages/3d-web-client-core/src/index.ts b/packages/3d-web-client-core/src/index.ts index 71954aad..ca80cd40 100644 --- a/packages/3d-web-client-core/src/index.ts +++ b/packages/3d-web-client-core/src/index.ts @@ -5,7 +5,7 @@ export * from "./character/url-position"; export * from "./helpers/math-helpers"; export { CharacterModelLoader } from "./character/CharacterModelLoader"; export { CharacterState, AnimationState } from "./character/CharacterState"; -export { KeyInputManager } from "./input/KeyInputManager"; +export { Key, KeyInputManager } from "./input/KeyInputManager"; export { VirtualJoystick } from "./input/VirtualJoystick"; export { MMLCompositionScene } from "./mml/MMLCompositionScene"; export { TweakPane } from "./tweakpane/TweakPane"; diff --git a/packages/3d-web-client-core/src/input/KeyInputManager.ts b/packages/3d-web-client-core/src/input/KeyInputManager.ts index 6a45aa59..11e30761 100644 --- a/packages/3d-web-client-core/src/input/KeyInputManager.ts +++ b/packages/3d-web-client-core/src/input/KeyInputManager.ts @@ -1,18 +1,23 @@ import { EventHandlerCollection } from "./EventHandlerCollection"; import { VirtualJoystick } from "./VirtualJoystick"; -enum Key { +export enum Key { W = "w", A = "a", S = "s", D = "d", SHIFT = "shift", SPACE = " ", + C = "c", } +type KeyCallback = () => void; +type BindingsType = Map; + export class KeyInputManager { private keys = new Map(); private eventHandlerCollection = new EventHandlerCollection(); + private bindings: BindingsType = new Map(); constructor(private shouldCaptureKeyPress: () => boolean = () => true) { this.eventHandlerCollection.add(document, "keydown", this.onKeyDown.bind(this)); @@ -41,12 +46,19 @@ export class KeyInputManager { private onKeyUp(event: KeyboardEvent): void { this.keys.set(event.key.toLowerCase(), false); + if (this.bindings.has(event.key.toLowerCase() as Key)) { + this.bindings.get(event.key.toLowerCase() as Key)!(); + } } public isKeyPressed(key: string): boolean { return this.keys.get(key) || false; } + public createKeyBinding(key: Key, callback: () => void): void { + this.bindings.set(key, callback); + } + public isMovementKeyPressed(): boolean { return [Key.W, Key.A, Key.S, Key.D].some((key) => this.isKeyPressed(key)); } @@ -91,5 +103,6 @@ export class KeyInputManager { public dispose() { this.eventHandlerCollection.clear(); + this.bindings.clear(); } } diff --git a/packages/3d-web-client-core/src/rendering/composer.ts b/packages/3d-web-client-core/src/rendering/composer.ts index 60941d37..bae18cf6 100644 --- a/packages/3d-web-client-core/src/rendering/composer.ts +++ b/packages/3d-web-client-core/src/rendering/composer.ts @@ -38,6 +38,7 @@ import { } from "three"; import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js"; +import { CameraManager } from "../camera/CameraManager"; import { Sun } from "../sun/Sun"; import { TimeManager } from "../time/TimeManager"; import { bcsValues } from "../tweakpane/blades/bcsFolder"; @@ -54,7 +55,7 @@ import { N8SSAOPass } from "./post-effects/n8-ssao/N8SSAOPass"; type ComposerContructorArgs = { scene: Scene; - camera: PerspectiveCamera; + cameraManager: CameraManager; spawnSun: boolean; environmentConfiguration?: EnvironmentConfiguration; }; @@ -98,7 +99,7 @@ export class Composer { private readonly scene: Scene; public postPostScene: Scene; - private readonly camera: PerspectiveCamera; + private readonly cameraManager: CameraManager; public readonly renderer: WebGLRenderer; public readonly effectComposer: EffectComposer; @@ -142,13 +143,13 @@ export class Composer { constructor({ scene, - camera, + cameraManager, spawnSun = false, environmentConfiguration, }: ComposerContructorArgs) { this.scene = scene; + this.cameraManager = cameraManager; this.postPostScene = new Scene(); - this.camera = camera; this.spawnSun = spawnSun; this.renderer = new WebGLRenderer({ powerPreference: "high-performance", @@ -172,16 +173,16 @@ export class Composer { frameBufferType: HalfFloatType, }); - this.renderPass = new RenderPass(this.scene, this.camera); + this.renderPass = new RenderPass(this.scene, this.cameraManager.activeCamera); - this.normalPass = new NormalPass(this.scene, this.camera); + this.normalPass = new NormalPass(this.scene, this.cameraManager.activeCamera); this.normalPass.enabled = ppssaoValues.enabled; this.normalTextureEffect = new TextureEffect({ blendFunction: BlendFunction.SKIP, texture: this.normalPass.texture, }); - this.ppssaoEffect = new SSAOEffect(this.camera, this.normalPass.texture, { + this.ppssaoEffect = new SSAOEffect(this.cameraManager.activeCamera, this.normalPass.texture, { blendFunction: ppssaoValues.blendFunction, distanceScaling: ppssaoValues.distanceScaling, depthAwareUpsampling: ppssaoValues.depthAwareUpsampling, @@ -199,7 +200,11 @@ export class Composer { worldProximityThreshold: ppssaoValues.worldProximityThreshold, worldProximityFalloff: ppssaoValues.worldProximityFalloff, }); - this.ppssaoPass = new EffectPass(this.camera, this.ppssaoEffect, this.normalTextureEffect); + this.ppssaoPass = new EffectPass( + this.cameraManager.activeCamera, + this.ppssaoEffect, + this.normalTextureEffect, + ); this.ppssaoPass.enabled = ppssaoValues.enabled; this.fxaaEffect = new FXAAEffect(); @@ -212,7 +217,12 @@ export class Composer { intensity: extrasValues.bloom, }); - this.n8aopass = new N8SSAOPass(this.scene, this.camera, this.width, this.height); + this.n8aopass = new N8SSAOPass( + this.scene, + this.cameraManager.activeCamera, + this.width, + this.height, + ); this.n8aopass.configuration.aoRadius = n8ssaoValues.aoRadius; this.n8aopass.configuration.distanceFalloff = n8ssaoValues.distanceFalloff; this.n8aopass.configuration.intensity = n8ssaoValues.intensity; @@ -226,8 +236,8 @@ export class Composer { this.n8aopass.configuration.denoiseRadius = n8ssaoValues.denoiseRadius; this.n8aopass.enabled = n8ssaoValues.enabled; - this.fxaaPass = new EffectPass(this.camera, this.fxaaEffect); - this.bloomPass = new EffectPass(this.camera, this.bloomEffect); + this.fxaaPass = new EffectPass(this.cameraManager.activeCamera, this.fxaaEffect); + this.bloomPass = new EffectPass(this.cameraManager.activeCamera, this.bloomEffect); this.toneMappingEffect = new ToneMappingEffect({ mode: toneMappingValues.mode, @@ -244,7 +254,7 @@ export class Composer { predicationMode: PredicationMode.DEPTH, }); - this.toneMappingPass = new EffectPass(this.camera, this.toneMappingEffect); + this.toneMappingPass = new EffectPass(this.cameraManager.activeCamera, this.toneMappingEffect); this.toneMappingPass.enabled = rendererValues.toneMapping === 5 || rendererValues.toneMapping === 0 ? true : false; @@ -257,7 +267,7 @@ export class Composer { this.gaussGrainEffect.uniforms.amount.value = extrasValues.grain; this.gaussGrainEffect.uniforms.alpha.value = 1.0; - this.smaaPass = new EffectPass(this.camera, this.smaaEffect); + this.smaaPass = new EffectPass(this.cameraManager.activeCamera, this.smaaEffect); this.effectComposer.addPass(this.renderPass); if (ppssaoValues.enabled) { @@ -355,8 +365,8 @@ export class Composer { } this.width = parentElement.clientWidth; this.height = parentElement.clientHeight; - this.camera.aspect = this.width / this.height; - this.camera.updateProjectionMatrix(); + this.cameraManager.activeCamera.aspect = this.width / this.height; + this.cameraManager.activeCamera.updateProjectionMatrix(); this.renderer.setPixelRatio(window.devicePixelRatio); this.resolution.set( this.width * window.devicePixelRatio, @@ -386,11 +396,12 @@ export class Composer { public render(timeManager: TimeManager): void { this.renderer.info.reset(); + this.renderPass.mainCamera = this.cameraManager.activeCamera; this.normalPass.texture.needsUpdate = true; this.gaussGrainEffect.uniforms.time.value = timeManager.time; this.effectComposer.render(); this.renderer.clearDepth(); - this.renderer.render(this.postPostScene, this.camera); + this.renderer.render(this.postPostScene, this.cameraManager.activeCamera); } public updateSkyboxRotation() { diff --git a/packages/3d-web-experience-client/src/Networked3dWebExperienceClient.ts b/packages/3d-web-experience-client/src/Networked3dWebExperienceClient.ts index bb79b61e..29e0e6f7 100644 --- a/packages/3d-web-experience-client/src/Networked3dWebExperienceClient.ts +++ b/packages/3d-web-experience-client/src/Networked3dWebExperienceClient.ts @@ -17,6 +17,7 @@ import { ErrorScreen, getSpawnPositionInsideCircle, GroundPlane, + Key, KeyInputManager, LoadingScreen, LoadingScreenConfig, @@ -173,7 +174,7 @@ export class Networked3dWebExperienceClient { this.composer = new Composer({ scene: this.scene, - camera: this.cameraManager.camera, + cameraManager: this.cameraManager, spawnSun: true, environmentConfiguration: this.config.environmentConfiguration, }); @@ -253,6 +254,11 @@ export class Networked3dWebExperienceClient { }, }); + this.keyInputManager.createKeyBinding(Key.C, () => { + this.cameraManager.toggleFlyCamera(); + this.composer.fitContainer(); + }); + this.characterManager = new CharacterManager({ composer: this.composer, characterModelLoader: this.characterModelLoader, From 1f3f6db0092230033796e80db2056f02abf048a6 Mon Sep 17 00:00:00 2001 From: TheCodeTherapy Date: Tue, 3 Dec 2024 23:06:14 +0000 Subject: [PATCH 2/2] adds toggleable config to enable orbial camera --- .../multi-user-3d-web-experience/client/src/index.ts | 1 + .../src/Networked3dWebExperienceClient.ts | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/example/multi-user-3d-web-experience/client/src/index.ts b/example/multi-user-3d-web-experience/client/src/index.ts index 9bf80df7..80e1d22c 100644 --- a/example/multi-user-3d-web-experience/client/src/index.ts +++ b/example/multi-user-3d-web-experience/client/src/index.ts @@ -34,6 +34,7 @@ const app = new Networked3dWebExperienceClient(holder, { avatarConfiguration: { availableAvatars: [], }, + allowOrbitalCamera: true, loadingScreen: { background: "#424242", color: "#ffffff", diff --git a/packages/3d-web-experience-client/src/Networked3dWebExperienceClient.ts b/packages/3d-web-experience-client/src/Networked3dWebExperienceClient.ts index 29e0e6f7..e43f27f6 100644 --- a/packages/3d-web-experience-client/src/Networked3dWebExperienceClient.ts +++ b/packages/3d-web-experience-client/src/Networked3dWebExperienceClient.ts @@ -77,6 +77,7 @@ type MMLDocumentConfiguration = { export type Networked3dWebExperienceClientConfig = { userNetworkAddress: string; sessionToken: string; + allowOrbitalCamera?: boolean; chatVisibleByDefault?: boolean; userNameToColorOptions?: StringToHslOptions; animationConfig: AnimationConfig; @@ -254,10 +255,12 @@ export class Networked3dWebExperienceClient { }, }); - this.keyInputManager.createKeyBinding(Key.C, () => { - this.cameraManager.toggleFlyCamera(); - this.composer.fitContainer(); - }); + if (this.config.allowOrbitalCamera) { + this.keyInputManager.createKeyBinding(Key.C, () => { + this.cameraManager.toggleFlyCamera(); + this.composer.fitContainer(); + }); + } this.characterManager = new CharacterManager({ composer: this.composer,