Improved quest editor camera controls.

This commit is contained in:
Daan Vanden Bosch 2020-10-01 22:54:11 +02:00
parent 04a7798f96
commit bbfc4403ff
4 changed files with 107 additions and 37 deletions

View File

@ -35,6 +35,7 @@ export class QuestEditorRendererView extends QuestRendererView {
this.element.addEventListener("focus", () => quest_editor_store.undo.make_current(), true);
// Must be initialized before camera controls.
this.entity_controls = this.disposable(
new QuestEntityControls(quest_editor_store, this.renderer),
);
@ -50,6 +51,7 @@ export class QuestEditorRendererView extends QuestRendererView {
),
);
// Must be initialized after QuestEntityControls.
this.renderer.init_camera_controls();
this.finalize_construction(QuestEditorRendererView);

View File

@ -1,9 +1,18 @@
import { QuestEntityModel } from "../model/QuestEntityModel";
import { Euler, Intersection, Mesh, Plane, Quaternion, Raycaster, Vector2, Vector3 } from "three";
import {
Euler,
Intersection,
Mesh,
Object3D,
Plane,
Quaternion,
Raycaster,
Vector2,
Vector3,
} from "three";
import { QuestRenderer } from "./QuestRenderer";
import { EntityUserData } from "./conversion/entities";
import { QuestNpcModel } from "../model/QuestNpcModel";
import { AreaUserData } from "./conversion/areas";
import { SectionModel } from "../model/SectionModel";
import { Disposable } from "../../core/observable/Disposable";
import { Disposer } from "../../core/observable/Disposer";
@ -21,10 +30,10 @@ import { CreateEntityAction } from "../actions/CreateEntityAction";
import { RotateEntityAction } from "../actions/RotateEntityAction";
import { RemoveEntityAction } from "../actions/RemoveEntityAction";
import { TranslateEntityAction } from "../actions/TranslateEntityAction";
import { Object3D } from "three/src/core/Object3D";
import { create_quest_npc } from "../../core/data_formats/parsing/quest/QuestNpc";
import { create_quest_object } from "../../core/data_formats/parsing/quest/QuestObject";
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { pick_ground } from "./pick_ground";
const ZERO_VECTOR = Object.freeze(new Vector3(0, 0, 0));
const UP_VECTOR = Object.freeze(new Vector3(0, 1, 0));
@ -917,38 +926,6 @@ const rotate_entity = (() => {
};
})();
/**
* @param renderer
* @param pointer_pos - pointer coordinates in normalized device space
* @param drag_adjust - vector from origin of entity to grabbing point
*/
function pick_ground(
renderer: QuestRenderer,
pointer_pos: Vector2,
drag_adjust: Vector3,
): {
intersection?: Intersection;
section?: SectionModel;
} {
raycaster.setFromCamera(pointer_pos, renderer.camera);
raycaster.ray.origin.add(drag_adjust);
const intersections = raycaster.intersectObjects(renderer.collision_geometry.children, true);
// Don't allow entities to be placed on very steep terrain.
// E.g. walls.
// TODO: make use of the flags field in the collision data.
for (const intersection of intersections) {
if (intersection.face!.normal.y > 0.75) {
return {
intersection,
section: (intersection.object.userData as AreaUserData).section,
};
}
}
return {};
}
const pick_nearest_visible_object = (() => {
const intersections: Intersection[] = [];

View File

@ -1,10 +1,22 @@
import { DisposableThreeRenderer, Renderer } from "../../core/rendering/Renderer";
import { Group, Mesh, MeshLambertMaterial, Object3D, PerspectiveCamera } from "three";
import {
Group,
Mesh,
MeshLambertMaterial,
Object3D,
PerspectiveCamera,
Vector2,
Vector3,
} from "three";
import { QuestEntityModel } from "../model/QuestEntityModel";
import { Quest3DModelManager } from "./Quest3DModelManager";
import { Disposer } from "../../core/observable/Disposer";
import { ColorType, EntityUserData, NPC_COLORS, OBJECT_COLORS } from "./conversion/entities";
import { QuestNpcModel } from "../model/QuestNpcModel";
import { pick_ground } from "./pick_ground";
const ZERO_VECTOR_2 = Object.freeze(new Vector2(0, 0));
const ZERO_VECTOR_3 = Object.freeze(new Vector3(0, 0, 0));
export class QuestRenderer extends Renderer {
private _collision_geometry = new Object3D();
@ -14,6 +26,8 @@ export class QuestRenderer extends Renderer {
private readonly entity_to_mesh = new Map<QuestEntityModel, Mesh>();
private hovered_mesh?: Mesh;
private selected_mesh?: Mesh;
private camera_target_timeout?: number;
private old_camera_target = new Vector3();
get debug(): boolean {
return super.debug;
@ -27,7 +41,7 @@ export class QuestRenderer extends Renderer {
}
}
readonly camera = new PerspectiveCamera(60, 1, 10, 10000);
readonly camera = new PerspectiveCamera(60, 1, 10, 5_000);
get collision_geometry(): Object3D {
return this._collision_geometry;
@ -68,6 +82,11 @@ export class QuestRenderer extends Renderer {
*/
init_camera_controls(): void {
super.init_camera_controls();
this.controls.verticalDragToForward = true;
this.controls.truckSpeed = 2.5;
this.controls.addEventListener("update", this.camera_controls_updated);
}
dispose(): void {
@ -159,6 +178,40 @@ export class QuestRenderer extends Renderer {
this.selected_mesh = undefined;
}
protected render(): void {
const distance = this.controls.distance;
this.camera.near = distance / 100;
this.camera.far = Math.max(1_000, distance * 5);
this.camera.updateProjectionMatrix();
super.render();
}
private camera_controls_updated = (): void => {
window.clearTimeout(this.camera_target_timeout);
// If we call update_camera_target directly here, the camera will
// randomly rotate when panning and releasing the mouse button quickly.
// No idea why, but wrapping this call in a timeout makes it work.
this.camera_target_timeout = window.setTimeout(this.update_camera_target, 100);
};
private update_camera_target = (): void => {
// If the user moved the camera, try setting the camera
// target to a better point.
this.controls.updateCameraUp();
const { intersection } = pick_ground(this, ZERO_VECTOR_2, ZERO_VECTOR_3);
if (intersection) {
this.controls.getTarget(this.old_camera_target);
const new_target = intersection.point;
if (new_target.distanceTo(this.old_camera_target) > 10) {
this.controls.setTarget(new_target.x, new_target.y, new_target.z);
}
}
this.camera_target_timeout = undefined;
};
}
function set_color(mesh: Mesh, type: ColorType): void {

View File

@ -0,0 +1,38 @@
import { QuestRenderer } from "./QuestRenderer";
import { Intersection, Raycaster, Vector2, Vector3 } from "three";
import { SectionModel } from "../model/SectionModel";
import { AreaUserData } from "./conversion/areas";
const raycaster = new Raycaster();
/**
* @param renderer
* @param pointer_pos - pointer coordinates in normalized device space
* @param drag_adjust - vector from origin of entity to grabbing point
*/
export function pick_ground(
renderer: QuestRenderer,
pointer_pos: Vector2,
drag_adjust: Vector3,
): {
intersection?: Intersection;
section?: SectionModel;
} {
raycaster.setFromCamera(pointer_pos, renderer.camera);
raycaster.ray.origin.add(drag_adjust);
const intersections = raycaster.intersectObjects(renderer.collision_geometry.children, true);
// Don't allow entities to be placed on very steep terrain.
// E.g. walls.
// TODO: make use of the flags field in the collision data.
for (const intersection of intersections) {
if (intersection.face!.normal.y > 0.75) {
return {
intersection,
section: (intersection.object.userData as AreaUserData).section,
};
}
}
return {};
}