Final image

Thursday, October 14th, 2010

After exactly one month of development I’m finally calling this project finished. Most of the time went into learning the new technology. Currently I’m waiting for the printers to send me the actual Lenticular print.

I’m attaching the files the sequence was made with. I actually had to do multiple passes. Once just to create the cartoon edge, once to create the actual images, and then using a third file to combine the two. The “birender” file is the one that looks most like the end result. Be carefull though, I used a computer at the Blender Institute to render it because it would have just taken too long to do on my laptop.

Here is a small video recorded on my mobile of what the print looks like

To the printers!

Thursday, October 7th, 2010

It is that time… last night I sent the final image off to the printer/laminator. I spent all of yesterday at the Blender Institute rendering the images. They turned out rather well I have to say!

Crazy scale!

The final interlaced image is: 16992 by 9558 pixels, 111mB, and made up of thirty six 8496 by 4779 pixel images. It’s huge! But since it’s 720DPI it’ll only print 2 foot wide. Rendering the images on an incredibly quick computer took nearly 5 hours.

Time for a title.

Last thing… I need a title for the work…

Time is running out

Monday, October 4th, 2010

Downloadable Files

After working on the lenticular for quite a while already, today I truly realised how little time I have left. The affordable art fair is coming up quick! So… that leads me to some decisions.

Printing

I would have liked to print at a local place, which would have saved me quite a bit of money, but that would have been a lot of back and forth, both to the printer and the laminator. There’s no time for this at all. So in stead I’ll spend a bit extra and have the laminator print and adjust my image to get an optimal result. It does limit the scale of the final image somewhat. He can’t print wider than 60cm (2 feet), which is a shame… but saves me rendertime!

Design

I’m sticking with my original idea for the design even though I had my doubts. When a process takes this long, at a certain point you always start doubting yourself. I think this will work rather well. Here’s a single image from the sequence… still needs a little work!

Scripting

I’ve been doing a lot of scripting as well… A nice new little script is one I wrote to add a line to the left of any image. So I can just make a full size interlaced image, crop it… add the line (for aligning the lense sheet), then print. I’ll also attach the latest version of the interlacing script.

Back to basics

Sunday, September 26th, 2010

Downloadable Files

After trying heaps of prints, my results started to get muddled. I kept running into multiple issues, and exposed a huuuge error in my thinking! So I decided to change my tactics and start at the beginning. I made a very very simple scene to test camera motion.

The big flaw

I was hoping to create a “deep tunnel” effect. Now this is nice… and may work, but… the motion you can show is rather limited. If you make a tunnel that can be viewed from multiple angles… then there’s a huge difference between the images. Basicly it’s the same as the effect where in a movie it looks like the wheels of a car are spinning backwards… if the difference between the frames is too large there’s no way for your brain to determin what’s actually going on.

The solution

I made a flat scene… then set the camera up to move “naturally”, basicly having the camera emulate the path the audience will take, at a certain distance relative to the scene. This is working really nicely!

This image works rather well! And has the movement I was looking for. Now the next step is to look at the achievable depth… I’ll do multiple iterations to see what is acceptable.

Script progress

Below this article you can download the current version of the python script I’m using to interlace images. I made some basic changes to it so that messages are nicer, and show a bit more data. But the biggest change is that I made it so the script can add a little black line on the left of the image so you can align your lense sheet. This is a real big find! Without it it was nearly impossible to align the sheet, now… it’s dead easy! Just make the black line show up when you’re looking straight at the image… beautiful… works every time!

Choices

Thursday, September 23rd, 2010

I’ve skipped work on this project for a few days. After spending a week on it, working all day every day my mind turned to mush. It’s rather a complex thing to work on. I took two days rest, had to work on another thing for a day, and now am back on it.

Progress is sweet!

The scene I have been working on is progressing well. I’m finding out lots doing my experiments. A big one is that motion should be small… Normally when you animate you think of frames per second and whether something moves quick enough in a second… When you make lenticulars it’s all about motion between frames… the difference between the frames shouldn’t be very big.

What I’m doing now is OpenGL renders from inside Blender 3D to check on camera motion. Here’s my latest little test (for a 40LPI sheet).

You can see that it doesn’t look like as much of a “washed out” mess as the previous tests. That is due to two factors. One is that the motion relative to the frames is much smaller… so the difference between frames is smaller too. And I’ve also cropped the image. So, it’s a full size render (two feet wide), but in this image you only see a detail of the large image.

Cropping

To crop I wanted to use the Border Render option in Blender 3D, but sadly that doesn’t work with OpenGL renders. Thankfully the aspect ratio settings do work there. To then crop all the images I get from OpenGL I wrote a tiny little extra python script which also uses the PIL library

  1. # --------------------------------------------------------------------------
  2. # ***** BEGIN GPL LICENSE BLOCK *****
  3. #
  4. # Copyright (C) 2010: macouno, http://www.macouno.com
  5. #
  6. # This program is free software; you can redistribute it and/or
  7. # modify it under the terms of the GNU General Public License
  8. # as published by the Free Software Foundation; either version 2
  9. # of the License, or (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program; if not, write to the Free Software Foundation,
  18. # Inc, 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
  19. #
  20. # ***** END GPL LICENCE BLOCK *****
  21. # --------------------------------------------------------------------------
  22.  
  23. '''
  24. This script takes a number of images from the directory the script is in itself,
  25. and composites them into one with alternating strips
  26.  
  27. It is the start of a script for doing composites for Lenticular style prints.
  28. It is still messy, quite experimental, and imperfect, but a first step.
  29.  
  30. Written to run with Python 2.5 and the PIL library
  31. '''
  32.  
  33. import math, glob
  34.  
  35. STATE = {}
  36.  
  37. # Set the word to look for in the tile images to identify them
  38. # (use names such as tilename.0001.png, or tilename0001.png for instance)
  39. STATE['tileName'] = 'stalkie'
  40.  
  41. imX = 14400.0
  42. imY = 8010.0
  43.  
  44. tileX = 960.0
  45. tileY = 8010.0
  46.  
  47. # Crop settings (for this project, testing with openGL renders)
  48. cropX = 3000.0
  49. cropY = 3000.0
  50.  
  51. offX = int((tileX / imX) * 3000.0)
  52. offY = int((tileY / imY) * 2000.0)
  53.  
  54. print 'offsets',offX,offY
  55.  
  56. xStart = offX
  57. yStart = offY
  58.  
  59. xEnd = int(offX + ((tileX / imX) * cropX ))
  60. yEnd = int(offY + ((tileY / imY) * cropY ))
  61.  
  62. print 'cropvalues',xStart, yStart, xEnd, yEnd
  63.  
  64. # Import the PIL library if possible
  65. try:
  66.         import PIL
  67.         from PIL import Image
  68. except:
  69.         print 'error PIL library not found'
  70.         exit()
  71.  
  72. for i, infile in enumerate(glob.glob('*.png')):
  73.  
  74.         if STATE['tileName'] in infile:
  75.                 print 'cropping',i,infile
  76.                 im = Image.open(infile)
  77.  
  78.                 im = im.crop((xStart, yStart, xEnd, yEnd))
  79.  
  80.                 im.save(infile)
  81.  
  82.                 print 'saved',infile
  83.  
  84. print '-- finished --'

Display clean python code for copying

# --------------------------------------------------------------------------
# ***** BEGIN GPL LICENSE BLOCK *****
#
# Copyright (C) 2010: macouno, http://www.macouno.com
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc, 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
# ***** END GPL LICENCE BLOCK *****
# --------------------------------------------------------------------------

'''
This script takes a number of images from the directory the script is in itself,
and composites them into one with alternating strips

It is the start of a script for doing composites for Lenticular style prints.
It is still messy, quite experimental, and imperfect, but a first step.

Written to run with Python 2.5 and the PIL library
'''

import math, glob

STATE = {}

# Set the word to look for in the tile images to identify them
# (use names such as tilename.0001.png, or tilename0001.png for instance)
STATE['tileName'] = 'stalkie'

imX = 14400.0
imY = 8010.0

tileX = 960.0
tileY = 8010.0

# Crop settings (for this project, testing with openGL renders)
cropX = 3000.0
cropY = 3000.0

offX = int((tileX / imX) * 3000.0)
offY = int((tileY / imY) * 2000.0)

print 'offsets',offX,offY

xStart = offX
yStart = offY

xEnd = int(offX + ((tileX / imX) * cropX ))
yEnd = int(offY + ((tileY / imY) * cropY ))

print 'cropvalues',xStart, yStart, xEnd, yEnd

# Import the PIL library if possible
try:
	import PIL
	from PIL import Image
except:
	print 'error PIL library not found'
	exit()

for i, infile in enumerate(glob.glob('*.png')):

	if STATE['tileName'] in infile:
		print 'cropping',i,infile
		im = Image.open(infile)

		im = im.crop((xStart, yStart, xEnd, yEnd))

		im.save(infile)

		print 'saved',infile

print '-- finished --'

For testing the final render in Blender I do want to use the Border render function in Blender, the script for that is in my previous post.

Decisions decisions

Since I’m getting closer and closer to the final design I called the printers today with a few more questions. The sad thing is that the printer who can do lenticular, doesn’t have a great deal of choice in materials to print on. But… he says it doesn’t actually matter much for lenticulars.

There is a printer nearby here in Amsterdam who can do amazing quality prints… but they’ve never done lenticular. And they don’t have what’s needed to stick the lense sheet on.

So what I want to do, is have the printer nearby do a gorgeous print, and have the dude in Rotterdam stick on the lense sheet.

There’s a problem with that though. Apparently each sheet can be slightly different. And so can every printer give different results. Thus… there’s no guarantee that a print made here in Amsterdam, that I test with the sheet I have, will line up perfect with the sheet at the company near Rotterdam.

That said… it’s actually cheaper if I do it that way.

Now the only decision left… do I buy an epson printer, which everyone says is best for lenticulars, and is also the brand the printers here in Amsterdam use…. Then I can test properly at home at higher DPI than I have been doing (1200dpi vs 600dpi)

I might hop on the bike right now to look at some prices… though I’m not sure it’s wise to spend even more money… To be continued! Definitely!

There’s testing and then there’s testing

Saturday, September 18th, 2010

Yesterday I went over to the Blender Institute to see if I could do a quicker, bigger test render. My idea was to render an image resembling my final result, but printable on A4 with my own printer. Nice idea, but… first, it wasn’t that much quicker… it was, but it still took hours. Then… the result… well… was rather disappointing.

Render results

Here’s the image as I created it.

Now… yeah I know it looks weird, but the entire idea for a Lenticular is that you slice together multiple images. In this case I used the maximum my printer can handle… an animation of 30 frames.

Here’s the rather crucial error I made in my thinking. The end result I want will be about 60cm (2 feetish) wide. But for the test I rendered the entire image at only 18cm (7inchish) wide. Which, at 600DPI is only 3600 pixels. And.. for every image you then only have one 30th of that. In this case… 120pixels per image. That is very very low resolution. You hardly get any detail at all. So the conclusion has to be that you should always print out your scene at the scale it’s going to be in the end product. And if you want a smaller test, do not resize the image, but in stead render a smaller portion of it (a crop).

The second error I made was the animation in the scene. Lenticulars work nice, but you nearly always see multiple frames at the same time (depending on your angle of view). That effect gets less the further away you stand, but it’s still there. So… if you’re animating… don’t overdo the motion! The difference between frames needs to be rather small. If an “object” is in a pixel at one frame, and the next frame a completely different object is there, that can be confusing.

And last… my lighting was far too dark. I looked at the “levels” in GIMP, and well.. it’s rather silly. So I need to look at my lighting, and consider that prints are darker than lcd screens… yah… obvious.

Solutions

For testing with a smaller part of the final image, you don’t really want to have to render the entire thing every time. It’s nicer to render just that part. Thankfully Blender has a “border render” function. So I wrote a tiny little script that calculates the “crop” of the render so I get a 18 x 18cm image (border render and crop in the render settings need to be enabled. And in this case I had the size of the render set as: 960 x 8010 pixels and at an aspect ratio of 15 to 1 (meant to be a single slice at 600DPI)

  1. import bpy
  2.  
  3. # The target pixel size
  4. xFin = 4252
  5. yFin = 4252
  6.  
  7. # The offsets (border positioning)
  8. xoff = 0.2
  9. yoff = 0.1
  10.  
  11. scn = bpy.data.scenes[0]
  12.  
  13. rnd = scn.render
  14.  
  15. xRes = rnd.resolution_x * rnd.pixel_aspect_x
  16. yRes = rnd.resolution_y * rnd.pixel_aspect_y
  17.  
  18. print('x',xRes)
  19. print('x',yRes)
  20.  
  21. xmin = 0.0 + xoff
  22. ymin = 0.0 + yoff
  23. xmax = ((1.0/xRes) * xFin) + xoff
  24. ymax = ((1.0/yRes) * yFin) + yoff
  25.  
  26. print(xmin, xmax, ymin, ymax)
  27.  
  28. rnd.border_min_x = xmin
  29. rnd.border_max_x = xmax
  30. rnd.border_min_y = ymin
  31. rnd.border_max_y = ymax
  32.  
  33. print('-- finished --')

Display clean python code for copying

import bpy

# The target pixel size
xFin = 4252
yFin = 4252

# The offsets (border positioning)
xoff = 0.2
yoff = 0.1

scn = bpy.data.scenes[0]

rnd = scn.render

xRes = rnd.resolution_x * rnd.pixel_aspect_x
yRes = rnd.resolution_y * rnd.pixel_aspect_y

print('x',xRes)
print('x',yRes)

xmin = 0.0 + xoff
ymin = 0.0 + yoff
xmax = ((1.0/xRes) * xFin) + xoff
ymax = ((1.0/yRes) * yFin) + yoff

print(xmin, xmax, ymin, ymax)

rnd.border_min_x = xmin
rnd.border_max_x = xmax
rnd.border_min_y = ymin
rnd.border_max_y = ymax

print('-- finished --')

Scripting = everything

Friday, September 17th, 2010

These last 2 days have been all about getting my scene set up in Blender 3D. And guess what… it’s been a challenge. Here’s why: I have 8660 objects, which make up 720 characters. Each character has 2 rigs, and at least 8 constraints, plus two sets of animation data. Oh, and I’m working on a laptop, which means, the computer basicly can’t handle it, becomes uncooperative, and everything takes for ever. Here’s me at my geekiest…

Solution

Thus, the only way to nicely edit things in my scene is to do everything by python script. Luckily Campbell Barton has done an amazing job with the Python API for the new Blender version (2.54). It is just magic. So I thought I’d share the little scripts I’ve made to mess around. I’ll try to comment them, but they’re not real “released” scripts, just stuff I’ve hacked together, but they might be of use to you.

Setting subdivision levels, and removing subsurf modifiers

  1. import bpy
  2.  
  3. for ob in bpy.data.objects:
  4.  
  5.     try:
  6.  
  7.         # Set the subdivision level for all objects with a mesh named 'iris'
  8.         if ob.data.name == 'iris':
  9.  
  10.             ob.data = bpy.data.meshes['simpleiris']
  11.  
  12.             print('set data for',ob.name)
  13.  
  14.             #for m in ob.modifiers:
  15.  
  16.                 if m.type == 'SUBSURF':
  17.  
  18.                     m.levels = 1
  19.                     m.render_levels = 1
  20.  
  21.         # Remove all subsurf modifiers from objects with a mesh named 'simpleiris'
  22.         if ob.data.name == 'simpleiris':
  23.  
  24.             ob.name = 'iris'
  25.  
  26.             d = 'x'
  27.  
  28.             for i, m in enumerate(ob.modifiers):
  29.  
  30.                 if m.type == 'SUBSURF':
  31.  
  32.                     d = i
  33.  
  34.             if not d == 'x':
  35.  
  36.                 ob.modifiers.remove(ob.modifiers[d])
  37.                 print('removing from',ob.name)
  38.  
  39.     except:
  40.         print('no data or error')
  41.  
  42. print(' -- finished --')

Display clean python code for copying

import bpy

for ob in bpy.data.objects:

    try:

        # Set the subdivision level for all objects with a mesh named 'iris'
        if ob.data.name == 'iris':

            ob.data = bpy.data.meshes['simpleiris']

            print('set data for',ob.name)

            #for m in ob.modifiers:

                if m.type == 'SUBSURF':

                    m.levels = 1
                    m.render_levels = 1

        # Remove all subsurf modifiers from objects with a mesh named 'simpleiris'
        if ob.data.name == 'simpleiris':

            ob.name = 'iris'

            d = 'x'

            for i, m in enumerate(ob.modifiers):

                if m.type == 'SUBSURF':

                    d = i

            if not d == 'x':

                ob.modifiers.remove(ob.modifiers[d])
                print('removing from',ob.name)

    except:
        print('no data or error')

print(' -- finished --')

Enable disable layers on specific objects

  1. import bpy
  2.  
  3. lTargets = [ob for ob in bpy.data.groups['localtargets'].objects]
  4.  
  5. print(len(lTargets),'to do')
  6.  
  7. #lTargets = [ob for ob in bpy.context.selected_objects]
  8.  
  9. #Loop through the target objects and enable/disable specific layers
  10. for c, ob in enumerate(lTargets):
  11.  
  12.     tLayers = []
  13.  
  14.     for i, l in enumerate(ob.layers):
  15.  
  16.         if i == 5:
  17.             tLayers.append(True)
  18.         elif i == 15:
  19.             tLayers.append(False)
  20.         elif i < 20:
  21.             tLayers.append(l)
  22.  
  23.         print(i,l)
  24.  
  25.     print(len(tLayers))
  26.  
  27.     ob.layers = tLayers
  28.  
  29.     print(c, ob.name)
  30.  
  31. print('--finished--')

Display clean python code for copying

import bpy

lTargets = [ob for ob in bpy.data.groups['localtargets'].objects]

print(len(lTargets),'to do')

#lTargets = [ob for ob in bpy.context.selected_objects]

#Loop through the target objects and enable/disable specific layers
for c, ob in enumerate(lTargets):

    tLayers = []

    for i, l in enumerate(ob.layers):

        if i == 5:
            tLayers.append(True)
        elif i == 15:
            tLayers.append(False)
        elif i < 20:
            tLayers.append(l)

        print(i,l)

    print(len(tLayers))

    ob.layers = tLayers

    print(c, ob.name)

print('--finished--')

Set the subdivision level on objects lower the further they are from the camera

  1. import bpy
  2.  
  3. camLoc = bpy.data.objects['Camera'].matrix_world.translation_part()
  4.  
  5. for ob in bpy.data.objects:
  6.  
  7.     try:
  8.  
  9.         n = ob.data.name
  10.  
  11.         # Only work on objects with a specific mesh linked in
  12.         if n == 'neck' or n == 'eyeball' or n == 'pupil' or n == 'foot':
  13.  
  14.             gLoc = ob.matrix_world.translation_part()
  15.  
  16.             # Get the vector from the camera to the object so we know how far away it is
  17.             relLoc = gLoc - camLoc
  18.  
  19.             if relLoc.length > 40:
  20.  
  21.                 nLevel = 0
  22.  
  23.             elif relLoc.length < 20 and not (n == 'pupil' or n == 'foot'):
  24.  
  25.                 nLevel = 2
  26.  
  27.             else:
  28.  
  29.                 nLevel = 1
  30.  
  31.             # Set the subdivision level
  32.             for m in ob.modifiers:
  33.  
  34.                 if m.type == 'SUBSURF':
  35.  
  36.                     m.render_levels = nLevel
  37.                     m.levels = nLevel
  38.  
  39.                     print('set level',nLevel,'for',ob.name)
  40.  
  41.     except:
  42.         print('no data or error')
  43.  
  44. print('--finished--')

Display clean python code for copying

import bpy

camLoc = bpy.data.objects['Camera'].matrix_world.translation_part()

for ob in bpy.data.objects:

    try:

        n = ob.data.name

        # Only work on objects with a specific mesh linked in
        if n == 'neck' or n == 'eyeball' or n == 'pupil' or n == 'foot':

            gLoc = ob.matrix_world.translation_part()

            # Get the vector from the camera to the object so we know how far away it is
            relLoc = gLoc - camLoc

            if relLoc.length > 40:

                nLevel = 0

            elif relLoc.length < 20 and not (n == 'pupil' or n == 'foot'):

                nLevel = 2

            else:

                nLevel = 1

            # Set the subdivision level
            for m in ob.modifiers:

                if m.type == 'SUBSURF':

                    m.render_levels = nLevel
                    m.levels = nLevel

                    print('set level',nLevel,'for',ob.name)

    except:
        print('no data or error')

print('--finished--')

Set a whole bunch of objects to a specific “local” location

  1. import bpy, mathutils
  2.  
  3. newLoc = mathutils.Vector([0.0, -5.0, 10.0])
  4. scene = bpy.data.scenes[0]
  5.  
  6. for ob in bpy.data.groups['localtargets'].objects:
  7.  
  8.     ob.location = newLoc
  9.     ob.update(scene)
  10.     print('reset',ob.name)
  11.  
  12. print('-- finished --')

Display clean python code for copying

import bpy, mathutils

newLoc = mathutils.Vector([0.0, -5.0, 10.0])
scene = bpy.data.scenes[0]

for ob in bpy.data.groups['localtargets'].objects:

    ob.location = newLoc
    ob.update(scene)
    print('reset',ob.name)

print('-- finished --')

Assign a specific action to a list of objects

  1. import bpy
  2.  
  3. oList = [ob for ob in bpy.data.groups['localtargets'].objects]
  4.  
  5. ac = bpy.data.actions['LocalTargetAction']
  6.  
  7. for ob in oList:
  8.  
  9.     try:
  10.         ob.animation_data.action = ac
  11.     except:
  12.         ob.animation_data_create()
  13.         ob.animation_data.action = ac
  14.  
  15.     print('set',ob.name)
  16.  
  17. print('-- finished --')

Display clean python code for copying

import bpy

oList = [ob for ob in bpy.data.groups['localtargets'].objects]

ac = bpy.data.actions['LocalTargetAction']

for ob in oList:

    try:
        ob.animation_data.action = ac
    except:
        ob.animation_data_create()
        ob.animation_data.action = ac

    print('set',ob.name)

print('-- finished --')

Setting a whole heap of bone rotations and keyframes for them at specific frames

  1. import bpy, math, random
  2.  
  3. scene = bpy.data.scenes[0]
  4. boneList = [[ob.pose.bones['iriscontrol'],0] for ob in bpy.data.objects if 'iriscontrol' in ob.name]
  5. startList = []
  6. blinkList = []
  7. endList = []
  8. cnt = 0
  9.  
  10. # Set a random frame for a blink
  11. for b in boneList:
  12.  
  13.     r = 15.0
  14.  
  15.     while r > 13 and r < 17:
  16.  
  17.         r = int(300.0 * random.random())
  18.  
  19.     startList.append(r)
  20.     blinkList.append(r+1)
  21.     endList.append(r+2)
  22.  
  23.     if r < 30:
  24.         cnt += 1
  25.  
  26. print('cnt',cnt)
  27.  
  28. #print(boneList)
  29.  
  30. # Loop through all the frames
  31. for f in range(31):
  32.  
  33.     fr = f+1
  34.  
  35.     scene.frame_set(fr)
  36.  
  37.     for i, b in enumerate(boneList):
  38.  
  39.         if startList[i] == fr:
  40.  
  41.             b[0].rotation_euler = [math.radians(-15.0), 0.0, 0.0]  
  42.  
  43.             b[0].keyframe_insert('rotation_euler')
  44.  
  45.             print('start blink',i)
  46.  
  47.         elif blinkList[i] == fr:
  48.  
  49.             b[0].rotation_euler = [math.radians(-0.0), 0.0, 0.0]  
  50.  
  51.             b[0].keyframe_insert('rotation_euler')
  52.  
  53.             b[1] = 3
  54.             boneList[i][1] = 3
  55.  
  56.             print('blink',i)
  57.  
  58.         if endList[i] == fr:
  59.  
  60.             b[0].rotation_euler = [math.radians(-15.0), 0.0, 0.0]  
  61.  
  62.             b[0].keyframe_insert('rotation_euler')
  63.  
  64.             b[1] = 0
  65.             boneList[i][1] = 0
  66.  
  67.             print('end blink', i)
  68.  
  69.     print('-- set',f)
  70.  
  71. print('-- finished --')

Display clean python code for copying

import bpy, math, random

scene = bpy.data.scenes[0]
boneList = [[ob.pose.bones['iriscontrol'],0] for ob in bpy.data.objects if 'iriscontrol' in ob.name]
startList = []
blinkList = []
endList = []
cnt = 0

# Set a random frame for a blink
for b in boneList:

    r = 15.0

    while r > 13 and r < 17:

        r = int(300.0 * random.random())

    startList.append(r)
    blinkList.append(r+1)
    endList.append(r+2)

    if r < 30:
        cnt += 1

print('cnt',cnt)

#print(boneList)

# Loop through all the frames
for f in range(31):

    fr = f+1

    scene.frame_set(fr)

    for i, b in enumerate(boneList):

        if startList[i] == fr:

            b[0].rotation_euler = [math.radians(-15.0), 0.0, 0.0]   

            b[0].keyframe_insert('rotation_euler')

            print('start blink',i)

        elif blinkList[i] == fr:

            b[0].rotation_euler = [math.radians(-0.0), 0.0, 0.0]   

            b[0].keyframe_insert('rotation_euler')

            b[1] = 3
            boneList[i][1] = 3

            print('blink',i)

        if endList[i] == fr:

            b[0].rotation_euler = [math.radians(-15.0), 0.0, 0.0]   

            b[0].keyframe_insert('rotation_euler')

            b[1] = 0
            boneList[i][1] = 0

            print('end blink', i)

    print('-- set',f)

print('-- finished --')

Offset NLA strips relative to lots of position calculations

  1. import bpy
  2.  
  3. offs = [ob.matrix_world.translation_part() for ob in bpy.data.groups['offsets'].objects]
  4. ofDist = 10
  5.  
  6. cLoc = bpy.data.objects['Camera'].matrix_world.translation_part()
  7.  
  8. minDist = 'x'
  9. maxDist = 'x'
  10.  
  11. start = 11
  12. end = 511
  13.  
  14. for ob in bpy.data.groups['localtargets'].objects:
  15.  
  16.     t = ob.animation_data.nla_tracks['NlaTrack']
  17.  
  18.     s = t.strips[0]
  19.  
  20.     offset = 0
  21.  
  22.     obLoc = ob.matrix_world.translation_part()
  23.  
  24.     cDist = obLoc - cLoc
  25.     cDist = cDist.length
  26.  
  27.     offset -= (cDist * 0.4)
  28.  
  29.     if minDist == 'x' or cDist < minDist:         minDist = cDist     if maxDist == 'x' or cDist > maxDist:
  30.         maxDist = cDist
  31.  
  32.     for i, of in enumerate(offs):
  33.  
  34.         cDist = obLoc - of
  35.         cDist = cDist.length
  36.  
  37.         if cDist < ofDist:
  38.  
  39.             # Use modulo to set a nice maximum deviation
  40.             if (i%2) == 1:
  41.                 if (i%3) == 1:
  42.                     max = -5
  43.                 else:
  44.                     max = -10
  45.             else:
  46.                 if(i%3) == 1:
  47.                     max = -15
  48.                 else:
  49.                     max = - 20
  50.  
  51.             cFact = (1.0/ofDist) * (ofDist - cDist)
  52.  
  53.             offset = offset + (max * cFact)
  54.  
  55.     s.action_frame_start = start + offset
  56.     s.action_frame_end = end + offset
  57.  
  58.     print('set', ob.name)
  59.  
  60. print('minDist',minDist)
  61. print('maxDist',maxDist)
  62. print('-- finished --')

Display clean python code for copying

import bpy

offs = [ob.matrix_world.translation_part() for ob in bpy.data.groups['offsets'].objects]
ofDist = 10

cLoc = bpy.data.objects['Camera'].matrix_world.translation_part()

minDist = 'x'
maxDist = 'x'

start = 11
end = 511

for ob in bpy.data.groups['localtargets'].objects:

    t = ob.animation_data.nla_tracks['NlaTrack']

    s = t.strips[0]

    offset = 0

    obLoc = ob.matrix_world.translation_part()

    cDist = obLoc - cLoc
    cDist = cDist.length

    offset -= (cDist * 0.4)

    if minDist == 'x' or cDist < minDist:         minDist = cDist     if maxDist == 'x' or cDist > maxDist:
        maxDist = cDist

    for i, of in enumerate(offs):

        cDist = obLoc - of
        cDist = cDist.length

        if cDist < ofDist:

            # Use modulo to set a nice maximum deviation
            if (i%2) == 1:
                if (i%3) == 1:
                    max = -5
                else:
                    max = -10
            else:
                if(i%3) == 1:
                    max = -15
                else:
                    max = - 20

            cFact = (1.0/ofDist) * (ofDist - cDist)

            offset = offset + (max * cFact)

    s.action_frame_start = start + offset
    s.action_frame_end = end + offset

    print('set', ob.name)

print('minDist',minDist)
print('maxDist',maxDist)
print('-- finished --')

Messing with Constraints

  1. import bpy, math
  2.  
  3. grp = bpy.data.groups['controllers']
  4.  
  5. ctrl = [ob for ob in grp.objects]
  6.  
  7. cam = bpy.data.objects['Camera']
  8. camLoc = cam.matrix_world.translation_part()
  9.  
  10. minDist = 'x'
  11. maxDist = 'x'
  12.  
  13. minF = 10
  14.  
  15. print(len(ctrl))
  16.  
  17. for ob in ctrl:
  18.  
  19.     # Get locations and rotations to use
  20.     obLoc = ob.matrix_world.translation_part()
  21.     pRot = math.degrees(ob.parent.rotation_euler[1])
  22.  
  23.     # Calculate the distance to the camera
  24.     obDist = obLoc - camLoc
  25.     obDist = obDist.length
  26.  
  27.     # Set the minimum and maximum distance from the camera
  28.     if minDist == 'x' or obDist < minDist:         minDist = obDist     if maxDist == 'x' or obDist > maxDist:
  29.         maxDist = obDist
  30.  
  31.     print('setting',ob.name)
  32.  
  33.     # Add new constraints
  34.     ob.constraints.new('TRACK_TO')
  35.     ob.constraints.new('TRANSFORM')
  36.     ob.constraints.new('LOCKED_TRACK')
  37.     ob.constraints.new('LOCKED_TRACK')
  38.  
  39.     # Retrieve the constraints
  40.     tr = ob.constraints[2] # track to
  41.     tf = ob.constraints[3] # transform
  42.     tr2 = ob.constraints[4] # track to
  43.     tr3 = ob.constraints[5] # locked track x
  44.     tr4 = ob.constraints[6] # locked track z
  45.  
  46.     ob.constraints.remove(tr2)
  47.  
  48.     # Use the rotation of the parent to set the influence of the camera position to world or local
  49.     if pRot > 360:
  50.         pRot = pRot - 360
  51.  
  52.     if pRot > 270:
  53.         inf = (1.0/90.0) * (90 - (360 - pRot))
  54.     elif pRot < 90:
  55.         inf = (1.0/90.0) * (90 - pRot)
  56.     else:
  57.         inf = 0.0
  58.  
  59.     # Set the influence of the constraints
  60.     tr.influence = 1.0
  61.     tf.influence = 1.0
  62.     tr2.influence = inf
  63.     tr3.influence = 0.0
  64.     tr4.influence = 0.0
  65.  
  66.     # Insert a keyframe
  67.     tr.keyframe_insert('influence')
  68.     tf.keyframe_insert('influence')
  69.     tr2.keyframe_insert('influence')
  70.     tr3.keyframe_insert('influence')
  71.     tr4.keyframe_insert('influence')
  72.  
  73.     # Set the track axis
  74.     tr.track_axis = 'TRACK_NEGATIVE_Y'
  75.     tr2.track_axis = 'TRACK_NEGATIVE_Y'
  76.     tr3.track_axis = 'TRACK_NEGATIVE_Y'
  77.     tr3.lock_axis = 'LOCK_X'
  78.     tr4.track_axis = 'TRACK_NEGATIVE_Y'
  79.     tr4.lock_axis = 'LOCK_Z'
  80.  
  81.     # Set the target for the constraint
  82.     tr.target = cam
  83.     tf.target = cam
  84.     tr2.target = cam
  85.     tr3.target = cam
  86.     tr4.target = cam
  87.  
  88.     # Set values for the transform constraint
  89.     tf.from_min_x = -50.0
  90.     tf.from_min_y = -50.0
  91.     tf.from_min_z = -50.0
  92.  
  93.     tf.from_max_x = 50.0
  94.     tf.from_max_y = 50.0
  95.     tf.from_max_z = 50.0
  96.  
  97.     tf.to_min_y = -0.5
  98.     tf.to_max_y = -0.5
  99.  
  100.     if obDist < 11.0:
  101.  
  102.         obDist = obDist - 6
  103.  
  104.         n = obDist * 0.20
  105.         n = -n
  106.  
  107.         if n < -1.0:             n = -1.0         elif n > 0.0:
  108.             n = 0.0
  109.  
  110.     else:
  111.         n = -1.0
  112.  
  113.     tf.to_min_z = n
  114.     tf.to_max_z = n
  115.  
  116.     tr.owner_space = 'LOCAL'
  117.     tf.owner_space = 'LOCAL'
  118.  
  119. print('minDist',minDist)
  120. print('maxDist',maxDist)
  121.  
  122. print('--finished--')

Display clean python code for copying

import bpy, math

grp = bpy.data.groups['controllers']

ctrl = [ob for ob in grp.objects]

cam = bpy.data.objects['Camera']
camLoc = cam.matrix_world.translation_part()

minDist = 'x'
maxDist = 'x'

minF = 10

print(len(ctrl))

for ob in ctrl:

    # Get locations and rotations to use
    obLoc = ob.matrix_world.translation_part()
    pRot = math.degrees(ob.parent.rotation_euler[1])

    # Calculate the distance to the camera
    obDist = obLoc - camLoc
    obDist = obDist.length

    # Set the minimum and maximum distance from the camera
    if minDist == 'x' or obDist < minDist:         minDist = obDist     if maxDist == 'x' or obDist > maxDist:
        maxDist = obDist

    print('setting',ob.name)

    # Add new constraints
    ob.constraints.new('TRACK_TO')
    ob.constraints.new('TRANSFORM')
    ob.constraints.new('LOCKED_TRACK')
    ob.constraints.new('LOCKED_TRACK')

    # Retrieve the constraints
    tr = ob.constraints[2] # track to
    tf = ob.constraints[3] # transform
    tr2 = ob.constraints[4] # track to
    tr3 = ob.constraints[5] # locked track x
    tr4 = ob.constraints[6] # locked track z

    ob.constraints.remove(tr2)

    # Use the rotation of the parent to set the influence of the camera position to world or local
    if pRot > 360:
        pRot = pRot - 360

    if pRot > 270:
        inf = (1.0/90.0) * (90 - (360 - pRot))
    elif pRot < 90:
        inf = (1.0/90.0) * (90 - pRot)
    else:
        inf = 0.0

    # Set the influence of the constraints
    tr.influence = 1.0
    tf.influence = 1.0
    tr2.influence = inf
    tr3.influence = 0.0
    tr4.influence = 0.0

    # Insert a keyframe
    tr.keyframe_insert('influence')
    tf.keyframe_insert('influence')
    tr2.keyframe_insert('influence')
    tr3.keyframe_insert('influence')
    tr4.keyframe_insert('influence')

    # Set the track axis
    tr.track_axis = 'TRACK_NEGATIVE_Y'
    tr2.track_axis = 'TRACK_NEGATIVE_Y'
    tr3.track_axis = 'TRACK_NEGATIVE_Y'
    tr3.lock_axis = 'LOCK_X'
    tr4.track_axis = 'TRACK_NEGATIVE_Y'
    tr4.lock_axis = 'LOCK_Z'

    # Set the target for the constraint
    tr.target = cam
    tf.target = cam
    tr2.target = cam
    tr3.target = cam
    tr4.target = cam

    # Set values for the transform constraint
    tf.from_min_x = -50.0
    tf.from_min_y = -50.0
    tf.from_min_z = -50.0

    tf.from_max_x = 50.0
    tf.from_max_y = 50.0
    tf.from_max_z = 50.0

    tf.to_min_y = -0.5
    tf.to_max_y = -0.5

    if obDist < 11.0:

        obDist = obDist - 6

        n = obDist * 0.20
        n = -n

        if n < -1.0:             n = -1.0         elif n > 0.0:
            n = 0.0

    else:
        n = -1.0

    tf.to_min_z = n
    tf.to_max_z = n

    tr.owner_space = 'LOCAL'
    tf.owner_space = 'LOCAL'

print('minDist',minDist)
print('maxDist',maxDist)

print('--finished--')

More progress = more problems

Wednesday, September 15th, 2010

Downloadable Files

It’s been a few days since I last posted a progress report on this project. Let me start by my findings about lenticular.

1. they loop!

Yes… apparently it’s something we have to live with, lenticulars loop. They have a limited viewing angle somewhere between 20 and 50 degrees. Thus if you have one that has a viewing angle of 20 degrees, and you look at it from 21 degrees, it’s pretty much the same as looking at it from 1 degree. Sort of… so… something we have to live with. Not perfect at all, but well… it’ll have to do.

2. too many pixels

Image sizes are just huuuge… say you want a nice 600DPI image so you can have 30 “tiles”, and it needs to be 10inches wide, or 25.4cm… that’s 6000px wide already! That’s big, especially since 10 inches is not nearly enough for what I have in mind… it’ll be more like 30 inches.

3. and then there’s the scene

You may have seen a little spherical character I’ve been working on for this project… well… there’s going to be lots of them. And they can’t be linked in dupligroups in Blender, cause we just don’t have enough controll.

This results in my Blender file having eh… well way too many objects. I managed to get the vert count just below 10.000.000 (it was 40 million before). So now I can render using “only” 3.2G of my computer’s ram. Nice, since it has 4G in total. I’m rather happy I bought a 64bit system, otherwise this would not have been possible at all.

Here’s a little preview of what I’m working on!

It needs work still, a lot of work, and animation, and lighting, and and… well… it’s progress

4. the python script

I made some changes to the python script to interlace the tiles. It now reads the current directory and only needs the image size set for the final result (will automaticly resize the tiles if nessecary). Thanks Beorn for the suggestion. (the script is at the bottom of this post).

Progressing, printing numbers and colours now

Friday, September 10th, 2010

Downloadable Files

A couple days ago I started development on Lenticular prints. I have to say it’s been going rather well, though it’s not exactly easy. You have to keep your wits about you with tile sizes, LPI, DPI and so forth. Here’s what I tried so far!

First prints

1. I started with a basic black and white image, alternating stripes. To see if it works at all.

2. That seemed to work so I tried a circle and a square

3. I found out that it’s nice to add a little bar on the left to line up the lenticular sheet. one weird thing is that from left to right the animation loops (so you can see the circle multiple times). It’s not just the left = circle and the right = square. So I decided to try something a bit more complex.

4. That was ABCDE, and logically the letters came past backwards, so I need to reverse the array with the images in it. I also tried the lentikit open source software, and found it does exactly what my script already does. And then Beorn stopped by and dedicated a fraction of his considerable brain power to think with me. It now seems obvious that you should not have the DPI of the printer as the variable, but in stead use the image size, and use multiple pixel strips per lense (5 px per tile for instance). So just now I did a rather crazy colourful test.

Because I’m now using 600DPI irrelevant of the number of tiles… the image got quite large… 3000 x 3000 px. This may be an issue later on. This is only a 12cm print… ish. What if I want a huge print at 1200DPI? That’ll be insane… though I think the PIL library can manage.

One odd thing I keep noticing is that the sequence does seem to loop when looking left to right… I need to check that, but I’m hoping to get a little advice.

In the meantime

Whilst I’m “waiting” for help I’ll start with the design/modelling of the image I have in mind. Here’s a first little sneak peak….

There’s going to be LOTS of these. Today I want to continue texturing, and hopefully rigging will follow tomorrow.

I’m still very excited about the project… there’s lots left to figure out!

You can download the current version of the script at the bottom of the article

Lenticular development started

Tuesday, September 7th, 2010

Downloadable Files

A few weeks ago I received a very interesting package in the mail… A couple of “lenticular lense sheets”. These should enable me to create prints that you can actually walk around and give you 3D vision even! It is not a new technology, but is has matured in recent years.

I found out about the technique a while ago already, but hadn’t really considered doing a lenticular project until Beorn Leonard showed me the website of Mark Ruff in Australia. Mark then was kind enough to refer me to Henri Clément in France who in turn gave me a link to Coen Holten who works just around the corner here in the Netherlands. Pixel (Coen’s company) also creates lenticular prints, and they provided me with a couple lenticular sheets to experiment with.

Getting informed

Knowing this isn’t really for the faint of heart I started reading… a LOT! Here’s a few of the pages I found.

Especially the last one was very informative.

Then I went to see about the software needed to create a lenticular ready image. And there’s quite a bit out there. Sadly… it seems like it’s either not very nice or somewhat too expensive, so I decided I might as well try on my own.

The basics

What it comes down to is this. You have a sheet with a number of stretched out lenses, the nr of lenses is measured in LPI (Lenses Per Inch). The ones I have are 20LPI and 40LPI. For each lense you want to have at least 1 pixel for every image (the more images the better, depending on the DPI or Dots Per Inch, your printer is capable of). So if I print at 600DPI for a 20LPI lense sheet I should be able to have 30 images combined in the final product.

First scripting experiment

So I set to creating a tiny script that can combine images in a way that would work. Nothing definite, but an experiment to see if I can get the sort of result I need. Here’s the three test images I made.

Then I want a script to take the first pixel from each, put them next to each other in a new image, then the second pixel from each, and so forth. Thus creating an “interlaced” composite image as seen below here.

This is not a perfect result to go straight to print, but if I wanted to print this for a 20LPI sheet, and set my printer to do it at 20 / 3 = 6.666666 DPI you should see a red, green or blue image depending on your viewing angle. Ehr, I think that’s correct, but I’ll start doing actual print experiments later today.

The script

The Python script I wrote is tiny and runs from the command-line. It requires Python 2.5 and the PIL (works with 1.1.7) image library. All the settings are entered manually in the script at this point, but it works. (You can download the script at the bottom of the article)

click here to close

Help keep these files free,
and support further development!