From 7074a41528cb3f28cfc231516c1844da3261fab3 Mon Sep 17 00:00:00 2001 From: 4DBug <4DBug@github.com> Date: Thu, 8 Oct 2026 17:38:33 -0500 Subject: [PATCH] Decal Viewer example (decal/image id -> imgui texture, demo image viewer with grid); backend CreateTexture/DestroyTexture --- backends/imgui_impl_roblox.lua | 15 +++++ build/ImGui.linemap.json | 2 +- build/ImGui.luau | 17 ++++++ examples/example_roblox/main.client.luau | 70 ++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 1 deletion(-) diff --git a/backends/imgui_impl_roblox.lua b/backends/imgui_impl_roblox.lua index c5677d2..a8a8010 100644 --- a/backends/imgui_impl_roblox.lua +++ b/backends/imgui_impl_roblox.lua @@ -187,6 +187,19 @@ function ImGui_ImplRoblox_UpdateTexture(tex) end end +--- User texture from RGBA8 pixels (e.g. EditableImage:ReadPixelsBuffer()). Returns an ImTextureRef for +--- ImGui.Image / draw_list:AddImage. Ids are never reused, so cached tiles can't show a stale image. +function ImGui_ImplRoblox_CreateTexture(pixels, w, h) + local bd = ImGui_ImplRoblox_GetBackendData() + bd.TextureCount = bd.TextureCount + 1 + bd.Textures[bd.TextureCount] = { Buffer = pixels, Width = w, Height = h } + return ImTextureRef(bd.TextureCount) +end + +function ImGui_ImplRoblox_DestroyTexture(tex_ref) + ImGui_ImplRoblox_GetBackendData().Textures[tex_ref._TexID] = nil +end + --------------------------------------------------------- -- RENDERING (software rasterizer into tile buffers) --------------------------------------------------------- @@ -1191,6 +1204,8 @@ ImGui_ImplRoblox = { SetRenderScale = ImGui_ImplRoblox_SetRenderScale, SetRenderRate = ImGui_ImplRoblox_SetRenderRate, SetOverlay = ImGui_ImplRoblox_SetOverlay, + CreateTexture = ImGui_ImplRoblox_CreateTexture, + DestroyTexture = ImGui_ImplRoblox_DestroyTexture, GetRenderScale = function() return ImGui_ImplRoblox_GetBackendData().Scale end, SetBusyRenderRate = ImGui_ImplRoblox_SetBusyRenderRate, SetProfiling = ImGui_ImplRoblox_SetProfiling, diff --git a/build/ImGui.linemap.json b/build/ImGui.linemap.json index 26adff8..d95d75f 100644 --- a/build/ImGui.linemap.json +++ b/build/ImGui.linemap.json @@ -1 +1 @@ -[[12, "imgui.lua"], [18472, "imgui_demo.lua"], [25757, "imgui_draw.lua"], [30995, "imgui_h.lua"], [33193, "imgui_impl_roblox.lua"], [34396, "imgui_impl_roblox_fonts.lua"], [40799, "imgui_internal.lua"], [43971, "imgui_tables.lua"], [48318, "imgui_widgets.lua"], [58073, "imstb_rectpack.lua"], [58519, "imstb_textedit.lua"], [59760, "imstb_truetype.lua"], [62228, "imstd_minstdio.lua"]] \ No newline at end of file +[[12, "imgui.lua"], [18472, "imgui_demo.lua"], [25757, "imgui_draw.lua"], [30995, "imgui_h.lua"], [33193, "imgui_impl_roblox.lua"], [34411, "imgui_impl_roblox_fonts.lua"], [40814, "imgui_internal.lua"], [43986, "imgui_tables.lua"], [48333, "imgui_widgets.lua"], [58088, "imstb_rectpack.lua"], [58534, "imstb_textedit.lua"], [59775, "imstb_truetype.lua"], [62243, "imstd_minstdio.lua"]] \ No newline at end of file diff --git a/build/ImGui.luau b/build/ImGui.luau index a06b27e..7a13cef 100644 --- a/build/ImGui.luau +++ b/build/ImGui.luau @@ -33379,6 +33379,19 @@ function ImGui_ImplRoblox_UpdateTexture(tex) end end +--- User texture from RGBA8 pixels (e.g. EditableImage:ReadPixelsBuffer()). Returns an ImTextureRef for +--- ImGui.Image / draw_list:AddImage. Ids are never reused, so cached tiles can't show a stale image. +function ImGui_ImplRoblox_CreateTexture(pixels, w, h) + local bd = ImGui_ImplRoblox_GetBackendData() + bd.TextureCount = bd.TextureCount + 1 + bd.Textures[bd.TextureCount] = { Buffer = pixels, Width = w, Height = h } + return ImTextureRef(bd.TextureCount) +end + +function ImGui_ImplRoblox_DestroyTexture(tex_ref) + ImGui_ImplRoblox_GetBackendData().Textures[tex_ref._TexID] = nil +end + --------------------------------------------------------- -- RENDERING (software rasterizer into tile buffers) --------------------------------------------------------- @@ -34383,6 +34396,8 @@ ImGui_ImplRoblox = { SetRenderScale = ImGui_ImplRoblox_SetRenderScale, SetRenderRate = ImGui_ImplRoblox_SetRenderRate, SetOverlay = ImGui_ImplRoblox_SetOverlay, + CreateTexture = ImGui_ImplRoblox_CreateTexture, + DestroyTexture = ImGui_ImplRoblox_DestroyTexture, GetRenderScale = function() return ImGui_ImplRoblox_GetBackendData().Scale end, SetBusyRenderRate = ImGui_ImplRoblox_SetBusyRenderRate, SetProfiling = ImGui_ImplRoblox_SetProfiling, @@ -62836,6 +62851,8 @@ return { ImGuiWindowSettings = ImGuiWindowSettings, ImGuiWindowStackData = ImGuiWindowStackData, ImGui_ImplRoblox = ImGui_ImplRoblox, + ImGui_ImplRoblox_CreateTexture = ImGui_ImplRoblox_CreateTexture, + ImGui_ImplRoblox_DestroyTexture = ImGui_ImplRoblox_DestroyTexture, ImGui_ImplRoblox_GetBackendData = ImGui_ImplRoblox_GetBackendData, ImGui_ImplRoblox_Init = ImGui_ImplRoblox_Init, ImGui_ImplRoblox_NewFrame = ImGui_ImplRoblox_NewFrame, diff --git a/examples/example_roblox/main.client.luau b/examples/example_roblox/main.client.luau index 2622499..4bd370e 100755 --- a/examples/example_roblox/main.client.luau +++ b/examples/example_roblox/main.client.luau @@ -4,6 +4,7 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") local RunService = game:GetService("RunService") local G = require(ReplicatedStorage:WaitForChild("ImGui")) +local ExampleImageViewerData, ExampleImageViewer_DrawOptions, ExampleImageViewer_DrawCanvas = G.ExampleImageViewerData, G.ExampleImageViewer_DrawOptions, G.ExampleImageViewer_DrawCanvas local ImGui, ImVec4, ImGuiConfigFlags, ImGui_ImplRoblox = G.ImGui, G.ImVec4, G.ImGuiConfigFlags, G.ImGui_ImplRoblox @@ -158,6 +159,73 @@ function ImGui.DestroyEmbed(id) if e then e.Frame:Destroy(); embeds[gid] = nil end end +-- Example: Decal Viewer. Loads a decal/image asset's pixels into an imgui texture and shows it in the demo's +-- image viewer (zoom with the wheel, drag to pan, toggleable pixel grid). +local AssetService = game:GetService("AssetService") +local MarketplaceService = game:GetService("MarketplaceService") +local decal = { input = ImGui.NewTextBuffer("", 32), status = "Enter a decal or image id", tex = nil, w = 0, h = 0, loading = false, + viewer = ExampleImageViewerData(), show = true } + +-- A Decal asset wraps an Image asset with a different id; the image is almost always uploaded just before it +-- (same creator, id slightly lower). ponytail: heuristic scan of 10 ids, InsertService:LoadAsset when owned is exact. +local function ResolveImageId(id) + local ok, info = pcall(MarketplaceService.GetProductInfo, MarketplaceService, id) + if not ok then return nil, "GetProductInfo failed: " .. tostring(info) end + if info.AssetTypeId == 1 then return id end -- already an Image + if info.AssetTypeId ~= 13 then return nil, "asset is not a decal or image" end + for cand = id - 1, id - 10, -1 do + local ok2, ci = pcall(MarketplaceService.GetProductInfo, MarketplaceService, cand) + if ok2 and ci.AssetTypeId == 1 and ci.Creator and info.Creator and ci.Creator.Id == info.Creator.Id then return cand end + end + return nil, "couldn't find the decal's image id (try the image id directly)" +end + +local function LoadDecal(id) + decal.loading = true + decal.status = "Loading " .. id .. "..." + task.spawn(function() + local image_id, err = ResolveImageId(id) + if image_id then + local ok, img = pcall(AssetService.CreateEditableImageAsync, AssetService, Content.fromUri("rbxassetid://" .. image_id)) + if ok then + local size = img.Size + local pixels = img:ReadPixelsBuffer(Vector2.zero, size) + img:Destroy() + if decal.tex then ImGui_ImplRoblox.DestroyTexture(decal.tex) end + decal.tex, decal.w, decal.h = ImGui_ImplRoblox.CreateTexture(pixels, size.X, size.Y), size.X, size.Y + decal.viewer.ViewReset = true + decal.status = string.format("Image %d: %dx%d", image_id, size.X, size.Y) + else + decal.status = "Can't load pixels: " .. tostring(img) .. " (EditableImage needs an asset this experience may use)" + end + else + decal.status = err + end + decal.loading = false + end) +end + +local function ShowDecalViewer() + if not decal.show then return end + decal.show = ImGui.Begin("Decal Viewer", decal.show) + ImGui.SetNextItemWidth(ImGui.GetFontSize() * 12) + local enter = ImGui.InputTextWithHint("##id", "decal id", decal.input, 32, G.ImGuiInputTextFlags.CharsDecimal + G.ImGuiInputTextFlags.EnterReturnsTrue) + ImGui.SameLine() + ImGui.BeginDisabled(decal.loading) + if (ImGui.Button("Load") or enter) and not decal.loading then + local id = tonumber(ImGui.TextBufferToString(decal.input)) + if id then LoadDecal(id) else decal.status = "Not a number" end + end + ImGui.EndDisabled() + ImGui.SameLine(); ImGui.TextDisabled(decal.status) + if decal.tex then + ExampleImageViewer_DrawOptions(decal.viewer) + local avail = ImGui.GetContentRegionAvail() + ExampleImageViewer_DrawCanvas(decal.viewer, G.ImVec2(math.max(avail.x, 50), math.max(avail.y, 50)), decal.tex, decal.w, decal.h) + end + ImGui.End() +end + -- Example: a ViewportFrame with a spinning part, edited live from imgui widgets local spin_speed = 1.0 local part_color = ImVec4(0.80, 0.65, 0.97, 1.0) @@ -211,6 +279,8 @@ RunService.Stepped:Connect(function() end PE() + ShowDecalViewer() + if show_viewport_window then show_viewport_window = ImGui.Begin("Viewport", show_viewport_window) spin_speed = ImGui.SliderFloat("speed", spin_speed, 0.0, 5.0)