Making beam cape curve when parent part is at an angle

Hi,

I’m working on creating a cape made of beams to represent what would be fabric, which works alright, it’s just that I’m not sure how I would apply a curve when the parent part is at an angle. I’m fine with the idea of bezier curves which I think I would be using for this, I’m just not too sure how I would make the cape curve like a real fabric cape would (assuming the cape needs to be “locked”/pinned to the humanoid root part’s top-left and top-right rear vertices).

Currently this is what I have.

My idea was initially to just not update the uppermost attachment when the humanoid root part rotates, however because the next attachments’ positions depend on this first attachment’s CFrame, it would make the cape not rotate when the parent part is at an angle, meaning the cape could stay at an angle which is not ideal because this doesn’t look very realistic:

image
image

Here is each segment, names are numbered 1-9 from top to bottom:

Ideally the cape would bend like so:

Anyway, here’s my script which is pretty unoptimized at the moment, I’m just working on getting the cape “physics” (if you can even call it that) right. I’ve added comments but I can give more explanation if needed:

for i,v in ipairs(attachments) do
	local previous = attachments[(tonumber(v.Name) :: number) - 1]
	if not previous then
		runService:BindToRenderStep('CapeStep', Enum.RenderPriority.First.Value - 1, function(deltaTime)
			v.WorldCFrame = CFrame.fromMatrix( -- "lock" the origin attachment to the vertical axis
				v.WorldPosition,
				-CFrame.identity.UpVector,
				v.WorldCFrame.UpVector,
				v.WorldCFrame.LookVector
			)
		end)
		continue
	end
	v:GetPropertyChangedSignal('CFrame'):Connect(function() -- this should "limit" each attachment to only be ~0.5 studs from each other
		local direction = previous.WorldCFrame:PointToObjectSpace(v.WorldCFrame.Position)
		if direction.Magnitude < 0.501 then -- 501 for floating point errors, we check because we don't want the re-entrancy depth limit to be exceeded which would happen without this if-statement as the CFrame is still updated a lot
			return
		end
		local unit = direction.Unit / 2 -- each attachment is .5 studs from one another so unit /2
		v.WorldPosition = previous.WorldCFrame:PointToWorldSpace(unit)
	end)
end

local function getValue(alpha)
	return math.sin(alpha / 2 * math.pi) -- moving along the sine wave looks better than linearly imo
end

while true do
	local previous = script.Parent:FindFirstChild('1').WorldCFrame
	for i = 2, #identity:GetChildren() + 1 do
		coroutine.wrap(function()
			local v = identity:FindFirstChild(i) -- identity is a part at CFrame.identity
			if v:IsA('Attachment') then
				local goal = previous * CFrame.new(0.5, 0, 0) -- because of how beams are oriented, X is the vertical axis
				local init = v.WorldCFrame
				previous = init
				local timePassed = 0
				while timePassed < DELAY_TIME do -- update the current attachment to get closer to the goal based on the previous attachment
					timePassed += task.wait()
					local interpolatedGoal = init:Lerp(goal, getValue(math.clamp(timePassed / DELAY_TIME, 0, 1)))
					v.WorldCFrame = interpolatedGoal
				end
			end
		end)()
	end
	task.wait(DELAY_TIME) -- 1/16 for how many studs the char can walk in a second
end

Thanks!

2 Likes

So I figured it out. Basically I lerped the goal attachment primary axis (which would be the vertical axis) using the sine out easing function to handle weight (making the cape more “steep” as it reaches the goal axis, with the current first attachment primary axis and that made it curve well.

Got the function from here:

Here’s the finished product:

And the added portion:

local function getWeightedAlpha(alpha)
	return math.sin((alpha * math.pi) / 2)
end

for i,v in ipairs(attachments) do
	local previous = attachments[(tonumber(v.Name) :: number) - 1]
	if not previous then
		runService:BindToRenderStep('CapeStep', Enum.RenderPriority.First.Value - 1, function(deltaTime)
			local currentRightVector = v.WorldAxis
			local isRootRotated = not currentRightVector:FuzzyEq(GOAL_WORLD_AXIS, 0.005)
			if isRootRotated then
				for i = 2, #attachments do
					attachments[i].WorldAxis = currentRightVector:Lerp(GOAL_WORLD_AXIS, getWeightedAlpha(i / (#attachments)))
				end
			else
				for i,v in ipairs(attachments) do
					v.WorldAxis = GOAL_WORLD_AXIS
				end
			end
		end)
		continue
	end
	v:GetPropertyChangedSignal('CFrame'):Connect(function()
		local direction = previous.WorldCFrame:PointToObjectSpace(v.WorldCFrame.Position)
		if direction.Magnitude < 0.501 then
			return
		end
		local unit = direction.Unit / 2
		v.WorldPosition = previous.WorldCFrame:PointToWorldSpace(unit)
	end)
end

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