EditorUi.cpp 59 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124
  1. // Copyright (C) 2009-present, Panagiotis Christopoulos Charitos and contributors.
  2. // All rights reserved.
  3. // Code licensed under the BSD License.
  4. // http://www.anki3d.org/LICENSE
  5. #include <AnKi/Editor/EditorUi.h>
  6. #include <AnKi/Scene.h>
  7. #include <AnKi/Resource/ResourceManager.h>
  8. #include <AnKi/Resource/ImageResource.h>
  9. #include <AnKi/Script/ScriptManager.h>
  10. #include <AnKi/Window/Input.h>
  11. #include <AnKi/Util/Filesystem.h>
  12. #include <AnKi/Renderer/Renderer.h>
  13. #include <AnKi/Renderer/Dbg.h>
  14. #include <AnKi/Collision.h>
  15. #include <filesystem>
  16. #include <ThirdParty/ImGui/Extra/IconsMaterialDesignIcons.h> // See all icons in https://pictogrammers.com/library/mdi/
  17. namespace anki {
  18. template<typename TFunc>
  19. class DeferredPop
  20. {
  21. public:
  22. TFunc m_func;
  23. DeferredPop(TFunc func)
  24. : m_func(func)
  25. {
  26. }
  27. ~DeferredPop()
  28. {
  29. m_func();
  30. }
  31. };
  32. #define ANKI_PUSH_POP(func, ...) \
  33. ImGui::Push##func(__VA_ARGS__); \
  34. DeferredPop ANKI_CONCATENATE(pop, __LINE__)([] { \
  35. ImGui::Pop##func(); \
  36. })
  37. /// This pushes a width for text input widgets that leaves "labelSize" chars for the label
  38. #define ANKI_PUSH_POP_TEXT_INPUT_WIDTH(labelSize) \
  39. const F32 labelWidthBase = ImGui::GetFontSize() * labelSize; /* Some amount of width for label, based on font size. */ \
  40. const F32 labelWidthMax = ImGui::GetContentRegionAvail().x * 0.40f; /* ...but always leave some room for framed widgets. */ \
  41. ANKI_PUSH_POP(ItemWidth, -min(labelWidthBase, labelWidthMax))
  42. // Using some NDC coords find the closest point these coords are to a ray
  43. static F32 projectNdcToRay(Vec2 ndc, Vec3 rayOrigin, Vec3 rayDir)
  44. {
  45. const Frustum& frustum = SceneGraph::getSingleton().getActiveCameraNode().getFirstComponentOfType<CameraComponent>().getFrustum();
  46. const Mat4 invMvp = frustum.getViewProjectionMatrix().invert();
  47. Vec4 v4 = frustum.getViewProjectionMatrix() * rayOrigin.xyz1();
  48. const Vec2 rayOriginNdc = v4.xy() / v4.w();
  49. v4 = frustum.getViewProjectionMatrix() * (rayOrigin + rayDir).xyz1();
  50. const Vec2 rayDirNdc = (v4.xy() / v4.w() - rayOriginNdc).normalize();
  51. const Vec2 disiredPosNdc = ndc.projectTo(rayOriginNdc, rayDirNdc);
  52. const Bool positiveSizeOfAxis = (disiredPosNdc - rayOriginNdc).dot(rayDirNdc) > 0.0f;
  53. // Create 2 lines (0 and 1) and find the closest point between them on line 1
  54. // Line equation: r(t) = a + t * b
  55. // Create line 0 which is built from the camera origin and a far point that was unprojected from NDC
  56. v4 = invMvp * Vec4(disiredPosNdc, 1.0f, 1.0f);
  57. const Vec3 a0 = v4.xyz() / v4.w();
  58. const Vec3 b0 = frustum.getWorldTransform().getOrigin().xyz() - a0;
  59. // Line 1 is built from the ray
  60. const Vec3 a1 = rayOrigin;
  61. const Vec3 b1 = rayDir; // = rayDir + rayOrigin - rayOrigin
  62. // Solve the system
  63. const Vec3 w = a0 - a1;
  64. const F32 A = b0.dot(b0);
  65. const F32 B = b0.dot(b1);
  66. const F32 C = b1.dot(b1);
  67. const F32 D = b0.dot(w);
  68. const F32 E = b1.dot(w);
  69. const F32 t = (A * E - B * D) / (A * C - B * B);
  70. const Vec3 touchingPoint = a1 + t * b1;
  71. const F32 distFromOrign = (touchingPoint - rayOrigin).length() * ((positiveSizeOfAxis) ? 1.0f : -1.0f);
  72. return distFromOrign;
  73. }
  74. static Bool projectNdcToPlane(Vec2 ndc, Plane plane, Vec3& point)
  75. {
  76. const Frustum& frustum = SceneGraph::getSingleton().getActiveCameraNode().getFirstComponentOfType<CameraComponent>().getFrustum();
  77. const Mat4 invMvp = frustum.getViewProjectionMatrix().invert();
  78. Vec4 v4 = invMvp * Vec4(ndc, 1.0f, 1.0f);
  79. v4 /= v4.w();
  80. const Vec3 rayOrigin = frustum.getWorldTransform().getOrigin().xyz();
  81. const Vec3 rayDir = (v4.xyz() - rayOrigin).normalize();
  82. Vec4 collisionPoint;
  83. const Bool collides = testCollision(plane, Ray(rayOrigin, rayDir), collisionPoint);
  84. if(collides)
  85. {
  86. point = collisionPoint.xyz();
  87. }
  88. return collides;
  89. }
  90. EditorUi::EditorUi()
  91. {
  92. Logger::getSingleton().addMessageHandler(this, loggerMessageHandler);
  93. gatherAssets(m_assetsWindow.m_assetPaths);
  94. ANKI_CHECKF(ResourceManager::getSingleton().loadResource("EngineAssets/Editor/Material.png", m_materialIcon));
  95. ANKI_CHECKF(ResourceManager::getSingleton().loadResource("EngineAssets/Editor/Mesh.png", m_meshIcon));
  96. }
  97. EditorUi::~EditorUi()
  98. {
  99. Logger::getSingleton().removeMessageHandler(this, loggerMessageHandler);
  100. }
  101. void EditorUi::listDir(const std::filesystem::path& rootPath, const std::filesystem::path& parentPath, AssetPath& parent, U32& id)
  102. {
  103. for(const auto& entry : std::filesystem::directory_iterator(parentPath))
  104. {
  105. if(entry.is_directory())
  106. {
  107. AssetPath& p = *parent.m_children.emplaceBack();
  108. const std::filesystem::path rpath = std::filesystem::relative(entry, parentPath);
  109. p.m_dirname = rpath.string().c_str();
  110. p.m_id = id++;
  111. listDir(rootPath, entry, p, id);
  112. }
  113. else if(entry.is_regular_file())
  114. {
  115. const String extension = entry.path().extension().string().c_str();
  116. AssetFile file;
  117. if(extension == ".ankitex")
  118. {
  119. file.m_type = AssetFileType::kTexture;
  120. }
  121. else if(extension == ".ankimtl")
  122. {
  123. file.m_type = AssetFileType::kMaterial;
  124. }
  125. else if(extension == ".ankimesh")
  126. {
  127. file.m_type = AssetFileType::kMesh;
  128. }
  129. else if(extension == ".ankipart")
  130. {
  131. file.m_type = AssetFileType::kParticleEmitter;
  132. }
  133. if(file.m_type != AssetFileType::kNone)
  134. {
  135. String rpath = std::filesystem::relative(entry.path(), rootPath).string().c_str();
  136. if(rpath.isEmpty())
  137. {
  138. // Sometimes it happens with paths that have links, ignore for now
  139. continue;
  140. }
  141. rpath.replaceAll("\\", "/");
  142. const String basefname = entry.path().filename().string().c_str();
  143. file.m_basename = basefname;
  144. file.m_filename = rpath;
  145. parent.m_files.emplaceBack(file);
  146. }
  147. }
  148. }
  149. };
  150. void EditorUi::gatherAssets(DynamicArray<AssetPath>& paths)
  151. {
  152. U32 id = 0;
  153. ResourceFilesystem::getSingleton().iterateAllResourceBasePaths([&](CString pathname) {
  154. AssetPath& path = *paths.emplaceBack();
  155. path.m_dirname = pathname;
  156. path.m_id = id++;
  157. std::filesystem::path stdpath(pathname.cstr());
  158. listDir(stdpath, stdpath, path, id);
  159. return FunctorContinue::kContinue;
  160. });
  161. }
  162. void EditorUi::loggerMessageHandler(void* ud, const LoggerMessageInfo& info)
  163. {
  164. EditorUi& self = *static_cast<EditorUi*>(ud);
  165. String logEntry;
  166. logEntry.sprintf("[%s][%-4s] %s [%s:%d][%s][%s]\n", kLoggerMessageTypeText[info.m_type], info.m_subsystem ? info.m_subsystem : "N/A", info.m_msg,
  167. info.m_file, info.m_line, info.m_func, info.m_threadName);
  168. LockGuard lock(self.m_consoleWindow.m_logMtx);
  169. self.m_consoleWindow.m_log.emplaceBack(std::pair(info.m_type, logEntry));
  170. self.m_consoleWindow.m_forceLogScrollDown = true;
  171. }
  172. void EditorUi::draw(UiCanvas& canvas)
  173. {
  174. m_canvas = &canvas;
  175. objectPicking();
  176. if(!m_font)
  177. {
  178. const Array<CString, 2> fnames = {"EngineAssets/UbuntuRegular.ttf", "EngineAssets/Editor/materialdesignicons-webfont.ttf"};
  179. m_font = canvas.addFonts(fnames);
  180. }
  181. if(!m_monospaceFont)
  182. {
  183. m_monospaceFont = canvas.addFont("EngineAssets/UbuntuMonoRegular.ttf");
  184. }
  185. ImGui::PushFont(m_font, m_fontSize);
  186. ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, 20.0f);
  187. const Vec4 oldWindowColor = ImGui::GetStyle().Colors[ImGuiCol_WindowBg];
  188. ImGui::GetStyle().Colors[ImGuiCol_WindowBg].w = 0.0f;
  189. ImGui::Begin("MainWindow", nullptr,
  190. ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize
  191. | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs);
  192. ImGui::GetStyle().Colors[ImGuiCol_WindowBg] = oldWindowColor;
  193. ImGui::SetWindowPos(Vec2(0.0f, 0.0f));
  194. ImGui::SetWindowSize(canvas.getSizef());
  195. mainMenu();
  196. ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport(), ImGuiDockNodeFlags_PassthruCentralNode);
  197. sceneHierarchyWindow();
  198. sceneNodePropertiesWindow();
  199. consoleWindow();
  200. assetsWindow();
  201. cVarsWindow();
  202. debugRtsWindow();
  203. {
  204. const Vec2 viewportSize = ImGui::GetMainViewport()->WorkSize;
  205. const Vec2 initialSize = Vec2(viewportSize.y() * 0.75f);
  206. const Vec2 initialPos = (viewportSize - initialSize) / 2.0f;
  207. m_imageViewer.drawWindow(canvas, initialPos, initialSize, 0);
  208. }
  209. {
  210. const Vec2 viewportSize = ImGui::GetMainViewport()->WorkSize;
  211. const Vec2 initialSize = Vec2(800.0f, 600.0f);
  212. const Vec2 initialPos = (viewportSize - initialSize) / 2.0f;
  213. m_particlesEditor.drawWindow(canvas, initialPos, initialSize, 0);
  214. }
  215. ImGui::End();
  216. ImGui::PopStyleVar();
  217. ImGui::PopFont();
  218. // Mouse is over any window if mouse is hovering or if clicked on a window
  219. m_mouseOverAnyWindow = ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow) || ImGui::GetIO().WantCaptureMouse;
  220. m_canvas = nullptr;
  221. }
  222. void EditorUi::mainMenu()
  223. {
  224. // The menu and toolbox is based on the comments in https://github.com/ocornut/imgui/issues/3518
  225. ImGuiViewport* viewport = ImGui::GetMainViewport();
  226. const ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar;
  227. if(ImGui::BeginViewportSideBar("##MainMenu", viewport, ImGuiDir_Up, ImGui::GetFrameHeight(), windowFlags))
  228. {
  229. if(ImGui::BeginMenuBar())
  230. {
  231. if(ImGui::BeginMenu(ICON_MDI_FOLDER_OUTLINE " File"))
  232. {
  233. if(ImGui::MenuItem(ICON_MDI_CLOSE_CIRCLE " Quit", "CTRL+Q"))
  234. {
  235. m_quit = true;
  236. }
  237. ImGui::EndMenu();
  238. }
  239. if(ImGui::BeginMenu(ICON_MDI_APPLICATION_OUTLINE " Windows"))
  240. {
  241. if(ImGui::MenuItem(ICON_MDI_APPLICATION_OUTLINE " Console"))
  242. {
  243. m_showConsoleWindow = true;
  244. }
  245. if(ImGui::MenuItem(ICON_MDI_APPLICATION_OUTLINE " SceneNode Props"))
  246. {
  247. m_showSceneNodePropsWindow = true;
  248. }
  249. if(ImGui::MenuItem(ICON_MDI_APPLICATION_OUTLINE " Scene Hierarchy"))
  250. {
  251. m_showSceneHierarcyWindow = true;
  252. }
  253. if(ImGui::MenuItem(ICON_MDI_APPLICATION_OUTLINE " Assets"))
  254. {
  255. m_showAssetsWindow = true;
  256. }
  257. if(ImGui::MenuItem(ICON_MDI_APPLICATION_OUTLINE " CVars Editor"))
  258. {
  259. m_showCVarEditorWindow = true;
  260. }
  261. if(ImGui::MenuItem(ICON_MDI_APPLICATION_OUTLINE " Debug Render Targets"))
  262. {
  263. m_showDebugRtsWindow = true;
  264. }
  265. ImGui::EndMenu();
  266. }
  267. if(ImGui::BeginMenu(ICON_MDI_CUBE_SCAN " Debug"))
  268. {
  269. Bool bBoundingBoxes = !!(Renderer::getSingleton().getDbg().getOptions() & DbgOption::kBoundingBoxes);
  270. if(ImGui::Checkbox("Visible Renderables", &bBoundingBoxes))
  271. {
  272. DbgOption options = Renderer::getSingleton().getDbg().getOptions();
  273. if(bBoundingBoxes)
  274. {
  275. options |= DbgOption::kBoundingBoxes;
  276. }
  277. else
  278. {
  279. options &= ~(DbgOption::kBoundingBoxes);
  280. }
  281. Renderer::getSingleton().getDbg().setOptions(options);
  282. }
  283. Bool bPhysics = !!(Renderer::getSingleton().getDbg().getOptions() & DbgOption::kPhysics);
  284. if(ImGui::Checkbox("Physics Bodies", &bPhysics))
  285. {
  286. DbgOption options = Renderer::getSingleton().getDbg().getOptions();
  287. if(bPhysics)
  288. {
  289. options |= DbgOption::kPhysics;
  290. }
  291. else
  292. {
  293. options &= ~DbgOption::kPhysics;
  294. }
  295. Renderer::getSingleton().getDbg().setOptions(options);
  296. }
  297. Bool bDepthTest = !!(Renderer::getSingleton().getDbg().getOptions() & DbgOption::kDepthTest);
  298. if(ImGui::Checkbox("Depth Test", &bDepthTest))
  299. {
  300. DbgOption options = Renderer::getSingleton().getDbg().getOptions();
  301. if(bDepthTest)
  302. {
  303. options |= DbgOption::kDepthTest;
  304. }
  305. else
  306. {
  307. options &= ~DbgOption::kDepthTest;
  308. }
  309. Renderer::getSingleton().getDbg().setOptions(options);
  310. }
  311. ImGui::EndMenu();
  312. }
  313. // Title
  314. {
  315. CString text = "AnKi 3D Engine Editor";
  316. const F32 menuBarWidth = ImGui::GetWindowWidth();
  317. const F32 textWidth = ImGui::CalcTextSize(text.cstr()).x;
  318. ImGui::SameLine(menuBarWidth - menuBarWidth / 2.0f - textWidth / 2.0f);
  319. ImGui::TextUnformatted(text.cstr());
  320. }
  321. // Quit bnt
  322. {
  323. const Char* text = ICON_MDI_CLOSE_CIRCLE;
  324. const Vec2 textSize = ImGui::CalcTextSize(text);
  325. const F32 menuBarWidth = ImGui::GetWindowWidth();
  326. ImGui::SameLine(menuBarWidth - textSize.x() - ImGui::GetStyle().FramePadding.x * 2.0f - kMargin);
  327. ImGui::PushStyleColor(ImGuiCol_ButtonHovered, Vec4(1.0f, 0.0f, 0.0f, 1.0f));
  328. if(ImGui::Button(text))
  329. {
  330. m_quit = true;
  331. }
  332. ImGui::PopStyleColor();
  333. }
  334. ImGui::EndMenuBar();
  335. }
  336. if(Input::getSingleton().getKey(KeyCode::kLeftCtrl) > 0 && Input::getSingleton().getKey(KeyCode::kQ) > 0)
  337. {
  338. m_quit = true;
  339. }
  340. }
  341. ImGui::End();
  342. // Toolbox
  343. if(ImGui::BeginViewportSideBar("##Toolbox", viewport, ImGuiDir_Up, ImGui::GetFrameHeight(), windowFlags))
  344. {
  345. if(ImGui::BeginMenuBar())
  346. {
  347. ImGui::SetNextItemWidth(ImGui::CalcTextSize("00.000").x);
  348. if(ImGui::SliderFloat(ICON_MDI_AXIS_ARROW "&" ICON_MDI_ARROW_EXPAND_ALL " Snapping", &m_toolbox.m_scaleTranslationSnapping, 0.0, 10.0f))
  349. {
  350. const F32 roundTo = 0.5f;
  351. m_toolbox.m_scaleTranslationSnapping = round(m_toolbox.m_scaleTranslationSnapping / roundTo) * roundTo;
  352. }
  353. ImGui::SameLine();
  354. ImGui::SetNextItemWidth(ImGui::CalcTextSize("00.000").x);
  355. if(ImGui::SliderFloat(ICON_MDI_ROTATE_ORBIT " Snapping", &m_toolbox.m_rotationSnappingDeg, 0.0, 90.0f))
  356. {
  357. const F32 roundTo = 1.0f;
  358. m_toolbox.m_rotationSnappingDeg = round(m_toolbox.m_rotationSnappingDeg / roundTo) * roundTo;
  359. }
  360. ImGui::EndMenuBar();
  361. }
  362. }
  363. ImGui::End();
  364. }
  365. void EditorUi::sceneNode(SceneNode& node)
  366. {
  367. ImGui::TableNextRow();
  368. ImGui::TableNextColumn();
  369. ImGui::PushID(I32(node.getUuid()));
  370. ImGuiTreeNodeFlags treeFlags = ImGuiTreeNodeFlags_None;
  371. treeFlags |= ImGuiTreeNodeFlags_OpenOnArrow
  372. | ImGuiTreeNodeFlags_OpenOnDoubleClick; // Standard opening mode as we are likely to want to add selection afterwards
  373. treeFlags |= ImGuiTreeNodeFlags_NavLeftJumpsToParent; // Left arrow support
  374. treeFlags |= ImGuiTreeNodeFlags_SpanFullWidth; // Span full width for easier mouse reach
  375. treeFlags |= ImGuiTreeNodeFlags_DrawLinesToNodes; // Always draw hierarchy outlines
  376. if(&node == m_sceneHierarchyWindow.m_selectedNode)
  377. {
  378. treeFlags |= ImGuiTreeNodeFlags_Selected;
  379. }
  380. if(!node.hasChildren())
  381. {
  382. treeFlags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_Bullet;
  383. }
  384. String componentsString;
  385. for(SceneComponentType sceneComponentType : EnumBitsIterable<SceneComponentType, SceneComponentTypeMask>(node.getSceneComponentMask()))
  386. {
  387. switch(sceneComponentType)
  388. {
  389. #define ANKI_DEFINE_SCENE_COMPONENT(name, weight, sceneNodeCanHaveMany, icon) \
  390. case SceneComponentType::k##name: \
  391. componentsString += ICON_MDI_##icon; \
  392. break;
  393. #include <AnKi/Scene/Components/SceneComponentClasses.def.h>
  394. default:
  395. ANKI_ASSERT(0);
  396. }
  397. }
  398. const Bool nodeOpen = ImGui::TreeNodeEx("", treeFlags, "%s %s", node.getName().cstr(), componentsString.cstr());
  399. if(ImGui::IsItemFocused())
  400. {
  401. m_sceneHierarchyWindow.m_selectedNode = &node;
  402. }
  403. if(nodeOpen)
  404. {
  405. for(SceneNode* child : node.getChildren())
  406. {
  407. sceneNode(*child);
  408. }
  409. ImGui::TreePop();
  410. }
  411. ImGui::PopID();
  412. }
  413. void EditorUi::sceneHierarchyWindow()
  414. {
  415. if(!m_showSceneHierarcyWindow)
  416. {
  417. return;
  418. }
  419. if(ImGui::GetFrameCount() > 1)
  420. {
  421. // Viewport is one frame delay so do that when frame >1
  422. const Vec2 viewportSize = ImGui::GetMainViewport()->WorkSize;
  423. const Vec2 viewportPos = ImGui::GetMainViewport()->WorkPos;
  424. ImGui::SetNextWindowPos(viewportPos, ImGuiCond_FirstUseEver);
  425. ImGui::SetNextWindowSize(Vec2(400.0f, viewportSize.y() - kConsoleHeight), ImGuiCond_FirstUseEver);
  426. }
  427. if(ImGui::Begin("Scene Hierarchy", &m_showSceneHierarcyWindow, ImGuiWindowFlags_NoCollapse))
  428. {
  429. filter(m_sceneHierarchyWindow.m_filter);
  430. if(ImGui::BeginChild("##tree", Vec2(0.0f), ImGuiChildFlags_Borders | ImGuiChildFlags_NavFlattened, ImGuiWindowFlags_None))
  431. {
  432. if(ImGui::BeginTable("##bg", 1, ImGuiTableFlags_RowBg))
  433. {
  434. SceneGraph::getSingleton().visitNodes([this](SceneNode& node) {
  435. if(!node.getParent() && m_sceneHierarchyWindow.m_filter.PassFilter(node.getName().cstr()))
  436. {
  437. sceneNode(node);
  438. }
  439. return FunctorContinue::kContinue;
  440. });
  441. ImGui::EndTable();
  442. }
  443. }
  444. ImGui::EndChild();
  445. }
  446. ImGui::End();
  447. }
  448. void EditorUi::sceneNodePropertiesWindow()
  449. {
  450. if(!m_showSceneNodePropsWindow)
  451. {
  452. return;
  453. }
  454. auto& state = m_sceneNodePropsWindow;
  455. if(ImGui::GetFrameCount() > 1)
  456. {
  457. // Viewport is one frame delay so do that when frame >1
  458. const Vec2 viewportSize = ImGui::GetMainViewport()->WorkSize;
  459. const Vec2 viewportPos = ImGui::GetMainViewport()->WorkPos;
  460. const F32 initialWidth = 500.0f;
  461. ImGui::SetNextWindowPos(Vec2(viewportSize.x() - initialWidth, viewportPos.y()), ImGuiCond_FirstUseEver);
  462. ImGui::SetNextWindowSize(Vec2(initialWidth, viewportSize.y() - kConsoleHeight), ImGuiCond_FirstUseEver);
  463. }
  464. if(ImGui::Begin("SceneNode Props", &m_showSceneNodePropsWindow, ImGuiWindowFlags_NoCollapse) && m_sceneHierarchyWindow.m_selectedNode)
  465. {
  466. SceneNode& node = *m_sceneHierarchyWindow.m_selectedNode;
  467. I32 id = 0;
  468. if(state.m_currentSceneNodeUuid != node.getUuid())
  469. {
  470. // Node changed, reset a few things
  471. state.m_currentSceneNodeUuid = node.getUuid();
  472. state.m_textEditorOpen = false;
  473. state.m_scriptComponentThatHasTheTextEditorOpen = 0;
  474. state.m_textEditorTxt.destroy();
  475. }
  476. ANKI_PUSH_POP_TEXT_INPUT_WIDTH(12);
  477. // Name
  478. {
  479. dummyButton(id++);
  480. Array<Char, kMaxTextInputLen> name;
  481. std::strncpy(name.getBegin(), node.getName().getBegin(), name.getSize());
  482. if(ImGui::InputText("Name", name.getBegin(), name.getSize(), ImGuiInputTextFlags_EnterReturnsTrue))
  483. {
  484. node.setName(&name[0]);
  485. }
  486. }
  487. // Local transform
  488. {
  489. dummyButton(id++);
  490. F32 localOrigin[3] = {node.getLocalOrigin().x(), node.getLocalOrigin().y(), node.getLocalOrigin().z()};
  491. if(ImGui::DragFloat3(ICON_MDI_AXIS_ARROW " Origin", localOrigin, 0.025f, -1000000.0f, 1000000.0f))
  492. {
  493. node.setLocalOrigin(Vec3(&localOrigin[0]));
  494. }
  495. }
  496. // Local scale
  497. {
  498. ImGui::PushID(id++);
  499. if(ImGui::Button(state.m_uniformScale ? ICON_MDI_LOCK : ICON_MDI_LOCK_OPEN))
  500. {
  501. state.m_uniformScale = !state.m_uniformScale;
  502. }
  503. ImGui::SetItemTooltip("Uniform/Non-uniform scale");
  504. ImGui::PopID();
  505. ImGui::SameLine();
  506. F32 localScale[3] = {node.getLocalScale().x(), node.getLocalScale().y(), node.getLocalScale().z()};
  507. if(ImGui::DragFloat3(ICON_MDI_ARROW_EXPAND_ALL " Scale", localScale, 0.0025f, 0.01f, 1000000.0f))
  508. {
  509. if(!state.m_uniformScale)
  510. {
  511. node.setLocalScale(Vec3(&localScale[0]));
  512. }
  513. else
  514. {
  515. // The component that have changed wins
  516. F32 scale = scale = localScale[2];
  517. if(localScale[0] != node.getLocalScale().x())
  518. {
  519. scale = localScale[0];
  520. }
  521. else if(localScale[1] != node.getLocalScale().y())
  522. {
  523. scale = localScale[1];
  524. }
  525. node.setLocalScale(Vec3(scale));
  526. }
  527. }
  528. }
  529. // Local rotation
  530. {
  531. dummyButton(id++);
  532. const Euler rot(node.getLocalRotation());
  533. F32 localRotation[3] = {toDegrees(rot.x()), toDegrees(rot.y()), toDegrees(rot.z())};
  534. if(ImGui::DragFloat3(ICON_MDI_ROTATE_ORBIT " Rotation", localRotation, 0.25f, -360.0f, 360.0f, "%.3f", ImGuiSliderFlags_AlwaysClamp))
  535. {
  536. const Euler rot(toRad(localRotation[0]), toRad(localRotation[1]), toRad(localRotation[2]));
  537. node.setLocalRotation(Mat3(rot));
  538. }
  539. ImGui::Text(" ");
  540. }
  541. // Component controls
  542. {
  543. if(ImGui::Button(ICON_MDI_PLUS_BOX))
  544. {
  545. switch(SceneComponentType(state.m_selectedSceneComponentType))
  546. {
  547. #define ANKI_DEFINE_SCENE_COMPONENT(name, weight, sceneNodeCanHaveMany, icon) \
  548. case SceneComponentType::k##name: \
  549. node.newComponent<name##Component>(); \
  550. break;
  551. #include <AnKi/Scene/Components/SceneComponentClasses.def.h>
  552. default:
  553. ANKI_ASSERT(0);
  554. }
  555. }
  556. ImGui::SetItemTooltip("Add new component");
  557. ImGui::SameLine();
  558. ImGui::SetNextItemWidth(-1.0f);
  559. I32 n = 0;
  560. if(ImGui::BeginCombo(" ", kSceneComponentTypeName[state.m_selectedSceneComponentType]))
  561. {
  562. for(const Char* name : kSceneComponentTypeName)
  563. {
  564. const Bool isSelected = (state.m_selectedSceneComponentType == n);
  565. if(ImGui::Selectable(name, isSelected))
  566. {
  567. state.m_selectedSceneComponentType = n;
  568. }
  569. // Set the initial focus when opening the combo (scrolling + keyboard navigation focus)
  570. if(isSelected)
  571. {
  572. ImGui::SetItemDefaultFocus();
  573. }
  574. ++n;
  575. }
  576. ImGui::EndCombo();
  577. }
  578. ImGui::Text(" ");
  579. }
  580. // Components
  581. {
  582. ANKI_PUSH_POP(StyleVar, ImGuiStyleVar_SeparatorTextAlign, Vec2(0.5f));
  583. U32 count = 0;
  584. node.iterateComponents([&](SceneComponent& comp) {
  585. ANKI_PUSH_POP(ID, comp.getUuid());
  586. const F32 alpha = 0.1f;
  587. ANKI_PUSH_POP(StyleColor, ImGuiCol_ChildBg, (count & 1) ? Vec4(0.0, 0.0f, 1.0f, alpha) : Vec4(1.0, 0.0f, 0.0f, alpha));
  588. if(ImGui::BeginChild("Child", Vec2(0.0f), ImGuiChildFlags_AutoResizeY))
  589. {
  590. ANKI_PUSH_POP_TEXT_INPUT_WIDTH(12);
  591. // Find the icon
  592. CString icon = ICON_MDI_TOY_BRICK;
  593. switch(comp.getType())
  594. {
  595. #define ANKI_DEFINE_SCENE_COMPONENT(name, weight, sceneNodeCanHaveMany, icon_) \
  596. case SceneComponentType::k##name: \
  597. icon = ANKI_CONCATENATE(ICON_MDI_, icon_); \
  598. break;
  599. #include <AnKi/Scene/Components/SceneComponentClasses.def.h>
  600. default:
  601. ANKI_ASSERT(0);
  602. }
  603. // Header
  604. {
  605. String label;
  606. label.sprintf(" %s %s (%u)", icon.cstr(), kSceneComponentTypeName[comp.getType()], comp.getUuid());
  607. ImGui::SeparatorText(label.cstr());
  608. if(ImGui::Button(ICON_MDI_MINUS_BOX))
  609. {
  610. ANKI_LOGW("TODO");
  611. }
  612. ImGui::SetItemTooltip("Delete Component");
  613. ImGui::SameLine();
  614. if(ImGui::Button(ICON_MDI_EYE))
  615. {
  616. ANKI_LOGW("TODO");
  617. }
  618. ImGui::SetItemTooltip("Disable component");
  619. }
  620. switch(comp.getType())
  621. {
  622. case SceneComponentType::kMove:
  623. case SceneComponentType::kUi:
  624. // Nothing
  625. break;
  626. case SceneComponentType::kScript:
  627. scriptComponent(static_cast<ScriptComponent&>(comp));
  628. break;
  629. case SceneComponentType::kMaterial:
  630. materialComponent(static_cast<MaterialComponent&>(comp));
  631. break;
  632. case SceneComponentType::kMesh:
  633. meshComponent(static_cast<MeshComponent&>(comp));
  634. break;
  635. case SceneComponentType::kSkin:
  636. skinComponent(static_cast<SkinComponent&>(comp));
  637. break;
  638. case SceneComponentType::kParticleEmitter2:
  639. particleEmitterComponent(static_cast<ParticleEmitter2Component&>(comp));
  640. break;
  641. case SceneComponentType::kLight:
  642. lightComponent(static_cast<LightComponent&>(comp));
  643. break;
  644. case SceneComponentType::kBody:
  645. bodyComponent(static_cast<BodyComponent&>(comp));
  646. break;
  647. case SceneComponentType::kJoint:
  648. jointComponent(static_cast<JointComponent&>(comp));
  649. break;
  650. case SceneComponentType::kDecal:
  651. decalComponent(static_cast<DecalComponent&>(comp));
  652. break;
  653. default:
  654. ImGui::Text("TODO");
  655. }
  656. }
  657. ImGui::EndChild();
  658. ImGui::Text(" ");
  659. ++count;
  660. });
  661. }
  662. }
  663. ImGui::End();
  664. }
  665. void EditorUi::scriptComponent(ScriptComponent& comp)
  666. {
  667. auto& state = m_sceneNodePropsWindow;
  668. // Play button
  669. {
  670. ImGui::SameLine();
  671. if(ImGui::Button(ICON_MDI_PLAY "##ScriptComponentResourceFilename"))
  672. {
  673. ANKI_LOGV("TODO");
  674. }
  675. ImGui::SetItemTooltip("Play script");
  676. }
  677. // Clear button
  678. {
  679. if(ImGui::Button(ICON_MDI_DELETE "##ScriptComponentResourceFilename"))
  680. {
  681. comp.setScriptResourceFilename("");
  682. }
  683. ImGui::SetItemTooltip("Clear");
  684. ImGui::SameLine();
  685. }
  686. // Filename input
  687. {
  688. const DynamicArray<CString> filenames = gatherResourceFilenames(".lua");
  689. const String currentFilename = (comp.hasScriptResource()) ? comp.getScriptResourceFilename() : "";
  690. U32 newSelectedFilename = kMaxU32;
  691. ImGui::SetNextItemWidth(-1.0f);
  692. comboWithFilter("##Filenames", filenames, currentFilename, newSelectedFilename, m_tempFilter);
  693. if(newSelectedFilename < filenames.getSize() && currentFilename != filenames[newSelectedFilename])
  694. {
  695. comp.setScriptResourceFilename(filenames[newSelectedFilename]);
  696. }
  697. }
  698. ImGui::Text(" -- or --");
  699. // Clear button
  700. {
  701. if(ImGui::Button(ICON_MDI_DELETE "##ScriptComponentScriptText"))
  702. {
  703. comp.setScriptText("");
  704. }
  705. ImGui::SetItemTooltip("Unset");
  706. ImGui::SameLine();
  707. }
  708. // Button
  709. {
  710. String buttonTxt;
  711. buttonTxt.sprintf(ICON_MDI_LANGUAGE_LUA " Embedded Script (%s)", comp.hasScriptText() ? "Set" : "Unset");
  712. const Bool showEditor = ImGui::Button(buttonTxt.cstr(), Vec2(-1.0f, 0.0f));
  713. if(showEditor && (state.m_scriptComponentThatHasTheTextEditorOpen == 0 || state.m_scriptComponentThatHasTheTextEditorOpen == comp.getUuid()))
  714. {
  715. state.m_textEditorOpen = true;
  716. state.m_scriptComponentThatHasTheTextEditorOpen = comp.getUuid();
  717. state.m_textEditorTxt =
  718. (comp.hasScriptText()) ? comp.getScriptText() : "function update(node, prevTime, crntTime)\n -- Your code here\nend";
  719. }
  720. }
  721. if(state.m_textEditorOpen && state.m_scriptComponentThatHasTheTextEditorOpen == comp.getUuid())
  722. {
  723. if(textEditorWindow(String().sprintf("ScriptComponent %u", comp.getUuid()), &state.m_textEditorOpen, state.m_textEditorTxt))
  724. {
  725. ANKI_LOGV("Updating ScriptComponent");
  726. comp.setScriptText(state.m_textEditorTxt);
  727. }
  728. if(!state.m_textEditorOpen)
  729. {
  730. state.m_scriptComponentThatHasTheTextEditorOpen = 0;
  731. state.m_textEditorTxt.destroy();
  732. }
  733. }
  734. }
  735. void EditorUi::materialComponent(MaterialComponent& comp)
  736. {
  737. if(!comp.isValid())
  738. {
  739. ImGui::SameLine();
  740. ImGui::TextUnformatted(ICON_MDI_ALERT);
  741. ImGui::SetItemTooltip("Component not valid");
  742. }
  743. // Filename
  744. {
  745. const DynamicArray<CString> mtlFilenames = gatherResourceFilenames(".ankimtl");
  746. const String currentFilename = (comp.hasMaterialResource()) ? comp.getMaterialFilename() : "";
  747. U32 newSelectedFilename = kMaxU32;
  748. ImGui::SetNextItemWidth(-1.0f);
  749. comboWithFilter("##Filenames", mtlFilenames, currentFilename, newSelectedFilename, m_tempFilter);
  750. if(newSelectedFilename < mtlFilenames.getSize() && currentFilename != mtlFilenames[newSelectedFilename])
  751. {
  752. comp.setMaterialFilename(mtlFilenames[newSelectedFilename]);
  753. }
  754. }
  755. // Submesh ID
  756. {
  757. I32 value = comp.getSubmeshIndex();
  758. if(ImGui::InputInt(ICON_MDI_VECTOR_POLYGON " Submesh ID", &value, 1, 1, 0))
  759. {
  760. comp.setSubmeshIndex(value);
  761. }
  762. }
  763. }
  764. void EditorUi::meshComponent(MeshComponent& comp)
  765. {
  766. if(!comp.isValid())
  767. {
  768. ImGui::SameLine();
  769. ImGui::TextUnformatted(ICON_MDI_ALERT);
  770. ImGui::SetItemTooltip("Component not valid");
  771. }
  772. // Filename
  773. {
  774. const DynamicArray<CString> meshFilenames = gatherResourceFilenames(".ankimesh");
  775. const String currentFilename = (comp.hasMeshResource()) ? comp.getMeshFilename() : "";
  776. U32 newSelectedFilename = kMaxU32;
  777. ImGui::SetNextItemWidth(-1.0f);
  778. comboWithFilter("##Filenames", meshFilenames, currentFilename, newSelectedFilename, m_tempFilter);
  779. if(newSelectedFilename < meshFilenames.getSize() && currentFilename != meshFilenames[newSelectedFilename])
  780. {
  781. comp.setMeshFilename(meshFilenames[newSelectedFilename]);
  782. }
  783. }
  784. }
  785. void EditorUi::skinComponent(SkinComponent& comp)
  786. {
  787. if(!comp.isValid())
  788. {
  789. ImGui::SameLine();
  790. ImGui::TextUnformatted(ICON_MDI_ALERT);
  791. ImGui::SetItemTooltip("Component not valid");
  792. }
  793. // Filename
  794. {
  795. const DynamicArray<CString> filenames = gatherResourceFilenames(".ankiskel");
  796. const String currentFilename = (comp.hasSkeletonResource()) ? comp.getSkeletonFilename() : "";
  797. U32 newSelectedFilename = kMaxU32;
  798. ImGui::SetNextItemWidth(-1.0f);
  799. comboWithFilter("##Filenames", filenames, currentFilename, newSelectedFilename, m_tempFilter);
  800. if(newSelectedFilename < filenames.getSize() && currentFilename != filenames[newSelectedFilename])
  801. {
  802. comp.setSkeletonFilename(filenames[newSelectedFilename]);
  803. }
  804. }
  805. }
  806. void EditorUi::particleEmitterComponent(ParticleEmitter2Component& comp)
  807. {
  808. // Filename
  809. {
  810. const DynamicArray<CString> filenames = gatherResourceFilenames(".ankiparts");
  811. const String currentFilename = (comp.hasParticleEmitterResource()) ? comp.getParticleEmitterFilename() : "";
  812. U32 newSelectedFilename = kMaxU32;
  813. ImGui::SetNextItemWidth(-1.0f);
  814. comboWithFilter("##Filenames", filenames, currentFilename, newSelectedFilename, m_tempFilter);
  815. if(newSelectedFilename < filenames.getSize() && currentFilename != filenames[newSelectedFilename])
  816. {
  817. comp.setParticleEmitterFilename(filenames[newSelectedFilename]);
  818. }
  819. }
  820. // Geometry type
  821. {
  822. dummyButton(0);
  823. if(ImGui::BeginCombo("Geometry Type", kParticleEmitterGeometryTypeName[comp.getParticleGeometryType()]))
  824. {
  825. for(ParticleGeometryType n : EnumIterable<ParticleGeometryType>())
  826. {
  827. const Bool isSelected = (comp.getParticleGeometryType() == n);
  828. if(ImGui::Selectable(kParticleEmitterGeometryTypeName[n], isSelected))
  829. {
  830. comp.setParticleGeometryType(n);
  831. }
  832. // Set the initial focus when opening the combo (scrolling + keyboard navigation focus)
  833. if(isSelected)
  834. {
  835. ImGui::SetItemDefaultFocus();
  836. }
  837. }
  838. ImGui::EndCombo();
  839. }
  840. }
  841. }
  842. void EditorUi::lightComponent(LightComponent& comp)
  843. {
  844. // Light type
  845. if(ImGui::BeginCombo("Type", kLightComponentTypeNames[comp.getLightComponentType()]))
  846. {
  847. for(LightComponentType type : EnumIterable<LightComponentType>())
  848. {
  849. const Bool selected = type == comp.getLightComponentType();
  850. if(ImGui::Selectable(kLightComponentTypeNames[type], selected))
  851. {
  852. comp.setLightComponentType(type);
  853. }
  854. // Set the initial focus when opening the combo (scrolling + keyboard navigation focus)
  855. if(selected)
  856. {
  857. ImGui::SetItemDefaultFocus();
  858. }
  859. }
  860. ImGui::EndCombo();
  861. }
  862. // Diffuse color
  863. Vec4 diffuseCol = comp.getDiffuseColor();
  864. if(ImGui::InputFloat4("Diffuse Color", &diffuseCol[0]))
  865. {
  866. comp.setDiffuseColor(diffuseCol.max(0.01f));
  867. }
  868. // Shadow
  869. Bool shadow = comp.getShadowEnabled();
  870. if(ImGui::Checkbox("Shadow", &shadow))
  871. {
  872. comp.setShadowEnabled(shadow);
  873. }
  874. if(comp.getLightComponentType() == LightComponentType::kPoint)
  875. {
  876. // Radius
  877. F32 radius = comp.getRadius();
  878. if(ImGui::SliderFloat("Radius", &radius, 0.01f, 100.0f))
  879. {
  880. comp.setRadius(max(radius, 0.01f));
  881. }
  882. }
  883. else if(comp.getLightComponentType() == LightComponentType::kSpot)
  884. {
  885. // Radius
  886. F32 distance = comp.getDistance();
  887. if(ImGui::SliderFloat("Distance", &distance, 0.01f, 100.0f))
  888. {
  889. comp.setDistance(max(distance, 0.01f));
  890. }
  891. // Inner & outter angles
  892. Vec2 angles(comp.getInnerAngle(), comp.getOuterAngle());
  893. angles[0] = toDegrees(angles[0]);
  894. angles[1] = toDegrees(angles[1]);
  895. if(ImGui::SliderFloat2("Inner & Outer Angles", &angles[0], 1.0f, 89.0f))
  896. {
  897. angles[0] = clamp(toRad(angles[0]), 1.0_degrees, 80.0_degrees);
  898. angles[1] = clamp(toRad(angles[1]), angles[0], 89.0_degrees);
  899. comp.setInnerAngle(angles[0]);
  900. comp.setOuterAngle(angles[1]);
  901. }
  902. }
  903. else
  904. {
  905. ANKI_ASSERT(comp.getLightComponentType() == LightComponentType::kDirectional);
  906. // Day of month
  907. I32 month, day;
  908. F32 hour;
  909. comp.getTimeOfDay(month, day, hour);
  910. Bool fieldChanged = false;
  911. if(ImGui::SliderInt("Month", &month, 0, 11))
  912. {
  913. fieldChanged = true;
  914. }
  915. if(ImGui::SliderInt("Day", &day, 0, 30))
  916. {
  917. fieldChanged = true;
  918. }
  919. if(ImGui::SliderFloat("Hour (0-24)", &hour, 0.0f, 24.0f))
  920. {
  921. fieldChanged = true;
  922. }
  923. if(fieldChanged)
  924. {
  925. comp.setDirectionFromTimeOfDay(month, day, hour);
  926. }
  927. }
  928. }
  929. void EditorUi::jointComponent(JointComponent& comp)
  930. {
  931. if(!comp.isValid())
  932. {
  933. ImGui::SameLine();
  934. ImGui::TextUnformatted(ICON_MDI_ALERT);
  935. ImGui::SetItemTooltip("Component not valid");
  936. }
  937. // Joint type
  938. if(ImGui::BeginCombo("Type", kJointComponentTypeName[comp.getJointType()]))
  939. {
  940. for(JointComponentyType type : EnumIterable<JointComponentyType>())
  941. {
  942. const Bool selected = type == comp.getJointType();
  943. if(ImGui::Selectable(kBodyComponentCollisionShapeTypeNames[type], selected))
  944. {
  945. comp.setJointType(type);
  946. }
  947. // Set the initial focus when opening the combo (scrolling + keyboard navigation focus)
  948. if(selected)
  949. {
  950. ImGui::SetItemDefaultFocus();
  951. }
  952. }
  953. ImGui::EndCombo();
  954. }
  955. }
  956. void EditorUi::bodyComponent(BodyComponent& comp)
  957. {
  958. if(!comp.isValid())
  959. {
  960. ImGui::SameLine();
  961. ImGui::TextUnformatted(ICON_MDI_ALERT);
  962. ImGui::SetItemTooltip("Component not valid");
  963. }
  964. // Shape type
  965. if(ImGui::BeginCombo("Type", kBodyComponentCollisionShapeTypeNames[comp.getCollisionShapeType()]))
  966. {
  967. for(BodyComponentCollisionShapeType type : EnumIterable<BodyComponentCollisionShapeType>())
  968. {
  969. const Bool selected = type == comp.getCollisionShapeType();
  970. if(ImGui::Selectable(kBodyComponentCollisionShapeTypeNames[type], selected))
  971. {
  972. comp.setCollisionShapeType(type);
  973. }
  974. // Set the initial focus when opening the combo (scrolling + keyboard navigation focus)
  975. if(selected)
  976. {
  977. ImGui::SetItemDefaultFocus();
  978. }
  979. }
  980. ImGui::EndCombo();
  981. }
  982. // Mass
  983. F32 mass = comp.getMass();
  984. if(ImGui::SliderFloat("Mass", &mass, 0.0f, 100.0f))
  985. {
  986. comp.setMass(mass);
  987. }
  988. if(comp.getCollisionShapeType() == BodyComponentCollisionShapeType::kAabb)
  989. {
  990. Vec3 extend = comp.getBoxExtend();
  991. if(ImGui::SliderFloat3("Box Extend", &extend[0], 0.01f, 100.0f))
  992. {
  993. comp.setBoxExtend(extend);
  994. }
  995. }
  996. else if(comp.getCollisionShapeType() == BodyComponentCollisionShapeType::kSphere)
  997. {
  998. F32 radius = comp.getSphereRadius();
  999. if(ImGui::SliderFloat("Radius", &radius, 0.01f, 100.0f))
  1000. {
  1001. comp.setSphereRadius(radius);
  1002. }
  1003. }
  1004. }
  1005. void EditorUi::decalComponent(DecalComponent& comp)
  1006. {
  1007. if(!comp.isValid())
  1008. {
  1009. ImGui::SameLine();
  1010. ImGui::TextUnformatted(ICON_MDI_ALERT);
  1011. ImGui::SetItemTooltip("Component not valid");
  1012. }
  1013. ImGui::SeparatorText("Diffuse");
  1014. // Diffuse filename
  1015. {
  1016. const DynamicArray<CString> filenames = gatherResourceFilenames(".ankitex");
  1017. const String currentFilename = (comp.hasDiffuseImageResource()) ? comp.getDiffuseImageFilename() : "";
  1018. U32 newSelectedFilename = kMaxU32;
  1019. ImGui::SetNextItemWidth(-1.0f);
  1020. comboWithFilter("##Filenames", filenames, currentFilename, newSelectedFilename, m_tempFilter);
  1021. if(newSelectedFilename < filenames.getSize() && currentFilename != filenames[newSelectedFilename])
  1022. {
  1023. comp.setDiffuseImageFilename(filenames[newSelectedFilename]);
  1024. }
  1025. }
  1026. // Diffuse factor
  1027. ImGui::SetNextItemWidth(-1.0f);
  1028. F32 diffFactor = comp.getDiffuseBlendFactor();
  1029. if(ImGui::SliderFloat("##Factor0", &diffFactor, 0.0f, 1.0f))
  1030. {
  1031. comp.setDiffuseBlendFactor(diffFactor);
  1032. }
  1033. ImGui::SetItemTooltip("Blend Factor");
  1034. ImGui::SeparatorText("Roughness and Metallic");
  1035. // Roughness/metallic filename
  1036. {
  1037. const DynamicArray<CString> filenames = gatherResourceFilenames(".ankitex");
  1038. const String currentFilename = (comp.hasRoughnessMetalnessImageResource()) ? comp.getRoughnessMetalnessImageFilename() : "";
  1039. U32 newSelectedFilename = kMaxU32;
  1040. ImGui::SetNextItemWidth(-1.0f);
  1041. comboWithFilter("##Filenames2", filenames, currentFilename, newSelectedFilename, m_tempFilter);
  1042. if(newSelectedFilename < filenames.getSize() && currentFilename != filenames[newSelectedFilename])
  1043. {
  1044. comp.setRoughnessMetalnessImageFilename(filenames[newSelectedFilename]);
  1045. }
  1046. }
  1047. // Roughness/metallic factor
  1048. ImGui::SetNextItemWidth(-1.0f);
  1049. F32 rmFactor = comp.getRoughnessMetalnessBlendFactor();
  1050. if(ImGui::SliderFloat("##Factor1", &rmFactor, 0.0f, 1.0f))
  1051. {
  1052. comp.setRoughnessMetalnessBlendFactor(rmFactor);
  1053. }
  1054. ImGui::SetItemTooltip("Blend Factor");
  1055. }
  1056. void EditorUi::cVarsWindow()
  1057. {
  1058. if(!m_showCVarEditorWindow)
  1059. {
  1060. return;
  1061. }
  1062. F32 maxCvarTextSize = 0.0f;
  1063. CVarSet::getSingleton().iterateCVars([&](CVar& cvar) {
  1064. maxCvarTextSize = max(maxCvarTextSize, ImGui::CalcTextSize(cvar.getName().cstr()).x);
  1065. return FunctorContinue::kContinue;
  1066. });
  1067. if(ImGui::GetFrameCount() > 1)
  1068. {
  1069. // Viewport is one frame delay so do that when frame >1
  1070. const Vec2 initialSize = Vec2(900.0f, m_canvas->getSizef().y() * 0.8f);
  1071. ImGui::SetNextWindowSize(initialSize, ImGuiCond_FirstUseEver);
  1072. ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Once, Vec2(0.5f));
  1073. }
  1074. if(ImGui::Begin("CVars Editor", &m_showCVarEditorWindow, 0))
  1075. {
  1076. filter(m_cvarsEditorWindow.m_filter);
  1077. if(ImGui::BeginChild("##Child", Vec2(0.0f), ImGuiChildFlags_Borders | ImGuiChildFlags_NavFlattened))
  1078. {
  1079. if(ImGui::BeginTable("Table", 2, 0))
  1080. {
  1081. ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, maxCvarTextSize + 20.0f);
  1082. ImGui::TableSetupColumn("Value");
  1083. I32 id = 0;
  1084. CVarSet::getSingleton().iterateCVars([&](CVar& cvar) {
  1085. if(!m_cvarsEditorWindow.m_filter.PassFilter(cvar.getName().cstr()))
  1086. {
  1087. return FunctorContinue::kContinue;
  1088. }
  1089. ImGui::TableNextRow();
  1090. ImGui::TableNextColumn();
  1091. ImGui::TextUnformatted(cvar.getName().cstr());
  1092. ImGui::TableNextColumn();
  1093. ImGui::PushID(id++);
  1094. ImGui::SetNextItemWidth(-1.0f);
  1095. if(cvar.getValueType() == CVarValueType::kBool)
  1096. {
  1097. BoolCVar& bcvar = static_cast<BoolCVar&>(cvar);
  1098. Bool val = bcvar;
  1099. ImGui::Checkbox("", &val);
  1100. bcvar = val;
  1101. }
  1102. else if(cvar.getValueType() == CVarValueType::kNumericF32)
  1103. {
  1104. NumericCVar<F32>& bcvar = static_cast<NumericCVar<F32>&>(cvar);
  1105. F32 val = bcvar;
  1106. ImGui::InputFloat("", &val);
  1107. bcvar = val;
  1108. }
  1109. else if(cvar.getValueType() == CVarValueType::kNumericF64)
  1110. {
  1111. NumericCVar<F64>& bcvar = static_cast<NumericCVar<F64>&>(cvar);
  1112. F64 val = bcvar;
  1113. ImGui::InputDouble("", &val);
  1114. bcvar = val;
  1115. }
  1116. else if(cvar.getValueType() == CVarValueType::kNumericU8)
  1117. {
  1118. NumericCVar<U8>& bcvar = static_cast<NumericCVar<U8>&>(cvar);
  1119. I32 val = bcvar;
  1120. ImGui::InputInt("", &val);
  1121. bcvar = U8(val);
  1122. }
  1123. else if(cvar.getValueType() == CVarValueType::kNumericU16)
  1124. {
  1125. NumericCVar<U16>& bcvar = static_cast<NumericCVar<U16>&>(cvar);
  1126. I32 val = bcvar;
  1127. ImGui::InputInt("", &val);
  1128. bcvar = U8(val);
  1129. }
  1130. else if(cvar.getValueType() == CVarValueType::kNumericU32)
  1131. {
  1132. NumericCVar<U32>& bcvar = static_cast<NumericCVar<U32>&>(cvar);
  1133. I32 val = bcvar;
  1134. ImGui::InputInt("", &val);
  1135. bcvar = val;
  1136. }
  1137. else if(cvar.getValueType() == CVarValueType::kNumericPtrSize)
  1138. {
  1139. NumericCVar<PtrSize>& bcvar = static_cast<NumericCVar<PtrSize>&>(cvar);
  1140. I32 val = I32(bcvar);
  1141. ImGui::InputInt("", &val);
  1142. bcvar = val;
  1143. }
  1144. else if(cvar.getValueType() == CVarValueType::kString)
  1145. {
  1146. StringCVar& bcvar = static_cast<StringCVar&>(cvar);
  1147. CString value = bcvar;
  1148. Char str[256];
  1149. std::strncpy(str, value.cstr(), sizeof(str));
  1150. ImGui::InputText("", str, sizeof(str));
  1151. bcvar = str;
  1152. }
  1153. else
  1154. {
  1155. ANKI_ASSERT(!"TODO");
  1156. }
  1157. ImGui::PopID();
  1158. return FunctorContinue::kContinue;
  1159. });
  1160. ImGui::EndTable();
  1161. }
  1162. }
  1163. ImGui::EndChild();
  1164. }
  1165. ImGui::End();
  1166. }
  1167. void EditorUi::debugRtsWindow()
  1168. {
  1169. if(!m_showDebugRtsWindow)
  1170. {
  1171. return;
  1172. }
  1173. if(ImGui::GetFrameCount() > 1)
  1174. {
  1175. // Viewport is one frame delay so do that when frame >1
  1176. const Vec2 initialSize = Vec2(450.0f, m_canvas->getSizef().y() * 0.4f);
  1177. ImGui::SetNextWindowSize(initialSize, ImGuiCond_FirstUseEver);
  1178. ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Once, Vec2(0.5f));
  1179. }
  1180. if(ImGui::Begin("Debug Render Targets", &m_showDebugRtsWindow, 0))
  1181. {
  1182. const Bool refresh = ImGui::Checkbox("Disable tonemapping", &m_debugRtsWindow.m_disableTonemapping);
  1183. ImGui::TextUnformatted("");
  1184. ImGui::Separator();
  1185. if(ImGui::BeginChild("Content", Vec2(-1.0f, -1.0f)))
  1186. {
  1187. // Gather the names
  1188. DynamicArray<String> rtNames;
  1189. Renderer::getSingleton().iterateDebugRenderTargetNames([&](CString name) {
  1190. rtNames.emplaceBack(name);
  1191. return FunctorContinue::kContinue;
  1192. });
  1193. std::sort(rtNames.getBegin(), rtNames.getEnd());
  1194. for(const String& name : rtNames)
  1195. {
  1196. Bool isActive = (name == Renderer::getSingleton().getCurrentDebugRenderTarget());
  1197. if(ImGui::Checkbox(name.cstr(), &isActive) || (isActive && refresh))
  1198. {
  1199. Renderer::getSingleton().setCurrentDebugRenderTarget(isActive ? name : "", m_debugRtsWindow.m_disableTonemapping);
  1200. }
  1201. }
  1202. }
  1203. ImGui::EndChild();
  1204. }
  1205. ImGui::End();
  1206. }
  1207. void EditorUi::consoleWindow()
  1208. {
  1209. if(!m_showConsoleWindow)
  1210. {
  1211. return;
  1212. }
  1213. auto& state = m_consoleWindow;
  1214. if(ImGui::GetFrameCount() > 1)
  1215. {
  1216. // Viewport is one frame delay so do that when frame >1
  1217. const Vec2 viewportSize = ImGui::GetMainViewport()->WorkSize;
  1218. const Vec2 viewportPos = ImGui::GetMainViewport()->WorkPos;
  1219. const Vec2 initialSize = Vec2(viewportSize.x() / 2.0f, kConsoleHeight);
  1220. const Vec2 initialPos = Vec2(0.0f, viewportPos.y() + viewportSize.y() - initialSize.y());
  1221. ImGui::SetNextWindowSize(initialSize, ImGuiCond_FirstUseEver);
  1222. ImGui::SetNextWindowPos(initialPos, ImGuiCond_FirstUseEver);
  1223. }
  1224. if(ImGui::Begin("Console", &m_showConsoleWindow, ImGuiWindowFlags_NoCollapse))
  1225. {
  1226. // Lua input
  1227. {
  1228. Char consoleTxt[kMaxTextInputLen] = "";
  1229. ImGui::SetNextItemWidth(-1.0f);
  1230. if(ImGui::InputTextWithHint(" ", ICON_MDI_LANGUAGE_LUA " LUA console", consoleTxt, sizeof(consoleTxt),
  1231. ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_EscapeClearsAll))
  1232. {
  1233. [[maybe_unused]] const Error err = ScriptManager::getSingleton().evalString(consoleTxt);
  1234. ImGui::SetKeyboardFocusHere(-1); // On enter it loses focus so call this to fix it
  1235. }
  1236. }
  1237. // Clear Log
  1238. {
  1239. if(ImGui::Button(ICON_MDI_DELETE))
  1240. {
  1241. state.m_log.destroy();
  1242. }
  1243. ImGui::SetItemTooltip("Clear log");
  1244. ImGui::SameLine();
  1245. }
  1246. // Search log
  1247. filter(state.m_logFilter);
  1248. // Log
  1249. {
  1250. if(ImGui::BeginChild("Log", Vec2(0.0f), ImGuiChildFlags_Borders | ImGuiChildFlags_NavFlattened))
  1251. {
  1252. ImGui::PushFont(m_monospaceFont, 0.0f);
  1253. if(ImGui::BeginTable("##bg", 1, ImGuiTableFlags_RowBg))
  1254. {
  1255. LockGuard lock(state.m_logMtx);
  1256. for(const auto& logEntry : state.m_log)
  1257. {
  1258. if(state.m_logFilter.PassFilter(logEntry.second.cstr()))
  1259. {
  1260. ImGui::TableNextRow();
  1261. ImGui::TableNextColumn();
  1262. constexpr Array<Vec3, U(LoggerMessageType::kCount)> colors = {Vec3(0.074f, 0.631f, 0.054f), Vec3(0.074f, 0.354f, 0.631f),
  1263. Vec3(1.0f, 0.0f, 0.0f), Vec3(0.756f, 0.611f, 0.0f),
  1264. Vec3(1.0f, 0.0f, 0.0f)};
  1265. ImGui::PushStyleColor(ImGuiCol_Text, colors[logEntry.first].xyz1());
  1266. ImGui::TextUnformatted(logEntry.second.cstr());
  1267. ImGui::PopStyleColor();
  1268. }
  1269. }
  1270. if(state.m_forceLogScrollDown)
  1271. {
  1272. ImGui::SetScrollHereY(1.0f);
  1273. state.m_forceLogScrollDown = false;
  1274. }
  1275. ImGui::EndTable();
  1276. }
  1277. ImGui::PopFont();
  1278. }
  1279. ImGui::EndChild();
  1280. }
  1281. }
  1282. ImGui::End();
  1283. }
  1284. void EditorUi::dirTree(const AssetPath& path)
  1285. {
  1286. auto& state = m_assetsWindow;
  1287. ImGui::TableNextRow();
  1288. ImGui::TableNextColumn();
  1289. ImGuiTreeNodeFlags treeFlags = ImGuiTreeNodeFlags_None;
  1290. treeFlags |= ImGuiTreeNodeFlags_OpenOnArrow
  1291. | ImGuiTreeNodeFlags_OpenOnDoubleClick; // Standard opening mode as we are likely to want to add selection afterwards
  1292. treeFlags |= ImGuiTreeNodeFlags_NavLeftJumpsToParent; // Left arrow support
  1293. treeFlags |= ImGuiTreeNodeFlags_SpanFullWidth; // Span full width for easier mouse reach
  1294. treeFlags |= ImGuiTreeNodeFlags_DrawLinesToNodes; // Always draw hierarchy outlines
  1295. if(state.m_pathSelected == &path)
  1296. {
  1297. treeFlags |= ImGuiTreeNodeFlags_Selected;
  1298. }
  1299. const Bool hasChildren = path.m_children.getSize();
  1300. if(!hasChildren)
  1301. {
  1302. treeFlags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_Bullet;
  1303. }
  1304. ImGui::PushID(path.m_id);
  1305. const Bool nodeOpen = ImGui::TreeNodeEx("", treeFlags, "%s", path.m_dirname.cstr());
  1306. ImGui::PopID();
  1307. ImGui::SetItemTooltip("%s", path.m_dirname.cstr());
  1308. if(ImGui::IsItemFocused())
  1309. {
  1310. state.m_pathSelected = &path;
  1311. }
  1312. if(nodeOpen)
  1313. {
  1314. for(const AssetPath& p : path.m_children)
  1315. {
  1316. dirTree(p);
  1317. }
  1318. ImGui::TreePop();
  1319. }
  1320. }
  1321. void EditorUi::assetsWindow()
  1322. {
  1323. if(!m_showAssetsWindow)
  1324. {
  1325. return;
  1326. }
  1327. auto& state = m_assetsWindow;
  1328. if(ImGui::GetFrameCount() > 1)
  1329. {
  1330. // Viewport is one frame delay so do that when frame >1
  1331. const Vec2 viewportSize = ImGui::GetMainViewport()->WorkSize;
  1332. const Vec2 viewportPos = ImGui::GetMainViewport()->WorkPos;
  1333. const Vec2 initialSize = Vec2(viewportSize.x() / 2.0f, kConsoleHeight);
  1334. const Vec2 initialPos = Vec2(viewportSize.x() / 2.0f, viewportPos.y() + viewportSize.y() - initialSize.y());
  1335. ImGui::SetNextWindowSize(initialSize, ImGuiCond_FirstUseEver);
  1336. ImGui::SetNextWindowPos(initialPos, ImGuiCond_FirstUseEver);
  1337. }
  1338. if(ImGui::Begin("Assets", &m_showAssetsWindow, ImGuiWindowFlags_NoCollapse))
  1339. {
  1340. // Left side
  1341. {
  1342. if(ImGui::BeginChild("Left", Vec2(300.0f, -1.0f), ImGuiChildFlags_ResizeX | ImGuiChildFlags_Borders | ImGuiChildFlags_NavFlattened))
  1343. {
  1344. if(ImGui::BeginTable("##bg", 1, ImGuiTableFlags_RowBg))
  1345. {
  1346. for(const AssetPath& p : state.m_assetPaths)
  1347. {
  1348. dirTree(p);
  1349. }
  1350. ImGui::EndTable();
  1351. }
  1352. }
  1353. ImGui::EndChild();
  1354. } // Left side
  1355. ImGui::SameLine();
  1356. // Right side
  1357. {
  1358. // Use the filter to gather the files
  1359. DynamicArray<const AssetFile*> filteredFiles;
  1360. if(state.m_pathSelected)
  1361. {
  1362. for(const AssetFile& f : state.m_pathSelected->m_files)
  1363. {
  1364. if(state.m_fileFilter.PassFilter(f.m_basename.cstr()))
  1365. {
  1366. filteredFiles.emplaceBack(&f);
  1367. }
  1368. }
  1369. }
  1370. if(ImGui::BeginChild("Right", Vec2(-1.0f, -1.0f), 0))
  1371. {
  1372. // Increase/decrease icon size
  1373. {
  1374. ImGui::TextUnformatted("Icon Size");
  1375. ImGui::SameLine();
  1376. ImGui::SetNextItemWidth(64.0f);
  1377. ImGui::SliderInt("##Icon Size", &state.m_cellSize, 5, 11, "%d", ImGuiSliderFlags_AlwaysClamp);
  1378. ImGui::SameLine();
  1379. }
  1380. filter(state.m_fileFilter);
  1381. if(ImGui::BeginChild("RightBottom", Vec2(-1.0f, -1.0f), 0))
  1382. {
  1383. const F32 cellWidth = F32(state.m_cellSize) * 16;
  1384. const U32 columnCount = U32(ImGui::GetContentRegionAvail().x / cellWidth);
  1385. ImGui::SetNextItemWidth(-1.0f);
  1386. const ImGuiTableFlags flags = ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersOuter
  1387. | ImGuiTableFlags_BordersV | ImGuiTableFlags_ContextMenuInBody;
  1388. if(filteredFiles.getSize() && ImGui::BeginTable("Grid", columnCount, flags))
  1389. {
  1390. const U32 rowCount = (filteredFiles.getSize() + columnCount - 1) / columnCount;
  1391. for(U32 row = 0; row < rowCount; ++row)
  1392. {
  1393. ImGui::TableNextRow();
  1394. for(U32 column = 0; column < columnCount; ++column)
  1395. {
  1396. ImGui::TableNextColumn();
  1397. const U32 idx = row * columnCount + column;
  1398. if(idx < filteredFiles.getSize())
  1399. {
  1400. const AssetFile& file = *filteredFiles[idx];
  1401. ImGui::PushID(idx);
  1402. if(file.m_type == AssetFileType::kMaterial)
  1403. {
  1404. ImTextureID id;
  1405. id.m_texture = &m_materialIcon->getTexture();
  1406. ImGui::ImageButton("##", id, Vec2(cellWidth));
  1407. }
  1408. else if(file.m_type == AssetFileType::kMesh)
  1409. {
  1410. ImTextureID id;
  1411. id.m_texture = &m_meshIcon->getTexture();
  1412. ImGui::ImageButton("##", id, Vec2(cellWidth));
  1413. }
  1414. else if(file.m_type == AssetFileType::kTexture)
  1415. {
  1416. ImageResourcePtr img;
  1417. loadImageToCache(file.m_filename, img);
  1418. ImTextureID id;
  1419. id.m_texture = &img->getTexture();
  1420. id.m_textureSubresource = TextureSubresourceDesc::all();
  1421. if(ImGui::ImageButton("##", id, Vec2(cellWidth)))
  1422. {
  1423. m_imageViewer.m_image = img;
  1424. m_imageViewer.m_open = true;
  1425. }
  1426. }
  1427. else if(file.m_type == AssetFileType::kParticleEmitter)
  1428. {
  1429. ImGui::PushFont(nullptr, cellWidth - 1.0f);
  1430. if(ImGui::Button(ICON_MDI_CREATION, Vec2(cellWidth)))
  1431. {
  1432. ParticleEmitterResource2Ptr rsrc;
  1433. ANKI_CHECKF(ResourceManager::getSingleton().loadResource(file.m_filename, rsrc));
  1434. m_particlesEditor.open(*rsrc);
  1435. }
  1436. ImGui::PopFont();
  1437. }
  1438. ImGui::PopID();
  1439. ImGui::TextWrapped("%s", file.m_basename.cstr());
  1440. ImGui::SetItemTooltip("%s", file.m_filename.cstr());
  1441. }
  1442. }
  1443. }
  1444. ImGui::EndTable();
  1445. }
  1446. else
  1447. {
  1448. ImGui::TextUnformatted("Empty");
  1449. }
  1450. }
  1451. ImGui::EndChild();
  1452. }
  1453. ImGui::EndChild();
  1454. } // Right side
  1455. }
  1456. ImGui::End();
  1457. }
  1458. void EditorUi::filter(ImGuiTextFilter& filter)
  1459. {
  1460. ImGui::SetNextItemWidth(-FLT_MIN);
  1461. ImGui::SetNextItemShortcut(ImGuiMod_Ctrl | ImGuiKey_F, ImGuiInputFlags_Tooltip);
  1462. ImGui::PushItemFlag(ImGuiItemFlags_NoNavDefaultFocus, true);
  1463. if(ImGui::InputTextWithHint("##Filter", ICON_MDI_MAGNIFY " Search incl,-excl", filter.InputBuf, IM_ARRAYSIZE(filter.InputBuf),
  1464. ImGuiInputTextFlags_EscapeClearsAll))
  1465. {
  1466. filter.Build();
  1467. }
  1468. ImGui::PopItemFlag();
  1469. }
  1470. void EditorUi::dummyButton(I32 id)
  1471. {
  1472. ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.0f);
  1473. ImGui::PushID(id);
  1474. ImGui::Button(ICON_MDI_LOCK);
  1475. ImGui::PopID();
  1476. ImGui::PopStyleVar();
  1477. ImGui::SameLine();
  1478. };
  1479. template<typename TItemArray>
  1480. void EditorUi::comboWithFilter(CString text, const TItemArray& items, CString selectedItemIn, U32& selectedItemOut, ImGuiTextFilter& filter)
  1481. {
  1482. if(ImGui::BeginCombo(text.cstr(), selectedItemIn.cstr()))
  1483. {
  1484. if(ImGui::IsWindowAppearing())
  1485. {
  1486. ImGui::SetKeyboardFocusHere();
  1487. filter.Clear();
  1488. }
  1489. ImGui::SetNextItemWidth(-1.0f);
  1490. if(ImGui::InputTextWithHint("##Filter", ICON_MDI_MAGNIFY " Search incl,-excl", filter.InputBuf, IM_ARRAYSIZE(filter.InputBuf),
  1491. ImGuiInputTextFlags_EscapeClearsAll))
  1492. {
  1493. filter.Build();
  1494. }
  1495. for(U32 i = 0; i < items.getSize(); ++i)
  1496. {
  1497. CString item = items[i];
  1498. if(!filter.PassFilter(item.cstr()))
  1499. {
  1500. continue;
  1501. }
  1502. const Bool isSelected = (selectedItemIn == item);
  1503. if(ImGui::Selectable(item.cstr(), isSelected))
  1504. {
  1505. selectedItemOut = i;
  1506. }
  1507. // Set the initial focus when opening the combo (scrolling + keyboard navigation focus)
  1508. if(isSelected)
  1509. {
  1510. ImGui::SetItemDefaultFocus();
  1511. }
  1512. }
  1513. ImGui::EndCombo();
  1514. }
  1515. }
  1516. Bool EditorUi::textEditorWindow(CString extraWindowTitle, Bool* pOpen, String& inout) const
  1517. {
  1518. Bool save = false;
  1519. const Vec2 windowSize(600.0f, 800.0f);
  1520. ImGui::SetNextWindowSize(windowSize, ImGuiCond_FirstUseEver);
  1521. ImGui::SetNextWindowPos(Vec2(ImGui::GetMainViewport()->WorkSize) / 2.0f - windowSize / 2.0f, ImGuiCond_FirstUseEver);
  1522. const String title = String().sprintf("Text Editor: %s", extraWindowTitle.cstr());
  1523. if(ImGui::Begin(title.cstr(), pOpen))
  1524. {
  1525. if(ImGui::Button(ICON_MDI_CONTENT_SAVE " Save"))
  1526. {
  1527. save = true;
  1528. }
  1529. ImGui::SameLine();
  1530. if(pOpen && ImGui::Button(ICON_MDI_CLOSE " Close"))
  1531. {
  1532. *pOpen = false;
  1533. }
  1534. if(ImGui::IsWindowFocused() && Input::getSingleton().getKey(KeyCode::kEscape) && pOpen)
  1535. {
  1536. *pOpen = false;
  1537. }
  1538. DynamicArray<Char> buffer;
  1539. buffer.resize(max(inout.getLength() + 1, 1024_U32), '\0');
  1540. if(inout.getLength())
  1541. {
  1542. std::strncpy(buffer.getBegin(), inout.cstr(), inout.getLength());
  1543. }
  1544. auto replaceTabCallback = [](ImGuiInputTextCallbackData* data) -> int {
  1545. if(data->CursorPos > 0 && data->Buf[data->CursorPos - 1] == '\t')
  1546. {
  1547. data->DeleteChars(data->CursorPos - 1, 1);
  1548. data->InsertChars(data->CursorPos, " ");
  1549. return 0;
  1550. }
  1551. else
  1552. {
  1553. return 0;
  1554. }
  1555. };
  1556. ImGui::PushFont(m_monospaceFont, 0.0f);
  1557. if(ImGui::InputTextMultiline("##", buffer.getBegin(), buffer.getSize(), Vec2(-1.0f),
  1558. ImGuiInputTextFlags_AllowTabInput | ImGuiInputTextFlags_CallbackEdit, replaceTabCallback))
  1559. {
  1560. inout = buffer.getBegin();
  1561. }
  1562. ImGui::PopFont();
  1563. }
  1564. ImGui::End();
  1565. return save;
  1566. }
  1567. void EditorUi::loadImageToCache(CString fname, ImageResourcePtr& img)
  1568. {
  1569. // Try to load first
  1570. ANKI_CHECKF(ResourceManager::getSingleton().loadResource(fname, img));
  1571. // Update the cache
  1572. const U32 crntFrame = ImGui::GetFrameCount();
  1573. Bool entryFound = false;
  1574. DynamicArray<ImageCacheEntry>& cache = m_assetsWindow.m_imageCache;
  1575. for(ImageCacheEntry& entry : cache)
  1576. {
  1577. if(entry.m_image->getUuid() == img->getUuid())
  1578. {
  1579. entry.m_lastSeenInFrame = crntFrame;
  1580. entryFound = true;
  1581. break;
  1582. }
  1583. }
  1584. if(!entryFound)
  1585. {
  1586. cache.emplaceBack(ImageCacheEntry{img, crntFrame});
  1587. }
  1588. // Try to remove stale entries
  1589. const U32 frameInactivityCount = 60 * 30; // ~30"
  1590. while(true)
  1591. {
  1592. Bool foundStaleEntry = false;
  1593. for(auto it = cache.getBegin(); it != cache.getEnd(); ++it)
  1594. {
  1595. ANKI_ASSERT(crntFrame >= it->m_lastSeenInFrame);
  1596. if(crntFrame - it->m_lastSeenInFrame > frameInactivityCount)
  1597. {
  1598. cache.erase(it);
  1599. foundStaleEntry = true;
  1600. break;
  1601. }
  1602. }
  1603. if(!foundStaleEntry)
  1604. {
  1605. break;
  1606. }
  1607. }
  1608. }
  1609. void EditorUi::objectPicking()
  1610. {
  1611. if(!m_mouseOverAnyWindow && Input::getSingleton().getMouseButton(MouseButton::kLeft) == 1)
  1612. {
  1613. const DbgObjectPickingResult& res = Renderer::getSingleton().getDbg().getObjectPickingResultAtMousePosition();
  1614. if(res.m_sceneNodeUuid != 0)
  1615. {
  1616. SceneGraph::getSingleton().visitNodes([this, uuid = res.m_sceneNodeUuid](SceneNode& node) {
  1617. if(node.getUuid() == uuid)
  1618. {
  1619. m_sceneHierarchyWindow.m_selectedNode = &node;
  1620. ANKI_LOGV("Selecting scene node: %s", node.getName().cstr());
  1621. return FunctorContinue::kStop;
  1622. }
  1623. return FunctorContinue::kContinue;
  1624. });
  1625. m_objectPicking.m_translationAxisSelected = kMaxU32;
  1626. m_objectPicking.m_scaleAxisSelected = kMaxU32;
  1627. m_objectPicking.m_rotationAxisSelected = kMaxU32;
  1628. }
  1629. else if(res.isValid())
  1630. {
  1631. // Clicked a gizmo
  1632. const Transform& nodeTrf = m_sceneHierarchyWindow.m_selectedNode->getLocalTransform();
  1633. const Vec3 nodeOrigin = nodeTrf.getOrigin().xyz();
  1634. Array<Vec3, 3> rotationAxis;
  1635. rotationAxis[0] = nodeTrf.getRotation().getXAxis();
  1636. rotationAxis[1] = nodeTrf.getRotation().getYAxis();
  1637. rotationAxis[2] = nodeTrf.getRotation().getZAxis();
  1638. if(res.m_translationAxis < 3)
  1639. {
  1640. m_objectPicking.m_translationAxisSelected = res.m_translationAxis;
  1641. m_objectPicking.m_scaleAxisSelected = kMaxU32;
  1642. m_objectPicking.m_rotationAxisSelected = kMaxU32;
  1643. }
  1644. else if(res.m_scaleAxis < 3)
  1645. {
  1646. m_objectPicking.m_translationAxisSelected = kMaxU32;
  1647. m_objectPicking.m_scaleAxisSelected = res.m_scaleAxis;
  1648. m_objectPicking.m_rotationAxisSelected = kMaxU32;
  1649. }
  1650. else if(res.m_rotationAxis < 3)
  1651. {
  1652. m_objectPicking.m_translationAxisSelected = kMaxU32;
  1653. m_objectPicking.m_scaleAxisSelected = kMaxU32;
  1654. m_objectPicking.m_rotationAxisSelected = res.m_rotationAxis;
  1655. // Calc the pivot point
  1656. const Transform camTrf =
  1657. SceneGraph::getSingleton().getActiveCameraNode().getFirstComponentOfType<CameraComponent>().getFrustum().getWorldTransform();
  1658. Plane axisPlane;
  1659. axisPlane.setFromRay(nodeOrigin, rotationAxis[res.m_rotationAxis]);
  1660. Bool collides = projectNdcToPlane(Input::getSingleton().getMousePositionNdc(), axisPlane, m_objectPicking.m_pivotPoint);
  1661. if(!collides)
  1662. {
  1663. // Clicked the gizmo from the back side, use the negative plane
  1664. axisPlane.setFromRay(nodeOrigin, -rotationAxis[res.m_rotationAxis]);
  1665. collides = projectNdcToPlane(Input::getSingleton().getMousePositionNdc(), axisPlane, m_objectPicking.m_pivotPoint);
  1666. if(!collides)
  1667. {
  1668. ANKI_LOGW("Can't determin the pivot point");
  1669. m_objectPicking.m_pivotPoint = nodeOrigin;
  1670. }
  1671. }
  1672. }
  1673. if(res.m_translationAxis < 3 || res.m_scaleAxis < 3)
  1674. {
  1675. const U32 axis = (res.m_translationAxis < 3) ? res.m_translationAxis : res.m_scaleAxis;
  1676. m_objectPicking.m_pivotPoint =
  1677. nodeOrigin + rotationAxis[axis] * projectNdcToRay(Input::getSingleton().getMousePositionNdc(), nodeOrigin, rotationAxis[axis]);
  1678. }
  1679. }
  1680. else
  1681. {
  1682. // Clicked on sky
  1683. m_sceneHierarchyWindow.m_selectedNode = nullptr;
  1684. m_objectPicking.m_translationAxisSelected = kMaxU32;
  1685. m_objectPicking.m_scaleAxisSelected = kMaxU32;
  1686. m_objectPicking.m_rotationAxisSelected = kMaxU32;
  1687. }
  1688. }
  1689. if(!m_mouseOverAnyWindow && Input::getSingleton().getMouseButton(MouseButton::kLeft) > 1 && m_sceneHierarchyWindow.m_selectedNode)
  1690. {
  1691. // Possibly dragging
  1692. const Transform& nodeTrf = m_sceneHierarchyWindow.m_selectedNode->getLocalTransform();
  1693. Array<Vec3, 3> rotationAxis;
  1694. rotationAxis[0] = nodeTrf.getRotation().getXAxis();
  1695. rotationAxis[1] = nodeTrf.getRotation().getYAxis();
  1696. rotationAxis[2] = nodeTrf.getRotation().getZAxis();
  1697. if(m_objectPicking.m_translationAxisSelected < 3)
  1698. {
  1699. const U32 axis = m_objectPicking.m_translationAxisSelected;
  1700. const F32 moveDistance = projectNdcToRay(Input::getSingleton().getMousePositionNdc(), m_objectPicking.m_pivotPoint, rotationAxis[axis]);
  1701. const Vec3 oldPosition = nodeTrf.getOrigin().xyz();
  1702. Vec3 newPosition = oldPosition + rotationAxis[axis] * moveDistance;
  1703. // Snap position
  1704. for(U32 i = 0; i < 3 && m_toolbox.m_scaleTranslationSnapping > kEpsilonf; ++i)
  1705. {
  1706. newPosition[i] = round(newPosition[i] / m_toolbox.m_scaleTranslationSnapping) * m_toolbox.m_scaleTranslationSnapping;
  1707. }
  1708. m_sceneHierarchyWindow.m_selectedNode->setLocalOrigin(newPosition);
  1709. // Update the pivot
  1710. const Vec3 moveDiff = newPosition - oldPosition; // Move the pivot as you moved the node origin
  1711. m_objectPicking.m_pivotPoint = m_objectPicking.m_pivotPoint + moveDiff;
  1712. }
  1713. else if(m_objectPicking.m_scaleAxisSelected < 3)
  1714. {
  1715. const U32 axis = m_objectPicking.m_scaleAxisSelected;
  1716. const F32 moveDistance = projectNdcToRay(Input::getSingleton().getMousePositionNdc(), m_objectPicking.m_pivotPoint, rotationAxis[axis]);
  1717. const F32 oldAxisScale = nodeTrf.getScale()[axis];
  1718. F32 newAxisScale = oldAxisScale + moveDistance;
  1719. // Snap scale
  1720. if(m_toolbox.m_scaleTranslationSnapping > kEpsilonf)
  1721. {
  1722. newAxisScale = round(newAxisScale / m_toolbox.m_scaleTranslationSnapping) * m_toolbox.m_scaleTranslationSnapping;
  1723. }
  1724. Vec3 scale = nodeTrf.getScale().xyz();
  1725. scale[axis] = max(newAxisScale, m_toolbox.m_scaleTranslationSnapping);
  1726. m_sceneHierarchyWindow.m_selectedNode->setLocalScale(scale);
  1727. // Update the pivot
  1728. const F32 adjustedMoveDistance = newAxisScale - oldAxisScale;
  1729. m_objectPicking.m_pivotPoint = m_objectPicking.m_pivotPoint + rotationAxis[axis] * adjustedMoveDistance;
  1730. }
  1731. else if(m_objectPicking.m_rotationAxisSelected < 3)
  1732. {
  1733. const U32 axis = m_objectPicking.m_rotationAxisSelected;
  1734. const Vec3 nodeOrigin = nodeTrf.getOrigin().xyz();
  1735. // Compute the new pivot point
  1736. Plane axisPlane;
  1737. axisPlane.setFromRay(nodeOrigin, rotationAxis[axis]);
  1738. Vec3 newPivotPoint;
  1739. Bool collides = projectNdcToPlane(Input::getSingleton().getMousePositionNdc(), axisPlane, newPivotPoint);
  1740. if(!collides)
  1741. {
  1742. // Clicked the gizmo from the back side, use the negative plane
  1743. axisPlane.setFromRay(nodeOrigin, -rotationAxis[axis]);
  1744. collides = projectNdcToPlane(Input::getSingleton().getMousePositionNdc(), axisPlane, newPivotPoint);
  1745. if(!collides)
  1746. {
  1747. ANKI_LOGW("Can't determin the pivot point");
  1748. newPivotPoint = nodeOrigin;
  1749. }
  1750. }
  1751. // Compute the angle between the points
  1752. const Vec3 dir0 = (newPivotPoint - nodeOrigin).normalize();
  1753. const Vec3 dir1 = (m_objectPicking.m_pivotPoint - nodeOrigin).normalize();
  1754. F32 angle = acos(saturate(dir1.dot(dir0)));
  1755. if(m_toolbox.m_rotationSnappingDeg > kEpsilonf)
  1756. {
  1757. const F32 rad = toRad(m_toolbox.m_rotationSnappingDeg);
  1758. angle = round(angle / rad) * rad;
  1759. }
  1760. Vec3 cross = dir1.cross(dir0);
  1761. if(cross != Vec3(0.0f))
  1762. {
  1763. cross = cross.normalize();
  1764. const F32 angleSign = (cross.dot(rotationAxis[axis]) < 1.0f) ? -1.0f : 1.0f;
  1765. angle *= angleSign;
  1766. // Apply the angle
  1767. if(m_objectPicking.m_rotationAxisSelected == 0)
  1768. {
  1769. m_sceneHierarchyWindow.m_selectedNode->rotateLocalX(angle);
  1770. }
  1771. else if(m_objectPicking.m_rotationAxisSelected == 1)
  1772. {
  1773. m_sceneHierarchyWindow.m_selectedNode->rotateLocalY(angle);
  1774. }
  1775. else
  1776. {
  1777. m_sceneHierarchyWindow.m_selectedNode->rotateLocalZ(angle);
  1778. }
  1779. // Use the snapped angle to adjust the pivot point
  1780. Mat3 rot;
  1781. rot.setZAxis(rotationAxis[axis]);
  1782. rot.setXAxis(dir1);
  1783. rot.setYAxis(rotationAxis[axis].cross(dir1).normalize());
  1784. rot.rotateZAxis(angle);
  1785. newPivotPoint = nodeOrigin + rot.getXAxis() * (newPivotPoint - nodeOrigin).length();
  1786. m_objectPicking.m_pivotPoint = newPivotPoint;
  1787. }
  1788. }
  1789. }
  1790. if(m_sceneHierarchyWindow.m_selectedNode)
  1791. {
  1792. Renderer::getSingleton().getDbg().enableGizmos(m_sceneHierarchyWindow.m_selectedNode->getWorldTransform(), true);
  1793. }
  1794. else
  1795. {
  1796. Renderer::getSingleton().getDbg().enableGizmos(Transform::getIdentity(), false);
  1797. }
  1798. }
  1799. DynamicArray<CString> EditorUi::gatherResourceFilenames(CString filenameContains)
  1800. {
  1801. DynamicArray<CString> out;
  1802. ResourceFilesystem::getSingleton().iterateAllFilenames([&](CString fname) {
  1803. if(fname.find(filenameContains) != CString::kNpos)
  1804. {
  1805. out.emplaceBack(fname);
  1806. }
  1807. return FunctorContinue::kContinue;
  1808. });
  1809. return out;
  1810. }
  1811. } // end namespace anki