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$

1 Answer 1

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$
1
  • $\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, 2015 at 14:11

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .