From 81c4d03325203f7e68debeffb62a2a16f11bff64 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Wed, 3 Jul 2019 00:43:17 +0200 Subject: [PATCH] Improved idle performance by not rendering at all when there's no user interaction or animation running in any of the renderers. Fixed memory leak. Fixed issues with current frame display and manual frame change in model viewer. --- .eslintrc.json | 3 +- src/domain/index.ts | 2 +- src/rendering/ModelRenderer.ts | 27 +++++-- src/rendering/QuestRenderer.ts | 136 +++++++++++++++------------------ src/rendering/Renderer.ts | 33 ++++++-- src/rendering/entities.ts | 9 --- src/stores/ModelViewerStore.ts | 14 ++-- 7 files changed, 119 insertions(+), 105 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 0444b054..f8b0f502 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -29,9 +29,10 @@ "@typescript-eslint/no-parameter-properties": "off", "@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/prefer-interface": "off", + "no-console": "warn", "no-constant-condition": ["warn", { "checkLoops": false }], "no-empty": "warn", - "prettier/prettier": ["warn"], + "prettier/prettier": "warn", "react/no-unescaped-entities": "off", "@typescript-eslint/no-non-null-assertion": "off" }, diff --git a/src/domain/index.ts b/src/domain/index.ts index ad35725d..b437917f 100644 --- a/src/domain/index.ts +++ b/src/domain/index.ts @@ -191,7 +191,7 @@ export class QuestEntity { } } - object_3d?: Object3D; + @observable object_3d?: Object3D; constructor(area_id: number, section_id: number, position: Vec3, rotation: Vec3) { if (Object.getPrototypeOf(this) === Object.getPrototypeOf(QuestEntity)) diff --git a/src/rendering/ModelRenderer.ts b/src/rendering/ModelRenderer.ts index 73ba78b1..75f00d73 100644 --- a/src/rendering/ModelRenderer.ts +++ b/src/rendering/ModelRenderer.ts @@ -18,11 +18,25 @@ export class ModelRenderer extends Renderer { constructor() { super(); + autorun(() => { - const show = model_viewer_store.show_skeleton; + const show_skeleton = model_viewer_store.show_skeleton; if (this.skeleton_helper) { - this.skeleton_helper.visible = show; + this.skeleton_helper.visible = show_skeleton; + this.schedule_render(); + } + + if (!model_viewer_store.animation_playing) { + // Reference animation_frame here to make sure we render when the user sets the frame manually. + model_viewer_store.animation_frame; + this.schedule_render(); + } + }); + + autorun(() => { + if (model_viewer_store.animation) { + this.schedule_render(); } }); } @@ -45,17 +59,20 @@ export class ModelRenderer extends Renderer { } this.model = model; + this.schedule_render(); } } protected render(): void { - this.controls.update(); - if (model_viewer_store.animation) { model_viewer_store.animation.mixer.update(this.clock.getDelta()); model_viewer_store.update_animation_frame(); } - this.renderer.render(this.scene, this.camera); + super.render(); + + if (model_viewer_store.animation && !model_viewer_store.animation.action.paused) { + this.schedule_render(); + } } } diff --git a/src/rendering/QuestRenderer.ts b/src/rendering/QuestRenderer.ts index 9d29f8dc..58929a52 100644 --- a/src/rendering/QuestRenderer.ts +++ b/src/rendering/QuestRenderer.ts @@ -1,3 +1,4 @@ +import { autorun, IReactionDisposer, when } from "mobx"; import { Intersection, Mesh, @@ -8,8 +9,8 @@ import { Vector2, Vector3, } from "three"; -import { Area, Quest, QuestEntity, QuestNpc, QuestObject, Section } from "../domain"; import { Vec3 } from "../data_formats/Vec3"; +import { Area, Quest, QuestEntity, QuestNpc, Section } from "../domain"; import { area_store } from "../stores/AreaStore"; import { quest_editor_store } from "../stores/QuestEditorStore"; import { @@ -29,23 +30,25 @@ export function get_quest_renderer(): QuestRenderer { return renderer; } -interface PickEntityResult { +type PickEntityResult = { object: Mesh; entity: QuestEntity; grab_offset: Vector3; drag_adjust: Vector3; drag_y: number; manipulating: boolean; -} +}; + +type EntityUserData = { + entity: QuestEntity; +}; export class QuestRenderer extends Renderer { private raycaster = new Raycaster(); private quest?: Quest; - private quest_entities_loaded = false; private area?: Area; - private objs: Map = new Map(); // Objs grouped by area id - private npcs: Map = new Map(); // Npcs grouped by area id + private entity_reaction_disposers: IReactionDisposer[] = []; private collision_geometry = new Object3D(); private render_geometry = new Object3D(); @@ -58,9 +61,9 @@ export class QuestRenderer extends Renderer { constructor() { super(); - this.renderer.domElement.addEventListener("mousedown", this.on_mouse_down); - this.renderer.domElement.addEventListener("mouseup", this.on_mouse_up); - this.renderer.domElement.addEventListener("mousemove", this.on_mouse_move); + this.dom_element.addEventListener("mousedown", this.on_mouse_down); + this.dom_element.addEventListener("mouseup", this.on_mouse_up); + this.dom_element.addEventListener("mousemove", this.on_mouse_move); this.scene.add(this.obj_geometry); this.scene.add(this.npc_geometry); @@ -76,24 +79,6 @@ export class QuestRenderer extends Renderer { if (this.quest !== quest) { this.quest = quest; - - this.objs.clear(); - this.npcs.clear(); - - if (quest) { - for (const obj of quest.objects) { - const array = this.objs.get(obj.area_id) || []; - array.push(obj); - this.objs.set(obj.area_id, array); - } - - for (const npc of quest.npcs) { - const array = this.npcs.get(npc.area_id) || []; - array.push(npc); - this.npcs.set(npc.area_id, array); - } - } - update = true; } @@ -102,24 +87,29 @@ export class QuestRenderer extends Renderer { } } - protected render(): void { - this.controls.update(); - this.add_loaded_entities(); - this.renderer.render(this.scene, this.camera); - } - private async update_geometry(): Promise { + this.dispose_entity_reactions(); + this.scene.remove(this.obj_geometry); this.scene.remove(this.npc_geometry); this.obj_geometry = new Object3D(); this.npc_geometry = new Object3D(); this.scene.add(this.obj_geometry); this.scene.add(this.npc_geometry); - this.quest_entities_loaded = false; this.scene.remove(this.collision_geometry); if (this.quest && this.area) { + // Add necessary entity geometry when it arrives. + for (const obj of this.quest.objects) { + this.update_entity_geometry(obj, this.obj_geometry); + } + + for (const npc of this.quest.npcs) { + this.update_entity_geometry(npc, this.npc_geometry); + } + + // Load necessary area geometry. const episode = this.quest.episode; const area_id = this.area.id; const variant = this.quest.area_variants.find(v => v.area.id === area_id); @@ -131,14 +121,12 @@ export class QuestRenderer extends Renderer { variant_id ); - if (this.quest && this.area) { - this.scene.remove(this.collision_geometry); + this.scene.remove(this.collision_geometry); - this.reset_camera(new Vector3(0, 800, 700), new Vector3(0, 0, 0)); + this.reset_camera(new Vector3(0, 800, 700), new Vector3(0, 0, 0)); - this.collision_geometry = collision_geometry; - this.scene.add(collision_geometry); - } + this.collision_geometry = collision_geometry; + this.scene.add(collision_geometry); const render_geometry = await area_store.get_area_render_geometry( episode, @@ -146,37 +134,39 @@ export class QuestRenderer extends Renderer { variant_id ); - if (this.quest && this.area) { - this.render_geometry = render_geometry; - } + this.render_geometry = render_geometry; } } - private add_loaded_entities(): void { - if (this.quest && this.area && !this.quest_entities_loaded) { - let loaded = true; + private update_entity_geometry(entity: QuestEntity, entity_geometry: Object3D): void { + if (this.area && entity.area_id === this.area.id) { + this.entity_reaction_disposers.push( + when( + () => entity.object_3d != undefined, + () => { + const object_3d = entity.object_3d!; + entity_geometry.add(object_3d); - for (const object of this.quest.objects) { - if (object.area_id === this.area.id) { - if (object.object_3d) { - this.obj_geometry.add(object.object_3d); - } else { - loaded = false; + this.entity_reaction_disposers.push( + autorun(() => { + const { x, y, z } = entity.position; + object_3d.position.set(x, y, z); + const rot = entity.rotation; + object_3d.rotation.set(rot.x, rot.y, rot.z); + this.schedule_render(); + }) + ); + + this.schedule_render(); } - } - } + ) + ); + } + } - for (const npc of this.quest.npcs) { - if (npc.area_id === this.area.id) { - if (npc.object_3d) { - this.npc_geometry.add(npc.object_3d); - } else { - loaded = false; - } - } - } - - this.quest_entities_loaded = loaded; + private dispose_entity_reactions(): void { + for (const disposer of this.entity_reaction_disposers) { + disposer(); } } @@ -222,6 +212,7 @@ export class QuestRenderer extends Renderer { if (selection_changed) { quest_editor_store.set_selected_entity(data && data.entity); + this.schedule_render(); } }; @@ -229,6 +220,7 @@ export class QuestRenderer extends Renderer { if (this.selected_data) { this.selected_data.manipulating = false; this.controls.enabled = true; + this.schedule_render(); } }; @@ -298,6 +290,8 @@ export class QuestRenderer extends Renderer { } } } + + this.schedule_render(); } else { // User is hovering. const old_data = this.hovered_data; @@ -311,6 +305,7 @@ export class QuestRenderer extends Renderer { } this.hovered_data = undefined; + this.schedule_render(); } if (data && (!old_data || data.object !== old_data.object)) { @@ -321,18 +316,11 @@ export class QuestRenderer extends Renderer { } this.hovered_data = data; + this.schedule_render(); } } }; - private pointer_pos_to_device_coords(e: MouseEvent): Vector2 { - const coords = new Vector2(); - this.renderer.getSize(coords); - coords.width = (e.offsetX / coords.width) * 2 - 1; - coords.height = (e.offsetY / coords.height) * -2 + 1; - return coords; - } - /** * @param pointer_pos - pointer coordinates in normalized device space */ @@ -350,7 +338,7 @@ export class QuestRenderer extends Renderer { const npc_dist = nearest_npc ? nearest_npc.distance : Infinity; const intersection = object_dist < npc_dist ? nearest_object : nearest_npc; - const entity = intersection.object.userData.entity; + const entity = (intersection.object.userData as EntityUserData).entity; // Vector that points from the grabbing point to the model's origin. const grab_offset = intersection.object.position.clone().sub(intersection.point); // Vector that points from the grabbing point to the terrain point directly under the model's origin. diff --git a/src/rendering/Renderer.ts b/src/rendering/Renderer.ts index 0d0f3b33..fa336f11 100644 --- a/src/rendering/Renderer.ts +++ b/src/rendering/Renderer.ts @@ -7,27 +7,30 @@ import { Scene, Vector3, WebGLRenderer, + Vector2, } from "three"; import OrbitControlsCreator from "three-orbit-controls"; const OrbitControls = OrbitControlsCreator(THREE); export class Renderer { - protected renderer = new WebGLRenderer({ antialias: true }); protected camera: PerspectiveCamera; protected controls: any; protected scene = new Scene(); + private renderer = new WebGLRenderer({ antialias: true }); + private render_scheduled = false; + constructor() { this.camera = new PerspectiveCamera(75, 1, 0.1, 5000); + this.controls = new OrbitControls(this.camera, this.renderer.domElement); this.controls.mouseButtons.ORBIT = MOUSE.RIGHT; this.controls.mouseButtons.PAN = MOUSE.LEFT; + this.controls.addEventListener("change", this.schedule_render); this.scene.background = new Color(0x151c21); this.scene.add(new HemisphereLight(0xffffff, 0x505050, 1)); - - requestAnimationFrame(this.render_loop); } get dom_element(): HTMLElement { @@ -40,19 +43,33 @@ export class Renderer { this.camera.updateProjectionMatrix(); } + protected schedule_render = () => { + if (!this.render_scheduled) { + this.render_scheduled = true; + requestAnimationFrame(this.call_render); + } + }; + + protected render(): void { + this.renderer.render(this.scene, this.camera); + } + protected reset_camera(position: Vector3, look_at: Vector3): void { this.controls.reset(); this.camera.position.copy(position); this.camera.lookAt(look_at); } - protected render(): void { - this.controls.update(); - this.renderer.render(this.scene, this.camera); + protected pointer_pos_to_device_coords(e: MouseEvent): Vector2 { + const coords = new Vector2(); + this.renderer.getSize(coords); + coords.width = (e.offsetX / coords.width) * 2 - 1; + coords.height = (e.offsetY / coords.height) * -2 + 1; + return coords; } - private render_loop = () => { + private call_render = () => { + this.render_scheduled = false; this.render(); - requestAnimationFrame(this.render_loop); }; } diff --git a/src/rendering/entities.ts b/src/rendering/entities.ts index 70db1c99..73fb3358 100644 --- a/src/rendering/entities.ts +++ b/src/rendering/entities.ts @@ -1,4 +1,3 @@ -import { autorun } from "mobx"; import { BufferGeometry, DoubleSide, Mesh, MeshLambertMaterial } from "three"; import { QuestEntity, QuestNpc, QuestObject } from "../domain"; @@ -33,13 +32,5 @@ function create_mesh( mesh.name = type; mesh.userData.entity = entity; - // TODO: dispose autorun? - autorun(() => { - const { x, y, z } = entity.position; - mesh.position.set(x, y, z); - const rot = entity.rotation; - mesh.rotation.set(rot.x, rot.y, rot.z); - }); - return mesh; } diff --git a/src/stores/ModelViewerStore.ts b/src/stores/ModelViewerStore.ts index 5e33c568..81988249 100644 --- a/src/stores/ModelViewerStore.ts +++ b/src/stores/ModelViewerStore.ts @@ -54,9 +54,9 @@ class ModelViewerStore { set_animation_frame = action("set_animation_frame", (frame: number) => { if (this.animation) { const frame_count = this.animation_frame_count; - frame = ((frame - 1) % frame_count) + 1; - if (frame < 1) frame = frame_count + frame; - this.animation.action.time = (frame - 1) / (frame_count - 1); + if (frame > frame_count) frame = 1; + if (frame < 1) frame = frame_count; + this.animation.action.time = (frame - 1) / PSO_FRAME_RATE; this.animation_frame = frame; } }); @@ -84,9 +84,9 @@ class ModelViewerStore { }); update_animation_frame = action("update_animation_frame", () => { - if (this.animation) { - const frame_count = this.animation_frame_count; - this.animation_frame = Math.floor(this.animation.action.time * (frame_count - 1) + 1); + if (this.animation && this.animation_playing) { + const time = this.animation.action.time; + this.animation_frame = Math.round(time * PSO_FRAME_RATE) + 1; } }); @@ -165,7 +165,7 @@ class ModelViewerStore { this.animation.action.play(); this.animation_playing = true; - this.animation_frame_count = PSO_FRAME_RATE * clip.duration + 1; + this.animation_frame_count = Math.round(PSO_FRAME_RATE * clip.duration) + 1; }); private get_player_ninja_object(model: PlayerModel): Promise> {