From 25b7cd5667fcb7aa4a9cd21556bc479084578bb2 Mon Sep 17 00:00:00 2001 From: myT <> Date: Sat, 28 Dec 2024 20:01:17 +0100 Subject: [PATCH] improved image/shader detail/explorer and shader editor windows - better image scaling - more shader and image filters - shader code saving - collapsible image/shader reference lists --- code/renderer/tr_gui.cpp | 456 ++++++++++++++++++++++++++++++--------- 1 file changed, 356 insertions(+), 100 deletions(-) diff --git a/code/renderer/tr_gui.cpp b/code/renderer/tr_gui.cpp index b0487d2..e69d32b 100644 --- a/code/renderer/tr_gui.cpp +++ b/code/renderer/tr_gui.cpp @@ -46,7 +46,9 @@ struct ImageWindow struct ShaderWindow { char formattedCode[SHADER_CODE_BUFFER_SIZE]; + const image_t* displayImages[MAX_IMAGE_ANIMATIONS * MAX_SHADER_STAGES + 6]; // +6 for sky box shader_t* shader; + int displayImageCount; bool active; }; @@ -76,11 +78,13 @@ struct ImageReplacements struct ShaderEditWindow { + char fileContent[SHADER_CODE_BUFFER_SIZE + MAX_QPATH * 2]; // code + header line char code[SHADER_CODE_BUFFER_SIZE]; char originalCode[SHADER_CODE_BUFFER_SIZE]; char chars[SHADER_CODE_BUFFER_SIZE]; char currentLine[1024]; char toolTip[1024]; + char shaderFilePath[MAX_QPATH]; vec2_t cursorCoords; shader_t originalShader; shader_t* shader; @@ -201,6 +205,11 @@ static bool IsNonEmpty(const char* string) return string != NULL && string[0] != '\0'; } +static void Checkbox(const char* label, bool value) +{ + ImGui::Checkbox(label, &value); +} + static void SetTooltipIfValid(const char* text) { if(IsNonEmpty(text) && ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal)) @@ -396,6 +405,7 @@ static void OpenShaderEdit(shader_t* shader) shaderEditWindow.active = true; shaderEditWindow.shader = shader; shaderEditWindow.originalShader = *shader; + shaderEditWindow.shaderFilePath[0] = '\0'; FormatShaderCode(shaderEditWindow.code, sizeof(shaderEditWindow.code), shader); Q_strncpyz(shaderEditWindow.originalCode, shaderEditWindow.code, sizeof(shaderEditWindow.originalCode)); tr.shaderParseFailed = qfalse; @@ -452,6 +462,7 @@ static void AddShaderReplacement(int shaderIndex) if(edit.active && edit.shader->index == shaderIndex) { RemoveShaderReplacement(shaderIndex); + R_SetShaderData(edit.shader, &edit.originalShader); edit.active = false; } @@ -611,14 +622,28 @@ static void DrawImageList() static char rawFilter[256]; DrawFilter(rawFilter, sizeof(rawFilter)); - static bool worldOnly = false; + struct ImageFilter + { + enum Id + { + None, + World, + NonWorld, + Count + }; + }; + + static int imageFilter = ImageFilter::None; + if(tr.world == NULL && imageFilter != ImageFilter::None) + { + imageFilter = ImageFilter::None; + } if(tr.world != NULL) { - ImGui::Checkbox("World surfaces only", &worldOnly); - } - else - { - worldOnly = false; + ImGui::Text("Surface type"); + ImGui::RadioButton("All surfaces", &imageFilter, ImageFilter::None); + ImGui::RadioButton("World only", &imageFilter, ImageFilter::World); + ImGui::RadioButton("Non-world only", &imageFilter, ImageFilter::NonWorld); } if(BeginTable("Images", 1)) @@ -626,14 +651,15 @@ static void DrawImageList() for(int i = 0; i < tr.numImages; ++i) { const image_t* image = tr.images[i]; - if(worldOnly && image->worldSurfaceRefCount <= 0) + if((imageFilter == ImageFilter::World && image->worldSurfaceRefCount <= 0) || + (imageFilter == ImageFilter::NonWorld && image->worldSurfaceRefCount > 0)) { continue; } - char filter[256]; - Com_sprintf(filter, sizeof(filter), rawFilter[0] == '*' ? "%s" : "*%s", rawFilter); - if(filter[0] != '\0' && !Com_Filter(filter, image->name)) + char nameFilter[256]; + Com_sprintf(nameFilter, sizeof(nameFilter), rawFilter[0] == '*' ? "%s" : "*%s", rawFilter); + if(nameFilter[0] != '\0' && !Com_Filter(nameFilter, image->name)) { continue; } @@ -658,6 +684,52 @@ static void DrawImageList() } } +// flp2 function from "Hacker's Delight" +static int LowerOrEqualPOT(int x) +{ + x = x | (x >> 1); + x = x | (x >> 2); + x = x | (x >> 4); + x = x | (x >> 8); + x = x | (x >> 16); + + return x - (x >> 1); +} + +static void DrawShaderReferenceListForImage(const image_t* image) +{ + for(int is = 0; is < ARRAY_LEN(tr.imageShaders); ++is) + { + const int i = tr.imageShaders[is] & 0xFFFF; + if(i != image->index) + { + continue; + } + + const int s = (tr.imageShaders[is] >> 16) & (MAX_SHADERS - 1); + const shader_t* const shader = tr.shaders[s]; + if(shader == NULL) + { + continue; + } + + if(ImGui::Selectable(va("%s (LM %d)##%d", shader->name, shader->lightmapIndex, is), false)) + { + OpenShaderDetails((shader_t*)shader); + } + else if(ImGui::IsItemHovered()) + { + const char* const shaderPath = R_GetShaderPath(shader); + if(shaderPath != NULL) + { + ImGui::BeginTooltip(); + ImGui::Text(shaderPath); + ImGui::EndTooltip(); + } + } + } +} + static void DrawImageWindow() { ImageWindow& window = imageWindow; @@ -713,34 +785,49 @@ static void DrawImageWindow() } } - ImGui::NewLine(); - ImGui::Text("Shaders:"); + int shaderCount = 0; for(int is = 0; is < ARRAY_LEN(tr.imageShaders); ++is) { const int i = tr.imageShaders[is] & 0xFFFF; - if(i != image->index) + if(i == image->index) { - continue; - } - - const int s = (tr.imageShaders[is] >> 16) & 0xFFFF; - const shader_t* const shader = tr.shaders[s]; - if(ImGui::Selectable(va("%s##%d", shader->name, is), false)) - { - OpenShaderDetails((shader_t*)shader); - } - else if(ImGui::IsItemHovered()) - { - const char* const shaderPath = R_GetShaderPath(shader); - if(shaderPath != NULL) - { - ImGui::BeginTooltip(); - ImGui::Text(shaderPath); - ImGui::EndTooltip(); - } + shaderCount++; } } + ImGui::NewLine(); + const char* const shaderListTitle = va("%d Shader Reference%s", + shaderCount, shaderCount > 1 ? "s" : ""); + if(shaderCount > 3) + { + if(ImGui::CollapsingHeader(shaderListTitle, NULL, ImGuiTreeNodeFlags_None)) + { + DrawShaderReferenceListForImage(image); + } + } + else + { + ImGui::Text(shaderListTitle); + DrawShaderReferenceListForImage(image); + } + ImGui::NewLine(); + + static int manualScaleLog = 0; + static bool autoScale = true; + static bool autoScaleDownOnly = false; + static bool autoScalePOT = false; + bool usePointFilter = false; + + ImGui::Checkbox("Auto-scale", &autoScale); + if(autoScale) + { + ImGui::Checkbox("Auto-scale down only", &autoScaleDownOnly); + ImGui::Checkbox("Auto-scale to power of 2 dimensions", &autoScalePOT); + } + else + { + ImGui::SliderInt("Manual scale", &manualScaleLog, -10, 10, "%d", ImGuiSliderFlags_AlwaysClamp); + } int width = image->width; int height = image->height; @@ -753,10 +840,76 @@ static void DrawImageWindow() width = max(width / 2, 1); height = max(height / 2, 1); } + if(autoScale && autoScalePOT) + { + const ImVec2 workSize = ImGui::GetMainViewport()->WorkSize; + const int maxWidth = LowerOrEqualPOT((int)workSize.x) / 2; + const int maxHeight = LowerOrEqualPOT((int)workSize.y) / 2; + if(width > maxWidth || height > maxHeight) + { + for(int i = 0; width > maxWidth || height > maxHeight; i++) + { + if(width == 1 || height == 1) + { + break; + } + width /= 2; + height /= 2; + } + } + else if(!autoScaleDownOnly) + { + usePointFilter = true; + for(int i = 0; width * 2 <= maxWidth && height * 2 <= maxHeight; i++) + { + width *= 2; + height *= 2; + } + } + } + else if(autoScale && !autoScalePOT) + { + const ImVec2 workSize = ImGui::GetMainViewport()->WorkSize; + const int maxWidth = (int)(workSize.x * 0.75f); + const int maxHeight = (int)(workSize.y * 0.75f); + const bool downScale = width > maxWidth || height > maxHeight; + const bool upScale = !downScale && !autoScaleDownOnly; + if(downScale || upScale) + { + usePointFilter = upScale; + const float scaleX = (float)width / (float)maxWidth; + const float scaleY = (float)height / (float)maxHeight; + const float scale = max(scaleX, scaleY); + width = (int)((float)width / scale); + height = (int)((float)height / scale); + } + } + else if(manualScaleLog > 0) + { + usePointFilter = true; + for(int i = 0; i < manualScaleLog; i++) + { + width *= 2; + height *= 2; + } + } + else if(manualScaleLog < 0) + { + for(int i = 0; i < -manualScaleLog; i++) + { + if(width == 1 || height == 1) + { + break; + } + width /= 2; + height /= 2; + } + } const ImTextureID textureId = (ImTextureID)image->textureIndex | - (ImTextureID)(window.mip << 16); + (ImTextureID)(window.mip << 16) | + (usePointFilter ? ImTextureID(1 << 31) : ImTextureID(0)); ImGui::Image(textureId, ImVec2(width, height)); } @@ -807,14 +960,34 @@ static void DrawShaderList() static char rawFilter[256]; DrawFilter(rawFilter, sizeof(rawFilter)); - static bool worldOnly = false; + struct ShaderFilter + { + enum Id + { + None, + World, + NonWorld, + NonLightmappedWorld, + Skybox, + Fog, + Count + }; + }; + + static int shaderFilter = ShaderFilter::None; + if(tr.world == NULL && shaderFilter != ShaderFilter::None) + { + shaderFilter = ShaderFilter::None; + } if(tr.world != NULL) { - ImGui::Checkbox("World surfaces only", &worldOnly); - } - else - { - worldOnly = false; + ImGui::Text("Surface type"); + ImGui::RadioButton("All surfaces", &shaderFilter, ShaderFilter::None); + ImGui::RadioButton("World only", &shaderFilter, ShaderFilter::World); + ImGui::RadioButton("Non-lightmapped world", &shaderFilter, ShaderFilter::NonLightmappedWorld); + ImGui::RadioButton("Non-world only", &shaderFilter, ShaderFilter::NonWorld); + ImGui::RadioButton("Skybox only", &shaderFilter, ShaderFilter::Skybox); + ImGui::RadioButton("Fog only", &shaderFilter, ShaderFilter::Fog); } if(BeginTable("Shaders", 1)) @@ -822,14 +995,18 @@ static void DrawShaderList() for(int s = 0; s < tr.numShaders; ++s) { shader_t* shader = tr.shaders[s]; - if(worldOnly && shader->worldSurfaceRefCount <= 0) + if((shaderFilter == ShaderFilter::World && shader->worldSurfaceRefCount <= 0) || + (shaderFilter == ShaderFilter::NonWorld && shader->worldSurfaceRefCount > 0) || + (shaderFilter == ShaderFilter::Skybox && !shader->isSky) || + (shaderFilter == ShaderFilter::Fog && !shader->isFog) || + (shaderFilter == ShaderFilter::NonLightmappedWorld && (shader->hasLightmapStage || shader->worldSurfaceRefCount <= 0))) { continue; } - char filter[256]; - Com_sprintf(filter, sizeof(filter), rawFilter[0] == '*' ? "%s" : "*%s", rawFilter); - if(filter[0] != '\0' && !Com_Filter(filter, shader->name)) + char nameFilter[256]; + Com_sprintf(nameFilter, sizeof(nameFilter), rawFilter[0] == '*' ? "%s" : "*%s", rawFilter); + if(nameFilter[0] != '\0' && !Com_Filter(nameFilter, shader->name)) { continue; } @@ -850,6 +1027,64 @@ static void DrawShaderList() } } +static void BuildImageReferenceListForShader(const shader_t* shader) +{ + ShaderWindow& window = shaderWindow; + window.displayImageCount = 0; + + if(shader->isSky) + { + for(int i = 0; i < 6; ++i) + { + const image_t* image = shader->sky.outerbox[i]; + if(image != NULL) + { + window.displayImages[window.displayImageCount++] = image; + } + } + } + + for(int s = 0; s < shader->numStages; ++s) + { + const textureBundle_t& bundle = shader->stages[s]->bundle; + const int imageCount = max(bundle.numImageAnimations, 1); + for(int i = 0; i < imageCount; ++i) + { + const image_t* const image = bundle.image[i]; + bool found = false; + for(int di = 0; di < window.displayImageCount; ++di) + { + if(window.displayImages[di] == image) + { + found = true; + break; + } + } + if(!found && window.displayImageCount < ARRAY_LEN(window.displayImages)) + { + window.displayImages[window.displayImageCount++] = image; + } + } + } +} + +static void DrawImageReferenceList() +{ + const ShaderWindow& window = shaderWindow; + for(int i = 0; i < window.displayImageCount; i++) + { + const image_t* const image = window.displayImages[i]; + if(ImGui::Selectable(va("%s", image->name), false)) + { + OpenImageDetails(image); + } + else if(ImGui::IsItemHovered()) + { + DrawImageToolTip(image); + } + } +} + static void DrawShaderWindow() { ShaderWindow& window = shaderWindow; @@ -857,7 +1092,7 @@ static void DrawShaderWindow() { if(ImGui::Begin(SHADER_WINDOW_NAME, &window.active, ImGuiWindowFlags_AlwaysAutoResize)) { - shader_t* shader = window.shader; + shader_t* const shader = window.shader; TitleText(shader->name); if(ImGui::Button("Copy Name")) @@ -893,65 +1128,31 @@ static void DrawShaderWindow() } ImGui::NewLine(); - ImGui::Text("Images:"); - if(shader->isSky) - { - for(int i = 0; i < 6; ++i) - { - const image_t* image = shader->sky.outerbox[i]; - if(image == NULL) - { - continue; - } + ImGui::Text("Sort key: %g", shader->sort); + ImGui::Text("Lightmap index: %d", shader->lightmapIndex); + ImGui::BeginDisabled(); + Checkbox("Opaque", shader->isOpaque); + Checkbox("Alpha-tested opaque", shader->isAlphaTestedOpaque); + Checkbox("Lightmap", shader->hasLightmapStage); + Checkbox("Dynamic vertex attribute(s)", shader->isDynamic); + ImGui::EndDisabled(); - if(ImGui::Selectable(va("%s##skybox_%d", image->name, i), false)) - { - OpenImageDetails(image); - } - else if(ImGui::IsItemHovered()) - { - DrawImageToolTip(image); - } + BuildImageReferenceListForShader(shader); + const int imageCount = window.displayImageCount; + ImGui::NewLine(); + const char* const imageListTitle = va("%d Image Reference%s", + imageCount, imageCount > 1 ? "s" : ""); + if(imageCount > 3) + { + if(ImGui::CollapsingHeader(imageListTitle, NULL, ImGuiTreeNodeFlags_None)) + { + DrawImageReferenceList(); } } - - const image_t* displayImages[MAX_IMAGE_ANIMATIONS * MAX_SHADER_STAGES]; - int displayImageCount = 0; - for(int s = 0; s < shader->numStages; ++s) + else { - const textureBundle_t& bundle = shader->stages[s]->bundle; - const int imageCount = max(bundle.numImageAnimations, 1); - for(int i = 0; i < imageCount; ++i) - { - const image_t* image = bundle.image[i]; - - bool found = false; - for(int di = 0; di < displayImageCount; ++di) - { - if(displayImages[di] == image) - { - found = true; - break; - } - } - if(found) - { - continue; - } - if(displayImageCount < ARRAY_LEN(displayImages)) - { - displayImages[displayImageCount++] = image; - } - - if(ImGui::Selectable(va("%s##%d_%d", image->name, s, i), false)) - { - OpenImageDetails(image); - } - else if(ImGui::IsItemHovered()) - { - DrawImageToolTip(image); - } - } + ImGui::Text(imageListTitle); + DrawImageReferenceList(); } ImGui::NewLine(); @@ -1858,6 +2059,28 @@ static int ShaderEditCallback(ImGuiInputTextCallbackData* data) return 0; } +static void SaveShaderCodeToFile() +{ + ShaderEditWindow& window = shaderEditWindow; + if(window.shader == NULL || + window.shader->name == NULL || + window.shaderFilePath[0] == '\0' || + window.code[0] == '\0') + { + return; + } + + // only skip newlines and not anything that isn't '{' since we might skip valuable data + Com_sprintf(window.fileContent, sizeof(window.fileContent), "%s\n", window.shader->name); + const char* code = window.code; + while(*code == '\n') + { + code++; + } + Q_strcat(window.fileContent, sizeof(window.fileContent), code); + ri.FS_WriteFile(window.shaderFilePath, window.fileContent, strlen(window.fileContent)); +} + static void DrawShaderEdit() { // handle the keyboard shortcut and delayed opening @@ -1933,6 +2156,8 @@ static void DrawShaderEdit() ImGui::NewLine(); ImGui::Text("Ctrl+K/L: Comment/uncomment code"); ImGui::Text("Ctrl+H : Toggle hinting"); + ImGui::Text("Ctrl+S : Save to shader file"); + ImGui::Text(" F5 : Apply code changes"); ImGui::Separator(); } @@ -1982,12 +2207,20 @@ static void DrawShaderEdit() editing = true; } + bool saveFile = false; if(IsShortcutPressed(ImGuiKey_H, ShortcutOptions::Local)) { ri.Cvar_Set(r_guiShaderEditHints->name, r_guiShaderEditHints->integer != 0 ? "0" : "1"); } - - if(window.charCount < ARRAY_LEN(window.chars) - 1) + else if(IsShortcutPressed(ImGuiKey_S, ShortcutOptions::Local)) + { + saveFile = true; + } + else if(ImGui::IsKeyPressed(ImGuiKey_F5, false)) + { + R_EditShader(window.shader, &window.originalShader, window.code); + } + else if(window.charCount < ARRAY_LEN(window.chars) - 1) { const bool ctrlDown = ImGui::IsKeyDown(ImGuiMod_Ctrl); const bool shiftDown = ImGui::IsKeyDown(ImGuiMod_Shift); @@ -2010,6 +2243,11 @@ static void DrawShaderEdit() { R_EditShader(window.shader, &window.originalShader, window.code); } + ImGui::SameLine(); + if(ImGui::Button("Save to File")) + { + saveFile = true; + } ImGui::SameLine(inputSize.x - ImGui::CalcTextSize("Close and Discard").x); if(ImGui::Button("Close and Discard")) { @@ -2028,6 +2266,24 @@ static void DrawShaderEdit() { ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f), "Warning: %s", tr.shaderParseWarnings[i]); } + + if(saveFile) + { + if(window.shaderFilePath[0] == '\0') + { + SaveFileDialog_Open("scripts", ".shader"); + } + else + { + SaveShaderCodeToFile(); + } + } + + if(SaveFileDialog_Do()) + { + Q_strncpyz(window.shaderFilePath, SaveFileDialog_GetPath(), sizeof(window.shaderFilePath)); + SaveShaderCodeToFile(); + } } else {