Python API: add more detailed example for RenderEngine.
authorBrecht Van Lommel <brechtvanlommel@gmail.com>
Tue, 26 Mar 2019 18:48:16 +0000 (19:48 +0100)
committerBrecht Van Lommel <brechtvanlommel@gmail.com>
Tue, 26 Mar 2019 18:57:51 +0000 (19:57 +0100)
doc/python_api/examples/bpy.types.RenderEngine.py

index f7b2be4..1195204 100644 (file)
@@ -4,82 +4,219 @@ Simple Render Engine
 """
 
 import bpy
+import bgl
 
 
 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 = "Flat Color Renderer"
+    bl_idname = "CUSTOM"
+    bl_label = "Custom"
     bl_use_preview = True
 
-    # 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):
+    # 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
+        self.draw_data = None
+
+    # 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):
+        pass
+
+    # 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:
-            self.render_preview(scene)
+            color = [0.1, 0.2, 0.1, 1.0]
         else:
-            self.render_scene(scene)
+            color = [0.2, 0.1, 0.1, 1.0]
 
-    # 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
+        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 = 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
-
-        # 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].passes["Combined"]
-        layer.rect = blue_rect
+        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):
+        region = context.region
+        view3d = context.space_data
+        depsgraph = context.depsgraph
+        scene = depsgraph.scene
+
+        # Get viewport dimensions
+        dimensions = region.width, region.height
+
+        if not self.scene_data:
+            # First time initialization
+            self.scene_data = []
+            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_update('MATERIAL'):
+                print("Materials updated")
+
+        # Loop over all object instances in the scene.
+        if first_time or depsgraph.id_type_update('OBJECT'):
+            for instance in depsgraph.object_instances:
+                pass
+
+    # 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):
+        region = context.region
+        depsgraph = context.depsgraph
+        scene = depsgraph.scene
+
+        # Get viewport dimensions
+        dimensions = region.width, region.height
+
+        # Bind shader that converts from scene linear to display space,
+        bgl.glEnable(bgl.GL_BLEND)
+        bgl.glBlendFunc(bgl.GL_ONE, bgl.GL_ONE_MINUS_SRC_ALPHA);
+        self.bind_display_space_shader(scene)
+
+        if not self.draw_data or self.draw_data.dimensions != dimensions:
+            self.draw_data = CustomDrawData(dimensions)
+
+        self.draw_data.draw()
+
+        self.unbind_display_space_shader()
+        bgl.glDisable(bgl.GL_BLEND)
+
+
+class CustomDrawData:
+    def __init__(self, dimensions):
+        # Generate dummy float image buffer
+        self.dimensions = dimensions
+        width, height = dimensions
+
+        pixels = [0.1, 0.2, 0.1, 1.0] * width * height
+        pixels = bgl.Buffer(bgl.GL_FLOAT, width * height * 4, pixels)
+
+        # Generate texture
+        self.texture = bgl.Buffer(bgl.GL_INT, 1)
+        bgl.glGenTextures(1, self.texture)
+        bgl.glActiveTexture(bgl.GL_TEXTURE0)
+        bgl.glBindTexture(bgl.GL_TEXTURE_2D, self.texture[0])
+        bgl.glTexImage2D(bgl.GL_TEXTURE_2D, 0, bgl.GL_RGBA16F, width, height, 0, bgl.GL_RGBA, bgl.GL_FLOAT, pixels)
+        bgl.glTexParameteri(bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MIN_FILTER, bgl.GL_LINEAR)
+        bgl.glTexParameteri(bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MAG_FILTER, bgl.GL_LINEAR)
+        bgl.glBindTexture(bgl.GL_TEXTURE_2D, 0)
+
+        # Bind shader that converts from scene linear to display space,
+        # use the scene's color management settings.
+        shader_program = bgl.Buffer(bgl.GL_INT, 1)
+        bgl.glGetIntegerv(bgl.GL_CURRENT_PROGRAM, shader_program);
+
+        # Generate vertex array
+        self.vertex_array = bgl.Buffer(bgl.GL_INT, 1)
+        bgl.glGenVertexArrays(1, self.vertex_array)
+
+        self.texturecoord_location = bgl.glGetAttribLocation(shader_program[0], "texCoord");
+        self.position_location = bgl.glGetAttribLocation(shader_program[0], "pos");
+
+        # Generate geometry buffers for drawing textured quad
+        position = [0.0, 0.0, width, 0.0, width, height, 0.0, height]
+        position = bgl.Buffer(bgl.GL_FLOAT, len(position), position)
+        texcoord = [0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0]
+        texcoord = bgl.Buffer(bgl.GL_FLOAT, len(texcoord), texcoord)
+
+        self.vertex_buffer = bgl.Buffer(bgl.GL_INT, 2)
+        bgl.glGenBuffers(2, self.vertex_buffer)
+        bgl.glBindBuffer(bgl.GL_ARRAY_BUFFER, self.vertex_buffer[0])
+        bgl.glBufferData(bgl.GL_ARRAY_BUFFER, 32, position, bgl.GL_STATIC_DRAW)
+        bgl.glBindBuffer(bgl.GL_ARRAY_BUFFER, self.vertex_buffer[1])
+        bgl.glBufferData(bgl.GL_ARRAY_BUFFER, 32, texcoord, bgl.GL_STATIC_DRAW)
+        bgl.glBindBuffer(bgl.GL_ARRAY_BUFFER, 0)
+
+    def __del__(self):
+        bgl.glDeleteBuffers(2, self.vertex_buffer)
+        bgl.glDeleteVertexArrays(1, self.vertex_array)
+        bgl.glBindTexture(bgl.GL_TEXTURE_2D, 0)
+        bgl.glDeleteTextures(1, self.texture)
+
+    def draw(self):
+        bgl.glActiveTexture(bgl.GL_TEXTURE0)
+        bgl.glBindTexture(bgl.GL_TEXTURE_2D, self.texture[0])
+
+        bgl.glBindVertexArray(self.vertex_array[0])
+        bgl.glEnableVertexAttribArray(self.texturecoord_location);
+        bgl.glEnableVertexAttribArray(self.position_location);
+
+        bgl.glBindBuffer(bgl.GL_ARRAY_BUFFER, self.vertex_buffer[0])
+        bgl.glVertexAttribPointer(self.position_location, 2, bgl.GL_FLOAT, bgl.GL_FALSE, 0, None)
+        bgl.glBindBuffer(bgl.GL_ARRAY_BUFFER, self.vertex_buffer[1])
+        bgl.glVertexAttribPointer(self.texturecoord_location, 2, bgl.GL_FLOAT, bgl.GL_FALSE, 0, None)
+        bgl.glBindBuffer(bgl.GL_ARRAY_BUFFER, 0)
+
+        bgl.glDrawArrays(bgl.GL_TRIANGLE_FAN, 0, 4);
+
+        bgl.glBindVertexArray(0)
+        bgl.glBindTexture(bgl.GL_TEXTURE_2D, 0)
+
+
+# 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)
 
-    # RenderEngines also need to tell UI Panels that they are compatible
-    # Otherwise most of the UI will be empty when the engine is selected.
-    # In this example, we need to see the main render image button and
-    # the material preview panel.
-    from bl_ui import (
-        properties_render,
-        properties_material,
-    )
-    properties_render.RENDER_PT_render.COMPAT_ENGINES.add(CustomRenderEngine.bl_idname)
-    properties_material.MATERIAL_PT_preview.COMPAT_ENGINES.add(CustomRenderEngine.bl_idname)
-
+    for panel in get_panels():
+        panel.COMPAT_ENGINES.add('CUSTOM')
 
 def unregister():
     bpy.utils.unregister_class(CustomRenderEngine)
 
-    from bl_ui import (
-        properties_render,
-        properties_material,
-    )
-    properties_render.RENDER_PT_render.COMPAT_ENGINES.remove(CustomRenderEngine.bl_idname)
-    properties_material.MATERIAL_PT_preview.COMPAT_ENGINES.remove(CustomRenderEngine.bl_idname)
+    for panel in get_panels():
+        if 'CUSTOM' in panel.COMPAT_ENGINES:
+            panel.COMPAT_ENGINES.remove('CUSTOM')
 
 
 if __name__ == "__main__":