Skip to content

Commit

Permalink
🦄 refactor: Improve CameraController
Browse files Browse the repository at this point in the history
  • Loading branch information
xiangechen committed Oct 18, 2023
1 parent 6c04900 commit d857f3a
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 112 deletions.
1 change: 1 addition & 0 deletions packages/chili-core/src/visual/cameraController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { XYZ } from "../math";

export interface ICameraController {
cameraType: "perspective" | "orthographic";
fitContent(): void;
lookAt(eye: XYZ, target: XYZ): void;
pan(dx: number, dy: number): void;
Expand Down
170 changes: 100 additions & 70 deletions packages/chili-three/src/cameraController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,39 +17,82 @@ import { ThreeView } from "./threeView";
import { ThreeVisualContext } from "./threeVisualContext";

export class CameraController implements ICameraController {
#minZoom: number;
private readonly initialLength: number;
private _target: Vector3 = new Vector3();
private get camera(): OrthographicCamera | PerspectiveCamera {
return this.view.camera;
}
private rotateStart: Vector3 | undefined;

zoomSpeed: number = 0.9;
zoomSpeed: number = 0.1;
rotateSpeed: number = 0.01;
#target: Vector3 = new Vector3();
#rotateStart: Vector3 | undefined;
readonly #perspectiveCamera: PerspectiveCamera;
readonly #orthographic: OrthographicCamera;

#cameraType: "perspective" | "orthographic" = "perspective";
get cameraType(): "perspective" | "orthographic" {
return this.#cameraType;
}
set cameraType(value: "perspective" | "orthographic") {
if (this.#cameraType === value) {
return;
}
this.#cameraType = value;
if (value === "perspective") {
this.updateCamera(this.#orthographic.position, this.#target, false);
} else {
this.updateCamera(this.#perspectiveCamera.position, this.#target, false);
}
}

get scale() {
return this.camera.position.distanceTo(this._target) / this.initialLength;
return this.#orthographic.zoom;
}

get camera(): PerspectiveCamera | OrthographicCamera {
return this.cameraType === "perspective" ? this.#perspectiveCamera : this.#orthographic;
}

constructor(readonly view: ThreeView) {
this.initialLength = this.camera.position.length();
this.#minZoom = this.initialLength * 0.01;
this.#perspectiveCamera = this.initPerspectiveCamera(view.container);
this.#orthographic = this.initOrthographicCamera(view.container);
}

private initPerspectiveCamera(container: HTMLElement) {
let k = container.clientWidth / container.clientHeight;
let camera = new PerspectiveCamera(45, k, 0.001, 1e12);
this.initCamera(camera);
return camera;
}

private initOrthographicCamera(container: HTMLElement) {
let camera = new OrthographicCamera(
-container.clientWidth * 0.5,
container.clientWidth * 0.5,
container.clientHeight * 0.5,
-container.clientHeight * 0.5,
1,
1e12,
);
this.initCamera(camera);
return camera;
}

private initCamera(camera: PerspectiveCamera | OrthographicCamera) {
camera.position.set(1500, 1500, 1500);
camera.lookAt(new Vector3());
camera.updateMatrixWorld(true);
return camera;
}

pan(dx: number, dy: number): void {
let { x, y } = this.view.worldToScreen(ThreeHelper.toXYZ(this._target));
let plane = this.cameraPlane(this._target);
let { x, y } = this.view.worldToScreen(ThreeHelper.toXYZ(this.#target));
let plane = this.cameraPlane(this.#target);
let p1 = this.planeIntersectCameraLineByMouse(x, y, plane);
let p2 = this.planeIntersectCameraLineByMouse(x - dx, y - dy, plane);
let vec = p2.sub(p1);
this.updateCamera(this.camera.position.add(vec), this._target.add(vec));
this.updateCamera(this.camera.position.add(vec), this.#target.add(vec));
}

private updateCamera(position: Vector3, target: Vector3, setTarget: boolean = true) {
if (setTarget) this._target.copy(target);
if (setTarget) this.#target.copy(target);
this.camera.position.copy(position);
this.camera.lookAt(this._target);
this.camera.lookAt(this.#target);
this.camera.updateProjectionMatrix();
}

Expand All @@ -74,10 +117,8 @@ export class CameraController implements ICameraController {
} else {
this.camera.getWorldDirection(vec);
}
return new Line3(
position.clone().add(vec.multiplyScalar(1e19)),
position.clone().sub(vec.multiplyScalar(1e19)),
);
vec.multiplyScalar(1e19);
return new Line3(position, position.clone().sub(vec));
}

private cameraPlane(position: Vector3) {
Expand All @@ -88,53 +129,53 @@ export class CameraController implements ICameraController {
}

startRotate(x: number, y: number): void {
// TODO
let shape = this.view.detected(ShapeType.Shape, x, y).at(0)?.owner;
if (shape instanceof ThreeShape) {
this.rotateStart = new Vector3();
this.#rotateStart = new Vector3();
let box = new Box3();
box.setFromObject(shape);
box.getCenter(this.rotateStart);
box.getCenter(this.#rotateStart);
} else {
this.rotateStart = undefined;
this.#rotateStart = undefined;
}
}

rotate(dx: number, dy: number): void {
let start = this.rotateStart ?? this._target;
let start = this.#rotateStart ?? this.#target;
let vecPos = this.camera.position.clone().sub(start);
let xvec = this.camera.up.clone().cross(vecPos).normalize();
let yvec = vecPos.clone().cross(xvec).normalize();
let matrixX = new Matrix4().makeRotationAxis(xvec, -dy * this.rotateSpeed);
let matrixY = new Matrix4().makeRotationAxis(yvec, -dx * this.rotateSpeed);
let matrix = new Matrix4().multiplyMatrices(matrixY, matrixX);
let position = ThreeHelper.transformVector(matrix, vecPos).add(start);
if (this.rotateStart) {
let vecTrt = this._target.clone().sub(this.camera.position);
this._target = ThreeHelper.transformVector(matrix, vecTrt).add(position);
if (this.#rotateStart) {
let vecTrt = this.#target.clone().sub(this.camera.position);
this.#target = ThreeHelper.transformVector(matrix, vecTrt).add(position);
}
this.camera.up.copy(this.camera.up.transformDirection(matrix));
this.updateCamera(position, this._target, false);
this.updateCamera(position, this.#target, false);
}

fitContent(): void {
let context = this.view.viewer.visual.context as ThreeVisualContext;
let vectors = ThreeHelper.cameraVectors(this.camera);
let rect = this.cameraRect(context.visualShapes, vectors.right, vectors.up);
if (this.camera instanceof PerspectiveCamera) {
let h = Math.max(rect.width / this.camera.aspect, rect.height);
let distance = (0.5 * h) / Math.tan((this.camera.fov * Math.PI) / 180 / 2.0);
let position = rect.center.clone().sub(vectors.direction.clone().multiplyScalar(distance));
this.updateCamera(position, rect.center);
}
if (!rect.width || !rect.height) return;
let h = Math.max(rect.width / this.#perspectiveCamera.aspect, rect.height);
let distance = (0.5 * h) / Math.tan((this.#perspectiveCamera.fov * Math.PI) / 180 / 2.0);
let position = rect.center.clone().sub(vectors.direction.clone().multiplyScalar(distance));
this.#orthographic.zoom = Math.min(
this.view.container.clientWidth / rect.width,
this.view.container.clientHeight / rect.height,
);
this.updateCamera(position, rect.center);
}

private cameraRect(object: Object3D, right: Vector3, up: Vector3) {
let box = this.getSceneBox(object);
let plane = this.cameraPlane(box.center);
let points = box.points.map((point) => this.planeIntersectCameraLine(plane, point));
let [minV, maxV, minH, maxH] = [Infinity, -Infinity, Infinity, -Infinity];
for (const point of points) {
for (const point of box.points) {
let dotV = point.dot(up);
let dotH = point.dot(right);
minV = Math.min(minV, dotV);
Expand Down Expand Up @@ -163,39 +204,28 @@ export class CameraController implements ICameraController {
}

zoom(x: number, y: number, delta: number): void {
let scale = delta > 0 ? this.zoomSpeed : 1 / this.zoomSpeed;
this.#orthographic.zoom *= delta > 0 ? 1 + this.zoomSpeed : 1 - this.zoomSpeed;
let point = this.mouseToWorld(x, y);
if (ThreeHelper.isOrthographicCamera(this.camera)) {
this.zoomOrthographicCamera(point, scale);
} else if (ThreeHelper.isPerspectiveCamera(this.camera)) {
this.zoomPerspectiveCamera(point, scale);
}
}

private zoomOrthographicCamera(point: Vector3, scale: number) {
let vec = point.clone().sub(this._target);
let xvec = new Vector3().setFromMatrixColumn(this.camera.matrix, 0);
let yvec = new Vector3().setFromMatrixColumn(this.camera.matrix, 1);
let x = vec.clone().dot(xvec);
let y = vec.clone().dot(yvec);
let vx = xvec.clone().multiplyScalar(x / scale - x);
let vy = yvec.clone().multiplyScalar(y / scale - y);
let vector = new Vector3().add(vx).add(vy);
this.camera.zoom /= scale;
this.updateCamera(this.camera.position.add(vector), this._target.add(vector));
}

private zoomPerspectiveCamera(point: Vector3, scale: number) {
let direction = this.camera.position.clone().sub(this._target);
let vector = this.camera.position.clone().sub(point).normalize();
let angle = vector.angleTo(direction);
let length = direction.length() * (scale - 1);
if (Math.abs(length) < this.#minZoom) {
length = length < 0 ? -this.#minZoom : this.#minZoom;
let cameraVectors = ThreeHelper.cameraVectors(this.camera);
let scale = delta > 0 ? this.zoomSpeed : -this.zoomSpeed;
let vec: Vector3;
if (this.#cameraType === "orthographic") {
vec = point.clone().sub(this.#target);
} else {
vec = this.camera.position.clone().sub(point);
let angle = vec.angleTo(cameraVectors.direction);
let length = this.camera.position.distanceTo(this.#target);
vec.setLength(length / Math.cos(angle));
}
let moveVector = vector.clone().multiplyScalar(length / Math.cos(angle));
this._target.add(moveVector.clone().sub(direction.clone().setLength(length)));
this.updateCamera(this.camera.position.add(moveVector), this._target, false);
let dx = vec.clone().dot(cameraVectors.right) * scale;
let dy = vec.clone().dot(cameraVectors.up) * scale;
let dz = this.camera.position.distanceTo(this.#target) * scale;
let moveVector = new Vector3()
.add(cameraVectors.right.clone().multiplyScalar(dx))
.add(cameraVectors.up.clone().multiplyScalar(dy));
this.#target.add(moveVector);
moveVector.add(cameraVectors.direction.clone().multiplyScalar(dz));
this.updateCamera(this.camera.position.add(moveVector), this.#target, false);
}

lookAt(eye: XYZ, target: XYZ): void {
Expand Down
63 changes: 21 additions & 42 deletions packages/chili-three/src/threeView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,8 @@ export class ThreeView extends Observable implements IView {
this._name = name;
}

private _camera: OrthographicCamera | PerspectiveCamera;
get camera(): PerspectiveCamera | OrthographicCamera {
return this._camera;
}
set camera(camera: PerspectiveCamera | OrthographicCamera) {
this._camera = camera;
return this.cameraController.camera;
}

constructor(
Expand All @@ -68,33 +64,15 @@ export class ThreeView extends Observable implements IView {
this._name = name;
this._scene = content.scene;
this._workplane = workplane;
this._camera = this.initCamera(container);
this._renderer = this.initRender(container);
this.cameraController = new CameraController(this);
this._renderer = this.initRender(container);
this.animate();
}

get renderer(): Renderer {
return this._renderer;
}

private initCamera(container: HTMLElement) {
let [w, h] = [container.clientWidth, container.clientHeight];
let camera = new PerspectiveCamera(30, w / h, 0.001, 1e12);
// let camera = new OrthographicCamera(
// -w / 2,
// w / 2,
// h / 2,
// -h / 2,
// 0.01,
// 1e12,
// );
camera.position.set(1000, 1000, 1000);
camera.lookAt(new Vector3());
camera.updateMatrixWorld(true);
return camera;
}

protected initRender(container: HTMLElement): Renderer {
let renderer = new WebGLRenderer({
antialias: true,
Expand All @@ -109,7 +87,7 @@ export class ThreeView extends Observable implements IView {
}

toImage(): string {
this._renderer.render(this._scene, this._camera);
this._renderer.render(this._scene, this.camera);
return this.renderer.domElement.toDataURL();
}

Expand Down Expand Up @@ -143,17 +121,17 @@ export class ThreeView extends Observable implements IView {
this.animate();
});
if (this._needsUpdate) {
this._renderer.render(this._scene, this._camera);
this._renderer.render(this._scene, this.camera);
this._needsUpdate = false;
}
}

resize(width: number, heigth: number) {
if (this._camera instanceof PerspectiveCamera) {
this._camera.aspect = width / heigth;
this._camera.updateProjectionMatrix();
} else if (this._camera instanceof OrthographicCamera) {
this._camera.updateProjectionMatrix();
if (this.camera instanceof PerspectiveCamera) {
this.camera.aspect = width / heigth;
this.camera.updateProjectionMatrix();
} else if (this.camera instanceof OrthographicCamera) {
this.camera.updateProjectionMatrix();
}
this._renderer.setSize(width, heigth);
this.update();
Expand All @@ -174,12 +152,12 @@ export class ThreeView extends Observable implements IView {
rayAt(mx: number, my: number): Ray {
let position = this.mouseToWorld(mx, my);
let vec = new Vector3();
if (this._camera instanceof PerspectiveCamera) {
vec = position.clone().sub(this._camera.position).normalize();
} else if (this._camera instanceof OrthographicCamera) {
this._camera.getWorldDirection(vec);
if (this.camera instanceof PerspectiveCamera) {
vec = position.clone().sub(this.camera.position).normalize();
} else if (this.camera instanceof OrthographicCamera) {
this.camera.getWorldDirection(vec);
}
let offset = position.clone().sub(this._camera.position).dot(vec);
let offset = position.clone().sub(this.camera.position).dot(vec);
position = position.clone().sub(vec.clone().multiplyScalar(offset));
return new Ray(ThreeHelper.toXYZ(position), ThreeHelper.toXYZ(vec));
}
Expand All @@ -192,28 +170,28 @@ export class ThreeView extends Observable implements IView {
worldToScreen(point: XYZ): XY {
let cx = this.width / 2;
let cy = this.heigth / 2;
let vec = new Vector3(point.x, point.y, point.z).project(this._camera);
let vec = new Vector3(point.x, point.y, point.z).project(this.camera);
return new XY(Math.round(cx * vec.x + cx), Math.round(-cy * vec.y + cy));
}

direction(): XYZ {
const vec = new Vector3();
this._camera.getWorldDirection(vec);
this.camera.getWorldDirection(vec);
return ThreeHelper.toXYZ(vec);
}

up(): XYZ {
return ThreeHelper.toXYZ(this._camera.up);
return ThreeHelper.toXYZ(this.camera.up);
}

private mouseToWorld(mx: number, my: number) {
let { x, y } = this.screenToCameraRect(mx, my);
return new Vector3(x, y, 0).unproject(this._camera);
return new Vector3(x, y, 0).unproject(this.camera);
}

rectDetected(shapeType: ShapeType, mx1: number, my1: number, mx2: number, my2: number) {
let detecteds: VisualShapeData[] = [];
const selectionBox = new SelectionBox(this._camera, this._scene);
const selectionBox = new SelectionBox(this.camera, this._scene);
const start = this.screenToCameraRect(mx1, my1);
const end = this.screenToCameraRect(mx2, my2);
selectionBox.startPoint.set(start.x, start.y, 0.5);
Expand Down Expand Up @@ -372,7 +350,8 @@ export class ThreeView extends Observable implements IView {

private initRaycaster(mx: number, my: number) {
let ray = this.rayAt(mx, my);
let threshold = Constants.RaycasterThreshold * this.cameraController.scale;
let threshold = Constants.RaycasterThreshold / this.cameraController.scale;
if (threshold < 5) threshold = 5;
let raycaster = new Raycaster(ThreeHelper.fromXYZ(ray.location), ThreeHelper.fromXYZ(ray.direction));
raycaster.params = { Line: { threshold }, Points: { threshold } };
return raycaster;
Expand Down

0 comments on commit d857f3a

Please sign in to comment.