Last active
March 9, 2025 05:40
-
-
Save Pyseph/8fee39acdb5c33d64c4df72d92cdc60a to your computer and use it in GitHub Desktop.
get bounding box of a Roblox model including particle emitters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--!strict | |
-- distance traveled from t=0..lifetime if Drag>0 means half-life=drag | |
local function ComputeDragFactor(drag: number, lifetime: number): number | |
-- if Drag = 0, theres no exponential change in speed | |
if drag == 0 then | |
return 1 | |
end | |
-- 'rate' = drag * ln(2). this is the exponent's coefficient in e^( ... ) | |
local rate = drag * math.log(2) | |
if drag > 0 then | |
-- velocity decays: v(t) = v0 * 2^(-drag * t) | |
-- distance factor = [1 - e^(-rate * lifetime)] / (rate * lifetime) | |
-- this fraction is the average speed (with decay) / initial speed | |
local numerator = 1 - math.exp(-rate * lifetime) + math.log(2) | |
return numerator / (rate * lifetime) | |
else | |
-- drag < 0 => velocity grows: v(t) = v0 * 2^(|drag| * t) | |
-- We flip the sign in the exponent | |
local growthRate = -rate -- = |drag| * ln(2) | |
local numerator = math.exp(growthRate * lifetime) - 1 | |
return numerator / (growthRate * lifetime) | |
end | |
end | |
local function GetBoundingBox(Object: Instance | {Instance}, ModelOrientation: CFrame?, Gizmo: any?) | |
local Orientation = ModelOrientation or CFrame.identity | |
local Descendants: {Instance} = if typeof(Object) == "Instance" then Object:GetDescendants() else Object | |
local MinX, MinY, MinZ = math.huge, math.huge, math.huge | |
local MaxX, MaxY, MaxZ = -math.huge, -math.huge, -math.huge | |
local function Update(ObjSize: Vector3, RelativeCFrame: CFrame) | |
local sx, sy, sz = ObjSize.X, ObjSize.Y, ObjSize.Z | |
local x, y, z, R00, R01, R02, R10, R11, R12, R20, R21, R22 = RelativeCFrame:GetComponents() | |
local wsx = (math.abs(R00) * sx + math.abs(R01) * sy + math.abs(R02) * sz) / 2 | |
local wsy = (math.abs(R10) * sx + math.abs(R11) * sy + math.abs(R12) * sz) / 2 | |
local wsz = (math.abs(R20) * sx + math.abs(R21) * sy + math.abs(R22) * sz) / 2 | |
MinX = MinX > x - wsx and x - wsx or MinX | |
MinY = MinY > y - wsy and y - wsy or MinY | |
MinZ = MinZ > z - wsz and z - wsz or MinZ | |
MaxX = MaxX < x + wsx and x + wsx or MaxX | |
MaxY = MaxY < y + wsy and y + wsy or MaxY | |
MaxZ = MaxZ < z + wsz and z + wsz or MaxZ | |
end | |
-- update bounding box with a point in object space | |
local function UpdateWithObjectPoint(objPoint: Vector3, radius: number) | |
local x, y, z = objPoint.X, objPoint.Y, objPoint.Z | |
MinX = MinX > x - radius and x - radius or MinX | |
MinY = MinY > y - radius and y - radius or MinY | |
MinZ = MinZ > z - radius and z - radius or MinZ | |
MaxX = MaxX < x + radius and x + radius or MaxX | |
MaxY = MaxY < y + radius and y + radius or MaxY | |
MaxZ = MaxZ < z + radius and z + radius or MaxZ | |
end | |
-- sample a cone (or double-cone) of directions around BaseDirection by dividing the X/Y spread angles into small increments | |
local function GetSpreadDirections(BaseDirection: Vector3, SpreadXDeg: number, SpreadYDeg: number, Steps: number): {Vector3} | |
-- convert half‐angles to radians | |
local XRad = math.rad(SpreadXDeg) | |
local YRad = math.rad(SpreadYDeg) | |
-- create a CFrame whose LookVector = BaseDirection. | |
-- that way, rotating around X or Y in local space will rotate around the emitter’s forward direction | |
local BaseCFrame = CFrame.lookAt(Vector3.zero, Vector3.zero + BaseDirection) | |
local Directions = {} | |
for i = 0, Steps do | |
-- alpha = pitch rotation from -HalfX to +HalfX | |
local Alpha = -XRad + (2 * XRad) * (i / Steps) | |
for j = 0, Steps do | |
-- beta = yaw rotation from -HalfY to +HalfY | |
local Beta = -YRad + (2 * YRad) * (j / Steps) | |
-- rotate around local X, then local Y | |
local Rotated = BaseCFrame * CFrame.Angles(Alpha, Beta, 0) | |
table.insert(Directions, Rotated.LookVector) | |
end | |
end | |
return Directions | |
end | |
-- calculate particleemitter bounding box | |
local function CalculateParticleEmitterBounds(Emitter: ParticleEmitter, ParentObject: BasePart | Attachment) | |
local ParticleMinX, ParticleMinY, ParticleMinZ = math.huge, math.huge, math.huge | |
local ParticleMaxX, ParticleMaxY, ParticleMaxZ = -math.huge, -math.huge, -math.huge | |
local IsParentPart = ParentObject:IsA("BasePart") | |
local function UpdateParticleBounds(ObjPoint: Vector3, Radius: number) | |
local X, Y, Z = ObjPoint.X, ObjPoint.Y, ObjPoint.Z | |
ParticleMinX = math.min(ParticleMinX, X - Radius) | |
ParticleMinY = math.min(ParticleMinY, Y - Radius) | |
ParticleMinZ = math.min(ParticleMinZ, Z - Radius) | |
ParticleMaxX = math.max(ParticleMaxX, X + Radius) | |
ParticleMaxY = math.max(ParticleMaxY, Y + Radius) | |
ParticleMaxZ = math.max(ParticleMaxZ, Z + Radius) | |
end | |
-- Gather emitter parameters | |
local MaxSize = 0 | |
for _, Keypoint in ipairs(Emitter.Size.Keypoints) do | |
MaxSize = math.max(MaxSize, Keypoint.Value) | |
end | |
local MaxSpeed = math.max(math.abs(Emitter.Speed.Min), math.abs(Emitter.Speed.Max)) | |
local MaxLifetime = Emitter.Lifetime.Max | |
local BaseDirection = ({ | |
[Enum.NormalId.Top] = Vector3.new(0, 1, 0), | |
[Enum.NormalId.Bottom] = Vector3.new(0, -1, 0), | |
[Enum.NormalId.Front] = Vector3.new(0, 0, -1), | |
[Enum.NormalId.Back] = Vector3.new(0, 0, 1), | |
[Enum.NormalId.Left] = Vector3.new(-1, 0, 0), | |
[Enum.NormalId.Right] = Vector3.new(1, 0, 0) | |
})[Emitter.EmissionDirection] | |
-- convert local direction to world direction (part rotation only, no translation) | |
local ObjectCFrame = IsParentPart and ParentObject.CFrame or (ParentObject:: Attachment).WorldCFrame | |
local WorldDirection = ObjectCFrame.Rotation * BaseDirection | |
-- get the fraction of distance traveled relative to no drag | |
local DragFactor = ComputeDragFactor(Emitter.Drag, MaxLifetime) | |
-- compute base distance from speed, lifetime, drag, acceleration, etc | |
local BaseDistance = MaxSpeed * MaxLifetime * DragFactor | |
-- roughly account for acceleration in the direction of WorldDirection | |
local Acceleration = Emitter.Acceleration | |
local AccelDot = Acceleration:Dot(WorldDirection) | |
if AccelDot > 0 then | |
BaseDistance += 0.5 * Acceleration.Magnitude * MaxLifetime * MaxLifetime | |
else | |
BaseDistance = math.max(0, BaseDistance - 0.5 * Acceleration.Magnitude * MaxLifetime * MaxLifetime) | |
end | |
-- collect the 8 corners of the part bounding box | |
local Corners = {} | |
local HalfSize = IsParentPart and (ParentObject:: BasePart).Size / 2 or Vector3.new(0.001, 0.001, 0.001) | |
for _, sx in ipairs({-1, 1}) do | |
for _, sy in ipairs({-1, 1}) do | |
for _, sz in ipairs({-1, 1}) do | |
local CornerWorldPos = (ObjectCFrame * CFrame.new(sx * HalfSize.X, sy * HalfSize.Y, sz * HalfSize.Z)).Position | |
table.insert(Corners, CornerWorldPos) | |
end | |
end | |
end | |
-- if full sphere, we can skip angle sampling | |
local IsFullSphere = (Emitter.SpreadAngle.X >= 180 and Emitter.SpreadAngle.Y >= 180) | |
if IsFullSphere then | |
-- for a full 360 degree spread, just treat it like a sphere from each corner | |
for _, CornerWorldPos in ipairs(Corners) do | |
local CornerObjPos = Orientation:PointToObjectSpace(CornerWorldPos) | |
UpdateWithObjectPoint(CornerObjPos, BaseDistance + MaxSize) | |
UpdateParticleBounds(CornerObjPos, BaseDistance + MaxSize) | |
end | |
else | |
-- for directional spread, sample angles around BaseDirection | |
local SpreadX = Emitter.SpreadAngle.X | |
local SpreadY = Emitter.SpreadAngle.Y | |
-- how many samples you want in each dimension. you can tweak this | |
local ANGLE_STEPS = 6 | |
-- Precompute all possible directions | |
local SpreadDirections = GetSpreadDirections(WorldDirection, SpreadY, SpreadX, ANGLE_STEPS) | |
for _, CornerWorldPos in ipairs(Corners) do | |
local CornerObjPos = Orientation:PointToObjectSpace(CornerWorldPos) | |
-- also update with the corner itself | |
UpdateWithObjectPoint(CornerObjPos, MaxSize) | |
UpdateParticleBounds(CornerObjPos, MaxSize) | |
for _, Dir in ipairs(SpreadDirections) do | |
local SampleWorldPos = CornerWorldPos + Dir * BaseDistance | |
local SampleObjPos = Orientation:PointToObjectSpace(SampleWorldPos) | |
UpdateWithObjectPoint(SampleObjPos, MaxSize) | |
UpdateParticleBounds(SampleObjPos, MaxSize) | |
end | |
end | |
end | |
-- draw the bounding box for just this particle emitter | |
if Gizmo then | |
local ParticleOmin = Vector3.new(ParticleMinX, ParticleMinY, ParticleMinZ) | |
local ParticleOmax = Vector3.new(ParticleMaxX, ParticleMaxY, ParticleMaxZ) | |
local ParticleSize = ParticleOmax - ParticleOmin | |
if ParticleSize.Magnitude > 0 and ParticleMinX ~= math.huge then | |
local ParticleCenter = Orientation:PointToWorldSpace((ParticleOmax + ParticleOmin) / 2) | |
local ParticleCFrame = CFrame.new(ParticleCenter) | |
Gizmo:DrawBox(ParticleCFrame, ParticleSize, 5, Color3.fromRGB(0, 200, 255)) | |
end | |
end | |
end | |
for _, Obj in Descendants do | |
if Obj:IsA("BasePart") then | |
local RelativeCFrame = Orientation:ToObjectSpace(Obj.CFrame) | |
local ObjSize = Obj.Size | |
Update(ObjSize, RelativeCFrame) | |
for _, Child in Obj:GetChildren() do | |
if Child:IsA("ParticleEmitter") then | |
CalculateParticleEmitterBounds(Child, Obj) | |
end | |
end | |
elseif Obj:IsA("Attachment") then | |
for _, Child in Obj:GetChildren() do | |
if Child:IsA("ParticleEmitter") then | |
CalculateParticleEmitterBounds(Child, Obj) | |
end | |
end | |
end | |
end | |
if MinX == math.huge then | |
return Vector3.zero, Orientation | |
end | |
local Omin, Omax = Vector3.new(MinX, MinY, MinZ), Vector3.new(MaxX, MaxY, MaxZ) | |
local Size = Omax - Omin | |
local CenterCFrame = Orientation - Orientation.Position + Orientation:PointToWorldSpace((Omax + Omin) / 2) | |
if Gizmo then | |
Gizmo:DrawBox(CenterCFrame, Size, 5, Color3.fromRGB(255, 255, 0)) | |
end | |
return Size, CenterCFrame | |
end | |
return GetBoundingBox |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment