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:
- Copy the data file from this example to the '\tmp' directory on the machine. If the '\tmp' directory does not exist, create it.
- 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.
- To execute the script, from the command line execute the following: "blender -P plotExample02.py"
#!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
Hi,
ReplyDeletegreat help... but the diameter of the cylinders produced in the function does'nt seem to be constant.
Send me an email, I will send you the example
fdecomite@yahoo.fr
here is the example :
ReplyDeletehttp://code.google.com/p/voronoijava/source/browse/Voronoi/src/test/exCourbeCompletdd.py
v and w must be normalized in function lineSegMe
ReplyDelete