Skip to content

Commit 2390d4b

Browse files
Merge tag 'rel-emu-rof2'
2 parents 49e8218 + 681a436 commit 2390d4b

8 files changed

Lines changed: 246 additions & 18 deletions

File tree

data/resources/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
## 4/26/2026
2+
3+
### Features
4+
5+
Support for stencil and alpha masking is now available through ImGui to c++ and lua. This allows for
6+
creating more advanced effects like rounded corners on anything or gradient text.
7+
8+
- For more info see [mq/imgui/AlphaMask.h](https://github.com/macroquest/macroquest/blob/master/include/mq/imgui/AlphaMask.h).
9+
- An example is available via `/lua run examples/imgui_mask_demo`
10+
11+
112
## 4/22/2026
213

314
### Bug fixes

include/mq/imgui/AlphaMask.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ enum class AlphaMaskOp : uint8_t
6767

6868
// Begins a new mask layer. Each call auto-assigns the next available stencil bit
6969
// (up to 8 per session). Draw mask image(s) after this call using AddImage or
70-
// AddImageNineSlice - only texels with alpha >= 0.5 write to the stencil.
70+
// with other draw calls - only texels with alpha >= 0.5 write to the stencil.
7171
// Call up to 8 times before BeginMaskedDraw.
7272
MQLIB_OBJECT void CreateMaskLayer(ImDrawList* draw_list);
7373

@@ -110,7 +110,7 @@ MQLIB_OBJECT void EndMaskedDraw(ImDrawList* draw_list);
110110
MQLIB_OBJECT void CreateCoverageMaskLayer(ImDrawList* draw_list, const ImVec2& p_min, const ImVec2& p_max);
111111

112112
// Commits the mask and begins the coverage-masked drawing scope. Draw calls issued
113-
// between here and EndAlphaBlendedDraw are blended using the framebuffer alpha
113+
// between here and EndAlphaMaskedDraw are blended using the framebuffer alpha
114114
// written by the mask textures as per-pixel coverage.
115115
MQLIB_OBJECT void BeginCoverageMaskedDraw(ImDrawList* draw_list);
116116

include/mq/imgui/Widgets.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ inline bool AddTextureAnimation(
150150
return DrawTextureAnimation(drawList, textureAnimation, pos, size, tintColor, borderColor);
151151
}
152152

153-
// Adds a CUITextureInfo to the specified draw list. This is thbe lowest level of the UI texture hierarchy, and represents a full individual texture.
153+
// Adds a CUITextureInfo to the specified draw list. This is the lowest level of the UI texture hierarchy, and represents a full individual texture.
154154
MQLIB_OBJECT bool DrawUITexture(
155155
ImDrawList* drawList,
156156
const eqlib::CUITextureInfo& textureInfo,

src/imgui/ImGuiUtils.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ struct NineSliceImageParams
106106
{
107107
// Image size
108108
ImVec2 p_min; // pixel coordinates of the upper left corner of the image
109-
ImVec2 p_max; // pixel coordiantes of the upper right corner of the image
109+
ImVec2 p_max; // pixel coordiantes of the lower right corner of the image
110110
ImVec2 uv_min; // texture coordinates of the upper left corner of the image
111111
ImVec2 uv_max; // texture coordinates of the lower right corner of the image
112112

src/main/datatypes/MQ2SpellType.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ enum class SpellMembers
7777
SpellGroup,
7878
SubSpellGroup,
7979
Beneficial,
80+
PreventsRegen,
8081
IsActiveAA,
8182
CalcIndex,
8283
NumEffects,
@@ -174,6 +175,7 @@ MQ2SpellType::MQ2SpellType() : MQ2Type("spell")
174175
ScopedTypeMember(SpellMembers, SpellGroup);
175176
ScopedTypeMember(SpellMembers, SubSpellGroup);
176177
ScopedTypeMember(SpellMembers, Beneficial);
178+
ScopedTypeMember(SpellMembers, PreventsRegen);
177179
ScopedTypeMember(SpellMembers, IsActiveAA);
178180
ScopedTypeMember(SpellMembers, CalcIndex);
179181
ScopedTypeMember(SpellMembers, NumEffects);
@@ -1188,6 +1190,11 @@ bool MQ2SpellType::GetMember(MQVarPtr VarPtr, const char* Member, char* Index, M
11881190
Dest.Type = pBoolType;
11891191
return true;
11901192

1193+
case SpellMembers::PreventsRegen:
1194+
Dest.Set(!pSpell->IsBeneficialSpell() && !pSpell->BypassRegenCheck);
1195+
Dest.Type = pBoolType;
1196+
return true;
1197+
11911198
case SpellMembers::IsActiveAA:
11921199
Dest.Set(IsActiveAA(pSpell->Name));
11931200
Dest.Type = pBoolType;

src/plugins/lua/bindings/lua_ImGuiCustom.cpp

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,14 @@ void RegisterBindings_ImGuiCustom(sol::state_view lua, sol::table& ImGui)
5858
// NineSlice image drawing
5959
ImGui.new_usertype<mq::imgui::NineSliceImageParams>(
6060
"NineSliceImageParams",
61-
"WithTextureCoords", &mq::imgui::NineSliceImageParams::WithTextureCoords,
62-
"WithPixelCoords", &mq::imgui::NineSliceImageParams::WithPixelCoords,
63-
61+
"WithTextureCoords", [](const ImVec2& texture_size, const ImVec4& p_margins, std::optional<ImVec2> uv_min, std::optional<ImVec2> uv_max)
62+
{
63+
return mq::imgui::NineSliceImageParams::WithTextureCoords(texture_size, p_margins, uv_min.value_or(ImVec2(0.0f, 0.0f)), uv_max.value_or(ImVec2(1.0f, 1.0f)));
64+
},
65+
"WithPixelCoords", [](const ImVec2& texture_size, const ImVec4& p_margins, std::optional<ImVec2> p_min, std::optional<ImVec2> p_max)
66+
{
67+
return mq::imgui::NineSliceImageParams::WithPixelCoords(texture_size, p_margins, p_min.value_or(ImVec2(0.0f, 0.0f)), p_max.value_or(ImVec2(-1.0f, -1.0f)));
68+
},
6469
"p_min", &mq::imgui::NineSliceImageParams::p_min,
6570
"p_max", &mq::imgui::NineSliceImageParams::p_max,
6671
"uv_min", &mq::imgui::NineSliceImageParams::uv_min,
@@ -76,23 +81,14 @@ void RegisterBindings_ImGuiCustom(sol::state_view lua, sol::table& ImGui)
7681
});
7782
// AddImageNineSlice -> found on ImDrawList bindings
7883

79-
// Masking
84+
// Masking -> found on ImDrawList bindings
8085

8186
lua.new_enum("AlphaMaskOp", std::initializer_list<std::pair<std::string_view, int>>{
8287
{ "Intersect" , static_cast<int>(mq::imgui::AlphaMaskOp::Intersect) },
8388
{ "Union" , static_cast<int>(mq::imgui::AlphaMaskOp::Union) },
8489
{ "Subtract" , static_cast<int>(mq::imgui::AlphaMaskOp::Subtract) },
8590
{ "Complement" , static_cast<int>(mq::imgui::AlphaMaskOp::Complement) },
8691
});
87-
ImGui.set_function("CreateMaskLayer", &mq::imgui::CreateMaskLayer);
88-
ImGui.set_function("BeginMaskedDraw", [](ImDrawList* draw_list, int op)
89-
{
90-
mq::imgui::BeginMaskedDraw(draw_list, static_cast<mq::imgui::AlphaMaskOp>(op));
91-
});
92-
ImGui.set_function("EndMaskedDraw", &mq::imgui::EndMaskedDraw);
93-
ImGui.set_function("CreateCoverageMaskLayer", &mq::imgui::CreateCoverageMaskLayer);
94-
ImGui.set_function("BeginCoverageMaskedDraw", &mq::imgui::BeginCoverageMaskedDraw);
95-
ImGui.set_function("EndCoverageMaskedDraw", &mq::imgui::EndCoverageMaskedDraw);
9692

9793
// Widgets: Utility
9894
ImGui.set_function("HelpMarker", [](const char* text, std::optional<float> width, std::optional<ImFont*> font) { mq::imgui::HelpMarker(text, width.value_or(450), font.value_or(nullptr)); });

src/plugins/lua/bindings/lua_ImGuiUserTypes.cpp

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
#include "pch.h"
1616

1717
#include "eqlib/game/UITextures.h"
18-
#include "mq/imgui/Widgets.h"
18+
#include "mq/imgui/AlphaMask.h"
1919
#include "mq/imgui/ImGuiUtils.h"
20+
#include "mq/imgui/Widgets.h"
2021

2122
#include "imgui/imgui.h"
2223
#include "imgui/imgui_internal.h"
@@ -506,6 +507,17 @@ void RegisterBindings_ImGuiUserTypes(sol::state_view lua)
506507
mq::imgui::AddImageNineSlice(&mThis, tex_ref, image_params, p_min, p_max, tint_col.value_or(IM_COL32_WHITE));
507508
});
508509

510+
imDrawList.set_function("CreateMaskLayer", &mq::imgui::CreateMaskLayer);
511+
imDrawList.set_function("BeginMaskedDraw", [](ImDrawList* draw_list, int op)
512+
{
513+
mq::imgui::BeginMaskedDraw(draw_list, static_cast<mq::imgui::AlphaMaskOp>(op));
514+
});
515+
imDrawList.set_function("EndMaskedDraw", &mq::imgui::EndMaskedDraw);
516+
517+
imDrawList.set_function("CreateCoverageMaskLayer", &mq::imgui::CreateCoverageMaskLayer);
518+
imDrawList.set_function("BeginCoverageMaskedDraw", &mq::imgui::BeginCoverageMaskedDraw);
519+
imDrawList.set_function("EndCoverageMaskedDraw", &mq::imgui::EndCoverageMaskedDraw);
520+
509521

510522
lua.new_usertype<ImGuiTableColumnSortSpecs>(
511523
"ImGuiTableSortSpecsColumn" , sol::no_constructor,
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
-- Demonstrates masked drawing with ImGui.
2+
3+
local mq = require 'mq'
4+
local imgui = require 'ImGui'
5+
6+
local winTex ---@type MQTexture
7+
local logoTex ---@type MQTexture
8+
local classAnim ---@type CTextureAnimation
9+
10+
local isOpen = true
11+
12+
-- window_pieces01.tga: 256x256 sprite sheet
13+
-- A_RecessedBox region: pixel (180,110)-(221,151), 41×41 px
14+
local TEX_W, TEX_H = 256.0, 256.0
15+
local NS_SRC_X1, NS_SRC_Y1 = 180, 110
16+
local NS_SRC_X2, NS_SRC_Y2 = 221, 151
17+
18+
-- A_GaugeBackground region: pixel (108,7)-(208,17) — used in coverage mask Demo B
19+
local GAUGE_UV_MIN = ImVec2(108 / 256, 7 / 256)
20+
local GAUGE_UV_MAX = ImVec2(208 / 256, 17 / 256)
21+
22+
-- Stencil Mask tab state
23+
local stencilComboIdx = AlphaMaskOp.Union + 1
24+
25+
local function loadTextures()
26+
if not winTex then
27+
winTex = mq.CreateTexture('uifiles/default/window_pieces01.tga')
28+
end
29+
if not logoTex then
30+
logoTex = mq.CreateTexture('uifiles/default/EQLS_background_01.tga')
31+
end
32+
if not classAnim then
33+
classAnim = mq.FindTextureAnimation('A_ClassAnim06')
34+
end
35+
end
36+
37+
38+
--
39+
-- Tab 1: Stencil Mask
40+
--
41+
42+
local function drawStencilMaskTab()
43+
imgui.TextWrapped(
44+
'Stencil masking composites two mask layers with a boolean operation, ' ..
45+
'then draws content clipped to the result.\n\n' ..
46+
'Each CreateMaskLayer call begins a new mask layer; shapes drawn ' ..
47+
'afterward become that layer\'s mask. BeginMaskedDraw combines them ' ..
48+
'all with the chosen operation and clips all subsequent draws until ' ..
49+
'EndMaskedDraw.')
50+
51+
imgui.Spacing()
52+
imgui.SetNextItemWidth(200)
53+
stencilComboIdx = imgui.Combo('AlphaMaskOp##stencil', stencilComboIdx,
54+
'Intersect\0Union\0Subtract\0Complement\0\0')
55+
56+
imgui.Spacing()
57+
imgui.Text('Mask Layer 0: 100x100 filled rect (+10, +10) to (+110, +110)')
58+
imgui.Text('Mask Layer 1: circle r=25 centered at (+110, +60)')
59+
imgui.Text('Content: texture drawn over 200x200 canvas')
60+
imgui.Spacing()
61+
62+
local dl = imgui.GetWindowDrawList()
63+
local p = imgui.GetCursorScreenPosVec()
64+
65+
-- Layer 0 — rectangular mask shape
66+
dl:CreateMaskLayer()
67+
dl:AddRectFilled(
68+
ImVec2(p.x + 10, p.y + 10),
69+
ImVec2(p.x + 110, p.y + 110),
70+
IM_COL32(255, 255, 255, 255))
71+
72+
-- Layer 1 — circular mask shape
73+
dl:CreateMaskLayer()
74+
dl:AddCircleFilled(
75+
ImVec2(p.x + 110, p.y + 60),
76+
25,
77+
IM_COL32(255, 255, 255, 255))
78+
79+
-- Apply the chosen boolean operation and draw content through the mask
80+
dl:BeginMaskedDraw(stencilComboIdx - 1)
81+
dl:AddImage(
82+
logoTex:GetTextureID(),
83+
ImVec2(p.x, p.y),
84+
ImVec2(p.x + 200, p.y + 200))
85+
dl:EndMaskedDraw()
86+
87+
-- Reserve layout space so other widgets don't overlap the drawn area
88+
imgui.Dummy(200, 200)
89+
90+
imgui.Spacing()
91+
imgui.Separator()
92+
imgui.TextWrapped(
93+
'Union: content shows where rect OR circle exists\n' ..
94+
'Intersect: only where both overlap\n' ..
95+
'Subtract: rect area minus the circle\n' ..
96+
'Complement: inverts the final combined mask')
97+
end
98+
99+
-- Tab 2: Coverage Mask
100+
-- Demonstrates framebuffer-alpha-based soft masking.
101+
local function drawCoverageMaskTab()
102+
imgui.TextWrapped(
103+
'Coverage masking uses the framebuffer alpha channel as a per-pixel ' ..
104+
'opacity map.\n\n' ..
105+
'CreateCoverageMaskLayer defines the region. Draw the mask shape ' ..
106+
'normally — the alpha of those draws is stored as the mask. ' ..
107+
'BeginCoverageMaskedDraw / EndCoverageMaskedDraw then multiplies ' ..
108+
'content alpha by that stored mask, producing smooth soft edges.\n\n' ..
109+
'Unlike stencil masking, semi-transparent mask shapes produce ' ..
110+
'proportionally faded content rather than hard clip boundaries.')
111+
112+
imgui.Spacing()
113+
114+
local dl = imgui.GetWindowDrawList()
115+
116+
-- Demo A: animated texture silhouette as a coverage mask
117+
-- The class texture's alpha channel (includes transparency in the sprite
118+
-- background) becomes the mask — content fills the texture's silhouette with
119+
-- a rainbow gradient and fades naturally at the sprite's soft edges.
120+
imgui.Separator()
121+
imgui.Text('Demo A — CTextureAnimation alpha as coverage mask')
122+
imgui.Separator()
123+
imgui.TextWrapped('Left = rainbow gradient masked to the textures\'s alpha silhouette\n' ..
124+
'Right = unmasked texture for reference')
125+
imgui.Spacing()
126+
127+
local size = ImVec2(64, 128)
128+
local a0 = imgui.GetCursorScreenPosVec()
129+
130+
-- Write the texture's alpha channel into the coverage mask
131+
dl:CreateCoverageMaskLayer(a0, a0 + size)
132+
dl:AddTextureAnimation(classAnim, a0, size)
133+
134+
-- Draw a rainbow gradient, feathered by the mask
135+
dl:BeginCoverageMaskedDraw()
136+
dl:AddRectFilledMultiColor(a0, a0 + size,
137+
IM_COL32(255, 50, 50, 255), -- top-left: red
138+
IM_COL32(255, 200, 50, 255), -- top-right: yellow
139+
IM_COL32( 50, 200, 255, 255), -- bottom-right: cyan
140+
IM_COL32(180, 50, 255, 255)) -- bottom-left: purple
141+
dl:EndCoverageMaskedDraw()
142+
143+
imgui.Dummy(size)
144+
imgui.SameLine()
145+
imgui.DrawTextureAnimation(classAnim, size)
146+
147+
imgui.Text('Note: Image will not appear if loaded before being ingame!')
148+
149+
-- Demo B: Text as coverage mask
150+
imgui.Spacing()
151+
imgui.Separator()
152+
imgui.Text('Demo B — Text as coverage mask')
153+
imgui.Separator()
154+
imgui.Spacing()
155+
156+
local bW, bH = 200, 36
157+
local b0 = imgui.GetCursorScreenPosVec()
158+
local b1 = ImVec2(b0.x + bW, b0.y + bH)
159+
160+
-- Write the gauge strip's alpha as the coverage mask
161+
dl:CreateCoverageMaskLayer(b0, b1)
162+
dl:AddText(nil, 36, b0, IM_COL32(255, 255, 255, 255), "Hello, World!")
163+
164+
-- Draw a blue-to-green gradient, shaped by the gauge strip's alpha
165+
dl:BeginCoverageMaskedDraw()
166+
dl:AddRectFilledMultiColor(b0, b1,
167+
IM_COL32(255, 50, 50, 255), -- top-left: red
168+
IM_COL32(255, 200, 50, 255), -- top-right: yellow
169+
IM_COL32( 50, 200, 255, 255), -- bottom-right: cyan
170+
IM_COL32(180, 50, 255, 255)) -- bottom-left: purple
171+
dl:EndCoverageMaskedDraw()
172+
173+
imgui.Dummy(bW, bH)
174+
end
175+
176+
local function renderUI()
177+
loadTextures()
178+
if not isOpen then return end
179+
180+
local show
181+
isOpen, show = imgui.Begin('ImGui Masked Drawing Demo', isOpen)
182+
if show then
183+
if imgui.BeginTabBar('##NSDemoTabs') then
184+
if imgui.BeginTabItem('Stencil Mask') then
185+
drawStencilMaskTab()
186+
imgui.EndTabItem()
187+
end
188+
if imgui.BeginTabItem('Coverage Mask') then
189+
drawCoverageMaskTab()
190+
imgui.EndTabItem()
191+
end
192+
imgui.EndTabBar()
193+
end
194+
end
195+
imgui.End()
196+
end
197+
198+
mq.imgui.init('imgui_mask_nineslice_demo', renderUI)
199+
200+
while isOpen do
201+
mq.delay(1000)
202+
end

0 commit comments

Comments
 (0)