Looking for help/ tips to create an addon for a proxy-like feature in Blender

Hi people,

I’m currently writing an addon with the purpose of creating proxy models (vertex clouds) of highpoly objects to spead up the viewport and reduce memory consumption. However, there are still some issues I can’t get a hold on and I hope you can help me out.

When creating the proxy object the addon provides to option:

  1. offload the original object/ mesh ("_hi" suffix) to a library file and load it back into the project file via button click or at render time and swap the proxy obejct’s mesh ("_Proxy" suffix) with the original one. (default)
  2. keep the orignal object ("_hi" suffix) in the project file and assign a fake user. At rendertime or button click, the proxy obejct’s mesh ("_Proxy" suffix) will be replaced by the original mesh.

After rendering, the original mesh will be removed from the file to reduce memory consumption, unless it has a fake user assigned.

Issue No.1:
When I use the option to keep the original mesh in the project file and use the buttons to swap meshes (Pre and Post Render) everything seems to work fine.
First time I do a final render (F12) it works as expected as well but somehow the original mesh gets deleted even though it has a fake user assigned. You can observe this in the outliner.
Logically, every follwing render can’t switch proxy mesh to original mesh, as the original is no longer inside the project file. Does anyone know why this happens?

Issue No.2:
Whenever I delete any object when the addon is activated I get an error message in the status bar. I guess it’s caused by the poll function. It doesn’t break the script but is a little irritating. How can I prevent this from happening?

Issue No.3:
The location of the library file is stored in an object property. I would like to display the path in the UI as an editable field that prompts the file browser which updates when the object property changes. But the editable field doesn’t reflect any changes made to the property whereas the simple UI label does. Is there any way to achieve that with the filepath field, too?
Unbenannt-1

I really hope some of you can help me with this. It would be greatly appreciated :grinning:
If you need further clarification let me know.

# ##### 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 #####


bl_info = {
    "name": "Proxy Tools",
    "author": "xrogueleaderx",
    "version": (2, 0),
    "blender": (2, 91, 0),
    "location": "View3D > UI > Proxy Tools",
    "description": "Tool to create vertex cloud representations of highpoly objects",
    "warning": "",
    "wiki_url": "",
    "category": "Object",
}

#----------------------------------------------------------------------------------------------------------------------------------------
# Import Modules
#----------------------------------------------------------------------------------------------------------------------------------------
import bpy
import os, sys

from bpy_extras.io_utils import (
    ExportHelper,)

from bpy.types import (
    AddonPreferences,
    Operator,
    Panel,
    PropertyGroup,)

from bpy.props import (
    StringProperty,
    IntProperty,
    FloatProperty,
    BoolProperty,
    EnumProperty,
    PointerProperty,)

from bpy.app.handlers import (
    persistent,)

#----------------------------------------------------------------------------------------------------------------------------------------
# Define Custom Properties Update Conditions
#----------------------------------------------------------------------------------------------------------------------------------------

def update_display_type(self, context):
    eval('bpy.ops.' + self.display_type_list + '()')
    
def update_filepath(self, context):
    obj = bpy.context.active_object
    bpy.data.objects[obj.name].proxy_object_settings.proxy_path = self.proxy_path

#----------------------------------------------------------------------------------------------------------------------------------------
# Define Custom Properties
#----------------------------------------------------------------------------------------------------------------------------------------
class PROPS_UL_ProxyToolSettings(PropertyGroup):    
    #create float property for Reduce Verts
    reduce_verts : FloatProperty(
        name = "Reduce Verts", 
        default =99.80, 
        min = 0.00, 
        max = 99.99, 
        precision = 2, 
        description = "Reduce vertices by percent", 
        subtype = "PERCENTAGE",
        )

    #create bool property Offload
    off_load : BoolProperty(
        name = "Off Load",  
        description="Store object data in separate library file",
        default=True,
        )
       
    #create enum property for display type 
    display_type_list : EnumProperty(
        name = "Display Type List",
        items = [("object.proxy_postrender","Point Cloud",'',1),
                ("object.proxy_prerender","Bounds",'',2)],
        description = "Set proxy object display type",
        default="object.proxy_postrender",
        update = update_display_type,
        )

class PROPS_UL_ProxyObjectSettings(PropertyGroup):    
    #create string property for Proxy Path
    proxy_path : StringProperty(
        name = "Proxy Path",
        description = "Define the directory to store the proxy object file",
        subtype = 'FILE_PATH',
        update = update_filepath,
        )
           
#----------------------------------------------------------------------------------------------------------------------------------------
# Changing Viewport Display for Proxy Objects
#----------------------------------------------------------------------------------------------------------------------------------------
@persistent
def ProxyPreRenderHandler(scene):
    #store all proxy objects in the secene in a list
    ProxyObjectList = [obj for obj in bpy.data.objects if ("_Proxy" in obj.name)]
    MeshNameList = []

    for mesh in bpy.data.meshes:
        if ("_hi") in mesh.name:
            MeshNameList.append(mesh.name)
                
    #set display type to bounds before exchanging meshes for better performance
    for obj in ProxyObjectList:
        if obj.type == 'MESH' and ("_Proxy" in obj.name):  
            obj.display_type = 'BOUNDS'
        
        if (obj.name[:-5]+"hi") in MeshNameList:
            if obj.name.endswith("_Proxy"):        
                obj.data = bpy.data.meshes[obj.name.replace("Proxy","hi")]
            else:
                #if proxy object is a duplicate with the suffix .XXX, remove the suffix first before attaching "hi" to the name
                obj.data = bpy.data.meshes[obj.name[:-9]+"hi"]
                
        elif (obj.name[:-9]+"hi") in MeshNameList:
            #if proxy object is a duplicate with the suffix .XXX, remove the suffix first before attaching "hi" to the name
            obj.data = bpy.data.meshes[obj.name[:-9]+"hi"]
            
        elif obj.proxy_object_settings.proxy_path != (''):           
            #link all highpoly meshes into master blend file
            filepath = obj.proxy_object_settings.proxy_path
            with bpy.data.libraries.load(filepath=filepath, link=True) as (data_from, data_to):
                data_to.meshes = [name for name in data_from.meshes if name.endswith("_hi")]
            #and switch proxy mesh to highpoly mesh
            if obj.name.endswith("_Proxy"):        
                obj.data = bpy.data.meshes[obj.name.replace("_Proxy","_hi")]
            else:
                #if proxy object is a duplicate with the suffix .XXX, remove the suffix first before attaching "hi" to the name
                obj.data = bpy.data.meshes[obj.name[:-9]+"hi"]
        else:
            return {"Original mesh not found! Check blend file and library path"}
        
@persistent
def ProxyPostRenderHandler(scene):
    #store all proxy objects in the scene in a list
    ProxyObjectList = [obj for obj in bpy.data.objects if ("_Proxy" in obj.name)]

    #switch highpoly meshes back to proxy mesh after rendering    
    for obj in ProxyObjectList:
        if obj.type == 'MESH' and ("_Proxy" in obj.name):
            if obj.name.endswith("_Proxy"):        
                obj.data = bpy.data.meshes[obj.name]
            else:
                #if proxy object is a duplicate with the suffix .XXX remove suffix first before exchanging mesh
                obj.data = bpy.data.meshes[obj.name[:-4]]
            #and reset display type back to solid 
            obj.display_type = 'SOLID'
            
    #remove remaining highpoly meshes from blendfile for better memory efficiency
    for block in bpy.data.meshes:
        if  ("_hi" in block.name) and block.use_fake_user == False:
            bpy.data.meshes.remove(block)
        
class OBJECT_OT_ProxyPreRender(Operator):
    bl_idname = 'object.proxy_prerender'
    bl_label = 'Pre Render'
    bl_description = 'Set proxy objects display to bounds'
    
    def execute(self, context):
        ProxyPreRenderHandler(context.scene)
        return {'FINISHED'}

class OBJECT_OT_ProxyPostRender(Operator):
    bl_idname = 'object.proxy_postrender'
    bl_label = 'Post Render'
    bl_description = 'Set proxy objects display to vertex cloud'
    
    def execute(self, context):
        ProxyPostRenderHandler(context.scene)
        return {'FINISHED'}

#-------------------------------------------------------------------------------------------------------------------------
#Create proxy object and offload source object with popup filebrowser
#-------------------------------------------------------------------------------------------------------------------------    
class OBJECT_OT_SaveToLibrary(Operator, ExportHelper):
    bl_idname = "proxy_tool.save_original_to_library"
    bl_label = "Save Original To Library"
    bl_description = "Create Proxy and store to external library"
    bl_options = {'UNDO'}
    
    filename_ext = '.blend'
    filter_global: StringProperty(
        default = '*.blend',
        options = {'HIDDEN'},
        )

    @classmethod
    def poll(cls, context):
        return context.object.type  == 'MESH'

    def execute(self, context):
        #store original location of source object
        originalLocation = [bpy.context.active_object.location[0], bpy.context.active_object.location[1],bpy.context.active_object.location[2]]

        #reset source object location to world origin
        bpy.ops.object.location_clear(clear_delta=True)

        #store source object data in variable
        SourceObject = bpy.context.active_object

        #duplicate source object to get proxy object
        bpy.ops.object.duplicate()
        #replace proxy object automatic suffix .XXX with _Proxy suffix    
        ProxyObject = bpy.context.active_object
        ProxyObject.name = ProxyObject.name[:-4]+"_Proxy"
        ProxyObject.data.name = ProxyObject.name
        bpy.data.objects[ProxyObject.name].use_fake_user = True
        bpy.data.meshes[ProxyObject.name].use_fake_user = True
        
        #simplify proxy object by applying all modifiers and stripping materials
        bpy.ops.object.convert(target='MESH')

        #create vertex cloud representation by deleting all edges, faces and high number of vertices
        bpy.ops.object.mode_set(mode='EDIT')
        bpy.context.tool_settings.mesh_select_mode = [True, False, False]
        bpy.ops.mesh.select_all(action='SELECT')
        bpy.ops.mesh.delete(type='EDGE_FACE')
        bpy.ops.mesh.select_random(percent=bpy.context.scene.proxy_tool.reduce_verts)

        bpy.ops.mesh.delete(type='VERT')
        bpy.ops.object.mode_set(mode='OBJECT')

        #add high poly suffix to source object and its mesh
        SourceObject.name = SourceObject.name + '_hi'
        SourceObject.data.name = SourceObject.name
        
        #offload source and proxy object to library file
        bpy.data.objects[ProxyObject.name].proxy_object_settings.proxy_path = self.properties.filepath
        datablocks = {obj for obj in bpy.data.objects if obj.name == SourceObject.name or obj.name == ProxyObject.name}
                
        bpy.data.libraries.write(self.filepath, datablocks, fake_user=True)
        
        #place proxy object at source object location
        bpy.data.objects[ProxyObject.name].location = originalLocation
        
        #remove source object
        bpy.data.objects.remove(SourceObject, do_unlink=True)
        
        #clean file from unused accumulated datablocks    
        for block in bpy.data.meshes:
            if block.users == 0:
                bpy.data.meshes.remove(block)
                
        return {'FINISHED'}
    
#-------------------------------------------------------------------------------------------------------------------------
#Create proxy object and store original locally
#-------------------------------------------------------------------------------------------------------------------------    
class OBJECT_OT_SaveLocal(Operator):
    bl_idname = "proxy_tool.save_local"
    bl_label = "Save Original Locally"
    bl_description = "Create Proxy and store in current file"
    bl_options = {'UNDO'}

    @classmethod
    def poll(cls, context):
        return context.object.type  == 'MESH'

    def execute(self, context):
        #store original location of source object
        originalLocation = [bpy.context.active_object.location[0], bpy.context.active_object.location[1],bpy.context.active_object.location[2]]

        #reset source object location to world origin
        bpy.ops.object.location_clear(clear_delta=True)

        #store source object data in variable
        SourceObject = bpy.context.active_object

        #duplicate source object to get proxy object
        bpy.ops.object.duplicate()
        #replace proxy object automatic suffix .XXX with _Proxy suffix    
        ProxyObject = bpy.context.active_object
        ProxyObject.name = ProxyObject.name[:-4]+"_Proxy"
        ProxyObject.data.name = ProxyObject.name
        bpy.data.objects[ProxyObject.name].use_fake_user = True
        bpy.data.meshes[ProxyObject.name].use_fake_user = True
        
        #simplify proxy object by applying all modifiers and stripping materials
        bpy.ops.object.convert(target='MESH')

        #create vertex cloud representation by deleting all edges, faces and high number of vertices
        bpy.ops.object.mode_set(mode='EDIT')
        bpy.context.tool_settings.mesh_select_mode = [True, False, False]
        bpy.ops.mesh.select_all(action='SELECT')
        bpy.ops.mesh.delete(type='EDGE_FACE')
        bpy.ops.mesh.select_random(percent=bpy.context.scene.proxy_tool.reduce_verts)

        bpy.ops.mesh.delete(type='VERT')
        bpy.ops.object.mode_set(mode='OBJECT')

        #add high poly suffix to source object and its mesh
        SourceObject.name = SourceObject.name + '_hi'
        SourceObject.data.name = SourceObject.name
        
        #place proxy object at source object location
        bpy.data.objects[ProxyObject.name].location = originalLocation
        
        #assign fake user to original mesh
        bpy.data.meshes[SourceObject.name].use_fake_user = True
        
        #remove source object
        bpy.data.objects.remove(SourceObject, do_unlink=True)
                
        return {'FINISHED'}
    
#----------------------------------------------------------------------------------------------------------------------------------------
# UI Panel
#----------------------------------------------------------------------------------------------------------------------------------------  
class OBJECT_PT_ProxyPanel(Panel):
    # create panel in UI under category Proxy
    bl_label = "Create Proxy"
    bl_idname = "TOOLS_PT_CreateProxy"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = "Proxy Tools"
    bl_context = "objectmode"

    def draw(self, context):
        #store source and proxy objects in lists
        SourceObjectList = [obj for obj in bpy.data.objects if obj.name.endswith("_hi")]
        ProxyObjectList = [obj for obj in bpy.data.objects if obj.name.endswith("_Proxy")]
        
        layout = self.layout
        scene = bpy.context.scene
        proxy_tool = scene.proxy_tool
        obj = bpy.data.objects[0]
        proxy_object_settings = obj.proxy_object_settings

        #create label
        layout.label(text = "Convert Object to Vertex Cloud")
        
        #create slider with Reduce Verts property
        layout.prop(proxy_tool, "reduce_verts", text = "Vertex Reduction")
    
        #create directory selection
#        row = layout.row()        
#        layout.prop(proxy_object_settings, "proxy_path", text = "Proxy Path:")
        
        row =layout.row()
        row.label(text = "Current Path:")
        obj = bpy.context.active_object
        row.label(text = bpy.data.objects[obj.name].proxy_object_settings.proxy_path)
    
        #create checkbox for off load geometry
        row = layout.row()
        row.prop(proxy_tool, "off_load", text = "Offload Geometry")
        
        #create conversion button
        row = layout.row()
        if context.scene.proxy_tool.off_load == True:
            row.operator(OBJECT_OT_SaveToLibrary.bl_idname, text = "Create Proxy")
        else:
            row.operator(OBJECT_OT_SaveLocal.bl_idname, text = "Create Proxy")
        
        layout.label(text = "Display Type")
        layout.prop(proxy_tool, "display_type_list", text = "")
        
        layout.label(text = "Convert Scene")
        
        row = layout.row()
        #create pre render button
        row.operator(OBJECT_OT_ProxyPreRender.bl_idname, icon = "CUBE")
        #create post render button
        row.operator(OBJECT_OT_ProxyPostRender.bl_idname, icon = "STICKY_UVS_DISABLE")
      
#----------------------------------------------------------------------------------------------------------------------------------------
# Script Registration for Classes and Handlers
#----------------------------------------------------------------------------------------------------------------------------------------
classes = (
    PROPS_UL_ProxyToolSettings,
    PROPS_UL_ProxyObjectSettings,
    OBJECT_PT_ProxyPanel,
    OBJECT_OT_SaveToLibrary,
    OBJECT_OT_SaveLocal,
    OBJECT_OT_ProxyPreRender,
    OBJECT_OT_ProxyPostRender,
    )

def register():
    from bpy.utils import register_class
    for cls in classes:
         register_class(cls)
    bpy.app.handlers.render_pre.append(ProxyPreRenderHandler)
    bpy.app.handlers.render_post.append(ProxyPostRenderHandler)
    bpy.types.Scene.proxy_tool = PointerProperty(type = PROPS_UL_ProxyToolSettings)
    bpy.types.Object.proxy_object_settings = PointerProperty(type = PROPS_UL_ProxyObjectSettings)

def unregister():
    from bpy.utils import unregister_class
    for cls in classes:
        unregister_class(cls)
    bpy.app.handlers.render_pre.remove(ProxyPreRenderHandler)
    bpy.app.handlers.render_post.remove(ProxyPostRenderHandler)
    del bpy.types.Scene.proxy_tool
    del bpy.types.Object.proxy_object_settings

if __name__ == "__main__":
    register()
4 Likes

HI,

It’s really strange you have no replies on this. This is a game changer for scene management.

I did some quick testing, and it all seems to work fine so far on the creation of the proxies and such.

There is some oddness with hierarchies though.
I can select a hierarchy of objects and proxy that. It works, but the child objects are still in the scene.
Strangely enough the proxy contains the child objects for render.
Also, the child objects don’t keep their original position in the scene, they move around the scene.
Probably due to the parent-child relationship being severed.

And I have to sometimes ‘kick’ Cycles into gear in the viewport by adding real geo in the scene fort rendering.

But… Awesome stuff here!! :slight_smile:

edit: there seems no mechanism in place to avoid the creation of a proxy from a proxy, which will break the switch buttons at the bottom.
And the View Bounds button will render the proxy in viewport shading mode. Not sure if that is intentional. :wink:
Also, with the Viewport Shading mode turned on, switching between Bounds/Cloud will lock up Blender + PC at some point for some time.

1 Like

Yes, the topic title might not reflect the importance of a feature like this. Guess that’s the reason :sweat_smile:

That’s a fair point. I rarely use parented objects myself but I will defintely have a look.

What do you mean by that?

Preventing proxy creation from proxy should be relatively easy.

Not sure what you mean here. If you use the PreRender Button the display type is set to bounds, all original meshes are loaded from the libraries and replace the proxy meshes. In my opinion there should be a manual way to do this e.g. for Viewport Render. Otherwise you will only see the vertex clouds in Viewport render.
Blender freezing when switching between bounds and clouds in viewport render (viewport shading?) should be normal depending on the size of the models that are linked into the file at that time. I’m afraid I can’t do anything about that :expressionless:

Yeah, that would be great. Now you get some odd results in the scene. It also keeps the child objects shown as point cloud when the parent is shown in bounds. After some switching between bounds/cloud they disappear. And pop up again, but not all at some point. :smiley:

Sorry, user error. Forget about that render comment.

Yes, that’s what I’m seeing here. I thought it was user error :wink:
The full proxy as set to bounds will only show in the Viewport Shading mode (render).

I looked a bit into the mechanics, and saw that the proxy file is just a dump of the Vertex data.
How can one swap back in the original geometry?
And looking at workflow, it will not work for linked data as it cannot pull the data in I guess?

Can I suggest some small text changes, as they confused me at first.
-Rename Bounds to Bounds/Rendered for the dropdown and button.
-And add to the button tooltip that the rendered geo will only show in Viewport Shading mode.
Might help what’s going on :wink:

Ah… forgot. It would be great to be able to ‘mix’ display views. So both cloud and bounds view at the same time. Not one or the other. That way you can have all set to bounds, but keep a small group to be displayed as cloud for certain tasks in the scene.

Again, thanks for doing this! :hugs: