Optimization of a 2D Raycast Renderer

Hello Fellows, I need a lil’ help getting this renderer working faster and better.

The idea is to lower the amount of loops in the program and get the grid of pixels that the screen is, bigger. Currently the limit I have it for decent running is at a 25 FPS and a 40x40 grid screen.

Game: 2D Raycast Renderer - Roblox
RBXL File:–

Beware of some bizzare variable names…

Client Renderer Code (Inside a SurfaceGui in PlayerGui)
--scripted by GreekForge
local rs = game:GetService("RunService")
local lib = require(script.Parent.Parent:WaitForChild("Library")) --cuts down on other uses, isn't involved in rendering
local coms = game.ReplicatedStorage.Remotes.Simple

local gui = script.Parent
local pFold = gui.Pixels --folder that contains the pixels

local on = false
local loading = false
local grid = 40 --50 is a lil mucho
local fovM = 0.6 -- 0.5 is about 90 degree fov

local pixel = Instance.new("TextLabel")
pixel.Text = "" --lmao
pixel.Size = UDim2.new(0, (800/grid), 0, (800/grid)) --800 is the size of the surfaceGui, I should just reference it tbh
pixel.BackgroundColor3 = Color3.fromRGB(170, 0, 0)
pixel.BorderSizePixel = 0

--What we shall use to render, the MINIMAP si (on the server)
local mach = workspace.MinimapMachine
local minMap = mach.MiniMap
local user = minMap.User --this is the lil part in the minimap machine

local function generatePixels()
	loading = true
	coroutine.wrap(function()
		for x=0, (grid-1) do
			coroutine.wrap(function()
				for y=0, (grid-1) do
					local p = pixel:Clone()
					p.Position = UDim2.new(0, x*(800/grid), 0, y*(800/grid))
					p.Name = "Pixel("..x..","..y..")"
					p.Parent = pFold
					wait()
				end
			end)()
			--wait()
		end
		repeat wait() until #pFold:GetChildren() >= (grid*grid)
		loading = false
	end)()
end

local function clear() --creates the "sky box"
	for i=0, (grid-1) do
		local gH = grid/2
		for j=0, (grid-1) do
			local nam = "Pixel("..i..","..j..")"
			local pix = pFold:FindFirstChild(nam)
			if j > gH then
				pix.BackgroundColor3 = Color3.fromRGB(150, 100, 100)
			else
				pix.BackgroundColor3 = Color3.fromRGB(150, 150, 255)
			end
		end
	end
end

coms.OnClientEvent:Connect(function(pass) --si, wasteful I guess
	if pass == "On" then
		on = true
	elseif pass == "Off" then
		on = false
	end
end)

coroutine.wrap(function() --sends the keys used to the server "Bleh"
	while wait() do
		if on then
			local kys = lib.getKeys()
			coms:FireServer(kys) --means the part is server side, possibly not a good idea
			wait(0.05)
		end
	end
end)()

while wait(1/25) do --wait seems to have a cap :T I think 30 Mhz
	if on then --if not using, why render?
		if #pFold:GetChildren() == 0 and not loading then
			generatePixels() --gotta have a screen
		end
		if not loading then
			clear()
			local uCF = user.CFrame
			local edge1 = -uCF.LookVector + (uCF.UpVector*fovM)
			local edge2 = -uCF.LookVector - (uCF.UpVector*fovM)
			
			for i=0, (grid-1) do --its grid -1 because of how its generated
				local dir = edge2:Lerp(edge1, ((i+0.5)/grid)) --starts on edge1 and as we travel on the x axis of the gui we get closer to edge2
				local hit, pos = workspace:FindPartOnRayWithIgnoreList(Ray.new(user.Position, dir*10), {user, user.Face})
				if hit then
					local gH = grid/2 --gridHalf
					local m1 = 1 - math.clamp(((pos - uCF.Position).Magnitude)/8, 0, 1) --simply the ratio of how far
					local m2 = math.floor((m1 * grid)/2) --gets the pixels needed to fill, by half
					local s1 = gH - m2 --where it begins shading the walls
					if m2 ~= 0 then
						for j=s1, math.clamp(((gH+m2)-1), (gH+1), (grid-1)) do --renders
							local nam = "Pixel("..i..","..j..")"
							local pix = pFold:FindFirstChild(nam)
							if pix then
								local perc = ((pos - user.Position).Magnitude / 3.5) or 1 --how "bright" the colors are
								pix.BackgroundColor3 = Color3.new(hit.Color.r*perc, hit.Color.g*perc, hit.Color.b*perc)
							end
						end
					end
				end
			end
		end
	end
end

Picture of device:

5 Likes

If you’re doing a Wolfenstein-style renderer, you don’t need 40x40 frames - you could just use 40 columns and resize them based on the distance from the player to the wall :smiley:

4 Likes

Never went into details, but how did they render brick walls correctly?

While technically a good idea, my next step is to texture these bad bois. Columns of changing height would not be able to reflect any changing texture pattern.

@nooneisback
They rendered bricks by determining what part of the wall the render ray touched and then consulting with a texture map that returns the column of colors needed to texture that section of the wall.
Although I’m not sure on the render method, on it being horizontally based or vertically based, I do know thats how others have done it and also what I intend to do.

I just need to optimize the renderer so that it can handle more calculations per second.

I probably should’ve been clearer on what I wanted to be reviewed.

The local script that I’ve pasted into the post is what I want optimized, as it takes the brunt of the rendering.

Surely you’d be able to do this within each of the frames? Texture check per column.

Add a texture to each frame? Yeah, sounds good at first. But the texture you’ll see won’t distort according to one-point perspective. It also would bring issues with resizing the textures, what happens when you’re really close? Or really far?

Working with the idea of the 40 column suggestion - which is something wolfenstein3d used, see here - you are given a unique advantage. Working from left to right using the suggested method, you’re able to see where on a specific part you’ve hit, take an existing texture that you’re trying to apply and figure out how it will warp to fit that specific segment of the column. Don’t forget, its a UI object, you have access to ImageLabels and all the weirdness that comes with them!

Additionally, the floor and ceiling don’t need to be rendered per frame - unless you’re trying to texture them. But even then, using the same vertical method, we can see an easier method. Instead of 40 frames per column, we can make it 3, then look at applying the texture in the same manner.

To take it to another level, we know the user doesn’t move when theres no inputs, therefore theres no need to actually do an update! Should save some frames when you’re staying still, which would come in handy if you ever have enemy/world object sprites in-game.

I’d personally move this to a local script, means you’ll have a little more immediate control over it! This being said, if you’re running on the server, rather than throwing everything into a while wait() do call you could try running on RunService.Heartbeat:connect(func), or RunService.Stepped:connect(func). These are both things that it looks like you planned to do earlier on in the code, but stepped away from doing.

With everything here taken into account, that should give you some significant performance increases!
You’ll also need to look at how you’re moving the User part, as the raycasting logic seems slightly off as its possible to clip through other parts (try holding W and S down, then wiggling against a wall).

All in all, you’re on the right track though!

I don’t totally think were on the same page for a 40 column approach.

What I believe @Elttob was saying was to have 40 Frames total that resize (change UDim Y) according to distance, this is an interesting idea but I wouldn’t know how to texture the Frame once it’s resized.

What I’m understanding from your video is that there are columns, yes, but they aren’t technically resized as simply more pixels are colored. As I’m doing:

for i=0, (grid-1) do --on a column by column basis (as i is the x axis for pixels)
	local dir = edge2:Lerp(edge1, ((i+0.5)/grid))
	local hit, pos = workspace:FindPartOnRayWithIgnoreList(Ray.new(user.Position, dir*10), {user, user.Face})
	if hit then
		local gH = grid/2 
		local m1 = 1 - math.clamp(((pos - uCF.Position).Magnitude)/8, 0, 1) --simply the ratio of how far
		local m2 = math.floor((m1 * grid)/2)
		local s1 = gH - m2 --where it begins shading the walls
		if m2 ~= 0 then
			for j=s1, math.clamp(((gH+m2)-1), (gH+1), (grid-1)) do --renders on a row by row basis
				local nam = "Pixel("..i..","..j..")"
				local pix = pFold:FindFirstChild(nam)
				if pix then
					local perc = ((pos - user.Position).Magnitude / 3.5) or 1
					pix.BackgroundColor3 = Color3.new(hit.Color.r*perc, hit.Color.g*perc, hit.Color.b*perc)
				end
			end
		end
	end
end

Would 3 frames per column really help me achieve detailed textures? How would I apply a texture on only 3 pixels of my screen?

Note: I already have it in a local script, and I moved away from attaching it to run service because it could have caused my game to either crash or heavily slow (basically I’m cautious).

You could have two static frames for the background meet in the middle to represent the sky and floor with a z index that always places them behind other frames. You now no longer need to worry about a huge amount of updates.

Let’s say the width of the screen has 40 unique points calculated to generate it’s image, these are represented by the 40 frames of changing size. Because we’ve moved to resizing, we no longer need to worry about updating Pixel(X,Y), but instead Column(N). We no longer need to do some unnecessary updating here.

Before we move to texturing, start with this, instead of updating 40x40, we update 1x40

Alright, I’ll try that. But I still don’t understand two things:

How does having two frames as background lower update amounts? I’ll still need to clear at the beginning of each frame (at least in my system), so why not just take care of clearing the frame by simply having them a certain color depending on Y position. Doesn’t increase the latency by much compared to simply setting them to black.
Clearing Code:

local function clear() --creates the "sky box"
	for i=0, (grid-1) do
		local gH = grid/2
		for j=0, (grid-1) do
			local pix = pTab[i][j] --I've changed my pixel accessing (Its much faster now)
			if j > gH then --0 at bottom
				pix.BackgroundColor3 = Color3.fromRGB(150, 100, 100) --floor
			else
				pix.BackgroundColor3 = Color3.fromRGB(150, 150, 255) --sky
			end
		end
	end
end

How do you texture a column compared to just my pixels?


EDIT: I’ve used you’re recommendations on my current one (though not the column system) and it’s much faster on the new version if you wanna check

1 Like

Thats so much smoother now! I’m maintaining a constant 60fps with that.
For texturing, you’ll need to figure out how much of the wall is being shown within the frame horizontally, as the distance is impacting what you can see greatly.

I have no actual experience with texturing, let me preface by saying that. However, I can offer you my best guess at how to go around handling this.
Firstly, heres a quick picture of what we’ll be going through.


Here we have a basic brick (if you squint) texture and 2 ways you can attempt to translate that onto a wall. Columns here are not correct, and should only be considered for visual cue.
In our first example, we take our texture and just chop it into X amount of columns, and resize all of those segments vertically in order to create a “textured” wall. This would look fine if the wall is directly infront of you, unfortunately we can also see that this will look incredibly stretched when put on a wall at an angle closer to parralel.

How can we go about fixing this?
Well, we first need to figure out what is happening horizontally. We’ve decided that our height is going to be a certain size based on distance, so we should be able to work out how wide our segment will be. This will greatly change based on;

  • distance
  • angle to the camera
  • field of view and its corrections.

I cannot offer you this equation, as I do not yet have it. I can tell you however, that using this equation will allow you to then figure out how much of the brick texture should be used. Rather than taking each segment and changing its size vertically, we are actually squishing the image into a smaller size than its default.

Because we can see that our first process results in a stretching of our texture, the easiest way to counter that is to squish it down. As you can see in our second texutre example, it actually looks like its been mapped onto something with depth, even though its just squished and shrunk.

There are likely far better explanations of this, and I’m sorry I can’t offer anything more indepth, but this is something completely achievable using ImageLabels and playing with the ImageRectOffsets and ImageRectSize!

My idea was simply determining what texture we needed to use by using the spot where the ray hit the brick/part to cross-reference to the texture. Though I will take that into consideration.

I mean, that is what you’ll be doing, but you’ll then need to deconstruct it slightly (What we’re attempting to do in my example) and reconstruct it in 2d space to give it “depth”. Also, sorry I necro-bumped this post! I didn’t even realise it’d be 2 months since last post! Was just v interested :slight_smile:

Well, simply hardcoding a texture into the machine seems to work well. Though I’ll still need to check it to ensure that the proposed issue doesn’t mess with what we have.

Don’t worry about necro-bumping it, I got the info I needed.

What I got:

Purpose of using the Image labels is that you wittle the amount of objects you’re using down with them, no need for frames anymore as they could instead be textured! Looking cool though!

1 Like

Yeah, that’s a good idea. But I want to stay true to the wolfenstein-3D idea of pixels. In any case I’m not gonna take this any further because I’ve got what I wanted. A “textured” 2D Render Raycaster. Thanks for your help.

1 Like