mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 08:39:19 +02:00
feat(editor): icons over the 3d view: the tools of the field of work along its left edge (the edit modes of the surroundings, the track tools, the road tools with the lanes, the terrain tools with the orthophoto), the modes of the gizmo at its top, the axes of the world as the camera sees them in its top right corner, a click on one looking along it, with the top view under them; the icons drawn by editor/icons/editor_icons.py into a png which CMake writes out as an array built into the executable; the gizmo modes and the top view left out of the toolbar; Polish translation
This commit is contained in:
398
application/editormodeviewport.cpp
Normal file
398
application/editormodeviewport.cpp
Normal file
@@ -0,0 +1,398 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
// over the 3d view: the tools of the field of work along its left edge, the modes of the gizmo at its top, the axes of the
|
||||
// world and the top view in its top right corner
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "application/editormode.h"
|
||||
#include "application/editoruilayer.h"
|
||||
#include "editor/editorIcons.hpp"
|
||||
#include "rendering/renderer.h"
|
||||
#include "utilities/Globals.h"
|
||||
#include "utilities/translation.h"
|
||||
#include "imgui/imgui.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
using editor_icons::icon;
|
||||
|
||||
struct tool_button
|
||||
{
|
||||
icon picture;
|
||||
char const *label;
|
||||
char const *key; // nullptr if there's none
|
||||
char const *tooltip; // nullptr if the label says enough
|
||||
bool chosen;
|
||||
std::function<void()> action;
|
||||
bool group{false}; // a gap before it, it starts another group
|
||||
bool enabled{true};
|
||||
};
|
||||
|
||||
float scale()
|
||||
{
|
||||
return std::max(1.0f, Global.ui_scale);
|
||||
}
|
||||
|
||||
// side of a button, the icon with some room around it
|
||||
float button_side()
|
||||
{
|
||||
return editor_icons::size() + 10.0f * scale();
|
||||
}
|
||||
|
||||
// a window of no looks of its own, for the buttons over the 3d view; it takes the mouse only where it lies
|
||||
bool begin_overlay(char const *Name, ImVec2 const &Position, ImVec2 const &Size)
|
||||
{
|
||||
ImGui::SetNextWindowViewport(ImGui::GetMainViewport()->ID);
|
||||
ImGui::SetNextWindowPos(Position);
|
||||
ImGui::SetNextWindowSize(Size);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(1.0f, 1.0f));
|
||||
auto const flags{ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav |
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoScrollWithMouse};
|
||||
auto const open{ImGui::Begin(Name, nullptr, flags)};
|
||||
ImGui::PopStyleVar(3);
|
||||
return open;
|
||||
}
|
||||
|
||||
// the button with the icon at the place; the label stands in for the icon if the renderer can't show images
|
||||
void icon_button(tool_button const &Button, ImVec2 const &Position, int const Id)
|
||||
{
|
||||
auto const side{button_side()};
|
||||
ImGui::SetCursorScreenPos(Position);
|
||||
ImGui::PushID(Id);
|
||||
auto const clicked{ImGui::InvisibleButton("##tool", ImVec2(side, side))};
|
||||
ImGui::PopID();
|
||||
auto const hovered{ImGui::IsItemHovered()};
|
||||
auto *list{ImGui::GetWindowDrawList()};
|
||||
ImVec2 const end{Position.x + side, Position.y + side};
|
||||
auto const rounding{4.0f * scale()};
|
||||
auto const background{Button.chosen ? ImGui::GetColorU32(ImGuiCol_ButtonActive) :
|
||||
hovered && Button.enabled ? ImGui::GetColorU32(ImGuiCol_ButtonHovered) :
|
||||
IM_COL32(24, 26, 30, 200)};
|
||||
list->AddRectFilled(Position, end, background, rounding);
|
||||
if (Button.chosen)
|
||||
list->AddRect(Position, end, ImGui::GetColorU32(ImGuiCol_CheckMark), rounding, 0, 1.5f * scale());
|
||||
auto const color{Button.enabled ? (Button.chosen || hovered ? IM_COL32(255, 255, 255, 255) : IM_COL32(215, 220, 225, 235)) : IM_COL32(150, 150, 150, 110)};
|
||||
ImVec2 const center{Position.x + side * 0.5f, Position.y + side * 0.5f};
|
||||
if (false == editor_icons::draw(list, Button.picture, center, color))
|
||||
{
|
||||
char const text[]{Button.label[0], (Button.label[0] != '\0' ? Button.label[1] : '\0'), '\0'};
|
||||
auto const size{ImGui::CalcTextSize(text)};
|
||||
list->AddText(ImVec2(center.x - size.x * 0.5f, center.y - size.y * 0.5f), color, text);
|
||||
}
|
||||
if (hovered)
|
||||
{
|
||||
std::string tip{Button.label};
|
||||
if (Button.key != nullptr)
|
||||
tip += std::string{" ["} + Button.key + "]";
|
||||
if (Button.tooltip != nullptr)
|
||||
tip += std::string{"\n"} + Button.tooltip;
|
||||
ImGui::SetTooltip("%s", tip.c_str());
|
||||
}
|
||||
if (clicked && Button.enabled && Button.action)
|
||||
Button.action();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void editor_mode::render_viewport_overlays()
|
||||
{
|
||||
auto const width{editor_ui::view_max().x - editor_ui::view_min().x};
|
||||
auto const height{editor_ui::view_max().y - editor_ui::view_min().y};
|
||||
// a view squeezed between the docks has no room for them
|
||||
if (width < 8.0f * button_side() || height < 6.0f * button_side())
|
||||
return;
|
||||
render_viewport_tools();
|
||||
render_viewport_gizmo();
|
||||
render_view_axes();
|
||||
}
|
||||
|
||||
void editor_mode::render_viewport_tools()
|
||||
{
|
||||
// the tools of the field of work, as in the toolbar
|
||||
std::vector<tool_button> buttons;
|
||||
buttons.reserve(16);
|
||||
auto const area{current_work_area()};
|
||||
switch (area)
|
||||
{
|
||||
case work_area::surroundings:
|
||||
{
|
||||
struct mode
|
||||
{
|
||||
icon picture;
|
||||
char const *label;
|
||||
char const *key;
|
||||
nodebank_panel::edit_mode which;
|
||||
};
|
||||
mode const modes[] = {{icon::select, STR_C("Select"), "1", nodebank_panel::MODIFY},
|
||||
{icon::insert, STR_C("Insert"), "2", nodebank_panel::ADD},
|
||||
{icon::brush, STR_C("Brush"), "3", nodebank_panel::BRUSH},
|
||||
{icon::area_fill, STR_C("Area fill"), "4", nodebank_panel::FILL},
|
||||
{icon::copy_to_bank, STR_C("Copy to bank"), "5", nodebank_panel::COPY}};
|
||||
for (auto const &entry : modes)
|
||||
buttons.push_back({entry.picture, entry.label, entry.key, nullptr, ui()->mode() == entry.which, [this, which = entry.which]() { choose_edit_mode(which); }});
|
||||
break;
|
||||
}
|
||||
case work_area::tracks:
|
||||
{
|
||||
auto const vehicle{m_track_tab == track_tab::lineside && m_vehicle.open};
|
||||
auto const add = [&](track_tab const Tab, icon const Picture, bool const Group) {
|
||||
auto const &modes{track_modes()};
|
||||
auto const found{std::find_if(modes.begin(), modes.end(), [&](track_mode const &Mode) { return Mode.tab == Tab; })};
|
||||
if (found == modes.end())
|
||||
return;
|
||||
// a tool chosen again gives way to the select one, as in the toolbar
|
||||
auto const chosen{m_track_tab == Tab && false == (Tab == track_tab::lineside && vehicle)};
|
||||
buttons.push_back({Picture, STR_C(found->label), found->key, STR_C(found->tooltip), chosen, [this, Tab, chosen]() { show_track_tab(chosen ? track_tab::path : Tab); }, Group});
|
||||
};
|
||||
add(track_tab::path, icon::select, false);
|
||||
add(track_tab::lay, icon::lay_track, false);
|
||||
add(track_tab::turnout, icon::turnout, false);
|
||||
add(track_tab::straights, icon::straight, false);
|
||||
add(track_tab::route, icon::curve, false);
|
||||
add(track_tab::turntable, icon::turntable, false);
|
||||
add(track_tab::signals, icon::signal, true);
|
||||
add(track_tab::lineside, icon::objects, false);
|
||||
buttons.push_back({icon::vehicle, STR_C("Vehicle"), nullptr, STR_C("A vehicle to drive, put on the selected track; it isn't written to the scenery files"), vehicle, [this, vehicle]() {
|
||||
show_track_tab(vehicle ? track_tab::path : track_tab::lineside);
|
||||
m_vehicle.expand = false == vehicle;
|
||||
}});
|
||||
add(track_tab::profile, icon::profile, true);
|
||||
add(track_tab::speed, icon::speed, false);
|
||||
add(track_tab::joints, icon::joints, false);
|
||||
add(track_tab::infra, icon::infra, false);
|
||||
buttons.push_back({icon::gauge, STR_C("Structure gauge"), nullptr, STR_C("Checks which models enter the structure gauge of the tracks and the clearance over the roads (skrajnia budowli)"), m_gauge.open,
|
||||
[this]() { m_gauge.open = !m_gauge.open; }});
|
||||
break;
|
||||
}
|
||||
case work_area::roads:
|
||||
{
|
||||
auto &tool{m_roadtool};
|
||||
buttons.push_back({icon::select, STR_C("Select"), nullptr, STR_C("LMB: a road piece, a junction, a point where pieces meet, a level crossing or a traffic point"), tool.tool == 0, [this]() { road_choose_tool(0); }});
|
||||
buttons.push_back({icon::road, STR_C("Build"), nullptr, STR_C("LMB: start a road, then each next point; on a loose end, the side of a road or a junction it's joined to them"), tool.tool == 1, [this]() { road_choose_tool(1); }});
|
||||
buttons.push_back({icon::place, STR_C("Place"), nullptr, STR_C("Level crossings, and the points where vehicles appear on the roads or are taken off them"), tool.tool == 2, [this]() { road_choose_tool(2); }});
|
||||
buttons.push_back({icon::lanes, STR_C("Show lanes"), nullptr, STR_C("The lanes of the roads drawn as the paths the vehicles take"), tool.lanes, [&tool]() { tool.lanes = !tool.lanes; }, true});
|
||||
break;
|
||||
}
|
||||
case work_area::terrain:
|
||||
{
|
||||
// what the left button does on the terrain, as in the toolbar
|
||||
auto const choose = [this](bool const Sculpt, bool const Smooth, bool const Chunks) {
|
||||
return [this, Sculpt, Smooth, Chunks]() {
|
||||
m_terrain_sculpt = Sculpt;
|
||||
m_terrain_brush_smooth = Smooth;
|
||||
m_chunk_edit = Chunks;
|
||||
};
|
||||
};
|
||||
buttons.push_back({icon::select, STR_C("Select"), nullptr, STR_C("LMB picks the models, as in the surroundings"), false == m_terrain_sculpt && false == m_chunk_edit, choose(false, m_terrain_brush_smooth, false)});
|
||||
buttons.push_back({icon::sculpt, STR_C("Sculpt"), nullptr, STR_C("LMB raises the terrain under the brush, Shift+LMB lowers it"), m_terrain_sculpt && false == m_terrain_brush_smooth, choose(true, false, false)});
|
||||
buttons.push_back({icon::smooth, STR_C("Smooth"), nullptr, STR_C("LMB evens the terrain out under the brush"), m_terrain_sculpt && m_terrain_brush_smooth, choose(true, true, false)});
|
||||
buttons.push_back({icon::chunks, STR_C("Chunks"), nullptr, STR_C("LMB adds a chunk next to the clicked one, Shift+LMB deletes it"), m_chunk_edit, choose(false, m_terrain_brush_smooth, true)});
|
||||
auto const orthophoto{m_orthophoto.enabled()};
|
||||
buttons.push_back({icon::orthophoto, STR_C("Orthophoto"), nullptr, STR_C("Aerial imagery of geoportal.gov.pl under the scenery, laid out by the origin of the scenery"), orthophoto,
|
||||
[this, orthophoto]() { m_orthophoto.enabled(false == orthophoto); }, true});
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (buttons.empty())
|
||||
return;
|
||||
|
||||
// down the left edge, in more columns if the view is too low for them; the bottom is left to the hints of the tools
|
||||
auto const side{button_side()};
|
||||
auto const gap{2.0f * scale()};
|
||||
auto const groupgap{8.0f * scale()};
|
||||
auto const margin{8.0f * scale()};
|
||||
ImVec2 const origin{editor_ui::view_min().x + margin, editor_ui::view_min().y + margin};
|
||||
auto const bottom{std::max(origin.y + 4.0f * side, editor_ui::view_max().y - 130.0f * scale())};
|
||||
std::vector<ImVec2> places;
|
||||
places.reserve(buttons.size());
|
||||
ImVec2 place{origin};
|
||||
for (auto const &button : buttons)
|
||||
{
|
||||
if (button.group && place.y > origin.y)
|
||||
place.y += groupgap - gap;
|
||||
if (place.y > origin.y && place.y + side > bottom)
|
||||
place = ImVec2(place.x + side + gap, origin.y);
|
||||
places.push_back(place);
|
||||
place.y += side + gap;
|
||||
}
|
||||
ImVec2 extent{0.0f, 0.0f};
|
||||
for (auto const &at : places)
|
||||
extent = ImVec2(std::max(extent.x, at.x + side - origin.x), std::max(extent.y, at.y + side - origin.y));
|
||||
auto const pad{3.0f * scale()};
|
||||
if (begin_overlay("##viewporttools", ImVec2(origin.x - pad, origin.y - pad), ImVec2(extent.x + 2.0f * pad, extent.y + 2.0f * pad)))
|
||||
{
|
||||
ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(origin.x - pad, origin.y - pad), ImVec2(origin.x + extent.x + pad, origin.y + extent.y + pad), IM_COL32(16, 18, 21, 140), 6.0f * scale());
|
||||
for (std::size_t index = 0; index < buttons.size(); ++index)
|
||||
icon_button(buttons[index], places[index], static_cast<int>(index));
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void editor_mode::render_viewport_gizmo()
|
||||
{
|
||||
// the gizmo moves the models and the tracks; on the terrain the mouse is the brush's
|
||||
if (false == m_gizmo_enabled || current_work_area() == work_area::terrain)
|
||||
return;
|
||||
auto const tooltip{STR_C("What the handles of the gizmo on the selection do when dragged; Ctrl held snaps the values")};
|
||||
std::array<tool_button, 4> const buttons{{
|
||||
{icon::translate, STR_C("Translate"), "Q", tooltip, m_gizmo_op == gizmo_operation::translate, [this]() { m_gizmo_op = gizmo_operation::translate; }},
|
||||
{icon::rotate, STR_C("Rotate"), "W", tooltip, m_gizmo_op == gizmo_operation::rotate, [this]() { m_gizmo_op = gizmo_operation::rotate; }},
|
||||
{icon::scale, STR_C("Scale"), "E", tooltip, m_gizmo_op == gizmo_operation::scale, [this]() { m_gizmo_op = gizmo_operation::scale; }},
|
||||
// the gizmo always scales along the axes of the model
|
||||
{icon::local_space, STR_C("Local space"), "R", STR_C("The handles along the axes of the selected model instead of the axes of the world"), m_gizmo_local || m_gizmo_op == gizmo_operation::scale,
|
||||
[this]() { m_gizmo_local = !m_gizmo_local; }, true, m_gizmo_op != gizmo_operation::scale},
|
||||
}};
|
||||
// in a row at the top, in the middle
|
||||
auto const side{button_side()};
|
||||
auto const gap{2.0f * scale()};
|
||||
auto const groupgap{8.0f * scale()};
|
||||
auto const margin{8.0f * scale()};
|
||||
auto const pad{3.0f * scale()};
|
||||
auto const width{4.0f * side + 2.0f * gap + groupgap};
|
||||
ImVec2 const origin{std::round((editor_ui::view_min().x + editor_ui::view_max().x - width) * 0.5f), editor_ui::view_min().y + margin};
|
||||
if (begin_overlay("##viewportgizmo", ImVec2(origin.x - pad, origin.y - pad), ImVec2(width + 2.0f * pad, side + 2.0f * pad)))
|
||||
{
|
||||
ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(origin.x - pad, origin.y - pad), ImVec2(origin.x + width + pad, origin.y + side + pad), IM_COL32(16, 18, 21, 140), 6.0f * scale());
|
||||
auto x{origin.x};
|
||||
for (std::size_t index = 0; index < buttons.size(); ++index)
|
||||
{
|
||||
if (buttons[index].group)
|
||||
x += groupgap - gap;
|
||||
icon_button(buttons[index], ImVec2(x, origin.y), static_cast<int>(index));
|
||||
x += side + gap;
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void editor_mode::render_view_axes()
|
||||
{
|
||||
// the axes of the world as the camera sees them, as in blender: a click on one looks along it
|
||||
auto const s{scale()};
|
||||
auto const radius{40.0f * s};
|
||||
auto const margin{8.0f * s};
|
||||
auto const side{button_side()};
|
||||
auto const size{2.0f * radius};
|
||||
ImVec2 const origin{editor_ui::view_max().x - margin - size, editor_ui::view_min().y + margin};
|
||||
if (begin_overlay("##viewaxes", origin, ImVec2(size, size + 6.0f * s + side)))
|
||||
{
|
||||
auto *list{ImGui::GetWindowDrawList()};
|
||||
ImVec2 const center{origin.x + radius, origin.y + radius};
|
||||
ImGui::SetCursorScreenPos(origin);
|
||||
ImGui::InvisibleButton("##axes", ImVec2(size, size));
|
||||
auto const hovered{ImGui::IsItemHovered()};
|
||||
auto const clicked{ImGui::IsItemClicked(ImGuiMouseButton_Left)};
|
||||
if (hovered)
|
||||
list->AddCircleFilled(center, radius, IM_COL32(255, 255, 255, 28), 48);
|
||||
// the view matrix is of the camera turned only, the axes go through it as they are
|
||||
glm::mat3 const view{GfxRenderer->Camera_View_Matrix()};
|
||||
struct end
|
||||
{
|
||||
int axis;
|
||||
bool positive;
|
||||
glm::vec3 seen;
|
||||
};
|
||||
std::array<end, 6> ends;
|
||||
for (int axis = 0; axis < 3; ++axis)
|
||||
{
|
||||
glm::vec3 direction{0.0f};
|
||||
direction[axis] = 1.0f;
|
||||
auto const seen{view * direction};
|
||||
ends[axis * 2] = {axis, true, seen};
|
||||
ends[axis * 2 + 1] = {axis, false, -seen};
|
||||
}
|
||||
// the farther ones first, the nearer ones over them
|
||||
std::sort(ends.begin(), ends.end(), [](end const &Left, end const &Right) { return Left.seen.z < Right.seen.z; });
|
||||
ImU32 const colors[]{IM_COL32(230, 70, 80, 255), IM_COL32(130, 200, 40, 255), IM_COL32(60, 135, 235, 255)};
|
||||
char const *const names[]{"X", "Y", "Z"};
|
||||
auto const reach{radius - 10.0f * s};
|
||||
auto const mouse{ImGui::GetIO().MousePos};
|
||||
end const *pointed{nullptr};
|
||||
auto const place = [&](end const &End) { return ImVec2(center.x + End.seen.x * reach, center.y - End.seen.y * reach); };
|
||||
for (auto const &entry : ends)
|
||||
{
|
||||
auto const at{place(entry)};
|
||||
auto const dot{(entry.positive ? 8.0f : 6.0f) * s};
|
||||
if (hovered && (mouse.x - at.x) * (mouse.x - at.x) + (mouse.y - at.y) * (mouse.y - at.y) <= (dot + 2.0f * s) * (dot + 2.0f * s))
|
||||
pointed = &entry; // the nearest one under the mouse, as it's drawn over the others
|
||||
}
|
||||
for (auto const &entry : ends)
|
||||
{
|
||||
auto const at{place(entry)};
|
||||
auto const color{colors[entry.axis]};
|
||||
if (entry.positive)
|
||||
{
|
||||
list->AddLine(center, at, color, 2.0f * s);
|
||||
list->AddCircleFilled(at, 8.0f * s, color, 24);
|
||||
auto const text{ImGui::CalcTextSize(names[entry.axis])};
|
||||
list->AddText(ImVec2(at.x - text.x * 0.5f, at.y - text.y * 0.5f), IM_COL32(20, 20, 20, 255), names[entry.axis]);
|
||||
}
|
||||
else
|
||||
{
|
||||
list->AddCircleFilled(at, 6.0f * s, (color & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 90), 24);
|
||||
list->AddCircle(at, 6.0f * s, color, 24, 1.5f * s);
|
||||
}
|
||||
if (&entry == pointed)
|
||||
list->AddCircle(at, (entry.positive ? 9.5f : 7.5f) * s, IM_COL32(255, 255, 255, 255), 24, 1.5f * s);
|
||||
}
|
||||
if (pointed != nullptr)
|
||||
{
|
||||
ImGui::SetTooltip(STR_C("Look along %s%s; from over the scenery it's the top view"), pointed->positive ? "-" : "+", names[pointed->axis]);
|
||||
if (clicked)
|
||||
look_along_axis(pointed->axis, pointed->positive);
|
||||
}
|
||||
// the top view, under the axes
|
||||
tool_button const ortho{Global.EditorOrtho ? icon::ortho : icon::perspective,
|
||||
STR_C("Top view, orthographic"),
|
||||
"O",
|
||||
STR_C("The view straight down without perspective, as on a map; again for the camera as it was"),
|
||||
Global.EditorOrtho,
|
||||
[this]() { toggle_ortho(); }};
|
||||
icon_button(ortho, ImVec2(std::round(center.x - side * 0.5f), origin.y + size + 6.0f * s), 0);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void editor_mode::look_along_axis(int const Axis, bool const Positive)
|
||||
{
|
||||
m_focus_active = false;
|
||||
if (Axis == 1)
|
||||
{
|
||||
if (Positive)
|
||||
{
|
||||
// from over the scenery: the top view
|
||||
if (false == Global.EditorOrtho)
|
||||
toggle_ortho();
|
||||
return;
|
||||
}
|
||||
if (Global.EditorOrtho)
|
||||
toggle_ortho();
|
||||
Camera.Angle.x = M_PI_2;
|
||||
Camera.Angle.z = 0.0;
|
||||
return;
|
||||
}
|
||||
if (Global.EditorOrtho)
|
||||
toggle_ortho();
|
||||
// from the side of the axis the camera's on, towards the other one; the yaw as the camera works it out of where it looks
|
||||
glm::dvec3 where{0.0};
|
||||
where[Axis] = Positive ? -1.0 : 1.0;
|
||||
Camera.Angle.y = std::atan2(-where.x, -where.z);
|
||||
Camera.Angle.x = 0.0;
|
||||
Camera.Angle.z = 0.0;
|
||||
}
|
||||
Reference in New Issue
Block a user