Help on Understanding Gun Sway Script with math.sin

You can write your topic however you want, but you need to answer these questions:

  1. What do you want to achieve? Keep it simple and clear!
    Understanding the code below which is working perfectly for gun sway
  2. What is the issue? Include screenshots / videos if possible!
    I want to understand how does this code work with every aspect of it.
  3. What solutions have you tried so far? Did you look for solutions on the Developer Hub?
    Yes, and asked my friends.
    After that, you should include more details if you have any. Try to make your topic as descriptive as possible, so that it’s easier for people to help you!

Hello, I needed gun sway script so I checked on internet for approaching methods to do this. I found a youtube tutorial which contains the code below. But I didn’t understand some parts of it. I need some clean explanations on it please

local swayAMT = -0.4
local swayCF = CFrame.new()
local lastCameraCF = CFrame.new()

RunService.RenderStepped:Connect(function()
	local rot = camera.CFrame:ToObjectSpace(lastCameraCF)
	local X,Y,Z = rot:ToOrientation()
	swayCF = swayCF:Lerp(CFrame.Angles(math.sin(X) *  swayAMT, math.sin(Y) * swayAMT, 0), 0.1)
	lastCameraCF = camera.CFrame
	for i, v in pairs(camera:GetChildren()) do
		if v:IsA("Model") then
			v:SetPrimaryPartCFrame(camera.CFrame * swayCF * aimCF)

Firstly why would we use :ToObjectSpace on a empty CFrame ?

 local lastCameraCF = CFrame.new() --doesn't it mean empty CFrame whose positions and rotations are 0 ?

local rot = camera.CFrame:ToObjectSpace(lastCameraCF) -- so why bother to use this function ? camera.CFrame is the same with this function since world's origin is also 0,0,0.

Secondly why are we using math.sin ? I saw on internet that it is also used for sin waves besides calculating sine. What is the goal for using it ?

2 Likes

You can’t use ToObjectSpace on a CFrame. You can only do it with two CFrames.

Even on the first RenderStepped, when lastCameraCF is CFrame.new(), the other CFrame (camera.CFrame) is probably not.

There are situations where it kiiiinda makes sense to do something like camera.CFrame:ToObjectSpace(CFrame.new()), or at least it’s not completely useless although I’d argue it’s confusing and unusual since it’s equivalent to just camera.CFrame:inverse(). I.e. print(part.CFrame:ToObjectSpace(CFrame.new()) == part.CFrame:Inverse()) should always print true.

Yes. Notice that a new value is assigned to that variable every frame, so most frames it does not have the value of a CFrame with no translation or rotation.

so why bother to use this function ?

Like I explained before, the two CFrames are not equal CFrame.new().

since world’s origin is also 0,0,0.`

A nitpick, but it’s not just about translation/position, but also about rotation/orientation.


Because it’s a cyclic (“repeating”), continuous (“smooth”) function that oscillates around 0 and goes to extremes of 1 and -1. That makes it handy for applying effects that can be expressed as numbers in a way where it varies smoothly between not being there (0), being there (1) and being there but opposite (-1).

I don’t know why the programmer chose to have the sway depend on the current camera orientation (the X, Y, Z variables), I’d personally have it depend on time instead. What does it actually look like in game?

2 Likes

I also used a similar method

I also compared it with aother tutorial which used mouse delta instead which acomplishes the same effect of getting sway in the direction of camera movement, which is what the to objectspace was doing.

Also here is an example of how ToObjectSpace can be used to find thr CFrame delta from CFrame A to CFrame B, or the rotational difference:

2 Likes

Firstly thanks for your reply. Here how it works in game :

Also can you explain me how this code understands if I move the mouse and acts correctly ? When we use local rot = camera.CFrame:ToObjectSpace(lastCameraCF) doesn’t it mean that rot’s orientation becomes the opposite direction of our camera so why don’t we look at our back with this?

Please tell me if I’m wrong; so this code takes the opposite rotation of our camera and puts them into variables(X,Y,Z).

local rot = camera.CFrame:ToObjectSpace(lastCameraCF)
local X,Y,Z = rot:ToOrientation()

In the line below, we rotate an empty CFrame with these variables with every rendered frame.

swayCF = swayCF:Lerp(CFrame.Angles(math.sin(X) *  swayAMT, math.sin(Y) * swayAMT, 0), 0.1)

And then we say this i don’t know why :
lastCameraCF = camera.CFrame
What is the point of this code ? In the first rendered frame, we will use camera.CFrame:ToObjectSpace() on a empty CFrame(lastCameraCF), but in the second rendered frame, it won’t be an empty CFrame due to this, it will give 0 for all values of CFrame in second rendered frame of this code :
local rot = camera.CFrame:ToObjectSpace(lastCameraCF)

Or is it used for not swaying when we don’t move, right ? Because if we don’t move, camera.CFrame won’t change so X,Y,Z values will be 0.

Because it’s a cyclic (“repeating”), continuous (“smooth”) function that oscillates around 0 and goes to extremes of 1 and -1. That makes it handy for applying effects that can be expressed as numbers in a way where it varies smoothly between not being there (0), being there (1) and being there but opposite (-1).

I see now. Thanks a lot.

2 Likes

Thanks, I will have a look. Though i don’t know if i can integrate spring module for my script.

2 Likes

And then we say this i don’t know why :
lastCameraCF = camera.CFrame

The code works by figuring out how the camera has rotated since the previous (“last”, I prefer the less ambiguous “previous”) frame. It does that by comparing the “current” CF to the previous CF, so it needs to store the “old/previous/last” CF for the next frame. It does that after it’s done processing the current frame, otherwise it would be pointless

When we use local rot = camera.CFrame:ToObjectSpace(lastCameraCF) doesn’t it mean that rot’s orientation becomes the opposite direction of our camera so why don’t we look at our back with this?

doesn’t it mean that rot’s orientation becomes the opposite direction of our camera

No, rot becomes lastCameraCF but relative to camera.CFrame. In other words, "the transformation from camera.CFrame to lastCameraCF". Since lastCameraCF is the CFrame of the camera at the previous frame, rot becomes the transformation from the previous frame’s CF to the current frame’s CF. Or “how the camera has moved since the last frame”. Well, it’s really the inverse of that because the original programmer flipped the arguments but oh well. I’m a bit confused RN by the double negatives, but i think that explains why the original programmer had to have a negative swayAMT. transform would be a way better variable name, or even better transformFromPrevFrame.

After the assignment, camera.CFrame == lastCameraCF * rot holds true. Another statement that illustrates what I mean (a and b are Parts):

print(a.CFrame * a.CFrame:ToObjectSpace(b.CFrame) == b.CFrame) --Always true

so why don’t we look at our back with this?

Not sure what you meant by this or “opposite direction of the camera”, sorry. If you explain that in more detail maybe I can help clear it up, but maybe it’s not needed.

It takes the inverse of the camera’s transformation from the last frame (rot) (inverse because of the flipped arguments I mentioned earlier) and turns that into Euler angles, which is a way of representing orientation or rotation in the way you’re used to from the properties window. “X, Y, Z” angles or preferably “pitch, yaw, roll” angles which is usually clearer Aircraft principal axes - Wikipedia

In the line below, we rotate an empty CFrame with these variables with every rendered frame.
swayCF = swayCF:Lerp(CFrame.Angles(math.sin(X) * swayAMT, math.sin(Y) * swayAMT, 0), 0.1)

Do you mean swayCF is “empty”? It’s only empty on the first frame, on subsequent frames it’s whatever it was set to on the previous frame. Do you mean that the CF constructed with CFrame.Angles is “empty”? Well it’s clearly not since it’s being constructed with non-zero parameters.

It’s not entirely accurate to say that swayCF gets rotated by these variables. After all, the line doesn’t say something like swayCF *= CFrame.Angles(blablabla). It’s more like it gets set to a specific orientation, which would be accurate if it weren’t for the Lerp call which just smooths out the movement which is absolutely necessary because mouse movement is super jittery. Plus it makes it look like the viewmodel has momentum.

Setting the sway to an orientation that follows the rotation (“movement” but for orientation) of the camera causes the viewmodel to “lead” where the camera is pointing.

Looking closely at it, I think it’s a mistake to use math.sin in this case. The fastest I was able to rotate the camera in an experiment was about 0.6 radians. Call it 1.0 to be generous. Here’s x and sin(x) plotted in [-1;1]:

As you can see, they’re pretty close so having the sin call in there doesn’t do a lot. It does kind of reduce the output a bit at the extreme ends, so it could be a valid approach to make the sway not go beyond a certain limit even with extreme camera movement. But the effect is so small I don’t think it’s worth the confusion, and it doesn’t work at even more extreme movements:

At inputs outside [-pi/2; pi], sin starts moving the wrong direction! So if you move the camera move than 90 degrees in a frame, the sway goes in the opposite direction?! Not sure if that’s the intentional, artistic choice of the original programmer but I don’t think that’s a good idea. First, it’s confusing to read compared to just math.clamp and takes all this analysis to figure out, second it doesn’t have much effect in the domain that’s usually relevant, so it’s very little gain for a decent amount of technical debt, and third it kinda breaks in edge cases. I’d remove it and just have this instead:

swayCF = swayCF:Lerp(CFrame.Angles(X *  swayAMT, Y * swayAMT, 0), 0.1)

In my earlier response I said some things about math.sin, they’re still true but don’t really apply in this situation. I didn’t fully understand back then what it was doing in the code, sorry if my response caused any confusion.


What is the point of this code ? In the first rendered frame, we will use camera.CFrame:ToObjectSpace() on a empty CFrame (lastCameraCF)

Again, ToObjectSpace works on TWO CFrames, not just one. So that LoC doesn’t do it “on a CF”, it does it on a pair of CFs. The camera CFrame is probably not empty, so the computation probably actually does do something. Even if it didn’t do anything to the camera CF, it would still have a purpose because it sets lastCFrame so something can actually happen on the next frame. To be fair it’s a tiiiny bit confusing that the original programmer set lastCameraCF to CFrame.new() at the top of the script, because that’s not a sensible “first previous” CFrame. A version that fixes this could look like

RunS.RenderStepped:Connect(function()
    --If this is the first frame then there's no sensible way of computing the sway, so just set the variable for next frame and pretend like there has been no movement since the last frame ("previous = current").
    _previousCameraCF = _previousCameraCF or camera.CFrame
    
    local rot = ... everything else like it was
end)

This shows a clear intent for how the sway should be handled in the edge case where there is no “previous” frame. This actually fixes minor a bug too, it’s not just just about coding style but thinking clearly about what the code does so good job spotting that there’s something weird. What happens if the player spawns in with a camera that’s yawed 179 degrees? They’ll get a massive spike of sway on the first frame, causing the viewmodel to jerk a bit to the left or right. It won’t be too extreme or even noticable because it’s just for a single frame and then it quickly falls back to a normal state but hey a bug is a bug :beetle: Another benefit is that there’s no ugly state variable at the top of the script (I mean you could have it but I wouldn’t). A down side is that it’s still a global variable that might get accidentally set by a different function. There’s loads of ways to “capture” that variable in a block so only the relevant code can see it, but this is already a big of a tangent, sorry xD

BTW the a = a or b thing might look a bit confusing at first and you could argue that it is, but it’s idiomatic (common) Lua so experienced coders will quickly recognize that it’s a way of providing a default value in situations where a value isn’t already provided.

is it used for not swaying when we don’t move, right ? Because if we don’t move, camera.CFrame won’t change so X,Y,Z values will be 0.

No, none of this code has anything to do with how the camera translates though space (X, Y, Z coordinates, from walking and stuff), only with how the camera rotates (from mouse movement, or touch or controller or VR or whatever). If you try walking but not moving the mouse you should see no sway at all. That’s why I don’t think X, Y and Z are good variable names in this case. Pitch, yaw and roll would make it clearer that it’s talking about rotation and not translation.

The ToOrientation call completely ignores the position component of the input CFrame, so if the camera moved a bit to the left or w/e that has no effect on the sway. You could use the same approach to make a “walk sway” though. It’d be a great challenge to see how well you understand all this stuff.

Anyway hope this wall of text helps clear some things up xD And I hope I got everything right :stuck_out_tongue: Ask away if you have follow up questions :+1:

3 Likes

Firstly, thank you again for your reply. I think I have understood how this code work thanks to you except the Lerp function which smooths and make the camera get back to its original place (at least that’s all I noticed after I tried the code without Lerp and just used sway.CFrame *CFrame.Angles(blablabla))

As you said, :ToObjectSpace’s arguments are used inversed in this code but it doesn’t really matter since we can just make swayAMT negative. So I tried how things would change and schematised on paint. Let me know if I understand it correctly. :

Let’s say we are directly looking our front whose pitch, yaw and roll values are 0. If we turn around 30 degree in an instant (actually this value is much smaller in roblox because we used RenderStepped which divides the change of orientation that happens in 1 frame with our fps rate. But to be able to explain it easily let’s say we rotated camera with our mouse 30 degree on yaw axis in 1 frame) we would sway the camera 3 degrees on yaw axis if swayAMT and Lerp’s Alpha(its second parameter, that is) is 0.1. Because in this code below : We are multiplying the angle with those numbers. But we can sway in 2 directions(I showed them as red lines that are called 1 and 2) which we can choose one of them by making the swayCF's values negative/positive or changing the arguments of :ToObjectSpace, both will work. I don’t know if I get it correctly. Also I totally negliged math.sin in this example.

swayCF = swayCF:Lerp(CFrame.Angles(pitch *  swayAMT, yaw * swayAMT, 0), 0.1)

I don’t think that’s true. The only way to make the sway go in the opposite direction is taking its opposite number (like 4 / -4). If you move more than 90 degrees, the sway amount will decrease gradually until 180 degrees. sin(0) = 0. sin(90) = 1, the peak sway amount. sin(180) = 0. So to make math.sin make sense, we must limit camera movement amount between 0-90, which we don’t need anything to do since, as you said 0.6 rad (34 degree~~) is the fastest amount you could have gotten. Maybe it is possible to exceed 90 degree at highest sensitivity but, who plays roblox with that sensitivity xD.

If I understand you correctly, did you mean that rotating camera, for example 120 degrees will give the same sway amount as rotating 60 degrees even though rotating 120 degrees is two times faster than 60 degrees. Because sin(120) = sin(60) = √3/2.

And math.sin actually works like Unit of magnitude if you think in that way. Like everything is between -1 and 1 and you can multiply it with whatever you want.

What you have said makes sense actually, I don’t know why it didn’t work when I switched parts’ places.

The CFrames of the two parts

1111111

Here, it doesn’t work.

222

But it works perfectly here.

I didn’t get this part. Can you elaborate please ? Instead of using lerp with 0.1 as a second parameter, why don’t we just multiply swayAMT with 0.1 ? So they will give the same number, right ?

I can’t thank you enough.

1 Like

No problem ^.^

the Lerp function which smooths and make the camera get back to its original place

No, what makes it go back to having no sway is that when you don’t move the cam, lastCameraCF equals camera.CFrame so rot becomes CFrame.new(). The Lerp only smooths the sway.

… we would sway the camera 3 degrees on yaw axis …

Yep, exactly.

we can sway in 2 directions

Yeah, if you flip the direction you should get a “lagging behind” effect instead of “leading in front of” effect. Which you prefer is an artistic choice. Maybe pistols, SMGs and carbines lead because they’re light and maneuverable, while LMGs and such lag behind to make them feel heavier.

I don’t think that’s true … 0, 90, 180…

Yeah you’re right, that’s a much better way of saying it xD

Nope. Take a look at sin(x) and x around the domain it doesn’t go weird (this time in degrees):

Near 0, the two functions behave identically. But closer to -90 or +90, sin(x) is like x but kind of smoothly dampened. Smoothly damping things is sometimes nice. Say you clamped the yaw rotation to [-90; 90] degrees:

If you just clamp it you get those sharp edges. But if you carefully use sin you can get a “perfectly” smooth transition where it gradually keeps going up but goes up slower and slower (and then finally stops at +1).

Again it doesn’t matter too much in this case as discussed, but it’s a totally valid technique that might come in handy. TBH I’d probably use an S curve or smoothstep instead for this application, but one more tool in the toolbox doesn’t hurt. Here’s a comparison:

Dashed lines are the derivatives. Red is sigmoid/S-curve, blue is sin, green is smoothstep. As you can see smoothstep is pretty much identical to sin, but it’s computation actually only uses multiplication and addition so it should be a bit faster. S-curve has different properties, it actually never reaches -1 or 1, but it’s derivative has no sudden changes.

Try this:

function CFrameFuzzyEq(a:CFrame, b: CFrame, epsilon: number?): boolean
	return a.Position:FuzzyEq(b.Position, epsilon) 
		and a.LookVector:FuzzyEq(b.LookVector, epsilon)
		and a.UpVector:FuzzyEq(b.UpVector, epsilon)
end

while wait() do
	print(CFrameFuzzyEq(a.CFrame * a.CFrame:ToObjectSpace(b.CFrame), b.CFrame)) --ACTUALLY always true
end

Floating point error caused the two CFs to sometimes be not exactly equal. I think xD

I wasmostly just nitpicking about the phrasing. In my mind, “rotated by” means “to transform by”, which we do with the *= (multiply and assign) operator, while “orienting to” means “to set the transformation” which we do with the “=” operator. That doesn’t have anything to do with Lerp. I just meant that, if you were to ignore the Lerp, that line doesn’t as much “rotate by” as it “orients to”. IMO, it’s just a matter of saying things precisely, not a huge deal as long as you understand what’s actually happening.

Instead of using lerp with 0.1 as a second parameter, why don’t we just multiply swayAMT with 0.1 ? So they will give the same number, right ?

Do you mean

swayCF = swayCF:Lerp(CFrame.Angles(X * swayAMT, Y * swayAMT, 0), 0.1) (A)

should be the same as

swayCF = CFrame.Angles(X * swayAMT * 0.1, Y * swayAMT * 0.1, 0) (B)

? Or perhaps that it should be the same as

swayCF *= CFrame.Angles(X * swayAMT * 0.1, Y * swayAMT * 0.1, 0) (C)

I mean, you could try it and see what happens xD That’d help build intuition about it for sure, I’d do it if I were you. It won’t be the same though. The most similar to A is B. It’s like A, but unsmoothed and only 0.1 times as much. Since mouse movement is not smooth you should clearly be able to see the jitter. C is completely different. It doesn’t set the sway, it rotates it by the camera rotation. So if you keep turning to the right the sway keeps increasing without returning to 0 automatically.

Here I’ve drawn the curve that y=0 followed by repeated y = Lerp(1.0, c) effectively creates:

It moves y towards the target smoothly but never gets there. If it’s far a way from the target then it moves fast, and the closer it gets the slower it moves. It’s an exponential decay function (I think, not 100% sure actually), just for the sake of knowing the names of different “smoothing functions” xD

2 Likes

Yes, (B) is what I exactly meant by my question. Mustn’t A and B be same, right ? I tried them but B is jittery. I mean, Lerp gets a point between 2 location’s distance with the input we give it between 0 and 1. If we give it 0.1, output will be distance/10 so there should be no difference when we do it manually like in B. I checked it with using Lerp with 2 different part’s positions.

workspace.Part1.Position = workspace.Part1.Position:Lerp(workspace.Part2.Position, 0.1)

It just teleports 0.1 of the distance, there was no smooth movement. I feel like I’m missing an obvious point.

Wow, there is a lot of easing style. I saw tons of them from the wikipedia link that you have sent. Though they look like identical a little xD. Are they built-in in roblox or do we need to write its formula ?

The rest is totally comprehended. I never thought I would spend this much time for such little code :face_with_open_eyes_and_hand_over_mouth: Thanks again.

1 Like

sin is an easing style, and I think smoothstep would be equivalent to the “cubic” easing style. For the S-curve you’ll have to write it yourself.

Mustn’t A and B be same

Nope. B would be the same as

swayCF = CFrame.new():Lerp( CFrame.Angles(X * swayAMT, Y * swayAMT, 0), 0.1 )

The difference here is what’s in front of the Lerp. The reason that A smooths the movement out is that it’s effectively “every frame, move sway 10% towards the target sway”. Of course on the next frame sway is a bit closer, so after 2 frames it will have moved 1-(1 - 0.1)^2 = 19% towards the target sway, 3 frames 1-(1-0.1)^3=27.1%, etc. Every frame it gets closer, slowing down as it gets closer. This is what gives rise to the exponential behavior.

B would just be “every frame, move sway to 10% of the target sway”. It gets there instantly, and it never goes beyond 10% of the target sway amount. Getting there instantly explains the jitteriness. A takes a while to get there, and goes to 100% of the target sway amount (well, it takes forever to get there but that’s the limit it goes towards).

I feel like I’m missing an obvious point.

Nothing’s obvious until you’ve learned it. The critical point is what goes before the Lerp.

I never thought I would spend this much time for such little code :face_with_open_eyes_and_hand_over_mouth: Thanks again.

Yeeeeeah coding feels like that sometimes. However it’s deep insight that you’ll take with you for all the next problems you hit, again more stuff for the toolbox. After coding for a while it speeds up a lot.

1 Like

So I’ve been thinking what you’ve said for 3 days and got it finally if the image below correct.

Thank you again.

1 Like

Nope, not AFAICT. Say you turn 30 degrees on the 1st frame. A gives 3 degrees, because 0.9 * 0 + 0.1 * 30 = 3. B gives 3 degrees, because 30 * 0.1 = 3. C gives 3 degrees because 0 + 30 * 0.1 = 3. So all good for frame 1. On frame 2 you still turn in the same direction, but half as fast. A gives 4.2 degrees, because 0.9 * 3 + 0.1 * 15 = 4,2. B is correct, so is C. On the 3rd frame you turn 90 degrees (super fast), so A gives 0.9 * 4.2 + 0.1 * 90 = 12,76 degrees. B and C are correct.

Here’s a spreadsheet giving those exact results, and for a bunch more frames which is neat for playing around:

dampening-strats-2 - Copy.txt (42.7 KB) (rename to .ods and open in LibreOffice Calc or Excel or maybe Google Sheets)

The numbers in the Rotation row can be manually changed, and the Orientation, A, B and C rows are computed from that. In the code so far we’ve used 0.1 as a constant, I made it so that can be changed in the top left corner. The right graph shows rotation and orientation, the left graph shows each dampening strategy. A is desirable because it returns to 0 and smoothly follows the rotation. B looks identical to the rotation in the screen shot, but notice that the Y axes have way different scales.

The noise thing is just some amount of random noise that gets added to the rotation. That’s relevant because mouse movement is so jittery, which is kind of like random noise. Here’s how the strats behave with constant 0.1 and 10 noise:

image

1 Like

Finally, I understood… I don’t know how to thank you enough !!

1 Like

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