Is this the best way to scale a model?

Edit 2023:
We now have the Model.Scale property! As well as Model:ScaleTo(scale) and Model:GetScale().


I’m trying to scale trees to give more foliage variation in my game. The code I’m using is shown below. It works fine. But is there a better way? Is this the best way?

Considering I’m trying to run this function on 10,000 items as quickly as possible, I’m trying to figure out if there’s any way to speed this up.

local function ScaleModel(model, scale)
	
	local primary = model.PrimaryPart
	local primaryCf = primary.CFrame
	
	for _,v in pairs(model:GetDescendants()) do
		if (v:IsA("BasePart")) then
			v.Size = (v.Size * scale)
			if (v ~= primary) then
				v.CFrame = (primaryCf + (primaryCf:inverse() * v.Position * scale))
			end
		end
	end
	
	return model
	
end

The models I’m scaling usually have no more than 2 or 3 parts, and are all MeshParts.

68 Likes

You think you can round scale to 0.05 increments? If you do that, then you can memoize copies of models with a given scale, and then clone those memoized copies instead.

7 Likes

A while back I heard about a plugin that does something like this. I haven’t used it yet but this could be something you could look into :stuck_out_tongue:
https://www.roblox.com/library/1032987767/Ultimate-Model-Stretch-Extend-1-dimensional

That’s a good idea. Keeping 0.05 increments might be hard, but I could do something similar with the scalar value. Just lock the scalar to a certain step and keep a lookup table of foliage generated at that given scale.

3 Likes

By rounding my scale to the nearest 0.1 and caching the results, it has improved performance by +30%. Higher memory usage of course, but nothing crazy.

Server is generating about 13,000 items in 1.4 seconds consistently.

7 Likes

What’s your code so far? Are you CFraming before or after parenting?

2 Likes

Before. Each foliage prefab has a different ‘transformation’ function that applies the object to a given position (i.e. rocks and trees need to be placed/rotated differently). All transformations are done before parenting. Final parenting of the whole folder is last, and thus allows server to replicate all items to the client however it sees best.

3 Likes

Are you using Documentation - Roblox Creator Hub to move models around?

1 Like

Yes

1 Like

CFrame operations can be batched, which appears to speed things up around 2x for me. I’ve found that grouping operations into batches of ~1000 offers the greatest speedup on my machine. To multiply a tuple of Vector3s by another CFrame, just do cf:PointToWorldSpace(…).

Also, you can store the scale and the object space transformation as a single CFrame, since CFrames by themselves aren’t required to be orthogonal:

local mPts,mSizes
local cf = primaryCf:inverse()
local ScaleMatrix = CFrame.new(0,0,0,scale,0,0,0,scale,0,0,0,scale)
cf=cf*ScaleMatrix
mPts = {cf:PointToWorldSpace(unpack(mPts))}
mSizes = {ScaleMatrix:VectorToWorldSpace(unpack(mSizes))}

Where mPts and mSizes are defined appropriately.

Edit: On second thought, this might not meet your needs if you’re only transforming 2-3 parts per model. It’s worth a try, though.

14 Likes

I feel like GetChildren would be faster than GetDescendants, although if it is, probably not enough to impact performance.

Maybe only scale half of the models? If variation is what you want, do you really need to scale every single one of them?

Also, you could maybe make a variable for primaryCf:inverse() as well. Means it would be called/calculated 2 to 3 times less often. Optionally use pointToObjectSpace like suremark suggested (not sure which would be faster).

And finally, do you really need to return the model? I mean, if you’re passing it in the function, I imagine you wouldn’t need another reference to it.

1 Like

Interesting. This isn’t documented anywhere, but it works

1 Like

Mind blown. Thanks for teaching me a new cframe method :smile:

1 Like

It was on the old wiki :smiley: I discovered it last week and have been trying to think of some useful applications for it that aren’t already handled by the ROBLOX engine, but you’d be the first to give me such an idea.

By the way, it works for all of the ToWorldSpace and ToObjectSpace methods, i.e. those plus the ones for points and vectors.

3 Likes

How do you handle welds in your models? Don’t all the welds break when rescaling the parts?

1 Like

Yeah they would probably break, or otherwise just put things in the wrong position. You would have to scale the C0/C1 properties of the welds.

If you’re auto-welding, I would recommend scaling first before welding

3 Likes

All the parts from my model have scaled, but their positions appear to remain the same. As a result, there are gaps between all my parts (I am scaling down). I do have welds connecting some parts together since it is a moving object. Solutions would be appreciated.

1 Like

I had the same issue, but I figured it out.

local function scaleModel(model, scale)
	local PrimaryPart = model.PrimaryPart
	local PrimaryPartCFrame = model:GetPrimaryPartCFrame()
	
	--Destroy welds
	for _,object in pairs(model:GetDescendants()) do
		if object:IsA('BasePart') then
			for _,object in pairs(object:GetDescendants()) do
				if object:IsA('Weld') or object:IsA('ManualWeld') or object:IsA('WeldConstraint') then
					pcall(function()
						object.Part0.Anchored = true
						object.Part1.Anchored = true
					end)
					object:Destroy()
				end
			end
		end
	end
	
	--Scale BaseParts
	for _,object in pairs(model:GetDescendants()) do
		if object:IsA('BasePart') then	
			object.Size = object.Size*scale
			
			local distance = (object.Position - PrimaryPartCFrame.p)
			local rotation = (object.CFrame - object.Position)
			object.CFrame = (CFrame.new(PrimaryPartCFrame.p + distance*scale) * rotation)
		end
	end
end

https://imgur.com/a/8rf0gKW

26 Likes

Hello guys,

I asked @KoxuVBC in a private message for a solution where the parts of the model are not anchored. He then gave me this advise:

This is my implementation, which works. I want to share it with you. Maybe even someone has a suggestion for improvement.

local function scaleModel(model, scale)
local PrimaryPart = model.PrimaryPart
local PrimaryPartCFrame = model:GetPrimaryPartCFrame()

--Destroy welds
for _,object in pairs(model:GetDescendants()) do
	if object:IsA('Weld') or object:IsA('ManualWeld') or object:IsA('WeldConstraint') then
		object:Destroy()			
	end		
end
for _,object in pairs(model:GetDescendants()) do
	if object:IsA('BasePart') then
		for _,object2 in pairs(model:GetDescendants()) do
			if object2:IsA('BasePart') then
				if object ~= object2 then
					local constraint = Instance.new("WeldConstraint")
					constraint.Part0 = object
					constraint.Part1 = object2
					constraint.Parent = object
				end
			end		
		end			
	end		
end

--Scale BaseParts
for _,object in pairs(model:GetDescendants()) do
	if object:IsA('BasePart') then	
		object.Size = object.Size*scale
		
		local distance = (object.Position - PrimaryPartCFrame.p)
		local rotation = (object.CFrame - object.Position)
		object.CFrame = (CFrame.new(PrimaryPartCFrame.p + distance*scale) * rotation)
	end
end

end

Greetings!

8 Likes

I don’t think that works well with R16: image

However it works great with R6:

1 Like