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$
  • $\begingroup$ Are you working on displaying measurements in rendered images? :D Great addon btw. Thanks for making it :) $\endgroup$ – gandalf3 Jun 5 '15 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 Jun 5 '15 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 Jun 5 '15 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 Jun 6 '15 at 21:09
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$
  • $\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$ – pink vertex Jun 6 '15 at 15:41
  • $\begingroup$ You might render patches (set the view matrix accordingly for each piece) and stitch them together in the image. $\endgroup$ – pink vertex Jun 6 '15 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 Jun 6 '15 at 16:29
  • $\begingroup$ @Antonioya, See what happens if you put some color for the text. $\endgroup$ – Mano-Wii Jun 6 '15 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 Jun 6 '15 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$
  • $\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 Jun 5 '15 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$
  • $\begingroup$ do you know how to draw text? I have tested and don't get anything. $\endgroup$ – Antonioya Jun 6 '15 at 7:37
  • $\begingroup$ @Antonioya, For me it worked. Did you make sure you have imported the blf module? $\endgroup$ – Mano-Wii Jun 6 '15 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 Jun 6 '15 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 Jun 6 '15 at 15:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.