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?
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):
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)
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)
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).
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…).
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.
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.
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.
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.
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.
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.