[Blender Python] Display images on 3D View using OpenGL (bgl)

at first

When writing add-ons for Blender, you often want to use images here. I think there is. If you use OpenGL (bgl) provided by Blender, you can display it, so I will try it.

There are two ways to read the image, so I will write each one.

When using bpy.data.images.load ()

sample

gl_texture_test.py


# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

# Blender2.77a

import bpy
import bgl


image_file_path = "C:/Works/blender_rogo.png "

class GL_Texture():
    def __init__(self, file_path):
        self.image = None
        self.width = 0
        self.height = 0
        
        self.load_image(file_path)
    
    def load_image(self, file_path):
        try:
            self.image = bpy.data.images.load(file_path)
        except Exception as e:
            print(e)
        
        if self.image:
            self.width, self.height = self.image.size
            self.image.gl_load(0, bgl.GL_NEAREST, bgl.GL_NEAREST)
    
    def remove(self):
        if self.image:
            try:
                self.image.user_clear()
                self.image.gl_free()
                #self.image.buffers_free()
                bpy.data.images.remove(self.image)
            except Exception as e:
                print(e)
    
    def bind(self):
        if self.image.bindcode[0]:
            bgl.glBindTexture(bgl.GL_TEXTURE_2D, self.image.bindcode[0])
        else:
            self.image.gl_load(0, bgl.GL_NEAREST, bgl.GL_NEAREST)
            print("reload gl texture")


class GL_Texture_test_Operator(bpy.types.Operator):
    bl_idname = "view3d.gl_texture_test_operator"
    bl_label = "View3D GL_Texture draw test"
    
    _handle_draw = None
    is_enabled = False
    _my_texture = None
    
    @staticmethod
    def draw_callback_px(self, context):
        bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
        bgl.glEnable(bgl.GL_BLEND)
        bgl.glEnable(bgl.GL_TEXTURE_2D)
        
        tex = GL_Texture_test_Operator._my_texture
        w = tex.width
        h =tex.height
        tex.bind()
        bgl.glBegin(bgl.GL_QUADS)
        bgl.glTexCoord2f(0.0, 0.0)
        bgl.glVertex2f(0.0, 0.0)
        bgl.glTexCoord2f(1.0, 0.0)
        bgl.glVertex2f(0.0+w, 0.0)
        bgl.glTexCoord2f(1.0, 1.0)
        bgl.glVertex2f(0.0+w, 0.0+h)
        bgl.glTexCoord2f(0.0, 1.0)
        bgl.glVertex2f(0.0, 0.0+h)
        bgl.glEnd()
        
        bgl.glDisable(bgl.GL_TEXTURE_2D)
        bgl.glDisable(bgl.GL_BLEND)
    
    @staticmethod
    def handle_add(self, context):
        GL_Texture_test_Operator._handle_draw = bpy.types.SpaceView3D.draw_handler_add(
                self.draw_callback_px, (self, context), 'WINDOW', 'POST_PIXEL')
    
    @staticmethod
    def handle_remove():
        if GL_Texture_test_Operator._handle_draw is not None:
            bpy.types.SpaceView3D.draw_handler_remove(GL_Texture_test_Operator._handle_draw, 'WINDOW')
            GL_Texture_test_Operator._handle_draw = None
            GL_Texture_test_Operator.is_enabled = False
    
    @classmethod
    def poll(cls, context):
        return context.area.type == 'VIEW_3D'
    
    def modal(self, context, event):
        if context.area:
            context.area.tag_redraw()
        return {'PASS_THROUGH'}
    
    def invoke(self, context, event):
        if context.area.type == 'VIEW_3D':
            if GL_Texture_test_Operator.is_enabled:
                self.cancel(context)
                return {'FINISHED'}
            else:
                GL_Texture_test_Operator._my_texture = GL_Texture(image_file_path)
                GL_Texture_test_Operator.handle_add(self, context)
                GL_Texture_test_Operator.is_enabled = True
                
                context.area.tag_redraw()
                context.window_manager.modal_handler_add(self)
                return {'RUNNING_MODAL'}
        else:
            self.report({'WARNING'}, "View3D not found, cannot run operator")
            return {'CANCELLED'}
    
    def cancel(self, context):
        GL_Texture_test_Operator.handle_remove()
        
        if GL_Texture_test_Operator._my_texture is not None:
            GL_Texture_test_Operator._my_texture.remove()
            GL_Texture_test_Operator._my_texture = None


class GL_Texture_test_panel(bpy.types.Panel):
    bl_label = "GL Texture test"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    
    def draw(self, context):
        layout = self.layout
        if GL_Texture_test_Operator.is_enabled:
            layout.operator("view3d.gl_texture_test_operator", "Stop", icon="PAUSE")
        else:
            layout.operator("view3d.gl_texture_test_operator", "Start", icon="PLAY")


def register():
    bpy.utils.register_class(GL_Texture_test_Operator)
    bpy.utils.register_class(GL_Texture_test_panel)

def unregister():
    bpy.utils.unregister_class(GL_Texture_test_panel)
    bpy.utils.unregister_class(GL_Texture_test_Operator)

if __name__ == "__main__":
    register()

Copy and paste it into Blender's text editor and run it. For image_file_path at the top, enter the path of the image you actually use.

blender_rogo.png I drew the Blender logo appropriately. Use this for the image to be displayed. If you don't have a suitable image, please use it.

Execution result

When you execute it, a button will be displayed in the property panel first, so press it. 2016-05-02_20h19_42.png

Then 2016-05-02_20h24_01.png

Description

The path check of the image file to be used is omitted. You should check it when you actually use it.

self.image.gl_load(0, bgl.GL_NEAREST, bgl.GL_NEAREST) I think that the image data is sent to the memory of OpenGL. I think the two arguments on the right are the storage method when scaling.

self.image.bindcode[0] The texture ID is included here after gl_load (). It wasn't a list before, but for some reason it has changed to a list. I don't know what data is contained except 0.

def bind(self):
    if self.image.bindcode[0]:
        bgl.glBindTexture(bgl.GL_TEXTURE_2D, self.image.bindcode[0])
    else:
        self.image.gl_load(0, bgl.GL_NEAREST, bgl.GL_NEAREST)
        print("reload gl texture")

As for this part, the texture memory is automatically released a few minutes after it is executed, so the texture is loaded again. Blender seems to be freeing memory on its own.

The drawing part is the same as OpenGL.

The good thing about this method

――It's easy anyway. --A wide variety of readable file formats

Where I think it's not so good

--Since the image file is loaded in Blender, you can see the loaded image like this during execution. (Although it is erased at the end) 2016-05-02_22h57_01.png --The texture memory is released for some reason after a few minutes of execution, so you need to gl_load () each time.

When reading image data into the buffer and registering it

GL_Texture class sample

Rewrite the GL_Texture class from the sample above and execute it.

GL_Texture class


class GL_Texture():
    def __init__(self, file_path):
        self.texture_id = 0
        self.width = 0
        self.height = 0
        
        self.load_8bit_bitmap(file_path)
    
    def load_8bit_bitmap(self, file_path):
        f = None
        bitmap_data = None
        gl_buffer = None
        
        try:
            f = open(file_path, 'rb')
            file_header = f.read(14)
            info_header = f.read(40)
            self.width = struct.unpack("<i", info_header[4:8])[0]
            self.height = struct.unpack("<i", info_header[8:12])[0]
            
            palette = []
            for i in range(256):
                rgbr = f.read(4)
                palette.append(struct.unpack("BBBB", rgbr)[0:3])
            
            gl_buffer = bgl.Buffer(bgl.GL_BYTE, self.width*self.height*4)
            for i in range(self.width*self.height):
                data = f.read(1)
                index = struct.unpack("B", data)[0]
                bgr = palette[index]
                gl_buffer[i*4] = bgr[2]
                gl_buffer[i*4+1] = bgr[1]
                gl_buffer[i*4+2] = bgr[0]
                
                if index == 0:
                    gl_buffer[i*4+3] = 0
                else:
                    gl_buffer[i*4+3] = 255
            
            f.close()
        except Exception as e:
            if f is not None:
                f.close()
            print(e)
            return False
            
        # set opengl
        textures = bgl.Buffer(bgl.GL_INT, 1)
        bgl.glGenTextures(1, textures)
        self.texture_id = textures[0]
        
        bgl.glBindTexture(bgl.GL_TEXTURE_2D, self.texture_id)
        
        bgl.glPixelStorei(bgl.GL_UNPACK_ALIGNMENT, 4)
        bgl.glTexImage2D(bgl.GL_TEXTURE_2D, 0, bgl.GL_RGBA,
                self.width, self.height, 0, bgl.GL_RGBA,
                bgl.GL_UNSIGNED_BYTE, gl_buffer)
        
        bgl.glTexParameteri(bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MAG_FILTER, bgl.GL_NEAREST)
        bgl.glTexParameteri(bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MIN_FILTER, bgl.GL_NEAREST)
        
    def remove(self):
        if self.texture_id:
            textures = bgl.Buffer(bgl.GL_INT, 1)
            textures[0] = self.texture_id
            bgl.glBindTexture(bgl.GL_TEXTURE_2D, 0)
            bgl.glDeleteTextures(1, textures)
            self.texture_id = 0
    
    def bind(self):
        bgl.glBindTexture(bgl.GL_TEXTURE_2D, self.texture_id)

Caution. Only 8-bit format bitmap files can be loaded. Index 0 is transparent. It seems that you cannot upload the bitmap file, so please prepare it yourself.

Description

It is a flow of opening a file, expanding it in a buffer, and registering it in OpenGL.

In bgl, if you want to pass a buffer as an argument, you need to use the Buffer class. I think that's the difference from ordinary OpenGL.

Since the file check process is omitted, please check various things when actually using it.

The good thing about this method

--Since it is registered in OpenGL directly without passing through the Image class of Blende, Blender does not release the memory without permission.

Where I think it's not so good

--Loading is a little slow. --You need to write as many load parts as there are formats you want to support.

Finally

Personally, I recommend using bpy.data.images.load () because it's easier and easier. It loads fast. The image file is also loaded in Blender during execution, but even if you save it as it is, it will not be saved if the number of users is 0, so you do not have to worry too much.

Recommended Posts

[Blender Python] Display images on 3D View using OpenGL (bgl)
View images in OpenCV from Python using an external USB camera on your MacBook
Save images using python3 requests
Display PIL images on Jupyter
Detect "temperature (using A / D converter)" using python on Raspberry Pi 3!
Broadcast on LINE using python
Introducing Python using pyenv on Ubuntu 20.04
Notes on using MeCab from Python
Preparing python using vscode on ubuntu
Install Pytorch on Blender 2.90 python on Windows
Generating multilingual text images using Python
Study on Tokyo Rent Using Python (3-2)
Notes on installing Python using PyEnv
View stack trace using [Python] inspect
Notes on using rstrip with python.
Install Python on CentOS using Pyenv
Study on Tokyo Rent Using Python (3-3)
Install Python on CentOS using pyenv
Detect analog signals with A / D converter using python on Raspberry Pi 3!
Let's make 3D animation only with Python without using Blender! [FBX SDK Python]
Watershed method in 3D images using ImageJ
Notes for using OpenCV on Windows10 Python 3.8.3.
Execute Python code on C ++ (using Boost.Python)
Detect "brightness" using python on Raspberry Pi 3!
Install python library on Lambda using [/ tmp]
Run servomotor on Raspberry Pi 3 using python
Study on Tokyo Rent Using Python (3-1 of 3)
[Python] Progress display by progress bar using tqdm
Detect temperature using python on Raspberry Pi 3!
Video processing using Python + OpenCV on Mac
Notes on using code formatter in Python
GUI display train delay information using python