Hello, I’m trying to tween a model in a viewportframe however I’m experiencing some issues. I believe the issue is related to the fact that physics don’t work in viewport frames, and consequencly welds don’t either. Without welds, I cannot tween models (as I normally weld all the parts to a single part to tween)
Here is my current code:
local function tweenModel(model, CF)
print("Model tweening")
-- Ensure a center part exists
local center = model:FindFirstChild("CenterPrimaryPart")
if not center then
center = Instance.new("Part")
center.CFrame = model:GetPivot()
center.Parent = model
center.Name = "CenterPrimaryPart"
end
-- Weld each part to the center part
for i,v in pairs(model:GetDescendants()) do
if v:IsA("BasePart") and v.Name ~= "CenterPrimaryPart" then
local weld = Instance.new("WeldConstraint")
weld.Parent = center
weld.Part0 = center
weld.Part1 = v
v.Anchored = false
end
end
-- Tween the center part
local tween = game:GetService("TweenService"):Create(center, TweenInfo.new(1, Enum.EasingStyle.Linear), {["CFrame"] = CF})
tween:Play()
tween.Completed:Wait()
end
Anything would help. And to clarify this code will be used in my upcoming resource for you guys: Record It : Capture and playback recordings in your Roblox experience. I plan to tween objects and models for replaying recorded events to offer smooth framerates and little preformance impact.
I tend to avoid TweenService:Create() for models entirely by essentially mimicking the service with a few changes here and there. In your case, I would manually interpolate between initial and goal CFrames using CFrame:Lerp() and then call :PivotTo() on the model every frame since viewport frames are typically handled locally:
local runSvc = game:GetService'RunService';
local twnSvc = game:GetService'TweenService';
local function tweenModel(model: Model, cf: CFrame, t: number, style: Enum.EasingStyle, dir: Enum.EasingDirection) -- adjustable parameters
local cf0 = model:GetPivot(); -- initial cframe
local a = 0; -- alpha [0, 1]
repeat
model:PivotTo(cf0:Lerp(cf, twnSvc:GetValue(math.min(a, 1), style, dir))); --lerp to goal cframe after adjusting alpha according to easing style
a += runSvc.RenderStepped:Wait() / t; -- increment alpha
until a >= 1;
end
Since RenderStepped:Wait() yields the function, we do not need to provide additional code to wait until the tween finishes before continuing.
Anchored objects (objects exempt from physics calculations) work best with manual CFrame manipulation. Welds should never be necessary for anchored objects as individual parts of a model can move in unison using :PivotTo(). Many adept scripters would agree that welding is at best a hacky alternative unless engine physics is necessary.
Since I wrote this on the fly, the function may be poorly structured or not work as expected, so please question it if needed! I tested it without observing any issues.
hey I tried your solution however it doesn’t appear to work.
Here is the script:
local runService = game:GetService("RunService")
local tweenService = game:GetService("TweenService")
local function tweenModel(model, cf)
local cf0 = model:GetPivot()
local a = 0
repeat
model:PivotTo(cf0:Lerp(cf, tweenService:GetValue(math.min(a, 1), Enum.EasingStyle.Linear, Enum.EasingDirection.In))) --lerp to goal cframe after adjusting alpha according to easing style
a += runService.RenderStepped:Wait() / .1 -- increment alpha
until a >= 1
end
As you can see in the viewportframe, the model isn’t correctly tweening to the new position. However, when I replace your code with pivotTo() it does in fact move.
Yeah using worldmodel should let you do what you’re trying to do. add a worldmodel in the viewport and move the original mode in the worldmodel, then you should be good to go.
I googled what a worldmodel does (as well as tried it out myself) and it doesn’t appear to support physics. what it does support however is animating of humanoids and some other things but is still limited.
For example you still cannot drop unanchored parts. All parts will be stuck anchored essentially.
Can you detail the specifics of your intentions? I’m not entirely sure what object in the viewport needs to move. Is the video using the function or just a PivotTo call?
This is mathematically equivalent to multiplying by 10, meaning that the tween ends in a tenth of a second. I’m unsure if that is your desirable outcome, but parameter t represented tween time; the larger the value, the smaller the increment and therefore the longer it takes for alpha to reach 1.
I am trying to tween any model in a viewportframe with the same behavior of tweenservice. For testing, I recorded my own player character walking around. since my recording system has been designed to handle players too, it should work fine. The only issue seems to be the tweening of the model (consider when I simply use PivotTo() without any kind of tween it works)
These tweens are very quick (ranging from .1 seconds) because its replaying actions (and higher FPS for recoding = very quick tweens)
your current approach doesn’t work properly with my animation script.
This provided context would drastically change my approach. I’m not sure why the function doesn’t work as opposed to a simple PivotTo, but several external factors can skew expected behavior.
As for a new and (possibly) more performant approach, moving a model between recorded positions in brief, constant intervals should not require smoothing/adjustment of the alpha value. Easing styles are difficult to notice especially at higher framerates and sometimes may even introduce clunkiness. A good alternative would be to interpolate rapidly between frames based on FPS:
local runSvc = game:GetService'RunService';
local function replay(model: PVInstance, frames: { CFrame }, fps: number)
local progress = 0;
runSvc:BindToRenderStep('replay', 0, function(dt)
local i = math.floor(progress) + 1;
local cf0 = frames[i];
local cf1 = frames[i + 1] or frames[1];
model:PivotTo(cf0:Lerp(cf1, progress % 1));
progress = (progress + fps * dt) % #frames;
end)
end
I use BindToRenderStep here, though you can reproduce an identical mechanism by subscribing to RenderStepped. Keep in mind that BindToRenderStep does not yield when called and neither does the function by association. To terminate the replay, you must use UnbindFromRenderStep.
As a test, I passed a single BasePart for the model, a track of frames (in our case, just a table of CFrames for simplicity) with each frame representing a corner of an arbitrary square, and different FPS values to yield different interpolation speeds:
In that case, you can modify the replay behavior such that it handles pauses in your animation track table. For instance, you can include numbers between keys in the table to denote pause times:
In this manner, interpolation should only occur between frames and after pauses. This is just a proposition though, and adding such a layer of complexity may require extensive critical thinking and trial/error. If you would like to stick with the original function or continue to use welds, feel free to do so.
My system already follows this structure. the only issue is tweening models to their correct CFrames because my tweenModel() doesn’t currently work.
As I mentioned I gave you a link to the current version if you want to test it out. Although your system could be considered more optimized, mine is easier to code but is still the same thing.
As you can see the endTime is -1 which means it lasts the entire duration, and the startTime is 0.
The reason I stored position and orientation differently is because I have to check if the position is different within the recording process and there are a lot of extra information to check for with CFrames opposed to merely position and orientation.
That’s a very intricate system! Since you already possess durations, positions, and orientations, you can easily employ the manual PivotTo interpolation seen in both of my propositions. Since PivotTo doesn’t require welds, it should (as proven) work seamlessly in viewport frames.
I would say my coding ability is that I am really good at making complex systems but I am limited in my knowledge for specific things.
For example, if I find a method that already works, I will always use that and not learn any other way of doing it. This is because I find it a bit tedious to do so when I already have a system that works.
However, I am now encountering a situation where the method I relied on for so long (tweeting w/ welds) doesn’t work. I don’t have a lot of experience with renderstepped (as I use while) nor do I have experience in Lerping.
With that in mind, here is my current system for replaying files:
while isRecording do
local data = recordingsPlaying[recordingId]
-- check if file has not ended
if data and data["time"] < data["endTime"] and data["time"] >= 0 then
-- retrieve the data folder inside the viewportframe to store stuff
local dataFolder = viewPortFrame.WorldModel:FindFirstChild("data")
for partName,partData in pairs(data["record"]) do
-- Check if a part exists
local isPlaced = dataFolder:FindFirstChild(partName)
-- Find closest recording
local closestRecoding, closestIndex = findClosestRecording(partData, data["time"])
-- Check if its within its end and start
local inBounds = checkInBounds(data["time"], closestRecoding["startTime"], closestRecoding["endTime"])
-- if in bounds
if inBounds then
-- if not placed then
if not isPlaced then
--place
placeObject(partName, closestRecoding["position"], closestRecoding["orientation"], dataFolder)
--elseif not correct position then
elseif checkPosition(isPlaced, closestRecoding["position"], closestRecoding["orientation"]) == false and isPlaced:GetAttribute("animating") == nil then
--animate()
task.spawn(animate, isPlaced, closestRecoding["position"], closestRecoding["orientation"])
end
-- if not in bounds
else
local nextRecording = findNextRecording(closestIndex, partData)
-- I check if the recording distance is .1 so that it allows files next to each other to animate and not immediately delete.
if nextRecording and not nextRecording["startTime"] - currentRecording["startTime"] >= .1 then
-- destroy()
isPlaced:Destroy()
elseif nextRecording == nil and isPlaced then
isPlaced:Destroy()
end
end
end
isRecording = not data["canceled"]
else
isRecording = false
end
local timeToWait = .1
task.wait(timeToWait)
data["time"] += timeToWait
end
so as you can see its a bit difficult to incorporate your current logic as I have specific systems to manage deletion, animation, placing, etc.
Another thing that is a bit difficult is that I have to stay in sync with the record file. Although I do admit my current system doesn’t do that, I can easily do this by changing the timeToWait to the record FPS.
to clarify also the section in my code that is “animate” is this function
function animate(object, position, orientation)
local finalCf = CFrame.new(Vector3.new(position["X"], position["Y"], position["Z"])) * CFrame.Angles(math.rad(orientation["X"]), math.rad(orientation["Y"]), math.rad(orientation["Z"]))
if object:IsA("Model") then
tweenModel(object, finalCf)
else
local tween = game:GetService("TweenService"):Create(object, TweenInfo.new(.1, Enum.EasingStyle.Linear), {["CFrame"] = finalCf})
tween:Play()
tween.Completed:Wait()
object:SetAttribute("animating", nil)
end
end
in which this function checks if its a model, then plays this function:
local function tweenModel(model, endCFrame)
local primaryPart = model.PrimaryPart
-- If there's no primary part, create a placeholder part
if not primaryPart then
primaryPart = Instance.new("Part")
primaryPart.Name = "ModelCenter"
primaryPart.Anchored = true
primaryPart.Size = Vector3.new()
primaryPart.CFrame = model:GetModelCFrame()
primaryPart.Parent = model
end
local startCFrame = primaryPart.CFrame
local info = TweenInfo.new(.1, Enum.EasingStyle.Linear, Enum.EasingDirection.InOut)
local tween = tweenService:Create(primaryPart, info, {CFrame = endCFrame})
tween:Play()
end
however my current tweenModel doesn’t seem to be working as intended. And from reviewing the function you offered, it seems that it wants me to give it a dictionary of frames. However, as mentioned, my system is a bit more different that yours and I don’t think it would work the same.
oh wait I am so dumb the reason it wasn’t working was because I had to set the attribute “animating” to false.
Here is the final script:
local function tweenModel(model: PVInstance, frames: { CFrame }, fps: number)
local primaryPart = model.PrimaryPart
-- If there's no primary part, create a placeholder part
if not primaryPart then
primaryPart = Instance.new("Part")
primaryPart.Name = "ModelCenter"
primaryPart.Anchored = true
primaryPart.Size = Vector3.new()
primaryPart.CFrame = model:GetModelCFrame()
primaryPart.Parent = model
end
local startCFrame = primaryPart.CFrame
local info = TweenInfo.new(1/fps, Enum.EasingStyle.Linear, Enum.EasingDirection.InOut)
local tween = tweenService:Create(primaryPart, info, {CFrame = frames})
tween:Play()
model:SetAttribute("animating", nil)
end
local runService = game:GetService("RunService")
local activeTweens = {}
local function tweenModel(model, targetCF, fps)
if activeTweens[model] then
activeTweens[model]:Disconnect()
activeTweens[model] = nil
end
local initialCF = model:GetPivot()
local progress = 0
local connection
connection = runService.RenderStepped:Connect(function(dt)
progress = math.min(1, progress + dt * fps)
local currentCF = initialCF:Lerp(targetCF, progress)
model:PivotTo(currentCF)
if progress >= 1 then
connection:Disconnect()
activeTweens[model] = nil
end
end)
activeTweens[model] = connection
end
{ CFrame } denotes a table of CFrame values, not a single CFrame. Attempting to index a CFrame with a number gives an error similar to what you encountered.
The second approach I gave you was intended to receive a sequence of CFrames (a primitive animation track) to interpolate between rapidly. Hence the arguments I used to test: