Raycasting straight up or down is wonky

I played a game the other day that had the premise of the LIDAR map from Garry’s Mod and enjoyed the concept, so I thought I’d recreate it myself and maybe put a spin on it. Currently I have the basic system down of raycasting and then placing dots where the rays hit, but when the camera looks straight up or straight down the dots seem to “compress” and the cone gets tighter. Truthfully I don’t fully understand the CFrame math that’s happening and just kinda brute forced it until it worked, so any help would be appreciated.

This is how it’s supposed to look:

This is when the camera is straight up:

This is when the camera is straight down:

And this is the code:

uis.InputBegan:Connect(function(input, gpe)
	if gpe then
		return
	else
		if input.KeyCode == Enum.KeyCode.F and debounce == false then
			debounce = true
			workspace.CurrentCamera.CameraType = Enum.CameraType.Scriptable
			hum.WalkSpeed = 0
			for y = 40, -40, -2 do
				local camera = workspace.CurrentCamera.CFrame
				for x = 80, -80, -2 do
					local ranX = math.random(x-1, x)
					local ranY = math.random(y-1, y)
					local rayresult = workspace:Raycast(camera.Position, (camera.LookVector * 100) + Vector3.new(camera.RightVector.X * -ranX,ranY,camera.ZVector.X * ranX),params)  
					-- negative rightvector + position zvector makes it go left
					-- positive rightvector + negative zvector makes it go right
					if rayresult then
						local newdot = dot:Clone()
						newdot.Position = rayresult.Position
						newdot.Parent = workspace.dotholder
						local otherdots = workspace:GetPartsInPart(newdot)
						if #otherdots > 1 then
							destroydots(otherdots, newdot)
						else
							table.insert(ignorelist, dot)
							task.delay(45, newdot.Destroy, newdot)
						end
					end
				end	
				runservice.Heartbeat:Wait(0.5)
			end
			workspace.CurrentCamera.CameraType = Enum.CameraType.Custom
			hum.WalkSpeed = 16
			debounce = false
		end
	end
end)

Extra info:

  • newdot is being cloned from ReplicatedStorage
  • destroydots is a function to make sure the dots don’t overlap, it saves performance since they’re already pretty close together
  • Everything in the character is blacklisted so the rays pass through them

This math is wrong:

local rayresult = workspace:Raycast(
	camera.Position, 
	camera.LookVector * 100 + 
		Vector3.new(
			camera.RightVector.X * -ranX,
			ranY,
			camera.ZVector.X * ranX),
	params)

When you take the component of a vector, like with .X, you discard any actual direction info. You’ve then passed this to Vector3.new which creates some position in world space, but you actually probably want a position in camera space:

local rayresult = workspace:Raycast(
	camera.Position, 
	camera.CFrame:PointToWorldSpace(
		-ranX,
		ranY,
		-100),
	params)

camera.CFrame:PointToWorldSpace() needs a Vector3 in order to be used, so I threw the values in one. I suppose the rays don’t get squished together now, but it seems that the origin is now offset from the camera. I had these parts equally spaced for testing early and you can see that it’s about 4 studs in one direction.

Also thanks for telling me about the vector components, it makes sense now that I think about it