CFrame rotation Issue relative to a Sphere

I’m trying to script a camera that
rotates relative to a sphere instead of a plane. I ran into a problem: if you rotate the cube around the Z axis and then start rotating it around the X axis, an issue occurs.

RunService.RenderStepped:Connect(function(dt)
   local character = player.Character
   if not character or not character:FindFirstChild("HumanoidRootPart") then return end
   local root = character.HumanoidRootPart
   
   local delta = UIS:GetMouseDelta()
   totalRot +=  delta

   local look = CFrame.lookAt(root.Position, planet.Position)
   local rotation = CFrame.Angles(math.rad(totalRot.X), 0, math.rad(totalRot.Y))
   
   part.CFrame = look * rotation
end)
1 Like

The issue is that the cube starts moving?

1 Like

The issue is that the rotation around the X-axis is incorrect, and after rotating around the Z-axis, it appears to rotate around the Y-axis instead

(So many edits because this is an interesting problem that I’ve never really thought about before…) Assuming the red cube represents the camera, and the face on the camera represents the front…

I think the problem is with look. The way it is now, look is positioned on the surface and looks inwards towards the center of the planet. You’d have to rotate it and correct this so it faces “forward” and the upwards direction is away from the planet, and then you can apply the total rotation (assuming face = front, it would be CFrame.Angles(vertical, horizontal, 0)). On a flat plane, you can just apply this rotation on a CFrame with the default orientation and it would work.

The only problem now is how do you choose where “forward” is on a point on a sphere? And I think this choice is what your current code is missing.

2 Likes

If whatever he said doesn’t work, try changing the pivot offset of the part when you rotate it.

1 Like

NEVERMIND, i did my own testing and i think the problem is the order that the angles are applied in. The horizontal angle needs to be applied before the vertical angle.

1 Like

Problem with your current approach
Currently, you are using the position of the root but you ignore the direction from which the character arrived to that position. When moving on a sphere, the character’s path along the surface is important for the end result that I believe you want. When the the player is not applying additional rotation with mouse movement and thus the camera rotation only depends on character movement, I believe that you’d like the camera to rotate in the exact same way as the tangent plane of the surface rotates.

Here’s an explanation of why the current position is not enough to define the end result I believe you want. Let’s assume that we have a sphere whose center point is origin (0, 0, 0) and radius is 5 and an object on the sphere surface. The orientation of the object is defined by three vectors: upvector that is parallel to the surface normal, and rightvector and lookvector that are parallel to the surface tangent plane.

Let’s assume that the object is initially at (0, 5, 0) i.e. the top of the sphere. Let’s assume that initially rightvector is (1, 0, 0) and lookvector is (0, 0, -1) and that when the object moves, it rotates in the same way as the surface tangent plane. This way, upvector will always be parallel to the surface normal. I’ll describe two ways for the object to reach point (5, 0, 0).

  • Option 1: Move to point (0, 0, 5) along a cross section circle that is parallel to yz-plane and then move to the destination (5, 0, 0) along a cross section circle that is parallel to the xz-plane: When the object reaches point (0, 0, 5), rightvector is still (1, 0, 0) but lookvector is (0, 1, 0). When it reaches the destination, rightvector has become (0, 0, -1) and this time the lookvector remains as (0, 1, 0).
  • Option 2: Moving to the destination (5, 0, 0) along a cross section circle that is parallel to the xy plane: When the object reaches the destination, rightvector is (0, -1, 0) and lookvector remains as (0, 0, -1).

Despite the object ending up in the same position, its orientation in that position is different. The upvector is of course the same regardless of the path taken but the rightvector and lookvector in the end position depend on the path taken. When the user is moving their character but not their mouse, I believe your camera should behave in the same way as the object I wrote about above.

Implementation suggestions
The way I’d suggest you to implement the camera rotation is that every frame, you first update the camera CFrame based on character movement. Then apply the angles defined by mouse movement during the frame.

Here’s some example code that you can use to test whether my assumption about the desired behavior is correct. Just put the code in a client script and maybe also disable Players.CharacterAutoLoads. Perhaps you can adapt the parts related to camera movement and rotation (mainly in onRenderStepped) to your code if this is the kind of camera behavior you want. I could be wrong about the result you are looking for so correct me if that’s the case.

--!strict
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")

local MOVEMENT_SPEED: number = 16
local DEGREES_PER_PIXEL: number = .25

local CHAR_DIST_FROM_SURFACE: number = 4
local CAMERA_DIST_FROM_CHAR: number = 10

local SPHERE_POS: Vector3 = Vector3.new(0, 100, 0)
local SPHERE_DIAMETER: number = 100

local CHAR_SIZE: Vector3 = Vector3.new(1, 1, 1)

local sphere: Part
local character: Part

local mousePrevPos: Vector2 = UserInputService:GetMouseLocation()
local charPrevPos: Vector3

local function move(deltaTime: number): ()
	local movementVec2D: Vector2 = Vector2.zero
	if UserInputService:IsKeyDown(Enum.KeyCode.W) then
		movementVec2D += Vector2.new(-1, 0)
	end
	if UserInputService:IsKeyDown(Enum.KeyCode.S) then
		movementVec2D += Vector2.new(1, 0)
	end
	if UserInputService:IsKeyDown(Enum.KeyCode.A) then
		movementVec2D += Vector2.new(0, -1)
	end
	if UserInputService:IsKeyDown(Enum.KeyCode.D) then
		movementVec2D += Vector2.new(0, 1)
	end
	if movementVec2D == Vector2.zero then
		return
	end
	movementVec2D = movementVec2D.Unit
	
	local oldCf: CFrame = character.CFrame
	local oldSurfaceNormal: Vector3 = (oldCf.Position - sphere.Position).Unit
	local cameraCf: CFrame = workspace.CurrentCamera.CFrame
	local backVec: Vector3 = cameraCf.RightVector:Cross(oldSurfaceNormal)
	local initialMovementDir: Vector3 =
		movementVec2D.X * backVec
		+ movementVec2D.Y * cameraCf.RightVector
	local rotationAxis: Vector3 = oldSurfaceNormal:Cross(initialMovementDir).Unit
	local angle: number = deltaTime * MOVEMENT_SPEED / (.5 * sphere.Size.Y + CHAR_DIST_FROM_SURFACE)
	local axisAngleCf: CFrame = CFrame.fromAxisAngle(rotationAxis, angle)
	character.Position = axisAngleCf * (character.Position - sphere.Position) + sphere.Position
	character.CFrame = CFrame.lookAlong(character.Position, axisAngleCf * initialMovementDir, axisAngleCf * oldSurfaceNormal)
end

local function onRenderStepped(deltaTime: number): ()
	move(deltaTime)
	
	local camera: Camera = workspace.CurrentCamera
	
	local prevSurfaceNormal: Vector3 = (charPrevPos - sphere.Position)
	local currentSurfaceNormal: Vector3 = (character.Position - sphere.Position)
	local rotationCf: CFrame = CFrame.fromRotationBetweenVectors(prevSurfaceNormal, currentSurfaceNormal)
	camera.CFrame = rotationCf * (camera.CFrame - sphere.Position) + sphere.Position
	
	if UserInputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton2) then
		local mousePos: Vector2 = UserInputService:GetMouseLocation()
		local mouseDelta: Vector2 = mousePos - mousePrevPos
		
		local extraHorizontalRotation: number = mouseDelta.X * math.rad(DEGREES_PER_PIXEL)
		local extraVerticalRotation: number = mouseDelta.Y * math.rad(DEGREES_PER_PIXEL)
		local horRotationCf: CFrame = CFrame.fromAxisAngle(currentSurfaceNormal, -extraHorizontalRotation)
		camera.CFrame -= character.Position
		camera.CFrame = horRotationCf * camera.CFrame
		local vertRotationCf: CFrame = CFrame.fromAxisAngle(camera.CFrame.RightVector, -extraVerticalRotation)
		camera.CFrame = vertRotationCf * camera.CFrame
		camera.CFrame += character.Position
	end
	
	mousePrevPos = UserInputService:GetMouseLocation()
	charPrevPos = character.Position
end

local function createSphere(): ()
	sphere = Instance.new("Part")
	sphere.Name = "CameraTestSphere"
	sphere.Material = Enum.Material.Rock
	sphere.BottomSurface, sphere.TopSurface = Enum.SurfaceType.Smooth, Enum.SurfaceType.Smooth
	sphere.Shape = Enum.PartType.Ball
	sphere.Size = SPHERE_DIAMETER * Vector3.one
	sphere.Position = SPHERE_POS
	sphere.Anchored = true
	sphere.Parent = workspace
	return sphere
end

local function createCharacter(): ()
	character = Instance.new("Part")
	character.Name = "CharacterPart"
	--character.BottomSurface, character.TopSurface = Enum.SurfaceType.Smooth, Enum.SurfaceType.Smooth
	character.FrontSurface = Enum.SurfaceType.Hinge
	character.Size = CHAR_SIZE
	character.Position = sphere.Position + (.5 * sphere.Size.Y + CHAR_DIST_FROM_SURFACE) * Vector3.yAxis
	character.Anchored = true
	character.Parent = workspace
end

local function startTest(): ()	
	createSphere()
	createCharacter()
	
	workspace.CurrentCamera.CameraType = Enum.CameraType.Scriptable
	workspace.CurrentCamera.CFrame = character.CFrame + CAMERA_DIST_FROM_CHAR * Vector3.zAxis
	
	charPrevPos = character.Position
	
	RunService.RenderStepped:Connect(onRenderStepped)
end

startTest()

Edit:
I’m not sure how clear my written explanation about the path dependency is so here’s also a video in case it helps someone who’s reading this in the future. In the video, red is right vector, green is up vector and blue is back vector.

3 Likes

Try this

local RunService = game:GetService("RunService")
local UIS = game:GetService("UserInputService")
local player = game.Players.LocalPlayer

local planet = workspace.Planet -- your spherical object
local camera = workspace.CurrentCamera
local radius = 20 -- distance from planet surface
local yaw = 0
local pitch = 0

RunService.RenderStepped:Connect(function(dt)
   local character = player.Character
   if not character or not character:FindFirstChild("HumanoidRootPart") then return end

   local delta = UIS:GetMouseDelta()
   yaw -= delta.X * 0.3
   pitch -= delta.Y * 0.3
   pitch = math.clamp(pitch, -89, 89) -- prevent flipping over poles

   local rot = CFrame.Angles(0, math.rad(yaw), 0) * CFrame.Angles(math.rad(pitch), 0, 0)
   local offset = rot.LookVector * radius

   local targetPos = planet.Position + offset
   camera.CFrame = CFrame.lookAt(targetPos, planet.Position)
end)

2 Likes

Yes, this is indeed the camera I wanted to make. Thank you for taking the time to answer my question and explain what I was doing wrong. I really appreciate it. Thank you!

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.