Skip to content

Commit

Permalink
implements the toggleable orbital camera (#184)
Browse files Browse the repository at this point in the history
* implements the toggleable orbital camera

* adds toggleable config to enable orbial camera
  • Loading branch information
TheCodeTherapy authored Dec 18, 2024
1 parent 19bc481 commit 16d1b9c
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions example/multi-user-3d-web-experience/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const app = new Networked3dWebExperienceClient(holder, {
avatarConfiguration: {
availableAvatars: [],
},
allowOrbitalCamera: true,
loadingScreen: {
background: "#424242",
color: "#ffffff",
Expand Down
68 changes: 62 additions & 6 deletions packages/3d-web-client-core/src/camera/CameraManager.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -238,7 +265,8 @@ export class CameraManager {
}

public dispose() {
this.eventHandlerCollection.clear();
this.disposeEventHandlers();
this.orbitControls.dispose();
document.body.style.cursor = "";
}

Expand All @@ -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 {
Expand All @@ -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);
Expand Down
8 changes: 4 additions & 4 deletions packages/3d-web-client-core/src/character/LocalController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/3d-web-client-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
15 changes: 14 additions & 1 deletion packages/3d-web-client-core/src/input/KeyInputManager.ts
Original file line number Diff line number Diff line change
@@ -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<Key, KeyCallback>;

export class KeyInputManager {
private keys = new Map<string, boolean>();
private eventHandlerCollection = new EventHandlerCollection();
private bindings: BindingsType = new Map();

constructor(private shouldCaptureKeyPress: () => boolean = () => true) {
this.eventHandlerCollection.add(document, "keydown", this.onKeyDown.bind(this));
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -91,5 +103,6 @@ export class KeyInputManager {

public dispose() {
this.eventHandlerCollection.clear();
this.bindings.clear();
}
}
43 changes: 27 additions & 16 deletions packages/3d-web-client-core/src/rendering/composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -54,7 +55,7 @@ import { N8SSAOPass } from "./post-effects/n8-ssao/N8SSAOPass";

type ComposerContructorArgs = {
scene: Scene;
camera: PerspectiveCamera;
cameraManager: CameraManager;
spawnSun: boolean;
environmentConfiguration?: EnvironmentConfiguration;
};
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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",
Expand All @@ -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,
Expand All @@ -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();
Expand All @@ -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;
Expand All @@ -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,
Expand All @@ -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;

Expand All @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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() {
Expand Down
Loading

0 comments on commit 16d1b9c

Please sign in to comment.