2
$\begingroup$

I've been attempting to make a custom render engine in Blender Python. From all the sample code that I gathered, I managed to put together a basic render engine, of which the result is shown below. All it does for now is render all the mesh objects in the current scene in their corresponding diffuse color, in respect to the viewport camera.

However, I noticed that when enabling the render engine, several panels appear differently in the UI. The materials panel in particular, which appears almost completely gone:

Eevee (left), Custom (right)

For the render engine, I have to support UV mapping which means I need the materials panel.

How can I keep the default panels? Will I have to code my own?


Source code:

import bpy
import bgl
import gpu
from gpu_extras.batch import batch_for_shader
from mathutils import * 


class DrawData:
    def __init__(self, ob):
        mesh = ob.data
        mat = ob.matrix_world
        
        diffuse_color = mesh.materials[0].diffuse_color
        verts = [mat @ v.co for v in mesh.vertices]
        ind = []

        for poly in mesh.polygons:
            ind.append([poly.vertices[i] for i in [0, 1, 2]])
            ind.append([poly.vertices[i] for i in [2, 3, 0]])
        
        self.color = diffuse_color
        self.verts = verts
        self.ind = ind

    def __del__(self):
        del self.color
        del self.verts
        del self.ind

    def draw(self):
        shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
        shader.bind()
        shader.uniform_float("color", self.color)
        
        batch = batch_for_shader(shader, "TRIS", {"pos": self.verts}, indices=self.ind)
        batch.draw(shader)


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 = "custom3d"
    bl_label = "Custom 3D"
    bl_use_preview = True

    # Init is called whenever a new render engine instance is created. Multiple
    # instances may exist at the same time, for example for a viewport and final
    # render.
    def __init__(self):
        self.scene_data = None
        print("RenderEngine created")

    # When the render engine instance is destroy, this is called. Clean up any
    # render engine data here, for example stopping running render threads.
    def __del__(self):
        del self.scene_data
        print("RenderEngine deleted")

    # This is the method called by Blender for both final renders (F12) and
    # small preview for materials, world and lights.
    def render(self, depsgraph):
        scene = depsgraph.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)

        # Fill the render result with a flat color. The framebuffer is
        # defined as a list of pixels, each pixel itself being a list of
        # R,G,B,A values.
        if self.is_preview:
            color = [0.1, 0.2, 0.1, 1.0]
        else:
            color = [0.2, 0.1, 0.1, 1.0]

        pixel_count = self.size_x * self.size_y
        rect = [color] * 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].passes["Combined"]
        layer.rect = rect
        self.end_result(result)

    # For viewport renders, this method gets called once at the start and
    # whenever the scene or 3D viewport changes. This method is where data
    # should be read from Blender in the same thread. Typically a render
    # thread will be started to do the work while keeping Blender responsive.
    def view_update(self, context, depsgraph):
        print("RenderEngine.view_update called")
        
        region = context.region
        view3d = context.space_data
        scene = depsgraph.scene

        # Get viewport dimensions
        dimensions = region.width, region.height

        if not self.scene_data:
            # First time initialization
            self.scene_data = {} # Dictionary
            first_time = True

            # Loop over all datablocks used in the scene.
            for datablock in depsgraph.ids:
                pass
        else:
            first_time = False

            # Test which datablocks changed
            for update in depsgraph.updates:
                print("Datablock updated: ", update.id.name)

            # Test if any material was added, removed or changed.
            if depsgraph.id_type_updated('MATERIAL'):
                print("Materials updated")

        # Loop over all object instances in the scene.
        if first_time or depsgraph.id_type_updated('OBJECT'):
            for instance in depsgraph.object_instances:
                ob = instance.object
                if ob.type == 'MESH':
                    print("\tname: %s \ttype: %s" % (ob.name, ob.type))
                    
                    self.scene_data[ob.name] = DrawData(ob)


    # For viewport renders, this method is called whenever Blender redraws
    # the 3D viewport. The renderer is expected to quickly draw the render
    # with OpenGL, and not perform other expensive work.
    # Blender will draw overlays for selection and editing on top of the
    # rendered image automatically.
    def view_draw(self, context, depsgraph):
        print("RenderEngine.view_draw called")

        view3d = context.space_data
        r3d = view3d.region_3d

        gpu.matrix.load_matrix(r3d.perspective_matrix)
        gpu.matrix.load_projection_matrix(Matrix.Identity(4))

        bgl.glClearColor(0.01,0.01,0.01,1)
        bgl.glClear(bgl.GL_COLOR_BUFFER_BIT | bgl.GL_DEPTH_BUFFER_BIT)
        bgl.glEnable(bgl.GL_DEPTH_TEST)
        
        for ob_name in self.scene_data:
            drawdata = self.scene_data[ob_name]
                 
            drawdata.draw()


# RenderEngines also need to tell UI Panels that they are compatible with.
# We recommend to enable all panels marked as BLENDER_RENDER, and then
# exclude any panels that are replaced by custom panels registered by the
# render engine, or that are not supported.
def get_panels():
    exclude_panels = {
        'VIEWLAYER_PT_filter',
        'VIEWLAYER_PT_layer_passes',
    }

    panels = []
    for panel in bpy.types.Panel.__subclasses__():
        if hasattr(panel, 'COMPAT_ENGINES') and 'BLENDER_RENDER' in panel.COMPAT_ENGINES:
            if panel.__name__ not in exclude_panels:
                panels.append(panel)

    return panels


def register():
    # Register the RenderEngine
    bpy.utils.register_class(CustomRenderEngine)

    for panel in get_panels():
        panel.COMPAT_ENGINES.add('custom3d')


def unregister():
    bpy.utils.unregister_class(CustomRenderEngine)

    for panel in get_panels():
        if 'custom3d' in panel.COMPAT_ENGINES:
            panel.COMPAT_ENGINES.remove('custom3d')


if __name__ == "__main__":
    register()
$\endgroup$

2 Answers 2

2
$\begingroup$

I assume you have enabled developer extras in PreferencesInterfaceDisplay. That done, you can sometimes look at the code to see why things are happening, by hovering over a field, right clicking, and selecting "Edit Source" from the context menu.

In this case, one clue can be found around line 1222 of ui.py where it uses variables like cycles.use_shading_nodes. That is, what it draws in the UI depends on what render engine you have set.

So the unfortunate answer to your question is that Blender makes UI decisions based on the render engine, and if you want to have it behave the same way for your engine as it does for cycles, you need to modify the Python UI code. The good news being that you should be able to get away with not having to modify the Blender C code.

I would suggest that you look at the source for a 3rd party Open Source render engine, like Lux Core Render to see how they handle such situations.

$\endgroup$
1
  • $\begingroup$ Thanks, this answer was most helpful. You definitely pointed me in the right direction with "Edit Source", which I had totally forgotten about. I will post an answer explaining what I did to solve it. $\endgroup$
    – Midas
    Mar 16, 2022 at 20:03
2
$\begingroup$

After reading Marty's answer and examining the source code of the built-in material panels, I realized that the problem actually lies in the code I posted above. In particular the line:

if hasattr(panel, 'COMPAT_ENGINES') and 'BLENDER_RENDER' in panel.COMPAT_ENGINES:

This basically means any panels that are not compatible with "BLENDER_RENDER" will be excluded, thus also excluding the Eevee panels.

To solve it, I changed the get_panels() method to also include any other panels that I want:

def get_panels():
    exclude_panels = {
        'VIEWLAYER_PT_filter',
        'VIEWLAYER_PT_layer_passes',
    }
    
    include_panels = {
        'EEVEE_MATERIAL_PT_context_material',
        'MATERIAL_PT_preview'
    }

    panels = []
    for panel in bpy.types.Panel.__subclasses__():
        if hasattr(panel, 'COMPAT_ENGINES'):
            if (('BLENDER_RENDER' in panel.COMPAT_ENGINES and panel.__name__ not in exclude_panels)
                or panel.__name__ in include_panels):
                panels.append(panel)

    return panels

In this case, I added the material slots and the material preview to the supported panels.

$\endgroup$

You must log in to answer this question.

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