#include #include "scene_view.h" #include "editor/asset_browser.h" #include "editor/asset_compiler.h" #include "editor/gizmo.h" #include "editor/log_ui.h" #include "editor/prefab_system.h" #include "editor/render_interface.h" #include "editor/studio_app.h" #include "editor/utils.h" #include "engine/crc32.h" #include "engine/delegate_list.h" #include "engine/engine.h" #include "engine/geometry.h" #include "engine/path.h" #include "engine/path.h" #include "engine/prefab.h" #include "engine/profiler.h" #include "engine/reflection.h" #include "engine/resource_manager.h" #include "engine/string.h" #include "engine/universe.h" #include "renderer/culling_system.h" #include "renderer/draw2d.h" #include "renderer/font.h" #include "renderer/gpu/gpu.h" #include "renderer/editor/editor_icon.h" #include "renderer/material.h" #include "renderer/model.h" #include "renderer/pipeline.h" #include "renderer/pose.h" #include "renderer/render_scene.h" #include "renderer/renderer.h" #include "renderer/shader.h" namespace Lumix { static const ComponentType MODEL_INSTANCE_TYPE = reflection::getComponentType("model_instance"); static const ComponentType PARTICLE_EMITTER_TYPE = reflection::getComponentType("particle_emitter"); static const ComponentType MESH_ACTOR_TYPE = reflection::getComponentType("rigid_actor"); struct UniverseViewImpl final : UniverseView { enum class MouseMode { NONE, SELECT, NAVIGATE, PAN, CUSTOM }; enum class SnapMode { NONE, FREE, VERTEX }; UniverseViewImpl(SceneView& view) : m_scene_view(view) , m_editor(view.m_editor) , m_scene(nullptr) , m_draw_cmds(view.m_app.getAllocator()) , m_draw_vertices(view.m_app.getAllocator()) { m_editor.universeCreated().bind<&UniverseViewImpl::onUniverseCreated>(this); m_editor.universeDestroyed().bind<&UniverseViewImpl::onUniverseDestroyed>(this); m_viewport.is_ortho = false; m_viewport.pos = DVec3(0); m_viewport.rot.set(0, 0, 0, 1); m_viewport.w = -1; m_viewport.h = -1; m_viewport.fov = view.m_app.getFOV(); m_viewport.near = 0.1f; m_viewport.far = 10000.f; ResourceManagerHub& rm = m_editor.getEngine().getResourceManager(); Path font_path("editor/fonts/NotoSans-Regular.ttf"); m_font_res = rm.load(font_path); m_font = m_font_res->addRef(16); onUniverseCreated(); } ~UniverseViewImpl() { m_font_res->decRefCount(); m_editor.universeCreated().unbind<&UniverseViewImpl::onUniverseCreated>(this); m_editor.universeDestroyed().unbind<&UniverseViewImpl::onUniverseDestroyed>(this); onUniverseDestroyed(); } void addCross(const DVec3& pos, float size, Color color) { addLine(*this, pos - Vec3(size, 0, 0), pos + Vec3(size, 0, 0), color); addLine(*this, pos - Vec3(0, size, 0), pos + Vec3(0, size, 0), color); addLine(*this, pos - Vec3(0, 0, size), pos + Vec3(0, 0, size), color); } void onUniverseCreated(){ m_scene = (RenderScene*)m_editor.getUniverse()->getScene(crc32("renderer")); m_icons = EditorIcons::create(m_editor, *m_scene); } void onUniverseDestroyed(){ m_icons.reset(); } void setSnapMode(bool enable, bool vertex_snap) override { m_snap_mode = enable ? (vertex_snap ? SnapMode::VERTEX : SnapMode::FREE) : SnapMode::NONE; } void previewSnapVertex() { if (m_snap_mode != SnapMode::VERTEX) return; DVec3 origin; Vec3 dir; m_viewport.getRay(m_mouse_pos, origin, dir); const RayCastModelHit hit = m_scene->castRay(origin, dir, INVALID_ENTITY); if (!hit.is_hit) return; const DVec3 snap_pos = getClosestVertex(hit); addCross(snap_pos, 1, Color::RED); } Vec2 getMouseSensitivity() override { return m_mouse_sensitivity; } void setMouseSensitivity(float x, float y) override { m_mouse_sensitivity.x = 10000 / x; m_mouse_sensitivity.y = 10000 / y; } void rectSelect() { Vec2 min = m_rect_selection_start; Vec2 max = m_mouse_pos; if (min.x > max.x) swap(min.x, max.x); if (min.y > max.y) swap(min.y, max.y); const ShiftedFrustum frustum = m_viewport.getFrustum(min, max); m_editor.selectEntities({}, false); for (int i = 0; i < (int)RenderableTypes::COUNT; ++i) { CullResult* renderables = m_scene->getRenderables(frustum, (RenderableTypes)i); CullResult* first = renderables; while (renderables) { m_editor.selectEntities(Span(renderables->entities, renderables->header.count), true); renderables = renderables->header.next; } if (first) first->free(m_editor.getEngine().getPageAllocator()); } } void onMouseUp(int x, int y, os::MouseButton button) { m_mouse_pos = {(float)x, (float)y}; if (m_mouse_mode == MouseMode::SELECT) { if ((m_rect_selection_start.x != m_mouse_pos.x || m_rect_selection_start.y != m_mouse_pos.y) && m_rect_selection_timer > 0.1f) { rectSelect(); } else { DVec3 origin; Vec3 dir; m_viewport.getRay(m_mouse_pos, origin, dir); const RayCastModelHit hit = m_scene->castRay(origin, dir, INVALID_ENTITY); const Array& selected_entities = m_editor.getSelectedEntities(); if (m_snap_mode != SnapMode::NONE && !selected_entities.empty() && hit.is_hit) { DVec3 snap_pos = origin + dir * hit.t; if (m_snap_mode == SnapMode::VERTEX) snap_pos = getClosestVertex(hit); const Quat rot = m_editor.getUniverse()->getRotation(selected_entities[0]); const Gizmo::Config& gizmo_cfg = m_scene_view.m_app.getGizmoConfig(); const Vec3 offset = rot.rotate(gizmo_cfg.getOffset()); m_editor.snapEntities(snap_pos - offset, gizmo_cfg.isTranslateMode()); } else { auto icon_hit = m_icons->raycast(origin, dir); if (icon_hit.entity != INVALID_ENTITY) { if(icon_hit.entity.isValid()) { EntityRef e = (EntityRef)icon_hit.entity; m_editor.selectEntities(Span(&e, 1), ImGui::GetIO().KeyCtrl); } } else if (hit.is_hit) { if(hit.entity.isValid()) { EntityRef entity = (EntityRef)hit.entity; m_editor.selectEntities(Span(&entity, 1), ImGui::GetIO().KeyCtrl); } } } } } m_is_mouse_down[(int)button] = false; if (m_mouse_handling_plugin) { m_mouse_handling_plugin->onMouseUp(*this, x, y, button); m_mouse_handling_plugin = nullptr; } m_mouse_mode = MouseMode::NONE; } Vec2 getMousePos() const override { return m_mouse_pos; } void resetPivot() override { m_scene_view.m_app.getGizmoConfig().setOffset(Vec3::ZERO); } void setCustomPivot() override { const Array& selected_entities = m_editor.getSelectedEntities(); if (selected_entities.empty()) return; DVec3 origin; Vec3 dir; m_viewport.getRay(m_mouse_pos, origin, dir); const RayCastModelHit hit = m_scene->castRay(origin, dir, INVALID_ENTITY); if (!hit.is_hit || hit.entity != selected_entities[0]) return; const DVec3 snap_pos = getClosestVertex(hit); const Transform tr = m_editor.getUniverse()->getTransform(selected_entities[0]); m_scene_view.m_app.getGizmoConfig().setOffset(tr.rot.conjugated() * (snap_pos - tr.pos).toFloat()); } DVec3 getClosestVertex(const RayCastModelHit& hit) const { ASSERT(hit.entity.isValid()); const EntityRef entity = (EntityRef)hit.entity; const DVec3& wpos = hit.origin + hit.t * hit.dir; Universe& universe = m_scene->getUniverse(); const Transform tr = universe.getTransform(entity); const Vec3 lpos = tr.rot.conjugated() * (wpos - tr.pos).toFloat(); if (!universe.hasComponent(entity, MODEL_INSTANCE_TYPE)) return wpos; Model* model = m_scene->getModelInstanceModel(entity); float min_dist_squared = FLT_MAX; Vec3 closest_vertex = lpos; auto processVertex = [&](const Vec3& vertex) { float dist_squared = (vertex - lpos).squaredLength(); if (dist_squared < min_dist_squared) { min_dist_squared = dist_squared; closest_vertex = vertex; } }; for (int i = 0, c = model->getMeshCount(); i < c; ++i) { Mesh& mesh = model->getMesh(i); if (mesh.areIndices16()) { const u16* indices = (const u16*)mesh.indices.data(); for (i32 i = 0, c = (i32)mesh.indices.size() >> 1; i < c; ++i) { Vec3 vertex = mesh.vertices[indices[i]]; processVertex(vertex); } } else { const u32* indices = (const u32*)mesh.indices.data(); for (i32 i = 0, c = (i32)mesh.indices.size() >> 2; i < c; ++i) { Vec3 vertex = mesh.vertices[indices[i]]; processVertex(vertex); } } } return tr.pos + tr.rot * closest_vertex; } void inputFrame() { for (auto& i : m_is_mouse_click) i = false; } bool isMouseClick(os::MouseButton button) const override { return m_is_mouse_click[(int)button]; } bool isMouseDown(os::MouseButton button) const override { return m_is_mouse_down[(int)button]; } void onMouseMove(int x, int y, int relx, int rely) { PROFILE_FUNCTION(); m_mouse_pos.set((float)x, (float)y); static const float MOUSE_MULTIPLIER = 1 / 200.0f; switch (m_mouse_mode) { case MouseMode::CUSTOM: { if (m_mouse_handling_plugin) { m_mouse_handling_plugin->onMouseMove(*this, x, y, relx, rely); } } break; case MouseMode::NAVIGATE: { const float yaw = -signum(relx) * (powf(fabsf((float)relx / m_mouse_sensitivity.x), 1.2f)); const float pitch = -signum(rely) * (powf(fabsf((float)rely / m_mouse_sensitivity.y), 1.2f)); rotateCamera(yaw, pitch); break; } case MouseMode::PAN: panCamera(relx * MOUSE_MULTIPLIER, rely * MOUSE_MULTIPLIER); break; case MouseMode::NONE: case MouseMode::SELECT: break; } } void onMouseDown(int x, int y, os::MouseButton button) { m_is_mouse_click[(int)button] = true; m_is_mouse_down[(int)button] = true; if(button == os::MouseButton::MIDDLE) { m_mouse_mode = MouseMode::PAN; } else if (button == os::MouseButton::RIGHT) { m_mouse_mode = MouseMode::NAVIGATE; } else if (button == os::MouseButton::LEFT) { DVec3 origin; Vec3 dir; m_viewport.getRay({(float)x, (float)y}, origin, dir); const RayCastModelHit hit = m_scene->castRay(origin, dir, INVALID_ENTITY); if (Gizmo::isActive()) return; if (m_scene_view.m_is_measure_active) { m_mouse_mode = MouseMode::NONE; if (!hit.is_hit) return; const DVec3 p = hit.origin + hit.t * hit.dir; if (m_scene_view.m_is_measure_from_set) m_scene_view.m_measure_to = p; else m_scene_view.m_measure_from = p; m_scene_view.m_is_measure_from_set = !m_scene_view.m_is_measure_from_set; return; } for (StudioApp::MousePlugin* plugin : m_scene_view.m_app.getMousePlugins()) { if (plugin->onMouseDown(*this, x, y)) { m_mouse_handling_plugin = plugin; m_mouse_mode = MouseMode::CUSTOM; return; } } m_mouse_mode = MouseMode::SELECT; m_rect_selection_start = {(float)x, (float)y}; m_rect_selection_timer = 0; } } const Viewport& getViewport() const override { return m_viewport; } void setViewport(const Viewport& vp) override { m_viewport = vp; } void copyTransform() override { if (m_editor.getSelectedEntities().empty()) return; m_editor.setEntitiesPositionsAndRotations(m_editor.getSelectedEntities().begin(), &m_viewport.pos, &m_viewport.rot, 1); } void lookAtSelected() override { const Universe* universe = m_editor.getUniverse(); if (m_editor.getSelectedEntities().empty()) return; m_go_to_parameters.m_is_active = true; m_go_to_parameters.m_t = 0; m_go_to_parameters.m_from = m_viewport.pos; const Vec3 dir = m_viewport.rot.rotate(Vec3(0, 0, 1)); m_go_to_parameters.m_to = universe->getPosition(m_editor.getSelectedEntities()[0]) + dir * 10; const double len = (m_go_to_parameters.m_to - m_go_to_parameters.m_from).length(); m_go_to_parameters.m_speed = maximum(100.0f / (len > 0 ? float(len) : 1), 2.0f); m_go_to_parameters.m_from_rot = m_go_to_parameters.m_to_rot = m_viewport.rot; } void setTopView() override { m_go_to_parameters.m_is_active = true; m_go_to_parameters.m_t = 0; m_go_to_parameters.m_from = m_viewport.pos; m_go_to_parameters.m_to = m_go_to_parameters.m_from; const Array& selected_entities = m_editor.getSelectedEntities(); if (!selected_entities.empty()) { auto* universe = m_editor.getUniverse(); m_go_to_parameters.m_to = universe->getPosition(selected_entities[0]) + Vec3(0, 10, 0); } m_go_to_parameters.m_speed = 2.0f; m_go_to_parameters.m_from_rot = m_viewport.rot; m_go_to_parameters.m_to_rot = Quat(Vec3(1, 0, 0), -PI * 0.5f); } void setFrontView() override { m_go_to_parameters.m_is_active = true; m_go_to_parameters.m_t = 0; m_go_to_parameters.m_from = m_viewport.pos; m_go_to_parameters.m_to = m_go_to_parameters.m_from; const Array& selected_entities = m_editor.getSelectedEntities(); if (!selected_entities.empty()) { auto* universe = m_editor.getUniverse(); m_go_to_parameters.m_to = universe->getPosition(selected_entities[0]) + Vec3(0, 0, -10); } m_go_to_parameters.m_speed = 2.0f; m_go_to_parameters.m_from_rot = m_viewport.rot; m_go_to_parameters.m_to_rot = Quat(Vec3(0, 1, 0), PI); } void setSideView() override { m_go_to_parameters.m_is_active = true; m_go_to_parameters.m_t = 0; m_go_to_parameters.m_from = m_viewport.pos; m_go_to_parameters.m_to = m_go_to_parameters.m_from; const Array& selected_entities = m_editor.getSelectedEntities(); if (!selected_entities.empty()) { auto* universe = m_editor.getUniverse(); m_go_to_parameters.m_to = universe->getPosition(selected_entities[0]) + Vec3(-10, 0, 0); } m_go_to_parameters.m_speed = 2.0f; m_go_to_parameters.m_from_rot = m_viewport.rot; m_go_to_parameters.m_to_rot = Quat(Vec3(0, 1, 0), -PI * 0.5f); } void moveCamera(float forward, float right, float up, float speed) override { const Quat rot = m_viewport.rot; right = m_scene_view.m_orbit_action.isActive() ? 0 : right; m_viewport.pos += rot.rotate(Vec3(0, 0, -1)) * forward * speed; m_viewport.pos += rot.rotate(Vec3(1, 0, 0)) * right * speed; m_viewport.pos += rot.rotate(Vec3(0, 1, 0)) * up * speed; } void rotateCamera(float yaw, float pitch) { const Universe* universe = m_editor.getUniverse(); DVec3 pos = m_viewport.pos; Quat rot = m_viewport.rot; const Quat old_rot = rot; Quat yaw_rot(Vec3(0, 1, 0), yaw); rot = yaw_rot * rot; rot.normalize(); Vec3 pitch_axis = rot.rotate(Vec3(1, 0, 0)); const Quat pitch_rot(pitch_axis, pitch); rot = pitch_rot * rot; rot.normalize(); if (m_scene_view.m_orbit_action.isActive() && !m_editor.getSelectedEntities().empty()) { const Vec3 dir = rot.rotate(Vec3(0, 0, 1)); const DVec3 entity_pos = universe->getPosition(m_editor.getSelectedEntities()[0]); const float dist = float((entity_pos - pos).length()); pos = entity_pos + dir * dist; } m_viewport.pos = pos; m_viewport.rot = rot; } void panCamera(float x, float y) { const Quat rot = m_viewport.rot; m_viewport.pos += rot.rotate(Vec3(x, 0, 0)); m_viewport.pos += rot.rotate(Vec3(0, -y, 0)); } void update(float time_delta) { if (m_mouse_mode == MouseMode::SELECT) { m_rect_selection_timer += time_delta; } if (!m_viewport.is_ortho) { m_viewport.fov = m_scene_view.m_app.getFOV(); } previewSnapVertex(); if (m_is_mouse_down[(int)os::MouseButton::LEFT] && m_mouse_mode == MouseMode::SELECT) { Draw2D& d2d = m_scene_view.getPipeline()->getDraw2D(); d2d.addRect(m_rect_selection_start, m_mouse_pos, Color(0xfffffFFF), 1); d2d.addRect(m_rect_selection_start - Vec2(1, 1), m_mouse_pos + Vec2(1, 1), Color(0, 0, 0, 0xff), 1); } if (!m_go_to_parameters.m_is_active) return; float t = easeInOut(m_go_to_parameters.m_t); m_go_to_parameters.m_t += m_editor.getEngine().getLastTimeDelta() * m_go_to_parameters.m_speed; DVec3 pos = m_go_to_parameters.m_from * (1 - t) + m_go_to_parameters.m_to * t; Quat rot; rot = nlerp(m_go_to_parameters.m_from_rot, m_go_to_parameters.m_to_rot, t); if (m_go_to_parameters.m_t >= 1) { pos = m_go_to_parameters.m_to; m_go_to_parameters.m_is_active = false; } m_viewport.pos = pos; m_viewport.rot = rot; } RayHit getCameraRaycastHit(int cam_x, int cam_y) override { RayHit res; const Vec2 center{float(cam_x), float(cam_y)}; DVec3 origin; Vec3 dir; m_viewport.getRay(center, origin, dir); const RayCastModelHit hit = m_scene->castRay(origin, dir, INVALID_ENTITY); DVec3 pos; if (hit.is_hit) { res.pos = origin + dir * hit.t; res.t = hit.t; res.entity = hit.entity; res.is_hit = true; return res; } res.pos = m_viewport.pos + m_viewport.rot.rotate(Vec3(0, 0, -2)); res.t = 2; res.entity = INVALID_ENTITY; res.is_hit = false; return res; } void addText2D(float x, float y, Color color, const char* text) override { if (m_font) m_scene_view.m_pipeline->getDraw2D().addText(*m_font, {x, y}, color, text); } Vertex* render(bool lines, u32 vertex_count) override { m_draw_vertices.resize(m_draw_vertices.size() + vertex_count); DrawCmd& cmd = m_draw_cmds.emplace(); cmd.lines = lines; cmd.vertex_count = vertex_count; return m_draw_vertices.end() - vertex_count; } struct DrawCmd { bool lines; u32 vertex_count; }; struct { bool m_is_active = false; DVec3 m_from; DVec3 m_to; Quat m_from_rot; Quat m_to_rot; float m_t; float m_speed; } m_go_to_parameters; WorldEditor& m_editor; SceneView& m_scene_view; Viewport m_viewport; FontResource* m_font_res; Font* m_font; MouseMode m_mouse_mode = MouseMode::NONE; SnapMode m_snap_mode = SnapMode::NONE; Vec2 m_mouse_pos; Vec2 m_mouse_sensitivity{200, 200}; bool m_is_mouse_down[(int)os::MouseButton::EXTENDED] = {}; bool m_is_mouse_click[(int)os::MouseButton::EXTENDED] = {}; StudioApp::MousePlugin* m_mouse_handling_plugin = nullptr; Vec2 m_rect_selection_start; float m_rect_selection_timer = 0; UniquePtr m_icons; RenderScene* m_scene; Array m_draw_vertices; Array m_draw_cmds; }; SceneView::SceneView(StudioApp& app) : m_app(app) , m_log_ui(app.getLogUI()) , m_editor(m_app.getWorldEditor()) { m_camera_speed = 0.1f; m_is_mouse_captured = false; m_show_stats = false; Engine& engine = m_editor.getEngine(); IAllocator& allocator = engine.getAllocator(); m_copy_move_action.init("Duplicate move", "Duplicate entity when moving with gizmo", "duplicateEntityMove", "", false); m_orbit_action.init("Orbit", "Orbit with RMB", "orbitRMB", "", false); m_toggle_gizmo_step_action.init("Enable/disable gizmo step", "Enable/disable gizmo step", "toggleGizmoStep", "", false); m_move_forward_action.init("Move forward", "Move camera forward", "moveForward", "", false); m_move_back_action.init("Move back", "Move camera back", "moveBack", "", false); m_move_left_action.init("Move left", "Move camera left", "moveLeft", "", false); m_move_right_action.init("Move right", "Move camera right", "moveRight", "", false); m_move_up_action.init("Move up", "Move camera up", "moveUp", "", false); m_move_down_action.init("Move down", "Move camera down", "moveDown", "", false); m_camera_speed_action.init(ICON_FA_CAMERA "Camera speed", "Reset camera speed", "cameraSpeed", ICON_FA_CAMERA, false); m_camera_speed_action.func.bind<&SceneView::resetCameraSpeed>(this); m_app.addAction(&m_copy_move_action); m_app.addAction(&m_orbit_action); m_app.addAction(&m_toggle_gizmo_step_action); m_app.addAction(&m_move_forward_action); m_app.addAction(&m_move_back_action); m_app.addAction(&m_move_left_action); m_app.addAction(&m_move_right_action); m_app.addAction(&m_move_up_action); m_app.addAction(&m_move_down_action); m_app.addAction(&m_camera_speed_action); const ResourceType pipeline_type("pipeline"); m_app.getAssetCompiler().registerExtension("pln", pipeline_type); } void SceneView::init() { m_view = LUMIX_NEW(m_editor.getAllocator(), UniverseViewImpl)(*this); m_editor.setView(m_view); Engine& engine = m_editor.getEngine(); auto* renderer = static_cast(engine.getPluginManager().getPlugin("renderer")); PipelineResource* pres = engine.getResourceManager().load(Path("pipelines/main.pln")); m_pipeline = Pipeline::create(*renderer, pres, "SCENE_VIEW", engine.getAllocator()); m_pipeline->addCustomCommandHandler("renderSelection").callback.bind<&SceneView::renderSelection>(this); m_pipeline->addCustomCommandHandler("renderGizmos").callback.bind<&SceneView::renderGizmos>(this); m_pipeline->addCustomCommandHandler("renderIcons").callback.bind<&SceneView::renderIcons>(this); ResourceManagerHub& rm = engine.getResourceManager(); m_debug_shape_shader = rm.load(Path("pipelines/debug_shape.shd")); } void SceneView::resetCameraSpeed() { m_camera_speed = 0.1f; } SceneView::~SceneView() { m_app.removeAction(&m_copy_move_action); m_app.removeAction(&m_orbit_action); m_app.removeAction(&m_toggle_gizmo_step_action); m_app.removeAction(&m_move_forward_action); m_app.removeAction(&m_move_back_action); m_app.removeAction(&m_move_left_action); m_app.removeAction(&m_move_right_action); m_app.removeAction(&m_move_up_action); m_app.removeAction(&m_move_down_action); m_app.removeAction(&m_camera_speed_action); m_editor.setView(nullptr); LUMIX_DELETE(m_app.getAllocator(), m_view); m_debug_shape_shader->decRefCount(); } void SceneView::manipulate() { const Array* selected = &m_editor.getSelectedEntities(); if (selected->empty()) return; const bool is_snap = m_toggle_gizmo_step_action.isActive(); Gizmo::Config& cfg = m_app.getGizmoConfig(); cfg.enableStep(is_snap); Transform tr = m_editor.getUniverse()->getTransform((*selected)[0]); tr.pos += tr.rot.rotate(cfg.getOffset()); const Transform old_pivot_tr = tr; const bool copy_move = m_copy_move_action.isActive(); if (!copy_move || !m_view->m_is_mouse_down[0]) { m_copy_moved = false; } if (!Gizmo::manipulate((*selected)[0].index, *m_view, Ref(tr), cfg)) return; if (copy_move && !m_copy_moved) { m_editor.duplicateEntities(); selected = &m_editor.getSelectedEntities(); Gizmo::setDragged((*selected)[0].index); m_copy_moved = true; } const Transform new_pivot_tr = tr; tr.pos -= tr.rot.rotate(cfg.getOffset()); Universe& universe = *m_editor.getUniverse(); switch (cfg.mode) { case Gizmo::Config::TRANSLATE: { const DVec3 diff = new_pivot_tr.pos - old_pivot_tr.pos; Array positions(m_app.getAllocator()); positions.resize(selected->size()); for (u32 i = 0, c = selected->size(); i < c; ++i) { positions[i] = universe.getPosition((*selected)[i]) + diff; } m_editor.setEntitiesPositions(selected->begin(), positions.begin(), positions.size()); break; } case Gizmo::Config::ROTATE: { Array poss(m_app.getAllocator()); Array rots(m_app.getAllocator()); rots.resize(selected->size()); poss.resize(selected->size()); for (u32 i = 0, c = selected->size(); i < c; ++i) { const Transform t = new_pivot_tr * old_pivot_tr.inverted() * universe.getTransform((*selected)[i]); poss[i] = t.pos; rots[i] = t.rot.normalized(); } m_editor.setEntitiesPositionsAndRotations(selected->begin(), poss.begin(), rots.begin(), rots.size()); break; } case Gizmo::Config::SCALE: { const float diff = new_pivot_tr.scale / old_pivot_tr.scale; Array scales(m_app.getAllocator()); scales.resize(selected->size()); for (u32 i = 0, c = selected->size(); i < c; ++i) { scales[i] = universe.getScale((*selected)[i]) * diff; } m_editor.setEntitiesScales(selected->begin(), scales.begin(), scales.size()); break; } default: ASSERT(false); break; } if (cfg.isAutosnapDown()) m_app.snapDown(); } void SceneView::update(float time_delta) { PROFILE_FUNCTION(); m_pipeline->setUniverse(m_editor.getUniverse()); m_view->update(time_delta); if (m_is_measure_active) { m_view->addCross(m_measure_from, 0.3f, Color::BLUE); m_view->addCross(m_measure_to, 0.3f, Color::BLUE); addLine(*m_view, m_measure_from, m_measure_to, Color::BLUE); } manipulate(); if (ImGui::IsAnyItemActive()) return; if (!m_is_mouse_captured) return; if (ImGui::GetIO().KeyCtrl) return; int screen_x = int(ImGui::GetIO().MousePos.x); int screen_y = int(ImGui::GetIO().MousePos.y); bool is_inside = screen_x >= m_screen_x && screen_y >= m_screen_y && screen_x <= m_screen_x + m_width && screen_y <= m_screen_y + m_height; if (!is_inside) return; m_camera_speed = maximum(0.01f, m_camera_speed + ImGui::GetIO().MouseWheel / 20.0f); float speed = m_camera_speed * time_delta * 60.f; if (ImGui::GetIO().KeyShift) speed *= 10; if (m_move_forward_action.isActive()) m_view->moveCamera(1.0f, 0, 0, speed); if (m_move_back_action.isActive()) m_view->moveCamera(-1.0f, 0, 0, speed); if (m_move_left_action.isActive()) m_view->moveCamera(0.0f, -1.0f, 0, speed); if (m_move_right_action.isActive()) m_view->moveCamera(0.0f, 1.0f, 0, speed); if (m_move_down_action.isActive()) m_view->moveCamera(0, 0, -1.0f, speed); if (m_move_up_action.isActive()) m_view->moveCamera(0, 0, 1.0f, speed); } void SceneView::renderIcons() { struct RenderJob : Renderer::RenderJob { RenderJob(IAllocator& allocator) : m_allocator(allocator) , m_items(allocator) {} void setup() override { PROFILE_FUNCTION(); Array data(m_allocator); m_ui->m_view->m_icons->getRenderData(&data); for (EditorIcons::RenderData& rd : data) { const Model* model = rd.model; if (!model || !model->isReady()) continue; for (int i = 0; i <= model->getLODIndices()[0].to; ++i) { const Mesh& mesh = model->getMesh(i); Item& item = m_items.emplace(); item.mesh = mesh.render_data; item.mtx = rd.mtx; item.material = mesh.material->getRenderData(); item.program = mesh.material->getShader()->getProgram(mesh.vertex_decl, item.material->define_mask); } } } void execute() override { PROFILE_FUNCTION(); const gpu::BufferHandle drawcall_ub = m_ui->m_pipeline->getDrawcallUniformBuffer(); for (const Item& item : m_items) { const Mesh::RenderData* rd = item.mesh; gpu::update(drawcall_ub, &item.mtx.columns[0].x, sizeof(item.mtx)); gpu::bindTextures(item.material->textures, 0, item.material->textures_count); gpu::useProgram(item.program); gpu::bindIndexBuffer(rd->index_buffer_handle); gpu::bindVertexBuffer(0, rd->vertex_buffer_handle, 0, rd->vb_stride); gpu::bindVertexBuffer(1, gpu::INVALID_BUFFER, 0, 0); gpu::setState(item.material->render_states); gpu::drawTriangles(0, rd->indices_count, rd->index_type); } } struct Item { gpu::ProgramHandle program; Mesh::RenderData* mesh; Material::RenderData* material; Matrix mtx; }; IAllocator& m_allocator; Array m_items; SceneView* m_ui; }; Engine& engine = m_editor.getEngine(); Renderer* renderer = static_cast(engine.getPluginManager().getPlugin("renderer")); IAllocator& allocator = renderer->getAllocator(); RenderJob& cmd = renderer->createJob(allocator); cmd.m_ui = this; renderer->queue(cmd, 0); } void SceneView::renderSelection() { struct RenderJob : Renderer::RenderJob { RenderJob(IAllocator& allocator) : m_allocator(allocator) , m_items(allocator) {} void setup() override { PROFILE_FUNCTION(); const Array& entities = m_editor->getSelectedEntities(); RenderScene* scene = m_pipeline->getScene(); const Universe& universe = scene->getUniverse(); for (EntityRef e : entities) { if (!scene->getUniverse().hasComponent(e, MODEL_INSTANCE_TYPE)) continue; const Model* model = scene->getModelInstanceModel(e); if (!model || !model->isReady()) continue; const Pose* pose = scene->lockPose(e); for (int i = 0; i <= model->getLODIndices()[0].to; ++i) { const Mesh& mesh = model->getMesh(i); Item& item = m_items.emplace(m_allocator); item.material = mesh.material->getRenderData(); u32 define_mask = item.material->define_mask; item.mesh = mesh.render_data; item.mtx = universe.getRelativeMatrix(e, m_cam_pos); if (pose && pose->count > 0 && mesh.type == Mesh::SKINNED) { define_mask |= skinned_define; item.pose.push(item.mtx); for (int j = 0, c = pose->count; j < c; ++j) { const Model::Bone& bone = model->getBone(j); const LocalRigidTransform tmp = {pose->positions[j], pose->rotations[j]}; item.pose.push((tmp * bone.inv_bind_transform).toMatrix()); } } item.program = mesh.material->getShader()->getProgram(mesh.vertex_decl, define_mask); } scene->unlockPose(e, false); } } void execute() override { PROFILE_FUNCTION(); const gpu::BufferHandle drawcall_ub = m_pipeline->getDrawcallUniformBuffer(); gpu::pushDebugGroup("selection"); for (const Item& item : m_items) { const Mesh::RenderData* rd = item.mesh; if (item.pose.empty()) { gpu::update(drawcall_ub, &item.mtx.columns[0].x, sizeof(item.mtx)); } else { struct { float layer; float fur_scale; float gravity; float padding; Matrix bones[256]; } dc; ASSERT(item.pose.size() < (i32)lengthOf(dc.bones)); dc.layer = 0; dc.fur_scale = 0; dc.gravity = 0; memcpy(dc.bones, item.pose.begin(), item.pose.byte_size()); const u32 size = item.pose.byte_size() + sizeof(Vec4); gpu::update(drawcall_ub, &dc, size); gpu::bindUniformBuffer(UniformBuffer::DRAWCALL, drawcall_ub, 0, size); } gpu::bindTextures(item.material->textures, 0, item.material->textures_count); gpu::useProgram(item.program); gpu::bindIndexBuffer(rd->index_buffer_handle); gpu::bindVertexBuffer(0, rd->vertex_buffer_handle, 0, rd->vb_stride); gpu::bindVertexBuffer(1, gpu::INVALID_BUFFER, 0, 0); gpu::setState(item.material->render_states); gpu::drawTriangles(0, rd->indices_count, rd->index_type); } gpu::popDebugGroup(); } struct Item { Item(IAllocator& allocator) : pose(allocator) {} Array pose; gpu::ProgramHandle program; Mesh::RenderData* mesh; Material::RenderData* material; Matrix mtx; }; IAllocator& m_allocator; u32 skinned_define; Array m_items; Pipeline* m_pipeline; WorldEditor* m_editor; DVec3 m_cam_pos; }; Engine& engine = m_editor.getEngine(); Renderer* renderer = static_cast(engine.getPluginManager().getPlugin("renderer")); IAllocator& allocator = renderer->getAllocator(); RenderJob& job = renderer->createJob(allocator); job.skinned_define = 1 << renderer->getShaderDefineIdx("SKINNED"); job.m_pipeline = m_pipeline.get(); job.m_editor = &m_editor; job.m_cam_pos = m_view->getViewport().pos; renderer->queue(job, 0); } void SceneView::renderGizmos() { struct Cmd : Renderer::RenderJob { Cmd(IAllocator& allocator) : cmds(allocator) {} void setup() override { PROFILE_FUNCTION(); viewport = view->m_view->getViewport(); cmds.swap(view->m_view->m_draw_cmds); Engine& engine = view->m_editor.getEngine(); renderer = static_cast(engine.getPluginManager().getPlugin("renderer")); auto& vertices = view->m_view->m_draw_vertices; vb = renderer->allocTransient(vertices.byte_size()); memcpy(vb.ptr, vertices.begin(), vertices.byte_size()); } void execute() override { PROFILE_FUNCTION(); if (cmds.empty()) return; renderer->beginProfileBlock("gizmos", 0); gpu::pushDebugGroup("gizmos"); gpu::setState(gpu::StateFlags::DEPTH_TEST | gpu::StateFlags::DEPTH_WRITE); u32 offset = 0; const gpu::BufferHandle drawcall_ub = view->getPipeline()->getDrawcallUniformBuffer(); const Matrix mtx = Matrix::IDENTITY; for (const UniverseViewImpl::DrawCmd& cmd : cmds) { gpu::update(drawcall_ub, &mtx.columns[0].x, sizeof(mtx)); gpu::useProgram(program); gpu::bindIndexBuffer(gpu::INVALID_BUFFER); gpu::bindVertexBuffer(0, vb.buffer, vb.offset + offset, sizeof(UniverseView::Vertex)); gpu::bindVertexBuffer(1, gpu::INVALID_BUFFER, 0, 0); const gpu::PrimitiveType primitive_type = cmd.lines ? gpu::PrimitiveType::LINES : gpu::PrimitiveType::TRIANGLES; gpu::drawArrays(primitive_type, 0, cmd.vertex_count); offset += cmd.vertex_count * sizeof(UniverseView::Vertex); } gpu::popDebugGroup(); renderer->endProfileBlock(); } Renderer* renderer; Renderer::TransientSlice ib; Renderer::TransientSlice vb; Viewport viewport; SceneView* view; gpu::ProgramHandle program; Array cmds; }; if (!m_debug_shape_shader || !m_debug_shape_shader->isReady()) return; Engine& engine = m_editor.getEngine(); Renderer* renderer = static_cast(engine.getPluginManager().getPlugin("renderer")); IAllocator& allocator = renderer->getAllocator(); Cmd& cmd = renderer->createJob(allocator); gpu::VertexDecl decl; decl.addAttribute(0, 0, 3, gpu::AttributeType::FLOAT, 0); decl.addAttribute(1, 12, 4, gpu::AttributeType::U8, gpu::Attribute::NORMALIZED); cmd.program = m_debug_shape_shader->getProgram(decl, 0); cmd.view = this; renderer->queue(cmd, 0); } void SceneView::captureMouse(bool capture) { if(m_is_mouse_captured == capture) return; m_is_mouse_captured = capture; os::showCursor(!m_is_mouse_captured); if (capture) { const os::Point p = os::getMouseScreenPos(); m_captured_mouse_x = p.x; m_captured_mouse_y = p.y; } else { os::setMouseScreenPos(m_captured_mouse_x, m_captured_mouse_y); os::unclipCursor(); } } RayCastModelHit SceneView::castRay(float x, float y) { auto* scene = m_pipeline->getScene(); ASSERT(scene); const Viewport& vp = m_view->getViewport(); DVec3 origin; Vec3 dir; vp.getRay({x * vp.w, y * vp.h}, origin, dir); return scene->castRay(origin, dir, INVALID_ENTITY); } void SceneView::handleDrop(const char* path, float x, float y) { const RayCastModelHit hit = castRay(x, y); if (Path::hasExtension(path, "par")) { const DVec3 pos = hit.origin + (hit.is_hit ? hit.t : 5) * hit.dir; m_editor.beginCommandGroup(crc32("insert_particle")); EntityRef entity = m_editor.addEntity(); m_editor.setEntitiesPositions(&entity, &pos, 1); m_editor.addComponent(Span(&entity, 1), PARTICLE_EMITTER_TYPE); m_editor.setProperty(PARTICLE_EMITTER_TYPE, "", -1, "Source", Span(&entity, 1), Path(path)); m_editor.endCommandGroup(); } else if (Path::hasExtension(path, "fbx")) { const DVec3 pos = hit.origin + (hit.is_hit ? hit.t : 5) * hit.dir; m_editor.beginCommandGroup(crc32("insert_mesh")); EntityRef entity = m_editor.addEntity(); m_editor.setEntitiesPositions(&entity, &pos, 1); m_editor.addComponent(Span(&entity, 1), MODEL_INSTANCE_TYPE); m_editor.setProperty(MODEL_INSTANCE_TYPE, "", -1, "Source", Span(&entity, 1), Path(path)); m_editor.endCommandGroup(); } else if (Path::hasExtension(path, "fab")) { ResourceManagerHub& manager = m_editor.getEngine().getResourceManager(); PrefabResource* prefab = manager.load(Path(path)); const DVec3 pos = hit.origin + (hit.is_hit ? hit.t : 1) * hit.dir; if (prefab->isEmpty()) { FileSystem& fs = m_editor.getEngine().getFileSystem(); while (fs.hasWork()) fs.processCallbacks(); } if (prefab->isReady()) { m_editor.getPrefabSystem().instantiatePrefab(*prefab, pos, Quat::IDENTITY, 1); } else { ASSERT(prefab->isFailure()); logError("Failed to load ", prefab->getPath()); } } else if (Path::hasExtension(path, "phy")) { if (hit.is_hit && hit.entity.isValid()) { m_editor.beginCommandGroup(crc32("insert_phy_component")); const EntityRef e = (EntityRef)hit.entity; m_editor.selectEntities(Span(&e, 1), false); m_editor.addComponent(Span(&e, 1), MESH_ACTOR_TYPE); m_editor.setProperty(MESH_ACTOR_TYPE, "", -1, "Source", Span(&e, 1), path); m_editor.endCommandGroup(); } else { const DVec3 pos = hit.origin + (hit.is_hit ? hit.t : 1) * hit.dir; m_editor.beginCommandGroup(crc32("insert_phy")); EntityRef entity = m_editor.addEntity(); m_editor.setEntitiesPositions(&entity, &pos, 1); m_editor.selectEntities(Span(&entity, 1), false); m_editor.addComponent(Span(&entity, 1), MESH_ACTOR_TYPE); m_editor.setProperty(MESH_ACTOR_TYPE, "", -1, "Source", Span(&entity, 1), path); m_editor.endCommandGroup(); } } } void SceneView::onToolbar() { static const char* actions_names[] = { "setTranslateGizmoMode", "setRotateGizmoMode", "setScaleGizmoMode", "setLocalCoordSystem", "setGlobalCoordSystem", "viewTop", "viewFront", "viewSide" }; auto pos = ImGui::GetCursorScreenPos(); if (ImGuiEx::BeginToolbar("scene_view_toolbar", pos, ImVec2(0, 24))) { for (auto* action_name : actions_names) { auto* action = m_app.getAction(action_name); action->toolbarButton(m_app.getBigIconFont()); } } m_app.getAction("cameraSpeed")->toolbarButton(m_app.getBigIconFont()); ImGui::PushItemWidth(50); ImGui::SameLine(); float offset = (24 - ImGui::GetTextLineHeightWithSpacing()) / 2; pos = ImGui::GetCursorPos(); pos.y += offset; ImGui::SetCursorPos(pos); ImGui::DragFloat("##camera_speed", &m_camera_speed, 0.1f, 0.01f, 999.0f, "%.2f"); Action* mode_action; if (m_app.getGizmoConfig().isTranslateMode()) { mode_action = m_app.getAction("setTranslateGizmoMode"); } else { mode_action = m_app.getAction("setRotateGizmoMode"); } ImGui::SameLine(); pos = ImGui::GetCursorPos(); pos.y -= offset; ImGui::SetCursorPos(pos); ImVec4 tint_color = ImGui::GetStyle().Colors[ImGuiCol_Text]; ImGui::TextUnformatted(mode_action->font_icon); ImGui::SameLine(); pos = ImGui::GetCursorPos(); pos.y += offset; ImGui::SetCursorPos(pos); float step = m_app.getGizmoConfig().getStep(); if (ImGui::DragFloat("##gizmoStep", &step, 1.0f, 0, 200)) { m_app.getGizmoConfig().setStep(step); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", "Snap amount"); ImGui::SameLine(); Viewport vp = m_view->getViewport(); if (ImGui::Checkbox("Ortho", &vp.is_ortho)) { m_view->setViewport(vp); } if (vp.is_ortho) { ImGui::SameLine(); if (ImGui::DragFloat("Ortho size", &vp.ortho_size)) { m_view->setViewport(vp); } } ImGui::SameLine(0); ImGui::Checkbox("Stats", &m_show_stats); ImGui::SameLine(0); m_pipeline->callLuaFunction("onGUI"); ImGui::SameLine(0); ImGui::Checkbox("Measure", &m_is_measure_active); if (m_is_measure_active) { ImGui::SameLine(0, 20); const double d = (m_measure_to - m_measure_from).length(); ImGui::Text(" | Measured distance: %f", d); } ImGui::PopItemWidth(); ImGuiEx::EndToolbar(); } void SceneView::handleEvents() { const bool handle_input = m_is_mouse_captured || (ImGui::IsItemHovered() && os::getFocused() == ImGui::GetWindowViewport()->PlatformHandle); const os::Event* events = m_app.getEvents(); for (int i = 0, c = m_app.getEventsCount(); i < c; ++i) { const os::Event& event = events[i]; switch (event.type) { case os::Event::Type::MOUSE_BUTTON: { const os::Point cp = os::getMouseScreenPos(); Vec2 rel_mp = { (float)cp.x, (float)cp.y }; rel_mp.x -= m_screen_x; rel_mp.y -= m_screen_y; if (handle_input) { if (event.mouse_button.button == os::MouseButton::RIGHT) { ImGui::SetWindowFocus(); captureMouse(event.mouse_button.down); } ImGuiEx::ResetActiveID(); if (event.mouse_button.down) { m_view->onMouseDown((int)rel_mp.x, (int)rel_mp.y, event.mouse_button.button); } else { m_view->onMouseUp((int)rel_mp.x, (int)rel_mp.y, event.mouse_button.button); } } else if (!event.mouse_button.down) { m_view->onMouseUp((int)rel_mp.x, (int)rel_mp.y, event.mouse_button.button); } break; } case os::Event::Type::MOUSE_MOVE: if (handle_input) { const os::Point cp = os::getMouseScreenPos(); Vec2 rel_mp = {(float)cp.x, (float)cp.y}; rel_mp.x -= m_screen_x; rel_mp.y -= m_screen_y; m_view->onMouseMove((int)rel_mp.x, (int)rel_mp.y, (int)event.mouse_move.xrel, (int)event.mouse_move.yrel); } break; } } } void SceneView::statsUI(float x, float y) { if (!m_show_stats) return; float toolbar_height = 24 + ImGui::GetStyle().FramePadding.y * 2; ImVec2 view_pos(x, y); view_pos.x += ImGui::GetStyle().FramePadding.x; view_pos.y += ImGui::GetStyle().FramePadding.y; ImGui::SetNextWindowPos(view_pos); auto col = ImGui::GetStyle().Colors[ImGuiCol_WindowBg]; col.w = 0.3f; ImGui::PushStyleColor(ImGuiCol_WindowBg, col); if (ImGui::Begin("###stats_overlay", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings)) { const auto& stats = m_pipeline->getStats(); ImGui::LabelText("Draw calls (scene view only)", "%d", stats.draw_call_count); ImGui::LabelText("Instances (scene view only)", "%d", stats.instance_count); char buf[30]; toCStringPretty(stats.triangle_count, Span(buf)); ImGui::LabelText("Triangles (scene view only)", "%s", buf); ImGui::LabelText("Resolution", "%dx%d", m_width, m_height); } ImGui::End(); ImGui::PopStyleColor(); } void SceneView::onWindowGUI() { PROFILE_FUNCTION(); m_pipeline->setUniverse(m_editor.getUniverse()); bool is_open = false; ImVec2 view_pos; const char* title = ICON_FA_GLOBE "Scene View###Scene View"; if (m_log_ui.getUnreadErrorCount() > 0) title = ICON_FA_GLOBE "Scene View | " ICON_FA_EXCLAMATION_TRIANGLE " errors in log###Scene View"; ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); if (ImGui::Begin(title, nullptr, ImGuiWindowFlags_NoScrollWithMouse)) { is_open = true; ImGui::Dummy(ImVec2(2, 2)); onToolbar(); const ImVec2 size = ImGui::GetContentRegionAvail(); Viewport vp = m_view->getViewport(); vp.w = (int)size.x; vp.h = (int)size.y; m_view->setViewport(vp); m_pipeline->setViewport(vp); m_pipeline->render(false); m_view->m_draw_vertices.clear(); m_view->m_draw_cmds.clear(); m_view->inputFrame(); const gpu::TextureHandle texture_handle = m_pipeline->getOutput(); if (size.x > 0 && size.y > 0) { const ImVec2 cursor_pos = ImGui::GetCursorScreenPos(); m_screen_x = int(cursor_pos.x); m_screen_y = int(cursor_pos.y); m_width = int(size.x); m_height = int(size.y); view_pos = ImGui::GetCursorScreenPos(); if (texture_handle) { void* t = texture_handle; if (gpu::isOriginBottomLeft()) { ImGui::Image(t, size, ImVec2(0, 1), ImVec2(1, 0)); } else { ImGui::Image(t, size); } } if (m_is_mouse_captured) { const ImVec2 pos = ImGui::GetItemRectMin(); const ImVec2 size = ImGui::GetItemRectSize(); os::clipCursor((int)pos.x, (int)pos.y, (int)size.x, (int)size.y); } if (ImGui::BeginDragDropTarget()) { if (auto* payload = ImGui::AcceptDragDropPayload("path")) { const ImVec2 drop_pos = (ImGui::GetMousePos() - view_pos) / size; handleDrop((const char*)payload->Data, drop_pos.x, drop_pos.y); } ImGui::EndDragDropTarget(); } handleEvents(); } } else { m_view->inputFrame(); m_view->m_draw_vertices.clear(); m_view->m_draw_cmds.clear(); } if (m_is_mouse_captured && os::getFocused() != ImGui::GetWindowViewport()->PlatformHandle) { captureMouse(false); } ImGui::End(); ImGui::PopStyleVar(); if (is_open) statsUI(view_pos.x, view_pos.y); } } // namespace Lumix