Showing posts with label python. Show all posts
Showing posts with label python. Show all posts

Tuesday, October 5, 2010

Beta release: How to render OpenStreetMaps using Python and Matplotlib

Introduction:

Open Street Maps is an collaboratively developed and open source map database. The entire OSM database or specific portions can be downloaded. The database includes information on roads, political boundaries, natural features, rail lines, trails, and much more. The website can generate output files with sufficient information to create custom and detailed surface street maps for many areas of the world. With Python and Matplotlib the XML data in an OpenStreetMaps OSM file can be rendered as a custom map can be rendered to meet specific needs. Below is an example of a map which can be generated from and OSM data file.


This is a small snippet of code which makes it relatively easy to see how to use the raw data from Open Street Maps. For a complete rendering examples see the Open Street Maps wiki and the pyrender source code.

Key Concepts:

An OSM file is generated by selecting an area of interest at www.OpenStreetMap.org and selecting the export tab. Under the export tab, there is an option to export the map to 'OpenStreetMap XML Data'. If this is selected, then an XML file which contains all of the data for the selected region is generated. This XML file includes road, railways, trails, and land features among other things. The XML file has a a few header elements which define the XML standard, the data source, and the bounds on the data set. After that, the data file consists of a long list of nodes in the map. Each node describes one point on the surface of the earth. After the nodes, the ways are defined. A way consists of a list of nodes and tags. The nodes are are order and describe something like a road or a lake. The tag provide sufficient data to understand what the way represents. After the ways, come relations. Relations combine other elements and provide tags which area associated with those elements.
To render roads, only nodes and ways need to be considered.
When plotting lots of elements, Matplotlib can be sped up significantly (3X or more) by turning off autoscaling.
Since the ways are not ordered in the OSM file, z-ordering in matplotlib provides a nice way to control what has the most prominence.
So that the roads which are wider than 1 pixel have smooth transitions, the solid_capstyle and the solid_endstyle are set to ‘round’.
To simplify conversion of the XML data in the OSM file into an object which behaves like a dictionary, a script from ActiveState was used.

The Code:

download source code
download example OSM file
#############################################################
## from http://code.activestate.com/recipes/534109-xml-to-python-data-structure/

import re
import xml.sax.handler

def xml2obj(src):
    """
    A simple function to converts XML data into native Python object.
    """

    non_id_char = re.compile('[^_0-9a-zA-Z]')
    def _name_mangle(name):
        return non_id_char.sub('_', name)

    class DataNode(object):
        def __init__(self):
            self._attrs = {}    # XML attributes and child elements
            self.data = None    # child text data
        def __len__(self):
            # treat single element as a list of 1
            return 1
        def __getitem__(self, key):
            if isinstance(key, basestring):
                return self._attrs.get(key,None)
            else:
                return [self][key]
        def __contains__(self, name):
            return self._attrs.has_key(name)
        def __nonzero__(self):
            return bool(self._attrs or self.data)
        def __getattr__(self, name):
            if name.startswith('__'):
                # need to do this for Python special methods???
                raise AttributeError(name)
            return self._attrs.get(name,None)
        def _add_xml_attr(self, name, value):
            if name in self._attrs:
                # multiple attribute of the same name are represented by a list
                children = self._attrs[name]
                if not isinstance(children, list):
                    children = [children]
                    self._attrs[name] = children
                children.append(value)
            else:
                self._attrs[name] = value
        def __str__(self):
            return self.data or ''
        def __repr__(self):
            items = sorted(self._attrs.items())
            if self.data:
                items.append(('data', self.data))
            return u'{%s}' % ', '.join([u'%s:%s' % (k,repr(v)) for k,v in items])

    class TreeBuilder(xml.sax.handler.ContentHandler):
        def __init__(self):
            self.stack = []
            self.root = DataNode()
            self.current = self.root
            self.text_parts = []
        def startElement(self, name, attrs):
            self.stack.append((self.current, self.text_parts))
            self.current = DataNode()
            self.text_parts = []
            # xml attributes --> python attributes
            for k, v in attrs.items():
                self.current._add_xml_attr(_name_mangle(k), v)
        def endElement(self, name):
            text = ''.join(self.text_parts).strip()
            if text:
                self.current.data = text
            if self.current._attrs:
                obj = self.current
            else:
                # a text only node is simply represented by the string
                obj = text or ''
            self.current, self.text_parts = self.stack.pop()
            self.current._add_xml_attr(_name_mangle(name), obj)
        def characters(self, content):
            self.text_parts.append(content)

    builder = TreeBuilder()
    if isinstance(src,basestring):
        xml.sax.parseString(src, builder)
    else:
        xml.sax.parse(src, builder)
    return builder.root._attrs.values()[0]



#############################################################



def render(myMap):



    # make dictionary of node IDs
    nodes = {}
    for node in myMap['node']:
        nodes[node['id']] = node
        
    ways = {}
    for way in myMap['way']:
        ways[way['id']]=way



    import pylab as p


    renderingRules = {
        'primary': dict(
                linestyle       = '-',
                linewidth       = 6,
                color           ='#ee82ee', 
                zorder          = -1,
                ),
        'primary_link': dict(
                linestyle       = '-',
                linewidth       = 6,
                color           = '#da70d6',
                zorder          = -1,            
                ),
        'secondary': dict(
                linestyle       = '-',
                linewidth       = 6,
                color           = '#d8bfd8',
                zorder          = -2,            
                ),
        'secondary_link': dict(
                linestyle       = '-',
                linewidth       = 6,
                color           = '#d8bfd8',
                zorder          = -2,            
                ),
        'tertiary': dict(
                linestyle       = '-',
                linewidth       = 4,
                color           = (0.0,0.0,0.7),
                zorder          = -3,            
                ),
        'tertiary_link': dict(
                linestyle       = '-',
                linewidth       = 4,
                color           = (0.0,0.0,0.7),
                zorder          = -3,            
                ),
        'residential': dict(
                linestyle       = '-',
                linewidth       = 1,
                color           = (0.1,0.1,0.1),
                zorder          = -99,            
                ),            
        'unclassified': dict(
                linestyle       = ':',
                linewidth       = 1,
                color           = (0.5,0.5,0.5),
                zorder          = -1,            
                ),
        'default': dict(
                linestyle       = '-',
                linewidth       = 3,
                color           = 'b',
                zorder          = -1,            
                ),
        }
                        

    # get bounds from OSM data            
    minX = float(myMap['bounds']['minlon'])
    maxX = float(myMap['bounds']['maxlon'])
    minY = float(myMap['bounds']['minlat'])
    maxY = float(myMap['bounds']['maxlat'])



    fig = p.figure()
    
    # by setting limits before hand, plotting is about 3 times faster
    ax = fig.add_subplot(111,autoscale_on=False,xlim=(minX,maxX),ylim=(minY,maxY))
    
    for idx,nodeID in enumerate(ways.keys()):
        wayTags         = ways[nodeID]['tag']
        if not wayTags==None:
            hwyTypeList  = [d['v'] for d in wayTags if d['k']=='highway']
            if len(hwyTypeList)>0:
                    wayType = hwyTypeList[0]  
            else:
                    wayType = None
        else:
            wayType = None
        try:
            if wayType in ['primary','primary_link',
                            'unclassified',
                            'secondary','secondary_link',
                            'tertiary','tertiary_link',
                            'residential',
                            'trunk','trunk_link',
                            'motorway','motorway_link',
                            ]:
                oldX = None
                oldY = None
                
                if wayType in renderingRules.keys():
                    thisRendering = renderingRules[wayType]
                else:
                    thisRendering = renderingRules['default']
                    
                for nCnt,nID in enumerate(ways[nodeID]['nd']):
                    y = float(nodes[nID['ref']]['lat'])
                    x = float(nodes[nID['ref']]['lon'])
                    if oldX == None:
                        pass
                    else:
                        p.plot([oldX,x],[oldY,y],
                            marker          = '',
                            linestyle       = thisRendering['linestyle'],
                            linewidth       = thisRendering['linewidth'],
                            color           = thisRendering['color'],
                            solid_capstyle  = 'round',
                            solid_joinstyle = 'round',
                            zorder          = thisRendering['zorder'],
                            )
                    oldX = x
                    oldY = y
                       
        except KeyError:
            pass
          
    print'Done Plotting'
    p.show()




########################################

src = file('map.osm')
myMap = xml2obj(src)
render(myMap)

Testing Configuration:

Other Useful Links and references:


Questions Answered:

  • How to visualize an Open Street Map from data
  • How to plot a map
  • How to draw an Open Street Map from the OSM file

Sunday, August 23, 2009

Release of Data Origami Library 0.006

The Data Origami Library for Blender 2.49a has been updated. This library's goal is to reduce the number of lines of python required to generate a useful image, animation, or game in Blender.

In this release, the image class now supports the following methods:

  • Orienting the camera by using a lookAt and lookFrom location
  • Adding Blocks defined by opposing corners
  • Adding Cylinders defined by end locations
  • Adding Spheres
  • Adding Text boxes
  • Adding Lighting which can be aimed at a target
  • Adding Tubes which can be used to draw arbitrary curves from either piecewise lines or smooth curves.

The library can be downloaded here. To install, simply unzip to a working directory and execute the test scripts from the Blender command line.

The images above were generated by the test scripts contained in the zip files.

This example shows how the library can be used to generate an image with just a few lines of python.

#BPY
# setup to allow the module to be imported from the same directory
#   the script is in
import sys
import os
filename = sys.argv[2]
filepath = os.path.dirname(filename)
sys.path.insert(0,filepath)
import OLib as MyLib

import Blender
import Blender.Mathutils
from Blender.Mathutils import *
from Blender import *

image = MyLib.Image('MyScene')
#image.lookFrom(3.5,3,15)
image.lookFrom(15,5,15)
image.lookAt(0,0,1)

ob = image.addPolyline()
image.addTube(tubeShape=ob)
image.addCylinder(p1=[0,0,0],p2=[0,0,5])

image.save(filepath+'\\images\\'+'test001_Tube.jpg')

To copy the code snippets easily, see:http://dataorigami.blogspot.com/2009/06/how-to-easily-copy-code-snippets-from.html

To execute the script in Blender, see:http://dataorigami.blogspot.com/2009/06/how-to-execute-script-from-command-line.html

To work with the script in a simple IDE, see:http://dataorigami.blogspot.com/2009/04/developing-scripts-for-blender.html

Sunday, August 16, 2009

Script to find the type of each object in a Blender scene

This examples applies to Blender 2.49 and some earlier versions.
import Blender
from Blender import *
scn = Scene.GetCurrent()
obs = scn.objects
for ob in obs:
    print 'Object named %s has type %s' % (ob.name,ob.type)

Monday, August 10, 2009

Release of Data Origami Library 0.002

The Data Origami Library has been updated. The image class now supports the following objects:
  • Orienting the camera by using a lookAt and lookFrom location
  • Blocks defined by opposing corners
  • Cylinders defined by end locations
  • Spheres
  • Text boxes

An issue with shading was corrected. The library now generates shadows correctly.

The library can be downloaded here.

The image above was generated by the test script here.

Sunday, August 2, 2009

The 0.001 Release of the Data Origami Library for Blender

This is a shell that will be expended in the future. It simplifies the problem of setting up an image in Blender. Other postings will build examples on top of this module.
For now, it is recommended to place this file in C:\tmp. I'll use that location in example scripts. A future release will have a more permanent recommended home.
Code: Download Code for the Origami Library 0.001. This file can be placed arbitrarily. Any script which imports this module must reference it by the full path because of how Blender handles files.
#!BPY
__doc__ = """
OLib_xxxx.py
Rev 0.001
This module setups up a scene for static rendering, animation or game engine operation
This module provides useful library functions.

This module does nothing from the Blender command line

------------------
History:
rev 0.001 8/2/2009
   Initial release to test structure and functionality
 edt 
"""

__author__ = "edt"
__version__ = "0.001 2009/08/03"
__url__="Website, dataOrigami.blogspot.com"
##############################################################

__doc__ = """
This module setups up a scene for static rendering, animation or game engine operation
This module imports useful library functions.

rev 0.001
   Initial release to test structure and functionality
 edt 8/2/2009
"""

 
import Blender
import Blender.Mathutils
from Blender.Mathutils import *

class Image(object):
 def __init__(self,name='Scene'):
  # create new scene
  scene = Blender.Scene.New(name)
  scene.makeCurrent()
  ##############################################################
  # add a camera and set it up
  #     
  camdata = Blender.Camera.New() 
  cam = scene.objects.new(camdata) 
  # use setLocation to control the position of the camera
  scene.objects.camera = cam
  self.cam   = cam
  self._lookAt_x = 0
  self._lookAt_y = 0
  self._lookAt_z = 0
  self.lookFrom(2.481,-1.508,1.1)
  ##############################################################
  # add a lamp and set it up
  #
  lampData = Blender.Lamp.New() 
  lampData.setEnergy(1.0) 
  lampData.setType('Lamp')
  lampData.mode |= Blender.Lamp.Modes["RayShadow"]  # make shadows appear
  lamp = scene.objects.new(lampData) 
  lamp.setLocation(2.0,2.0,5.0) 
  lamp.setEuler(120*(3.1415/180),30*(3.1415/180),-30*(3.1415/180))
  #
  ################################################################
  # setup world
  world = Blender.World.New(name+'_World')
  world.setCurrent()
  
  
  self.scene = scene
  self.lamp  = lamp
  self.world = world
 
 def lookFrom(self,*args):
  if len(args)==1:
   # we have an object which contains look information
   x = args[0].x
   y = args[0].y
   z = args[0].z
  elif len(args)==3:
   # we have x,y,z coordinates
   x = args[0]
   y = args[1]
   z = args[2]
  # set camera position to x,y,z 
  self.cam.setLocation(x,y,z)
  self.lookAt()
 
 def lookAt(self,*args):
  if len(args)==0:
   to_x = self._lookAt_x
   to_y = self._lookAt_y
   to_z = self._lookAt_z
   up_x = 0
   up_y = 1
   up_z = 0
  elif len(args)==1:
   # we have an object which contains look information
   to_x = args[0].x
   to_y = args[0].y
   to_z = args[0].z
   up_x = 0
   up_y = 1
   up_z = 0
  elif len(args)==2:
   # we have an object which contains look information
   to_x = args[0].x
   to_y = args[0].y
   to_z = args[0].z
   up_x = args[1].x
   up_y = args[1].y
   up_z = args[1].z
  elif len(args)==3:
   # we have x,y,z coordinates
   to_x = args[0]
   to_y = args[1]
   to_z = args[2]
   up_x = 0
   up_y = 1
   up_z = 0
  elif len(args)==6:
   # we have x,y,z coordinates
   to_x = args[0]
   to_y = args[1]
   to_z = args[2]
   up_x = args[3]
   up_y = args[4]
   up_z = args[5]
  # save view target
  self._lookAt_x = to_x
  self._lookAt_y = to_y
  self._lookAt_z = to_z
  # set camera position to x,y,z 
  from_x, from_y, from_z = self.cam.getLocation()
  p1 = Vector([from_x,from_y,from_z])
  p2 = Vector([to_x,to_y,to_z])
  
  # form a vector that points in the direction from p1 to p2
  dir = p2-p1                  
  #print 'from='+str(p1)
  #print 'to='+str(p2)
  #print 'dir='+str(dir)
  
  dir.normalize()
  #u = dir
  up = Vector([up_x,up_y,up_z])
  up.normalize()
  dir_ref = Vector([0,0,-1.0])   # direction the camera naturally points w/o rotation
  if True: #abs(AngleBetweenVecs(dir,dir_ref))>1e-6:
   d = -dir
   e = up-up*DotVecs(dir,up)
   e.normalize()
   f = -CrossVecs(d,e)
   A = Matrix(
     [f[0],f[1],f[2],0],
     [e[0],e[1],e[2],0],
     [d[0],d[1],d[2],0],
     [from_x,from_y,from_z,1])
  else:
   # the direction of view is parallel to the z-axis
   A = Matrix(
     [1,0,0,0],
     [0,1,0,0],
     [0,0,1,0],
     [from_x,from_y,from_z,1]) 
  # apply the transform to the camera   
  self.cam.setMatrix(A)

 def add(self,item,name='noName'):
  # this method allows any object to be added to the scene
  ob = self.scene.objects.new(item,name)
  return ob
  
 def delete(self,item):
  self.scene.objects.unlink(item)
  
 def addSphere(self,**kwargs):
  # default the values
  diameter = kwargs.get('diameter',1.0)
  segments = kwargs.get('segments',8)
  rings = kwargs.get('rings',8)
  loc   = kwargs.get('location',[0,0,0])
  useIco = kwargs.get('useIco',False)
  useUV = kwargs.get('useUV',True)
  subdivisions = kwargs.get('subdivisions',2)
  name = kwargs.get('name','sphere')
  if useIco:
   sphere = Blender.Mesh.Primitives.Icosphere(subdivisions,diameter)
  else: 
   sphere = Blender.Mesh.Primitives.UVsphere(segments,rings,diameter)
  ob = self.add(sphere,name)  
  ob.setLocation(loc)
  return ob
  
 def addCube(self,*kwargs):
  pass
  
 def addCylinder(self,**kwargs):
  pass
  
 def addPlane(self,**kwargs):
  pass
 
 def addText(self,**kwargs):
  loc   = kwargs.get('location',[0,0,0])
  name  = kwargs.get('name','sphere')
  txtStr= kwargs.get('text','Default Message')
  width = kwargs.get('width',1.0)
  height= kwargs.get('height',1.0)
  col   = kwargs.get('color',[1.0,1.0,1.0])
  align = kwargs.get('align','LEFT')
  txt = Blender.Text3d.New()
  txt.setText(txtStr)
  txt.setSize(0.1)
  alignDict = {'LEFT':Blender.Text3d.LEFT,'RIGHT':Blender.Text3d.RIGHT,
      'MIDDLE':Blender.Text3d.MIDDLE, 'FLUSH':Blender.Text3d.FLUSH}
  txt.setAlignment(alignDict[align])
  mat=Blender.Material.New('textMat')
  mat.rgbCol = col
  ob = self.add(txt)
  me = Blender.Mesh.New('textMesh')
  me.getFromObject(ob)
  me.materials += [mat]
  self.delete(ob)
  newOb = self.add(me,name)
  
  # force a redraw to ensure that the bounding box is updated!!
  Blender.Window.RedrawAll() 
  boundBox = newOb.getBoundBox(1)
  upperBox = max(boundBox)
  lowerBox = min(boundBox)
  initialWidth = upperBox[0]-lowerBox[0]
  initialHeight = upperBox[1]-lowerBox[1]
  widthRatio = width/initialWidth
  heightRatio = height/initialHeight
  ratio = min(widthRatio,heightRatio)
  
  newOb.setSize(ratio,ratio,ratio)
  newOb.setLocation(loc)
  return newOb


 
 def addWorld(self,**kwargs):
  pass
 
 def drawNow(self,**kwargs):
  xSize = kwargs.get('xSize',1024)
  ySize = kwargs.get('ySize',800)
  overSampleLevel = kwargs.get('oversamplingLevel',16)
  #print 'Image.drawNow() executing'
  self.context = self.scene.getRenderingContext() 
  # enable seperate window for rendering 
  Blender.Scene.Render.EnableDispWin() 
  self.context.imageType = Blender.Scene.Render.JPEG
  if overSampleLevel>0:
   self.context.enableOversampling(True)
   self.context.setOversamplingLevel(overSampleLevel)
  else:
   self.context.enableOversampling(False)   
  self.context.imageSizeX(xSize)
  self.context.imageSizeY(ySize)
  # draw the image 
  self.context.render()   
 
 def save(self,filename='image.jpg'):
  self.drawNow()
  self.context.saveRenderedImage(filename) 

class Animation(object):
 pass
 
class BEG(object):
 pass


# This does not work with Blender
#if name=='__main__':
# # test/demo codes here
# pass
 

To copy the code snippets easily, see:http://dataorigami.blogspot.com/2009/06/how-to-easily-copy-code-snippets-from.html

To execute the script in Blender, see:http://dataorigami.blogspot.com/2009/06/how-to-execute-script-from-command-line.html

To work with the script in a simple IDE, see:http://dataorigami.blogspot.com/2009/04/developing-scripts-for-blender.html

Using Blender Subsurfaces from Python

In Blender, one of the ways to improve the smoothness of an object is to use subsurfaces. Programatically, this is done by changing the modifiers attached to an object. In this example, a UVsphere is modified by changing the number of rings and segments. The same basic UVsphere is also modified by adding subsurface levels. From looking at the table, it is possible to see how increasing subsurface level and rings/segments both get a smooth looking sphere.

To get this example to run, you will need to copy the Data Origami Library rev 0.001 to C:\tmp (or modify the path to point to your location of choice).

Code:
Download Code to draw the spheres with different rings, segments and subsurface levels.

#BPY
__doc__ = """
Sphere Subsurface Example
Rev 1.0
This script demonstrates the effect of subsurface. 
This script depends on the Origami Library and expects it to be located at C:\tmp.

This script is executed at the command line by:>blender -P subsurf_ex_XXXX.py"""

__author__ = "edt"
__version__ = "1.0 2009/08/03"
__url__="Website, dataOrigami.blogspot.com"

##############################################################


import sys
# make sure a copy of OLib_001.py is at C:\tmp
# add to the system path to point to the file to import
sys.path.append("c:\\tmp")

import OLib_001 as MyLib
import Blender
import Blender.Mathutils
from Blender.Mathutils import *
from Blender import *

image = MyLib.Image('MyScene')
image.lookFrom(3.5,3,15)
image.lookAt(3.5,3,0)

image.addText(text='UVSpheres with SubSurfaces',width=5,height=1,
   location=[5,6.5,0],align='MIDDLE')

xList = range(0,10)
ringList = [3,4,5,6,7,8,9,10,11,12]
segmentList = ringList

image.addText(text='Num Rings =',width=10,height=0.4,
   location=[-0.25,-0.75,0],align='RIGHT',
   color=[1.0,0.0,0.0])
ob=image.addText(text='Num SubSurfaces', width=20,height=0.4,
            location=[-1,3,0],align='MIDDLE',
   color=[0.0,1.0,0.0])
ob.setEuler(0,0,90*3.1415/180.0)
for i in range(0,len(ringList)):
 rings = ringList[i]
 segments = segmentList[i]
 image.addText(text=str(rings),width=1,height=0.4,
    location=[xList[i],-0.75,0],align='MIDDLE',
    color=[1.0,0.0,0.0])
 for subSurfLevel in range(0,7):
  # add a UVsphere
  UVSphereOb = image.addSphere(diameter=0.5,rings=rings, 
   segments=segments,location=[xList[i],subSurfLevel,0])
  mods = UVSphereOb.modifiers
  mod = mods.append(Modifier.Types.SUBSURF)   # add a new subsurf modifier
  mod[Modifier.Settings.LEVELS] = subSurfLevel # set subsurf subdivision levels to 3
  image.addText(text=str(subSurfLevel),width=1,height=0.4,
  location=[-0.5,subSurfLevel-0.2,0],align='RIGHT',
  color=[0.0,1.0,0.0])
  
image.save('SubSurfaceExample.jpg')



To copy the code snippets easily, see:http://dataorigami.blogspot.com/2009/06/how-to-easily-copy-code-snippets-from.html

To execute the script in Blender, see:http://dataorigami.blogspot.com/2009/06/how-to-execute-script-from-command-line.html

To work with the script in a simple IDE, see:http://dataorigami.blogspot.com/2009/04/developing-scripts-for-blender.html

Sunday, July 19, 2009

How to draw a curve in Blender using Python - A Quick-N-Dirty Example

In a previous example, curves were generated using cylinder and spheres. This is computationally expensive and does not fully leverage Blender's capabilities. Rather than building curves using primitive meshes, the Blender.Curve objects can be used. This example demonstrates a simple script which creates a curve in 3D using Blender curves.

Code:
Download Code
#!BPY
__doc__ = """
curveTest_xxxx.py
Rev 1.0

The purpose of this script is to demonstrate the use of blender curves.

This script is executed at the command line by:
>blender -P curveTest_xxxx.py
"""
__author__ = "edt"
__version__ = "1.0 2009/07/19"
__url__="Website, dataOrigami.blogspot.com"


##############################################################
# load the modules used in the script
from Blender import Curve, Object, Scene
from Blender.Scene import Render 
from Blender import *

##############################################################
# Get rid of the cube from the default scene
scene = Scene.GetCurrent() 
for ob in scene.objects:
   if (cmp(ob.getName(),'Cube')==0):
     scene.objects.unlink(ob)
##############################################################
# misc setup

cu = Curve.New()             # create new  curve data
scn = Scene.GetCurrent()    # get current scene
ob = scn.objects.new(cu)     # make a new curve from the curve data

##############################################################

def bezList2Curve(bezier_vecs):
    '''
    Take a list or vector triples and converts them into a bezier curve object
    '''
    
    def bezFromVecs(vecs):
      '''
      Bezier triple from 3 vecs, shortcut functon
      '''
      bt= BezTriple.New(vecs[0].x, vecs[0].y, vecs[0].z,                        
     vecs[1].x, vecs[1].y, vecs[1].z,
     vecs[2].x, vecs[2].y, vecs[2].z)
      bt.handleTypes= (BezTriple.HandleTypes.FREE, BezTriple.HandleTypes.FREE)
      return bt
    
    # Create the curve data with one point
    cu= Curve.New()
    cu.appendNurb(bezFromVecs(bezier_vecs[0])) # We must add with a point to start with
    cu_nurb= cu[0] # Get the first curve just added in the CurveData
    i= 1 # skip first vec triple because it was used to init the curve
    while i<len(bezier_vecs):
      bt_vec_triple= bezier_vecs[i]
      bt= bezFromVecs(bt_vec_triple)
      cu_nurb.append(bt)
      i+=1
    
    # Add the Curve into the scene
    cu.setFlag(cu.getFlag() | 1)  # this fixes a visibility issue with curves in z axis
    scn= Scene.GetCurrent()
    ob = scn.objects.new(cu)
    return ob

##############################################################
# class to make it easier to pass info to the curve function
    
from Blender.Mathutils import *
class Point(object):
 def __init__(self,x,y,z):
  self.x = x
  self.y = y
  self.z = z

##############################################################
# create the curve of interest from Bezier knots and handles
 
# bezier points -> handle, knot, handle 
bezier_vecs = [ [Point(1,0,0),Point(-1.1,0,0),Point(1,0,0)],
    [Point(1,1,0),Point(1.1,1.1,0),Point(1,1,0)],
    [Point(0,1,0),Point(0,1.1,0),Point(0,1,0)],
    [Point(1,0,1),Point(1.1,1.1,1.1),Point(1,0,1)]]
ob = bezList2Curve(bezier_vecs)
mat = Material.New('curveMat') # create a new Material called 'newMat' 
mat.rgbCol = [1.0, 1.0, 0.0]      # change its color 
ob.getData(False,True).materials += [mat]
ob.getData(False,True).ext1 = 0.1

##############################################################
# draw axes for reference

bezier_vecs = [ [Point(-2,0,0),Point(-2,0,0),Point(-2,0,0)],
    [Point(2,0,0),Point(2,0,0),Point(2,0,0)]]
ob = bezList2Curve(bezier_vecs)
mat = Material.New('curveMat') # create a new Material called 'newMat' 
mat.rgbCol = [1.0, 1.0, 1.0]      # change its color 
ob.getData(False,True).materials += [mat]
ob.getData(False,True).ext1 = 0.1

bezier_vecs = [ [Point(0,-2,0),Point(0,-2,0),Point(0,-2,0)],
    [Point(0,2,0),Point(0,2,0),Point(0,2,0)]]
ob = bezList2Curve(bezier_vecs)
ob.getData(False,True).materials += [mat]
ob.getData(False,True).ext1 = 0.1

bezier_vecs = [ [Point(0,0,-2),Point(0,0,-2),Point(0,0,-2)],
    [Point(0,0,2),Point(0,0,2),Point(0,0,2)]]
ob = bezList2Curve(bezier_vecs)
ob.getData(False,True).materials += [mat]
ob.getData(False,True).ext1 = 0.1

#######################################
# add spheres to points in space to make the gridding understandable
# and make it easier to see what the curve is doing.

xList = range(-2,3,2)
yList = range(-2,3,2)
zList = range(-2,3,2)

for x in xList:
 for y in yList:
  for z in zList:
   markerMe = Mesh.Primitives.UVsphere(8,8,0.3/2.0)
   A = Matrix(
    [1,0,0,0],
    [0,1,0,0],
    [0,0,1,0],
    [x,y,z,1]) 
   markerMe.transform(A,True)
   if x==0 and y==0 and z==0:
    mat2 = Material.New('curveMat') # create a new Material called 'newMat' 
    mat2.rgbCol = [0.0, 0.0, 1.0] 
    markerMe.materials += [mat2]
   else:
    markerMe.materials += [mat]
   markerOb = scene.objects.new(markerMe,'marker')

#######################################
# render the image and save it
#
context = scn.getRenderingContext() 
# enable seperate window for rendering 
Render.EnableDispWin() 
context.imageType = Render.JPEG
# draw the image 
context.render() 
# save the image to disk 
# to the location specified by RenderPath 
# by default this will be a jpg file 
context.saveRenderedImage('CurveExample.jpg') 

Window.RedrawAll() 
#
########################################

To copy the code snippets easily, see:http://dataorigami.blogspot.com/2009/06/how-to-easily-copy-code-snippets-from.html

To execute the script in Blender, see:http://dataorigami.blogspot.com/2009/06/how-to-execute-script-from-command-line.html

To work with the script in a simple IDE, see:http://dataorigami.blogspot.com/2009/04/developing-scripts-for-blender.html

Friday, July 3, 2009

Quick-N-Dirty: A rough example of an interactive data visualization in the Blender Game Engine

This example combines the Blender Game Engine example with the graphing example to allow simple navigation of the camera while looking at a graph. This example needs some fine tuning to improve the performance. The graph has too many elements to render quickly. However, it does demonstrate how to get all of the elements to work together in a data visualization. To run this example, construct the blender file described in this link. Save this file to the '\tmp' directory. Copy the code below to a python file and execute from the command line. For detailed instructions for each of these steps, see the links at the end of the posting. Once the script runs, Blender should open and draw the graph in the 3D window with the view defaulted to the camera view. Press the 'P' key to start the Blender Game Engine. Code:
#!BPY

__doc__ = """
PlotExample02.py

Example demonstrating the following techniques in Blender 2.48:
1) Use of the transform matrix
2) Use of material properties to control how objects are rendered
3) Delete (unlink) and replacement of default objects in a scene
4) Use of cross products to generate a basis for the transform matrix
5) Using python to render an image
6) Using python to save the rendered image to disk
7) Use of Blender Vector and Matrix classes
8) Defining a function in Python
9) Adding text using text3d
10) Sizing text to a box using getBoundingBox()

This script is executed at the command line by:
>blender -P plotExample02.py
"""
__author__ = "edt"
__version__ = "1.0 2009/07/01"
__url__="Website, dataOrigami.blogspot.com"

##########################################################3
import Blender
import bpy
from Blender import *
from Blender.Scene import Render
from Blender import Text

##############################################################
# load the template blender file
Blender.Load('c:\\tmp\\bgeExample004.blend')

##############################################################
# load the modules used in the script
import Blender
import bpy
from Blender import *
from Blender.Scene import Render
from Blender import Text
from Blender import Mathutils
from Blender.Mathutils import *
import math

##############################################################
# define function(s)

def frange(start, end=None, inc=None):
    "A range function, that does accept float increments..."

    if end == None:
        end = start + 0.0
        start = 0.0

    if inc == None:
        inc = 1.0

    L = []
    while 1:
        next = start + len(L) * inc
        if inc < 0 and next >= end:
            break
        elif inc < 0 and next <= end:
            break
        L.append(next)
      
    return L


def lineSegMe(p1,p2,dia=0.1,verts=16):
 """
 This function returns a mesh which forms a line from point p1 to p2.
 The points can be passes as either blender vectors or lists of [x,y,z] points.
 This line is cylinder which goes from point p1 to p2.
 Optionally the diameter and number of vertices used to describe the line are passed.
 -------------------
 The line is formed by creating a cylinder with a length equal to the distance
 point p1 and p2. The line is then oriented using the transform matrix to rotate
 and translate the cylinder.
 """
 # use the class constructors from Blender to form vectors for p1 and p2
 p1 = Vector(p1)
 p2 = Vector(p2)

 # form a vector that points in the direction from p1 to p2
 dir = p2-p1                

 # get the length of the line we want that goes from p1 to p2
 length = dir.length

 # use Mesh.Primitives.Cylinder to create a mesh for the line
 me = Mesh.Primitives.Cylinder(verts,dia,length)

 ###############
 # in the next few steps, the direction vector is used to form a basis
 #      see http://en.wikipedia.org/wiki/Basis_(linear_algebra)
 # which allows us to create a transform matrix to rotate the cylinder
 # along the direction we want. The basic idea is that the vector from
 # p1 to p2 points in the direction we want. The cylinder created by
 # Mesh.Primitives.Cylinder is oriented along the z-axis. To rotate the
 # cylinder, we  # rotate the z-axis in this direction. To completely specify
 # how to rotate, we need to provide information on how to rotate the x and y axes.
 # To define this, a matrix which is orthonormal (see http://en.wikipedia.org/wiki/Orthonormal)
 # is created from the direction vector. To create the other vectors in the
 # orthonormal basis, cross products are used to find orthogonal vectors.
 #
 # use the normalize function to set the length of the direction vector to 1
 dir.normalize()
 u = dir
 uu = Vector([0,0,1.0])
 #print AngleBetweenVecs(u,uu)
 if (abs(AngleBetweenVecs(u,uu))%180.0)>1e-3:
  # the direction of the line is different
  # from the z-axis
  # find the orthonormal basis
  v = CrossVecs(u,uu)
  w = CrossVecs(u,v)
  # form the transform matrix:
  #   > The first 3 rows and 3 columns form
  #   a rotation matrix because the any vertex transformed by this
  #   matrix will be the same distance from the origin as the original
  #   vertex. If this property is not preserved, then any shape formed
  #   will be skewed and scaled by the transform.
  #   > The first 3 columns in the last row define the translation
  #   applied to any vertex. In this function, the translation move the
  #   moves the end of the cylinder to the origin, then moves the end
  #   to p1.
  A = Matrix(
    [w[0],w[1],w[2],0],
    [v[0],v[1],v[2],0],
    [u[0],u[1],u[2],0],
    [dir[0]/2.0*length+p1[0],dir[1]/2.0*length+p1[1],dir[2]/2.0*length+p1[2],1])
 else:
  # the direction of the line is parallel to the z-axis
  # see the notes above on how the matrix is formed.
  A = Matrix(
    [1,0,0,0],
    [0,1,0,0],
    [0,0,1,0],
    [dir[0]/2.0*length+p1[0],dir[1]/2.0*length+p1[1],dir[2]/2.0*length+p1[2],1])

 # apply the transform to the cylinder 
 me.transform(A,True)
 return me

def curve(pList,color=[1.0,1.0,1.0],dia=0.1,verts=4):
 lineMeList = []
 mat = Material.New('lineMat')  # create a new Material for the line
 mat.rgbCol = color     # change the color of the line
 for i in range(0,len(pList)-1):
  p1 = pList[i]
  p2 = pList[i+1]
  lineMe = lineSegMe(p1,p2,dia,verts)
  lineMe.materials += [mat]
  lineMeList.append(lineMe)
  # check to see if another line segment will follow
  if i<len(pList)-2:
   jointMe = Mesh.Primitives.UVsphere(verts,verts,dia/2.0)
   A = Matrix(
    [1,0,0,0],
    [0,1,0,0],
    [0,0,1,0],
    [p2[0],p2[1],p2[2],1])
   jointMe.transform(A,True)
   jointMe.materials += [mat]
   lineMeList.append(jointMe)
 return lineMeList


def scrubScene(saveList=[]):
 'removes all objects in scene, except objects in save list'
 scene = Scene.GetCurrent()
 for ob in scene.objects:
  if not ob.getName() in saveList:
   scene.objects.unlink(ob)

def combineMeshesIntoOb(meList,obName):
 'adds all meshes in meList into a single object in the scene'
 # TODO: add logic to detect empty list
 # TODO: add logic to enable/disable joining
 join = False
 rooted = False
 for me in meList:
  if not rooted:
   combinedOb= scene.objects.new(me,obName)
   rooted = True
  else:
   localCombinedOb = scene.objects.new(me,'local'+obName)
   if join:
    combinedOb.join([localCombinedOb])
    scene.objects.unlink(localCombinedOb)
 return combinedOb

def textInBox(txtStr="defaultText",col=[1.0,1.0,1.0],width=1.0,height=1.0):
 txt = Text3d.New()
 txt.setText(txtStr)
 txt.setSize(0.1)
 mat=Material.New('textMat')
 mat.rgbCol = col
 ob = scene.objects.new(txt)
 me = Blender.Mesh.New('textMesh')
 me.getFromObject(ob)
 me.materials += [mat]
 scene.objects.unlink(ob)
 newOb = scene.objects.new(me)

 # force a redraw to ensure that the bounding box is updated!!
 Window.RedrawAll()
 boundBox = newOb.getBoundBox(1)
 upperBox = max(boundBox)
 lowerBox = min(boundBox)
 initialWidth = upperBox[0]-lowerBox[0]
 initialHeight = upperBox[1]-lowerBox[1]
 widthRatio = width/initialWidth
 heightRatio = height/initialHeight
 ratio = min(widthRatio,heightRatio)

 newOb.setSize(ratio,ratio,ratio)
 newOb.setMaterials([mat])
 return newOb


##############################################################
##############################################################
##############################################################
# name for the embedded script in the loaded file
gameScriptName = 'MainScript.py'

##############################################################
# Get rid of the lamp and cube from the default scene
#     - leave the camera, it has the linkages for the scripting
#       for the BGE.
#     - Note: Do NOT make any object the BGE scripts invisible
#       via ob.layers=[], this will disable the scripts
#       attached to the object.
scene = Scene.GetCurrent()
for ob in scene.objects:
   print ob.getName()
   if ((cmp(ob.getName(),'Lamp')==0)
    (cmp(ob.getName(),'Cube')==0)):
  scene.objects.unlink(ob)


##############################################################
# find camera in file and set it up
#   
#camdata = Camera.New()
#cam = scene.objects.new(camdata)
cam = Blender.Object.Get('Camera')
# use setLocation to control the position of the camera
cam.setLocation(0.9,0.5,2.1)
# use set Euler to control the angle of the camera
cam.setEuler(0*(3.1415/180),10*(3.1415/180),0*(3.1415/180))
scene.objects.camera = cam
##############################################################
# add a lamp and set it up
#
lampData = Lamp.New()
lampData.setEnergy(1.0)
lampData.setType('Lamp')
lampData.mode = Lamp.Modes["RayShadow"]  # make shadows appear
lamp = scene.objects.new(lampData)
lamp.setLocation(2.0,2.0,5.0)
lamp.setEuler(120*(3.1415/180),30*(3.1415/180),-30*(3.1415/180))
##############################################################
# load the data
#
f = open('c:\\tmp\\EmploymentPopRatio.txt', 'r')
foundData = False
data = {'x':[],'y':[],'z':[]}
labels = {'x':[],'y':[],'z':[]}
for line in f:
 fields = line.split(',')
 if not foundData:
  for entry in fields:
   if entry == 'Year':
    # this is the header row of data
    foundData = True
 else:
  print len(line)
  if len(line)>1:
   for i in range(0,13):
    if i==0:
     print line
     print fields[0]
     year =  float(fields[0])
    else:
     month = i
     if not fields[i]==' ':
      print '::'+fields[i]+''
      data['x'].append(float(year+(month-1)/12.0))
      labels['x'].append(str(year)+','+str(month))
      data['y'].append(float(fields[i]))
      data['z'].append(0.05)
 

#############################
#############################

axisList = ['x','y','z']


# setup the data ranges

graphSetup = {}
graphSetup["x"]={'max':2010,'min':1940,'inc':10}
graphSetup["y"]={'max':70,'min':50,'inc':5}
graphSetup["z"]={'max':1,'min':0,'inc':1}
graphSetup["title"]='Employment to Population Ratio'
graphSetup["xLabels"]=[]
for x in frange(graphSetup['x']['min'],graphSetup['x']['max']+graphSetup['x']['inc'],graphSetup['x']['inc']):
 graphSetup['xLabels'].append(str(x))

print graphSetup['xLabels']

# scale the data for presentation
scale={}
offset={}
for axis in axisList:
 scale[axis]=1.0/float(graphSetup[axis]['max']-graphSetup[axis]['min'])
 offset[axis] = float(graphSetup[axis]['min'])

# build point list
pointList = []
for i in range(0,len(data['x'])):
 p = {}
 for axis in scale.keys():
  p[axis]=(data[axis][i]-offset[axis])*scale[axis]
 point = Vector([p['x'],p['y'],p['z']])
 pointList.append(point)

##############################################################
# create the objects in the scene and bind materials to them
# draw the data from the graph
meList = curve(pointList,[1.0,0.1,0.4],0.01,8)
combineMeshesIntoOb(meList,'curveOb')
#
##############################
# create a grid for the different axes
#
tics = {}
scaledTics = {'x':[],'y':[],'z':[]}
for axis in scaledTics.keys():
 tics[axis]=frange(graphSetup[axis]['min'],
      graphSetup[axis]['max']+graphSetup[axis]['inc'],
      graphSetup[axis]['inc'])
 for tic in tics[axis]:
  scaledTics[axis].append((tic-offset[axis])*scale[axis])

# draw x-y axes

def plotGrid(tics={'x':[],'y':[],'z':[]},axes=['x','y'],col=[1.0,1.0,0.0],isTics=False):
 ''
 def plotGridLines(tics,axis,P1,P2,col):
  'plot the grid elements'
  for tic in tics[axis]:
   R = Vector([tic,1.0,0.0])
   point1 = P1*R
   point2 = P2*R
   pointList = [point1,point2]
   meList = curve(pointList,col,0.01,8)
   combineMeshesIntoOb(meList,'GridElement')
 #
 def plotGridCorners(cornerLocs,col):
  mat = Material.New('axisMat') # create a new Material called 'newMat'
  mat.rgbCol = col
  for i in range(0,len(cornerLocs)):
   cornerMe = Mesh.Primitives.UVsphere(32,32,0.01)
   cornerOb = scene.objects.new(cornerMe,'originOb')
   cornerOb.getData(False,True).materials+=[mat]
   cornerOb.setLocation(cornerLocs[i][0],cornerLocs[i][1],cornerLocs[i][2])
 #
 if isTics:
  gridLen = 0.1
 else:
  gridLen = 1.0
 if 'x' in axes and 'y' in axes:
  P1 = Matrix([1.0,0.0,0.0],[0.0,0.0,0.0],[0.0,0.0,0.0])
  P2 = Matrix([1.0,0.0,0.0],[0.0,gridLen,0.0],[0.0,0.0,0.0])
  plotGridLines(tics,'x',P1,P2,col)
  P1 = Matrix([0.0,gridLen,0.0],[1.0,0.0,0.0],[0.0,0.0,0.0])
  P2 = Matrix([0.0,0.0,0.0],[1.0,0.0,0.0],[0.0,0.0,0.0])
  plotGridLines(tics,'y',P1,P2,col)
  if isTics:
   cornerLocs =[[0,0,0]]
  else:
   cornerLocs =[[0,0,0],[0,1,0],[1,1,0],[1,0,0]]
  plotGridCorners(cornerLocs,col)
 elif 'x' in axes and 'z' in axes:
  P1 = Matrix([1.0,0.0,0.0],[0.0,0.0,0.0],[0.0,0.0,0.0])
  P2 = Matrix([1.0,0.0,0.0],[0.0,0.0,0.0],[0.0,gridLen,0.0])
  plotGridLines(tics,'x',P1,P2,col)
  P1 = Matrix([0.0,0.0,0.0],[0.0,0.0,0.0],[1.0,0.0,0.0])
  P2 = Matrix([0.0,gridLen,0.0],[0.0,0.0,0.0],[1.0,0.0,0.0])
  plotGridLines(tics,'z',P1,P2,col)
  if isTics:
   cornerLocs =[[0,0,0]]
  else:
   cornerLocs =[[0,0,0],[0,1,0],[0,1,1],[0,0,1]]
  plotGridCorners(cornerLocs,col)
 elif 'y' in axes and 'z' in axes:
  P1 = Matrix([0.0,0.0,0.0],[1.0,0.0,0.0],[0.0,0.0,0.0])
  P2 = Matrix([0.0,0.0,0.0],[1.0,0.0,0.0],[0.0,gridLen,0.0])
  plotGridLines(tics,'y',P1,P2,col)
  P1 = Matrix([0.0,0.0,0.0],[0.0,gridLen,0.0],[1.0,0.0,0.0])
  P2 = Matrix([0.0,0.0,0.0],[0.0,0.0,0.0],[1.0,0.0,0.0])
  plotGridLines(tics,'z',P1,P2,col)
  if isTics:
   cornerLocs =[[0,0,0]]
  else:
   cornerLocs =[[0,0,0],[0,1,0],[1,1,0],[1,0,0]]
  plotGridCorners(cornerLocs,col)

plotGrid(scaledTics,axes=['x','y'],col=[1.0,1.0,0.0],isTics=False)
#plotGrid(scaledTics,axes=['x','z'],col=[1.0,1.0,0.0],isTics=False)
#plotGrid(scaledTics,axes=['y','z'],col=[1.0,1.0,0.0],isTics=False)

##############################
# create x-axis labels
#
for i in range(0,len(graphSetup['xLabels'])):
 label = graphSetup['xLabels'][i]
 xLoc = scaledTics['x'][i]
 xTicOb=textInBox(label,[1.0,0.0,0.0],0.1,0.1)
 xTicOb.setLocation(xLoc,-0.01,0.05)
 xTicOb.setEuler(0.0,0.0,-90.0*(3.1415/180.0))

##############################
# create y-axis labels
#
for i in range(0,len(tics['y'])):
 yLabel = str(tics['y'][i])+'%'
 yLoc = scaledTics['y'][i]
 yTicOb=textInBox(yLabel,[1.0,0.0,0.0],0.1,0.1)
 yTicOb.setLocation(-0.11,yLoc-0.025,0.05)

##############################
# create a title
#
titleOb=textInBox(graphSetup['title'],[1.0,0.0,0.0],1.0,1.0)
titleOb.setLocation(0.0,1.05,0.05)

##############################
#
mat = Material.New('xyBackMat') # create a new Material called 'newMat'
mat.rgbCol = [1.0, 1.0, 1.0]      # change its color
xyBackMe = Mesh.Primitives.Grid(2,2)
A = Matrix(
 [0.5,0,0,0],
 [0,0.5,0,0],
 [0,0,0.5,0],
 [0.5,0.5,-0.05,1])
xyBackMe.transform(A,True)
xyBackOb = scene.objects.new(xyBackMe,'xyBackMe')
xyBackOb.getData(False,True).materials += [mat]
########################################
########################################
#######################################
# setup the event handler to animate the polygon
#
# create the text that handles the event
gameScript = """
#######################################
# tasks for each iteration thru this script
# 1) Output diagnostic information to console
# 2) pan the camera in response to the mouse and keyboard
#    a) x,y controlled by arrow keys
#    b) rotation controlled by shift arrow keys
#    c) mouse moves in x,y
#    d) shift mouse cause rotation
# 3) rotate the dislayed objects at a fixed rate using system time

print 'BGE script executing...'

import Blender
print Blender.sys.time()

# GameLogic has been added to the global namespace no need to import

# for keyboard event comparison
import GameKeys

# support for Vector(), Matrix() types and advanced functions like AngleBetweenVecs(v1,v2) and RotationMatrix(...)
import Mathutils

# for functions like getWindowWidth(), getWindowHeight()
import Rasterizer

# for matrix operations
import Mathutils

def main():
 print 'linked script started'
 frame = Blender.Get('curframe')
 sc    = Blender.Scene.GetCurrent()
 print 'linked script completed - Frame %d' % frame
 frame = frame+1
 Blender.Set('curframe',frame)

 cont = GameLogic.getCurrentController()

 # The KX_GameObject that owns this controller.
 own = cont.getOwner()

 # for scripts that deal with spacial logic
 own_pos = own.getPosition()


 # Some example functions, remove to write your own script.
 # check for a positive sensor, will run on any object without errors.
 print 'Logic info for KX_GameObject', own.getName()
 input = False

 for sens in cont.getSensors():
  # The sensor can be on another object, we may want to use it
  own_sens = sens.getOwner()
  print '    sensor:', sens.getName(),
  if sens.isPositive():
   print '(true)'
   input = True
  else:
   print '(false)'
  sensName = sens.getName()
  if sensName=='sMouse':
   print '..mouse x = '+ str(sens.getXPosition())
   print '..mouse y = '+ str(sens.getYPosition())
  if (sensName=='sKeyboard') and (sens.isPositive()):
   keyList = sens.getCurrentlyPressedKeys()
   keyStr = ''
   for key in keyList:
    keyStr = keyStr + str(key[0]) + ','
   print '..key     = '+keyStr

 for actu in cont.getActuators():
  # The actuator can be on another object, we may want to use it
  own_actu = actu.getOwner()
  print '    actuator:', sens.getName()

  # This runs the actuator or turns it off
  # note that actuators will continue to run unless explicitly turned off.
  if input:
   GameLogic.addActiveActuator(actu, True)
  else:
   GameLogic.addActiveActuator(actu, False)

 # Its also good practice to get sensors and actuators by names
 # so any changes to their order wont break the script.

 sce = GameLogic.getCurrentScene()
 ob   = sce.getObjectList()['OBCamera']
 sens = cont.getSensor('sKeyboard')
 if sens.isPositive():
  print '--key pressed--'
  fullKeyList = sens.getCurrentlyPressedKeys()
  keyList=[]
  for key in fullKeyList:
   keyList.append(key[0])
  pos = ob.getPosition()
  orientL = ob.getOrientation()
  orientM = Mathutils.Matrix(orientL[0],orientL[1],orientL[2])
  orientM.transpose()
  orient  = orientM.toEuler()
  if (GameKeys.RIGHTSHIFTKEY in keyList) or (GameKeys.LEFTSHIFTKEY in keyList):
   print '--Shift Pressed--'
   if GameKeys.RIGHTARROWKEY in keyList:
    orient[1]=orient[1]+0.5
    print '--Right Arrow Key--'
   elif GameKeys.LEFTARROWKEY in keyList:
    orient[1]=orient[1]-0.5
    print '--Left Arrow Key--'
   if GameKeys.UPARROWKEY in keyList:
    orient[0]=orient[0]+0.5
    print '--Up Arrow Key--'
   elif GameKeys.DOWNARROWKEY in keyList:
    orient[0]=orient[0]-0.5
    print '--Down Arrow Key--'
   else:
    print '--other key--'
  else:
   if GameKeys.RIGHTARROWKEY in keyList:
    pos[0]=pos[0]-0.02
    print '--Right Arrow Key--'
   elif GameKeys.LEFTARROWKEY in keyList:
    pos[0]=pos[0]+0.02
    print '--Left Arrow Key--'
   if GameKeys.UPARROWKEY in keyList:
    pos[1]=pos[1]-0.02
    print '--Up Arrow Key--'
   elif GameKeys.DOWNARROWKEY in keyList:
    pos[1]=pos[1]+0.02
    print '--Down Arrow Key--'
   else:
    print '--other key--'
  orientM = orient.toMatrix()
  orientM.transpose()
  ob.setOrientation(orientM)
  ob.setPosition(pos)
 #ob.setPosition([frame/20.0,0.0,0.0])

main()
"""
#EVENT = "FrameChanged"
txt = Text.Get(gameScriptName) #get the gamescript
txt.clear()                    # clear the existing script
txt.write(gameScript)          # appending text

#######################################
# set 3-d view to use the camera
Blender.Window.CameraView()
########################################

To copy the code snippets easily, see:http://dataorigami.blogspot.com/2009/06/how-to-easily-copy-code-snippets-from.html

To execute the script in Blender, see:http://dataorigami.blogspot.com/2009/06/how-to-execute-script-from-command-line.html

To work with the script in a simple IDE, see:http://dataorigami.blogspot.com/2009/04/developing-scripts-for-blender.html

Wednesday, July 1, 2009

Quick-N-Dirty: A graph with labels in Blender

This example shows how to generate a 2-D graph in Blender with a 3-D appearance. This example illustrates how to
  • generate a graph from data,
  • size and position text anywhere and of any size,
  • delete (unlink) all elements from a scene including the default cube, lamp, and camera, and
  • combine multiple meshes into a single mesh.

The repetitive and complex tasks in the example are implemented using functions. To build a line segment in the graph curve in the graph, the function 'lineSegMe' generate a mesh tube that runs from one point to another. To generate a complex curve, these tubes are strung together using the 'curve' function. Between each tube, a sphere is inserted to ensure that the graph looks smooth. To remove all elements from the default scene, the 'scrubScene' function is defined. Finally, the function 'textInABox' is defined to scale a string so it fits into a box witha given height and width.

How to run the example:

  1. Copy the data file from this example to the '\tmp' directory on the machine. If the '\tmp' directory does not exist, create it.
  2. Copy the code example into 'plotExample02.py' and save the file. See the notes on working with notepad to see how to setup Notepad to automate testing and execution of Blender scripts.
  3. To execute the script, from the command line execute the following: "blender -P plotExample02.py"
Code:
#!BPY

__doc__ = """
PlotExample02.py

Example demonstrating the following techniques in Blender 2.48:
1) Use of the transform matrix
2) Use of material properties to control how objects are rendered
3) Delete (unlink) and replacement of default objects in a scene
4) Use of cross products to generate a basis for the transform matrix
5) Using python to render an image
6) Using python to save the rendered image to disk
7) Use of Blender Vector and Matrix classes
8) Defining a function in Python
9) Adding text using text3d
10) Sizing text to a box using getBoundingBox()

This script is executed at the command line by:
>blender -P plotExample02.py
"""
__author__ = "edt"
__version__ = "1.0 2009/07/01"
__url__="Website, dataOrigami.blogspot.com"


##############################################################
# load the modules used in the script
import Blender
import bpy
from Blender import * 
from Blender.Scene import Render 
from Blender import Text
from Blender import Mathutils
from Blender.Mathutils import *
import math

##############################################################
# define function(s) 

def frange(start, end=None, inc=None):
    "A range function, that does accept float increments..."

    if end == None:
        end = start + 0.0
        start = 0.0

    if inc == None:
        inc = 1.0

    L = []
    while 1:
        next = start + len(L) * inc
        if inc > 0 and next >= end:
            break
        elif inc < 0 and next <= end:
            break
        L.append(next)
        
    return L


def lineSegMe(p1,p2,dia=0.1,verts=16):
 """
 This function returns a mesh which forms a line from point p1 to p2. 
 The points can be passes as either blender vectors or lists of [x,y,z] points.
 This line is cylinder which goes from point p1 to p2.
 Optionally the diameter and number of vertices used to describe the line are passed.
 -------------------
 The line is formed by creating a cylinder with a length equal to the distance
 point p1 and p2. The line is then oriented using the transform matrix to rotate
 and translate the cylinder. 
 """
 # use the class constructors from Blender to form vectors for p1 and p2
 p1 = Vector(p1)
 p2 = Vector(p2)
 
 # form a vector that points in the direction from p1 to p2
 dir = p2-p1                  
 
 # get the length of the line we want that goes from p1 to p2
 length = dir.length

 # use Mesh.Primitives.Cylinder to create a mesh for the line
 me = Mesh.Primitives.Cylinder(verts,dia,length)
 
 ###############
 # in the next few steps, the direction vector is used to form a basis
 #      see http://en.wikipedia.org/wiki/Basis_(linear_algebra)
 # which allows us to create a transform matrix to rotate the cylinder 
 # along the direction we want. The basic idea is that the vector from
 # p1 to p2 points in the direction we want. The cylinder created by 
 # Mesh.Primitives.Cylinder is oriented along the z-axis. To rotate the 
 # cylinder, we  # rotate the z-axis in this direction. To completely specify
 # how to rotate, we need to provide information on how to rotate the x and y axes. 
 # To define this, a matrix which is orthonormal (see http://en.wikipedia.org/wiki/Orthonormal)
 # is created from the direction vector. To create the other vectors in the 
 # orthonormal basis, cross products are used to find orthogonal vectors.
 # 
 # use the normalize function to set the length of the direction vector to 1
 dir.normalize()
 u = dir
 uu = Vector([0,0,1.0])
 #print AngleBetweenVecs(u,uu)
 if (abs(AngleBetweenVecs(u,uu))%180.0)>1e-3:
  # the direction of the line is different
  # from the z-axis
  # find the orthonormal basis
  v = CrossVecs(u,uu) 
  w = CrossVecs(u,v)
  # form the transform matrix: 
  #   > The first 3 rows and 3 columns form
  #   a rotation matrix because the any vertex transformed by this 
  #   matrix will be the same distance from the origin as the original
  #   vertex. If this property is not preserved, then any shape formed 
  #   will be skewed and scaled by the transform.
  #   > The first 3 columns in the last row define the translation
  #   applied to any vertex. In this function, the translation move the 
  #   moves the end of the cylinder to the origin, then moves the end
  #   to p1.
  A = Matrix(
    [w[0],w[1],w[2],0],
    [v[0],v[1],v[2],0],
    [u[0],u[1],u[2],0],
    [dir[0]/2.0*length+p1[0],dir[1]/2.0*length+p1[1],dir[2]/2.0*length+p1[2],1])
 else:
  # the direction of the line is parallel to the z-axis
  # see the notes above on how the matrix is formed.
  A = Matrix(
    [1,0,0,0],
    [0,1,0,0],
    [0,0,1,0],
    [dir[0]/2.0*length+p1[0],dir[1]/2.0*length+p1[1],dir[2]/2.0*length+p1[2],1]) 

 # apply the transform to the cylinder   
 me.transform(A,True)
 return me

def curve(pList,color=[1.0,1.0,1.0],dia=0.1,verts=4):
 lineMeList = []
 mat = Material.New('lineMat')  # create a new Material for the line 
 mat.rgbCol = color     # change the color of the line
 for i in range(0,len(pList)-1):
  p1 = pList[i]
  p2 = pList[i+1]
  lineMe = lineSegMe(p1,p2,dia,verts)
  lineMe.materials += [mat]
  lineMeList.append(lineMe)
  # check to see if another line segment will follow
  if i<len(pList)-2:
   jointMe = Mesh.Primitives.UVsphere(verts,verts,dia/2.0)
   A = Matrix(
    [1,0,0,0],
    [0,1,0,0],
    [0,0,1,0],
    [p2[0],p2[1],p2[2],1]) 
   jointMe.transform(A,True)
   jointMe.materials += [mat]
   lineMeList.append(jointMe)
 return lineMeList

 
def scrubScene(saveList=[]):
 'removes all objects in scene, except objects in save list'
 scene = Scene.GetCurrent()
 for ob in scene.objects:
  if not ob.getName() in saveList:
   scene.objects.unlink(ob)
  
def combineMeshesIntoOb(meList,obName):
 'adds all meshes in meList into a single object in the scene'
 # TODO: add logic to detect empty list
 # TODO: add logic to enable/disable joining
 join = False
 rooted = False
 for me in meList:
  if not rooted:
   combinedOb= scene.objects.new(me,obName)
   rooted = True
  else:
   localCombinedOb = scene.objects.new(me,'local'+obName)
   if join:
    combinedOb.join([localCombinedOb])
    scene.objects.unlink(localCombinedOb)
 return combinedOb

def textInBox(txtStr="defaultText",col=[1.0,1.0,1.0],width=1.0,height=1.0):
 txt = Text3d.New()
 txt.setText(txtStr)
 txt.setSize(0.1)
 mat=Material.New('textMat')
 mat.rgbCol = col
 ob = scene.objects.new(txt)
 me = Blender.Mesh.New('textMesh')
 me.getFromObject(ob)
 me.materials += [mat]
 scene.objects.unlink(ob)
 newOb = scene.objects.new(me)

 # force a redraw to ensure that the bounding box is updated!!
 Window.RedrawAll() 
 boundBox = newOb.getBoundBox(1)
 upperBox = max(boundBox)
 lowerBox = min(boundBox)
 initialWidth = upperBox[0]-lowerBox[0]
 initialHeight = upperBox[1]-lowerBox[1]
 widthRatio = width/initialWidth
 heightRatio = height/initialHeight
 ratio = min(widthRatio,heightRatio)
 
 newOb.setSize(ratio,ratio,ratio)
 return newOb

 
##############################################################
##############################################################
# clean out any objects from the scene
scrubScene()
scene = Scene.GetCurrent()

##############################################################
# add a camera and set it up
#     
camdata = Camera.New() 
cam = scene.objects.new(camdata) 
# use setLocation to control the position of the camera
cam.setLocation(0.9,0.5,2.1) 
# use set Euler to control the angle of the camera
cam.setEuler(0*(3.1415/180),10*(3.1415/180),0*(3.1415/180)) 
scene.objects.camera = cam
##############################################################
# add a lamp and set it up
#
lampData = Lamp.New() 
lampData.setEnergy(1.0) 
lampData.setType('Lamp')
lampData.mode |= Lamp.Modes["RayShadow"]  # make shadows appear
lamp = scene.objects.new(lampData) 
lamp.setLocation(2.0,2.0,5.0) 
lamp.setEuler(120*(3.1415/180),30*(3.1415/180),-30*(3.1415/180))
##############################################################
# load the data
#
f = open('c:\\tmp\\EmploymentPopRatio.txt', 'r')
foundData = False
data = {'x':[],'y':[],'z':[]}
labels = {'x':[],'y':[],'z':[]}
for line in f:
 fields = line.split(',')
 if not foundData:
  for entry in fields:
   if entry == 'Year':
    # this is the header row of data
    foundData = True
 else:
  print len(line)
  if len(line)>1:
   for i in range(0,13):
    if i==0:
     print line
     print fields[0]
     year =  float(fields[0])
    else:
     month = i
     if not fields[i]==' ':
      print '::'+fields[i]+'||'
      data['x'].append(float(year+(month-1)/12.0))
      labels['x'].append(str(year)+','+str(month))
      data['y'].append(float(fields[i]))
      data['z'].append(0.05)
   

#############################
#############################

axisList = ['x','y','z']


# setup the data ranges

graphSetup = {}
graphSetup["x"]={'max':2010,'min':1940,'inc':10}
graphSetup["y"]={'max':70,'min':50,'inc':5}
graphSetup["z"]={'max':1,'min':0,'inc':1}
graphSetup["title"]='Employment to Population Ratio'
graphSetup["xLabels"]=[]
for x in frange(graphSetup['x']['min'],graphSetup['x']['max']+graphSetup['x']['inc'],graphSetup['x']['inc']):
 graphSetup['xLabels'].append(str(x))

print graphSetup['xLabels']
 
# scale the data for presentation
scale={}
offset={}
for axis in axisList:
 scale[axis]=1.0/float(graphSetup[axis]['max']-graphSetup[axis]['min'])
 offset[axis] = float(graphSetup[axis]['min'])

# build point list
pointList = []
for i in range(0,len(data['x'])):
 p = {}
 for axis in scale.keys():
  p[axis]=(data[axis][i]-offset[axis])*scale[axis]
 point = Vector([p['x'],p['y'],p['z']])
 pointList.append(point)

##############################################################
# create the objects in the scene and bind materials to them
# draw the data from the graph
meList = curve(pointList,[1.0,0.1,0.4],0.01,8)
combineMeshesIntoOb(meList,'curveOb')
#
##############################
# create a grid for the different axes
#
tics = {}
scaledTics = {'x':[],'y':[],'z':[]}
for axis in scaledTics.keys():
 tics[axis]=frange(graphSetup[axis]['min'],
      graphSetup[axis]['max']+graphSetup[axis]['inc'],
      graphSetup[axis]['inc'])
 for tic in tics[axis]:
  scaledTics[axis].append((tic-offset[axis])*scale[axis])

# draw x-y axes  

def plotGrid(tics={'x':[],'y':[],'z':[]},axes=['x','y'],col=[1.0,1.0,0.0],isTics=False):
 ''
 def plotGridLines(tics,axis,P1,P2,col):
  'plot the grid elements' 
  for tic in tics[axis]:
   R = Vector([tic,1.0,0.0])
   point1 = P1*R
   point2 = P2*R
   pointList = [point1,point2]
   meList = curve(pointList,col,0.01,8)
   combineMeshesIntoOb(meList,'GridElement')
 #
 def plotGridCorners(cornerLocs,col):
  mat = Material.New('axisMat') # create a new Material called 'newMat' 
  mat.rgbCol = col 
  for i in range(0,len(cornerLocs)):
   cornerMe = Mesh.Primitives.UVsphere(32,32,0.01)
   cornerOb = scene.objects.new(cornerMe,'originOb')
   cornerOb.getData(False,True).materials+=[mat]
   cornerOb.setLocation(cornerLocs[i][0],cornerLocs[i][1],cornerLocs[i][2])
 #
 if isTics:
  gridLen = 0.1
 else:
  gridLen = 1.0
 if 'x' in axes and 'y' in axes:
  P1 = Matrix([1.0,0.0,0.0],[0.0,0.0,0.0],[0.0,0.0,0.0])
  P2 = Matrix([1.0,0.0,0.0],[0.0,gridLen,0.0],[0.0,0.0,0.0])
  plotGridLines(tics,'x',P1,P2,col)
  P1 = Matrix([0.0,gridLen,0.0],[1.0,0.0,0.0],[0.0,0.0,0.0])
  P2 = Matrix([0.0,0.0,0.0],[1.0,0.0,0.0],[0.0,0.0,0.0])
  plotGridLines(tics,'y',P1,P2,col)
  if isTics:
   cornerLocs =[[0,0,0]]  
  else:
   cornerLocs =[[0,0,0],[0,1,0],[1,1,0],[1,0,0]]
  plotGridCorners(cornerLocs,col)
 elif 'x' in axes and 'z' in axes:
  P1 = Matrix([1.0,0.0,0.0],[0.0,0.0,0.0],[0.0,0.0,0.0])
  P2 = Matrix([1.0,0.0,0.0],[0.0,0.0,0.0],[0.0,gridLen,0.0])
  plotGridLines(tics,'x',P1,P2,col)
  P1 = Matrix([0.0,0.0,0.0],[0.0,0.0,0.0],[1.0,0.0,0.0])
  P2 = Matrix([0.0,gridLen,0.0],[0.0,0.0,0.0],[1.0,0.0,0.0])
  plotGridLines(tics,'z',P1,P2,col)
  if isTics:
   cornerLocs =[[0,0,0]]  
  else:
   cornerLocs =[[0,0,0],[0,1,0],[0,1,1],[0,0,1]]
  plotGridCorners(cornerLocs,col)
 elif 'y' in axes and 'z' in axes:
  P1 = Matrix([0.0,0.0,0.0],[1.0,0.0,0.0],[0.0,0.0,0.0])
  P2 = Matrix([0.0,0.0,0.0],[1.0,0.0,0.0],[0.0,gridLen,0.0])
  plotGridLines(tics,'y',P1,P2,col)
  P1 = Matrix([0.0,0.0,0.0],[0.0,gridLen,0.0],[1.0,0.0,0.0])
  P2 = Matrix([0.0,0.0,0.0],[0.0,0.0,0.0],[1.0,0.0,0.0])
  plotGridLines(tics,'z',P1,P2,col)
  if isTics:
   cornerLocs =[[0,0,0]]  
  else:
   cornerLocs =[[0,0,0],[0,1,0],[1,1,0],[1,0,0]]
  plotGridCorners(cornerLocs,col)
 
plotGrid(scaledTics,axes=['x','y'],col=[1.0,1.0,0.0],isTics=False) 
#plotGrid(scaledTics,axes=['x','z'],col=[1.0,1.0,0.0],isTics=False) 
#plotGrid(scaledTics,axes=['y','z'],col=[1.0,1.0,0.0],isTics=False) 
  
##############################
# create x-axis labels
# 



for i in range(0,len(graphSetup['xLabels'])):
 label = graphSetup['xLabels'][i]
 xLoc = scaledTics['x'][i]
 xTicOb=textInBox(label,[1.0,0.0,0.0],0.1,0.1) 
 xTicOb.setLocation(xLoc,-0.01,0.05)
 xTicOb.setEuler(0.0,0.0,-90.0*(3.1415/180.0))
  
##############################
# create y-axis labels
# 
for i in range(0,len(tics['y'])):  
 yLabel = str(tics['y'][i])+'%'
 yLoc = scaledTics['y'][i]
 yTicOb=textInBox(yLabel,[1.0,0.0,0.0],0.1,0.1)
 yTicOb.setLocation(-0.11,yLoc-0.025,0.05)
 

##############################
# create a title
# 

titleOb=textInBox(graphSetup['title'],[1.0,0.0,0.0],1.0,1.0) 
titleOb.setLocation(0.0,1.05,0.05)
##############################
#
mat = Material.New('xyBackMat') # create a new Material called 'newMat' 
mat.rgbCol = [1.0, 1.0, 1.0]      # change its color 
xyBackMe = Mesh.Primitives.Grid(2,2)
A = Matrix(
 [0.5,0,0,0],
 [0,0.5,0,0],
 [0,0,0.5,0],
 [0.5,0.5,-0.05,1])
xyBackMe.transform(A,True)
xyBackOb = scene.objects.new(xyBackMe,'xyBackMe')
xyBackOb.getData(False,True).materials += [mat]
# this makes the grid appear as an outline in the Blender editor
#xyBackOb.setDrawType(Object.DrawTypes["WIRE"])

#######################################
# render the image and save the image
#
context = scene.getRenderingContext() 
# enable seperate window for rendering 
Render.EnableDispWin() 
context.imageType = Render.JPEG
# draw the image 
context.render() 
# save the image to disk 
# to the location specified by RenderPath 
# by default this will be a jpg file 
context.saveRenderedImage('PlotExample02.jpg') 

Window.RedrawAll() 
#
########################################


To copy the code snippets easily, see:http://dataorigami.blogspot.com/2009/06/how-to-easily-copy-code-snippets-from.html

To execute the script in Blender, see:http://dataorigami.blogspot.com/2009/06/how-to-execute-script-from-command-line.html

To work with the script in a simple IDE, see:http://dataorigami.blogspot.com/2009/04/developing-scripts-for-blender.html

Saturday, June 20, 2009

How to execute a script from the command line in Blender

With Blender you can execute a Python script when Blender is launched by specifying the scipt:

>blender -P myscript.py

Blender tranform matrix and building scenes from primitive meshes

In this tutorial, we'll work through several concepts in Blender. The three key concepts covered are:
  • the use of the transform matrix (mesh.transform()) to orient and place a mesh in a scene,
  • the use of material properties to control how a mesh is rendered, and
  • the use of Blender Vector and Matrix classes.
This example was built to begin working towards the generation of graphs in Blender. One of the first steps in generating graphs is the ability to draw a line from one point to another. This can be done in many ways. Two ways to do this are the direct generation of meshes that form the graph or assemling primitives into a graph. This example works through the basic steps in locating and orienting primitive elements. Blender provides functions to generate some simple primitive meshes: tubes, cones, planes, grids, cubes, spheres, and the monkey face. When Blender creates these meshes they are placed on the origin in the scene. The put the meshes elsewhere, the meshes must be moved using setLocation() and setEuler() or by setting the transform matrix and using transform() function. In this example, the meshes are placed using the transform() function. The main work is done in the function lineMe(...) which creates a line from one point to another by creating a tube mesh, then rotating and translating the mesh to make it go between the two points. This function is defined in lines 38 through 111. To rotate the tube/cylinder, the ends of the line are used to determine the lenght of the tube. A tube is created equal to the length of the lines. The tube is rotated to the direction of the line by manually forming a rotation matrix which is placed in the tranform matrix in lines 79-111. This matrix is also configured to moved one end of the tube to one end of the line. With this function defined, we can easily place lines anywhere we want in the scene. So the example puts a red line in, then adds 3 lines, one for each axis. Finally, to form a grid to see where things are, the mesh primitive, grid is added. The mesh returned by the Mesh.Primitivies.Grid() initially appears as a solid plane. To make the mesh appear as a grid, the material attached to it is configured to only display the vertices and edges in the rendering. This is done in line 201. Also, since the appearance of a plane in the editor can get in the way, the grid is set to appear in the editor as a mesh in line 207. When this script is executed, it should generate the image shown at the top of this posting and save the image to your render path.
#!BPY

__doc__ = """
OrientationExample.py

Example demonstrating the following techniques in Blender 2.48:
1) Use of the transform matrix
2) Use of material properties to control how objects are rendered
3) Delete (unlink) and replacement of default objects in a scene
4) Use of cross products to generate a basis for the transform matrix
5) Using python to render an image
6) Using python to save the rendered image to disk
7) Use of Blender Vector and Matrix classes
8) Defining a function in Python

This script is executed at the command line by:
>blender -P OrientationExample.py
"""
__author__ = "edt"
__version__ = "1.0 2009/06/20"
__url__="Website, dataOrigami.blogspot.com"


##############################################################
# load the modules used in the script
import Blender
import bpy
from Blender import *
from Blender.Scene import Render
from Blender import Text
from Blender import Mathutils
from Blender.Mathutils import *
import math

##############################################################
# define function(s)

def lineMe(p1,p2,dia=0.1,verts=16):
 """
 This function returns a mesh which forms a line from point p1 to p2.
 The points can be passes as either blender vectors or lists of [x,y,z] points.
 This line is cylinder which goes from point p1 to p2.
 Optionally the diameter and number of vertices used to describe the line are passed.
 -------------------
 The line is formed by creating a cylinder with a length equal to the distance
 point p1 and p2. The line is then oriented using the transform matrix to rotate
 and translate the cylinder.
 """
 # use the class constructors from Blender to form vectors for p1 and p2
 p1 = Vector(p1)
 p2 = Vector(p2)

 # form a vector that points in the direction from p1 to p2
 dir = p2-p1          

 # get the length of the line we want that goes from p1 to p2
 length = dir.length

 # use Mesh.Primitives.Cylinder to create a mesh for the line
 me = Mesh.Primitives.Cylinder(verts,dia,length)

 ###############
 # in the next few steps, the direction vector is used to form a basis
 #      see http://en.wikipedia.org/wiki/Basis_(linear_algebra)
 # which allows us to create a transform matrix to rotate the cylinder
 # along the direction we want. The basic idea is that the vector from
 # p1 to p2 points in the direction we want. The cylinder created by
 # Mesh.Primitives.Cylinder is oriented along the z-axis. To rotate the
 # cylinder, we  # rotate the z-axis in this direction. To completely specify
 # how to rotate, we need to provide information on how to rotate the x and y axes.
 # To define this, a matrix which is orthonormal (see http://en.wikipedia.org/wiki/Orthonormal)
 # is created from the direction vector. To create the other vectors in the
 # orthonormal basis, cross products are used to find orthogonal vectors.
 #
 # use the normalize function to set the length of the direction vector to 1
 dir.normalize()
 u = dir
 uu = Vector([0,0,1.0])
 if abs(AngleBetweenVecs(u,uu))>1e-6:
  # the direction of the line is different
  # from the z-axis
  # find the orthonormal basis
  v = CrossVecs(u,uu)
  w = CrossVecs(u,v)
  # form the transform matrix:
  #   > The first 3 rows and 3 columns form
  #   a rotation matrix because the any vertex transformed by this
  #   matrix will be the same distance from the origin as the original
  #   vertex. If this property is not preserved, then any shape formed
  #   will be skewed and scaled by the transform.
  #   > The first 3 columns in the last row define the translation
  #   applied to any vertex. In this function, the translation move the
  #   moves the end of the cylinder to the origin, then moves the end
  #   to p1.
  A = Matrix(
    [w[0],w[1],w[2],0],
    [v[0],v[1],v[2],0],
    [u[0],u[1],u[2],0],
    [dir[0]/2.0*length+p1[0],dir[1]/2.0*length+p1[1],dir[2]/2.0*length+p1[2],1])
 else:
  # the direction of the line is parallel to the z-axis
  # see the notes above on how the matrix is formed.
  A = Matrix(
    [1,0,0,0],
    [0,1,0,0],
    [0,0,1,0],
    [dir[0]/2.0*length+p1[0],dir[1]/2.0*length+p1[1],dir[2]/2.0*length+p1[2],1])

 # apply the transform to the cylinder
 me.transform(A,True)
 return me

##############################################################
# Get rid of the lamp and cube from the default scene
scene = Scene.GetCurrent()
for ob in scene.objects:
   print ob.getName()
   if ((cmp(ob.getName(),'Lamp')==0)
       (cmp(ob.getName(),'Cube')==0)
    (cmp(ob.getName(),'Camera')==0)):
     scene.objects.unlink(ob)
##############################################################
# add a camera and set it up
#
camdata = Camera.New()
cam = scene.objects.new(camdata)
# use setLocation to control the position of the camera
cam.setLocation(2.481,-1.508,1.1)
# use set Euler to control the angle of the camera
cam.setEuler(73.559*(3.1415/180),0.620*(3.1415/180),46.692*(3.1415/180))
scene.objects.camera = cam
##############################################################
# add a lamp and set it up
#
lampdata = Lamp.New()
lampdata.setEnergy(1.0)
lampdata.setType('Hemi')
lamp = scene.objects.new(lampdata)
lamp.setLocation(-5.0,-6.0,5.0)
lamp.setEuler(0.0,0.0,0.0)
##############################################################
# create the objects in the scene and bind materials to them
#

# define the ends of the line
p1 = Vector([0.2,0.3,-0.1])
p2 = Vector([0.5,1.0,1.0])
# form a mesh for th eline
me = lineMe(p1,p2)
# add the line to the scene
ob = scene.objects.new(me,'lineOb')
# bind a red material to the line
mat = Material.New('redMat') # create a new Material called 'redMat'
mat.rgbCol = [0.8, 0.1, 0.5] # change the color of the line
#
# attach the material to the mesh
#   note 1) a different technique is used for meshes
#       compared to how materials are attached to objects
#   note 2) we can attach the material to the mesh even after
#       the mesh was assigned to the object. This happens
#       because the mesh and the object really are pointers
#       to the same information in memory.
me.materials += [mat]
#
##############################
#
# create the lines for axes and a ball at the origin
p0 = Vector([0,0,0])
px = Vector([1,0,0])
py = Vector([0,1,0])
pz = Vector([0,0,1])
xMe = lineMe(p0,px)
yMe = lineMe(p0,py)
zMe = lineMe(p0,pz)
xOb = scene.objects.new(xMe,'xAxisOb')
yOb = scene.objects.new(yMe,'yAxisOb')
zOb = scene.objects.new(zMe,'zAxisOb')
originMe = Mesh.Primitives.UVsphere(32,32,0.1)
originOb = scene.objects.new(originMe,'originOb')

mat = Material.New('axisMat') # create a new Material called 'newMat'
mat.rgbCol = [1.0, 1.0, 0.0] # change its color

# attach the materials using two different techniques
#  >for the xaxis and the origin, the material is attached to the mesh by
#  by referenging the mesh from the object using
#  getData
#  > for the y & z-axis, we use the mesh reference. Since the
#  objects in the scene just point to the original mesh
#  attached to them, changing the mesh changes the object.
originOb.getData(False,True).materials+=[mat]
xOb.getData(False,True).materials += [mat]
yMe.materials += [mat]
zMe.materials += [mat]

# create a grid for the axes and place it on the x-y plane with
# use material properties to make it look like a grid
#  by only drawing vertices and edges
mat = Material.New('axisGridMat') # create a new Material called 'newMat'
mat.rgbCol = [1.0, 1.0, 0.0]      # change its color
mat.mode = Material.Modes.WIRE   # only render edges and vertices
zGridMe = Mesh.Primitives.Grid(10,10,1)
zGridOb = scene.objects.new(zGridMe,'zGridOb')
zGridOb.setLocation(0.5,0.5,0)
zGridOb.getData(False,True).materials += [mat]
# this makes the grid appear as an outline in the Blender editor
zGridOb.setDrawType(Object.DrawTypes["WIRE"])
#######################################
# render the image and save the image
#
context = scene.getRenderingContext()
# enable seperate window for rendering
Render.EnableDispWin()
context.imageType = Render.JPEG
# draw the image
context.render()
# save the image to disk
# to the location specified by RenderPath
# by default this will be a jpg file
context.saveRenderedImage('LineOrientationExample.jpg')
Window.RedrawAll()
#
########################################


To copy the code snippets easily, see:http://dataorigami.blogspot.com/2009/06/how-to-easily-copy-code-snippets-from.html

To execute the script in Blender, see:http://dataorigami.blogspot.com/2009/06/how-to-execute-script-from-command-line.html

To work with the script in a simple IDE, see:http://dataorigami.blogspot.com/2009/04/developing-scripts-for-blender.html

Thursday, May 21, 2009

Using Python and Blender Game Engine to build an interactive visualization

The purpose of this example is to demonstrate how to use Python and the Blender Game Engine (BGE) to create an interactive visualization. When done, the blender model will allow you to rotate and move a polyhedron (pyramid) using the arrow keys when the game engine is running. I'm assuming you have anough knowledge that I do not need to walk you through every step in the Blender environment. As far as I can tell, Blender 2.48 does not support programmatic definition of BGE sensors, controllers, and actuators. Therefore, to use Python to define an interactive visualization in Blender, we need to start with a Blender file that provides everything needed that Python can not add. This seed model consists of an text object, sensors, and a controller. The script that builds the visualization then loads this model and modifies it. To build the seed model start by opening Blender. You should see a screen similar to this: So it is easy to see what is going on, split the screen. On the left hand screen, change to the text editor screen. Click on that screen. Now open a new text block using either alt-N or File>New. Name this new text block 'MainScript.py'. When you are done you should see a screen similar to this: The next step is to add the sensors and a controller to the scene. Click in the bottom display window. Press F4 or select the Logic button. This panel will allow you to add sensors and a controller. After pressing F4 you should see the following: Make the camera visible in the logic panel. This can be done by pressing the 'a' key twice. This deselects everything, then selects everything. Once this is done, the logic panel should look like this: To add the sensors, in the sensors column, press the add button next to the camera. This will cause a form for a sensor to appear. Add 5 sensors. The five sensors should be configured as:
  • 'always' named 'sAlways',
  • 'keyboard' named 'sKeyboard' with 'all' selected,
  • 'mouse' named 'sMouseRight' with 'right button' selected,
  • 'mouse' named 'sMouseLeft' with 'left button' selected,
  • 'mouse' named 'sMouse' with 'movement selected'
The always sensor will trigger a script on every frame when the Blender Game Engine (BGE) runs. The keyboard sensor will trigger a script when a key is pressed or released. The mouse sensors will trigger and event when the mouse buttons are pressed or the mouse moves. The names are added so it is easy to reference the sensors in the BGE script. Once done, you should see the following: Next, a controller is added to the camera object. Press the add button beside the camera button in the controller column. Configure the controller as a 'Python' controller. Specify the script as 'MainScript.py'. This causes the script stored in the text block 'MainScript.py' to be executed on a trigger. We will load the script into this block using the script we will write later. Finally, connect all of the sensors to the controller by clicking on the circle to the right of each sensor then clicking on the circle to the left of the controller. When you are done, it should look like this: Save this blender file as 'bgeExample.blend'. To simplify things, save the files into the 'tmp' directory. If this directory does not exist, create it. At this point, we have a template that can be modified by a Python script. The next step is to build that script. To simplify things, copy the following code into your favorite editor and save. My preference is to work with Notepad++. See these instructions to execute Blender from inside Notepad++. To see everything work, execute this script, then when Blender opens, press the 'P' key. This will start the game engine and allow you to move/rotate the polygon using the arrow and shift-arrow keys. You should see the following once the game engine starts: To use Python to create the visualization ...
#!BPY
"""
Name: 'HowToBuildAnInteractiveBGEScene'
Blender: 248
Group: ''
"""
##########################################################3
import Blender
import bpy
from Blender import * 
from Blender.Scene import Render 
from Blender import Text

##############################################################
# load the template blender file
Blender.Load('c:\\tmp\\bgeExample.blend')

##############################################################
# reload the modules because a load resets the Python interpreter
import Blender
import bpy
from Blender import * 
from Blender.Scene import Render 
from Blender import Text


##############################################################
# name for the embedded script in the loaded file
gameScriptName = 'MainScript.py'

##############################################################
# Get rid of the lamp and cube from the default scene
#     - leave the camera, it has the linkages for the scripting
#       for the BGE.
#     - Note: Do NOT make any object the BGE scripts invisible
#       via ob.layers=[], this will disable the scripts
#       attached to the object.
scene = Scene.GetCurrent() 
for ob in scene.objects:
   print ob.getName()
   if ((cmp(ob.getName(),'Lamp')==0) |
    (cmp(ob.getName(),'Cube')==0)):
  scene.objects.unlink(ob)
##############################################################
# reuse the camera in the default scene
##############################################################
# add a lamp and set it up
#
lampdata = Lamp.New() 
lampdata.setEnergy(10.0) 
lampdata.setType('Hemi')
lamp = scene.objects.new(lampdata) 
lamp.setLocation(-5.0,-6.0,5.0) 
lamp.setEuler(0.0,0.0,0.0) 
##############################################################
# create the mesh and bind a material to it
#

coords=[ [-1,-1,-1], [1,-1,-1], [1,1,-1], [-1,1,-1], [0,0,1] ]  
faces= [ [3,2,1,0], [0,1,4], [1,2,4], [2,3,4], [3,0,4] ]

me = bpy.data.meshes.new('myMesh')
me.verts.extend(coords)
me.faces.extend(faces)
ob = scene.objects.new(me,'myObj')
ob.setEuler(0.2,0.2,0.1)
# 
mat = Material.New('newMat') # create a new Material called 'newMat' 
mat.rgbCol = [0.8, 0.2, 0.2] # change its color 
# 
# attach the material to the mesh
#   note 1) a different technique is used for meshes
#       compared to how materials are attached to objects 
#   note 2) we can attach the material to the mesh even after
#       the mesh was assigned to the object. This happens
#       because the mesh and the object really are pointers
#       to the same information in memory. 
me.materials += [mat]
# 
Window.RedrawAll() 
#
#######################################
# setup the BGE handler to animate the polygon
#   based on keyboard inputs
#
# create the text that handles the event
gameScript = """
#######################################
# tasks for each iteration thru this script
# 1) Output diagnostic information to console
# 2) rotate or move myObj based on keyboard input
#      - the arrow keys move the object up/dn, rt/left
#      - shift + arrows key rotates the object

print 'BGE script executing...'

import Blender
print Blender.sys.time()

# GameLogic has been added to the global namespace no need to import

# for keyboard event comparison
import GameKeys 

# support for Vector(), Matrix() types and advanced functions like AngleBetweenVecs(v1,v2) and RotationMatrix(...)
import Mathutils 

# for functions like getWindowWidth(), getWindowHeight()
import Rasterizer

# for matrix operations
import Mathutils

def main():
 cont = GameLogic.getCurrentController()
 sce = GameLogic.getCurrentScene()
 ob   = sce.getObjectList()['OBmyObj']
 sens = cont.getSensor('sKeyboard') 
 if sens.isPositive():
  print '--key pressed--'
  fullKeyList = sens.getCurrentlyPressedKeys()
  keyList=[]
  for key in fullKeyList:
   keyList.append(key[0])
  pos = ob.getPosition()
  orientL = ob.getOrientation()
  orientM = Mathutils.Matrix(orientL[0],orientL[1],orientL[2])
  orientM.transpose()
  orient  = orientM.toEuler()
  if (GameKeys.RIGHTSHIFTKEY in keyList) or (GameKeys.LEFTSHIFTKEY in keyList):
   print '--Shift Pressed--'
   if GameKeys.RIGHTARROWKEY in keyList:
    orient[1]=orient[1]+0.5
    print '--Right Arrow Key--'
   elif GameKeys.LEFTARROWKEY in keyList:
    orient[1]=orient[1]-0.5
    print '--Left Arrow Key--'
   if GameKeys.UPARROWKEY in keyList:
    orient[0]=orient[0]+0.5
    print '--Up Arrow Key--'
   elif GameKeys.DOWNARROWKEY in keyList:
    orient[0]=orient[0]-0.5
    print '--Down Arrow Key--'  
   else:
    print '--other key--'
  else:
   if GameKeys.RIGHTARROWKEY in keyList:
    pos[0]=pos[0]+0.1
    print '--Right Arrow Key--'
   elif GameKeys.LEFTARROWKEY in keyList:
    pos[0]=pos[0]-0.1
    print '--Left Arrow Key--'
   if GameKeys.UPARROWKEY in keyList:
    pos[1]=pos[1]+0.1
    print '--Up Arrow Key--'
   elif GameKeys.DOWNARROWKEY in keyList:
    pos[1]=pos[1]-0.1
    print '--Down Arrow Key--'  
   else:
    print '--other key--'
  orientM = orient.toMatrix()
  orientM.transpose()
  ob.setOrientation(orientM)
  ob.setPosition(pos)  

main()  
"""
txt = Text.Get(gameScriptName) #get the gamescript
txt.clear()                    # clear the existing script
txt.write(gameScript)          # appending text

#######################################
#  Save the blender file
# 
Blender.Save('C:\\tmp\\FullBGEExample.blend',1)
This script deletes the default lamp and cube, then adds a new lamp and polygon. The default camera is kept since it has the links for the game engine attached to it. The script adds the game engine script to the 'MainScript.py' text block. Once the script executes, Blender should open and look like this: To see everything work, execute this script, then when Blender opens, press the 'P' key. This will start the game engine and allow you to move/rotate the polygon using the arrow and shift-arrow keys. You should see the following once the game engine starts:
New code formatting: Thanks to techqi and Alex Gorbatchev.