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:
- 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)
- 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?
I really hope some of you can help me with this. It would be greatly appreciated
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()