Jump to content
  • entries
    941
  • comments
    5,894
  • views
    865,889

Idea Fell Apart


Josh

2,543 views

 Share

At 100% scaling this image appears correctly:

blogentry-1-0-32192800-1468265046.png

 

At 200% scaling it falls apart. The line points are in fact scaled correctly, but they are not surrounding the shape as intended:

blogentry-1-0-34013200-1468265096.png

 

So I think instead of converting the coordinate system back and forth between scaled and non-scaled coordinates, the creation function needs to multiply the coordinates by the scaling factor. That means if you create a 70x30 pixel widget and the GUI is using a 200% scaling factor, it will actually create a 140x60 pixel widget instead. However, the little issues like what is pictured above will go away.

 

This sucks though because if you do this, you will get wrong results:

gui:SetScale(2)
local widgetA = Widget:Create(0,0,200,20,gui)
local widgetB = Widget:Create(0,0,200, widgetA:GetPosition().y + widgetA:GetHeight(),gui )

 

widgetB would be created at a Y position of 80 (20 * 2 * 2)

 

I fear whatever I implement will simply get ignored by script programmers and they will never test against different DPI scales.

 Share

9 Comments


Recommended Comments

Can't widgetA:GetPosition() and widgetA:GetHeight() just divide their result by the scale factor? Any input is multiplied by scale, any output is divided by it.

Link to comment

That's the cause of the incorrect line drawing that is technically correct, just not what I wanted.

Link to comment

What does the math look like? GPU rasterizers implement techniques to specifically avoid this type of problem since you would otherwise end up seeing gaps between triangles through the same types of scaling.

Link to comment

I took a page out of Aggror's and use Anchors for positioning and then I have a "units" for x,y,w,h. Also, when using images, I believe you have to have a system where you can have it swap what detailed image is used for a range of resolutions given what you've just pointed out. You can't pick 1 image resolution and have it scale nicely to any screen resolution.

 

The hardest part about using "units" is that the values don't really make much sense and it's hard to position/scale things but in my opinion that's why placing and scaling in a wysiwyg editor is ideal for GUI.

 

This is my entire gui code for anchor/position/scale (it also alters with keys when an element is selected), but all of the things you're running into is why users need a GUI system that does all of this for them. Because it's a pain in the *** to do it on our own smile.png. It's also why GUI's are entire libraries of their own. A lot goes into making a good system.

 

if ui ~= nil then return end

ui = {}

ui.selectedElement = nil

ui.Anchors = {}
ui.Anchors.TopLeft = 0
ui.Anchors.TopCenter = 1
ui.Anchors.TopRight = 2
ui.Anchors.RightCenter = 3
ui.Anchors.BottomRight = 4
ui.Anchors.BottomCenter = 5
ui.Anchors.BottomLeft = 6
ui.Anchors.LeftCenter = 7
ui.Anchors.Center = 8

ui.anchorText = function(anchor)
if anchor == 0 then return "Top Left" end
if anchor == 1 then return "Top Center" end
if anchor == 2 then return "Top Right" end
if anchor == 3 then return "Right Center" end
if anchor == 4 then return "Bottom Right" end
if anchor == 5 then return "Bottom Center" end
if anchor == 6 then return "Bottom Left" end
if anchor == 7 then return "Left Center" end
if anchor == 8 then return "Center" end
end

-- look at key presses to change the position/scale/anchor settings for given element
ui.update = function()
if ui.selectedElement == nil then return end

local window = Window:GetCurrent()
local moveScale = 0.001

-- position
if window:KeyDown(Key.Left) then
ui.selectedElement.x = ui.selectedElement.x - moveScale
end
if window:KeyDown(Key.Right) then
ui.selectedElement.x = ui.selectedElement.x + moveScale
end
if window:KeyDown(Key.Down) then
ui.selectedElement.y = ui.selectedElement.y + moveScale
end
if window:KeyDown(Key.Up) then
ui.selectedElement.y = ui.selectedElement.y - moveScale
end

-- scale
if window:KeyDown(Key.A) then
ui.selectedElement.width = ui.selectedElement.width - moveScale
end
if window:KeyDown(Key.D) then
ui.selectedElement.width = ui.selectedElement.width + moveScale
end
if window:KeyDown(Key.W) then
ui.selectedElement.height = ui.selectedElement.height + moveScale
end
if window:KeyDown(Key.S) then
ui.selectedElement.height = ui.selectedElement.height - moveScale
end

-- anchoring
if window:KeyDown(Key.D1) then
ui.selectedElement.anchor = ui.Anchors.TopLeft
end
if window:KeyDown(Key.D2) then
ui.selectedElement.anchor = ui.Anchors.TopCenter
end
if window:KeyDown(Key.D3) then
ui.selectedElement.anchor = ui.Anchors.TopRight
end
if window:KeyDown(Key.D4) then
ui.selectedElement.anchor = ui.Anchors.RightCenter
end
if window:KeyDown(Key.D5) then
ui.selectedElement.anchor = ui.Anchors.BottomRight
end
if window:KeyDown(Key.D6) then
ui.selectedElement.anchor = ui.Anchors.BottomCenter
end
if window:KeyDown(Key.D7) then
ui.selectedElement.anchor = ui.Anchors.BottomLeft
end
if window:KeyDown(Key.D8) then
ui.selectedElement.anchor = ui.Anchors.LeftCenter
end
if window:KeyDown(Key.D9) then
ui.selectedElement.anchor = ui.Anchors.Center
end
end

ui.draw = function(context)
if ui.selectedElement == nil then return end

local pos = ui.getPosition(ui.selectedElement)
local scale = ui.getScale(ui.selectedElement)

context:DrawText("Selected ui element: "..ui.selectedElement.name, 0, 0)
context:DrawText(string.format("x: %s, y: %s, w: %s, h: %s, anchor: %s", ui.selectedElement.x, ui.selectedElement.y, ui.selectedElement.width, ui.selectedElement.height, ui.anchorText(ui.selectedElement.anchor)), 0, 20)
end

ui.selectElement = function(e)
ui.selectedElement = e
end

ui.pointOverElement = function(e)
local pos = ui.getPosition(e)
local scale = ui.getScale(e)
local window = Window:GetCurrent()
local mpos = window:GetMousePosition()

if mpos.x < pos.x then return false end
if mpos.x > pos.x + scale.width then return false end
if mpos.y < pos.y then return false end
if mpos.y > pos.y + scale.height then return false end

return true
end

ui.getPosition = function(e)
local res = {}
res.width = context:GetWidth()
res.height = context:GetHeight()

return ui.GetPositionInPixels(e.anchor, e.x, e.y, res.height)
end

ui.getScale = function(e)
local res = {}
res.width = context:GetWidth()
res.height = context:GetHeight()

return ui.GetSizeInPixels(res, e.width, e.height)
end

ui.GetPositionInPixels = function(anchor, x, y, resHeight)
local pos = {}

pos.x = ui.XUnitsToPixels(anchor, x, resHeight)
pos.y = ui.YUnitsToPixels(anchor, y, resHeight)

return pos
end

ui.GetSizeInPixels = function(res, elementWidth, elementHeight)
local size = {}

size.width = ui.WidthUnitsToPixels(res, elementWidth)
size.height = ui.HeightUnitsToPixels(res, elementHeight)

return size
end

ui.WidthUnitsToPixels = function(res, eWidth)
local size = 0

if res.height < res.width then size = res.height else size = res.width end

return eWidth * size
end

ui.HeightUnitsToPixels = function(res, eHeight)
local size = 0

if res.height < res.width then size = res.height else size = res.width end

return eHeight * size
end

ui.XUnitsToPixels = function(anchor, x, resHeight)
return ui.GetAnchorPoint(anchor).x + (x * resHeight)
end

ui.YUnitsToPixels = function(anchor, y, resHeight)
return ui.GetAnchorPoint(anchor).y + (y * resHeight)
end

ui.GetAnchorPoint = function(anchor)
local p = {}
local context = Context:GetCurrent()
local width = context:GetWidth()
local height = context:GetHeight()

if anchor == ui.Anchors.TopLeft then
p.x = 0
p.y = 0
elseif anchor == ui.Anchors.TopCenter then
p.x = (width / 2)
p.y = 0
elseif anchor == ui.Anchors.TopRight then
p.x = width
p.y = 0
elseif anchor == ui.Anchors.RightCenter then
p.x = width
p.y = (height / 2)
elseif anchor == ui.Anchors.BottomRight then
p.x = width
p.y = height
elseif anchor == ui.Anchors.BottomCenter then
p.x = (width / 2)
p.y = height
elseif anchor == ui.Anchors.BottomLeft then
p.x = 0
p.y = height
elseif anchor == ui.Anchors.LeftCenter then
p.x = 0
p.y = (height / 2)
elseif anchor == ui.Anchors.Center then
p.x = (width / 2)
p.y = (height / 2)
end

return p
end

Link to comment

Well, basically the button drawing code draws a line on the right side and bottom of the shape. When that coordinate is scaled up 200%, it becomes the coordinate one pixel away from the full width and height.

 

If I draw a pixel at (9,0) when that is scaled up to 200% it becomes (18,0) but the left-most pixel of the widget would be at position 19 (20 wide, base 0). That's exactly why the black lines in the second image above appear to be off by one pixel. If you think of each upscaled pixel as a 2x2 block it makes sense.

 

I think I am going to make drawing commands always use the global (scaled) coordinates, while creation of the widgets will continue to use the local coordinates. That way you only really need to understand this stuff if you are writing new widget scripts.

 

Speaking of those widget scripts, I would really like to see what new widgets you are able to come up with. I know it's only been out for a couple of days but if you have your own GUI and are considering moving your code into this framework, it would really help to get your feedback and figure out what you need as early as possible.

 

I suspect the simplification of the Steam UI (all controls now use simple background gradients with no borders) was done in order to support variable DPI settings and avoid the line width issue altogether.

Link to comment

I think that's why you can't use %'s with this stuff. I won't put it in our game but one of the more complex controls that we have is the list view control for the high score. I can try this system out in a test project with that.

Link to comment

If you choose to go the non-global route, why not just use an abstract coordinate shared vertex between lines segments? Then you would only have 4 vertices instead of 8, and this issue would be avoided. Then you just offset the top/left by one or the bottom/right by one.

Link to comment

If you choose to go the non-global route, why not just use an abstract coordinate shared vertex between lines segments? Then you would only have 4 vertices instead of 8, and this issue would be avoided. Then you just offset the top/left by one or the bottom/right by one.

I do not understand this.

Link to comment

Each line segment is made up of two vertices (pixels) right now, so you have 8 vertices. If you share the vertices between line segments, then you will never have gaps. You need to treat the vertex positions (such as (0,9)) as continuous values rather than discrete values.

 

?id=722847405&fileuploadsuccess=1

 

One way to simplify this is to offset the coordinates by (.5,.5), as this is what GPUs have done in the past. The blue rectangle are the offset coordinates. The yellow fills are to indicate that that pixel is filled by the top/left. The red indicates the bottom/right. It's not super important for 3D, but for 2D UI it makes more sense.

 

Rules:

  • We will take the floor of the filled in pixel value except at the end of the line segment.
  • If the line segment is the bottom or the right, we will take the ceiling.

It's mathematically impossible for gaps or overlaps to form with this set of rules. For rasterizing 3D triangles, barycentric coordinates are used in order to generalize these rules, but the same idea applies. Either way, you get crisp, accurate edges. smile.png

Link to comment
Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...