Tuesday, May 21, 2019

A 3D Python Maze for Art of Illusion

I've always had a fascination with mazes. While messing with the Python plugin for Art of Illusion I decided to write a maze generator in Python. The image to the right is a rendering of the maze that is produced by that code. I did it as a Scripted Object, which was probably not the best idea since it recalculates the maze whenever something changes, but my excuse is that I was testing out some things related to Python scripted objects in Art of Illusion.

I'm including the code below, but with the caveat that it has a defect at one of the boundaries that I haven't figured out yet. It doesn't remove a wall for one or two of the cells. Aside from that, it will produce a good 3D maze.

import random
mazeWidth = 25
mazeLength = 25
wallLength = 1.2
wallHeight = 2.25
wallThickness = 0.05

WALL_UP = 0
WALL_DOWN = 1
WALL_FIXED = 2

class cell():
  X = 0
  Y = 0
  N = WALL_UP
  S = WALL_UP
  E = WALL_UP
  W = WALL_UP
  visited = False
 
def createmaze(width, length):
  m = []
  for l in range(length):
    row = []
    for w in range(width):
      _cell = cell()
      _cell.X = w
      _cell.Y = l
      row.append(_cell)
    m.append(row)
   
  for l in range(length):
    m[l][0].N = WALL_FIXED
    m[l][width-1].S = WALL_FIXED
   
  for w in range(width):
    m[0][w].W = WALL_FIXED
    m[length-1][w].E = WALL_FIXED
   
  return m
 
def createWall(length, height, thickness):
  wall = Cube(thickness, length, height)
  return ObjectInfo(wall, CoordinateSystem(), "")
 
def drawMaze():
  for i in range(mazeLength):
    for j in range(mazeWidth):
      if maze[i][j].W != WALL_DOWN:
        obj = createWall(wallLength, wallHeight, wallThickness)
        if not obj is None:
          obj.getCoords().setOrientation(90.0, 0.0, 0.0)
          obj.getCoords().setOrigin(Vec3(j*wallLength-(wallLength*0.5)-(mazeWidth*wallLength/2), wallHeight/2, i*wallLength-(mazeLength*wallLength/2)))
          self.addObject(obj)
     
      if maze[i][j].N != WALL_DOWN:
        obj = createWall(wallLength, wallHeight, wallThickness)
        if not obj is None:
          obj.getCoords().setOrientation(90.0, 90.0, 0.0)
          obj.getCoords().setOrigin(Vec3(j*wallLength-(mazeWidth*wallLength/2), wallHeight/2, i*wallLength-(wallLength*0.5)-(mazeLength*wallLength/2)))
          self.addObject(obj)
 
  for i in range(mazeLength):
    obj = createWall(wallLength, wallHeight, wallThickness)
    if not obj is None:
      obj.getCoords().setOrientation(90.0, 0.0, 0.0)
      obj.getCoords().setOrigin(Vec3(mazeWidth*wallLength-(wallLength*0.5)-(mazeWidth*wallLength/2), wallHeight/2, i*wallLength-(mazeLength*wallLength/2)))
      self.addObject(obj)
   
  for j in range(mazeWidth):
    obj = createWall(wallLength, wallHeight, wallThickness)
    if not obj is None:
      obj.getCoords().setOrientation(90.0, 90.0, 0.0)
      obj.getCoords().setOrigin(Vec3(j*wallLength-(mazeWidth*wallLength/2), wallHeight/2, mazeLength*wallLength-(wallLength*0.5)-(mazeLength*wallLength/2)))
      self.addObject(obj)
     
def chooseUnvisited():
  neighbors = []
  if (current.N == WALL_UP) and (current.Y-1 >=0) and (not maze[current.Y-1][current.X].visited):
    neighbors.append(maze[current.Y-1][current.X])
  if (current.S == WALL_UP) and (current.Y+1 < mazeLength) and (not maze[current.Y+1][current.X].visited):
    neighbors.append(maze[current.Y+1][current.X])
  if (current.W == WALL_UP) and (current.X-1 >=0) and (not maze[current.Y][current.X-1].visited):
    neighbors.append(maze[current.Y][current.X-1])
  if (current.E == WALL_UP) and (current.X+1 < mazeWidth) and (not maze[current.Y][current.X+1].visited):
    neighbors.append(maze[current.Y][current.X+1])
 
  if len(neighbors) > 0:
    return random.choice(neighbors)
  else:
    return current
   
def removeWall(cur, nxt):
  print("Cur "+str(cur.X)+" "+str(cur.Y))
  print("Nxt "+str(nxt.X)+" "+str(nxt.Y))
  if nxt != cur:
    if cur.Y < nxt.Y and cur.S != WALL_FIXED:
      print("removeWall 1")
      maze[cur.Y][cur.X].S = WALL_DOWN
      maze[nxt.Y][nxt.X].N = WALL_DOWN
    elif cur.Y > nxt.Y and cur.N != WALL_FIXED:
      print("removeWall 2")
      maze[cur.Y][cur.X].N = WALL_DOWN
      maze[nxt.Y][nxt.X].S = WALL_DOWN
    elif cur.X < nxt.X and cur.E != WALL_FIXED:
      print("removeWall 3")   
      maze[cur.Y][cur.X].E = WALL_DOWN
      maze[nxt.Y][nxt.X].W = WALL_DOWN
    elif cur.X > nxt.X and cur.W != WALL_FIXED:
      print("removeWall 4")   
      maze[cur.Y][cur.X].W = WALL_DOWN
      maze[nxt.Y][nxt.X].E = WALL_DOWN    
    else:
      print("removeWall 5")   

maze = createmaze(mazeWidth, mazeLength)
theStack = []
current = maze[0][0] # choose initial
theStack.append(current) # push current

# choose unvisited neighbor, pop if none found
next = chooseUnvisited()
next.visited = True
print("Next "+str(next.X)+" "+str(next.Y))
while len(theStack) > 0:
  while next == current and len(theStack) > 0:
    current = theStack.pop()
    next = chooseUnvisited()
    next.visited = True
    print("Next "+str(next.X)+" "+str(next.Y))
 
  removeWall(current, next)
 
  current = next
  next = chooseUnvisited()
  next.visited = True
  if next != current:
    theStack.append(current)  
   
drawMaze()

Tuesday, May 14, 2019

Advantage of Python over Java

Here's a question for you: what is the advantage of using Python over Java? Because of the recent publication of the second edition of Extending Art of Illusion I've been spending time working on scripts for Art of Illusion. Straight out of the box, Art of Illusion provides scripting capability for Groovy and BeanShell. This makes sense because Art of Illusion is written in Java and both of these scripting languages are based on Java. But most other graphics programs use some variation on Python as their scripting language. In a post on Python Scripts in Art of Illusion I wrote about creating a plugin that adds Python to the languages that Art of Illusion supports. I use Python for things I do in my day job mostly because it's available. But with adding it to Art of Illusion I have a direct comparison between Java and Python. It got me thinking. Is there some reason to choose to script in Python rather than using Java, Groovy, or BeanShell?


When I setup the interpreter, I did my best to provide consistency between what can be done with Python and what can be done with Groovy. The types of scripts are the same. The editor windows are the same. The libraries linked in from Art of Illusion are the same. I'm using the Jython interpreter, so everything that can be called from a Java class can be called from Python. So, in considering the question it really does come down to language differences.


Groovy and Python handle blocks differently. Java is very similar to C, so a block is whatever is within curly braces. In Python blocks are handled by indenting. There's nothing particularly advantageous for one over the other. It is personal preference. Personally, I've fine with both. Comments are also handled differently. Groovy uses C++ style comments while Python uses # to mark a comment. Again, that's just personal preference. Strings are handled by both and what you can do with strings is very similar.


The biggest difference between Python and Java is that Python has powerful list processing capability built right in while Java treats it as more of an afterthought. Given that 3D graphics scenes are lists of objects and objects are lists of polygons and polygons are lists of vertices, how a scripting language processes lists is important. In Python, a list is denoted by square brackets, []. In Art of Illusion we're most likely to get a list by using values returned from some Java function. In addition to lists, Python has dictionaries (square brackets {}), which are an unordered table that maps keys to values. It also has sets, which are unordered collections of unique objects.


A list might simply be every object in the scene or every object of a certain type and we want to perform some operation on each of the objects in the list. We might use a dictionary if we want to refer to objects by name. Sets are only truly useful in combination. We might have a set of edge vertices and a set of interior vertices. We might have a set of visible vertices and a set of invisible vertices. If we wanted to perform some operation on all of the visible edge vertices then we could get the intersection of the edge vertices set and the visible set and then loop through that set rather than having a check in our loop for whether a set of conditions is met. Of course, it is possible to do this with Java as well, but having it inherent in the language is nice.

Sunday, May 12, 2019

Python Scripts in Art of Illusion

I took a break from the cloth simulator to look at adding Python as a scripting language for Art of Illusion. I had some success at pulling in the Jython library and was able to get the following Tool Script to add a Cube to the scene:

undo = UndoRecord(window, True)
obj = Cube(1.0, 1.0, 1.0)
objInfo = ObjectInfo(obj, CoordinateSystem(), "Cube "+str(1))
window.addObject(objInfo, undo)
window.updateImage()
window.setUndoRecord(undo)


In a video I demonstrated that a Groovy script could be used to add a Cube to the scene. The Groovy script was as follows:

undo = new UndoRecord(window, true);
obj = new Cube(1.0, 1.0, 1.0);
objInfo = new ObjectInfo(obj, new CoordinateSystem(), "Cube "+1);

window.addObject(objInfo, undo);                      
window.updateImage();
window.setUndoRecord(undo);

As you can tell, the code is essentially the same except Python doesn't use the new keyword, True is capitalized in Python, and Python won't concatenate an integer to a string without a conversion function. I'm sure there will be more noticeable differences once I start using it along with the language features of Python, but this was just a proof of concept.

Unfortunately, I've been less successful at implementing the code required to use Python to create a Scripted Object in Art of Illusion. It might be easier if I were modifying the Art of Illusion code rather than trying to do this as a plugin, but I keep running into one typecasting issue or another. I had hoped that I might use this as the vehicle to demonstrate the value of the reference material at the back of Extending Art of Illusion. I can say that I've had the book open to that material the whole time I've been working on this and I have put it to good use. I can also say that I'm very glad this book is available in hardback. It costs a little extra, but being able to open it and leave it open makes a world of difference.

UPDATE: After writing this I found a way to get it working for Scripted Objects. I put it in a repository at https://github.com/TimothyFish/AOI_with_python.git so I wouldn't lose what I had working. It's not ready for general use, but it is working. To use it, you will need to have the Jython.jar file in your class path for Art of Illusion. If you know what that means then have fun. If not then it's probably better that you don't mess with it. I would explain it but I only just got it working myself and I don't know that I can give you a clean cut set of instructions yet.

For anyone who is interested, the following is the Python version of the Axis script that is in Extending Art of Illusion:

from artofillusion.script import ScriptedObject
scene = theScript.getScene()
count = 0

length = 5.0
size = 0.01

def createAxis(length, diameter):
  axis = Cylinder(length, diameter, diameter, 1.0)
  return ObjectInfo(axis, CoordinateSystem(), "")
 
obj = createAxis(length, size)
if not obj is None:
  theScript.addObject(obj)
 
obj = createAxis(length, size)
obj.getCoords().setOrientation(90.0, 0.0, 0.0)
if not obj is None:
  theScript.addObject(obj)

obj = createAxis(length, size)
obj.getCoords().setOrientation(0.0, 0.0, 90.0)
if not obj is None:
  theScript.addObject(obj)


You may notice that I'm using "theScript" here instead of "script". This is because there is a conflict between "script" and "artofillusion.script" with Jython interprets the code.

Friday, May 10, 2019

Art of Illusion Examples

For each of the first ten chapters of Extending Art of Illusion I have created a video that demonstrates some aspect of it. One of the things I found when writing the book was that it is difficult to demonstrate animation on the printed page. In chapter 7 I give code for a tracker object that will rotate an object or objects so that the object is always pointing at one of the other scene objects, no matter where it is located in the scene. In the book I tried to show this with a sequence of still images taken from the animation. But in the video I have been able to show the video of a head with eyes that follow an object as it moves around the scene. Not only that, but it is possible to show that these changes are occurring even as the scene is being edited.

Below is the list of videos:

Chapter 1: Use a Script to Add a Cube to Art of Illusion
Chapter 2: Storing and Loading
Chapter 3: Position Objects on Floor
Chapter 4: Point at Objects
Chapter 5: Resting One Object on Another
Chapter 6: Adding Custom Objects
Chapter 7: Tracking Movement
Chapter 8: Procedural Modules
Chapter 9: The Room
Chapter 10: Modeling Cloth in Art of Illusion