# Is possible to draw over render image and save it?

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.

• Are you working on displaying measurements in rendered images? :D Great addon btw. Thanks for making it :) – gandalf3 Jun 5 '15 at 9:42
• 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. – vvoovv Jun 5 '15 at 12:02
• 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. – Antonioya Jun 5 '15 at 16:08
• render the measurements to SVG that way they can be styled easily too using groups and classes and ids, and composited later. – zeffii Jun 6 '15 at 21:09

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']

result.save_render(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)

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.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.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:

• 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. – pink vertex Jun 6 '15 at 15:41
• You might render patches (set the view matrix accordingly for each piece) and stitch them together in the image. – pink vertex Jun 6 '15 at 15:45
• @pink vertex I'm not an expert on this, could you provide me any example of render by parts and put together? – Antonioya Jun 6 '15 at 16:29
• @Antonioya, See what happens if you put some color for the text. – Mano-Wii Jun 6 '15 at 16:55
• @Mano-Wii I have tested:bgl.glColor3f(1.0, 0.0, 1.0) before blf.draw and nothing. I'm missing something :-( – Antonioya Jun 6 '15 at 18:07

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)


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]

• Yes, I don't think you can draw directly, my idea was save and redraw on top. Thanks! I think this can help me. – Antonioya Jun 5 '15 at 13:22

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()
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

return {'RUNNING_MODAL'}
else:
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.

• do you know how to draw text? I have tested and don't get anything. – Antonioya Jun 6 '15 at 7:37
• @Antonioya, For me it worked. Did you make sure you have imported the blf module? – Mano-Wii Jun 6 '15 at 11:12
• Yes, blf is in the source code of my example. The line works, but the text don't.. Try my example. – Antonioya Jun 6 '15 at 15:25
• 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. – Mano-Wii Jun 6 '15 at 15:57