To scale solar system space engine

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()

Hi Croxis,

Very interesting ! I’m trying to develop a stellar system editor, and I just discovered Panda3D and python…

I’m going to try to understand your app and I think we’ll have some discussion in a near future ^-^ See you soon !

EDIT #1 : see my orbital camera code snippet. Next I’ll try to copy your app while retaining my camera system.

EDIT #2 : Gamasutra articles are quite interesting, the log Z-buffer w/ scaling and the use of impostors are great ideas.

I decided to redo the tutorial (I still can’t find the moon!) into multiple levels, each one adding a new layer. The first (edited above) is just simulation. This adds the next layer of representing the bodies.

# -*- 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:
* A mesh will be held a max of 10,000 panda units. This will causes objects in orbit to pass through the mesh. What should be done instead is an exponential scaling so the z buffer can function as intended.
* Onlye simple, circular orbits are possible.

Introduction
Now that the simulation is developed there needs to be a way to present it to the user. This is the view in MVC. There are a number of possible ways to do this. One way involves a BodyMesh objects who will listen for position updates from the simulation and then act appropriately for the conditions. This can put a lot of chatter into the message system for large, complex system. Instead the simulation classess will also manage the meshes. This is not a true MVC style but will free up the message system for more important messages. Like shooting guns!

Due to scale issues our camera will remain at the origin. All graphics will move relative to the camera. When a mesh reaches a certain distance it will scale down, help giving the illusion of distance and prevent the clip plane from causing maheam.

Effects, such as lighting, atmosphere, and other effects will not be covered in this tutorial

This tutorial will set up the visual layer.
'''

title="Solarsystem prototype 2."

'''It is very important that the textures are stored where we hardcoded them to be.'''

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 is a dictionary with some star color data. This will map the star type with the correct color
# The lighting will (should?) eventually also reflect this, although multiple lights will be needed to simulate a black body
# Data from: http://www.vendian.org/mncharity/dir3/starcolor/
colorDatabase = {'G2V': [255, 245, 242], 'M3V': [255, 206, 129], 'K2V': [255, 227, 196]}


# Config Overrides
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 initiate panda3d
from direct.directbase.DirectStart import *

from panda3d.core import Vec3, Point3
#from panda3d.core import PointLight, VBase4
# Now we import Texture!
from panda3d.core import NodePath, Texture
from direct.showbase import DirectObject

# Time acceleration factor
# 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
TIMEFACTOR = 10

# Connivance constant, number of seconds in an Earth day
SECONDSINDAY = 86400

# 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)
# View of the moon
position = Point3(1.4999e+08, 00, 0)

# Set the background to black
# It is outer space you know
base.setBackgroundColor(0, 0, 0)   

# Mouse controls must be disabled and we will rotate the camera to get a good shot the sun and any other bodies
base.disableMouse() 
base.camera.setHpr(75,0,0)

# Simulation classes
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()
        self.position = render.getRelativePoint(self.center, Point3(self.orbit, 0, 0)) 
        
    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)) 
        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'] 
        
        # Set up grpahic components
        self.mesh = loader.loadModel("planet_sphere")
        # Mipmap - Important to have. It uses more memory but improves performance 
        texture = loader.loadTexture(bodyDB['texture'])
        texture.setMinfilter(Texture.FTLinearMipmapLinear)
        self.mesh.setTexture(texture, 1)
        
        # It would make sense to parent the mesh to this node and then position the mesh relative to render.
        # In practice this results in great deal of jittering 
        self.mesh.reparentTo(render)

        # Directly start rotation
        self.setupRotation()
        # Initiate visual
        taskMgr.add(self.updateVisual, 'planetRendering', taskChain = 'body_simulation')
    
    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.
        Idealy we would rotate this node and have the mesh attached. In practice this results in a great deal of jitter.'''
        rotation = self.rotation * SECONDSINDAY / TIMEFACTOR
        # This changed to rotate mesh. Reluctenlty this currently is not being stored in this node
        dayPeriod = self.mesh.hprInterval(rotation, Vec3(360, 0, 0))
        dayPeriod.loop()
    
    def updateVisual(self, task):
        '''Updates the visual attributes.'''
        # We look at the difference of the body's true position and our own true position
        difference = self.position - position
        # We use this to not only determine if the body will be to scale or not
        # But also how much to scale it down by.
        if difference.length() < 10000:
            self.mesh.setPos(render, difference)
            self.mesh.setScale(self.radius)
        else:
            bodyPosition = difference/(difference.length()/10000)
            self.mesh.setPos(render, bodyPosition)
            scale = difference.length()/10000
            self.mesh.setScale(self.radius/scale)
        return task.cont

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']


# Simulation Task chain
taskMgr.setupTaskChain('body_simulation', numThreads = None, tickClock = None,
    threadPriority = None, frameBudget = None,
    frameSync = None, timeslicePriority = None)

# 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 = []

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()

Post reserved for tutorial 3, which will have to wait until 1.7.1

Tutorial 4! Due to length I split up the files and am now hosting it on my website. Tutorial 4 covers ODE physics, movement, p3d packaging, and data storage.

panda3d.croxis.net/Tutorial4