1
$\begingroup$

I have a custom file format that can contain (among other data) videos and still images, along with per-frame and global metadata (like e.g. the camera transformation matrix per frame or the camera FOV).

This custom file format is implemented behind a C API, which in turn is wrapped in a Python class.

Is there a way to implement a custom render output format, preferably using the python environment (in other words, are these formats available in the python environment somehow)?

The only other alternative I see is forking the Blender source code and implementing it straight in C++.


Note: I've already looked at using handlers (http://www.blender.org/api/blender_python_api_2_61_0/bpy.app.handlers.html) and that seems like a possible alternate course.

Unfortunately that requires an operator which I try to avoid, and I also prefer to see my file format next to the PNGs and AVI's in the output format list.

$\endgroup$
4
$\begingroup$

I don't think that's possible, without adding this to the source and compile it. As workaround you can try to create a custom render engine class and create property to add a path to your application like this:

enter image description here

The following script creates a discription scene file (text) for every frame you've selected in the scene and uses custom application to render it:

import bpy
import mathutils
from mathutils import Vector

# LOGIC
class CustomRenderEngine(bpy.types.RenderEngine):

    # These three members are used by blender to set up the
    # RenderEngine; define its internal name, visible name and capabilities.
    bl_idname = 'custom_renderer'
    bl_label = 'Application Name'
    bl_use_preview = True

    def formatNumbers(self, number):
        return '%0*d' % (4, number)

    def get_allObjectsInSceneByType(self, type_string):
        objects = []
        for objs in bpy.data.objects: # searches for type in the selection
            if (objs.type == type_string):
                if objs.hide == True:
                    pass
                else:
                    objects.append(objs)
        return objects

    # Return Object Transformations
    def get_objectLocation(self, object):
        return bpy.data.objects[object.name].location

    def get_objectRotation(self, object):
        return bpy.data.objects[object.name].rotation_euler

    def get_objectScale(self, object):
        return bpy.data.objects[object.name].scale

    # Return Geo Type
    def get_geoType(self, object):
        verticies = []
        for vertex in object.data.vertices:  
            verticies.append(vertex.co)
        if len(verticies) == 8:
            return "CUBE"
        else:
            return "SPHERE"

    # Return verticies of Object
    def get_objectVerts(self, object):
        verticies = []
        for vertex in object.data.vertices:  
            verticies.append(vertex.co)
            #print (vertex.co)
        return verticies

    # Return Geo Center 
    def get_GeometryCenter(self,obj):

        sumWCoord = [0,0,0]
        numbVert = 0
        if obj.type == 'MESH':
            for vert in obj.data.vertices:
                wmtx = obj.matrix_world
                worldCoord = vert.co * wmtx
                sumWCoord[0] += worldCoord[0]
                sumWCoord[1] += worldCoord[1]
                sumWCoord[2] += worldCoord[2]
                numbVert += 1
            sumWCoord[0] = sumWCoord[0]/numbVert
            sumWCoord[1] = sumWCoord[1]/numbVert
            sumWCoord[2] = sumWCoord[2]/numbVert
        return sumWCoord

    # Return max vert distance
    def max_vertDistance(self, ob):

        verts = ob.data.vertices
        centre = Vector([0,0,0])
        for vert in verts:
            centre += vert.co

        centre = centre / len(verts)
        distanceFromCentre = 0.0

        for vert in verts:
            vecFromCentre = vert.co - centre
            vertDistanceFromCentre = vecFromCentre.length
            if vertDistanceFromCentre > distanceFromCentre:
                distanceFromCentre = vertDistanceFromCentre
                vert1 = vert

        distance = 0.0
        for vert in verts:
            vec = vert.co - vert1.co
            vertsDistance = vec.length
            if vertsDistance > distance:
                distance = vertsDistance
                vert2 = vert

        #return vert1, vert2, distance
        print ("vert:", vert1.co)
        return vert1.co, vert2.co

    # Return max vert distance NEW
    def max_vertDistance2(self, ob):

        verts = ob.data.vertices
        centre = Vector([0,0,0])
        for vert in verts:
            centre += vert.co

        centre = centre / len(verts)
        distanceFromCentre = 0.0

        for vert in verts:
            vecFromCentre = vert.co - centre
            vertDistanceFromCentre = vecFromCentre.length
            if vertDistanceFromCentre > distanceFromCentre:
                distanceFromCentre = vertDistanceFromCentre
                vert1 = vert

        distance = 0.0
        for vert in verts:
            vec = vert.co - vert1.co
            vertsDistance = vec.length
            if vertsDistance > distance:
                distance = vertsDistance
                vert2 = vert

        twoVerts = [vert1, vert2]

        newTwo = []
        wm = ob.matrix_world
        for v in twoVerts:
            world = wm * v.co
            #print(world)
            newTwo.append(world)

        #return vert1.co, vert2.co
        return newTwo

    # Calculate Raduis of sphere
    def get_radiusSphere(self, obj):
        return (obj.dimensions[0] / 2)

    # Return Materials of Geo
    def get_material(self, object):

        mat_diff = bpy.data.objects[object.name].active_material.diffuse_color
        mat_diff_int = bpy.data.objects[object.name].active_material.diffuse_intensity
        mat_spec = bpy.data.objects[object.name].active_material.specular_color
        mat_spec_int = bpy.data.objects[object.name].active_material.specular_intensity
        mat_amb = bpy.data.worlds["World"].ambient_color
        mat_amb_int = bpy.data.worlds["World"].custom.world_amount

        return mat_diff, mat_diff_int, mat_spec, mat_spec_int, mat_amb_int

    def diff_dimensions(self, dimensions):
        dimList = [round(dimensions.x, 3), round(dimensions.y, 3), round(dimensions.z, 3)]
        return [i for i in dimList if dimList.count(i) > 1]

    # Return Material List
    def get_materialList(self):
        materialNames = []
        for material in bpy.data.materials:
            materialNames.append(material.name)
        return materialNames

    def app_materials(self):
        string = ""

        amb_r = bpy.context.scene.world.ambient_color.r
        amb_g = bpy.context.scene.world.ambient_color.g
        amb_b = bpy.context.scene.world.ambient_color.b
        ambient_int = bpy.context.scene.world.custom.world_amount        

        for i in bpy.data.materials:
            diff_r = bpy.data.materials[i.name].diffuse_color.r
            diff_g = bpy.data.materials[i.name].diffuse_color.g
            diff_b = bpy.data.materials[i.name].diffuse_color.b
            diff_int = bpy.data.materials[i.name].diffuse_intensity

            spec_r = bpy.data.materials[i.name].specular_color.r
            spec_b = bpy.data.materials[i.name].specular_color.b
            spec_g = bpy.data.materials[i.name].specular_color.g
            spec_int = bpy.data.materials[i.name].specular_intensity
            spec_hardness = bpy.data.materials[i.name].specular_hardness

            string += ("define material ") + i.name + " "

            # ambient
            string += str(amb_r * ambient_int)[:5] + " "
            string += str(amb_b * ambient_int)[:5] + " "
            string += str(amb_g * ambient_int)[:5] + " "

            # diffuse
            string += str(diff_r * diff_int)[:5] + " "
            string += str(diff_g * diff_int)[:5] + " "
            string += str(diff_b * diff_int)[:5] + " "

            # specular
            string += str(spec_r * spec_int)[:5] + " "
            string += str(spec_g * spec_int)[:5] + " "
            string += str(spec_b* spec_int)[:5] + " "
            string += str(spec_hardness)        

            string += "\n"

        return string

    # This is the only method called by blender, in this example
    # we use it to detect preview rendering and call the implementation
    # in another method.
    def render(self, scene):
        scale = scene.render.resolution_percentage / 100.0
        self.size_x = int(scene.render.resolution_x * scale)
        self.size_y = int(scene.render.resolution_y * scale)

        if scene.name == 'preview':
            self.render_preview(scene)
        else:
            self.render_scene(scene)

    # In this example, we fill the preview renders with a flat green color.
    def render_preview(self, scene):
        pixel_count = self.size_x * self.size_y

        # The framebuffer is defined as a list of pixels, each pixel
        # itself being a list of R,G,B,A values
        green_rect = [[0.0, 1.0, 0.0, 1.0]] * pixel_count

        # Here we write the pixel values to the RenderResult
        result = self.begin_result(0, 0, self.size_x, self.size_y)
        layer = result.layers[0]
        layer.rect = green_rect
        self.end_result(result)

    # In this example, we fill the full renders with a flat blue color.
    def render_scene(self, scene):
        pixel_count = self.size_x * self.size_y

        materialstring = self.app_materials()

        # collect geometry
        geo = self.get_allObjectsInSceneByType("MESH")
        geostring = ""
        transformstring = ""

        for item in geo:

            if (self.get_geoType(item) == "CUBE"):

                #print ("Location:", self.get_objectLocation(item))
                #print ("CENTER IS:", self.get_GeometryCenter(item))
                #print ("dimensions:", item.dimensions)
                #self.get_objectVerts(item)

                geostring += "define shape box "
                geostring += item.name + " "

                # first point
                geostring += str(self.max_vertDistance2(item)[0][0])[:5] + " "
                geostring += str(self.max_vertDistance2(item)[0][3])[:5] + " "
                geostring += str(self.max_vertDistance2(item)[0][4])[:5] + " "

                # second point
                geostring += str(self.max_vertDistance2(item)[1][0])[:5] + " "
                geostring += str(self.max_vertDistance2(item)[1][5])[:5] + " "
                geostring += str(self.max_vertDistance2(item)[1][6])[:5] + " "

                # material
                geostring += " " + bpy.data.objects[item.name].active_material.name
                geostring += "\n"

            else:

                geostring += "define shape sphere "               
                geostring += item.name + " "

                # center
                geostring += str(self.get_objectLocation(item)[0])[:5] + " "
                geostring += str(self.get_objectLocation(item)[2])[:5] + " "
                geostring += str(self.get_objectLocation(item)[1])[:5] + " "

                # radius
                geostring += str(self.get_radiusSphere(item))[:5]

                geostring += " " + bpy.data.objects[item.name].active_material.name
                geostring += "\n"

                # check for transformation
                dims = bpy.data.objects[item.name].dimensions                
                #print (round(dims.x, 3), round(dims.y, 3), round(dims.z, 3))
                #print (self.diff_dimensions(dims))

                if len(self.diff_dimensions(dims)) < 3:
                    #print ("Transformation found!", item.name)
                    scle = bpy.data.objects[item.name].scale
                    transformstring += "transform "
                    transformstring += item.name + " scale " + str(round(scle.x,3)) + " " + str(round(scle.z,3)) + " " + str(round(scle.y,3)) + "\n" 
                    transformstring += "transform " + item.name + " translate 0 0 0\n"      
                '''
                print (scene.custom.app_path)
                print (scene.custom.scene_name)
                print (bpy.data.worlds["World"].ambient_color)
                print (bpy.context.scene.world.ambient_color)
                print (self.get_materialList())
                '''

        # collect lamps
        lamps = self.get_allObjectsInSceneByType("LAMP")
        lampstring = ""

        amb_r = bpy.context.scene.world.ambient_color.r
        amb_g = bpy.context.scene.world.ambient_color.g
        amb_b = bpy.context.scene.world.ambient_color.b

        for i in lamps:
            lampstring += "define light " + i.name + " "

            # position
            lampstring += str(self.get_objectLocation(i)[0])[:5] + " "
            lampstring += str(self.get_objectLocation(i)[2])[:5] + " "
            lampstring += str(self.get_objectLocation(i)[1])[:5] + " "

            # light ambient
            lampstring += str(amb_r)[:5] + " "
            lampstring += str(amb_g)[:5] + " "
            lampstring += str(amb_b)[:5] + " "

            # light diffuse
            lamp_energy = bpy.data.lamps[i.name].energy
            lampstring += str(bpy.data.lamps[i.name].color[0] * lamp_energy)[:5] + " "
            lampstring += str(bpy.data.lamps[i.name].color[1] * lamp_energy)[:5] + " "
            lampstring += str(bpy.data.lamps[i.name].color[2] * lamp_energy)[:5] + " "

            lampstring += "\n"

            #print ("Lamp energy:", bpy.data.lamps[i.name].energy)
            #print ("Lamp color:", bpy.data.lamps[i.name].color)
            #print ("Lamp location:", self.get_objectLocation(i))

        # get active camera

        obj_camera = bpy.context.scene.camera
        cameraName = obj_camera.name
        camerastring = "define camera " + cameraName + " "
        camerastring += str(bpy.data.cameras[cameraName].lens)[:5] + " "
        #camerastring += "35" + " "
        camerastring += str(self.get_objectLocation(obj_camera)[0])[:5] + " "
        camerastring += str(self.get_objectLocation(obj_camera)[2])[:5] + " "
        camerastring += str(self.get_objectLocation(obj_camera)[1])[:5] + " "

        # calculate up and direction vector
        cam = bpy.data.objects[obj_camera.name]
        #up = cam.matrix_world * Vector((0.0, 1.0, 0.0))
        #up = cam.matrix_world.to_3x3() * Vector((0.0, 1.0, 0.0))

        up = cam.matrix_world.to_quaternion() * Vector((0.0, 1.0, 0.0))
        #up = Vector((0.0, 1.0, 0.0))
        print ("UPVECTOR:", up)

        #cam_direction = cam.matrix_world * Vector((0.0, 0.0, -1.0))
        #cam_direction = cam.matrix_world.to_3x3() * Vector((0.0, 0.0, -1.0))

        cam_direction = cam.matrix_world.to_quaternion() * Vector((0.0, -1.0, -1.0))

        #cam_direction = Vector((0.0, 0.0, -1.0))
        #cam_direction.normalize()
        #up.normalize()

        camerastring += str(cam_direction[0])[:5] + " "
        camerastring += str(cam_direction[1])[:5] + " "
        camerastring += str(cam_direction[2])[:5] + " "

        camerastring += str(up[0])[:5] + " "
        camerastring += str(up[1])[:5] + " "
        camerastring += str(up[2])[:5] + " "

        camerastring += "\n"

        #camerastring = "define camera auge 60.0 1.0 1.0 3 0 -0.1 -1.0 0 1 0\n"

        # background color

        bgColor = bpy.data.worlds["World"].ambient_color
        bgColorstring = "define background-color " + str(bgColor[0]) + " " + str(bgColor[1]) + " " + str(bgColor[2]) + "\n"


        # ------------------------------------------------------------------------
        # render settings
        # ------------------------------------------------------------------------

        renderer = bpy.path.abspath(scene.custom.app_path)
        frameNumber = bpy.data.scenes["Scene"].frame_current

        scenename = scene.custom.scene_name        
        scale = scene.render.resolution_percentage / 100.0
        size_x = int(scene.render.resolution_x * scale)
        size_y = int(scene.render.resolution_y * scale)

        if not scenename:
            scenename = "AppScene"

        outfile_ppm = bpy.data.scenes["Scene"].render.filepath + scenename + "_" + str(self.formatNumbers(frameNumber)) + ".ppm"
        outfile_custom = bpy.data.scenes["Scene"].render.filepath + scenename + "_" + str(self.formatNumbers(frameNumber)) + ".sdf"

        rendersettingstring = "render " + obj_camera.name + " "
        rendersettingstring += outfile_ppm + " "
        rendersettingstring += str(size_x) + " "
        rendersettingstring += str(size_y) + " "
        rendersettingstring += "\n"

        file_content = bgColorstring + materialstring + geostring + transformstring + lampstring + camerastring + rendersettingstring

        # write custom file
        f = open(outfile_custom,'w')
        f.write(file_content)
        f.close()

        import signal
        import subprocess, time

        # for single image
        single_frame = subprocess.call([renderer, outfile_sdf])

        # for animation
        '''
        app_animation = subprocess.Popen([renderer, outfile_sdf])
        print ('app_animation\'s pid = ',app_animation.pid)
        time.sleep(170)
        app_animation.terminate()
        '''         
        print (materialstring + geostring + lampstring + camerastring + rendersettingstring)

        # The framebuffer is defined as a list of pixels, each pixel
        # itself being a list of R,G,B,A values
        blue_rect = [[0.0, 0.0, 1.0, 1.0]] * pixel_count

        # Here we write the pixel values to the RenderResult
        result = self.begin_result(0, 0, self.size_x, self.size_y)
        layer = result.layers[0]
        layer.rect = blue_rect
        self.end_result(result)


# Register the RenderEngine
bpy.utils.register_class(CustomRenderEngine)

# ======================================================================================
# UI
# ======================================================================================

from bpy.types import (AddonPreferences,
                       PropertyGroup,
                       Operator,
                       )
from bpy.props import (StringProperty,
                       BoolProperty,
                       IntProperty,
                       FloatProperty,
                       FloatVectorProperty,
                       EnumProperty,
                       PointerProperty,
                       )

# -----------------------------------------------------------------------------------
# Render properties
# example: https://github.com/meta-androcto/blenderpython/blob/master/scripts/archive/game_gamekit/ui.py
# -----------------------------------------------------------------------------------

from bl_ui import properties_render
properties_render.RENDER_PT_render.COMPAT_ENGINES.add('custom_renderer')
properties_render.RENDER_PT_dimensions.COMPAT_ENGINES.add('custom_renderer')
#properties_render.RENDER_PT_antialiasing.COMPAT_ENGINES.add('custom_renderer')
#properties_render.RENDER_PT_shading.COMPAT_ENGINES.add('custom_renderer')
properties_render.RENDER_PT_output.COMPAT_ENGINES.add('custom_renderer')
del properties_render

class RenderButtonsPanel():
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "render"
    # COMPAT_ENGINES must be defined in each subclass, external engines can add themselves here

    @classmethod
    def poll(cls, context):
        rd = context.scene.render
        return (rd.use_game_engine is False) and (rd.engine in cls.COMPAT_ENGINES)

class RenderCustomSettingsScene(PropertyGroup):

    deletefiles_enable = BoolProperty(
            name="Delete files",
            description="Delete files after rendering.",
            default=True)
    scene_name = StringProperty(
            name="Scene Name",
            description="Name of sdf scene to create.",
            maxlen=1024)
    app_path = StringProperty(
            name="Export scene path",
            # description="Path to directory where the exported scene is created"
            description="Path to Custom Engine",
            maxlen=1024, subtype="FILE_PATH")

class RENDER_PT_custom_export_settings(RenderButtonsPanel, bpy.types.Panel):
    bl_label = "Render Connection"
    COMPAT_ENGINES = {'custom_renderer'}

    def draw_header(self, context):
        self.layout.label(icon='CONSOLE')
    def draw(self, context):
        layout = self.layout
        scene = context.scene

        row = layout.row()
        row.column().prop(scene.custom, "app_path", text="Path to App")
        row = layout.row()
        row.column().prop(scene.custom, "scene_name", text="Scene Name")
        #row = layout.row()
        #row.column().prop(scene.custom, "deletefiles_enable", text="Delete files after Rendering")

# -----------------------------------------------------------------------------------
# Material properties
# -----------------------------------------------------------------------------------

from bl_ui import properties_material
properties_material.MATERIAL_PT_context_material.COMPAT_ENGINES.add('custom_renderer')
properties_material.MATERIAL_PT_diffuse.COMPAT_ENGINES.add('custom_renderer')
#properties_material.MATERIAL_PT_preview.COMPAT_ENGINES.add('custom_renderer')
#properties_material.MATERIAL_PT_shading.COMPAT_ENGINES.add('custom_renderer')
#properties_material.MATERIAL_PT_shadow.COMPAT_ENGINES.add('custom_renderer')
properties_material.MATERIAL_PT_specular.COMPAT_ENGINES.add('custom_renderer')
#properties_material.MATERIAL_PT_transp.COMPAT_ENGINES.add('custom_renderer')
del properties_material

class MaterialButtonsPanel():
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "material"
    # COMPAT_ENGINES must be defined in each subclass, external engines can add themselves here

    @classmethod
    def poll(cls, context):
        mat = context.material
        rd = context.scene.render
        return mat and (rd.use_game_engine is False) and (rd.engine in cls.COMPAT_ENGINES)

class RenderCustomSettingsMaterial(PropertyGroup):

    test_enable = BoolProperty(
        name="Enable",
        description="test enabler",
        default=False)

    test_amount = FloatProperty(
        name="amount",
        description="test",
        min=0.0, max=1.0, soft_min=0.01, soft_max=1.0, default=0.25)

# -----------------------------------------------------------------------------------
# Camera properties
# -----------------------------------------------------------------------------------

from bl_ui import properties_data_camera
properties_data_camera.DATA_PT_lens.COMPAT_ENGINES.add('custom_renderer')
del properties_data_camera

# -----------------------------------------------------------------------------------
# Lamp properties
# -----------------------------------------------------------------------------------

from bl_ui import properties_data_lamp
properties_data_lamp.DATA_PT_lamp.COMPAT_ENGINES.add('custom_renderer')
properties_data_lamp.DATA_PT_preview.COMPAT_ENGINES.add('custom_renderer')
del properties_data_lamp

# -----------------------------------------------------------------------------------
# World properties
# -----------------------------------------------------------------------------------

from bl_ui import properties_world
class WorldButtonsPanel():
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "world"
    # COMPAT_ENGINES must be defined in each subclass, external engines can add themselves here

    @classmethod
    def poll(cls, context):
        wld = context.world
        rd = context.scene.render
        return wld and (rd.use_game_engine is False) and (rd.engine in cls.COMPAT_ENGINES)

class RenderCustomSettingsWorld(PropertyGroup):        

    world_amount = FloatProperty(
        name="amount",
        description="test",
        min=0.0, max=1.0, soft_min=0.01, soft_max=1.0, default=0.25)

class WORLD_PT_world(WorldButtonsPanel, bpy.types.Panel):
    bl_label = "World"
    COMPAT_ENGINES = {'custom_renderer'}

    def draw(self, context):
        layout = self.layout
        wld = context.world

        row = layout.row()
        row.column().prop(wld, "ambient_color")
        #row = layout.row()
        #row.column().prop(wld, "world_amount")
        row = layout.row()
        row.column().prop(wld.custom, "world_amount", slider=True)

#bpy.utils.register_class(WORLD_PT_world)


# ======================================================================================
# Register
# ======================================================================================

def register():
    bpy.utils.register_module(__name__)
    bpy.types.Scene.custom = PointerProperty(type=RenderCustomSettingsScene)
    #bpy.types.Material.custom = PointerProperty(type=RenderCustomSettingsMaterial)
    bpy.types.World.custom = PointerProperty(type=RenderCustomSettingsWorld)

def unregister():
    bpy.utils.unregister_module(__name__)
    del bpy.types.Scene.custom
    del bpy.types.Material.custom
    del bpy.types.World.custom
    del bpy.types.Scene.custom

if __name__ == "__main__":
    register()

Note: The script was written a while ago, it is a bit messy but I hope you get the idea.


For a simpler approach you can also extend the render panel. Best practice is to take a look into the templates that are shipped with blender:

enter image description here

To define your custom file output you can create a custom file property like this:

app_path = StringProperty(
            name="Export files to:",
            description=""
            description="Path",
            maxlen=1024, subtype="DIR_PATH")

or use scn.render.filepath. For this case here you can find a working example: Is it possible to render only keyframes from Dope-sheet?

$\endgroup$
  • $\begingroup$ Thanks for the in-depth response! I've started using a custom renderer, but unfortunately had to abandon it, because I need Cycles for the OSL shaders that are outputting most of my images. Is it easy (or even possible) to have a custom renderer inherit from the Cycles renderer, and only override the file output? $\endgroup$ – Sektor Jan 20 '15 at 14:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.