Drawing circular meter?

Hi,

I am trying to create a circular meter that can drain over time, etc. I’ve created a version using lots of small rectangles created with surface.DrawTexturedRectRotated, but it is too grainy.

Someone might suggest stencils as an alternative way to do this, but I know nothing about stencils or if they can be used on 2D ui elements. Some guidance would be appreciated. Anyway, here is the grainy version I’m currently doing using rectangles along with the code (drains as the player holds SHIFT)

image



local iply = LocalPlayer()

function circlexy(ang, radius, offx, offy, linewidth)

ang = ang - 90
ang = math.rad( ang )
local x = math.cos( ang ) * (radius - linewidth) + offx
local y = math.sin( ang ) * (radius - linewidth) + offy

return x, y;
end

function DrawPartialCircle( x, y, radius, linewidth, startangle, endangle, aa )


	-- Thanks for getting me started on how to do this python1320 <3
    aa = aa or 1;
    startangle = math.Clamp( startangle or 0, -360, 360 );
    endangle = math.Clamp( endangle or 360, -90, 360 );
     
	draw.NoTexture()
	surface.SetDrawColor( color or color_white )
	 
    if endangle < startangle then
        local temp = endangle;
        endangle = startangle;
        startangle = temp;
    end

    for i=1, endangle, aa do

		local x1, y1 = circlexy( i, radius, x, y, linewidth )
        surface.DrawTexturedRectRotated( x1, y1, linewidth, aa*20, -i );
    end
end

local factor = ( 360 / 100 )
local plystamina = ( (plystamina or 100) * factor )

local sx = ScrW() * ( 9 / 10 )
local sy = ScrH() * ( 9 / 10 )

timer.Create( "StamDrain", 0.01, 0, function()
 
if ( input.IsKeyDown( KEY_LSHIFT ) ) then
plystamina = ( plystamina - 1 )
	if plystamina == 0 then
		plystamina = 100 * factor
	end

end
 
end )

hook.Add("HUDPaint", "circlehudpaint", function()

	DrawPartialCircle( sx, sy, 50, 1, 0, plystamina, 0.5 )

end )

Well, I never saw that someone was using a rotated rectangles for this, this is an overkill to performance, people usually use surface.DrawPoly to draw each segment of circle bar. Some time long ago I wrote an optimized rectangular version that was meant to be used with textures and draws up to 5 triangles only, that’s what you can see in many games. I think I can find this code a bit later.

UPDATE: So, the code is pretty complex, but it should work with textures

local function DrawRadialRect(x, y, width, height, fraction, fractionOffset, counterClockWise)
	fraction = math.Clamp(fraction, 0, 1)
	fractionOffset = math.Clamp(fractionOffset or 0, 0, 1) - 0.25
	counterClockWise = counterClockWise or false

	if (fraction == 1) then
		surface.DrawTexturedRect(x, y, width, height)
		return
	end
	if (fraction == 0) then
		return
	end
	local startAngle = 2 * math.pi * ((fractionOffset + 0.5) % 1 - 0.5)
	local endAngle = counterClockWise
		and 2 * math.pi * ((fractionOffset - fraction + 0.5) % 1 - 0.5)
		or 2 * math.pi * ((fractionOffset + fraction + 0.5) % 1 - 0.5)
	local cornerAngleOffset = 0.5 * math.pi - math.atan(width / height)
	local cornerAngles = {
		cornerAngleOffset,
		math.pi - cornerAngleOffset,
		math.pi + cornerAngleOffset,
		2 * math.pi - cornerAngleOffset
	}
	local vertices = {}
	local deltaAngle = counterClockWise
		and (startAngle - endAngle) % (2 * math.pi)
		or (endAngle - startAngle) % (2 * math.pi)
	local startVertexIndex = 0
	local vertexIndex = 0
	local maxDeltaCornerAngle = 0
	for i = 1, 4 do
		local deltaCornerAngle  = counterClockWise
			and deltaAngle - (cornerAngles[i] + startAngle) % (2 * math.pi)
			or deltaAngle - (cornerAngles[i] - startAngle) % (2 * math.pi)
		if (deltaCornerAngle > 0) then
			vertexIndex = vertexIndex + 1
			local coordinateIndex = counterClockWise and 5 - i or i
			local coordinateU = math.floor((2 + coordinateIndex) / 2 % 2)
			local coordinateV = math.floor((1 + coordinateIndex) / 2 % 2)
			vertices[vertexIndex] = {
				x = x + width * coordinateU,
				y = y + height * coordinateV,
				u = coordinateU,
				v = coordinateV
			}
			if (deltaCornerAngle > maxDeltaCornerAngle) then
				maxDeltaCornerAngle = deltaCornerAngle
				startVertexIndex = vertexIndex
			end
		end
	end
	local indices = { 1, 2, 3, 4, 5 }
	for i = 1, vertexIndex do
		indices[i] = (i + startVertexIndex - 2) % vertexIndex + 1
	end
	local centerX = x + 0.5 * width
	local centerY = y + 0.5 * height
	local startAngleRectFactor = math.max(
		math.abs(math.cos(startAngle)) * 2 / width,
		math.abs(math.sin(startAngle)) * 2 / height
	)
	local startAngleOffsetX = math.cos(startAngle) / startAngleRectFactor
	local startAngleOffsetY = math.sin(startAngle) / startAngleRectFactor
	local endAngleRectFactor = math.max(
		math.abs(math.cos(endAngle)) * 2 / width,
		math.abs(math.sin(endAngle)) * 2 / height
	)
	local endAngleOffsetX  = math.cos(endAngle) / endAngleRectFactor
	local endAngleOffsetY  = math.sin(endAngle) / endAngleRectFactor
	vertices[vertexIndex + 1] = {
		x = centerX + endAngleOffsetX,
		y = centerY + endAngleOffsetY,
		u = 0.5 + 0.5 * endAngleOffsetX * 2 / width,
		v = 0.5 + 0.5 * endAngleOffsetY * 2 / height
	}
	local centerVertex = {
		x = centerX,
		y = centerY,
		u = 0.5,
		v = 0.5
	}
	local edgeVertex = {
		x = centerX + startAngleOffsetX,
		y = centerY + startAngleOffsetY,
		u = 0.5 + 0.5 * startAngleOffsetX * 2 / width,
		v = 0.5 + 0.5 * startAngleOffsetY * 2 / height
	}
	for i = 1, vertexIndex + 1 do
		local cornerVertex = vertices[indices[i]]
		if (counterClockWise) then
			surface.DrawPoly({ cornerVertex, edgeVertex, centerVertex })
		else
			surface.DrawPoly({ centerVertex, edgeVertex, cornerVertex })
		end
		edgeVertex = cornerVertex
	end
end

-- example 
local textureId = surface.GetTextureID("effects/wheel_ring")
hook.Add("HUDPaint", "circlehudpaint", function()
	surface.SetDrawColor(color_white)
	surface.SetTexture(textureId)
	DrawRadialRect(64, 64, 128, 128, (CurTime() * 0.25) % 1)
end)

Thank you VERY much for this.

I will post a quick update here with some issues I’m having- maybe you or someone else will know what to do.

1.) I’m trying this on a custom vtf/vmt I made in VTFEdit, but it appears to be glitching with the render order of the world or something unless I’m looking at certain parts of the map (usually up or towards the skybox)
The intended look is for the circle to be white. Here is non-working vs working (when looking up):

Glitch occurring:

Glitch not occurring:

2.) My second problem is that drawing the rect in dimensions other than the dimensions of the original vtf causes some pixelation (ex: drawing a vtf that is 128x128 in a rectangle that is 64x64). Is there any file format that will allow VTFs to be resized without pixelation?

Thank you all

I can’t tell much without code, show me the .vmt file and also how you draw and apply this material. Generally UI materials are meant to derive from UnlitGeneric shader or something other that doesn’t account for lighting.

I didn’t quite get what are you trying to do, doesn’t making texture the same size as rectangle solving your problem? Then you might try enabling mipmap generation for .vtf.

Changing the VMT to UnlitGeneric fixed it for me. Thank you for that.

As for rescaling:
I am basically using your provided code to draw the rectangle.
Creating it same size would solve the problem, but I want my gamemode UI to scale up/down for different screen sizes and thus I’d prefer to only have to make one vtf and have it resizable.

Update:
However I’ll note that it’s actually not so bad right now- seems like it rescales decently. If there’s some way to make it even better that’d be cool but this is not as urgent of an issue anymore.