tview/lib/imgui_tex_inspect.cpp

1183 lines
40 KiB
C++

// ImGuiTexInspect, a texture inspector widget for dear imgui
//-------------------------------------------------------------------------
// [SECTION] INCLUDES
//-------------------------------------------------------------------------
#define IMGUI_DEFINE_MATH_OPERATORS
#include "imgui_tex_inspect.h"
#include "imgui_tex_inspect_internal.h"
#include "imgui.h"
#include "imgui_internal.h"
#if defined(_MSC_VER)
#pragma warning(disable : 4996) // 'sprintf' considered unsafe
#endif
namespace ImGuiTexInspect
{
//-------------------------------------------------------------------------
// [SECTION] FORWARD DECLARATIONS
//-------------------------------------------------------------------------
void UpdateShaderOptions(Inspector *inspector);
void InspectorDrawCallback(const ImDrawList *parent_list, const ImDrawCmd *cmd);
bool GetVisibleTexelRegionAndGetData(Inspector *inspector, ImVec2 &texelTL, ImVec2 &texelBR);
//-------------------------------------------------------------------------
// [SECTION] GLOBAL STATE
//-------------------------------------------------------------------------
// Input mapping structure, default values listed in the comments.
struct InputMap
{
ImGuiMouseButton PanButton; // LMB enables panning when held
InputMap();
};
InputMap::InputMap()
{
PanButton = ImGuiMouseButton_Left;
}
// Settings configured via SetNextPanelOptions etc.
struct NextPanelSettings
{
InspectorFlags ToSet = 0;
InspectorFlags ToClear = 0;
};
// Main context / configuration structure for imgui_tex_inspect
struct Context
{
InputMap Input; // Input mapping config
ImGuiStorage Inspectors; // All the inspectors we've seen
Inspector * CurrentInspector; // Inspector currently being processed
NextPanelSettings NextPanelOptions; // Options configured for next inspector panel
float ZoomRate = 1.3f; // How fast mouse wheel affects zoom
float DefaultPanelHeight = 600; // Height of panel in pixels
float DefaultInitialPanelWidth = 600; // Only applies when window first appears
int MaxAnnotations = 1000; // Limit number of texel annotations for performance
};
Context *GContext = nullptr;
//-------------------------------------------------------------------------
// [SECTION] USER FUNCTIONS
//-------------------------------------------------------------------------
void Init()
{
// Nothing to do here. But there might be in a later version. So client code should still call it!
}
void Shutdown()
{
// Nothing to do here. But there might be in a later version. So client code should still call it!
}
Context *CreateContext()
{
GContext = IM_NEW(Context);
SetCurrentContext(GContext);
return GContext;
}
void DestroyContext(Context *ctx)
{
if (ctx == NULL)
{
ctx = GContext;
}
if (ctx == GContext)
{
GContext = NULL;
}
for (ImGuiStorage::ImGuiStoragePair &pair : ctx->Inspectors.Data)
{
Inspector *inspector = (Inspector *)pair.val_p;
if (inspector)
{
IM_DELETE(inspector);
}
}
IM_DELETE(ctx);
}
void SetCurrentContext(Context *context)
{
ImGuiTexInspect::GContext = context;
}
void SetNextPanelFlags(InspectorFlags setFlags, InspectorFlags clearFlags)
{
SetFlag(GContext->NextPanelOptions.ToSet, setFlags);
SetFlag(GContext->NextPanelOptions.ToClear, clearFlags);
}
bool BeginInspectorPanel(const char *title, ImTextureID texture, ImVec2 textureSize, InspectorFlags flags,
SizeIncludingBorder sizeIncludingBorder)
{
const int borderWidth = 0;
// Unpack size param. It's in the SizeIncludingBorder structure just to make sure users know what they're requesting
ImVec2 size = sizeIncludingBorder.Size;
ImGuiWindow *window = ImGui::GetCurrentWindow();
Context *ctx = GContext;
const ImGuiID ID = window->GetID(title);
const ImGuiIO &IO = ImGui::GetIO();
// Create or find inspector
bool justCreated = GetByKey(ctx, ID) == NULL;
ctx->CurrentInspector = GetOrAddByKey(ctx, ID);
Inspector *inspector = ctx->CurrentInspector;
justCreated |= !inspector->Initialized;
// Cache the basics
inspector->ID = ID;
inspector->Texture = texture;
inspector->TextureSize = textureSize;
inspector->Initialized = true;
// Handle incoming flags. We keep special track of the
// newly set flags because somethings only take effect
// the first time the flag is set.
InspectorFlags newlySetFlags = ctx->NextPanelOptions.ToSet;
if (justCreated)
{
SetFlag(newlySetFlags, flags);
inspector->MaxAnnotatedTexels = ctx->MaxAnnotations;
}
SetFlag(inspector->Flags, newlySetFlags);
ClearFlag(inspector->Flags, ctx->NextPanelOptions.ToClear);
ClearFlag(newlySetFlags, ctx->NextPanelOptions.ToClear);
ctx->NextPanelOptions = NextPanelSettings();
// Calculate panel size
ImVec2 contentRegionAvail = ImGui::GetContentRegionAvail();
ImVec2 panelSize;
// A size value of zero indicates we should use defaults
if (justCreated)
{
panelSize = {size.x == 0 ? ImMax(ctx->DefaultInitialPanelWidth, contentRegionAvail.x) : size.x,
size.y == 0 ? ctx->DefaultPanelHeight : size.y};
}
else
{
panelSize = {size.x == 0 ? contentRegionAvail.x : size.x, size.y == 0 ? ctx->DefaultPanelHeight : size.y};
}
inspector->PanelSize = panelSize;
ImVec2 availablePanelSize = panelSize - ImVec2(borderWidth, borderWidth) * 2;
{
// Possibly update scale
float newScale = -1;
if (HasFlag(newlySetFlags, InspectorFlags_FillVertical))
{
newScale = availablePanelSize.y / textureSize.y;
}
else if (HasFlag(newlySetFlags, InspectorFlags_FillHorizontal))
{
newScale = availablePanelSize.x / textureSize.x;
}
else if (justCreated)
{
newScale = 1;
}
if (newScale != -1)
{
inspector->Scale = ImVec2(newScale, newScale);
SetPanPos(inspector, ImVec2(0.5f, 0.5f));
}
}
RoundPanPos(inspector);
ImVec2 textureSizePixels = inspector->Scale * textureSize; // Size whole texture would appear on screen
ImVec2 viewSizeUV = availablePanelSize / textureSizePixels; // Cropped size in terms of UV
ImVec2 uv0 = inspector->PanPos - viewSizeUV * 0.5;
ImVec2 uv1 = inspector->PanPos + viewSizeUV * 0.5;
ImVec2 drawImageOffset{borderWidth, borderWidth};
ImVec2 viewSize = availablePanelSize;
if ((inspector->Flags & InspectorFlags_ShowWrap) == 0)
{
/* Don't crop the texture to UV [0,1] range. What you see outside this
* range will depend on API and texture properties */
if (textureSizePixels.x < availablePanelSize.x)
{
// Not big enough to horizontally fill view
viewSize.x = ImFloor(textureSizePixels.x);
drawImageOffset.x += ImFloor((availablePanelSize.x - textureSizePixels.x) / 2);
uv0.x = 0;
uv1.x = 1;
viewSizeUV.x = 1;
inspector->PanPos.x = 0.5f;
}
if (textureSizePixels.y < availablePanelSize.y)
{
// Not big enough to vertically fill view
viewSize.y = ImFloor(textureSizePixels.y);
drawImageOffset.y += ImFloor((availablePanelSize.y - textureSizePixels.y) / 2);
uv0.y = 0;
uv1.y = 1;
viewSizeUV.y = 1;
inspector->PanPos.y = 0.5;
}
}
if (HasFlag(flags,InspectorFlags_FlipX))
{
ImSwap(uv0.x, uv1.x);
viewSizeUV.x *= -1;
}
if (HasFlag(flags,InspectorFlags_FlipY))
{
ImSwap(uv0.y, uv1.y);
viewSizeUV.y *= -1;
}
inspector->ViewSize = viewSize;
inspector->ViewSizeUV = viewSizeUV;
/* We use mouse scroll to zoom so we don't want scroll to propagate to
* parent window. For this to happen we must NOT set
* ImGuiWindowFlags_NoScrollWithMouse. This seems strange but it's the way
* ImGui works. Also we must ensure the ScrollMax.y is not zero for the
* child window. */
if (ImGui::BeginChild(title, panelSize, false, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoMove))
{
// See comment above
ImGui::GetCurrentWindow()->ScrollMax.y = 1.0f;
// Callback for using our own image shader
ImGui::GetWindowDrawList()->AddCallback(InspectorDrawCallback, inspector);
// Keep track of size of area that we draw for borders later
inspector->PanelTopLeftPixel = ImGui::GetCursorScreenPos();
ImGui::SetCursorPos(ImGui::GetCursorPos() + drawImageOffset);
inspector->ViewTopLeftPixel = ImGui::GetCursorScreenPos();
UpdateShaderOptions(inspector);
inspector->CachedShaderOptions = inspector->ActiveShaderOptions;
ImGui::Image(texture, viewSize, uv0, uv1);
ImGui::GetWindowDrawList()->AddCallback(ImDrawCallback_ResetRenderState, nullptr);
/* Matrices for going back and forth between texel coordinates in the
* texture and screen coordinates based on where texture is drawn.
* Useful for annotations and mouse hover etc. */
inspector->TexelsToPixels = GetTexelsToPixels(inspector->ViewTopLeftPixel, viewSize, uv0, viewSizeUV, inspector->TextureSize);
inspector->PixelsToTexels = inspector->TexelsToPixels.Inverse();
ImVec2 mousePos = ImGui::GetMousePos();
ImVec2 mousePosTexel = inspector->PixelsToTexels * mousePos;
ImVec2 mouseUV = mousePosTexel / textureSize;
mousePosTexel.x = Modulus(mousePosTexel.x, textureSize.x);
mousePosTexel.y = Modulus(mousePosTexel.y, textureSize.y);
if (ImGui::IsItemHovered() && (inspector->Flags & ImGuiTexInspect::InspectorFlags_NoTooltip) == 0)
{
// Show a tooltip for currently hovered texel
ImVec2 texelTL;
ImVec2 texelBR;
if (GetVisibleTexelRegionAndGetData(inspector, texelTL, texelBR))
{
ImVec4 color = GetTexel(&inspector->Buffer, (int)mousePosTexel.x, (int)mousePosTexel.y);
char buffer[128];
sprintf(buffer, "UV: (%.5f, %.5f)\nTexel: (%d, %d)", mouseUV.x, mouseUV.y, (int)mousePosTexel.x, (int)mousePosTexel.y);
ImGui::ColorTooltip(buffer, &color.x, 0);
}
}
bool hovered = ImGui::IsWindowHovered();
{ //DRAGGING
// start drag
if (!inspector->IsDragging && hovered && IO.MouseClicked[ctx->Input.PanButton])
{
inspector->IsDragging = true;
}
// carry on dragging
else if (inspector->IsDragging)
{
ImVec2 uvDelta = IO.MouseDelta * viewSizeUV / viewSize;
inspector->PanPos -= uvDelta;
RoundPanPos(inspector);
}
// end drag
if (inspector->IsDragging && (IO.MouseReleased[ctx->Input.PanButton] || !IO.MouseDown[ctx->Input.PanButton]))
{
inspector->IsDragging = false;
}
}
// ZOOM
if (hovered && IO.MouseWheel != 0)
{
float zoomRate = ctx->ZoomRate;
float scale = inspector->Scale.y;
float prevScale = scale;
bool keepTexelSizeRegular = scale > inspector->MinimumGridSize && !HasFlag(inspector->Flags, InspectorFlags_NoGrid);
if (IO.MouseWheel > 0)
{
scale *= zoomRate;
if (keepTexelSizeRegular)
{
// It looks nicer when all the grid cells are the same size
// so keep scale integer when zoomed in
scale = ImCeil(scale);
}
}
else
{
scale /= zoomRate;
if (keepTexelSizeRegular)
{
// See comment above. We're doing a floor this time to make
// sure the scale always changes when scrolling
scale = ImFloorSigned(scale);
}
}
/* To make it easy to get back to 1:1 size we ensure that we stop
* here without going straight past it*/
if ((prevScale < 1 && scale > 1) || (prevScale > 1 && scale < 1))
{
scale = 1;
}
SetScale(inspector, ImVec2(inspector->PixelAspectRatio * scale, scale));
SetPanPos(inspector, inspector->PanPos + (mouseUV - inspector->PanPos) * (1 - prevScale / scale));
}
return true;
}
else
{
return false;
}
}
bool BeginInspectorPanel(const char *name, ImTextureID texture, ImVec2 textureSize, InspectorFlags flags)
{
return BeginInspectorPanel(name, texture, textureSize, flags, SizeIncludingBorder{{0, 0}});
}
bool BeginInspectorPanel(const char *name, ImTextureID texture, ImVec2 textureSize, InspectorFlags flags, SizeExcludingBorder size)
{
// Correct the size to include the border, but preserve 0 which has a special meaning
return BeginInspectorPanel(name, texture, textureSize, flags,
SizeIncludingBorder{ImVec2{size.size.x == 0 ? 0 : size.size.x + 2,
size.size.y == 0 ? 0 : size.size.y + 2}});
}
void EndInspectorPanel()
{
const ImU32 innerBorderColour = 0xFFFFFFFF;
const ImU32 outerBorderColour = 0x00000000;
Inspector *inspector = GContext->CurrentInspector;
// Draw out border around whole inspector panel
ImGui::GetWindowDrawList()->AddRect(inspector->PanelTopLeftPixel, inspector->PanelTopLeftPixel + inspector->PanelSize,
outerBorderColour);
// Draw innder border around texture. If zoomed in this will completely cover the outer border
ImGui::GetWindowDrawList()->AddRect(inspector->ViewTopLeftPixel - ImVec2(1, 1),
inspector->ViewTopLeftPixel + inspector->ViewSize + ImVec2(1, 1), innerBorderColour);
ImGui::EndChild();
// We set this back to false every frame in case the texture is dynamic
if (!HasFlag(inspector->Flags, InspectorFlags_NoAutoReadTexture))
{
inspector->HaveCurrentTexelData = false;
}
}
void ReleaseInspectorData(ImGuiID ID)
{
Inspector *inspector = GetByKey(GContext, ID);
if (inspector == NULL)
return;
if (inspector->DataBuffer)
{
IM_FREE(inspector->DataBuffer);
inspector->DataBuffer = NULL;
inspector->DataBufferSize = 0;
}
/* In a later version we will remove inspector from the inspector table
* altogether. For now we reset the whole inspector structure to prevent
* clients relying on persisted data.
*/
*inspector = Inspector();
}
ImGuiID CurrentInspector_GetID()
{
return GContext->CurrentInspector->ID;
}
void CurrentInspector_SetColorMatrix(const float (&matrix)[16], const float (&colorOffset)[4])
{
Inspector *inspector = GContext->CurrentInspector;
ShaderOptions *shaderOptions = &inspector->ActiveShaderOptions;
memcpy(shaderOptions->ColorTransform, matrix, sizeof(matrix));
memcpy(shaderOptions->ColorOffset, colorOffset, sizeof(colorOffset));
}
void CurrentInspector_ResetColorMatrix()
{
Inspector *inspector = GContext->CurrentInspector;
ShaderOptions *shaderOptions = &inspector->ActiveShaderOptions;
shaderOptions->ResetColorTransform();
}
void CurrentInspector_SetAlphaMode(InspectorAlphaMode mode)
{
Inspector *inspector = GContext->CurrentInspector;
ShaderOptions *shaderOptions = &inspector->ActiveShaderOptions;
inspector->AlphaMode = mode;
switch (mode)
{
case InspectorAlphaMode_Black:
shaderOptions->BackgroundColor = ImVec4(0, 0, 0, 1);
shaderOptions->DisableFinalAlpha = 1;
shaderOptions->PremultiplyAlpha = 1;
break;
case InspectorAlphaMode_White:
shaderOptions->BackgroundColor = ImVec4(1, 1, 1, 1);
shaderOptions->DisableFinalAlpha = 1;
shaderOptions->PremultiplyAlpha = 1;
break;
case InspectorAlphaMode_ImGui:
shaderOptions->BackgroundColor = ImVec4(0, 0, 0, 0);
shaderOptions->DisableFinalAlpha = 0;
shaderOptions->PremultiplyAlpha = 0;
break;
case InspectorAlphaMode_CustomColor:
shaderOptions->BackgroundColor = inspector->CustomBackgroundColor;
shaderOptions->DisableFinalAlpha = 1;
shaderOptions->PremultiplyAlpha = 1;
break;
}
}
void CurrentInspector_SetFlags(InspectorFlags toSet, InspectorFlags toClear)
{
Inspector *inspector = GContext->CurrentInspector;
SetFlag(inspector->Flags, toSet);
ClearFlag(inspector->Flags, toClear);
}
void CurrentInspector_SetGridColor(ImU32 color)
{
Inspector *inspector = GContext->CurrentInspector;
float alpha = inspector->ActiveShaderOptions.GridColor.w;
inspector->ActiveShaderOptions.GridColor = ImColor(color);
inspector->ActiveShaderOptions.GridColor.w = alpha;
}
void CurrentInspector_SetMaxAnnotations(int maxAnnotations)
{
Inspector *inspector = GContext->CurrentInspector;
inspector->MaxAnnotatedTexels = maxAnnotations;
}
void CurrentInspector_InvalidateTextureCache()
{
Inspector *inspector = GContext->CurrentInspector;
inspector->HaveCurrentTexelData = false;
}
void CurrentInspector_SetCustomBackgroundColor(ImVec4 color)
{
Inspector *inspector = GContext->CurrentInspector;
inspector->CustomBackgroundColor = color;
if (inspector->AlphaMode == InspectorAlphaMode_CustomColor)
{
inspector->ActiveShaderOptions.BackgroundColor = color;
}
}
void CurrentInspector_SetCustomBackgroundColor(ImU32 color)
{
CurrentInspector_SetCustomBackgroundColor(ImGui::ColorConvertU32ToFloat4(color));
}
void DrawColorMatrixEditor()
{
const char *colorVectorNames[] = {"R", "G", "B", "A", "1"};
const char *finalColorVectorNames[] = {"R'", "G'", "B'", "A'"};
const float dragSpeed = 0.02f;
Inspector *inspector = GContext->CurrentInspector;
ShaderOptions *shaderOptions = &inspector->ActiveShaderOptions;
// Left hand side of equation. The final color vector which is the actual drawn color
TextVector("FinalColorVector", finalColorVectorNames, IM_ARRAYSIZE(finalColorVectorNames));
ImGui::SameLine();
ImGui::TextUnformatted("=");
ImGui::SameLine();
// Right hand side of the equation: the Matrix. This is the editable part
ImGui::BeginGroup();
for (int i = 0; i < 4; ++i)
{
ImGui::PushID(i);
for (int j = 0; j < 4; ++j)
{
ImGui::PushID(j);
ImGui::SetNextItemWidth(50);
ImGui::DragFloat("##f", &shaderOptions->ColorTransform[j * 4 + i], dragSpeed);
ImGui::PopID();
ImGui::SameLine();
}
ImGui::SetNextItemWidth(50);
ImGui::DragFloat("##offset", &shaderOptions->ColorOffset[i], dragSpeed);
ImGui::PopID();
}
ImGui::EndGroup();
ImGui::SameLine();
ImGui::TextUnformatted("*");
ImGui::SameLine();
// Right hand side of equation. The input vector, the source color of the texel.
TextVector("ColorVector", colorVectorNames, IM_ARRAYSIZE(colorVectorNames));
}
void DrawGridEditor()
{
Inspector *inspector = GContext->CurrentInspector;
ImGui::BeginGroup();
bool gridEnabled = !HasFlag(inspector->Flags, InspectorFlags_NoGrid);
if (ImGui::Checkbox("Grid", &gridEnabled))
{
if (gridEnabled)
{
CurrentInspector_ClearFlags(InspectorFlags_NoGrid);
}
else
{
CurrentInspector_SetFlags(InspectorFlags_NoGrid);
}
}
if (gridEnabled)
{
ImGui::SameLine();
ImGui::ColorEdit3("Grid Color", (float *)&inspector->ActiveShaderOptions.GridColor, ImGuiColorEditFlags_NoInputs);
}
ImGui::EndGroup();
}
void DrawColorChannelSelector()
{
Inspector *inspector = GContext->CurrentInspector;
ShaderOptions *shaderOptions = &inspector->ActiveShaderOptions;
ImGuiStorage *storage = ImGui::GetStateStorage();
const ImGuiID greyScaleID = ImGui::GetID("greyScale");
bool greyScale = storage->GetBool(greyScaleID, false);
bool red = shaderOptions->ColorTransform[0] > 0;
bool green = shaderOptions->ColorTransform[5] > 0;
bool blue = shaderOptions->ColorTransform[10] > 0;
bool changed = false;
// In greyScale made we draw the red, green, blue checkboxes as disabled
if (greyScale)
{
PushDisabled();
}
ImGui::BeginGroup();
changed |= ImGui::Checkbox("Red", &red);
changed |= ImGui::Checkbox("Green", &green);
changed |= ImGui::Checkbox("Blue", &blue);
ImGui::EndGroup();
ImGui::SameLine();
if (greyScale)
{
PopDisabled();
}
if (changed)
{
// Overwrite the color transform matrix with one based on the settings
shaderOptions->ResetColorTransform();
shaderOptions->ColorTransform[0] = red ? 1.0f : 0.0f;
shaderOptions->ColorTransform[5] = green ? 1.0f : 0.0f;
shaderOptions->ColorTransform[10] = blue ? 1.0f : 0.0f;
}
ImGui::BeginGroup();
if (ImGui::Checkbox("Grey", &greyScale))
{
shaderOptions->ResetColorTransform();
storage->SetBool(greyScaleID, greyScale);
if (greyScale)
{
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
shaderOptions->ColorTransform[i * 4 + j] = 0.333f;
}
}
}
}
ImGui::EndGroup();
}
void DrawAlphaModeSelector()
{
Inspector *inspector = GContext->CurrentInspector;
const char *alphaModes[] = {"ImGui Background", "Black", "White", "Custom Color"};
ImGui::SetNextItemWidth(200);
InspectorAlphaMode currentAlphaMode = inspector->AlphaMode;
ImGui::Combo("Alpha Modes", (int *)&currentAlphaMode, alphaModes, IM_ARRAYSIZE(alphaModes));
CurrentInspector_SetAlphaMode(currentAlphaMode);
if (inspector->AlphaMode == InspectorAlphaMode_CustomColor)
{
ImVec4 backgroundColor = inspector->CustomBackgroundColor;
if (ImGui::ColorEdit3("Background Color", (float *)&backgroundColor, 0))
{
CurrentInspector_SetCustomBackgroundColor(backgroundColor);
}
}
}
void SetZoomRate(float rate)
{
GContext->ZoomRate = rate;
}
//-------------------------------------------------------------------------
// [SECTION] Life Cycle
//-------------------------------------------------------------------------
Inspector::~Inspector()
{
if (DataBuffer)
{
IM_FREE(DataBuffer);
}
}
//-------------------------------------------------------------------------
// [SECTION] Scaling and Panning
//-------------------------------------------------------------------------
void RoundPanPos(Inspector *inspector)
{
if ((inspector->Flags & InspectorFlags_ShowWrap) > 0)
{
/* PanPos is the point in the center of the current view. Allow the
* user to pan anywhere as long as the view center is inside the
* texture.*/
inspector->PanPos = ImClamp(inspector->PanPos, ImVec2(0, 0), ImVec2(1, 1));
}
else
{
/* When ShowWrap mode is disabled the limits are a bit more strict. We
* try to keep it so that the user cannot pan past the edge of the
* texture at all.*/
ImVec2 absViewSizeUV = Abs(inspector->ViewSizeUV);
inspector->PanPos = ImMax(inspector->PanPos - absViewSizeUV / 2, ImVec2(0, 0)) + absViewSizeUV / 2;
inspector->PanPos = ImMin(inspector->PanPos + absViewSizeUV / 2, ImVec2(1, 1)) - absViewSizeUV / 2;
}
/* If inspector->scale is 1 then we should ensure that pixels are aligned
* with texel centers to get pixel-perfect texture rendering*/
ImVec2 topLeftSubTexel = inspector->PanPos * inspector->Scale * inspector->TextureSize - inspector->ViewSize * 0.5f;
if (inspector->Scale.x >= 1)
{
topLeftSubTexel.x = Round(topLeftSubTexel.x);
}
if (inspector->Scale.y >= 1)
{
topLeftSubTexel.y = Round(topLeftSubTexel.y);
}
inspector->PanPos = (topLeftSubTexel + inspector->ViewSize * 0.5f) / (inspector->Scale * inspector->TextureSize);
}
void SetPanPos(Inspector *inspector, ImVec2 pos)
{
inspector->PanPos = pos;
RoundPanPos(inspector);
}
void SetScale(Inspector *inspector, ImVec2 scale)
{
scale = ImClamp(scale, inspector->ScaleMin, inspector->ScaleMax);
inspector->ViewSizeUV *= inspector->Scale / scale;
inspector->Scale = scale;
// Only force nearest sampling if zoomed in
inspector->ActiveShaderOptions.ForceNearestSampling =
(inspector->Scale.x > 1.0f || inspector->Scale.y > 1.0f) && !HasFlag(inspector->Flags, InspectorFlags_NoForceFilterNearest);
inspector->ActiveShaderOptions.GridWidth = ImVec2(1.0f / inspector->Scale.x, 1.0f / inspector->Scale.y);
}
void SetScale(Inspector *inspector, float scaleY)
{
SetScale(inspector, ImVec2(scaleY * inspector->PixelAspectRatio, scaleY));
}
//-------------------------------------------------------------------------
// [SECTION] INSPECTOR MAP
//-------------------------------------------------------------------------
Inspector *GetByKey(const Context *ctx, ImGuiID key)
{
return (Inspector *)ctx->Inspectors.GetVoidPtr(key);
}
Inspector *GetOrAddByKey(Context *ctx, ImGuiID key)
{
Inspector *inspector = GetByKey(ctx, key);
if (inspector)
{
return inspector;
}
else
{
inspector = IM_NEW(Inspector);
ctx->Inspectors.SetVoidPtr(key, inspector);
return inspector;
}
}
//-------------------------------------------------------------------------
// [SECTION] TextureConversion class
//-------------------------------------------------------------------------
void ShaderOptions::ResetColorTransform()
{
memset(ColorTransform, 0, sizeof(ColorTransform));
for (int i = 0; i < 4; ++i)
{
ColorTransform[i * 4 + i] = 1;
}
}
ShaderOptions::ShaderOptions()
{
ResetColorTransform();
memset(ColorOffset, 0, sizeof(ColorOffset));
}
//-------------------------------------------------------------------------
// [SECTION] UI and CONFIG
//-------------------------------------------------------------------------
void UpdateShaderOptions(Inspector *inspector)
{
if (HasFlag(inspector->Flags, InspectorFlags_NoGrid) == false && inspector->Scale.y > inspector->MinimumGridSize)
{
// Enable grid in shader
inspector->ActiveShaderOptions.GridColor.w = 1;
SetScale(inspector, Round(inspector->Scale.y));
}
else
{
// Disable grid in shader
inspector->ActiveShaderOptions.GridColor.w = 0;
}
inspector->ActiveShaderOptions.ForceNearestSampling =
(inspector->Scale.x > 1.0f || inspector->Scale.y > 1.0f) && !HasFlag(inspector->Flags, InspectorFlags_NoForceFilterNearest);
}
// Draws a single column ImGui table with one row for each provided string
void TextVector(const char *title, const char *const *strings, int stringCount)
{
ImGui::BeginGroup();
ImGui::SetNextItemWidth(50);
if (ImGui::BeginTable(title, 1, ImGuiTableFlags_BordersOuter | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoHostExtendX))
{
for (int i = 0; i < stringCount; ++i)
{
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::SetNextItemWidth(50);
ImGui::Text("%s", strings[i]);
}
ImGui::EndTable();
}
ImGui::EndGroup();
}
const ImGuiCol disabledUIColorIds[] = {ImGuiCol_FrameBg,
ImGuiCol_FrameBgActive,
ImGuiCol_FrameBgHovered,
ImGuiCol_Text,
ImGuiCol_CheckMark};
// Push disabled style for ImGui elements
void PushDisabled()
{
for (ImGuiCol colorId : disabledUIColorIds)
{
ImVec4 color = ImGui::GetStyleColorVec4(colorId);
color = color * ImVec4(0.5f, 0.5f, 0.5f, 0.5f);
ImGui::PushStyleColor(colorId, color);
}
ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
}
// Pop disabled style for ImGui elements
void PopDisabled()
{
for (ImGuiCol colorId : disabledUIColorIds)
{
(void)colorId;
ImGui::PopStyleColor();
}
ImGui::PopItemFlag();
}
//-------------------------------------------------------------------------
// [SECTION] Rendering & Buffer Management
//-------------------------------------------------------------------------
void InspectorDrawCallback(const ImDrawList *parent_list, const ImDrawCmd *cmd)
{
// Forward call to API-specific backend
Inspector *inspector = (Inspector *)cmd->UserCallbackData;
BackEnd_SetShader(parent_list, cmd, inspector);
}
// Calculate a transform to convert from texel coordinates to screen pixel coordinates
Transform2D GetTexelsToPixels(ImVec2 screenTopLeft, ImVec2 screenViewSize, ImVec2 uvTopLeft, ImVec2 uvViewSize, ImVec2 textureSize)
{
ImVec2 uvToPixel = screenViewSize / uvViewSize;
Transform2D transform;
transform.Scale = uvToPixel / textureSize;
transform.Translate.x = screenTopLeft.x - uvTopLeft.x * uvToPixel.x;
transform.Translate.y = screenTopLeft.y - uvTopLeft.y * uvToPixel.y;
return transform;
}
/* Fills in the AnnotationsDesc structure which provides all necessary
* information for code which draw annoations. Returns false if no annoations
* should be drawn. The maxAnnotatedTexels argument provides a way to override
* the default maxAnnotatedTexels.
*/
bool GetAnnotationDesc(AnnotationsDesc *ad, ImU64 maxAnnotatedTexels)
{
Inspector *inspector = GContext->CurrentInspector;
if (maxAnnotatedTexels == 0)
{
maxAnnotatedTexels = inspector->MaxAnnotatedTexels;
}
if (maxAnnotatedTexels != 0)
{
/* Check if we would draw too many annotations. This is to avoid poor
* frame rate when too zoomed out. Increase MaxAnnotatedTexels if you
* want to draw more annotations. Note that we don't use texelTL &
* texelBR to get total visible texels as this would cause flickering
* while panning as the exact number of visible texels changes.
*/
ImVec2 screenViewSizeTexels = Abs(inspector->PixelsToTexels.Scale) * inspector->ViewSize;
ImU64 approxVisibleTexelCount = (ImU64)screenViewSizeTexels.x * (ImU64)screenViewSizeTexels.y;
if (approxVisibleTexelCount > maxAnnotatedTexels)
{
return false;
}
}
// texelTL & texelBL will describe the currently visible texel region
ImVec2 texelTL;
ImVec2 texelBR;
if (GetVisibleTexelRegionAndGetData(inspector, texelTL, texelBR))
{
ad->Buffer= inspector->Buffer;
ad->DrawList = ImGui::GetWindowDrawList();
ad->TexelsToPixels = inspector->TexelsToPixels;
ad->TexelTopLeft = texelTL;
ad->TexelViewSize = texelBR - texelTL;
return true;
}
return false;
}
/* Calculates currently visible region of texture (which is returned in texelTL
* and texelBR) then also actually ensure that that data is in memory. Returns
* false if fetching data failed.
*/
bool GetVisibleTexelRegionAndGetData(Inspector *inspector, ImVec2 &texelTL, ImVec2 &texelBR)
{
/* Figure out which texels correspond to the top left and bottom right
* corners of the texture view. The plus + ImVec2(1,1) is because we
* want to draw partially visible texels on the bottom and right edges.
*/
texelTL = ImFloor(inspector->PixelsToTexels * inspector->ViewTopLeftPixel);
texelBR = ImFloor(inspector->PixelsToTexels * (inspector->ViewTopLeftPixel + inspector->ViewSize));
if (texelTL.x > texelBR.x)
{
ImSwap(texelTL.x, texelBR.x);
}
if (texelTL.y > texelBR.y)
{
ImSwap(texelTL.y, texelBR.y);
}
/* Add ImVec2(1,1) because we want to draw partially visible texels on the
* bottom and right edges.*/
texelBR += ImVec2(1,1);
texelTL = ImClamp(texelTL, ImVec2(0, 0), inspector->TextureSize);
texelBR = ImClamp(texelBR, ImVec2(0, 0), inspector->TextureSize);
if (inspector->HaveCurrentTexelData)
{
return true;
}
// Now request pixel data for this region from backend
ImVec2 texelViewSize = texelBR - texelTL;
if (ImMin(texelViewSize.x, texelViewSize.y) > 0)
{
if (BackEnd_GetData(inspector, inspector->Texture, (int)texelTL.x, (int)texelTL.y, (int)texelViewSize.x, (int)texelViewSize.y,
&inspector->Buffer))
{
inspector->HaveCurrentTexelData = true;
return true;
}
}
return false;
}
/* This is a function the backends can use to allocate a buffer for storing
* texture texel data. The buffer is owned by the inpsector so the backend
* code doesn't need to worry about freeing it.
*/
ImU8 *GetBuffer(Inspector *inspector, size_t bytes)
{
if (inspector->DataBufferSize < bytes || inspector->DataBuffer == nullptr)
{
// We need to allocate a buffer
if (inspector->DataBuffer)
{
IM_FREE(inspector->DataBuffer);
}
// Allocate slightly more than we need to avoid reallocating
// very frequently in the case that size is increasing.
size_t size = bytes * 5 / 4;
inspector->DataBuffer = (ImU8 *)IM_ALLOC(size);
inspector->DataBufferSize = size;
}
return inspector->DataBuffer;
}
ImVec4 GetTexel(const BufferDesc *bd, int x, int y)
{
if (x < bd->StartX || x >= bd->StartX + bd->Width || y < bd->StartY || y >= bd->StartY + bd->Height)
{
// Outside the range of data in the buffer.
return ImVec4();
}
// Calculate position in array
size_t offset = ((size_t)bd->LineStride * (y - bd->StartY) + bd->Stride * (x - bd->StartX));
if (bd->Data_float)
{
const float *texel = bd->Data_float + offset;
// It's possible our buffer doesn't have all 4 channels so fill gaps in with zeros
return ImVec4( texel[bd->Red],
bd->ChannelCount >= 2 ? texel[bd->Green] : 0,
bd->ChannelCount >= 3 ? texel[bd->Blue] : 0,
bd->ChannelCount >= 4 ? texel[bd->Alpha] : 0);
}
else if (bd->Data_uint8_t)
{
const ImU8 *texel = bd->Data_uint8_t + offset;
// It's possible our buffer doesn't have all 4 channels so fill gaps in with zeros.
// Also map from [0,255] to [0,1]
return ImVec4( (float)texel[bd->Red] / 255.0f,
bd->ChannelCount >= 2 ? (float)texel[bd->Green] / 255.0f : 0,
bd->ChannelCount >= 3 ? (float)texel[bd->Blue] / 255.0f : 0,
bd->ChannelCount >= 4 ? (float)texel[bd->Alpha] / 255.0f : 0);
}
else
{
return ImVec4();
}
}
//-------------------------------------------------------------------------
// [SECTION] Annotations
//-------------------------------------------------------------------------
ValueText::ValueText(Format format)
{
/* The ValueText annotation draws a string inside each texel displaying the
* values of each channel. We now select a format string based on the enum
* parameter*/
switch (format)
{
case Format::HexString:
TextFormatString = "#%02X%02X%02X%02X";
TextColumnCount = 9;
TextRowCount = 1;
FormatAsFloats = false;
break;
case Format::BytesHex:
TextFormatString = "R:#%02X\nG:#%02X\nB:#%02X\nA:#%02X";
TextColumnCount = 5;
TextRowCount = 4;
FormatAsFloats = false;
break;
case Format::BytesDec:
TextFormatString = "R:%3d\nG:%3d\nB:%3d\nA:%3d";
TextColumnCount = 5;
TextRowCount = 4;
FormatAsFloats = false;
break;
case Format::Floats:
TextFormatString = "%5.3f\n%5.3f\n%5.3f\n%5.3f";
TextColumnCount = 5;
TextRowCount = 4;
FormatAsFloats = true;
break;
}
}
void ValueText::DrawAnnotation(ImDrawList *drawList, ImVec2 texel, Transform2D texelsToPixels, ImVec4 value)
{
char buffer[64];
float fontHeight = ImGui::GetFontSize();
float fontWidth = fontHeight / 2; /* WARNING this is a hack that gets a constant
* character width from half the height. This work for the default font but
* won't work on other fonts which may even not be monospace.*/
// Calculate size of text and check if it fits
ImVec2 textSize = ImVec2((float)TextColumnCount * fontWidth, (float)TextRowCount * fontHeight);
if (textSize.x > ImAbs(texelsToPixels.Scale.x) || textSize.y > ImAbs(texelsToPixels.Scale.y))
{
// Not enough room in texel to fit the text. Don't draw it.
return;
}
/* Choose black or white text based on how bright the texel. I.e. don't
* draw black text on a dark background or vice versa. */
float brightness = (value.x + value.y + value.z) * value.w / 3;
ImU32 lineColor = brightness > 0.5 ? 0xFF000000 : 0xFFFFFFFF;
if (FormatAsFloats)
{
sprintf(buffer, TextFormatString, value.x, value.y, value.z, value.w);
}
else
{
/* Map [0,1] to [0,255]. Also clamp it since input data wasn't
* necessarily in [0,1] range. */
ImU8 r = (ImU8)Round((ImClamp(value.x, 0.0f, 1.0f)) * 255);
ImU8 g = (ImU8)Round((ImClamp(value.y, 0.0f, 1.0f)) * 255);
ImU8 b = (ImU8)Round((ImClamp(value.z, 0.0f, 1.0f)) * 255);
ImU8 a = (ImU8)Round((ImClamp(value.w, 0.0f, 1.0f)) * 255);
sprintf(buffer, TextFormatString, r, g, b, a);
}
// Add text to drawlist!
ImVec2 pixelCenter = texelsToPixels * texel;
drawList->AddText(pixelCenter - textSize * 0.5f, lineColor, buffer);
}
Arrow::Arrow(int xVectorIndex, int yVectorIndex, ImVec2 lineScale)
: VectorIndex_x(xVectorIndex), VectorIndex_y(yVectorIndex), LineScale(lineScale)
{
}
Arrow &Arrow::UsePreset(Preset preset)
{
switch (preset)
{
default:
case Preset::NormalMap:
VectorIndex_x = 0;
VectorIndex_y = 1;
LineScale = ImVec2(1, -1);
ZeroPoint = ImVec2(128.0f / 255, 128.0f / 255);
break;
case Preset::NormalizedFloat:
VectorIndex_x = 0;
VectorIndex_y = 1;
LineScale = ImVec2(0.5f, -0.5f);
ZeroPoint = ImVec2(0, 0);
break;
}
return *this;
}
void Arrow::DrawAnnotation(ImDrawList *drawList, ImVec2 texel, Transform2D texelsToPixels, ImVec4 value)
{
const float arrowHeadScale = 0.35f;
const ImU32 lineColor = 0xFFFFFFFF;
float *vecPtr = &value.x;
// Draw an arrow!
ImVec2 lineDir = (ImVec2(vecPtr[VectorIndex_x], vecPtr[VectorIndex_y]) - ZeroPoint) * LineScale;
ImVec2 lineStart = texel;
ImVec2 lineEnd = lineStart + lineDir;
ImVec2 arrowHead1 = ImVec2(lineDir.x - lineDir.y, lineDir.x + lineDir.y) * -arrowHeadScale;
ImVec2 arrowHead2 = ImVec2(lineDir.x + lineDir.y, -lineDir.x + lineDir.y) * -arrowHeadScale;
DrawAnnotationLine(drawList, lineStart, lineEnd, texelsToPixels, lineColor);
DrawAnnotationLine(drawList, lineEnd, lineEnd + arrowHead1, texelsToPixels, lineColor);
DrawAnnotationLine(drawList, lineEnd, lineEnd + arrowHead2, texelsToPixels, lineColor);
}
void DrawAnnotationLine(ImDrawList *drawList, ImVec2 fromTexel, ImVec2 toTexel, Transform2D texelsToPixels, ImU32 color)
{
ImVec2 lineFrom = texelsToPixels * fromTexel;
ImVec2 lineTo = texelsToPixels * toTexel;
drawList->AddLine(lineFrom, lineTo, color, 1.0f);
}
} // namespace ImGuiTexInspect