mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-05 15:28:29 +08:00
Improved quest editor camera controls.
This commit is contained in:
parent
04a7798f96
commit
bbfc4403ff
@ -35,6 +35,7 @@ export class QuestEditorRendererView extends QuestRendererView {
|
|||||||
|
|
||||||
this.element.addEventListener("focus", () => quest_editor_store.undo.make_current(), true);
|
this.element.addEventListener("focus", () => quest_editor_store.undo.make_current(), true);
|
||||||
|
|
||||||
|
// Must be initialized before camera controls.
|
||||||
this.entity_controls = this.disposable(
|
this.entity_controls = this.disposable(
|
||||||
new QuestEntityControls(quest_editor_store, this.renderer),
|
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.renderer.init_camera_controls();
|
||||||
|
|
||||||
this.finalize_construction(QuestEditorRendererView);
|
this.finalize_construction(QuestEditorRendererView);
|
||||||
|
@ -1,9 +1,18 @@
|
|||||||
import { QuestEntityModel } from "../model/QuestEntityModel";
|
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 { QuestRenderer } from "./QuestRenderer";
|
||||||
import { EntityUserData } from "./conversion/entities";
|
import { EntityUserData } from "./conversion/entities";
|
||||||
import { QuestNpcModel } from "../model/QuestNpcModel";
|
import { QuestNpcModel } from "../model/QuestNpcModel";
|
||||||
import { AreaUserData } from "./conversion/areas";
|
|
||||||
import { SectionModel } from "../model/SectionModel";
|
import { SectionModel } from "../model/SectionModel";
|
||||||
import { Disposable } from "../../core/observable/Disposable";
|
import { Disposable } from "../../core/observable/Disposable";
|
||||||
import { Disposer } from "../../core/observable/Disposer";
|
import { Disposer } from "../../core/observable/Disposer";
|
||||||
@ -21,10 +30,10 @@ import { CreateEntityAction } from "../actions/CreateEntityAction";
|
|||||||
import { RotateEntityAction } from "../actions/RotateEntityAction";
|
import { RotateEntityAction } from "../actions/RotateEntityAction";
|
||||||
import { RemoveEntityAction } from "../actions/RemoveEntityAction";
|
import { RemoveEntityAction } from "../actions/RemoveEntityAction";
|
||||||
import { TranslateEntityAction } from "../actions/TranslateEntityAction";
|
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_npc } from "../../core/data_formats/parsing/quest/QuestNpc";
|
||||||
import { create_quest_object } from "../../core/data_formats/parsing/quest/QuestObject";
|
import { create_quest_object } from "../../core/data_formats/parsing/quest/QuestObject";
|
||||||
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
|
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 ZERO_VECTOR = Object.freeze(new Vector3(0, 0, 0));
|
||||||
const UP_VECTOR = Object.freeze(new Vector3(0, 1, 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 pick_nearest_visible_object = (() => {
|
||||||
const intersections: Intersection[] = [];
|
const intersections: Intersection[] = [];
|
||||||
|
|
||||||
|
@ -1,10 +1,22 @@
|
|||||||
import { DisposableThreeRenderer, Renderer } from "../../core/rendering/Renderer";
|
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 { QuestEntityModel } from "../model/QuestEntityModel";
|
||||||
import { Quest3DModelManager } from "./Quest3DModelManager";
|
import { Quest3DModelManager } from "./Quest3DModelManager";
|
||||||
import { Disposer } from "../../core/observable/Disposer";
|
import { Disposer } from "../../core/observable/Disposer";
|
||||||
import { ColorType, EntityUserData, NPC_COLORS, OBJECT_COLORS } from "./conversion/entities";
|
import { ColorType, EntityUserData, NPC_COLORS, OBJECT_COLORS } from "./conversion/entities";
|
||||||
import { QuestNpcModel } from "../model/QuestNpcModel";
|
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 {
|
export class QuestRenderer extends Renderer {
|
||||||
private _collision_geometry = new Object3D();
|
private _collision_geometry = new Object3D();
|
||||||
@ -14,6 +26,8 @@ export class QuestRenderer extends Renderer {
|
|||||||
private readonly entity_to_mesh = new Map<QuestEntityModel, Mesh>();
|
private readonly entity_to_mesh = new Map<QuestEntityModel, Mesh>();
|
||||||
private hovered_mesh?: Mesh;
|
private hovered_mesh?: Mesh;
|
||||||
private selected_mesh?: Mesh;
|
private selected_mesh?: Mesh;
|
||||||
|
private camera_target_timeout?: number;
|
||||||
|
private old_camera_target = new Vector3();
|
||||||
|
|
||||||
get debug(): boolean {
|
get debug(): boolean {
|
||||||
return super.debug;
|
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 {
|
get collision_geometry(): Object3D {
|
||||||
return this._collision_geometry;
|
return this._collision_geometry;
|
||||||
@ -68,6 +82,11 @@ export class QuestRenderer extends Renderer {
|
|||||||
*/
|
*/
|
||||||
init_camera_controls(): void {
|
init_camera_controls(): void {
|
||||||
super.init_camera_controls();
|
super.init_camera_controls();
|
||||||
|
|
||||||
|
this.controls.verticalDragToForward = true;
|
||||||
|
this.controls.truckSpeed = 2.5;
|
||||||
|
|
||||||
|
this.controls.addEventListener("update", this.camera_controls_updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
@ -159,6 +178,40 @@ export class QuestRenderer extends Renderer {
|
|||||||
|
|
||||||
this.selected_mesh = undefined;
|
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 {
|
function set_color(mesh: Mesh, type: ColorType): void {
|
||||||
|
38
src/quest_editor/rendering/pick_ground.ts
Normal file
38
src/quest_editor/rendering/pick_ground.ts
Normal 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 {};
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user