Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Movable Camera pivot point Fix #265 #266

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 47 additions & 9 deletions libs/ff-three/source/CameraController.ts
Original file line number Diff line number Diff line change
@@ -10,6 +10,9 @@ import {
Vector3,
Matrix4,
Box3,
Spherical,
Euler,
Quaternion,
} from "three";

import math from "@ff/core/math";
@@ -30,6 +33,9 @@ const _mat4 = new Matrix4();
const _box3 = new Box3();
const _vec3a = new Vector3();
const _vec3b = new Vector3();
const _vec3c = new Vector3();
const _eua = new Euler();
const _quat = new Quaternion();

enum EControllerMode { Orbit, FirstPerson }
enum EManipMode { Off, Pan, Orbit, Dolly, Zoom, PanDolly, Roll }
@@ -42,6 +48,7 @@ export default class CameraController implements IManip

orbit = new Vector3(0, 0, 0);
offset = new Vector3(0, 0, 50);
pivot = new Vector3(0, 0, 0);

minOrbit = new Vector3(-90, -Infinity, -Infinity);
maxOrbit = new Vector3(90, Infinity, Infinity);
@@ -146,15 +153,22 @@ export default class CameraController implements IManip
this.viewportHeight = height;
}

/**
* Copy the object's matrix into the controller's properties
* effectively the inverse operation of updateCamera
*/
updateController(object?: Object3D, adaptLimits?: boolean)
{
const camera = this.camera;
object = object || camera;

const orbit = this.orbit;
const offset = this.offset;
threeMath.decomposeOrbitMatrix(object.matrix, orbit, offset);
this.orbit.multiplyScalar(threeMath.RAD2DEG);
object.matrix.decompose(_vec3b, _quat, _vec3c);
//Rotation
_eua.setFromQuaternion(_quat, "YXZ");
_vec3a.setFromEuler(_eua).multiplyScalar(threeMath.RAD2DEG);
this.orbit.copy(_vec3a);
this.offset.copy(_vec3b.sub(this.pivot).applyQuaternion(_quat.invert()));

if (adaptLimits) {
this.minOffset.min(offset);
@@ -177,15 +191,32 @@ export default class CameraController implements IManip
return;
}


// _vec3a.copy(this.orbit).multiplyScalar(math.DEG2RAD);
// _eua.setFromVector3(_vec3a, "YXZ");
// _quat.setFromEuler(_eua);
// //Position, relative to pivot point
// _vec3b.copy(this.offset).applyEuler(_eua).add(this.pivot);
// //Keep scale
// _vec3c.setFromMatrixScale(object.matrix);
// //Compose everything
// object.matrix.compose(_vec3b, _quat, _vec3c);


// rotate box to camera space
_vec3a.copy(this.orbit).multiplyScalar(math.DEG2RAD);
_quat.setFromEuler(_eua.setFromVector3(_vec3a));
_vec3b.setScalar(0);
threeMath.composeOrbitMatrix(_vec3a, _vec3b, _mat4);

_vec3c.setScalar(1);
//Ignore the pivot point for now. Rotate the box into camera space
_mat4.compose(_vec3b, _quat, _vec3c);
_box3.copy(box).applyMatrix4(_mat4.transpose());
_box3.getSize(_vec3a);
_box3.getCenter(_vec3b);

_vec3c.copy(this.pivot).applyMatrix4(_mat4);
_vec3b.sub(_vec3c);

offset.x = _vec3b.x;
offset.y = _vec3b.y;

@@ -221,7 +252,17 @@ export default class CameraController implements IManip
}

_vec3a.copy(this.orbit).multiplyScalar(math.DEG2RAD);
_vec3b.copy(this.offset);
_eua.setFromVector3(_vec3a, "YXZ");
_quat.setFromEuler(_eua);
//Position, relative to pivot point
_vec3b.copy(this.offset).applyEuler(_eua).add(this.pivot);
//Keep scale
_vec3c.setFromMatrixScale(object.matrix);
//Compose everything
object.matrix.compose(_vec3b, _quat, _vec3c);


object.matrixWorldNeedsUpdate = true;

if (camera.isOrthographicCamera) {
_vec3b.z = this.maxOffset.z; // fixed distance = maxOffset.z
@@ -230,9 +271,6 @@ export default class CameraController implements IManip
camera.updateProjectionMatrix();
}

threeMath.composeOrbitMatrix(_vec3a, _vec3b, object.matrix);
object.matrixWorldNeedsUpdate = true;

return true;
}

3 changes: 3 additions & 0 deletions libs/ff-three/source/math.ts
Original file line number Diff line number Diff line change
@@ -37,6 +37,9 @@ const math = {
DEG2RAD: 0.01745329251994329576923690768489,
RAD2DEG: 57.295779513082320876798154814105,

/**
* one-step orbit matrix composition when the pivot point is (0, 0, 0).
*/
composeOrbitMatrix: function(orientation: Vector3, offset: Vector3, result?: Matrix4): Matrix4
{
const pitch = orientation.x;
8 changes: 5 additions & 3 deletions source/client/components/CVAnnotationsTask.ts
Original file line number Diff line number Diff line change
@@ -162,13 +162,15 @@ export default class CVAnnotationsTask extends CVTask
{
const machine = this._machine;
const props = machine.getTargetProperties();
const orbitIdx = props.findIndex((elem) => {return elem.name == "Orbit"});
const offsetIdx = props.findIndex((elem) => {return elem.name == "Offset"});
const retainIdx = [];
for(let i = 0; i < props.length; i++) {
if(["Pivot", "Orbit", "Offset"].includes(props[i].name)) retainIdx.push(i);
}

// set non camera properties to null to skip them
const values = machine.getCurrentValues();
values.forEach((v, idx) => {
if(idx != orbitIdx && idx != offsetIdx) {
if(!retainIdx.includes(idx)) {
values[idx] = null;
}
});
97 changes: 81 additions & 16 deletions source/client/components/CVOrbitNavigation.ts
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@
* limitations under the License.
*/

import { Box3 } from "three";
import { Box3, Euler, Matrix4, Quaternion, Vector3 } from "three";

import CObject3D, { Node, types } from "@ff/scene/components/CObject3D";

@@ -25,11 +25,14 @@ import CScene, { IRenderContext } from "@ff/scene/components/CScene";
import CTransform, { ERotationOrder } from "@ff/scene/components/CTransform";
import { EProjection } from "@ff/three/UniversalCamera";

import { INavigation } from "client/schema/setup";
import { ENavigationType, TNavigationType, INavigation } from "client/schema/setup";

import CVScene from "./CVScene";
import CVAssetManager from "./CVAssetManager";
import CVARManager from "./CVARManager";
import CVModel2 from "./CVModel2";
import { getMeshTransform } from "client/utils/Helpers";
import { DEG2RAD, RAD2DEG } from "three/src/math/MathUtils";

////////////////////////////////////////////////////////////////////////////////

@@ -72,14 +75,14 @@ export default class CVOrbitNavigation extends CObject3D
promptEnabled: types.Boolean("Settings.PromptEnabled", true),
isInUse: types.Boolean("Camera.IsInUse", false),
preset: types.Enum("Camera.ViewPreset", EViewPreset, EViewPreset.None),
projection: types.Enum("Camera.Projection", EProjection, EProjection.Perspective),
lightsFollowCamera: types.Boolean("Navigation.LightsFollowCam", true),
autoRotation: types.Boolean("Navigation.AutoRotation", false),
autoRotationSpeed: types.Number("Navigation.AutoRotationSpeed", 10),
zoomExtents: types.Event("Settings.ZoomExtents"),
autoZoom: types.Boolean("Settings.AutoZoom", true),
orbit: types.Vector3("Current.Orbit", [ -25, -25, 0 ]),
offset: types.Vector3("Current.Offset", [ 0, 0, 100 ]),
pivot: types.Vector3("Current.Pivot", [ 0, 0, 0 ]),
minOrbit: types.Vector3("Limits.Min.Orbit", [ -90, -Infinity, -Infinity ]),
minOffset: types.Vector3("Limits.Min.Offset", [ -Infinity, -Infinity, 0.1 ]),
maxOrbit: types.Vector3("Limits.Max.Orbit", [ 90, Infinity, Infinity ]),
@@ -98,6 +101,8 @@ export default class CVOrbitNavigation extends CObject3D
private _isAutoZooming = false;
private _autoRotationStartTime = null;
private _initYOrbit = null;
private _projection :EProjection = null;
private _clickDebounce :number = null

constructor(node: Node, id: string)
{
@@ -110,6 +115,7 @@ export default class CVOrbitNavigation extends CObject3D
this.ins.enabled,
this.ins.orbit,
this.ins.offset,
this.ins.pivot,
this.ins.autoZoom,
this.ins.autoRotation,
this.ins.autoRotationSpeed,
@@ -125,6 +131,7 @@ export default class CVOrbitNavigation extends CObject3D
return [
this.ins.orbit,
this.ins.offset,
this.ins.pivot,
];
}

@@ -168,13 +175,8 @@ export default class CVOrbitNavigation extends CObject3D
const cameraComponent = this._scene.activeCameraComponent;
const camera = cameraComponent ? cameraComponent.camera : null;

const { projection, preset, orbit, offset } = ins;
const { preset, orbit, offset, pivot } = ins;

// camera projection
if (cameraComponent && projection.changed) {
camera.setProjection(projection.getValidatedValue());
cameraComponent.ins.projection.setValue(projection.value, true);
}

// camera preset
if (preset.changed && preset.value !== EViewPreset.None) {
@@ -200,9 +202,10 @@ export default class CVOrbitNavigation extends CObject3D
const { minOrbit, minOffset, maxOrbit, maxOffset} = ins;

// orbit, offset and limits
if (orbit.changed || offset.changed) {
if (orbit.changed || offset.changed || pivot.changed) {
controller.orbit.fromArray(orbit.value);
controller.offset.fromArray(offset.value);
controller.pivot.fromArray(pivot.value);
}

if (minOrbit.changed || minOffset.changed || maxOrbit.changed || maxOffset.changed) {
@@ -262,7 +265,8 @@ export default class CVOrbitNavigation extends CObject3D
controller.camera = cameraComponent.camera;

const transform = cameraComponent.transform;
const forceUpdate = this.changed || ins.autoRotation.value || ins.promptActive.value;

const forceUpdate = this.changed || this._projection != cameraComponent.ins.projection.value || ins.autoRotation.value || ins.promptActive.value;

if ((ins.autoRotation.value || ins.promptActive.value) && this._autoRotationStartTime) {
const now = performance.now();
@@ -299,6 +303,7 @@ export default class CVOrbitNavigation extends CObject3D
}

if (controller.updateCamera(transform.object3D, forceUpdate)) {
this._projection = cameraComponent.ins.projection.value;
controller.orbit.toArray(ins.orbit.value);
ins.orbit.set(true);
controller.offset.toArray(ins.offset.value);
@@ -364,6 +369,7 @@ export default class CVOrbitNavigation extends CObject3D
lightsFollowCamera: !!data.lightsFollowCamera,
orbit: orbit.orbit,
offset: orbit.offset,
pivot: orbit.pivot || [ 0, 0, 0 ],
minOrbit: _replaceNull(orbit.minOrbit, -Infinity),
maxOrbit: _replaceNull(orbit.maxOrbit, Infinity),
minOffset: _replaceNull(orbit.minOffset, -Infinity),
@@ -386,6 +392,7 @@ export default class CVOrbitNavigation extends CObject3D
data.orbit = {
orbit: ins.orbit.cloneValue(),
offset: ins.offset.cloneValue(),
pivot: ins.pivot.cloneValue(),
minOrbit: ins.minOrbit.cloneValue(),
maxOrbit: ins.maxOrbit.cloneValue(),
minOffset: ins.minOffset.cloneValue(),
@@ -410,18 +417,76 @@ export default class CVOrbitNavigation extends CObject3D
return;
}

if (this.ins.enabled.value && this._scene.activeCameraComponent) {
if (event.type === "pointer-down" && window.getSelection().type !== "None") {
if (!this.ins.enabled.value || !this._scene.activeCameraComponent) {
return;
}

if (event.type === "pointer-down" ) {
if(window.getSelection().type !== "None"){
window.getSelection().removeAllRanges();
}
this._controller.setViewportSize(viewport.width, viewport.height);
this._controller.onPointer(event);
event.stopPropagation = true;
const ts = event.originalEvent.timeStamp;
if(ts < this._clickDebounce + 400){
this.onDoubleClick({...event, type: "double-click", wheel: 0});
this._clickDebounce = 0;
}else{
this._clickDebounce = ts;
}
}
this._controller.setViewportSize(viewport.width, viewport.height);
this._controller.onPointer(event);

event.stopPropagation = true;
this._hasChanged = true;
}

protected onDoubleClick(event: ITriggerEvent){
if(event.component?.typeName != "CVModel2") return;
const model = event.component as CVModel2;
const meshTransform = getMeshTransform(model.object3D, event.object3D);
let pos = new Vector3(), rot = new Quaternion(), scale = new Vector3();
model.transform.object3D.matrix.decompose(pos, rot, scale)

//Add CVNode's transform
const invMeshTransform = meshTransform.clone().invert();
const bounds = model.localBoundingBox.clone().applyMatrix4(meshTransform);
// add mesh's "pose".
let localPosition = event.view.pickPosition(event as any, bounds)
.applyMatrix4(invMeshTransform) //Add internal transform
.applyMatrix4(model.object3D.matrix) //Add mesh "pose"
.applyMatrix4(model.transform.object3D.matrixWorld) //Add mesh's "transform" (attached CTransform)

const orbit = new Vector3().fromArray(this.ins.orbit.value).multiplyScalar(DEG2RAD);
const pivot = new Vector3().fromArray(this.ins.pivot.value);

//we compute the new orbit and offset.z values to keep the camera in place
let orbitRad = new Euler().setFromVector3(orbit, "YXZ");
let orbitQuat = new Quaternion().setFromEuler(orbitRad);
//Offset from pivot with applied rotation
const offset = new Vector3().fromArray(this.ins.offset.value).applyQuaternion(orbitQuat);
//Current camera absolute position
const camPos = pivot.clone().add(offset);
//We want the camera position to stay the same with the new parameters
//First we need to get the path from the camera to the new pivot
const clickToCam = camPos.clone().sub(localPosition);
//We then use it to "look at" the new pivot
orbitQuat.setFromUnitVectors(
new Vector3(0, 0, 1),
clickToCam.clone().normalize(),
);

//Rotation
orbitRad.setFromQuaternion(orbitQuat, "YXZ");
const orbitAngles = new Vector3().setFromEuler(orbitRad).multiplyScalar(RAD2DEG);


//New pivot is straight-up where the user clicked
this.ins.pivot.setValue(localPosition.toArray());
//We always keep roll as-it-was because it tends to add up in disorienting ways
this.ins.orbit.setValue([orbitAngles.x, orbitAngles.y, this.ins.orbit.value[2]]);
this.ins.offset.setValue([0, 0, clickToCam.length()]);
}

protected onTrigger(event: ITriggerEvent)
{
const viewport = event.viewport;
2 changes: 1 addition & 1 deletion source/client/components/CVViewTool.ts
Original file line number Diff line number Diff line change
@@ -63,7 +63,7 @@ export class ViewToolView extends ToolView<CVViewTool>
const navigation = document.setup.navigation;
const language = document.setup.language;

const projection = navigation.ins.projection;
const projection = navigation.scene.activeCameraComponent.ins.projection;
const preset = navigation.ins.preset;
const zoom = navigation.ins.zoomExtents;

38 changes: 35 additions & 3 deletions source/client/schema/json/setup.schema.json
Original file line number Diff line number Diff line change
@@ -6,6 +6,20 @@
"description": "Tours and settings for explorer documents (background, interface, etc.)",

"definitions": {
"vector3nullable": {
"description": "3-component vector where each member can be null",
"$id": "#vector3",
"type": "array",
"items": {
"oneOf":[
{"type": "number"},
{"type": "null"}
]
},
"minItems": 3,
"maxItems": 3,
"default": [ 0, 0, 0 ]
},
"viewer": {
"type": "object",
"properties": {
@@ -101,11 +115,29 @@
"type": "boolean"
},
"orbit": {
"$comment": "TODO: Implement",

"type": "object",
"properties": {

"orbit":{
"$ref": "./common.schema.json#/definitions/vector3"
},
"offset": {
"$ref": "./common.schema.json#/definitions/vector3"
},
"pivot": {
"$ref": "./common.schema.json#/definitions/vector3"
},
"minOrbit": {
"$ref": "#/definitions/vector3nullable"
},
"maxOrbit": {
"$ref": "#/definitions/vector3nullable"
},
"minOffset": {
"$ref": "#/definitions/vector3nullable"
},
"maxOffset": {
"$ref": "#/definitions/vector3nullable"
}
}
},
"walk": {
1 change: 1 addition & 0 deletions source/client/schema/setup.ts
Original file line number Diff line number Diff line change
@@ -96,6 +96,7 @@ export interface IOrbitNavigation
{
orbit: number[];
offset: number[];
pivot?: number[];
minOrbit: number[];
maxOrbit: number[];
minOffset: number[];
3 changes: 3 additions & 0 deletions source/client/ui/SceneView.ts
Original file line number Diff line number Diff line change
@@ -71,6 +71,7 @@ export default class SceneView extends SystemView
this.ownerDocument.addEventListener("pointermove", this.manipTarget.onPointerMove); // To catch out of frame drag releases
this.ownerDocument.addEventListener("pointerup", this.onPointerUpOrCancel); // To catch out of frame drag releases
this.ownerDocument.addEventListener("pointercancel", this.onPointerUpOrCancel); // To catch out of frame drag releases
this.addEventListener("dblclick", this.manipTarget.onDoubleClick);
this.addEventListener("wheel", this.manipTarget.onWheel);
this.addEventListener("contextmenu", this.manipTarget.onContextMenu);
this.addEventListener("keydown", this.manipTarget.onKeyDown);
@@ -168,6 +169,7 @@ export default class SceneView extends SystemView
this.ownerDocument.addEventListener("pointermove", this.manipTarget.onPointerMove); // To catch out of frame drag releases
this.ownerDocument.addEventListener("pointerup", this.onPointerUpOrCancel); // To catch out of frame drag releases
this.ownerDocument.addEventListener("pointercancel", this.onPointerUpOrCancel); // To catch out of frame drag releases
this.addEventListener("dblclick", this.manipTarget.onDoubleClick);
this.addEventListener("wheel", this.manipTarget.onWheel);
this.addEventListener("contextmenu", this.manipTarget.onContextMenu);
this.addEventListener("keydown", this.manipTarget.onKeyDown);
@@ -186,6 +188,7 @@ export default class SceneView extends SystemView
this.ownerDocument.removeEventListener("pointermove", this.manipTarget.onPointerMove); // To catch out of frame drag releases
this.ownerDocument.removeEventListener("pointerup", this.onPointerUpOrCancel); // To catch out of frame drag releases
this.ownerDocument.removeEventListener("pointercancel", this.onPointerUpOrCancel); // To catch out of frame drag releases
this.removeEventListener("dblclick", this.manipTarget.onDoubleClick);
this.removeEventListener("wheel", this.manipTarget.onWheel);
this.removeEventListener("contextmenu", this.manipTarget.onContextMenu);
this.removeEventListener("keydown", this.manipTarget.onKeyDown);