Simple and fast 2D Raycast Renderer

Hey all. I have recently been looking at 2D Raycast Renderers on roblox, and I’ve been noticing a common issue with most peoples’ creations. The main mistake I often see when people do these, is over engineering them. And I thought I’d just make a much more simple, optimized, fast, and higher quality raycast renderer that is free for use!

Screenshots of the renderer in use

image

If you don’t know what a 2D Raycast Renderer is, it is a 2D top-down map that simulates a 3D environment by casting a bunch of rays from a point in the 2D map, with columns that will change size and colour to give the illusion of depth.

Hope you guys find a use for this.
Enjoy!

Engine source code
local Gui = script.Parent
local MainFrame = Gui:WaitForChild("MainFrame")
local Canvas = MainFrame:WaitForChild("Canvas")

local RS = game:GetService("RunService")

local SettingsModule = require(Gui:WaitForChild("MainSettings"))

local CameraPart = SettingsModule.CameraPart
local LevelWorkspace = SettingsModule.CurrentMap


local FrameSizeX = (1 / SettingsModule.Resolution)

local SpritesTable = {}

local function GenerateFrames() 
	-- Generate columns
	for i = 1, SettingsModule.Resolution do
		local FramePosX = 1 / SettingsModule.Resolution * (i - 1)
		
		-- Main column
		local Frame = Instance.new("Frame")
		Frame.ClipsDescendants = true
		Frame.LayoutOrder = i
		Frame.Name = "Frame" .. i
		Frame.BorderSizePixel = 0
		Frame.AnchorPoint = Vector2.new(0, SettingsModule.CameraHeight)
		Frame.Size = UDim2.fromScale(FrameSizeX, 0)
		Frame.Position = UDim2.fromScale(FramePosX, 0.5)

		Frame.Parent = Canvas
	end
	
end

local function ClearCanvas() -- Clears any old frames
	for i, Frame in ipairs(Canvas:GetChildren()) do
		if Frame:IsA("GuiObject") then
			Frame:Destroy()
		end
	end
end


function NormalToFace(normalVector, part)
	
	local function GetNormalFromFace(part, normalId)
		return part.CFrame:VectorToWorldSpace(Vector3.FromNormalId(normalId))
	end

	local TOLERANCE_VALUE = 1 - 0.001
	local allFaceNormalIds = {
		Enum.NormalId.Front,
		Enum.NormalId.Back,
		Enum.NormalId.Bottom,
		Enum.NormalId.Top,
		Enum.NormalId.Left,
		Enum.NormalId.Right
	}    

	for _, normalId in pairs(allFaceNormalIds) do
		-- If the two vectors are almost parallel,
		if GetNormalFromFace(part, normalId):Dot(normalVector) > TOLERANCE_VALUE then
			return normalId -- We found it!
		end
	end

	return nil -- None found within tolerance.

end

local function UpdateFrames() -- Render the picture
	local Count = -SettingsModule.Resolution / 2
	
	-- Frames
	for i, Frame in ipairs(Canvas:GetChildren()) do
		if Frame:IsA("Frame") then
			
			local Params = RaycastParams.new()
			Params.FilterType = Enum.RaycastFilterType.Whitelist
			Params.FilterDescendantsInstances = LevelWorkspace:GetChildren()
			
			local ViewRayOrigin = CameraPart.Position
			local RayAngle = CFrame.Angles(0, -math.rad((Count * SettingsModule.FieldOfView) / SettingsModule.Resolution), 0)
			local ViewRayDirection = (CameraPart.CFrame * RayAngle).LookVector * SettingsModule.RenderDistance
			
			local ViewRay = workspace:Raycast(ViewRayOrigin, ViewRayDirection, Params)
			if ViewRay and ViewRay.Instance and ViewRay.Instance:IsA("BasePart") and ViewRay.Instance.Transparency < 1 then
				local HitPart = ViewRay.Instance
				--print(ViewRay.Instance)
				--print(ViewRay.Position)
				
				if HitPart:FindFirstChild("Texture") and HitPart.Texture:IsA("ImageLabel") and SettingsModule.TexturesEnabled then
					if Frame.ImageFrame.Image ~= HitPart.Texture.Image then
						-- Implement image for texturing
						Frame.ImageFrame.Image = HitPart.Texture.Image
					end
				elseif SettingsModule.TexturesEnabled then
					Frame.ImageFrame.Image = "" 
				end
				
				local Distance = (ViewRayOrigin - ViewRay.Position).Magnitude
				
				
				Frame.Size = UDim2.fromScale(FrameSizeX, (1 / Distance) * SettingsModule.ObjectHeightMultiplier)
				
				-- Shading
				
				local Face = NormalToFace(ViewRay.Normal, HitPart)
				
				if Face == Enum.NormalId.Right or Face == Enum.NormalId.Left then
					Frame.BackgroundColor3 = Color3.new(HitPart.Color.R / SettingsModule.ShadingLevel, HitPart.Color.G / SettingsModule.ShadingLevel, HitPart.Color.B / SettingsModule.ShadingLevel)
				else
					Frame.BackgroundColor3 = HitPart.Color
				end
				
				
				-- Debug
				
				--local DebugLaser = Instance.new("Part") -- Visualise the rays
				--DebugLaser.Anchored = true
				--DebugLaser.Name = "DebugLaser"
				--DebugLaser.Size = Vector3.new(0.2, 0.2, Distance)
				--DebugLaser.CFrame = CFrame.new(ViewRayOrigin, ViewRay.Position) * CFrame.new(0, 0, -Distance/2)
				--DebugLaser.Parent = workspace
				
			else
				Frame.BackgroundTransparency = 1
			end
			
			Count = Count + 1
			
		end
	end

end

ClearCanvas() -- Just incase.

GenerateFrames() -- Generate all the columns that will be used.

UpdateFrames()

script.Parent.Enabled = true

RS.RenderStepped:Connect(function() -- Always render the picture
	UpdateFrames()
end)

Roblox place file
Raycast project.rbxl (31.0 KB)

49 Likes

I made something similar a while back, the real challenge is a BSP rendering engine.

8 Likes

Amazing! I created a Maze game out of it!

4 Likes

would it be possible to make it so if you touch a certain part in the Raycast you can script an event? such as a badge giver?

3 Likes

Yes, you can use the function :GetTouchingParts() with the CameraPart.

3 Likes

Ah yes exactly what I was looking for, Perfect! Thanks. :slight_smile:

3 Likes

hmm, I seem to be struggline to figure out how to create it… Any Tips?

2 Likes

Oh. My. God.

This is pretty useful for games that wants to use a custom rendering engine and stuff like that!

Best resource that i have seen on my whole roblox life.

3 Likes

here’s a somewhat basic way to recreate the Touched() function.

As mentioned in the documentation, GetTouchingParts() doesn’t work with non-collidable parts. So we will have to do something a lil bit hacky to get it to work.

local Gui = script.Parent -- The main renderer gui
local SettingsModule = require(Gui:WaitForChild("MainSettings")) -- The module which contains settings of our renderer
local CameraPart = SettingsModule.CameraPart -- The player
local PartToTouch = SettingsModule.CurrentMap:WaitForChild("WinPart") -- The part that you want to touch. Make sure it is set to CanCollide = false.
local RunService = game:GetService("RunService") -- RunService is good for client-sided loops.

RunService.RenderStepped:Connect(function() -- Fires when ever a frame has been rendered from your computer
	local connection = CameraPart.Touched:Connect(function() end) -- Since GetTouchingParts() doesn't work with parts that aren't collidable, you will need a TouchInterest (Something that is connected to its Touched event)
	local TouchingParts = CameraPart:GetTouchingParts() -- returns a table of all the parts that are intersecting with the camera.
	connection:Disconnect()
	for i, child in ipairs(TouchingParts) do -- loops though all the parts the player is touching
		if child == PartToTouch then -- Checks if the part we are touching is PartToTouch
			print("PartToTouch has been touched!") -- For testing purposes. Removing this is recommended in the final version.
			-- Enter code here
		end
	end
end)
1 Like

Made a more complex version with textures.

(this one isn’t open-source)

I really like it

Gives off a Nostalgic feel ( like Doom)

Keep up the awesome work :smile:

2 Likes

Really great resource! Only problem is there is an annoying fisheye effect that I can’t seem to fix; any way to get rid of this issue?

FIX: For anyone else curious, I fixed it by multiplying distance by getting variables for each euler angle of RayAngle and multiplying distance by math.cos(RayAngleY)

It should be noted that there is still a slight warp on the sides when doing this, but it fixes the nasty fisheye!

1 Like

Can you please show me what RayAngleY is? When I’m trying to print or multiply
RayAngle.Y it gives 0

1 Like

I used local RayAngleX, RayAngleY, RayAngleZ = RayAngle:ToEulerAnglesXYZ() and just took RayAngleY.

EDIT: The full formula is local Distance = ((ViewRayOrigin - ViewRay.Position).Magnitude) * math.cos(RayAngleY).

1 Like