This is my second draft/prototype on rendering a solar system with correct scales. Useful for Celestial type programs and spaceship games.
This is less ugly and terrible than the first. This first is a tutorial of sorts on developing the underlaying simulation. No pictures yet.
Feed back, suggestions, criticisms are always welcome.
My goal is to make this something official panda sample worthy, hence the copious comments.
Working p3d here: croxis.dyndns.org/prototype.p3d
# -*- coding: utf-8 -*-
'''Prototype of rendering a planetary system to scale.
Tutorial 1
Many thanks to all the people who answered my stupid questions and to those who work on Panda3D
These four articles also provided great assistance in understanding what needs to be done conceptually
http://www.gamasutra.com/view/feature/3098/a_realtime_procedural_universe_.php
http://www.gamasutra.com/view/feature/3042/a_realtime_procedural_universe_.php
http://www.gamasutra.com/view/feature/2984/a_realtime_procedural_universe_.php
http://www.gamasutra.com/view/feature/2511/a_realtime_procedural_universe_.php
Current limitations of this version:
* TBA
Introduction
Several programming paradigms call for separating input, output, and the underlaying simulation. A commonly used paradigm is the Model-View-Controller (MVC) paradigm. In most other Panda tutorials this is not the case, the simulation data is stored in the models, physics is acted on the models, and so on. For simple programs this is acceptable. For what we are attempting to achieve here it is not.
If we were to just render a solar system to scale we could still apply the same rules. However this engine is intended for a spaceship game. Graphics cards are unable to handle such drastic differences in scale -- see links above for further explanations. We must use illusions to simulate true scale and distances of objects, so the values stored in our models will be different than the "true values" in the simulation.
This tutorial will set up the simulation layer. No fancy graphics here.... yet.
'''
# I like to define the window title in a variable, kind of a kick start to my code.
title="Solarsystem prototype."
'''The data for the planetary bodies needs to be stored somewhere. Ideally this is stored separate from the code in some sort of text format or SQL database. There are many ways to format and store the data for retrieval in code. XML is the most famous, but there is also JSON, basic INI type files, YAML, and many others. For this tutorial the database is in dictionary format.
We will be using a simple solar system model. Innate to any system is the barycenter, the center of mass. Because all systems have them it will not be defined. Currently we will not model eccentricity or orbit tilts, just simple circle orbits. The top level int he solar system dictionary will orbit the system barycenter, which is also coordinates 0,0,0. These bodies will usually be star(s). If there are any bodies which orbit those top level objects, such as planets, those top level objects will also define them. Then those objects will define anything underneath, such as moons, and so on. These objects can also be defined as barycenters. This allows us to simulate things like the Pluto-Chiron system.'''
starDatabase = {
'Sol': # Name of the solar system. This allows us to define multiple systems.
{
'Sun': {
'type': 'star', # This will determine what info we want to read
'spectral': 'G2V', # Will be used in conjunction with another database to determine color
'radius': 695500.0, # Radius in kilometers
'mass': '2e+30', # Mass in kilograms
'rotation': 25.38, # Length of day, in Earth Days
'absolute magnitude': 4.83, # brightness of the star, used for lighting
'texture': 'sun_1k_tex.jpg', # Hardcoded path to the texture
'axis': 0, # Tilt of body in decimal degrees
'orbit': 0, # If orbit is defined as 0 then the object will be centered on the higher level barycenter
'period': 0, # Number of earthdays to orbit the higher level body
'bodies': # Any bodies which orbit this one are defined here
{
'mercury': {
'type': 'body',
'radius': 2439,
'mass': 3.3022e+23,
'rotation': 58.646,
'axis': 0.1266,
'orbit': 57909100,
'texture': 'mercury_1k_tex.jpg',
'period': 87.969
},
'earth': {
'type': 'body',
'orbit': 149598261,
'texture': 'earth_1k_tex.jpg',
'period': 365.256363,
'radius': 6371,
'mass': 5.9742e+24,
'rotation': 1,
'axis': 23.439,
'bodies': {
'Moon': {
'type': 'body',
'radius': 1737.1,
'mass': 7.3477e+22,
'rotation': 27.321582,
'axis': 1.5424,
'orbit': 384399,
'texture': 'moon_1k_tex.jpg',
'period': 27.321582
}
}
}
}
}
}
}
# Here are overrides to panda's Config.prc file (unless programs can have their own config.prc file? This was not clear to me in the manual. This is where most panda programs start
from pandac.PandaModules import loadPrcFileData
loadPrcFileData( '', 'frame-rate-meter-scale 0.035' )
loadPrcFileData( '', 'frame-rate-meter-side-margin 0.1' )
loadPrcFileData( '', 'show-frame-rate-meter 1' )
loadPrcFileData( '', 'window-title '+title )
loadPrcFileData('', "sync-video 0")
# Vital import line to initialte panda3d
from direct.directbase.DirectStart import *
# It is recommended python style to import only needed classess and modules
# This helps the namespace from becoming too poluted
# Are there actual memory savings in doing this?
from panda3d.core import Vec3, Point3
#from panda3d.core import PointLight, VBase4
from panda3d.core import NodePath
#from direct.showbase import DirectObject
# This is the time scale factor on how fast to speed up the solar system
# The simulator will currently attempt to model orbit and rotation speeds in real time
# This factor can speed up, or slow down, the rate.
# Default is 31,536,000 (365.25*24*60), or the earth orbits the sun in one minute
#TIMEFACTOR = 525969.162726
# My factor where it orbits once every 5 minutes
#TIMEFACTOR = 105193.832545
# Once an hour
#TIMEFACTOR = 8766.1527121
# Once a day
TIMEFACTOR = 365.256363004
# Realtime
#TIMEFACTOR = 1
# Convinance constant, number of seconds in an Earth day
SECONDSINDAY = 86400
'''Stars, barycenters, planets, moons, asteroids all have some things in common. We will create a base class which all the others.'''
class BaseObject(NodePath):
'''Simulation objects will behave in many ways like the render system. There will be position, child objects, parent objects. Some additional functions will be added that may be useful down the road. For now we will also use this as the barycenter object.'''
def __init__(self, name, bodyDB, parentNode):
'''We need to store some additional data than what is usually in a nodepath.
self.position: absolute position in solar system
self.scale: true scale (radius)
self.center: Node whos rotation will indicate body's' position in orbit'''
# Because we are defining our own __init__ function we also need to call the __init__ of the parent classess
# When this is done self is passed as well. This is the only time self is passed.
NodePath.__init__(self, name)
self.orbit = bodyDB['orbit']
self.period = bodyDB['period']
self.center = NodePath(name + "_orbit_root")
# Parent to the higher level object
self.center.reparentTo(parentNode)
self.reparentTo(self.center)
# Set node position to proper orbital distances relative to parent
self.setPos(self.orbit, 0, 0)
if self.orbit:
self.setupOrbit()
def getBarycenterMass(self):
'''In some situations the sum of the orbiting bodies will add significant mass to the body's system. This function will total all the mass of the orbiting bodies and return it. As a possible optimization this can be computed upon generation. This function would then factor the size of the system and the distance of object under gravitational influence and determine if barycentermass is needed or just the body mass. For now, it is blank!'''
def setupOrbit(self):
'''Here we create our orbiting task code
# We convert number of earth days in data file into seconds
# It is then divided by time factor to change simulation to desired speed'''
period = self.period * SECONDSINDAY / TIMEFACTOR
orbitPeriod = self.center.hprInterval(period, Vec3(360, 0, 0))
orbitPeriod.loop()
# Here is the task for position updating
taskMgr.add(self.orbitUpdate, self.getName() + '_position_update', taskChain = 'body_simulation')
def orbitUpdate(self, task):
'''Update the position of the object as it orbits its parent body.
self.center will be rotated appropriately, and the new position will be extrapolated from there
Currently only circular, flat orbits are being modeled.
If there is no orbit then return and end the task.'''
if not self.orbit:
return task.done
self.position = render.getRelativePoint(self.center, Point3(self.orbit, 0, 0))
print self.getName(), self.position
#self.setPos(self.position)
return task.cont
class Body(BaseObject):
'''This is a physical body! Code relieved for bodies will be here.'''
def __init__(self, name, bodyDB, parentNode):
'''We pass the name to the BaseObject, bodyDB will have vital info for us!'''
BaseObject.__init__(self, name, bodyDB, parentNode)
self.mass = bodyDB['mass']
self.radius = bodyDB['radius']
self.rotation = bodyDB['rotation']
self.setupRotation()
def getAcceleration(self, distance):
'''Returns the accelleration caused by this body from the given distance.
This is multipled by the time scale factor to keep physics effects the same.'''
a = G * float(self.mass) / ((distance*1000)**2)
return a*TIMEFACTOR
def setupRotation(self):
'''# Here we establish a rotation interval for the day/night cycle.
# This is also stored in data in Earth days so once again we need to convert into seconds'''
rotation = self.rotation * SECONDSINDAY / TIMEFACTOR
dayPeriod = self.hprInterval(rotation, Vec3(360, 0, 0))
dayPeriod.loop()
class Star(Body):
'''This is a star! It will require some additional data and different data from planets'''
def __init__(self, name, bodyDB, parentNode):
Body.__init__(self, name, bodyDB, parentNode)
self.absoluteM = bodyDB['absolute magnitude']
self.spectral = bodyDB['spectral']
# This is a specific, separate task chain for managing planet position
# This gives us an opportunity to put the graphics update for the solar system bodies
# in its own thread and separate it from any game logic
taskMgr.setupTaskChain('body_simulation', numThreads = None, tickClock = None,
threadPriority = None, frameBudget = None,
frameSync = None, timeslicePriority = None)
# Position is the true position of the viewer in the system
# This default puts us 9000 km from the center of the Earth
#position = Point3(149598261, -9000, 0.0)
# This position will point at the earth and sun
#position = Point3(149617261.0, 8000, 0.0)
# This position will be at mercury
position = Point3(58022100, 0, 0)
# At the sun!
#position = Point3(0,-1.0e+7,0)
# Set the background to black
# It is outer space you know
base.setBackgroundColor(0, 0, 0)
# A core node for the solar system. In many ways like 'render' for the 3D tree
system = render.attachNewNode('system')
# Store celestial bodies here
# Later we will iterate through this list and adjust the scale of each body one by one.
# Although now we are using bodies as nodes themselves this may become redundant.
bodies = []
'''In simple programs such as these the code to generate our solar system does not need to be in a function or class. However if regeneration needs to be repeated, or a new system generated mid run, then it will need to be in a function. It could even be in a DirectObject class and listen for a regeneration event.'''
def generateSolarSystem(name):
'''Documentation string for this function. Will show up with command: print functionName.__doc__ as well as API doc generators. Generate solarsystem of the name passed'''
# Because this engine will be designed to load all sorts of fictional and real star systems
# We will pull the Sol star system directly from the list.
starSystemDB = starDatabase[name]
for bodyName, bodyDB in starSystemDB.items():
generateNode(bodyName, bodyDB, system)
def generateNode(name, DB, parentNode):
'''This is a recursive function. If a body has child bodies defined it will call this function again.'''
if DB['type'] == 'body':
body = Body(name, DB, parentNode)
elif DB['type'] == 'star':
body = Star(name, DB, parentNode)
elif DB['type'] == 'barycenter':
body = BaseObject(name, DB, parentNode)
# TODO: Use panda notification system
print name, 'generated'
if 'bodies' in DB:
for bodyName, bodyDB in DB['bodies'].items():
generateNode(bodyName, bodyDB, body)
bodies.append(body)
# Now generate our solar system!
generateSolarSystem('Sol')
# And we run FOREVER!
run()