Plowing (Van Gogh Bedroom) Project


Summary
Ilan Strauss, Sylvia Niemira and David Colburn made a three-dimensional version of Van Gogh's bedroom as a reference to the room created in Richard Powers' Plowing the Dark. To do this, Ilana created the walls and furniture, modified from the original painting. She gave these to Sylvia, who turned them into vector-based images through Adobe inDesign. And David put them into the Skybox. Finally, Ilana typed some passages out of Plowing the Dark, to be read by computer voice software as visitors enter the scene and made the website.


Step 1: Drawing , Image Editing, and Audio Process (Ilana Strauss)
I tried my best to make the four walls, ceiling, floor, and side of furniture look like Van Gogh could have painted them. To do this, I found a high resolution photo of the Van Gogh bedroom painting on Google images. I brought it into Adobe Photoshop. There, I used a combination of photo editing techniques and drawing to create bits of the room not shown in the painting.


Walls
I modified the walls by removing the images of the furniture with Adobe Photoshop and use the same program to draw and fill in the gaps. I also imagined what might exist in the space Van Gogh doesn't show – space behind the furniture, parts of the walls that aren't shown, etc.

For each image to send to Sylvia, I started by cropping a section that I meant to cover. For example, when I wanted to create the back wall, I cropped out the back wall portion of the painting. Much of the back wall is shown in the painting, but a large amount of it is covered by the bed.

Also, the top few inches of the wall isn't shown. I needed these to be able to build the wall up to the point where it can attach to the ceiling. So, I increased the canvas size and both copy chunks of the wall and used the stamp tool to get a realistic looking top few inches. Additionally, I extended the window using the paintbrush.

The back wall is completely unseen in the painting. So I could be as creative as I wanted with it. I toyed with various ideas -- adding a scene from another Van Gogh painting, putting in a completely random image (say, the DeLorean from Back to the Future), etc -- but I ended up settling on making it look like another wall of the room.

I added a door, which was really a modified piece of the closet from the Van Gogh painting. Then, for fun, I added various paintings. I added a piece I had painted, a Don McLean album cover, since he wrote a song about Van Gogh, some of Sylvia's artwork, and other things of the sort. On a similar note, I found a photo of Powers to put on the ceiling.

Audio
For the audio, I typed up three passages from Richard Powers'Plowing the Dark. I then put them into a voice to audio program online at http://www2.research.att.com/~ttsweb/tts/demo.php.
Furniture
For the furniture, I used parts of the images in the painting, as well as outside material, and imagined what the object would look like in the 3rd dimension. I created the various sides of the furniture to be put onto a transparent cube in the room.

Complete accuracy (making the exact fuzzy mix of colors rather than one, solid color for example) wasn't necessary, since I knew most of the colors will be simplified and made into shapes anyway when Sylvia put them in Illustrator.

The large area behind the bed was pretty much up to me to decide what to do with. Since clothes were hanging off the rack near the bed, I drew some more clothes.

I used other Photoshop effects for other parts of the room. Sometimes, it was necessary to use the liquify function, to stretch certain shapes (such as the chair) that had been painted on a tilt. The sort of diagonal lines Van Gogh used to create perspective in the painting were unnecessary in this new, three-dimensional creating.

Detail had to be added to some objects. Van Gogh's bed, in particular, was lacking in detail in the painting. So I looked at various photos of beds and created additional folds in the covers and shadows in the pillows.

Website
I made a website for the project using HTML and some CSS. I wanted to keep it to one page for simplicity. Since just throwing all the information one one page would make it a ridiculously long page, I used scrollboxes.



Step 2: Making Vector Images (Sylvia Niemira)
Images need to have bold colors that are not too complex (not too many different colors) and have crisp lines of all objects in the picture. The best way to achieve these results is to make a vector-based image in Adobe Illustrator, using an original image with more detail.


Create an Adobe Illustrator file that is 1024x1024 pixels.



Copy and paste the original image into Illustrator. Press down the shift key and drag the corner of the image while it is selected to resize it to as large as possible without it becoming distorted. Lock the layer that the image is on my clicking the small box next to the name of the layer on the layer palate. A lock should appear.



Create a new layer. Draw a rectangle the size of the "paper" and make it the color of the background using the eyedropper tool. Save this color by clicking on the box that contains this color and dragging it over to the color palate. Make this layer invisible by clicking on the eyeball next to the layer name on the layer palate. The eye will disappear, and the layer will be invisible.



Create a new layer. Using the pencil tool, outline an object in the picture. Make it the proper color using the eyedropper tool. Save this color. Make this layer invisible.



Create a new layer. Using the pencil tool, outline details of the object that are highlights of the main color. Make them the main color by clicking on the swatch in the color palate (previously created), then double-clicking on the box containing this color, and dragging the circle directly up. This makes these shapes a highlight color. Save this new color, then make the layer invisible.



Make lowlights (shadows) using the same process, but making the details' colors darker.

Continue this process for each object in the picture until the picture is complete.

Make the layer with the original picture invisible. Select the entire picture by pressing [Ctrl]+[a]. Then copy the selection. Open a photoshop file and create a document that is 1024x1024 pixels. Paste the image (if given the option, paste as a "smart object"). Drag to make it the size of the file by pressing shift as you drag the corner. Save the file as a jpg.

A few helpful tips:

If you make a mistake with the pencil tool, you can draw over it again while it is still selected to correct the mistake.

Don't be afraid to zoom in really close. The image may become distorted, but you'll be able to draw more detailed vectors. (Zoom in using [Ctrl]+[+]; zoom out using [Ctrl]+[-]; zoom all the way out using [Ctrl]+[0])

For things like fabric, there may be a large dark shadow, with folds and details inside the shadow. To achieve this effect, create the details as you normally would. Then make the shadow on a separate layer, but color it black and change the color effect to "multiply" and take down the opacity, as shown below.





Step 3: Programming (David Colburn)
he skybox is used to draw each of the walls, the ceiling, and the floor of the room. There are three object classes, the chair, the wand, and the bed. Each class contains information for drawing the object and for interacting with it. The objects use texture mapping, like the skybox, and are incomplete because they do not have transparent backgrounds.


#!/usr/bin/python
from PySZG import *
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
import sys
from PIL import Image
import skybox
firstFrame = True

LOCAL_UNITS_PER_FOOT = 1.

#### Class definitions & implementations. ####
# Chair: a chair with a white frame that changes green when touched
# (i.e. when the effector tip is within 1 ft.) and can be dragged around.

class Chair(arPyInteractable):
def __init__(self):
# must call superclass init so that on…() methods get called
# when appropriate.
arPyInteractable.__init__(self)

def __del__(self):
print ‘deleting Chair.’

# This will get called on each frame in which this object is selected for
# interaction (‘touched’)
def onProcessInteraction( self, effector ):
# button 3 toggles visibility (actually solid/wireframe)
if effector.getOnButton(3):
self.setVisible( not self.getVisible() )

def draw(self):
glColor3f(1,1,1)
glPushMatrix()
glMultMatrixf( self.getMatrix().toTuple() )
glEnable(GL_TEXTURE_2D)
glDisable(GL_DEPTH_TEST)
glPushAttrib(GL_ENABLE_BIT)

# Load texture 7 (chair)
glBindTexture(GL_TEXTURE_2D, 7)

# The following are relative coordinates to the chair’s position
glBegin(GL_QUADS)
glTexCoord2f(0.0, 0.0)
glVertex3f(-1.0, -2.0, 0.0)
glTexCoord2f(1.0, 0.0)
glVertex3f(1.0, -2.0, 0.0)
glTexCoord2f(1.0, 1.0)
glVertex3f(1.0, 2.0, 0.0)
glTexCoord2f(0.0, 1.0)
glVertex3f(-1.0, 2.0, 0.0)
glEnd()
glPopAttrib()
glBindTexture(GL_TEXTURE_2D, 0)
glEnable(GL_DEPTH_TEST)
glScalef( 2., 4., 1./12. )
if self.getVisible():
# set one of two colors depending on if this object has been selected for interaction
if self.getHighlight():
glColor3f( 0,1,0 )
else:
glColor3f( 1,1,1 )
glutWireCube(1)
glPopMatrix()

class Bed(arPyInteractable):
def __init__(self):
# must call superclass init so that on…() methods get called
# when appropriate.
arPyInteractable.__init__(self)

def __del__(self):
print ‘deleting Bed.’

# This will get called on each frame in which this object is selected for
# interaction (‘touched’)
def onProcessInteraction( self, effector ):
# button 3 toggles visibility (actually solid/wireframe)
if effector.getOnButton(3):
self.setVisible( not self.getVisible() )

def draw(self):
glColor3f(1,1,1)
glPushMatrix()
glMultMatrixf( self.getMatrix().toTuple() )
glEnable(GL_TEXTURE_2D)
glDisable(GL_DEPTH_TEST)
glPushAttrib(GL_ENABLE_BIT)

# Load texture 8 (bed)
glBindTexture(GL_TEXTURE_2D, 8)

# The following are relative coordinates to the bed’s position
glBegin(GL_QUADS)
glTexCoord2f(0.0, 0.0)
glVertex3f(-2.0, -1.0, 0.0)
glTexCoord2f(1.0, 0.0)
glVertex3f(2.0, -1.0, 0.0)
glTexCoord2f(1.0, 1.0)
glVertex3f(2.0, 1.0, 0.0)
glTexCoord2f(0.0, 1.0)
glVertex3f(-2.0, 1.0, 0.0)
glEnd()
glPopAttrib()
glBindTexture(GL_TEXTURE_2D, 0)
glEnable(GL_DEPTH_TEST)
glScalef( 4., 2., 1./12. )
if self.getVisible():
# set one of two colors depending on if this object has been selected for interaction
if self.getHighlight():
glColor3f( 0,1,0 )
else:
glColor3f( 1,1,1 )
glutWireCube(1)
glPopMatrix()

# RodEffector: an effector that uses input matrix 1 for its position/orientatin
# and has 6 buttons starting at index 0. It is visually and functionally a 5-ft.
# rod with the hot spot at the tip (see szg/src/interaction/arEffector.h)
class RodEffector( arEffector ):
def __init__(self):
arEffector.__init__(self,1,6,0,0,0)

# set length to 5 ft.
self.setTipOffset( arVector3(0,0,-5) )

# set to interact with closest object within 1 ft. of tip
# (see PySZG.py for alternative classes for selecting objects)
self.setInteractionSelector( arDistanceInteractionSelector(1.) )

# set to grab an object (that has already been selected for interaction
# using rule specified on previous line) when button 0 or button 2
# is pressed and held. Button 0 will allow user to drag the object with orientation
# change, button 2 will allow dragging but square will maintain fixed orientation.
# The arGrabCondition specifies that a grab will occur whenever the value
# of the specified button event # is > 0.5.
self.setDrag( arGrabCondition( AR_EVENT_BUTTON, 0, 0.5 ), arWandRelativeDrag() )
self.setDrag( arGrabCondition( AR_EVENT_BUTTON, 2, 0.5 ), arWandTranslationDrag() )

def draw(self):
glPushMatrix()
glMultMatrixf( self.getCenterMatrix().toTuple() )

# Draw grey rectangular solid 2″x2″x5′
glScalef( 2./12, 2./12., 5. )
glColor3f( .5,.5,.5 )
glutSolidCube(1.)

# Superimpose slightly larger black wireframe (makes it easier to see shape)
glColor3f(0,0,0)
glutWireCube(1.03)
glPopMatrix()

##### The application framework ####
#
# The arPyMasterSlaveFramework automatically installs certain of its methods as the
# master/slave callbacks, e.g. the draw callback is onDraw(), etc.

class SkeletonFramework(arPyMasterSlaveFramework):
def __init__(self):
arPyMasterSlaveFramework.__init__(self)

# Our objects and effector
self.theChair = Chair()
self.theBed = Bed()
self.theWand = RodEffector()

# List of objects to interact with
self.interactionList = [self.theChair, self.theBed]

# Tell the framework what units we’re using.
self.setUnitConversion( LOCAL_UNITS_PER_FOOT )

# Near & far clipping planes.
nearClipDistance = .1*LOCAL_UNITS_PER_FOOT
farClipDistance = 100.*LOCAL_UNITS_PER_FOOT
self.setClipPlanes( nearClipDistance, farClipDistance )

#### Framework callbacks — see szg/src/framework/arMasterSlaveFramework.h ####

# start (formerly init) callback (called in arMasterSlaveFramework::start())
# NOTE: now called before window is created, so no OpenGL initialization here.
def onStart( self, client ):
# Necessary to call base class method for interactive prompt
# functionality to work (run this program with ‘–prompt’).
arPyMasterSlaveFramework.onStart( self, client )

# Register variables to be shared between master & slaves
self.initSequenceTransfer(‘transferChair’)
self.initSequenceTransfer(‘transferBed’)

#Setup navigation, so we can drive around with the joystick
#
# Tilting the joystick by more than 20% along axis 1 (the vertical on ours) will cause
# translation along Z (forwards/backwards). This is actually the default behavior, so this
# line isn’t necessary.
self.setNavTransCondition( ‘z’, AR_EVENT_AXIS, 1, 0.2 )

# Tilting joystick left or right (axis 0) will rotate left/right around vertical axis (default is left/right
# translation)
self.setNavRotCondition( ‘y’, AR_EVENT_AXIS, 0, 0.2 )

# Set translation & rotation speeds to 5 ft/sec & 30 deg/sec (defaults)
self.setNavTransSpeed( 5. )
self.setNavRotSpeed( 30. )

# set square’s initial position
self.theChair.setMatrix( ar_translationMatrix(0,2,-5) )
self.theBed.setMatrix( ar_translationMatrix(1,7,-3) )

return True

# Callback for doing window OpenGL initialization. Called from framework.start().
def onWindowStartGL( self, winInfo ):
# Clear the cube
glClearColor(0,0,0,0)

# Load the skybox (textures 1-6)
skybox.loadskybox()

# Load the chair image
chairimg = Image.open(“./images/chair.jpg”).resize((256,512))
chairtex = chairimg.tostring()

# Load the bed image
bedimg = Image.open(“./images/bed.jpg”).resize((512,256))
bedtex = bedimg.tostring()

# Bind the chair to texture 7
glGenTextures(1)
glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
glBindTexture(GL_TEXTURE_2D, 7)
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP)
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP)
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 256, 512, 0, GL_RGB, GL_UNSIGNED_BYTE, chairtex)

# Bind the bed to texture 8
glGenTextures(1)
glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
glBindTexture(GL_TEXTURE_2D, 8)
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP)
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP)
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 256, 512, 0, GL_RGB, GL_UNSIGNED_BYTE, bedtex)

# Callback called before data is transferred from master to slaves. Now
# called _only on the master_. This is where anything having to do with
# processing user input or random variables should happen. Normally, most
# of the frame-by-frame work of the program should be done here.
def onPreExchange( self ):
# Necessary to call base class method for interactive prompt
# functionality to work (run this program with ‘–prompt’).
arPyMasterSlaveFramework.onPreExchange(self)

# handle joystick-based navigation (drive around). The resulting
# navigation matrix is automagically transferred to the slaves.
self.navUpdate()

# update the input state (placement matrix & button states) of our effector.
self.theWand.updateState( self.getInputState() )

# Handle any interaction with the squares (see interaction docs).
# Any grabbing/dragging/deletion of squares happens in here.
ar_processInteractionList( self.theWand, self.interactionList )

# Pack data we have to transfer to slaves into appropriate variables
# What we end up with here is a tuple containing two Ints and a nested tuple
# containing 16 Floats. At this end (i.e. in the master), any sequence type
# is allowed (e.g. lists, numarray arrays, array module arrays, I don’t know what
# all else), but the elements of any sequence must be Ints, Floats, or Strings (or
# a nested sequence). They don’t all have to have the same type within a sequence,
# but only those types are allowed.
transferTupleChair = (self.theChair.getHighlight(), self.theChair.getVisible(), \
self.theChair.getMatrix().toTuple())
transferTupleBed = (self.theBed.getHighlight(), self.theBed.getVisible(), \
self.theBed.getMatrix().toTuple())
self.setSequence( ‘transferChair’, transferTupleChair )
self.setSequence( ‘transferBed’, transferTupleBed )

# Callback called after transfer of data from master to slaves. Mostly used to
# synchronize slaves with master based on transferred data. Note that you normally
# Shouldn’t be doing a whole lot of computation here; the exception would be if
# you have a complex state that can be computed from a relatively small number of
# parameters, you might compute those parameters on the master, transfer them to
# the slaves, & do the complex state computation in master and slaves here.
def onPostExchange( self ):
# Do stuff after slaves got data and are again in sync with the master.
if not self.getMaster():

# Update effector’s input state. On the slaves we only need the matrix
# to be updated, for rendering purposes.
self.theWand.updateState( self.getInputState() )

# Unpack our transfer sequence. Note that no matter what sequence types
# you used at the input end in the master, they all come out as tuples here.
# For example, if in the master you passed in a sequence that was a 3-element
# numarray array of Floats, when you unpacked it here you’d have a 3-element
# tuple of Floats.
transferTupleChair = self.getSequence( ‘transferChair’ )
self.theChair.setHighlight( transferTupleChair[0] )
self.theChair.setVisible( transferTupleChair[1] )
self.theChair.setMatrix( arMatrix4( transferTupleChair[2] ) )
transferTupleBed = self.getSequence( ‘transferBed’ )
self.theBed.setHighlight( transferTupleBed[0] )
self.theBed.setVisible( transferTupleBed[1] )
self.theBed.setMatrix( arMatrix4( transferTupleBed[2] ) )

# Draw callback
def onDraw( self, win, viewport ):
skybox.drawskybox()
global firstFrame
if firstFrame:
screen = viewport.getScreen()
print ‘Screen (camera) parameters:’
print ‘ center:’,screen.getCenter()
print ‘ normal:’,screen.getNormal()
print ‘ up:’,screen.getUp()
print ‘ size:’,screen.getWidth(), ‘x’, screen.getHeight()
firstFrame = False
# Load the [inverse of the] navigation matrix onto the OpenGL modelview matrix stack.
self.loadNavMatrix()

# Draw stuff
self.theChair.draw()
self.theBed.draw()
self.theWand.draw()

if __name__ == ‘__main__’:
framework = SkeletonFramework()
if not framework.init(sys.argv):
raise PySZGException,’Unable to init framework.’
print ‘Framework inited.’
# Never returns unless something goes wrong
if not framework.start():
raise PySZGExcreption,’Unable to start framework.’