Keeping part aligned to no rotation given camera tilt

Hello,

I am working on a project where the camera tilts to the sides and forwards/backwards (pitch and roll) to keep the player’s direction of movement forwards quite often. How can I keep a part (or model) flat, no matter the pitch/roll of the camera?

image

The crude drawing kind of illustrates this: the red is the camera’s pitch/roll, the blue is the plane that is created by infinitely stretching that pitch/roll (or in other words, the surface the player is sitting on), and the black is the object I want to remain at a constant orientation. In other words, the black part should always look like it is the same orientation no matter which way the camera is pitching or rolling (the purple arrow should stay up). I would like the part to not rotate with the camera in a way that keeps it in front (in other words, follows yaw).

I have tried multiplying the CFrame of the player’s position with the orientation of the black part to world space, but that did not work.

EDIT: Here is a reference (notice how the temple in the background is unaffected by the camera tilt):

Bumping this. Still no solutions!

Still bumping - have tired aligning the midpoint of the background objects with the camera’s roll, but this did not work. I’m probably doing something wrong here.


local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local player = Players.LocalPlayer

if not player.Character then
	player.CharacterAdded:Wait()
end

local function getFlatPart(character)
	if not character then return nil end
	return character:FindFirstChild("FlatPart")
end

if not getFlatPart(player.Character) then
	local onFlatPartAdded; onFlatPartAdded = player.Character.ChildAdded:Connect(function(child)
		if child.Name == "FlatPart" then
			onFlatPartAdded:Disconnect()
		end
	end)
end

local character = player.Character
local camera = workspace.CurrentCamera
local flatPart = getFlatPart(character)

RunService.RenderStepped:Connect(function()
	if not flatPart then
		character = player.Character
		flatPart = getFlatPart(character)
		if not flatPart then return end
	end

	local cameraCFrame = camera.CFrame
	local _, yaw, _ = cameraCFrame:ToEulerAnglesXYZ()
	local yawRotation = CFrame.Angles(0, yaw, 0)
	local pitchRollCFrame = cameraCFrame * CFrame.Angles(0, -yaw, 0)
	local inversePitchRoll = pitchRollCFrame.Rotation:Inverse()

	flatPart.CFrame = CFrame.new(flatPart.Position) * CFrame.fromMatrix(Vector3.new(), inversePitchRoll)
end)

smthg like this?

Unfortunately, no. This doesn’t consider the original rotation of the part. Also, CFrame.fromMatrix takes in four Vector3 arguments, not two. It sort of works, but not fully.

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local player = Players.LocalPlayer

local function getFlatPart(character)
    if not character then return nil end
    return character:FindFirstChild("FlatPart")
end

local function waitForFlatPart(character)
    if not character then return nil end
    local flatPart = getFlatPart(character)
    if flatPart then return flatPart end

    local flatPartAdded = character.ChildAdded:Connect(function(child)
        if child.Name == "FlatPart" then
            flatPartAdded:Disconnect()
            return child
        end
    end)

    local timeout = 5
    local startTime = os.time()

    while true do
        flatPart = getFlatPart(character)
        if flatPart then
            flatPartAdded:Disconnect()
            return flatPart
        end

        if os.time() - startTime > timeout then
            flatPartAdded:Disconnect()
            return nil
        end

        RunService.Heartbeat:Wait()
    end
end

if not player.Character then
    player.CharacterAdded:Wait()
end

local character = player.Character
local camera = workspace.CurrentCamera

local flatPart = waitForFlatPart(character)

if not flatPart then return end

local initialRotation = flatPart.CFrame.Rotation

RunService.RenderStepped:Connect(function()
    if not flatPart or not flatPart.Parent then
        character = player.Character
        flatPart = waitForFlatPart(character)
        if not flatPart then return end
        initialRotation = flatPart.CFrame.Rotation
    end

    local cameraCFrame = camera.CFrame
    local _, yaw, _ = cameraCFrame:ToEulerAnglesXYZ()
    local yawRotation = CFrame.Angles(0, yaw, 0)

    flatPart.CFrame = CFrame.new(flatPart.Position) * yawRotation * initialRotation
end)

In super monkey ball I’m pretty sure the camera never tilts. It seems like it’s actually the level that’s tilting.

I’m not quite sure what you’re trying to achieve. Maybe showing us what you currently have in your game would be helpful.

1 Like

Yeah, relative to the background, the camera only ever tilts and rolls a tiny amount, just for the impact and turning lean effects, and the temple stays upright because it’s rendered to the skybox (2D art).

1 Like

The mechanism behind the movement of the ball is a combination of the camera tilting and a gravity vector being applied to the ball in the direction of the player’s tilt. The stage itself never truly moves; it’s just an illusion. This is the behavior I am currently replicating via a combination of camera manipulation and VectorForces. If you’re curious, you can look at the partial decomp for more information and to confirm this.

Here’s a GIF (sorry for poor quality, this forum can’t handle an actual .mp4…).
smb2
Notice how the background objects currently tilt with the camera. I do not want that to be the case; I want them to behave like the background objects in the linked video.

1 Like

Yes, re-orienting a whole 3D world is always done in the camera view matrix, because that’s one quick transform, whereas rotating all the level geometry in world space would be crazy expensive. You can actually call game.Workspace:PivotTo( someOrientation ), but you definitely don’t want to!!

But this is kind of irrelevant here, because the temple in the background is just in a skybox which, in SMB, is a completely separate render pass that’s done in the camera’s coordinate space. The camera is sometimes getting an additional small rotation offset from it’s reference “upright” orientation, but it’s just a few degrees to give the illusion of accelerations, which is why the temple only moves tiny amounts up and down when the ball lands.

In Roblox, the skybox is NOT in camera space, it’s in world space, so you can’t do exactly this same thing the same way, because there is no way to set the CFrame of the Skybox. If you want to create a similar illusion with Parts/Models, you have to be aware that you’ve got to move them in world space, and the parts have to be “local parts” that exist and get manipulated client-side, and shouldn’t even exist on the server.

2 Likes

This is entirely client-sided already. Doing any of this on the server would introduce unwanted latency. I did try using world space as mentioned in the OP, but I couldn’t figure it out. Moving the background to the player is simple (just offset the model by the player’s X/Z coordinates) but the rotation, not so much

Yeah, I saw you already had your code on RenderStepped, which is of course the only place it can go. And I’m guessing you already understand that this event is in the render thread, so you have to do minimal computation there or it directly impacts your framerate.

As for the orientation: it may not be obvious at first glance, but this is exactly the same problem that I just addressed in another post today, and included an example for: Rotate CFrame UpVector, loosely preserve LookVector - #18 by EmilyBendsSpace

But instead of a Dummy tracking the UpVector of a floor or Part, you want the whole scenery model to track the UpVector of the camera. And just like in that example, you can’t just use the inverse of the previous camera CFrame times the current, because you don’t want the full 3-axis rotational difference, you only want the shortest-path rotation of the UpVector from prev to current.

1 Like
if CurrentThemeEnvironment then
	local OldUp = workspace.CurrentCamera.CFrame.UpVector
	local BaseEnvironmentPivot: CFrame = CurrentThemeEnvironment:GetPivot()
	EnvironmentConnection = workspace.CurrentCamera:GetPropertyChangedSignal("CFrame"):Connect(function()
		local PlayerPos = Vector3.new(Player.Character.PrimaryPart.Position.X, 0, Player.Character.PrimaryPart.Position.Z)
		local EnvironmentCFrame = CurrentThemeEnvironment:GetPivot()
		local NewUp = workspace.CurrentCamera.CFrame.UpVector
		local Axis, Angle = GetAxisAngleToRotateVectorAtoB(OldUp, NewUp)
		local RotCF = CFrame.fromAxisAngle(Axis, Angle)
		EnvironmentCFrame = RotCF * (EnvironmentCFrame - EnvironmentCFrame.Position) + EnvironmentCFrame.Position
		CurrentThemeEnvironment:PivotTo(EnvironmentCFrame)
		OldUp = NewUp
	end)
end
...

Tried using your code to apply this principle, and it does work fairly well; however, there is now another issue I didn’t expect which is that the background is now tilting somewhat as the camera rotates, causing the background objects to appear as if they are tilting forwards and backwards; I would like it to remain flat no matter the orientation of the camera (it’s especially noticable with the large posts in the GIF I posted earlier - it looks like they are tilting towards the player as the camera rotates). It’s not quite the same thing as making it stick to the skybox, as then it will be affected by the rotation of the camera. I’m sure I’m not fully understanding this somehow.

I’m not sure I understand exactly what you’re aiming for, maybe it requires further illustration, or a screenshot of what you have now vs. what you expect?

If you were exactly trying to mimic the skybox effect, where things in the enviroment are treated like they are infinitely far away, then you have to make the enviroment translate with the camera as well, to maintain the same position offset from the camera.

Also, I don’t think you want the rotation change to be the difference in old vs current camera UpVector’s in your use case, but rather the difference between environment up and camera up. Some of your tilting may be from accumulating error by doing it incrementally.

1 Like

If you were exactly trying to mimic the skybox effect, where things in the enviroment are treated like they are infinitely far away

Not quite. The skybox in SMB does move closer to the player as they approach; in fact, you can actually collide with the skybox objects if you get close enough. They are models with collision boxes, and I do want to retain this behavior.

Instead of trying to explain, I’ll just say I want the models to behave exactly as they do in the video I’ve linked in the OP. I’ll try out your suggestion and see if that helps!

I should point out also, that the helper function I was using in that demo place is probably introducing the incremental error when used with something like the camera CFrame that updates at a very high frame rate. I wrote that function for another purpose altogether. You can see in the code that if the angle difference between the vectors is small, it’s treated like zero. That means every time the camera orientation changes by a tiny amount smaller than the threshold, the camera still moves but the enviroment is not updated, and that error accumulates. That’s why in your example it would be important to always correct the environment to align to the current camera orientation, not try to accumulate the per-frame changes. This keeps the absolute error below that EPSILON value.

1 Like