I am trying to create a custom camera in Roblox, and all works except for collisions.
This is my code for the collisons, it is located in a function that is connected to a BindToRenderStep. Note this is not the full function just the snippet of code I am working with:

Camera.CFrame = CFrame.new(cameraCFrame.Position, cameraFocus.Position)
if(Popper)then
local CheckRay = Ray.new(Head.Position + Vector3.new(0,0,0), cameraCFrame.Position - Head.Position)
local HitPart, HitPosition, Normal = game.Workspace:FindPartOnRay(CheckRay, Character, false, true)
print(Normal)
if HitPart then
Camera.CFrame = (Camera.CFrame - (Camera.CFrame.Position - (HitPosition)) + ((Head.Position - Camera.CFrame.Position).Unit) * 0.372)
end
end

You will either want to move the camera away from the wall using the surface normal, or move the camera closer to the player, or both. The reason this happens is the Camera Frustum. If the angle between the camera direction and the wall is less than the angle between the camera direction and the edge of the screen, the camera will appear to clip into the wall, even if the camera’s CFrame is not inside the wall.

The math used in your first and second code blocks are not similar. Your first code block only moves the camera toward the player, and the second code block only moves it away from the wall.

You also have magic numbers, which is not a good idea. Either calculate them from the state of the system, or assign them as variables at the top of the script.

I recommend calculating it, because the necessary distance changes based on device type and screen size

You will have to do some math to determine if the near clipping plane will be inside the object based on its surface normal. The near clipping plane is shaped like the player’s screen, but a simple approach would be to approximate it as a cone by circumscribing the rectangle with a circle. The diameter of the circle is the hypotenuse of the triangle formed by cutting the rectangle in half.

An example of a circumscribed rectangle

We can find the sides of this rectangle with the screen size of the user, the FieldOfView of the camera, and the clipping plane distance (which is called NearPlaneZ).

Using the laws of similar triangles, we know that whatever the screen ratio is will be the side lengths to the triangle forming the hypotenuse forming the diameter of the cone, and from the diameter we can get the radius. The radius is important because it’s the largest distance the camera will need to be offset by in any direction to avoid clipping.

Roblox fixes the FieldOfView to the height of the screen. I am unsure if this is still true when a mobile device is in portrait mode, but that is an edge case. The vertical cross section of a cone is a triangle bisected by the camera’s direction. Cut that in half along the axis and we get two right triangles, with an angle half of the FOV:

The base of the triangle is known as -NearPlaneZ
The angle of the triangle is known as FieldOfView/2

Therefore, the height of the triangle is tan(FieldOfView/2)*-NearPlaneZ

Moving to the circumscribed rectangle:

The height of the rectangle is twice the height of the previous triangle, which I will call h
The base of the rectangle is the height of the rectangle, multiplied by the width of the screen, divided by the height of the screen, which I will call b
The hypotenuse of the rectangle can be found using pythagoras’ theorem, which I will call H

For simplicity, we can store the screen ratio as a value since it won’t change very often. The ratio we want is the width of the screen divided by the height, which I will call k. This allows us to simplify pythagoras’s theorem:

H = sqrt(h^2 + b^2) H = sqrt(h^2 + (h*k)^2) H = sqrt(h^2 + h^2*k^2)

we know H is twice the distance we need, so we can divide H by two to get the radius of the circle.

This only needs to be calculated when one of three properties change: screen height, screen width, and near clipping plane. The near clipping plane we can assume will never change, so that leaves only screen height and screen width, which can be done in a separate coroutine and modify a shared value.