How to blur uis on surfaceGUI using BoatBomber's GradientCanvas?

Hi, I plan to use @boatbomber’s GradientCanvas for a project and I wanted to blur the uis (or the canvas) created by Gradient Canvas and i have totally no idea how. I was thinking of using the Box Blur Kernel method, but I dont have a clue how to do that in a script in roblox or even incorporate it into GradientCanvas.

mind telling what boatbombers GradientCanvas is?

I assume you are talking about GitHub - boatbomber/ViewportCanvas? Box blur is basically just a square that moves around the image, changing the centre pixel to the average of those around it. I would recommend using a 2D array with values of each pixel so that it can be traversed quickly. However, if you want the image to be more blurry/have a larger box radius then it gets expensive, fast. To solve this you can do two passes through the image:

  • Row-by-row taking an average of the horizontal neighbours
  • Column-by-column, using the result of the first pass, taking an average of the vertical neighbours

To speed up calculating the sum, you can do the total of the first radius pixels and then add the next pixel and minus the previous one. Then simply divide by radius for the average.

I would recommend reading this article, it explains the concept in a relatively simple manner and compares the results of box, Gaussian and stack blurring. I would also suggest reading Mario Klingeman’s Research: StackBlur for an explanation of how it works.
I’m not sure how clear this is, so let me know if there’s anything I can clarify.

Nope nada, Remember @boatbomber said that ViewportCanvas isnt designed for quick pixel updates and instead recommends GradientCanvas. My project involves quick pixel updates.

@Qinrir , take a look at this: GradientCanvas - Github, This module by the genius boatbomber, uses UIgradients to reduce the number of pixels needed for high quality images.

So for example instead of using 15K+ pixels for an image, which is laggy, Gradient canvas reduces the number of pixels while maintaining quality, doing so, less Pixels (Gui elements) makes it lag less.

Also, so to do the box blur, i have to set the color of the pixels to the average of the pixels surrounding it. uhh, yeah i need clarification on that one @bobbybob2131.

1 Like

So let’s say you want to blur an image with a box blur of radius 1. First, you need to convert it into a 2D array of colour values. For this I will just use single values, in practice they would likely be Color3s. But what is a 2D array? It is an array, with lots of other arrays inside it. For this example we will use the following data:

local pixelData = { -- Array within an array
	{4, 5, 2, 8},
	{1, 4, 5, 5},
	{3, 4, 9, 2},
	{5, 8, 9, 3}
}

To blur a pixel, we need the average of the values around it within radius distance. Lets take pixel (2, 2), which has a value of 4.

To blur it all we need are the adjacent pixels,

Coordinate Value
(1, 1) 4
(1, 2) 5
(1, 3) 2
(2, 1) 1
(2, 2) 4
(2, 3) 5
(3, 1) 3
(3, 2) 4
(3, 3) 9

The total of these is 37, which divided by 9 gives us 4.1111… for the value of our original pixel (2, 2). Not very different, but the change will build up as the rest of the pixels are blurred. For the corner/edge pixels, you will need to do a fraction of the box but the idea is the same.

Alrighty thanks! however im questioning its effectiveness. So if you didnt know, the pixel quality is 25x25, im not sure if its effective… However I do plan to store an array of pixels to get their colors and get their average. What I really need is to get all of the said pixels, maybe i could peak into the gradientCanvas module. This can all be fixed by making a getPixels function within the module :slight_smile: .

After that im going to make a CreateBlur function. Here’s an example code:

function CreateBlur(Pixels: Array, Radius: Number)
  for i, Pixel in pairs(Pixels) do
      local NearestPixels = {}
      table.clear(NearestPixels)
         for i, PixelNear in pairs(Pixels) do
                 if PixelNear ~= Pixel then
                        if (PixelNear.AbsolutePosition - Pixel.AbsolutePosition).magnitude < Radius then
                             table.insert(NearestPixels, PixelNear)
                       end
                 end
         end

       -- The rest of the code adds up the Color3s of the pixels and divides it by Color3.new(9,9,9)
  end
end

That is a rush code but i guess you know where im going.

@bobbybob2131 , I have a problem, I can’t seem to get the nearest pixels around a single pixel, sadly roblox does not support spatial query system for 2D objects. However there is a Raycast2 module, but I need it to look for surrounding pixels. I tried multiple things but i keep getting nil as a result. Like since im using gradientCanvas, i intergrated a GetPixels. That didnt work either. Im going to send you the module code via pm, but promise me you wont steal the code.

Its just not working at all,I tried literally anything and it kept failling.

Issue resolved in DMs (hopefully). For any future readers, the code I used to grab nearby pixels is as follows,

local meanDenominator = BLUR_RADIUS * 9
local nearPixels = table.create(meanDenominator - 1)
local nearIndex = 1

for y = 1, self._height do
	for x = 1, self._width do
		local frame = self._pixels[x][y]

		for i = -BLUR_RADIUS, BLUR_RADIUS do				
			for j = -BLUR_RADIUS, BLUR_RADIUS do
				if j == 0 then continue end -- Starting pixel
				nearPixels[nearIndex] = self._pixels[x + i][y + j]
				nearIndex += 1
			end
		end
	end
end

where self._pixels is a 2D array of Frames (and self._height/self._width are the dimensions of said array) and BLUR_RADIUS is the size of the box to blur (in this case 1). It should also be noted that this is the simplest, but slowest, method and I would recommend referring to the articles in my early post for optimisation tips/alternative methods.
Edit: Added @CoderHusk’s suggestion so memory is only allocated once.

You should probably turn that into a batch array and not use table.insert() and instead do something like self._pixels[stream] = nearPixels.

hey um, i keep getting the error “trying to index nil to number” using the code

function Mirror:Blur(BLUR_RADIUS :number)
	local self: Mirror = self
	
	if BLUR_RADIUS == nil or BLUR_RADIUS == 0 then BLUR_RADIUS = 1 end
	
	local meanDenominator = BLUR_RADIUS * 9
	local Total = meanDenominator - 1
	local nearPixels = table.create(Total)
	local nearIndex = 1
	
	print(nearPixels[1])
	
	
	for y = 1, self._height do
		for x = 1, self._width do
			local frame = self._pixels[x][y]

			for iBlur = -BLUR_RADIUS, BLUR_RADIUS do				
				for jBlur = -BLUR_RADIUS, BLUR_RADIUS do
					if jBlur == 0 then continue end -- Starting pixel
					local XCor = x+iBlur
					local YCor = y + jBlur
					
					
					print(XCor.." and "..YCor)
					nearPixels[nearIndex] = self._pixels[XCor + iBlur][YCor + jBlur]
					nearIndex = nearIndex + 1
				end
			end
		end
	end
end

idk why…

if i print nearpixel[1] it will return nil.

That’s because you are printing it before any values have been assigned, you need to print it after the loops.

even if thats true, the indexing nil to number error is still present for some reason, I tried printing within the loop and its still giving me nil.

This post has still not been resolved… this is gonna take a while…

Hi @bobbybob2131 , Its been a while. My question here is… should the blur be a seperate layer or just the same thing… oh wait no… thats fine I think i semi-already figured it out… almost there.

Behold, the BoxBlur gui effect script that I made:

--[[ This code is a over-simplified test of what i've come up with to do a box blur effect. This does not require a seperate layer of frames (due to that being laggy)
and its built with BoatBomber's GradientCanvas in mind, thus why I used Color3s instead of frames for this whole thing. This may not be perfect and maybe its still possibly laggy
But it depends of how much we can optimize the bazookas out of this. So this changes the color of each individual pixel, that means, it already re-uses the frames that are created
and not using a seperate layer, more guis = more lag, so thats a good thing.
]]--

local Color3Table = {} -- A table that has all of the color3s that the frame has.
local Height = 5 -- Test Frame Height 
local width = 5 -- Test Frame width
local BlurRadius = 9 -- obviously a test radius




local function BlurPixel(x, y) -- Decided to make a function that blurs an individual pixel. Basically the backbone of the whole thing.
    local Pixel = Color3Table[x][y] -- Finds the current Pixel via the use of x and y, sets it under the variable "Pixel".
    local NearestColor3s = {} -- This is the most crucial table of the entirety of this code, stores all the color3s that are within the blur radius.
   
    -- Because of the fact that roblox's Color3 does not support math operations, i'll have to split it into 3 values for the R,G and B values that are needed to set up a Color3 value.
    local R = 0 
    local G = 0 
    local B = 0
    ------------
    local TotalNearPixels = 0 -- Total number of Color3 values that are in the blur radius or simply put it as, the number of Color3s in the "NearestColor3s" table.
    
    -- This piece of code is inspired by the code you sent me, guess its over-simplified, what it does is store the Color3s that are surrounding the Current Pixel into a table.
    -- This doesn't use magnitude or vector2s, just uses the index position of the Color3Table.
    for i = -BlurRadius, BlurRadius do
        local NearPixel = Color3Table[x+i][y+i]
        table.insert(NearestColor3s, NearPixel)
        TotalNearPixels = TotalNearPixels + 1
    end
    ------------------
    
    -- This code adds up the RGB values of all of the Color3s within the NearestColor3s table, adding them up into a total value.
    for i,Color in ipairs(NearestColor3s) do
       R = R + Color.R
       G = G + Color.G
       B = B + Color.B
    end 
    -------------------
    
    -- The following line of code divides the RGB by the number of Near Pixels.
    R = R / TotalNearPixels 
    G = G / TotalNearPixels
    B = B / TotalNearPixels
    ----------------------------
    Color3Table[x][y] = Color3.New(R,G,B) -- This sets the color of the current Pixel.
end


-- This code simply scrolls through the entirety of the frame through the height and width of the frame, then uses the x and y positions (numbers not vectors) to..
-- (continuation) Be used for the BlurPixel function
for y = 1, Height do 
    for x = 1, width do
        BlurPixel(x,y)  
    end
end

This is totally an amalgamation of what i’ve learnt on this article.