Note: Even if I may have gone a little bit over the top with this, it’s still not perfectly realistic
Lucky for you, I just finished my aerodynamics script.
Okay basically, it uses some math and the size of the model to draw an outer box of positions (shown as attachments below)
→
Then each of these attachments raycasts inwards (represented by this yellow line) halfway into the model.
Halfway since, usually if it’s a symmetrical/centered model it will only take half of whatever side it’s on to hit something. (You can easily change it to 100% of the side’s length)
The spots where ray’s hit then have attachments placed on them (for actual purposes this time instead of visualization), and then are stored into a table for later.
As you can see, it’s not perfect since it’s a very off center model.
However, if we use something like a sphere (padding = 0.5 studs):
Or a Robloxian Home (padding = 2.5 studs):
Alongside each attachment a linearvelocity is also created, then parented inside of it.
Okay, we’re done with all the fancy attachment stuff. After that it gets pretty simple.
First you should probably know some units.
Let’s get on with the calculations.
If we type in “drag formula” into the google search bar, the first equation that comes up is the one provided by Wikipedia on the sidebar.
There’s also one by NASA, but it’s just the same, and looks less fancy, so I’ll stick to the one above.
Alright, first up is p. Or density of fluid. Since we’re likely flying airships through air, let’s find that first.
The only formula I could find that only needed the altitude in meters for air pressure was from Quora. Which isn’t the best but we’ll make due with it.
Putting this together gives us
local p = (101.325 * (1 - ((0.0065 * h) / 288.15)) ^ ((9.80665 * M) / (8.31447 * 0.0065)))
Now there’s a few issues, here. First of all, this is using earth gravity.
We can fix this by doing (workspace.Gravity * 0.28)
to convert the gravity of the workspace into meters. Next up, what’s M
? Well after a quick google search it turns out to be molar mass. So we need the molar mass of air. Easy enough, one more shows it to be 28.96g/Mol
(or more exactly 28.9628g/Mol
). I’m not exactly sure what the units here stand for, so please correct me on this. I’m assuming that the g
in g/Mol
stands for grams, and since the rest of the constants for the equations are in meters and such, and it’s usually kg/m^3
instead of g/m^3
when checking density, I’m gonna go ahead and assume it’s meant to be kg/Mol
. So we would go ahead, and divide the numerator by 1,000. And we get: 0.0289628Kg/Mol
.
Now we can plug that in:
local p = (101.325 * (1 - ((0.0065 * h) / 288.15)) ^ (((workspace.Gravity * 0.28) * 0.0289628) / (8.31447 * 0.0065)))
Since we are going to be substituting h
for Position.Y
we can just use that, and convert it to meters.
local p = (101.325 * (1 - ((0.0065 * (Position.Y / 0.28)) / 288.15)) ^ (((workspace.Gravity * 0.28) * 0.0289628) / (8.31447 * 0.0065)))
--to convert from meters to studs you multiply by 0.28, so to convert from studs to meters you divide by 0.28
Alright, all looks good and all when we test this in the output:
However an issue comes up when you go above a certain altitude between 10,000 and 13,000 studs. I’ve gone ahead behind the scenes and found the magic number where it switches from real numbers to
NaN
, it seems that
12412.615384615384
is the breaking point. We can avoid this by clamping with some simple logic:
local p = (101.325 * (1 - ((0.0065 * ((Position.Y > 12412.615384615384 and 12412.615384615384 or Position.Y) / 0.28)) / 288.15)) ^ (((workspace.Gravity * 0.28) * 0.0289628) / (8.31447 * 0.0065)))
Basically this says, if altitude > magic number then be magic number instead, if not be altitude
Why not clamp the lowerbound too? you may ask. Well, if we plug in negative numbers to this equation, the air pressure becomes greater (and it doesn’t have a limit, it can go past the negative version of the magic number).
Meaning if you kept falling and falling, there would be a point where you would have enough force to go further down, basically you would start bobbing up and down and float like if you plummeted into the ocean (this effect doesn’t occur at sea level though). I haven’t actually gone ahead and tested this myself so I’m not sure what exactly what would happen.
ok. thats cool, but what do we do we these arbitrary numbers that this equation spits out?
So the thing is, I don’t know. So we’ll turn the equation into the air density at the current altitude.
(It’s much easier than you think)
If we examen our equation, (101.325 * (1 - ((0.0065 * ((Position.Y > 12412.615384615384 and 12412.615384615384 or Position.Y) / 0.28)) / 288.15)) ^ (((workspace.Gravity * 0.28) * 0.0289628) / (8.31447 * 0.0065)))
, it seems everything after the 101.325
is just the percentage of how much air there is relative to the amount at sea level.
(If you multiply by 100 it would be ~99.763%)
Using this, we can just grab our air density at sea level from google, which happens to be
1.293kgm^-3
or
1.293kg/m^3
coming from NASA
here.
Now all we do is plug it in at the front of our percentage, replacing the
101.325
giving us this:
local p = (1.293 * (1 - ((0.0065 * ((Position.Y > 12412.615384615384 and 12412.615384615384 or Position.Y) / 0.28)) / 288.15)) ^ (((workspace.Gravity * 0.28) * 0.0289628) / (8.31447 * 0.0065)))
This also fixes the problem I didn’t mention where kPa
was equal to kiloNewtons/m^2
. m^2. Density is measured in m^3
.
Note: I forgot one last thing, since we clamp to 12412.615384615384
studs in altitude, anything equal or over that will make the equation return 0 density for air. If this is an issue, say your airships or whatever are flying really high in the sky, we can do this:
local SeaLevel = 0 --0 is the default, nothing changes (in studs of course)
local p = (1.293 * (1 - ((0.0065 * (((Position.Y - SeaLevel) > (12412.615384615384 + SeaLevel) and 12412.615384615384 or (Position.Y - SeaLevel)) / 0.28)) / 288.15)) ^ (((workspace.Gravity * 0.28) * 0.0289628) / (8.31447 * 0.0065)))
Anyways with that out of the way, we can move on to the next variable in our drag formula. Oh. Oh velocity? Should be pretty easy enough. All we need to do is:
local Difference = (OldPosition - Position) --important that we have it reversed as Origin - Destination instead of Destination - Origin for later (we get to remove one minus sign)
local Velocity = Difference.Magnitude * 0.28 --since magnitude is absolute it wont be affected by the remark above
Alright so, next is Drag Coefficient.
Googling it shows this formula.
Usually this would be really hard to calculate, and at first when I saw this I was like “WHHAAA, We’re solving for Fd in the first place!!!”
Well, remember our trusty attachment system we set up?
Well, if an object is moving, it should have a previous position. There should also be a direction to move in to get to that direction. Unfortunately, I did a bad job of explaining about the attachment table. So I’ll do that right now.
So basically, there’s a variable alongside the attachment table called AttachmentCount
. and Since the attachment table is a dictionary where each index is the attachment instance and the value is the current WorldPosition
of the attachment. (type = {[Attachment]: Vector3}
). Because of this, we can’t do #Attachments
. Since the #
operator only works on tables with numerical indexes. So for every attachment we add, we just do:
AttachmentCount += 1
Alright, now that that’s covered, what we can do, is every RunService HeartBeat. In each heartbeat, define the dragCoefficient of the current frame, and on second thought the projected area.
Both of them are basically done at the same time due to their nature, so why not knock out the last two variables at the same time?
The projected area is kinda hard to explain, but for example if you were to look at a sphere from the side, you wouldn’t see the entire 3 dimensional sphere, but instead a part of it. Since it’s a sphere, you are able to see half of it at once. Which also happens to be it’s drag coefficient, 0.5.
So the plan is, is on each heartbeat, loop through every single attachment.
We’re then going to raycast backwards against the direction we are moving from each attachment. This way, if the ray hits, we know we are in front of our model and dragging it back with drag .
If the ray hits, we have to increment both the drag coefficient and projected area by a certain number. But what number?
Well since our drag coefficient is kind of like a percentage between 0 and 1, we can increment it up by the reciprocal of how many attachments we have. That way, if say, exactly 1/2 of all of our attachment’s have their rays hits, the drag coefficient will be 0.5, or 1/2. Same goes for 3/4, and etc. with any decimal in between 0 and 1.
Next is our projected area.
In the image above, we have a square. Let’s imagine each attachment as part of a square on the surface of our model. The value or area of each square would be the side length squared. We can find the side length by getting the distance between an attachment and one of it’s neighbors.
However, since the distance between each attachment is defined by the padding, we don’t have to figure this out. Okay, that’s cool. But we want the value of each attachment, not the entire square. Since a square has four vertices, four attachments, we can divide the area by 4 and distribute it to each attachment. Meaning that each attachment is worth Padding ^ 2 / 4
.
Alright so let’s code this last part.
local Kg = ((9.80665 / 0.28) / workspace.Gravity * 0.28 ^ -3) --our Kg from earlier
local reciprocal = 1 / AttachmentCount
local paddingUnitArea = Padding ^ 2 / 4
game:GetService("RunService").Heartbeat:Connect(function(dt)
local dragCoefficient = 0
local projectedArea = 0
end)
First we’re doing that, next we have to raycast. Since we are going to be raycasting for potentially thousands of attachments, we want the ray to be as small as possible in order to be as inexpensive as possible. Since the attachment is directly on top of the model’s surface, we only have to raycast 0.001 studs in the opposite direction we’re moving:
--these are the same raycast params used to raycast attachments onto the model's surface
local Params = RaycastParams.new()
Params.FilterDescendantsInstances = {Model}
Params.FilterType = Enum.RaycastFilterType.Include
Params.RespectCanCollide = true
Params.IgnoreWater = true
local Kg = ((9.80665 / 0.28) / workspace.Gravity * 0.28 ^ -3)
local reciprocal = 1 / AttachmentCount
local paddingUnitArea = Padding ^ 2 / 4
game:GetService("RunService").Heartbeat:Connect(function(dt)
local dragCoefficient = 0
local projectedArea = 0
for Attachment, oldPos in pairs(Attachments) do
if Attachment.WorldPosition ~= oldPos then --this if statement is SUPER important, if your model stops moving and you do not have this, it will attempt to get the direction and return NaN, messing up the drag, and then the model just disappears from NaN velocity
local results = workspace:Raycast(Attachment.WorldPosition, ((oldPos - Attachment.WorldPosition).Unit * 0.001), Params)
--^^ remember from earlier how it was reversed?
if results then
dragCoefficient += reciprocal
projectedArea += paddingUnitArea
end
end
end
end)
And yeah! That’s basically all the variables defined.
Now we just have to put it all together:
local Kg = ((9.80665 / 0.28) / workspace.Gravity * 0.28 ^ -3)
local reciprocal = AttachmentCount ^ -1
local paddingUnitArea = Padding ^ 2 / 4
game:GetService("RunService").Heartbeat:Connect(function()
local dragCoefficient = 0
local projectedArea = 0
for Attachment, oldPos in pairs(Attachments) do
if Attachment.WorldPosition ~= oldPos then
local results = workspace:Raycast(Attachment.WorldPosition, ((oldPos - Attachment.WorldPosition).Unit * 0.001), Params)
if results then
dragCoefficient += reciprocal
projectedArea += paddingUnitArea
end
end
end
--commence my original notes below before i started making this post
for Attachment, oldPos in pairs(Attachments) do
if Attachment.WorldPosition ~= oldPos then
local Difference = (oldPos - Attachment.WorldPosition)
--^^usually its destination - origin, but this way its origin - destination so the direction is automatically negative, but since magnitude is absolute i dont have to worry about that
--if i multiply the height in studs by the reciprocal of 0.28 then the altitude will be higher to account for using all real life units
--that way so i dont have to convert every single term in the equation below into roblox units, i only have to convert the altitude
local airDensity = (1.293 * (1 - ((0.0065 * ((Attachment.WorldPosition.Y - Sealevel) > (12412.615384615384 + Sealevel) and 12412.615384615384 or (Attachment.WorldPosition.Y - Sealevel)) * 0.28^-1) / 288.15)) ^ (((workspace.Gravity * 0.28--[[9.80665]]) * 0.02896) / (8.31447 * 0.0065)))
local velocity = Difference.Magnitude * 0.28
--the reason i commented out one of the terms for the exponent for air density was because it was earth's gravity, so i just changed it to roblox gravity in meters (workspace.Gravity * 0.28)
local Drag = ((Kg * 3.903325) * airDensity * velocity ^ 2 * dragCoefficient * projectedArea)
--i didn't mention terminal velocity but i'll do it below, its pretty easy and super important
--(super important meaning if u don't do it u get sent flying millions of studs away)
local terminalVelocity = ((2 * Kg * Mass * workspace.Gravity)--[[(78.4532 * Mass * workspace.Gravity)]] / (airDensity * dragCoefficient * projectedArea)) ^ 0.5
--^^ commented out because its basically earth gravity (in studs) * roblox gravity * mass
Attachment.LinearVelocity.MaxForce = terminalVelocity
Attachment.LinearVelocity.VectorVelocity = Drag * Difference.Unit
--^^ * Difference.Unit that way so if you are moving in any direction the drag should apply opposite
Attachments[Attachment] = Attachment.WorldPosition
end
end
end)
Alright yeah, so that terminal velocity. Forgot to mention it. It’s pretty easy, all you do is caculate it then set the max force of the LinearVelocity to the terminal velocity.
This is the formula, as you can see, we basically have all the variables except Mass
Mass is pretty easy to get though, I just put this at the top of my script and ta-da!:
local Mass = 0
for _, Part in pairs(Model:GetDescendants()) do
if Part:IsA("BasePart") then
Mass += Part:GetMass()
end
end
Now all that leaves is, nothing… We’re done! Yay! My brain can finally relax after nine hours of typing this. Maybe I can regrow braincells by sleeping.
Before I post this though, here’s the full script if you wanna try it out yourself.
You can follow the comments and put it inside a model, or require it from another script and call the function on a model. I recommend letting the padding go to the default, but you can mess around with it, just don’t make it too small, because depending on the model’s size, so many raycasts will happen that your studio crashes.
It’s here in all of it’s glory for you to dissect and stuff, so enjoy. I hope this post helped! Feel free to ask my any questions or anything.
--// In a module script, you can just remove the outer function and change-
--// -the parameters into variables
return function(Model: Model, Padding: number?, Sealevel: number?)
Sealevel = Sealevel or 0
local Size = Model:GetExtentsSize()
local originCFrame = Model:GetBoundingBox()
Padding = Padding or (Size.Magnitude / 20)
local Mass = 0
for _, Part in pairs(Model:GetDescendants()) do
if Part:IsA("BasePart") then
Mass += Part:GetMass()
end
end
local Locations = {}
local Vectors = {X = "LookVector", Y = "UpVector", Z = "RightVector"}
local Params = RaycastParams.new()
Params.FilterDescendantsInstances = {Model}
Params.FilterType = Enum.RaycastFilterType.Include
Params.RespectCanCollide = true
Params.IgnoreWater = true
local function FillSide(Face: Vector3)
local Sides
if string.find(tostring(Face), "%-") then
Sides = Vector3.new(-1, -1, -1) - Face
else
Sides = Vector3.new(1, 1, 1) - Face
end
local Axis1, Axis2
for Axis, V in pairs({X = Sides.X, Y = Sides.Y, Z = Sides.Z}) do
if V ~= 0 then
if not Axis1 then
Axis1 = Axis
else
Axis2 = Axis
break
end
end
end
local AxisE
for Axis, V in pairs({X = Face.X, Y = Face.Y, Z = Face.Z}) do
if V ~= 0 then
AxisE = Axis
break
end
end
for A1 = -(Size[Axis1] / 2), (Size[Axis1] / 2), Padding do
for A2 = -(Size[Axis2] / 2), (Size[Axis2] / 2), Padding do
local A1P = Sides[Axis1] * originCFrame[Vectors[(AxisE == "Y" and Axis2 or Axis1)]] * A1
local A2P = Sides[Axis2] * originCFrame[Vectors[(AxisE == "Y" and Axis1 or AxisE)]] * A2
local AEV = Face[AxisE] * originCFrame[Vectors[(AxisE == "Y" and AxisE or Axis2)]] * (Size[AxisE] / 2)
Locations[(originCFrame + (A1P + A2P + AEV))] = {Face, AxisE}
end
end
end
-- making an outer box of positions that will soon raycast inwards to put attachments on the surface of the model
FillSide(Vector3.new(1, 0, 0))
FillSide(Vector3.new(0, 1, 0))
FillSide(Vector3.new(0, 0, 1))
FillSide(Vector3.new(0, 0, -1))
FillSide(Vector3.new(0, -1, 0))
FillSide(Vector3.new(-1, 0, 0))
local Attachments = {}
local AttachmentCount = 0
local Rotation = {X = CFrame.Angles(0, math.rad(90), 0), Y = CFrame.Angles(0, math.rad(90), math.rad(180)), Z = CFrame.Angles(math.rad(180), math.rad(90), 0)}
for CF, T in pairs(Locations) do
local dir = ((originCFrame * Rotation[T[2]])[Vectors[T[2]]] * T[1][T[2]] * Size[T[2]] / 2)
local results = workspace:Raycast(CF.Position, dir, Params)
if results then
local Attachment = Instance.new("Attachment")
Attachment.Name = "DYNAMIC_ATTACHMENT"
Attachment.Parent = results.Instance
Attachment.WorldPosition = results.Position
local LinearVelocity = Instance.new("LinearVelocity")
LinearVelocity.Attachment0 = Attachment
LinearVelocity.RelativeTo = Enum.ActuatorRelativeTo.Attachment0
LinearVelocity.Parent = Attachment
AttachmentCount += 1
Attachments[Attachment] = results.Position
end
end
--UNITS:
--1 KG = 0.125 Roblox Mass Unit
--1 Stud = 0.28 meters (or 1 meter = 3 + 4/7 studs)
--local p = (101.325 * (1 - ((0.0065 * (h > 12412.615384615384 and 12412.615384615384 or h) * 0.28^-1) / 288.15)) ^ ((9.80665 * 0.02896) / (8.31447 * 0.0065)))
--air density in newtons per square meters^^, convert to kg/m^3
--new, updated, kg/m^3 version:
--local p = (1.293 * (1 - ((0.0065 * ((h - Sealevel) > 12412.615384615384 and 12412.615384615384 or (h - Sealevel)) * 0.28^-1) / 288.15)) ^ ((9.80665 * 0.02896) / (8.31447 * 0.0065)))
--^^ replaced 101.325 (sea level i think) with 1.293 because 1.293 is the density of air or smth?
--since air density is in kg/m^3 units instead of the sea level thingy the output should be those units too cuz its using them
--local Reciprocal = (#Attachments) ^ -1
--Cd += reciprocal --Cd or drag coefficient ranging from 0 to 1
--A += padding^2/4
--multiply drag force by 9.80665 * 8 or 78.4532 to convert to Roblox Mass Units
--EDIT: earth gravity ^^, roblox gravity would be workspace.Gravity * 2.24 (8 * 0.28 = 2.24)
--^^ studs/second
--multiply by dt to make it consistent, nvm dont do this since velocity is already measured in studs/dt instead of studs/s
local Kg = ((9.80665 / 0.28) / workspace.Gravity * 0.28 ^ -3)
local reciprocal = AttachmentCount ^ -1
local paddingUnitArea = Padding ^ 2 / 4
game:GetService("RunService").Heartbeat:Connect(function()
local dragCoefficient = 0
local projectedArea = 0
for Attachment, oldPos in pairs(Attachments) do
if Attachment.WorldPosition ~= oldPos then
local results = workspace:Raycast(Attachment.WorldPosition, ((oldPos - Attachment.WorldPosition).Unit * 0.001), Params)
if results then
dragCoefficient += reciprocal
projectedArea += paddingUnitArea
end
end
end
for Attachment, oldPos in pairs(Attachments) do
if Attachment.WorldPosition ~= oldPos then
local Difference = (oldPos - Attachment.WorldPosition)
--^^usually its destination - origin, but this way its origin - destination so the direction is automatically negative, but since magnitude is absolute i dont have to worry about that
--if i multiply the height in studs by the reciprocal of 0.28 then the altitude will be higher to account for using all real life units
--that way so i dont have to convert every single term in the equation below into roblox units, i only have to convert the altitude
local airDensity = (1.293 * (1 - ((0.0065 * ((Attachment.WorldPosition.Y -
Sealevel) > (12412.615384615384 + Sealevel) and 12412.615384615384 or (Attachment.WorldPosition.Y - Sealevel)) * 0.28^-1) / 288.15)) ^ (((workspace.Gravity * 0.28--[[9.80665]]) * 0.0289628) / (8.31447 * 0.0065)))
local velocity = Difference.Magnitude * 0.28
--the reason i commented out one of the terms for the exponent for air density was because it was earth's gravity, so i just changed it to roblox gravity in meters (workspace.Gravity * 0.28)
local Drag = ((Kg * 3.903325) * airDensity * velocity ^ 2 * dragCoefficient * projectedArea)
local terminalVelocity = ((2 * Kg * Mass * workspace.Gravity)--[[(78.4532 * Mass * workspace.Gravity)]] / (airDensity * dragCoefficient * projectedArea)) ^ 0.5
--^^ commented out because its basically earth gravity (in studs) * roblox gravity * mass
Attachment.LinearVelocity.MaxForce = terminalVelocity
Attachment.LinearVelocity.VectorVelocity = Drag * Difference.Unit
--^^ * Difference.Unit that way so if you are moving in any direction the drag should apply opposite
Attachments[Attachment] = Attachment.WorldPosition
end
end
end)
end
Left side is before falling, and right side is mid-fall during runtime. (With attachment and force stuff visibility enabled)
Edit: I’m pretty sure you aren’t going to use this exact method since it take’s a second or two for the script to fully setup, and it may not give the exact results you want. However, it’s definitely a good starting place for ideas on how you should go about calculating drag.