As per the Design Notes, OrbitLib does not ship with an offical rendering library. However, many users expressed interest in seeing an example of rendering OrbitLib objects in the Roblox Workspace. This article will provide a set of functions for doing so.
Conversions
Before using OrbitLib values, some common-sense conversions must be made.
OrbitLib and Roblox Reference Vectors
As per the Design Notes, the reference vector directions between Roblox and OrbitLib are different. if we use OrbitLib coordinates without converting, we will find that our entire scene will have an undesired rotation of 90 degrees. Before we display OrbitLib coordinates in Roblox instances, always be sure to run the following conversions:
Conversion Examples
-- Convert from Roblox coordinates to OrbitLib coordinates.functionRobloxToOrbitLib(x:number,y:number,z:number)returnx,-z,yend-- Convert from OrbitLib coordinates to Roblox Coordinates.functionOrbitLibToRoblox(i:number,j:number,k:number)returni,k,-jend
Studs and Kilometers
As we are rendering in Roblox, we will want to decide a scale at which we will render OrbitLib objects. Let's define a conversion factor between Roblox Studs and kilometers.
In more advanced renderers, the bonus of using a unit conversion like the one above is that a zoom feature can be implemented easily. In the WebGL Demo, zoom is implemented by changing the scale and re-rendering the scene every time the user moves the mouse wheel.
Rendering a CelestialBody
We can render a celestial body as a sphere with its given radius.
-- Renders the CelestialBody as a part.functionRenderCelestialBodyAsPart(celestialBody)-- Calculate radius and then convert to studs.localdiameterStuds=(celestialBody.radius*2)*oneKilometerEqualsStuds-- Create part to matchlocalpart=Instance.new("Part")part.Anchored=truepart.Name=celestialBody.namepart.Shape=Enum.PartType.Ballpart.Size=Vector3.new(diameterStuds,diameterStuds,diameterStuds)part.TopSurface=Enum.SurfaceType.Smoothpart.BottomSurface=Enum.SurfaceType.Smoothpart.Position=Vector3.new(0,0,0)-- OPTIONAL: Uncomment the below line to have the function itself move-- the new part into the workspace.-- part.Parent = game.Workspacereturnpartend
Here is an example of Earth rendered in an empty Roblox place with oneKilometerEqualsStuds = 1000.
Tracing Orbits
One way of visualizing orbits is to "trace" them mathematically. Orbits are polar functions, which means they take an angle as an argument. If we take a bunch of samples between some start and end angle, we will "trace" an arc of the orbit as it goes around the parent body.
The following function provides an implementation of this strategy. It takes a start angle ( rad) and end angle ( rad), and traces segmentCount points between the start and end angle along the path of the orbit. It also creates a part at each sampled point and returns the table of them.
--[[ Traces the orbit between the startAngle and endAngle, creating segmentCount samples.--]]functionTraceOrbitArc(orbit:table,startAngle:number,endAngle:number,segmentCount:number)localpartsCreated=table.create(segmentCount)-- Take segmentCount samples along the orbit between [startAngle, endAngle]localsegmentIndex=0localincrement=(endAngle-startAngle)/segmentCountfori=startAngle,endAngle,incrementdo-- Get positionlocalx,y,z=orbit:GetPositionVelocityECI(i)x,y,z=RobloxToOrbitLib(x,y,z)-- Convert OrbitLab to Robloxx=x*oneKilometerEqualsStuds-- Convert from kilometers to Roblox studs.y=y*oneKilometerEqualsStudsz=z*oneKilometerEqualsStudslocalsamplePart=Instance.new("Part")samplePart.Anchored=truesamplePart.Size=Vector3.new(0.5,0.5,0.5)samplePart.Name="Sample"samplePart.Position=Vector3.new(x,y,z)samplePart.TopSurface=Enum.SurfaceType.SmoothsamplePart.BottomSurface=Enum.SurfaceType.SmoothsamplePart.Parent=game.WorkspacepartsCreated[segmentIndex]=0segmentIndex+=1endreturnpartsCreatedend
Tracing Elliptical Orbits
To trace elliptical orbits, we want to sample the entire curve.
There are points 2π radians in a circle. This means that if we set a start angle of 0 radians, and an end angle of 2π radians, we will trace the entire path of the orbit.
Tracing Hyperbolic Orbits
Hyperbolic orbits represent a special case. If one traces a hyperbola between 0 radians and 2π radians, they will discover that the hyperbola has two distinct parts.
We only want to trace the part of the hyperbola that is closest to the planet because this is represents the actual trajectory. To only trace this part of the hyperbola, we can solve for the asymptotes of the trajectory equation.
Skipping the derivation, we get that that range of the relevant part of the hyperbola is -arccos(-(1/eccentricity)) < Θ < arccos(-(1/eccentricity)).
Putting It Together
We can use the orbit's Orbit.conic field to find out whether the orbit is elliptical or hyperbolic. Using this field, we can write a general function that selects the right way to trace the orbit like so:
--[[ Traces the orbit between the startAngle and endAngle, creating segmentCount samples.--]]functionTraceOrbitArc(orbit:table,startAngle:number,endAngle:number,segmentCount:number)localpartsCreated=table.create(segmentCount)-- Take segmentCount samples along the orbit between [startAngle, endAngle]localsegmentIndex=0localincrement=(endAngle-startAngle)/segmentCountfori=startAngle,endAngle,incrementdo-- Get positionlocalx,y,z=orbit:GetPositionVelocityECI(i)x,y,z=RobloxToOrbitLib(x,y,z)-- Convert OrbitLab to Robloxx=x*oneKilometerEqualsStuds-- Convert from kilometers to Roblox studs.y=y*oneKilometerEqualsStudsz=z*oneKilometerEqualsStudslocalsamplePart=Instance.new("Part")samplePart.Anchored=truesamplePart.Size=Vector3.new(0.5,0.5,0.5)samplePart.Name="Sample"samplePart.Position=Vector3.new(x,y,z)samplePart.Parent=game.WorkspacepartsCreated[segmentIndex]=0segmentIndex+=1endreturnpartsCreatedend-- Traces the given orbit with the number of segments.functionTraceOrbit(orbit:table,segmentCount:number)iforbit.conic~="HYPERBOLA"thenTraceOrbitArc(orbit,0,2*math.pi,segmentCount)elselocalasymptote=math.acos(-(1/orbit.eccentricity))TraceOrbitArc(orbit,-asymptote,asymptote,segmentCount)endend
Rendering Orbital Motion
Because spacecraft motion on large scales is a very slow process, let's first define a variable called timeAcceleration that represents how many times faster than realtime our simulation will process. Let's also define currentTime to be equal to zero; this will be our "clock" variable that stores the current time of the simulation.
Using a simple Heartbeat loop that repositions a part, we can increment time by the step time times our time acceleration. Then, we can calculate the predicted position of the spacecraft using OrbitLib's Orbital Prediction functions and set the part's position accordingly.
-- Import OrbitLiblocalCelestialBody=require(game.ReplicatedStorage.OrbitLib.CelestialBody)localOrbit=require(game.ReplicatedStorage.OrbitLib.Orbit)-- Convert from Roblox coordinates to OrbitLib coordinates.functionRobloxToOrbitLib(x:number,y:number,z:number)returnx,-z,yend-- Convert from OrbitLib coordinates to Roblox Coordinates.functionOrbitLibToRoblox(i:number,j:number,k:number)returni,k,-jend-- Conversion FactorslocaloneStudEqualsKilometers=1000localoneKilometerEqualsStuds=1/oneStudEqualsKilometers-- Time ValueslocaltimeAcceleration=10000-- How many times normal speedlocalcurrentTime=0-- Prediction constantslocalTOLERANCE=0.0008-- A constant used for calculating precision of prediction-- See Orbital Prediction for documentation.-- Parent body and orbitlocalearth=CelestialBody.new("Earth",5.972e24,6378.1)localorbit=Orbit.fromKeplerianElements(earth,0.3,22000,1,0,0,0,0)-- Makes a part to show the spacecraft's position.functionCreateSpacecraftPart()-- Create part to matchlocalpart=Instance.new("Part")part.Anchored=truepart.Color=Color3.new(1,0,0)part.Name="Spacecraft"part.Size=Vector3.new(1,1,1)part.Shape=Enum.PartType.Ballpart.TopSurface=Enum.SurfaceType.Smoothpart.BottomSurface=Enum.SurfaceType.Smoothpart.Position=Vector3.new(0,0,0)part.Parent=game.Workspacereturnpartend-- Part for spacecraftlocalspacecraftPart=CreateSpacecraftPart()-- Positions the spacecraft based on the current time.functionPositionPartBasedOnPrediction(orbit:table,part:BasePart,currentTime:number)-- Get the predicted true anomalylocaltrueAnomaly=orbit:UniversalPrediction(currentTime,TOLERANCE)print(trueAnomaly)-- Get ECI position from true anomalylocalcurrentPosXEci,currentPosYEci,currentPosZEci=orbit:GetPositionVelocityECI(trueAnomaly)-- Convert ECI position from OrbitLib reference vectors-- to Roblox reference vectorscurrentPosXEci,currentPosYEci,currentPosZEci=OrbitLibToRoblox(currentPosXEci,currentPosYEci,currentPosZEci)-- Convert from kilometers to Roblox studs from-- our arbitrary conversion factorcurrentPosXEci=currentPosXEci*oneKilometerEqualsStudscurrentPosYEci=currentPosYEci*oneKilometerEqualsStudscurrentPosZEci=currentPosZEci*oneKilometerEqualsStuds-- Position partpart.Position=Vector3.new(currentPosXEci,currentPosYEci,currentPosZEci)end-- A handler for the Heartbeat event. The argument step contains the time since the last frame.functionOnHeartbeat(step:number)currentTime+=step*timeAccelerationPositionPartBasedOnPrediction(orbit,spacecraftPart,currentTime)endgame:GetService("RunService").Heartbeat:Connect(OnHeartbeat)