4
$\begingroup$

I would like to draw on top of render image and save it. For example, I would like to draw a text or a line and then save the final image as PNG.

I was thinking to use something like this: Is it possible to create image data and save to a file from a script

But, to make a line here or text would be a nightmare calculating each pixel position. I would like to use OpenGL primitives for drawing on top.

$\endgroup$
4
  • $\begingroup$ Are you working on displaying measurements in rendered images? :D Great addon btw. Thanks for making it :) $\endgroup$
    – gandalf3
    Commented Jun 5, 2015 at 9:42
  • $\begingroup$ How about making lines and texts as Blender 3D objects? For example a line can be done as a thin cylinder and a text as Blender 3D text. $\endgroup$
    – vvoovv
    Commented Jun 5, 2015 at 12:02
  • 1
    $\begingroup$ Yes, I'm working on render MeasureIt, and about adding objects for render, I don't like the idea to fill the scene with garbage objects. Anyway, I think I have one idea that works. $\endgroup$
    – Antonioya
    Commented Jun 5, 2015 at 16:08
  • 1
    $\begingroup$ render the measurements to SVG that way they can be styled easily too using groups and classes and ids, and composited later. $\endgroup$
    – zeffii
    Commented Jun 6, 2015 at 21:09

3 Answers 3

3
$\begingroup$

Well, I think I have an idea of how to do it:

import bpy
import bgl
import blf
import bpy_extras.image_utils as img_utils

# Get the render image
path = "c:\\tmp\\tmp_render.png"
try:
    result = bpy.data.images['Render Result']
    if result.has_data is False:
        bpy.ops.render.render()
        result = bpy.data.images['Render Result']
except:
    bpy.ops.render.render()
    result = bpy.data.images['Render Result']

# Save and reload
result.save_render(path)
img = img_utils.load_image(path)


viewport_info = bgl.Buffer(bgl.GL_INT, 4)
bgl.glGetIntegerv(bgl.GL_VIEWPORT, viewport_info)

scene = bpy.context.scene
render_scale = scene.render.resolution_percentage / 100

WIDTH  = int(scene.render.resolution_x * render_scale)
HEIGHT = int(scene.render.resolution_y * render_scale)

# Load image on memory
img.gl_load(0, bgl.GL_NEAREST, bgl.GL_NEAREST)
tex = img.bindcode

# Create output image (to apply texture)
out = bpy.data.images.new("output", WIDTH, HEIGHT)
buffer = bgl.Buffer(bgl.GL_FLOAT, WIDTH * HEIGHT * 4)

bgl.glDisable(bgl.GL_SCISSOR_TEST) # if remove this line, get blender screenshot not image 
bgl.glViewport(0, 0, WIDTH, HEIGHT)

bgl.glMatrixMode(bgl.GL_PROJECTION)
bgl.glLoadIdentity()
bgl.gluOrtho2D(0, WIDTH, 0, HEIGHT)

# Clear
bgl.glClearColor(0.0, 0.0, 0.0, 0.0)
bgl.glClear(bgl.GL_COLOR_BUFFER_BIT | bgl.GL_DEPTH_BUFFER_BIT)

bgl.glEnable(bgl.GL_TEXTURE_2D)
bgl.glBindTexture(bgl.GL_TEXTURE_2D, tex)

bgl.glBegin(bgl.GL_QUADS)
bgl.glColor3f(1.0, 1.0, 1.0)
bgl.glTexCoord2f(0.0, 0.0)
bgl.glVertex2f(0.0, 0.0)
bgl.glTexCoord2f(1.0, 0.0)
bgl.glVertex2f(WIDTH, 0.0)
bgl.glTexCoord2f(1.0, 1.0)
bgl.glVertex2f(WIDTH, HEIGHT)
bgl.glTexCoord2f(0.0, 1.0)
bgl.glVertex2f(0.0, HEIGHT)
bgl.glEnd()

bgl.glLineWidth(10)
bgl.glBegin(bgl.GL_LINES)
bgl.glColor4f(1.0, 0.0, 0.0, 1.0)
#bgl.glTexCoord2f(0.0, 0.0)
bgl.glVertex2f(0.0, 0.0)
#bgl.glTexCoord2f(1.0, 1.0)
bgl.glVertex2f(WIDTH, HEIGHT)
bgl.glEnd()

# Draw a Text
font_id = 0
bgl.glColor4f(1.0, 1.0, 0.0, 1.0)
blf.size(font_id, 18, 72)
blf.position(font_id, 0.5, 0.5, 0)
blf.draw(font_id, "Hello World")   

#
bgl.glFinish()
bgl.glReadPixels(0, 0, WIDTH, HEIGHT , bgl.GL_RGBA, bgl.GL_FLOAT, buffer) # read image data
out.pixels = buffer[:] # Assign image data
img.gl_free() # free opengl image memory

#reset
bgl.glEnable(bgl.GL_SCISSOR_TEST)
# restore opengl defaults
bgl.glLineWidth(1)
bgl.glDisable(bgl.GL_BLEND)
bgl.glColor4f(0.0, 0.0, 0.0, 1.0)

Result:enter image description here

$\endgroup$
7
  • $\begingroup$ You are using the OpenGL context which is used by blender to render the UI, viewport, scene etc. This is actually a hack. bgl.glViewport may use a section of the screen but indeed, it can not be larger than the screen. $\endgroup$ Commented Jun 6, 2015 at 15:41
  • $\begingroup$ You might render patches (set the view matrix accordingly for each piece) and stitch them together in the image. $\endgroup$ Commented Jun 6, 2015 at 15:45
  • $\begingroup$ @pink vertex I'm not an expert on this, could you provide me any example of render by parts and put together? $\endgroup$
    – Antonioya
    Commented Jun 6, 2015 at 16:29
  • $\begingroup$ @Antonioya, See what happens if you put some color for the text. $\endgroup$
    – Mano-Wii
    Commented Jun 6, 2015 at 16:55
  • $\begingroup$ @Mano-Wii I have tested:bgl.glColor3f(1.0, 0.0, 1.0) before blf.draw and nothing. I'm missing something :-( $\endgroup$
    – Antonioya
    Commented Jun 6, 2015 at 18:07
2
$\begingroup$

I think you will have to save the render result, reopen it in the image editor to make it accessible, edit its data and save again.

import bpy
import bpy_extras.image_utils as img_utils

path = "your_file_path"
result = bpy.data.images['Render_Result']
result.save_render(path)
img = img_utils.load_image(path)

See this answer for drawing with OpenGL.

You can also attach a drawing handler to bpy.types.SpaceImageEditor to provide a live preview but i don't think you can alter the Render_Result in 2.74 from the python api directly.


Rendering a Tile

Use

dx = TILE_X / WIDTH
dy = TILE_Y / HEIGHT
#...
bgl.gluOrtho2D(dx*col, dx*(col+1), dy*row, dy*(row+1))

Write back the result(see this answer):

tmp = list(img[:]) 
#copy the image once
#modify tmp for each tile    
#finally copy tmp to output image

#def set_pixel(data, x, y, width, value, channels=4):
#    for i in range(channels):
#        img[channels * (y*width + x) + i] = value[i]

#copy lines
for k in range(TILE_Y):
   i0 = 4*(
       WIDTH  * (row * TILE_Y + k) +
       TILE_X *  col               
   )
   i1 = i0 + TILE_X*4
   tmp[i0:i1] = buffer[k*TILE_X*4: (k+1)*TILE_X*4]      
$\endgroup$
1
  • $\begingroup$ Yes, I don't think you can draw directly, my idea was save and redraw on top. Thanks! I think this can help me. $\endgroup$
    – Antonioya
    Commented Jun 5, 2015 at 13:22
1
$\begingroup$

I liked your solution @Antonioya. You can clear the even more script. For example, this script here already is with draw_callback_px function and with a modal.

import bpy
import bgl
import blf

def draw_callback_px(self, context):

    font_id = 0  # XXX, need to find out how best to get this.

    # draw some text
    bgl.glColor4f(1.0, 1.0, 1.0, 0.8)
    blf.position(font_id, 15, 30, 0)
    blf.size(font_id, 20, 72)
    blf.draw(font_id, "Hello Word " )

    bgl.glEnable(bgl.GL_BLEND)
    bgl.glColor4f(0.0, 0.0, 0.0, 0.5)
    bgl.glLineWidth(20)

    bgl.glBegin(bgl.GL_LINE_STRIP)
    bgl.glColor3f(1.0, 0.0, 0.0)
    bgl.glVertex2f(100, 250)
    bgl.glVertex2f(400, 300)
    bgl.glEnd()

    # restore opengl defaults
    bgl.glLineWidth(1)
    bgl.glDisable(bgl.GL_BLEND)
    bgl.glColor4f(0.0, 0.0, 0.0, 1.0)

class ModalDrawOperator(bpy.types.Operator):
    """Draw a line with the mouse"""
    bl_idname = "view3d.modal_operator"
    bl_label = "Simple Modal View3D Operator"

    def modal(self, context, event):
        context.area.tag_redraw()

        if event.type == 'LEFTMOUSE' and event.value == 'PRESS':

            # Image resolution
            u = 800
            v = 600

            #bgl.glDisable(bgl.GL_SCISSOR_TEST)
            bgl.glViewport(0, 0, u, v)

            bgl.glMatrixMode(bgl.GL_PROJECTION)
            #bgl.glPushMatrix()
            bgl.glLoadIdentity()
            rx = context.region.width
            ry = context.region.height
            bgl.gluOrtho2D(0, rx, 0, ry)

            bgl.glClearColor(0.0, 0.0, 0.0, 0.0)
            bgl.glClear(bgl.GL_COLOR_BUFFER_BIT | bgl.GL_DEPTH_BUFFER_BIT)

            draw_callback_px(self, context)

            bgl.glFinish()
            out = bpy.data.images.new("output", u, v)
            buffer = bgl.Buffer(bgl.GL_FLOAT, u * v * 4)
            bgl.glReadPixels(0, 0, u, v , bgl.GL_RGBA, bgl.GL_FLOAT, buffer)
            out.pixels = buffer[:]
            out.gl_free()
            #bgl.glEnable(bgl.GL_SCISSOR_TEST)

        elif event.type in {'ESC'}:
            for a in bpy.data.images:
                a.gl_free()

            bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
            return {'CANCELLED'}

        return {'PASS_THROUGH'}

    def invoke(self, context, event):
        if context.area.type == 'VIEW_3D':
            # the arguments we pass the the callback
            args = (self, context)
            # Add the region OpenGL drawing callback
            # draw in view space with 'POST_PIXEL', 'POST_VIEW' or 'PRE_VIEW'
            self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL')

            self.mouse_path = event.mouse_region_x, event.mouse_region_y

            context.window_manager.modal_handler_add(self)
            return {'RUNNING_MODAL'}
        else:
            self.report({'WARNING'}, "View3D not found, cannot run operator")
            return {'CANCELLED'}


def register():
    bpy.utils.register_class(ModalDrawOperator)


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

if __name__ == "__main__":
    register()

What it does ... when you press the left mouse button, the script takes the resolution you set for the image, clean (alpha = 0) OpenGL that is in the background, draws once callback function, creates a new image, reads the OpenGL pixels, draws those pixels in the new image and ends.

enter image description here

$\endgroup$
4
  • $\begingroup$ do you know how to draw text? I have tested and don't get anything. $\endgroup$
    – Antonioya
    Commented Jun 6, 2015 at 7:37
  • $\begingroup$ @Antonioya, For me it worked. Did you make sure you have imported the blf module? $\endgroup$
    – Mano-Wii
    Commented Jun 6, 2015 at 11:12
  • $\begingroup$ Yes, blf is in the source code of my example. The line works, but the text don't.. Try my example. $\endgroup$
    – Antonioya
    Commented Jun 6, 2015 at 15:25
  • $\begingroup$ I edited the script to show how to display the 'Hello World' text at the bottom Left. Note: In the function glReadPixels, values for pixels that lie outside the window connected to the current GL context are undefined. $\endgroup$
    – Mano-Wii
    Commented Jun 6, 2015 at 15:57

You must log in to answer this question.

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