diff --git a/package-lock.json b/package-lock.json index 1cff6a5..1cb8ff5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "concurrently": "^9.0.1", "cross-env": "^7.0.3", "eslint": "^8.56.0", - "playcanvas": "^2.1.0", + "playcanvas": "^2.2.1", "qrious": "^4.0.2", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -4118,10 +4118,11 @@ } }, "node_modules/playcanvas": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/playcanvas/-/playcanvas-2.1.0.tgz", - "integrity": "sha512-o8Oat/+edEffVG09oUQnQlEVKYu47S15yDLz9/bkbE5ur9q5JsmPbOiCn7ds7kud60opvPb7CkQnKuS+8tlN+w==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/playcanvas/-/playcanvas-2.2.1.tgz", + "integrity": "sha512-K56A1T7AaQlHNiCmKk2KXnV12H/FCE51TXETPc6RlhMRDjS85nqs/lP6NVrTTg287oJRp6DtOvhoStgkHJMzFA==", "dev": true, + "license": "MIT", "dependencies": { "@types/webxr": "^0.5.16", "@webgpu/types": "^0.1.40" diff --git a/package.json b/package.json index 2917ed9..e797b3f 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "concurrently": "^9.0.1", "cross-env": "^7.0.3", "eslint": "^8.56.0", - "playcanvas": "^2.1.0", + "playcanvas": "^2.2.1", "qrious": "^4.0.2", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/rollup.config.mjs b/rollup.config.mjs index 9faa3ec..cef1ab2 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -1,17 +1,18 @@ // official rollup plugins +import path from 'path'; + import alias from '@rollup/plugin-alias'; +import commonjs from '@rollup/plugin-commonjs'; import image from '@rollup/plugin-image'; -import commonjs from "@rollup/plugin-commonjs"; -import json from "@rollup/plugin-json"; -import path from 'path'; -import resolve from "@rollup/plugin-node-resolve"; +import json from '@rollup/plugin-json'; +import resolve from '@rollup/plugin-node-resolve'; import replace from '@rollup/plugin-replace'; -import sass from 'rollup-plugin-sass'; import terser from '@rollup/plugin-terser'; -import typescript from "@rollup/plugin-typescript"; +import typescript from '@rollup/plugin-typescript'; +import sass from 'rollup-plugin-sass'; // custom plugins -import { copyAndWatch } from "./plugins/copy-and-watch.mjs"; +import { copyAndWatch } from './plugins/copy-and-watch.mjs'; // debug, profile, release const BUILD_TYPE = process.env.BUILD_TYPE || 'release'; @@ -22,16 +23,16 @@ const ENGINE_PATH = path.resolve(ENGINE_DIR, 'build', ENGINE_NAME); const PCUI_DIR = path.resolve(process.env.PCUI_PATH || 'node_modules/@playcanvas/pcui'); const BLUE_OUT = '\x1b[34m'; -const BOLD_OUT = `\x1b[1m`; -const REGULAR_OUT = `\x1b[22m`; -const RESET_OUT = `\x1b[0m`; +const BOLD_OUT = '\x1b[1m'; +const REGULAR_OUT = '\x1b[22m'; +const RESET_OUT = '\x1b[0m'; const title = [ - `Building PlayCanvas Model Viewer`, + 'Building PlayCanvas Model Viewer', `type ${BOLD_OUT}${BUILD_TYPE}${REGULAR_OUT}`, `engine ${BOLD_OUT}${ENGINE_DIR}${REGULAR_OUT}`, `pcui ${BOLD_OUT}${PCUI_DIR}${REGULAR_OUT}` -].map(l => `${BLUE_OUT}${l}`).join(`\n`); +].map(l => `${BLUE_OUT}${l}`).join('\n'); console.log(`${BLUE_OUT}${title}${RESET_OUT}\n`); const TARGETS = [ @@ -73,6 +74,7 @@ export default { image({ dom: true }), alias({ entries: { + 'playcanvas/scripts': path.resolve(ENGINE_DIR, 'scripts'), 'playcanvas': ENGINE_PATH, '@playcanvas/pcui': PCUI_DIR } diff --git a/src/app.ts b/src/app.ts index 9eb4d1b..4230620 100644 --- a/src/app.ts +++ b/src/app.ts @@ -15,7 +15,8 @@ import { TextureHandler, XrManager, GSplatComponentSystem, - GSplatHandler + GSplatHandler, + ScriptComponentSystem } from 'playcanvas'; class App extends AppBase { @@ -38,9 +39,7 @@ class App extends AppBase { appOptions.assetPrefix = options.assetPrefix; appOptions.scriptsOrder = options.scriptsOrder; - // @ts-ignore appOptions.lightmapper = Lightmapper; - // @ts-ignore appOptions.xr = XrManager; this.init(appOptions); @@ -52,27 +51,20 @@ class App extends AppBase { RenderComponentSystem, CameraComponentSystem, LightComponentSystem, - GSplatComponentSystem + GSplatComponentSystem, + ScriptComponentSystem ]; } addResourceHandles(appOptions: AppOptions) { appOptions.resourceHandlers = [ - // @ts-ignore RenderHandler, - // @ts-ignore AnimClipHandler, - // @ts-ignore AnimStateGraphHandler, - // @ts-ignore TextureHandler, - // @ts-ignore CubemapHandler, - // @ts-ignore BinaryHandler, - // @ts-ignore ContainerHandler, - // @ts-ignore GSplatHandler ]; } diff --git a/src/cameras/base-camera.ts b/src/cameras/base-camera.ts deleted file mode 100644 index 7827484..0000000 --- a/src/cameras/base-camera.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { Entity, Vec3, Vec2, math } from 'playcanvas'; - -type PointerMoveEvent = PointerEvent & { - mozMovementX: number; - mozMovementY: number; - webkitMovementX: number; - webkitMovementY: number; -} - -const LOOK_MAX_ANGLE = 90; - -abstract class BaseCamera { - entity: Entity; - - target: HTMLElement = document.documentElement; - - sceneSize: number = 100; - - lookSensitivity: number = 0.2; - - lookDamping: number = 0.97; - - moveDamping: number = 0.98; - - protected _camera: Entity = null; - - protected _origin: Vec3 = new Vec3(0, 1, 0); - - protected _position: Vec3 = new Vec3(); - - protected _dir: Vec2 = new Vec2(); - - protected _angles: Vec3 = new Vec3(); - - constructor(target: HTMLElement, options: Record = {}) { - this.entity = new Entity(options.name ?? 'base-camera'); - this.target = target; - this.sceneSize = options.sceneSize ?? this.sceneSize; - this.lookSensitivity = options.lookSensitivity ?? this.lookSensitivity; - this.lookDamping = options.lookDamping ?? this.lookDamping; - this.moveDamping = options.moveDamping ?? this.moveDamping; - - this._onPointerDown = this._onPointerDown.bind(this); - this._onPointerMove = this._onPointerMove.bind(this); - this._onPointerUp = this._onPointerUp.bind(this); - } - - private _smoothLook(dt: number) { - const lerpRate = 1 - Math.pow(this.lookDamping, dt * 1000); - this._angles.x = math.lerp(this._angles.x, this._dir.x, lerpRate); - this._angles.y = math.lerp(this._angles.y, this._dir.y, lerpRate); - this.entity.setEulerAngles(this._angles); - } - - private _smoothMove(dt: number) { - this._position.lerp(this._position, this._origin, 1 - Math.pow(this.moveDamping, dt * 1000)); - this.entity.setPosition(this._position); - } - - private _onContextMenu(event: MouseEvent) { - event.preventDefault(); - } - - protected abstract _onPointerDown(event: PointerEvent): void - - protected abstract _onPointerMove(event: PointerMoveEvent): void - - protected abstract _onPointerUp(event: PointerEvent): void - - protected _look(event: PointerMoveEvent) { - if (event.target !== this.target) { - return; - } - const movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0; - const movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0; - this._dir.x = math.clamp(this._dir.x - movementY * this.lookSensitivity, -LOOK_MAX_ANGLE, LOOK_MAX_ANGLE); - this._dir.y -= movementX * this.lookSensitivity; - } - - attach(camera: Entity) { - this._camera = camera; - this._camera.setLocalEulerAngles(0, 0, 0); - - window.addEventListener('pointerdown', this._onPointerDown); - window.addEventListener('pointermove', this._onPointerMove); - window.addEventListener('pointerup', this._onPointerUp); - window.addEventListener('contextmenu', this._onContextMenu); - - this.entity.addChild(camera); - } - - detach() { - window.removeEventListener('pointermove', this._onPointerMove); - window.removeEventListener('pointerdown', this._onPointerDown); - window.removeEventListener('pointerup', this._onPointerUp); - window.removeEventListener('contextmenu', this._onContextMenu); - - this.entity.removeChild(this._camera); - this._camera = null; - - this._dir.x = this._angles.x; - this._dir.y = this._angles.y; - - this._origin.copy(this._position); - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - update(dt: number) { - if (!this._camera) { - return; - } - - this._smoothLook(dt); - this._smoothMove(dt); - } -} - -export { BaseCamera }; diff --git a/src/cameras/multi-camera.ts b/src/cameras/multi-camera.ts deleted file mode 100644 index 145af4f..0000000 --- a/src/cameras/multi-camera.ts +++ /dev/null @@ -1,367 +0,0 @@ -import { Entity, Vec2, Vec3, Ray, Plane, math } from 'playcanvas'; - -import { BaseCamera } from './base-camera'; - -type PointerMoveEvent = PointerEvent & { - mozMovementX: number; - mozMovementY: number; - webkitMovementX: number; - webkitMovementY: number; -} - -const tmpVa = new Vec2(); -const tmpV1 = new Vec3(); -const tmpV2 = new Vec3(); -const tmpR1 = new Ray(); -const tmpP1 = new Plane(); - -const PASSIVE: any = { passive: false }; - -class MultiCamera extends BaseCamera { - lookSensitivity: number = 0.2; - - lookDamping: number = 0.97; - - moveDamping: number = 0.98; - - pinchSpeed: number = 5; - - wheelSpeed: number = 0.005; - - zoomMin: number = 0.001; - - zoomMax: number = 10; - - zoomScaleMin: number = 0.01; - - moveSpeed: number = 2; - - sprintSpeed: number = 4; - - crouchSpeed: number = 1; - - private _zoomDist: number = 0; - - private _cameraDist: number = 0; - - private _pointerEvents: Map = new Map(); - - private _lastPinchDist: number = -1; - - private _lastPosition = new Vec2(); - - private _panning: boolean = false; - - private _flying: boolean = false; - - private _key = { - forward: false, - backward: false, - left: false, - right: false, - up: false, - down: false, - sprint: false, - crouch: false - }; - - constructor(target: HTMLElement, options: Record = {}) { - super(target, options); - - this.pinchSpeed = options.pinchSpeed ?? this.pinchSpeed; - this.wheelSpeed = options.wheelSpeed ?? this.wheelSpeed; - this.zoomMin = options.zoomMin ?? this.zoomMin; - this.zoomMax = options.zoomMax ?? this.zoomMax; - this.moveSpeed = options.moveSpeed ?? this.moveSpeed; - this.sprintSpeed = options.sprintSpeed ?? this.sprintSpeed; - this.crouchSpeed = options.crouchSpeed ?? this.crouchSpeed; - - this._onWheel = this._onWheel.bind(this); - this._onKeyDown = this._onKeyDown.bind(this); - this._onKeyUp = this._onKeyUp.bind(this); - } - - protected _onPointerDown(event: PointerEvent) { - if (!this._camera) { - return; - } - this._pointerEvents.set(event.pointerId, event); - if (this._pointerEvents.size === 2) { - this._lastPinchDist = this._getPinchDist(); - this._getMidPoint(this._lastPosition); - this._panning = true; - } - if (event.shiftKey || event.button === 1) { - this._lastPosition.set(event.clientX, event.clientY); - this._panning = true; - } - if (event.button === 2) { - this._zoomDist = this._cameraDist; - this._origin.copy(this._camera.getPosition()); - this._position.copy(this._origin); - this._camera.setLocalPosition(0, 0, 0); - this._flying = true; - } - } - - protected _onPointerMove(event: PointerMoveEvent) { - if (this._pointerEvents.size === 0) { - return; - } - - this._pointerEvents.set(event.pointerId, event); - - if (this._pointerEvents.size === 1) { - if (this._panning) { - // mouse pan - this._pan(tmpVa.set(event.clientX, event.clientY)); - } else { - super._look(event); - } - return; - } - - if (this._pointerEvents.size === 2) { - // touch pan - this._pan(this._getMidPoint(tmpVa)); - - // pinch zoom - const pinchDist = this._getPinchDist(); - if (this._lastPinchDist > 0) { - this._zoom((this._lastPinchDist - pinchDist) * this.pinchSpeed); - } - this._lastPinchDist = pinchDist; - } - - } - - protected _onPointerUp(event: PointerEvent) { - this._pointerEvents.delete(event.pointerId); - if (this._pointerEvents.size < 2) { - this._lastPinchDist = -1; - this._panning = false; - } - if (this._panning) { - this._panning = false; - } - if (this._flying) { - tmpV1.copy(this.entity.forward).mulScalar(this._zoomDist); - this._origin.add(tmpV1); - this._position.add(tmpV1); - this._flying = false; - } - } - - private _onWheel(event: WheelEvent) { - event.preventDefault(); - this._zoom(event.deltaY); - } - - private _onKeyDown(event: KeyboardEvent) { - event.stopPropagation(); - switch (event.key.toLowerCase()) { - case 'w': - this._key.forward = true; - break; - case 's': - this._key.backward = true; - break; - case 'a': - this._key.left = true; - break; - case 'd': - this._key.right = true; - break; - case 'q': - this._key.up = true; - break; - case 'e': - this._key.down = true; - break; - case 'shift': - this._key.sprint = true; - break; - case 'control': - this._key.crouch = true; - break; - } - } - - private _onKeyUp(event: KeyboardEvent) { - event.stopPropagation(); - switch (event.key.toLowerCase()) { - case 'w': - this._key.forward = false; - break; - case 's': - this._key.backward = false; - break; - case 'a': - this._key.left = false; - break; - case 'd': - this._key.right = false; - break; - case 'q': - this._key.up = false; - break; - case 'e': - this._key.down = false; - break; - case 'shift': - this._key.sprint = false; - break; - case 'control': - this._key.crouch = false; - break; - } - } - - private _move(dt: number) { - tmpV1.set(0, 0, 0); - if (this._key.forward) { - tmpV1.add(this.entity.forward); - } - if (this._key.backward) { - tmpV1.sub(this.entity.forward); - } - if (this._key.left) { - tmpV1.sub(this.entity.right); - } - if (this._key.right) { - tmpV1.add(this.entity.right); - } - if (this._key.up) { - tmpV1.add(this.entity.up); - } - if (this._key.down) { - tmpV1.sub(this.entity.up); - } - tmpV1.normalize(); - const speed = this._key.crouch ? this.crouchSpeed : this._key.sprint ? this.sprintSpeed : this.moveSpeed; - tmpV1.mulScalar(this.sceneSize * speed * dt); - this._origin.add(tmpV1); - } - - private _getMidPoint(out: Vec2) { - const [a, b] = this._pointerEvents.values(); - const dx = a.clientX - b.clientX; - const dy = a.clientY - b.clientY; - return out.set(b.clientX + dx * 0.5, b.clientY + dy * 0.5); - } - - private _getPinchDist() { - const [a, b] = this._pointerEvents.values(); - const dx = a.clientX - b.clientX; - const dy = a.clientY - b.clientY; - return Math.sqrt(dx * dx + dy * dy); - } - - private _screenToWorldPan(pos: Vec2, point: Vec3) { - const mouseW = this._camera.camera.screenToWorld(pos.x, pos.y, 1); - const cameraPos = this._camera.getPosition(); - - const focusDirScaled = tmpV1.copy(this.entity.forward).mulScalar(this._zoomDist); - const focalPos = tmpV2.add2(cameraPos, focusDirScaled); - const planeNormal = focusDirScaled.mulScalar(-1).normalize(); - - const plane = tmpP1.setFromPointNormal(focalPos, planeNormal); - const ray = tmpR1.set(cameraPos, mouseW.sub(cameraPos).normalize()); - - plane.intersectsRay(ray, point); - } - - - private _pan(pos: Vec2) { - const start = new Vec3(); - const end = new Vec3(); - - this._screenToWorldPan(this._lastPosition, start); - this._screenToWorldPan(pos, end); - - tmpV1.sub2(start, end); - this._origin.add(tmpV1); - - this._lastPosition.copy(pos); - } - - private _zoom(delta: number) { - const min = this._camera.camera.nearClip + this.zoomMin * this.sceneSize; - const max = this.zoomMax * this.sceneSize; - const scale = math.clamp(this._zoomDist / (max - min), this.zoomScaleMin, 1); - this._zoomDist += (delta * this.wheelSpeed * this.sceneSize * scale); - this._zoomDist = math.clamp(this._zoomDist, min, max); - } - - focus(point: Vec3, start?: Vec3) { - if (!this._camera) { - return; - } - if (!start) { - this._origin.copy(point); - return; - } - - tmpV1.sub2(start, point); - const elev = Math.atan2(tmpV1.y, tmpV1.z) * math.RAD_TO_DEG; - const azim = Math.atan2(tmpV1.x, tmpV1.z) * math.RAD_TO_DEG; - this._dir.set(-elev, -azim); - - this._origin.copy(point); - this._camera.setPosition(start); - this._camera.setLocalEulerAngles(0, 0, 0); - - this._zoomDist = tmpV1.length(); - } - - resetZoom(zoomDist: number = 0) { - this._zoomDist = zoomDist; - } - - attach(camera: Entity) { - super.attach(camera); - - window.addEventListener('wheel', this._onWheel, PASSIVE); - window.addEventListener('keydown', this._onKeyDown, false); - window.addEventListener('keyup', this._onKeyUp, false); - } - - detach() { - super.detach(); - - window.removeEventListener('wheel', this._onWheel, PASSIVE); - window.removeEventListener('keydown', this._onKeyDown, false); - window.removeEventListener('keyup', this._onKeyUp, false); - - this._pointerEvents.clear(); - this._lastPinchDist = -1; - this._panning = false; - this._key = { - forward: false, - backward: false, - left: false, - right: false, - up: false, - down: false, - sprint: false, - crouch: false - }; - } - - update(dt: number) { - if (!this._camera) { - return; - } - - if (!this._flying) { - this._cameraDist = math.lerp(this._cameraDist, this._zoomDist, 1 - Math.pow(this.moveDamping, dt * 1000)); - this._camera.setLocalPosition(0, 0, this._cameraDist); - } - - this._move(dt); - - super.update(dt); - } -} - -export { MultiCamera }; diff --git a/src/viewer.ts b/src/viewer.ts index f76c608..b2c9a04 100644 --- a/src/viewer.ts +++ b/src/viewer.ts @@ -60,9 +60,10 @@ import { Vec4, WebglGraphicsDevice } from 'playcanvas'; +// @ts-ignore +import { MultiCamera } from 'playcanvas/scripts/camera/multi-camera.js'; import { App } from './app'; -import { MultiCamera } from './cameras/multi-camera'; import { DebugLines } from './debug-lines'; import { CreateDropHandler } from './drop-handler'; import { Multiframe } from './multiframe'; @@ -222,15 +223,10 @@ class Viewer { frustumCulling: true, clearColor: new Color(0, 0, 0, 0) }); + camera.addComponent('script'); + this.multiCamera = camera.script.create(MultiCamera); camera.camera.requestSceneColorMap(true); - // create camera controls - this.multiCamera = new MultiCamera(canvas, { - name: 'multi-camera' - }); - app.root.addChild(this.multiCamera.entity); - this.multiCamera.attach(camera); - app.keyboard.on(EVENT_KEYDOWN, (event) => { switch (event.key) { case KEY_F: {