EditorUi.cpp 60 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139
  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. if(ImGui::Button(ICON_MDI_CONTENT_SAVE_ALL))
  348. {
  349. if(SceneGraph::getSingleton().saveToTextFile("./scene.ankiscene"))
  350. {
  351. ANKI_LOGE("Failed to save scene");
  352. }
  353. else
  354. {
  355. ANKI_LOGI("Scene saved");
  356. }
  357. }
  358. ImGui::SetItemTooltip("Save scene");
  359. ImGui::SameLine();
  360. ImGui::SetNextItemWidth(ImGui::CalcTextSize("00.000").x);
  361. if(ImGui::SliderFloat(ICON_MDI_AXIS_ARROW "&" ICON_MDI_ARROW_EXPAND_ALL " Snapping", &m_toolbox.m_scaleTranslationSnapping, 0.0, 10.0f))
  362. {
  363. const F32 roundTo = 0.5f;
  364. m_toolbox.m_scaleTranslationSnapping = round(m_toolbox.m_scaleTranslationSnapping / roundTo) * roundTo;
  365. }
  366. ImGui::SameLine();
  367. ImGui::SetNextItemWidth(ImGui::CalcTextSize("00.000").x);
  368. if(ImGui::SliderFloat(ICON_MDI_ROTATE_ORBIT " Snapping", &m_toolbox.m_rotationSnappingDeg, 0.0, 90.0f))
  369. {
  370. const F32 roundTo = 1.0f;
  371. m_toolbox.m_rotationSnappingDeg = round(m_toolbox.m_rotationSnappingDeg / roundTo) * roundTo;
  372. }
  373. ImGui::EndMenuBar();
  374. }
  375. }
  376. ImGui::End();
  377. }
  378. void EditorUi::sceneNode(SceneNode& node)
  379. {
  380. ImGui::TableNextRow();
  381. ImGui::TableNextColumn();
  382. ImGui::PushID(I32(node.getUuid()));
  383. ImGuiTreeNodeFlags treeFlags = ImGuiTreeNodeFlags_None;
  384. treeFlags |= ImGuiTreeNodeFlags_OpenOnArrow
  385. | ImGuiTreeNodeFlags_OpenOnDoubleClick; // Standard opening mode as we are likely to want to add selection afterwards
  386. treeFlags |= ImGuiTreeNodeFlags_NavLeftJumpsToParent; // Left arrow support
  387. treeFlags |= ImGuiTreeNodeFlags_SpanFullWidth; // Span full width for easier mouse reach
  388. treeFlags |= ImGuiTreeNodeFlags_DrawLinesToNodes; // Always draw hierarchy outlines
  389. if(&node == m_sceneHierarchyWindow.m_selectedNode)
  390. {
  391. treeFlags |= ImGuiTreeNodeFlags_Selected;
  392. }
  393. if(!node.hasChildren())
  394. {
  395. treeFlags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_Bullet;
  396. }
  397. String componentsString;
  398. for(SceneComponentType sceneComponentType : EnumBitsIterable<SceneComponentType, SceneComponentTypeMask>(node.getSceneComponentMask()))
  399. {
  400. switch(sceneComponentType)
  401. {
  402. #define ANKI_DEFINE_SCENE_COMPONENT(name, weight, sceneNodeCanHaveMany, icon, serializable) \
  403. case SceneComponentType::k##name: \
  404. componentsString += ICON_MDI_##icon; \
  405. break;
  406. #include <AnKi/Scene/Components/SceneComponentClasses.def.h>
  407. default:
  408. ANKI_ASSERT(0);
  409. }
  410. }
  411. const Bool nodeOpen = ImGui::TreeNodeEx("", treeFlags, "%s %s", node.getName().cstr(), componentsString.cstr());
  412. if(ImGui::IsItemFocused())
  413. {
  414. m_sceneHierarchyWindow.m_selectedNode = &node;
  415. }
  416. if(nodeOpen)
  417. {
  418. for(SceneNode* child : node.getChildren())
  419. {
  420. sceneNode(*child);
  421. }
  422. ImGui::TreePop();
  423. }
  424. ImGui::PopID();
  425. }
  426. void EditorUi::sceneHierarchyWindow()
  427. {
  428. if(!m_showSceneHierarcyWindow)
  429. {
  430. return;
  431. }
  432. if(ImGui::GetFrameCount() > 1)
  433. {
  434. // Viewport is one frame delay so do that when frame >1
  435. const Vec2 viewportSize = ImGui::GetMainViewport()->WorkSize;
  436. const Vec2 viewportPos = ImGui::GetMainViewport()->WorkPos;
  437. ImGui::SetNextWindowPos(viewportPos, ImGuiCond_FirstUseEver);
  438. ImGui::SetNextWindowSize(Vec2(400.0f, viewportSize.y - kConsoleHeight), ImGuiCond_FirstUseEver);
  439. }
  440. if(ImGui::Begin("Scene Hierarchy", &m_showSceneHierarcyWindow, ImGuiWindowFlags_NoCollapse))
  441. {
  442. filter(m_sceneHierarchyWindow.m_filter);
  443. if(ImGui::BeginChild("##tree", Vec2(0.0f), ImGuiChildFlags_Borders | ImGuiChildFlags_NavFlattened, ImGuiWindowFlags_None))
  444. {
  445. if(ImGui::BeginTable("##bg", 1, ImGuiTableFlags_RowBg))
  446. {
  447. SceneGraph::getSingleton().visitNodes([this](SceneNode& node) {
  448. if(!node.getParent() && m_sceneHierarchyWindow.m_filter.PassFilter(node.getName().cstr()))
  449. {
  450. sceneNode(node);
  451. }
  452. return FunctorContinue::kContinue;
  453. });
  454. ImGui::EndTable();
  455. }
  456. }
  457. ImGui::EndChild();
  458. }
  459. ImGui::End();
  460. }
  461. void EditorUi::sceneNodePropertiesWindow()
  462. {
  463. if(!m_showSceneNodePropsWindow)
  464. {
  465. return;
  466. }
  467. auto& state = m_sceneNodePropsWindow;
  468. if(ImGui::GetFrameCount() > 1)
  469. {
  470. // Viewport is one frame delay so do that when frame >1
  471. const Vec2 viewportSize = ImGui::GetMainViewport()->WorkSize;
  472. const Vec2 viewportPos = ImGui::GetMainViewport()->WorkPos;
  473. const F32 initialWidth = 500.0f;
  474. ImGui::SetNextWindowPos(Vec2(viewportSize.x - initialWidth, viewportPos.y), ImGuiCond_FirstUseEver);
  475. ImGui::SetNextWindowSize(Vec2(initialWidth, viewportSize.y - kConsoleHeight), ImGuiCond_FirstUseEver);
  476. }
  477. if(ImGui::Begin("SceneNode Props", &m_showSceneNodePropsWindow, ImGuiWindowFlags_NoCollapse) && m_sceneHierarchyWindow.m_selectedNode)
  478. {
  479. SceneNode& node = *m_sceneHierarchyWindow.m_selectedNode;
  480. I32 id = 0;
  481. if(state.m_currentSceneNodeUuid != node.getUuid())
  482. {
  483. // Node changed, reset a few things
  484. state.m_currentSceneNodeUuid = node.getUuid();
  485. state.m_textEditorOpen = false;
  486. state.m_scriptComponentThatHasTheTextEditorOpen = 0;
  487. state.m_textEditorTxt.destroy();
  488. }
  489. ANKI_PUSH_POP_TEXT_INPUT_WIDTH(12);
  490. // Name
  491. {
  492. dummyButton(id++);
  493. Array<Char, kMaxTextInputLen> name;
  494. std::strncpy(name.getBegin(), node.getName().getBegin(), name.getSize());
  495. if(ImGui::InputText("Name", name.getBegin(), name.getSize(), ImGuiInputTextFlags_EnterReturnsTrue))
  496. {
  497. node.setName(&name[0]);
  498. }
  499. }
  500. // Local transform
  501. {
  502. dummyButton(id++);
  503. F32 localOrigin[3] = {node.getLocalOrigin().x, node.getLocalOrigin().y, node.getLocalOrigin().z};
  504. if(ImGui::DragFloat3(ICON_MDI_AXIS_ARROW " Origin", localOrigin, 0.025f, -1000000.0f, 1000000.0f))
  505. {
  506. node.setLocalOrigin(Vec3(&localOrigin[0]));
  507. }
  508. }
  509. // Local scale
  510. {
  511. ImGui::PushID(id++);
  512. if(ImGui::Button(state.m_uniformScale ? ICON_MDI_LOCK : ICON_MDI_LOCK_OPEN))
  513. {
  514. state.m_uniformScale = !state.m_uniformScale;
  515. }
  516. ImGui::SetItemTooltip("Uniform/Non-uniform scale");
  517. ImGui::PopID();
  518. ImGui::SameLine();
  519. F32 localScale[3] = {node.getLocalScale().x, node.getLocalScale().y, node.getLocalScale().z};
  520. if(ImGui::DragFloat3(ICON_MDI_ARROW_EXPAND_ALL " Scale", localScale, 0.0025f, 0.01f, 1000000.0f))
  521. {
  522. if(!state.m_uniformScale)
  523. {
  524. node.setLocalScale(Vec3(&localScale[0]));
  525. }
  526. else
  527. {
  528. // The component that have changed wins
  529. F32 scale = scale = localScale[2];
  530. if(localScale[0] != node.getLocalScale().x)
  531. {
  532. scale = localScale[0];
  533. }
  534. else if(localScale[1] != node.getLocalScale().y)
  535. {
  536. scale = localScale[1];
  537. }
  538. node.setLocalScale(Vec3(scale));
  539. }
  540. }
  541. }
  542. // Local rotation
  543. {
  544. dummyButton(id++);
  545. const Euler rot(node.getLocalRotation());
  546. F32 localRotation[3] = {toDegrees(rot.x), toDegrees(rot.y), toDegrees(rot.z)};
  547. if(ImGui::DragFloat3(ICON_MDI_ROTATE_ORBIT " Rotation", localRotation, 0.25f, -360.0f, 360.0f, "%.3f", ImGuiSliderFlags_AlwaysClamp))
  548. {
  549. const Euler rot(toRad(localRotation[0]), toRad(localRotation[1]), toRad(localRotation[2]));
  550. node.setLocalRotation(Mat3(rot));
  551. }
  552. ImGui::Text(" ");
  553. }
  554. // Component controls
  555. {
  556. if(ImGui::Button(ICON_MDI_PLUS_BOX))
  557. {
  558. switch(SceneComponentType(state.m_selectedSceneComponentType))
  559. {
  560. #define ANKI_DEFINE_SCENE_COMPONENT(name, weight, sceneNodeCanHaveMany, icon, serializable) \
  561. case SceneComponentType::k##name: \
  562. node.newComponent<name##Component>(); \
  563. break;
  564. #include <AnKi/Scene/Components/SceneComponentClasses.def.h>
  565. default:
  566. ANKI_ASSERT(0);
  567. }
  568. }
  569. ImGui::SetItemTooltip("Add new component");
  570. ImGui::SameLine();
  571. ImGui::SetNextItemWidth(-1.0f);
  572. I32 n = 0;
  573. if(ImGui::BeginCombo(" ", kSceneComponentTypeName[state.m_selectedSceneComponentType]))
  574. {
  575. for(const Char* name : kSceneComponentTypeName)
  576. {
  577. const Bool isSelected = (state.m_selectedSceneComponentType == n);
  578. if(ImGui::Selectable(name, isSelected))
  579. {
  580. state.m_selectedSceneComponentType = n;
  581. }
  582. // Set the initial focus when opening the combo (scrolling + keyboard navigation focus)
  583. if(isSelected)
  584. {
  585. ImGui::SetItemDefaultFocus();
  586. }
  587. ++n;
  588. }
  589. ImGui::EndCombo();
  590. }
  591. ImGui::Text(" ");
  592. }
  593. // Components
  594. {
  595. ANKI_PUSH_POP(StyleVar, ImGuiStyleVar_SeparatorTextAlign, Vec2(0.5f));
  596. U32 count = 0;
  597. node.iterateComponents([&](SceneComponent& comp) {
  598. ANKI_PUSH_POP(ID, comp.getUuid());
  599. const F32 alpha = 0.1f;
  600. ANKI_PUSH_POP(StyleColor, ImGuiCol_ChildBg, (count & 1) ? Vec4(0.0, 0.0f, 1.0f, alpha) : Vec4(1.0, 0.0f, 0.0f, alpha));
  601. if(ImGui::BeginChild("Child", Vec2(0.0f), ImGuiChildFlags_AutoResizeY))
  602. {
  603. ANKI_PUSH_POP_TEXT_INPUT_WIDTH(12);
  604. // Find the icon
  605. CString icon = ICON_MDI_TOY_BRICK;
  606. switch(comp.getType())
  607. {
  608. #define ANKI_DEFINE_SCENE_COMPONENT(name, weight, sceneNodeCanHaveMany, icon_, serializable) \
  609. case SceneComponentType::k##name: \
  610. icon = ANKI_CONCATENATE(ICON_MDI_, icon_); \
  611. break;
  612. #include <AnKi/Scene/Components/SceneComponentClasses.def.h>
  613. default:
  614. ANKI_ASSERT(0);
  615. }
  616. // Header
  617. {
  618. String label;
  619. label.sprintf(" %s %s (%u)", icon.cstr(), kSceneComponentTypeName[comp.getType()], comp.getUuid());
  620. ImGui::SeparatorText(label.cstr());
  621. if(ImGui::Button(ICON_MDI_MINUS_BOX))
  622. {
  623. ANKI_LOGW("TODO");
  624. }
  625. ImGui::SetItemTooltip("Delete Component");
  626. ImGui::SameLine();
  627. if(ImGui::Button(ICON_MDI_EYE))
  628. {
  629. ANKI_LOGW("TODO");
  630. }
  631. ImGui::SetItemTooltip("Disable component");
  632. }
  633. switch(comp.getType())
  634. {
  635. case SceneComponentType::kMove:
  636. case SceneComponentType::kUi:
  637. // Nothing
  638. break;
  639. case SceneComponentType::kScript:
  640. scriptComponent(static_cast<ScriptComponent&>(comp));
  641. break;
  642. case SceneComponentType::kMaterial:
  643. materialComponent(static_cast<MaterialComponent&>(comp));
  644. break;
  645. case SceneComponentType::kMesh:
  646. meshComponent(static_cast<MeshComponent&>(comp));
  647. break;
  648. case SceneComponentType::kSkin:
  649. skinComponent(static_cast<SkinComponent&>(comp));
  650. break;
  651. case SceneComponentType::kParticleEmitter2:
  652. particleEmitterComponent(static_cast<ParticleEmitter2Component&>(comp));
  653. break;
  654. case SceneComponentType::kLight:
  655. lightComponent(static_cast<LightComponent&>(comp));
  656. break;
  657. case SceneComponentType::kBody:
  658. bodyComponent(static_cast<BodyComponent&>(comp));
  659. break;
  660. case SceneComponentType::kJoint:
  661. jointComponent(static_cast<JointComponent&>(comp));
  662. break;
  663. case SceneComponentType::kDecal:
  664. decalComponent(static_cast<DecalComponent&>(comp));
  665. break;
  666. default:
  667. ImGui::Text("TODO");
  668. }
  669. }
  670. ImGui::EndChild();
  671. ImGui::Text(" ");
  672. ++count;
  673. });
  674. }
  675. }
  676. ImGui::End();
  677. }
  678. void EditorUi::scriptComponent(ScriptComponent& comp)
  679. {
  680. auto& state = m_sceneNodePropsWindow;
  681. // Play button
  682. {
  683. ImGui::SameLine();
  684. if(ImGui::Button(ICON_MDI_PLAY "##ScriptComponentResourceFilename"))
  685. {
  686. ANKI_LOGV("TODO");
  687. }
  688. ImGui::SetItemTooltip("Play script");
  689. }
  690. // Clear button
  691. {
  692. if(ImGui::Button(ICON_MDI_DELETE "##ScriptComponentResourceFilename"))
  693. {
  694. comp.setScriptResourceFilename("");
  695. }
  696. ImGui::SetItemTooltip("Clear");
  697. ImGui::SameLine();
  698. }
  699. // Filename input
  700. {
  701. const DynamicArray<CString> filenames = gatherResourceFilenames(".lua");
  702. const String currentFilename = (comp.hasScriptResource()) ? comp.getScriptResourceFilename() : "";
  703. U32 newSelectedFilename = kMaxU32;
  704. ImGui::SetNextItemWidth(-1.0f);
  705. comboWithFilter("##Filenames", filenames, currentFilename, newSelectedFilename, m_tempFilter);
  706. if(newSelectedFilename < filenames.getSize() && currentFilename != filenames[newSelectedFilename])
  707. {
  708. comp.setScriptResourceFilename(filenames[newSelectedFilename]);
  709. }
  710. }
  711. ImGui::Text(" -- or --");
  712. // Clear button
  713. {
  714. if(ImGui::Button(ICON_MDI_DELETE "##ScriptComponentScriptText"))
  715. {
  716. comp.setScriptText("");
  717. }
  718. ImGui::SetItemTooltip("Unset");
  719. ImGui::SameLine();
  720. }
  721. // Button
  722. {
  723. String buttonTxt;
  724. buttonTxt.sprintf(ICON_MDI_LANGUAGE_LUA " Embedded Script (%s)", comp.hasScriptText() ? "Set" : "Unset");
  725. const Bool showEditor = ImGui::Button(buttonTxt.cstr(), Vec2(-1.0f, 0.0f));
  726. if(showEditor && (state.m_scriptComponentThatHasTheTextEditorOpen == 0 || state.m_scriptComponentThatHasTheTextEditorOpen == comp.getUuid()))
  727. {
  728. state.m_textEditorOpen = true;
  729. state.m_scriptComponentThatHasTheTextEditorOpen = comp.getUuid();
  730. state.m_textEditorTxt =
  731. (comp.hasScriptText()) ? comp.getScriptText() : "function update(node, prevTime, crntTime)\n -- Your code here\nend";
  732. }
  733. }
  734. if(state.m_textEditorOpen && state.m_scriptComponentThatHasTheTextEditorOpen == comp.getUuid())
  735. {
  736. if(textEditorWindow(String().sprintf("ScriptComponent %u", comp.getUuid()), &state.m_textEditorOpen, state.m_textEditorTxt))
  737. {
  738. ANKI_LOGV("Updating ScriptComponent");
  739. comp.setScriptText(state.m_textEditorTxt);
  740. }
  741. if(!state.m_textEditorOpen)
  742. {
  743. state.m_scriptComponentThatHasTheTextEditorOpen = 0;
  744. state.m_textEditorTxt.destroy();
  745. }
  746. }
  747. }
  748. void EditorUi::materialComponent(MaterialComponent& comp)
  749. {
  750. if(!comp.isValid())
  751. {
  752. ImGui::SameLine();
  753. ImGui::TextUnformatted(ICON_MDI_ALERT);
  754. ImGui::SetItemTooltip("Component not valid");
  755. }
  756. // Filename
  757. {
  758. const DynamicArray<CString> mtlFilenames = gatherResourceFilenames(".ankimtl");
  759. const String currentFilename = (comp.hasMaterialResource()) ? comp.getMaterialFilename() : "";
  760. U32 newSelectedFilename = kMaxU32;
  761. ImGui::SetNextItemWidth(-1.0f);
  762. comboWithFilter("##Filenames", mtlFilenames, currentFilename, newSelectedFilename, m_tempFilter);
  763. if(newSelectedFilename < mtlFilenames.getSize() && currentFilename != mtlFilenames[newSelectedFilename])
  764. {
  765. comp.setMaterialFilename(mtlFilenames[newSelectedFilename]);
  766. }
  767. }
  768. // Submesh ID
  769. {
  770. I32 value = comp.getSubmeshIndex();
  771. if(ImGui::InputInt(ICON_MDI_VECTOR_POLYGON " Submesh ID", &value, 1, 1, 0))
  772. {
  773. comp.setSubmeshIndex(value);
  774. }
  775. }
  776. }
  777. void EditorUi::meshComponent(MeshComponent& comp)
  778. {
  779. if(!comp.isValid())
  780. {
  781. ImGui::SameLine();
  782. ImGui::TextUnformatted(ICON_MDI_ALERT);
  783. ImGui::SetItemTooltip("Component not valid");
  784. }
  785. // Filename
  786. {
  787. const DynamicArray<CString> meshFilenames = gatherResourceFilenames(".ankimesh");
  788. const String currentFilename = (comp.hasMeshResource()) ? comp.getMeshFilename() : "";
  789. U32 newSelectedFilename = kMaxU32;
  790. ImGui::SetNextItemWidth(-1.0f);
  791. comboWithFilter("##Filenames", meshFilenames, currentFilename, newSelectedFilename, m_tempFilter);
  792. if(newSelectedFilename < meshFilenames.getSize() && currentFilename != meshFilenames[newSelectedFilename])
  793. {
  794. comp.setMeshFilename(meshFilenames[newSelectedFilename]);
  795. }
  796. }
  797. }
  798. void EditorUi::skinComponent(SkinComponent& comp)
  799. {
  800. if(!comp.isValid())
  801. {
  802. ImGui::SameLine();
  803. ImGui::TextUnformatted(ICON_MDI_ALERT);
  804. ImGui::SetItemTooltip("Component not valid");
  805. }
  806. // Filename
  807. {
  808. const DynamicArray<CString> filenames = gatherResourceFilenames(".ankiskel");
  809. const String currentFilename = (comp.hasSkeletonResource()) ? comp.getSkeletonFilename() : "";
  810. U32 newSelectedFilename = kMaxU32;
  811. ImGui::SetNextItemWidth(-1.0f);
  812. comboWithFilter("##Filenames", filenames, currentFilename, newSelectedFilename, m_tempFilter);
  813. if(newSelectedFilename < filenames.getSize() && currentFilename != filenames[newSelectedFilename])
  814. {
  815. comp.setSkeletonFilename(filenames[newSelectedFilename]);
  816. }
  817. }
  818. }
  819. void EditorUi::particleEmitterComponent(ParticleEmitter2Component& comp)
  820. {
  821. // Filename
  822. {
  823. const DynamicArray<CString> filenames = gatherResourceFilenames(".ankiparts");
  824. const String currentFilename = (comp.hasParticleEmitterResource()) ? comp.getParticleEmitterFilename() : "";
  825. U32 newSelectedFilename = kMaxU32;
  826. ImGui::SetNextItemWidth(-1.0f);
  827. comboWithFilter("##Filenames", filenames, currentFilename, newSelectedFilename, m_tempFilter);
  828. if(newSelectedFilename < filenames.getSize() && currentFilename != filenames[newSelectedFilename])
  829. {
  830. comp.setParticleEmitterFilename(filenames[newSelectedFilename]);
  831. }
  832. }
  833. // Geometry type
  834. {
  835. dummyButton(0);
  836. if(ImGui::BeginCombo("Geometry Type", kParticleEmitterGeometryTypeName[comp.getParticleGeometryType()]))
  837. {
  838. for(ParticleGeometryType n : EnumIterable<ParticleGeometryType>())
  839. {
  840. const Bool isSelected = (comp.getParticleGeometryType() == n);
  841. if(ImGui::Selectable(kParticleEmitterGeometryTypeName[n], isSelected))
  842. {
  843. comp.setParticleGeometryType(n);
  844. }
  845. // Set the initial focus when opening the combo (scrolling + keyboard navigation focus)
  846. if(isSelected)
  847. {
  848. ImGui::SetItemDefaultFocus();
  849. }
  850. }
  851. ImGui::EndCombo();
  852. }
  853. }
  854. }
  855. void EditorUi::lightComponent(LightComponent& comp)
  856. {
  857. // Light type
  858. if(ImGui::BeginCombo("Type", kLightComponentTypeNames[comp.getLightComponentType()]))
  859. {
  860. for(LightComponentType type : EnumIterable<LightComponentType>())
  861. {
  862. const Bool selected = type == comp.getLightComponentType();
  863. if(ImGui::Selectable(kLightComponentTypeNames[type], selected))
  864. {
  865. comp.setLightComponentType(type);
  866. }
  867. // Set the initial focus when opening the combo (scrolling + keyboard navigation focus)
  868. if(selected)
  869. {
  870. ImGui::SetItemDefaultFocus();
  871. }
  872. }
  873. ImGui::EndCombo();
  874. }
  875. // Diffuse color
  876. Vec4 diffuseCol = comp.getDiffuseColor();
  877. if(ImGui::InputFloat4("Diffuse Color", &diffuseCol[0]))
  878. {
  879. comp.setDiffuseColor(diffuseCol.max(0.01f));
  880. }
  881. // Shadow
  882. Bool shadow = comp.getShadowEnabled();
  883. if(ImGui::Checkbox("Shadow", &shadow))
  884. {
  885. comp.setShadowEnabled(shadow);
  886. }
  887. if(comp.getLightComponentType() == LightComponentType::kPoint)
  888. {
  889. // Radius
  890. F32 radius = comp.getRadius();
  891. if(ImGui::SliderFloat("Radius", &radius, 0.01f, 100.0f))
  892. {
  893. comp.setRadius(max(radius, 0.01f));
  894. }
  895. }
  896. else if(comp.getLightComponentType() == LightComponentType::kSpot)
  897. {
  898. // Radius
  899. F32 distance = comp.getDistance();
  900. if(ImGui::SliderFloat("Distance", &distance, 0.01f, 100.0f))
  901. {
  902. comp.setDistance(max(distance, 0.01f));
  903. }
  904. // Inner & outter angles
  905. Vec2 angles(comp.getInnerAngle(), comp.getOuterAngle());
  906. angles[0] = toDegrees(angles[0]);
  907. angles[1] = toDegrees(angles[1]);
  908. if(ImGui::SliderFloat2("Inner & Outer Angles", &angles[0], 1.0f, 89.0f))
  909. {
  910. angles[0] = clamp(toRad(angles[0]), 1.0_degrees, 80.0_degrees);
  911. angles[1] = clamp(toRad(angles[1]), angles[0], 89.0_degrees);
  912. comp.setInnerAngle(angles[0]);
  913. comp.setOuterAngle(angles[1]);
  914. }
  915. }
  916. else
  917. {
  918. ANKI_ASSERT(comp.getLightComponentType() == LightComponentType::kDirectional);
  919. // Day of month
  920. I32 month, day;
  921. F32 hour;
  922. comp.getTimeOfDay(month, day, hour);
  923. Bool fieldChanged = false;
  924. if(ImGui::SliderInt("Month", &month, 0, 11))
  925. {
  926. fieldChanged = true;
  927. }
  928. if(ImGui::SliderInt("Day", &day, 0, 30))
  929. {
  930. fieldChanged = true;
  931. }
  932. if(ImGui::SliderFloat("Hour (0-24)", &hour, 0.0f, 24.0f))
  933. {
  934. fieldChanged = true;
  935. }
  936. if(fieldChanged)
  937. {
  938. comp.setDirectionFromTimeOfDay(month, day, hour);
  939. }
  940. }
  941. }
  942. void EditorUi::jointComponent(JointComponent& comp)
  943. {
  944. if(!comp.isValid())
  945. {
  946. ImGui::SameLine();
  947. ImGui::TextUnformatted(ICON_MDI_ALERT);
  948. ImGui::SetItemTooltip("Component not valid");
  949. }
  950. // Joint type
  951. if(ImGui::BeginCombo("Type", kJointComponentTypeName[comp.getJointType()]))
  952. {
  953. for(JointComponentyType type : EnumIterable<JointComponentyType>())
  954. {
  955. const Bool selected = type == comp.getJointType();
  956. if(ImGui::Selectable(kBodyComponentCollisionShapeTypeNames[type], selected))
  957. {
  958. comp.setJointType(type);
  959. }
  960. // Set the initial focus when opening the combo (scrolling + keyboard navigation focus)
  961. if(selected)
  962. {
  963. ImGui::SetItemDefaultFocus();
  964. }
  965. }
  966. ImGui::EndCombo();
  967. }
  968. }
  969. void EditorUi::bodyComponent(BodyComponent& comp)
  970. {
  971. if(!comp.isValid())
  972. {
  973. ImGui::SameLine();
  974. ImGui::TextUnformatted(ICON_MDI_ALERT);
  975. ImGui::SetItemTooltip("Component not valid");
  976. }
  977. // Shape type
  978. if(ImGui::BeginCombo("Type", kBodyComponentCollisionShapeTypeNames[comp.getCollisionShapeType()]))
  979. {
  980. for(BodyComponentCollisionShapeType type : EnumIterable<BodyComponentCollisionShapeType>())
  981. {
  982. const Bool selected = type == comp.getCollisionShapeType();
  983. if(ImGui::Selectable(kBodyComponentCollisionShapeTypeNames[type], selected))
  984. {
  985. comp.setCollisionShapeType(type);
  986. }
  987. // Set the initial focus when opening the combo (scrolling + keyboard navigation focus)
  988. if(selected)
  989. {
  990. ImGui::SetItemDefaultFocus();
  991. }
  992. }
  993. ImGui::EndCombo();
  994. }
  995. // Mass
  996. F32 mass = comp.getMass();
  997. if(ImGui::SliderFloat("Mass", &mass, 0.0f, 100.0f))
  998. {
  999. comp.setMass(mass);
  1000. }
  1001. if(comp.getCollisionShapeType() == BodyComponentCollisionShapeType::kAabb)
  1002. {
  1003. Vec3 extend = comp.getBoxExtend();
  1004. if(ImGui::SliderFloat3("Box Extend", &extend[0], 0.01f, 100.0f))
  1005. {
  1006. comp.setBoxExtend(extend);
  1007. }
  1008. }
  1009. else if(comp.getCollisionShapeType() == BodyComponentCollisionShapeType::kSphere)
  1010. {
  1011. F32 radius = comp.getSphereRadius();
  1012. if(ImGui::SliderFloat("Radius", &radius, 0.01f, 100.0f))
  1013. {
  1014. comp.setSphereRadius(radius);
  1015. }
  1016. }
  1017. }
  1018. void EditorUi::decalComponent(DecalComponent& comp)
  1019. {
  1020. if(!comp.isValid())
  1021. {
  1022. ImGui::SameLine();
  1023. ImGui::TextUnformatted(ICON_MDI_ALERT);
  1024. ImGui::SetItemTooltip("Component not valid");
  1025. }
  1026. ImGui::SeparatorText("Diffuse");
  1027. // Diffuse filename
  1028. {
  1029. const DynamicArray<CString> filenames = gatherResourceFilenames(".ankitex");
  1030. const String currentFilename = (comp.hasDiffuseImageResource()) ? comp.getDiffuseImageFilename() : "";
  1031. U32 newSelectedFilename = kMaxU32;
  1032. ImGui::SetNextItemWidth(-1.0f);
  1033. comboWithFilter("##Filenames", filenames, currentFilename, newSelectedFilename, m_tempFilter);
  1034. if(newSelectedFilename < filenames.getSize() && currentFilename != filenames[newSelectedFilename])
  1035. {
  1036. comp.setDiffuseImageFilename(filenames[newSelectedFilename]);
  1037. }
  1038. }
  1039. // Diffuse factor
  1040. ImGui::SetNextItemWidth(-1.0f);
  1041. F32 diffFactor = comp.getDiffuseBlendFactor();
  1042. if(ImGui::SliderFloat("##Factor0", &diffFactor, 0.0f, 1.0f))
  1043. {
  1044. comp.setDiffuseBlendFactor(diffFactor);
  1045. }
  1046. ImGui::SetItemTooltip("Blend Factor");
  1047. ImGui::SeparatorText("Roughness and Metallic");
  1048. // Roughness/metallic filename
  1049. {
  1050. const DynamicArray<CString> filenames = gatherResourceFilenames(".ankitex");
  1051. const String currentFilename = (comp.hasRoughnessMetalnessImageResource()) ? comp.getRoughnessMetalnessImageFilename() : "";
  1052. U32 newSelectedFilename = kMaxU32;
  1053. ImGui::SetNextItemWidth(-1.0f);
  1054. comboWithFilter("##Filenames2", filenames, currentFilename, newSelectedFilename, m_tempFilter);
  1055. if(newSelectedFilename < filenames.getSize() && currentFilename != filenames[newSelectedFilename])
  1056. {
  1057. comp.setRoughnessMetalnessImageFilename(filenames[newSelectedFilename]);
  1058. }
  1059. }
  1060. // Roughness/metallic factor
  1061. ImGui::SetNextItemWidth(-1.0f);
  1062. F32 rmFactor = comp.getRoughnessMetalnessBlendFactor();
  1063. if(ImGui::SliderFloat("##Factor1", &rmFactor, 0.0f, 1.0f))
  1064. {
  1065. comp.setRoughnessMetalnessBlendFactor(rmFactor);
  1066. }
  1067. ImGui::SetItemTooltip("Blend Factor");
  1068. }
  1069. void EditorUi::cVarsWindow()
  1070. {
  1071. if(!m_showCVarEditorWindow)
  1072. {
  1073. return;
  1074. }
  1075. F32 maxCvarTextSize = 0.0f;
  1076. CVarSet::getSingleton().iterateCVars([&](CVar& cvar) {
  1077. maxCvarTextSize = max(maxCvarTextSize, ImGui::CalcTextSize(cvar.getName().cstr()).x);
  1078. return FunctorContinue::kContinue;
  1079. });
  1080. if(ImGui::GetFrameCount() > 1)
  1081. {
  1082. // Viewport is one frame delay so do that when frame >1
  1083. const Vec2 initialSize = Vec2(900.0f, m_canvas->getSizef().y * 0.8f);
  1084. ImGui::SetNextWindowSize(initialSize, ImGuiCond_FirstUseEver);
  1085. ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Once, Vec2(0.5f));
  1086. }
  1087. if(ImGui::Begin("CVars Editor", &m_showCVarEditorWindow, 0))
  1088. {
  1089. filter(m_cvarsEditorWindow.m_filter);
  1090. if(ImGui::BeginChild("##Child", Vec2(0.0f), ImGuiChildFlags_Borders | ImGuiChildFlags_NavFlattened))
  1091. {
  1092. if(ImGui::BeginTable("Table", 2, 0))
  1093. {
  1094. ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, maxCvarTextSize + 20.0f);
  1095. ImGui::TableSetupColumn("Value");
  1096. I32 id = 0;
  1097. CVarSet::getSingleton().iterateCVars([&](CVar& cvar) {
  1098. if(!m_cvarsEditorWindow.m_filter.PassFilter(cvar.getName().cstr()))
  1099. {
  1100. return FunctorContinue::kContinue;
  1101. }
  1102. ImGui::TableNextRow();
  1103. ImGui::TableNextColumn();
  1104. ImGui::TextUnformatted(cvar.getName().cstr());
  1105. ImGui::TableNextColumn();
  1106. ImGui::PushID(id++);
  1107. ImGui::SetNextItemWidth(-1.0f);
  1108. if(cvar.getValueType() == CVarValueType::kBool)
  1109. {
  1110. BoolCVar& bcvar = static_cast<BoolCVar&>(cvar);
  1111. Bool val = bcvar;
  1112. ImGui::Checkbox("", &val);
  1113. bcvar = val;
  1114. }
  1115. else if(cvar.getValueType() == CVarValueType::kNumericF32)
  1116. {
  1117. NumericCVar<F32>& bcvar = static_cast<NumericCVar<F32>&>(cvar);
  1118. F32 val = bcvar;
  1119. ImGui::InputFloat("", &val);
  1120. bcvar = val;
  1121. }
  1122. else if(cvar.getValueType() == CVarValueType::kNumericF64)
  1123. {
  1124. NumericCVar<F64>& bcvar = static_cast<NumericCVar<F64>&>(cvar);
  1125. F64 val = bcvar;
  1126. ImGui::InputDouble("", &val);
  1127. bcvar = val;
  1128. }
  1129. else if(cvar.getValueType() == CVarValueType::kNumericU8)
  1130. {
  1131. NumericCVar<U8>& bcvar = static_cast<NumericCVar<U8>&>(cvar);
  1132. I32 val = bcvar;
  1133. ImGui::InputInt("", &val);
  1134. bcvar = U8(val);
  1135. }
  1136. else if(cvar.getValueType() == CVarValueType::kNumericU16)
  1137. {
  1138. NumericCVar<U16>& bcvar = static_cast<NumericCVar<U16>&>(cvar);
  1139. I32 val = bcvar;
  1140. ImGui::InputInt("", &val);
  1141. bcvar = U8(val);
  1142. }
  1143. else if(cvar.getValueType() == CVarValueType::kNumericU32)
  1144. {
  1145. NumericCVar<U32>& bcvar = static_cast<NumericCVar<U32>&>(cvar);
  1146. I32 val = bcvar;
  1147. ImGui::InputInt("", &val);
  1148. bcvar = val;
  1149. }
  1150. else if(cvar.getValueType() == CVarValueType::kNumericPtrSize)
  1151. {
  1152. NumericCVar<PtrSize>& bcvar = static_cast<NumericCVar<PtrSize>&>(cvar);
  1153. I32 val = I32(bcvar);
  1154. ImGui::InputInt("", &val);
  1155. bcvar = val;
  1156. }
  1157. else if(cvar.getValueType() == CVarValueType::kString)
  1158. {
  1159. StringCVar& bcvar = static_cast<StringCVar&>(cvar);
  1160. CString value = bcvar;
  1161. Char str[256];
  1162. std::strncpy(str, value.cstr(), sizeof(str));
  1163. ImGui::InputText("", str, sizeof(str));
  1164. bcvar = str;
  1165. }
  1166. else
  1167. {
  1168. ANKI_ASSERT(!"TODO");
  1169. }
  1170. ImGui::PopID();
  1171. return FunctorContinue::kContinue;
  1172. });
  1173. ImGui::EndTable();
  1174. }
  1175. }
  1176. ImGui::EndChild();
  1177. }
  1178. ImGui::End();
  1179. }
  1180. void EditorUi::debugRtsWindow()
  1181. {
  1182. if(!m_showDebugRtsWindow)
  1183. {
  1184. return;
  1185. }
  1186. if(ImGui::GetFrameCount() > 1)
  1187. {
  1188. // Viewport is one frame delay so do that when frame >1
  1189. const Vec2 initialSize = Vec2(450.0f, m_canvas->getSizef().y * 0.4f);
  1190. ImGui::SetNextWindowSize(initialSize, ImGuiCond_FirstUseEver);
  1191. ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Once, Vec2(0.5f));
  1192. }
  1193. if(ImGui::Begin("Debug Render Targets", &m_showDebugRtsWindow, 0))
  1194. {
  1195. const Bool refresh = ImGui::Checkbox("Disable tonemapping", &m_debugRtsWindow.m_disableTonemapping);
  1196. ImGui::TextUnformatted("");
  1197. ImGui::Separator();
  1198. if(ImGui::BeginChild("Content", Vec2(-1.0f, -1.0f)))
  1199. {
  1200. // Gather the names
  1201. DynamicArray<String> rtNames;
  1202. Renderer::getSingleton().iterateDebugRenderTargetNames([&](CString name) {
  1203. rtNames.emplaceBack(name);
  1204. return FunctorContinue::kContinue;
  1205. });
  1206. std::sort(rtNames.getBegin(), rtNames.getEnd());
  1207. for(const String& name : rtNames)
  1208. {
  1209. Bool isActive = (name == Renderer::getSingleton().getCurrentDebugRenderTarget());
  1210. if(ImGui::Checkbox(name.cstr(), &isActive) || (isActive && refresh))
  1211. {
  1212. Renderer::getSingleton().setCurrentDebugRenderTarget(isActive ? name : "", m_debugRtsWindow.m_disableTonemapping);
  1213. }
  1214. }
  1215. }
  1216. ImGui::EndChild();
  1217. }
  1218. ImGui::End();
  1219. }
  1220. void EditorUi::consoleWindow()
  1221. {
  1222. if(!m_showConsoleWindow)
  1223. {
  1224. return;
  1225. }
  1226. auto& state = m_consoleWindow;
  1227. if(ImGui::GetFrameCount() > 1)
  1228. {
  1229. // Viewport is one frame delay so do that when frame >1
  1230. const Vec2 viewportSize = ImGui::GetMainViewport()->WorkSize;
  1231. const Vec2 viewportPos = ImGui::GetMainViewport()->WorkPos;
  1232. const Vec2 initialSize = Vec2(viewportSize.x / 2.0f, kConsoleHeight);
  1233. const Vec2 initialPos = Vec2(0.0f, viewportPos.y + viewportSize.y - initialSize.y);
  1234. ImGui::SetNextWindowSize(initialSize, ImGuiCond_FirstUseEver);
  1235. ImGui::SetNextWindowPos(initialPos, ImGuiCond_FirstUseEver);
  1236. }
  1237. if(ImGui::Begin("Console", &m_showConsoleWindow, ImGuiWindowFlags_NoCollapse))
  1238. {
  1239. // Lua input
  1240. {
  1241. Char consoleTxt[kMaxTextInputLen] = "";
  1242. ImGui::SetNextItemWidth(-1.0f);
  1243. if(ImGui::InputTextWithHint(" ", ICON_MDI_LANGUAGE_LUA " LUA console", consoleTxt, sizeof(consoleTxt),
  1244. ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_EscapeClearsAll))
  1245. {
  1246. [[maybe_unused]] const Error err = ScriptManager::getSingleton().evalString(consoleTxt);
  1247. ImGui::SetKeyboardFocusHere(-1); // On enter it loses focus so call this to fix it
  1248. }
  1249. }
  1250. // Clear Log
  1251. {
  1252. if(ImGui::Button(ICON_MDI_DELETE))
  1253. {
  1254. state.m_log.destroy();
  1255. }
  1256. ImGui::SetItemTooltip("Clear log");
  1257. ImGui::SameLine();
  1258. }
  1259. // Search log
  1260. filter(state.m_logFilter);
  1261. // Log
  1262. {
  1263. if(ImGui::BeginChild("Log", Vec2(0.0f), ImGuiChildFlags_Borders | ImGuiChildFlags_NavFlattened))
  1264. {
  1265. ImGui::PushFont(m_monospaceFont, 0.0f);
  1266. if(ImGui::BeginTable("##bg", 1, ImGuiTableFlags_RowBg))
  1267. {
  1268. LockGuard lock(state.m_logMtx);
  1269. for(const auto& logEntry : state.m_log)
  1270. {
  1271. if(state.m_logFilter.PassFilter(logEntry.second.cstr()))
  1272. {
  1273. ImGui::TableNextRow();
  1274. ImGui::TableNextColumn();
  1275. constexpr Array<Vec3, U(LoggerMessageType::kCount)> colors = {Vec3(0.074f, 0.631f, 0.054f), Vec3(0.074f, 0.354f, 0.631f),
  1276. Vec3(1.0f, 0.0f, 0.0f), Vec3(0.756f, 0.611f, 0.0f),
  1277. Vec3(1.0f, 0.0f, 0.0f)};
  1278. ImGui::PushStyleColor(ImGuiCol_Text, Vec4(colors[logEntry.first].xyz1));
  1279. ImGui::TextUnformatted(logEntry.second.cstr());
  1280. ImGui::PopStyleColor();
  1281. }
  1282. }
  1283. if(state.m_forceLogScrollDown)
  1284. {
  1285. ImGui::SetScrollHereY(1.0f);
  1286. state.m_forceLogScrollDown = false;
  1287. }
  1288. ImGui::EndTable();
  1289. }
  1290. ImGui::PopFont();
  1291. }
  1292. ImGui::EndChild();
  1293. }
  1294. }
  1295. ImGui::End();
  1296. }
  1297. void EditorUi::dirTree(const AssetPath& path)
  1298. {
  1299. auto& state = m_assetsWindow;
  1300. ImGui::TableNextRow();
  1301. ImGui::TableNextColumn();
  1302. ImGuiTreeNodeFlags treeFlags = ImGuiTreeNodeFlags_None;
  1303. treeFlags |= ImGuiTreeNodeFlags_OpenOnArrow
  1304. | ImGuiTreeNodeFlags_OpenOnDoubleClick; // Standard opening mode as we are likely to want to add selection afterwards
  1305. treeFlags |= ImGuiTreeNodeFlags_NavLeftJumpsToParent; // Left arrow support
  1306. treeFlags |= ImGuiTreeNodeFlags_SpanFullWidth; // Span full width for easier mouse reach
  1307. treeFlags |= ImGuiTreeNodeFlags_DrawLinesToNodes; // Always draw hierarchy outlines
  1308. if(state.m_pathSelected == &path)
  1309. {
  1310. treeFlags |= ImGuiTreeNodeFlags_Selected;
  1311. }
  1312. const Bool hasChildren = path.m_children.getSize();
  1313. if(!hasChildren)
  1314. {
  1315. treeFlags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_Bullet;
  1316. }
  1317. ImGui::PushID(path.m_id);
  1318. const Bool nodeOpen = ImGui::TreeNodeEx("", treeFlags, "%s", path.m_dirname.cstr());
  1319. ImGui::PopID();
  1320. ImGui::SetItemTooltip("%s", path.m_dirname.cstr());
  1321. if(ImGui::IsItemFocused())
  1322. {
  1323. state.m_pathSelected = &path;
  1324. }
  1325. if(nodeOpen)
  1326. {
  1327. for(const AssetPath& p : path.m_children)
  1328. {
  1329. dirTree(p);
  1330. }
  1331. ImGui::TreePop();
  1332. }
  1333. }
  1334. void EditorUi::assetsWindow()
  1335. {
  1336. if(!m_showAssetsWindow)
  1337. {
  1338. return;
  1339. }
  1340. auto& state = m_assetsWindow;
  1341. if(ImGui::GetFrameCount() > 1)
  1342. {
  1343. // Viewport is one frame delay so do that when frame >1
  1344. const Vec2 viewportSize = ImGui::GetMainViewport()->WorkSize;
  1345. const Vec2 viewportPos = ImGui::GetMainViewport()->WorkPos;
  1346. const Vec2 initialSize = Vec2(viewportSize.x / 2.0f, kConsoleHeight);
  1347. const Vec2 initialPos = Vec2(viewportSize.x / 2.0f, viewportPos.y + viewportSize.y - initialSize.y);
  1348. ImGui::SetNextWindowSize(initialSize, ImGuiCond_FirstUseEver);
  1349. ImGui::SetNextWindowPos(initialPos, ImGuiCond_FirstUseEver);
  1350. }
  1351. if(ImGui::Begin("Assets", &m_showAssetsWindow, ImGuiWindowFlags_NoCollapse))
  1352. {
  1353. // Left side
  1354. {
  1355. if(ImGui::BeginChild("Left", Vec2(300.0f, -1.0f), ImGuiChildFlags_ResizeX | ImGuiChildFlags_Borders | ImGuiChildFlags_NavFlattened))
  1356. {
  1357. if(ImGui::BeginTable("##bg", 1, ImGuiTableFlags_RowBg))
  1358. {
  1359. for(const AssetPath& p : state.m_assetPaths)
  1360. {
  1361. dirTree(p);
  1362. }
  1363. ImGui::EndTable();
  1364. }
  1365. }
  1366. ImGui::EndChild();
  1367. } // Left side
  1368. ImGui::SameLine();
  1369. // Right side
  1370. {
  1371. // Use the filter to gather the files
  1372. DynamicArray<const AssetFile*> filteredFiles;
  1373. if(state.m_pathSelected)
  1374. {
  1375. for(const AssetFile& f : state.m_pathSelected->m_files)
  1376. {
  1377. if(state.m_fileFilter.PassFilter(f.m_basename.cstr()))
  1378. {
  1379. filteredFiles.emplaceBack(&f);
  1380. }
  1381. }
  1382. }
  1383. if(ImGui::BeginChild("Right", Vec2(-1.0f, -1.0f), 0))
  1384. {
  1385. // Increase/decrease icon size
  1386. {
  1387. ImGui::TextUnformatted("Icon Size");
  1388. ImGui::SameLine();
  1389. ImGui::SetNextItemWidth(64.0f);
  1390. ImGui::SliderInt("##Icon Size", &state.m_cellSize, 5, 11, "%d", ImGuiSliderFlags_AlwaysClamp);
  1391. ImGui::SameLine();
  1392. }
  1393. filter(state.m_fileFilter);
  1394. if(ImGui::BeginChild("RightBottom", Vec2(-1.0f, -1.0f), 0))
  1395. {
  1396. const F32 cellWidth = F32(state.m_cellSize) * 16;
  1397. const U32 columnCount = U32(ImGui::GetContentRegionAvail().x / cellWidth);
  1398. ImGui::SetNextItemWidth(-1.0f);
  1399. const ImGuiTableFlags flags = ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersOuter
  1400. | ImGuiTableFlags_BordersV | ImGuiTableFlags_ContextMenuInBody;
  1401. if(filteredFiles.getSize() && ImGui::BeginTable("Grid", columnCount, flags))
  1402. {
  1403. const U32 rowCount = (filteredFiles.getSize() + columnCount - 1) / columnCount;
  1404. for(U32 row = 0; row < rowCount; ++row)
  1405. {
  1406. ImGui::TableNextRow();
  1407. for(U32 column = 0; column < columnCount; ++column)
  1408. {
  1409. ImGui::TableNextColumn();
  1410. const U32 idx = row * columnCount + column;
  1411. if(idx < filteredFiles.getSize())
  1412. {
  1413. const AssetFile& file = *filteredFiles[idx];
  1414. ImGui::PushID(idx);
  1415. if(file.m_type == AssetFileType::kMaterial)
  1416. {
  1417. ImTextureID id;
  1418. id.m_texture = &m_materialIcon->getTexture();
  1419. ImGui::ImageButton("##", id, Vec2(cellWidth));
  1420. }
  1421. else if(file.m_type == AssetFileType::kMesh)
  1422. {
  1423. ImTextureID id;
  1424. id.m_texture = &m_meshIcon->getTexture();
  1425. ImGui::ImageButton("##", id, Vec2(cellWidth));
  1426. }
  1427. else if(file.m_type == AssetFileType::kTexture)
  1428. {
  1429. ImageResourcePtr img;
  1430. loadImageToCache(file.m_filename, img);
  1431. ImTextureID id;
  1432. id.m_texture = &img->getTexture();
  1433. id.m_textureSubresource = TextureSubresourceDesc::all();
  1434. if(ImGui::ImageButton("##", id, Vec2(cellWidth)))
  1435. {
  1436. m_imageViewer.m_image = img;
  1437. m_imageViewer.m_open = true;
  1438. }
  1439. }
  1440. else if(file.m_type == AssetFileType::kParticleEmitter)
  1441. {
  1442. ImGui::PushFont(nullptr, cellWidth - 1.0f);
  1443. if(ImGui::Button(ICON_MDI_CREATION, Vec2(cellWidth)))
  1444. {
  1445. ParticleEmitterResource2Ptr rsrc;
  1446. ANKI_CHECKF(ResourceManager::getSingleton().loadResource(file.m_filename, rsrc));
  1447. m_particlesEditor.open(*rsrc);
  1448. }
  1449. ImGui::PopFont();
  1450. }
  1451. ImGui::PopID();
  1452. ImGui::TextWrapped("%s", file.m_basename.cstr());
  1453. ImGui::SetItemTooltip("%s", file.m_filename.cstr());
  1454. }
  1455. }
  1456. }
  1457. ImGui::EndTable();
  1458. }
  1459. else
  1460. {
  1461. ImGui::TextUnformatted("Empty");
  1462. }
  1463. }
  1464. ImGui::EndChild();
  1465. }
  1466. ImGui::EndChild();
  1467. } // Right side
  1468. }
  1469. ImGui::End();
  1470. }
  1471. void EditorUi::filter(ImGuiTextFilter& filter)
  1472. {
  1473. ImGui::SetNextItemWidth(-FLT_MIN);
  1474. ImGui::SetNextItemShortcut(ImGuiMod_Ctrl | ImGuiKey_F, ImGuiInputFlags_Tooltip);
  1475. ImGui::PushItemFlag(ImGuiItemFlags_NoNavDefaultFocus, true);
  1476. if(ImGui::InputTextWithHint("##Filter", ICON_MDI_MAGNIFY " Search incl,-excl", filter.InputBuf, IM_ARRAYSIZE(filter.InputBuf),
  1477. ImGuiInputTextFlags_EscapeClearsAll))
  1478. {
  1479. filter.Build();
  1480. }
  1481. ImGui::PopItemFlag();
  1482. }
  1483. void EditorUi::dummyButton(I32 id)
  1484. {
  1485. ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.0f);
  1486. ImGui::PushID(id);
  1487. ImGui::Button(ICON_MDI_LOCK);
  1488. ImGui::PopID();
  1489. ImGui::PopStyleVar();
  1490. ImGui::SameLine();
  1491. };
  1492. template<typename TItemArray>
  1493. void EditorUi::comboWithFilter(CString text, const TItemArray& items, CString selectedItemIn, U32& selectedItemOut, ImGuiTextFilter& filter)
  1494. {
  1495. if(ImGui::BeginCombo(text.cstr(), selectedItemIn.cstr()))
  1496. {
  1497. if(ImGui::IsWindowAppearing())
  1498. {
  1499. ImGui::SetKeyboardFocusHere();
  1500. filter.Clear();
  1501. }
  1502. ImGui::SetNextItemWidth(-1.0f);
  1503. if(ImGui::InputTextWithHint("##Filter", ICON_MDI_MAGNIFY " Search incl,-excl", filter.InputBuf, IM_ARRAYSIZE(filter.InputBuf),
  1504. ImGuiInputTextFlags_EscapeClearsAll))
  1505. {
  1506. filter.Build();
  1507. }
  1508. for(U32 i = 0; i < items.getSize(); ++i)
  1509. {
  1510. CString item = items[i];
  1511. if(!filter.PassFilter(item.cstr()))
  1512. {
  1513. continue;
  1514. }
  1515. const Bool isSelected = (selectedItemIn == item);
  1516. if(ImGui::Selectable(item.cstr(), isSelected))
  1517. {
  1518. selectedItemOut = i;
  1519. }
  1520. // Set the initial focus when opening the combo (scrolling + keyboard navigation focus)
  1521. if(isSelected)
  1522. {
  1523. ImGui::SetItemDefaultFocus();
  1524. }
  1525. }
  1526. ImGui::EndCombo();
  1527. }
  1528. }
  1529. Bool EditorUi::textEditorWindow(CString extraWindowTitle, Bool* pOpen, String& inout) const
  1530. {
  1531. Bool save = false;
  1532. const Vec2 windowSize(600.0f, 800.0f);
  1533. ImGui::SetNextWindowSize(windowSize, ImGuiCond_FirstUseEver);
  1534. ImGui::SetNextWindowPos(Vec2(ImGui::GetMainViewport()->WorkSize) / 2.0f - windowSize / 2.0f, ImGuiCond_FirstUseEver);
  1535. const String title = String().sprintf("Text Editor: %s", extraWindowTitle.cstr());
  1536. if(ImGui::Begin(title.cstr(), pOpen))
  1537. {
  1538. if(ImGui::Button(ICON_MDI_CONTENT_SAVE " Save"))
  1539. {
  1540. save = true;
  1541. }
  1542. ImGui::SameLine();
  1543. if(pOpen && ImGui::Button(ICON_MDI_CLOSE " Close"))
  1544. {
  1545. *pOpen = false;
  1546. }
  1547. if(ImGui::IsWindowFocused() && Input::getSingleton().getKey(KeyCode::kEscape) && pOpen)
  1548. {
  1549. *pOpen = false;
  1550. }
  1551. DynamicArray<Char> buffer;
  1552. buffer.resize(max(inout.getLength() + 1, 1024_U32), '\0');
  1553. if(inout.getLength())
  1554. {
  1555. std::strncpy(buffer.getBegin(), inout.cstr(), inout.getLength());
  1556. }
  1557. auto replaceTabCallback = [](ImGuiInputTextCallbackData* data) -> int {
  1558. if(data->CursorPos > 0 && data->Buf[data->CursorPos - 1] == '\t')
  1559. {
  1560. data->DeleteChars(data->CursorPos - 1, 1);
  1561. data->InsertChars(data->CursorPos, " ");
  1562. return 0;
  1563. }
  1564. else
  1565. {
  1566. return 0;
  1567. }
  1568. };
  1569. ImGui::PushFont(m_monospaceFont, 0.0f);
  1570. if(ImGui::InputTextMultiline("##", buffer.getBegin(), buffer.getSize(), Vec2(-1.0f),
  1571. ImGuiInputTextFlags_AllowTabInput | ImGuiInputTextFlags_CallbackEdit, replaceTabCallback))
  1572. {
  1573. inout = buffer.getBegin();
  1574. }
  1575. ImGui::PopFont();
  1576. }
  1577. ImGui::End();
  1578. return save;
  1579. }
  1580. void EditorUi::loadImageToCache(CString fname, ImageResourcePtr& img)
  1581. {
  1582. // Try to load first
  1583. ANKI_CHECKF(ResourceManager::getSingleton().loadResource(fname, img));
  1584. // Update the cache
  1585. const U32 crntFrame = ImGui::GetFrameCount();
  1586. Bool entryFound = false;
  1587. DynamicArray<ImageCacheEntry>& cache = m_assetsWindow.m_imageCache;
  1588. for(ImageCacheEntry& entry : cache)
  1589. {
  1590. if(entry.m_image->getUuid() == img->getUuid())
  1591. {
  1592. entry.m_lastSeenInFrame = crntFrame;
  1593. entryFound = true;
  1594. break;
  1595. }
  1596. }
  1597. if(!entryFound)
  1598. {
  1599. cache.emplaceBack(ImageCacheEntry{img, crntFrame});
  1600. }
  1601. // Try to remove stale entries
  1602. const U32 frameInactivityCount = 60 * 30; // ~30"
  1603. while(true)
  1604. {
  1605. Bool foundStaleEntry = false;
  1606. for(auto it = cache.getBegin(); it != cache.getEnd(); ++it)
  1607. {
  1608. ANKI_ASSERT(crntFrame >= it->m_lastSeenInFrame);
  1609. if(crntFrame - it->m_lastSeenInFrame > frameInactivityCount)
  1610. {
  1611. cache.erase(it);
  1612. foundStaleEntry = true;
  1613. break;
  1614. }
  1615. }
  1616. if(!foundStaleEntry)
  1617. {
  1618. break;
  1619. }
  1620. }
  1621. }
  1622. void EditorUi::objectPicking()
  1623. {
  1624. if(!m_mouseOverAnyWindow && Input::getSingleton().getMouseButton(MouseButton::kLeft) == 1)
  1625. {
  1626. const DbgObjectPickingResult& res = Renderer::getSingleton().getDbg().getObjectPickingResultAtMousePosition();
  1627. if(res.m_sceneNodeUuid != 0)
  1628. {
  1629. SceneGraph::getSingleton().visitNodes([this, uuid = res.m_sceneNodeUuid](SceneNode& node) {
  1630. if(node.getUuid() == uuid)
  1631. {
  1632. m_sceneHierarchyWindow.m_selectedNode = &node;
  1633. ANKI_LOGV("Selecting scene node: %s", node.getName().cstr());
  1634. return FunctorContinue::kStop;
  1635. }
  1636. return FunctorContinue::kContinue;
  1637. });
  1638. m_objectPicking.m_translationAxisSelected = kMaxU32;
  1639. m_objectPicking.m_scaleAxisSelected = kMaxU32;
  1640. m_objectPicking.m_rotationAxisSelected = kMaxU32;
  1641. }
  1642. else if(res.isValid())
  1643. {
  1644. // Clicked a gizmo
  1645. const Transform& nodeTrf = m_sceneHierarchyWindow.m_selectedNode->getLocalTransform();
  1646. const Vec3 nodeOrigin = nodeTrf.getOrigin().xyz;
  1647. Array<Vec3, 3> rotationAxis;
  1648. rotationAxis[0] = nodeTrf.getRotation().getXAxis();
  1649. rotationAxis[1] = nodeTrf.getRotation().getYAxis();
  1650. rotationAxis[2] = nodeTrf.getRotation().getZAxis();
  1651. if(res.m_translationAxis < 3)
  1652. {
  1653. m_objectPicking.m_translationAxisSelected = res.m_translationAxis;
  1654. m_objectPicking.m_scaleAxisSelected = kMaxU32;
  1655. m_objectPicking.m_rotationAxisSelected = kMaxU32;
  1656. }
  1657. else if(res.m_scaleAxis < 3)
  1658. {
  1659. m_objectPicking.m_translationAxisSelected = kMaxU32;
  1660. m_objectPicking.m_scaleAxisSelected = res.m_scaleAxis;
  1661. m_objectPicking.m_rotationAxisSelected = kMaxU32;
  1662. }
  1663. else if(res.m_rotationAxis < 3)
  1664. {
  1665. m_objectPicking.m_translationAxisSelected = kMaxU32;
  1666. m_objectPicking.m_scaleAxisSelected = kMaxU32;
  1667. m_objectPicking.m_rotationAxisSelected = res.m_rotationAxis;
  1668. // Calc the pivot point
  1669. const Transform camTrf =
  1670. SceneGraph::getSingleton().getActiveCameraNode().getFirstComponentOfType<CameraComponent>().getFrustum().getWorldTransform();
  1671. Plane axisPlane;
  1672. axisPlane.setFromRay(nodeOrigin, rotationAxis[res.m_rotationAxis]);
  1673. Bool collides = projectNdcToPlane(Input::getSingleton().getMousePositionNdc(), axisPlane, m_objectPicking.m_pivotPoint);
  1674. if(!collides)
  1675. {
  1676. // Clicked the gizmo from the back side, use the negative plane
  1677. axisPlane.setFromRay(nodeOrigin, -rotationAxis[res.m_rotationAxis]);
  1678. collides = projectNdcToPlane(Input::getSingleton().getMousePositionNdc(), axisPlane, m_objectPicking.m_pivotPoint);
  1679. if(!collides)
  1680. {
  1681. ANKI_LOGW("Can't determin the pivot point");
  1682. m_objectPicking.m_pivotPoint = nodeOrigin;
  1683. }
  1684. }
  1685. }
  1686. if(res.m_translationAxis < 3 || res.m_scaleAxis < 3)
  1687. {
  1688. const U32 axis = (res.m_translationAxis < 3) ? res.m_translationAxis : res.m_scaleAxis;
  1689. m_objectPicking.m_pivotPoint =
  1690. nodeOrigin + rotationAxis[axis] * projectNdcToRay(Input::getSingleton().getMousePositionNdc(), nodeOrigin, rotationAxis[axis]);
  1691. }
  1692. }
  1693. else
  1694. {
  1695. // Clicked on sky
  1696. m_sceneHierarchyWindow.m_selectedNode = nullptr;
  1697. m_objectPicking.m_translationAxisSelected = kMaxU32;
  1698. m_objectPicking.m_scaleAxisSelected = kMaxU32;
  1699. m_objectPicking.m_rotationAxisSelected = kMaxU32;
  1700. }
  1701. }
  1702. if(!m_mouseOverAnyWindow && Input::getSingleton().getMouseButton(MouseButton::kLeft) > 1 && m_sceneHierarchyWindow.m_selectedNode)
  1703. {
  1704. // Possibly dragging
  1705. const Transform& nodeTrf = m_sceneHierarchyWindow.m_selectedNode->getLocalTransform();
  1706. Array<Vec3, 3> rotationAxis;
  1707. rotationAxis[0] = nodeTrf.getRotation().getXAxis();
  1708. rotationAxis[1] = nodeTrf.getRotation().getYAxis();
  1709. rotationAxis[2] = nodeTrf.getRotation().getZAxis();
  1710. if(m_objectPicking.m_translationAxisSelected < 3)
  1711. {
  1712. const U32 axis = m_objectPicking.m_translationAxisSelected;
  1713. const F32 moveDistance = projectNdcToRay(Input::getSingleton().getMousePositionNdc(), m_objectPicking.m_pivotPoint, rotationAxis[axis]);
  1714. const Vec3 oldPosition = nodeTrf.getOrigin().xyz;
  1715. Vec3 newPosition = oldPosition + rotationAxis[axis] * moveDistance;
  1716. // Snap position
  1717. for(U32 i = 0; i < 3 && m_toolbox.m_scaleTranslationSnapping > kEpsilonf; ++i)
  1718. {
  1719. newPosition[i] = round(newPosition[i] / m_toolbox.m_scaleTranslationSnapping) * m_toolbox.m_scaleTranslationSnapping;
  1720. }
  1721. m_sceneHierarchyWindow.m_selectedNode->setLocalOrigin(newPosition);
  1722. // Update the pivot
  1723. const Vec3 moveDiff = newPosition - oldPosition; // Move the pivot as you moved the node origin
  1724. m_objectPicking.m_pivotPoint = m_objectPicking.m_pivotPoint + moveDiff;
  1725. }
  1726. else if(m_objectPicking.m_scaleAxisSelected < 3)
  1727. {
  1728. const U32 axis = m_objectPicking.m_scaleAxisSelected;
  1729. const F32 moveDistance = projectNdcToRay(Input::getSingleton().getMousePositionNdc(), m_objectPicking.m_pivotPoint, rotationAxis[axis]);
  1730. const F32 oldAxisScale = nodeTrf.getScale()[axis];
  1731. F32 newAxisScale = oldAxisScale + moveDistance;
  1732. // Snap scale
  1733. if(m_toolbox.m_scaleTranslationSnapping > kEpsilonf)
  1734. {
  1735. newAxisScale = round(newAxisScale / m_toolbox.m_scaleTranslationSnapping) * m_toolbox.m_scaleTranslationSnapping;
  1736. }
  1737. Vec3 scale = nodeTrf.getScale().xyz;
  1738. scale[axis] = max(newAxisScale, m_toolbox.m_scaleTranslationSnapping);
  1739. m_sceneHierarchyWindow.m_selectedNode->setLocalScale(scale);
  1740. // Update the pivot
  1741. const F32 adjustedMoveDistance = newAxisScale - oldAxisScale;
  1742. m_objectPicking.m_pivotPoint = m_objectPicking.m_pivotPoint + rotationAxis[axis] * adjustedMoveDistance;
  1743. }
  1744. else if(m_objectPicking.m_rotationAxisSelected < 3)
  1745. {
  1746. const U32 axis = m_objectPicking.m_rotationAxisSelected;
  1747. const Vec3 nodeOrigin = nodeTrf.getOrigin().xyz;
  1748. // Compute the new pivot point
  1749. Plane axisPlane;
  1750. axisPlane.setFromRay(nodeOrigin, rotationAxis[axis]);
  1751. Vec3 newPivotPoint;
  1752. Bool collides = projectNdcToPlane(Input::getSingleton().getMousePositionNdc(), axisPlane, newPivotPoint);
  1753. if(!collides)
  1754. {
  1755. // Clicked the gizmo from the back side, use the negative plane
  1756. axisPlane.setFromRay(nodeOrigin, -rotationAxis[axis]);
  1757. collides = projectNdcToPlane(Input::getSingleton().getMousePositionNdc(), axisPlane, newPivotPoint);
  1758. if(!collides)
  1759. {
  1760. ANKI_LOGW("Can't determin the pivot point");
  1761. newPivotPoint = nodeOrigin;
  1762. }
  1763. }
  1764. // Compute the angle between the points
  1765. const Vec3 dir0 = (newPivotPoint - nodeOrigin).normalize();
  1766. const Vec3 dir1 = (m_objectPicking.m_pivotPoint - nodeOrigin).normalize();
  1767. F32 angle = acos(saturate(dir1.dot(dir0)));
  1768. if(m_toolbox.m_rotationSnappingDeg > kEpsilonf)
  1769. {
  1770. const F32 rad = toRad(m_toolbox.m_rotationSnappingDeg);
  1771. angle = round(angle / rad) * rad;
  1772. }
  1773. Vec3 cross = dir1.cross(dir0);
  1774. if(cross != Vec3(0.0f))
  1775. {
  1776. cross = cross.normalize();
  1777. const F32 angleSign = (cross.dot(rotationAxis[axis]) < 1.0f) ? -1.0f : 1.0f;
  1778. angle *= angleSign;
  1779. // Apply the angle
  1780. if(m_objectPicking.m_rotationAxisSelected == 0)
  1781. {
  1782. m_sceneHierarchyWindow.m_selectedNode->rotateLocalX(angle);
  1783. }
  1784. else if(m_objectPicking.m_rotationAxisSelected == 1)
  1785. {
  1786. m_sceneHierarchyWindow.m_selectedNode->rotateLocalY(angle);
  1787. }
  1788. else
  1789. {
  1790. m_sceneHierarchyWindow.m_selectedNode->rotateLocalZ(angle);
  1791. }
  1792. // Use the snapped angle to adjust the pivot point
  1793. Mat3 rot;
  1794. rot.setZAxis(rotationAxis[axis]);
  1795. rot.setXAxis(dir1);
  1796. rot.setYAxis(rotationAxis[axis].cross(dir1).normalize());
  1797. rot.rotateZAxis(angle);
  1798. newPivotPoint = nodeOrigin + rot.getXAxis() * (newPivotPoint - nodeOrigin).length();
  1799. m_objectPicking.m_pivotPoint = newPivotPoint;
  1800. }
  1801. }
  1802. }
  1803. if(m_sceneHierarchyWindow.m_selectedNode)
  1804. {
  1805. Renderer::getSingleton().getDbg().enableGizmos(m_sceneHierarchyWindow.m_selectedNode->getWorldTransform(), true);
  1806. }
  1807. else
  1808. {
  1809. Renderer::getSingleton().getDbg().enableGizmos(Transform::getIdentity(), false);
  1810. }
  1811. }
  1812. DynamicArray<CString> EditorUi::gatherResourceFilenames(CString filenameContains)
  1813. {
  1814. DynamicArray<CString> out;
  1815. ResourceFilesystem::getSingleton().iterateAllFilenames([&](CString fname) {
  1816. if(fname.find(filenameContains) != CString::kNpos)
  1817. {
  1818. out.emplaceBack(fname);
  1819. }
  1820. return FunctorContinue::kContinue;
  1821. });
  1822. return out;
  1823. }
  1824. } // end namespace anki