Blender Renderfarm unter Linux aufsetzen

RenderbeispielBeim rumexperimentieren mit Blender habe ich nach einer Möglichkeit gesucht, um den Rendervorgang ein wenig auszulagern. Ich wurde auch schnell fündig, leider ist das gefundene Script von ideasman nur für einzelne Frames zu gebrauchen. Also habe ich ein wenig Hand angelegt und die fehlende Funktionalität, ganze Animationen zu berechnen, mit ein paar Zeilen eingebaut.

Beim Testen sind mir keine Fehler aufgefallen. Falls doch jemand etwas hat: Ab in die Kommentare!

Genug Vorgeschichte. Wie bringt man Blender zum verteilten Rendern?

Benötigte Programme

1. Blender mit einem Pythonscript füttern

Das Script unterteilt jedes Bild in 256×256 Pixel große Quadrate, die einzeln berechnet und gespeichert werden. Bereits begonnene oder fertiggestellte Teile werden von den anderen Clients ignoriert.

Die Datei render.py im Projektverzeichnis ablegen:

import Blender
from Blender import sys, Scene, Noise
from Blender.Scene import Render
import string

NUM = '0123456789'
DEFAULT_TILE_SIZE=256 # px

# Clamp
def max(number):
   if number > 1.0:
      number = 1.0
   return number

def isfile(file):
   try:
      open(file)
      return 1
   except:
      return 0

def randList(list):
   randList = []
   lenList = len(list)
   while lenList != len(randList):
      randIndex = int( Noise.random() * len(list)  )
      randList.append( list[randIndex] )
      list.remove( list[randIndex] )

   return randList

# Strip all chars after a '.'
def stripExt(text):
   print 'filename ', text
   return text[:text.index('.')]

def getParts():

   name = Blender.Get('filename')
   name = stripExt(name)

   tileSize = DEFAULT_TILE_SIZE

   # OK either way we have a tile size.
   # Get the data
   scn = Scene.GetCurrent()
   context = scn.getRenderingContext()

   xPix = context.imageSizeX()
   yPix = context.imageSizeY()

   xParts = int(xPix / tileSize)
   yParts = int(yPix / tileSize)

   # Incase the tile size is larger then the render size.
   if xParts < 2:
      xParts = 2
   if yParts < 2:
      yParts = 2   

   return xParts, yParts

# Makes a list of rects that setBorder will pass.
def makeBorderList(xparts, yparts):

   borderRectList=[] #We store all the rects here and then return them.

   xlen = 1.0 / xparts
   ylen = 1.0 / yparts
   xPoint = 0.0 # This stores the current X value, and incriments xlen each iteration until its equel to 1.0
   yPoint = 0.0

   counter = 1 # Inde each border

   while xPoint < 0.999:
      while yPoint < 0.999:

         # Write the rect to the list
         borderRectList.append( (counter, max(xPoint), max(yPoint), max(xPoint+xlen), max(yPoint+ylen)) )
         counter += 1 # Keep a tag of which index this one is.

         yPoint += ylen
      # Reset yPoint for the next colum.
      yPoint = 0.0

      xPoint += xlen
   return borderRectList

# SETS UP DEFAULTS NEEDED FOR OUTPUTTING AN IMAGE THAT CAN BE COMPOSITED.
scn = Scene.GetCurrent()
context = scn.getRenderingContext()
context.enableBorderRender(1)
context.enableRGBAColor() # Save RGBA
context.setImageType(Render.PNG) # Save RGBA
context.enableExtensions(1)

# Make image name
imageName = Blender.Get('filename')

# Remove .blend
imageName = stripExt(imageName)
renderName = imageName + '_' # frameNum.png will be added.

# Set the start and end frame to the current frame.
staFrame = Blender.Get( 'staframe')
endFrame = Blender.Get( 'endframe')
# Keep track of frames rendered, only for a report.

Blender.Set( 'curframe', 1)
xParts, yParts = getParts()
randBorderList = randList( makeBorderList(xParts,yParts) )

print '= staframe: ', staFrame
print '= endframe: ', endFrame
print '=== frames: ', endFrame-staFrame+1
for curFrame in range(staFrame,endFrame):
	context.startFrame(curFrame)
	context.endFrame(curFrame)
	for border in randBorderList:

	   # Set the new file name WITH X/Y parts
	   # blendfilename_partnum_framenum.ext
	   # eg. render_01_0001.png
	   partNum = str(border[0])
	   while len(partNum) < 4:
	      partNum = '0' + partNum

	   # CREATE THE REAL NAME OF THE OUTPUT FILE
	   frameNum = str(curFrame)
	   while len(frameNum) < 4:
	      frameNum = '0' + frameNum

	   uniqueRenderName = renderName + partNum + '_'
	   fileToRender = uniqueRenderName + frameNum + '.png'

	   print '= frame: ', curFrame
	   print '== part: ', partNum

	   # Check that the file isnt alredy there
	   if isfile(fileToRender) == 0:

	      # TOUCH FILE SO NOBODY OVERWRITES IT.
	      # Create a dummy file so no other nodes try to render the image.
	      file = open(fileToRender,"w")
	      file.close()

	      # SET RENDER NAME AND PATH.
	      context.setRenderPath('//' + uniqueRenderName) # // is the currentdir

	      # Set border
	      context.setBorder(border[1], border[2], border[3], border[4] ) 

	      # RENDER THE IMAGE
	      context.renderAnim() # This saves the pics.
	   else:
	      print '= was already rendered'

# Quit
Blender.Quit()

2. Projektordner freigeben

Man muss dafür sorgen, dass auf allen Rechnern die mithelfen sollen das Projektverzeichnis eingebunden ist. Hierzu bieten sich Samba und nfs an. Das folgende Beispiel nutzt nfs.

Auf dem Hauptsystem kann man die nfs-Freigabe in der Datei /etc/exports einstellen:

/home/eisfrei/blender/projekt 192.168.0.1/24(rw,anonuid=1000,anongid=100,all_squash,sync)

Sämtliche Renderknechte binden nun die Freigabe ein:

mount.nfs 192.168.0.23:/home/eisfrei/blender/projekt /mnt/

3. Rendern

Auf allen Systemen die mitwerkeln sollen wechselt man in das Projektverzeichnis und startet das Rendern mit:
blender -b projekt.blend -P render.py -a

Jetzt erst mal eine Runde hinlegen und abwarten, während eine Unmenge von Pngs erzeugt wird.

4. Einzelteile zusammenkitten

Zum verkleben der Bildbestandteile benutze ich eine Kombination aus Perl und ImageMagick. Das Perlscript sucht die Bestandteile zusammen und übergibt diese an ImageMagick, welches diese dann alle schön zusammenleimt.

merge.pl

#!/usr/bin/perl
use strict;
my $fps=25;
my $project=shift;
my @files;
print $project,"\n";
opendir(DIR, ".");
@files = grep(/$project[_][0-9]+_[0-9]+\.png$/,readdir(DIR));
closedir(DIR);

my %frames;
my %parts;

foreach my $file (@files)
{
	print "pruefe ",$file,"\n";
	if ($file=~/$project[_]([0-9]+)_([0-9]+)\.png$/)
	{
		$frames{$2}=1;
		$parts{$1}=1;
	}
}

foreach my $fr (sort(keys(%frames)))
{
	my $cmd='convert ';
	foreach my $pa (keys(%parts))
	{
		$cmd.=$project.'_'.$pa.'_'.$fr.'.png ';
	}
	$cmd.=' -flatten '.$project.'_fr_'.$fr.'.jpg';
	print $cmd,"\n";
	`$cmd`;
}

Das Perlscript ruft man mit dem angehängten Projektnamen auf:

./merge.pl projekt

5. Aus den Frames ein Video erstellen

Um den einzelnen Frames zu Bewegung zu verhelfen benutze ich mencoder. Es gibt aber hunderte von Möglichkeiten, dies zu realisieren.

mencoder "mf://projekt_fr_*.jpg" -mf fps=25 -o output.avi -ovc lavc -lavcopts vcodec=mpeg4

Für die Ungeduldigen …

habe ich alles in einer Datei zusammengefasst und auch noch eine Szene zum ausprobieren beigelegt: renderfarm.tgz

Die Datei muss nur noch entpackt werden und dann dies ausgeführt werden:

blender -b projekt.blend -P render.py -a
./merge.pl projekt
mencoder "mf://projekt_fr_*.jpg" -mf fps=25 -o output.avi -ovc lavc -lavcopts vcodec=mpeg4

Einschränkungen

Max. 9999 Frames – Diese Einschränkung lässt sich aber relativ einfach beseitigen, indem man

while len(frameNum) < 4:

auf einen höheren Wert setzt. Dies führt allerdings zu leeren Lockfiles.

4 Anmerkung zu “Blender Renderfarm unter Linux aufsetzen

  1. Pingback: Blender Cluster « bytesnake

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert