├── README.md ├── collectionize.py ├── cursor_to_transform.py ├── fix_negative_scale.py ├── old ├── bake_bevel.py ├── load_alphas.py ├── mesh_curves.py ├── triplanar_uv.py ├── uv_area.py ├── vdb_remesh.py └── vdb_surf.py └── view_active_in_outliner.py /README.md: -------------------------------------------------------------------------------- 1 | # blender-scripts 2 | Miscellanous Blender Python scripts 3 | 4 | | Name | Description | 5 | | ---- | ---- | 6 | | mesh_curves.py | Calculates mesh curvature and outputs the data to vertex colors. Options: red/green, grayscale, inverted | 7 | | bake_bevel.py | Old code to calculate smooth bevels for edges at texture level | 8 | | load_alphas.py | Quick hack to load a folder of images or close them all | 9 | | triplanar_uv.py | Experimenting with UV projection | 10 | | uv_area.py | Calculate used UV area, not counting overlapping | 11 | | vdb_remesh.py | OpenVDB based remeshing | 12 | | collectionize.py | Create a hierarchy of collections or empties based on separator in name (like props_chair_wooden etc) | 13 | | view_active_in_outliner.py | Highlight active selected object in outliner | 14 | | fix_negative_scale.py | Fix negative scale objects for Godot (broken normals) | 15 | -------------------------------------------------------------------------------- /collectionize.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import mathutils as mu 3 | from collections import defaultdict 4 | 5 | bl_info = { 6 | "name": "Collectionize", 7 | "author": "ambi", 8 | "version": (1, 0), 9 | "blender": (3, 1, 0), 10 | "location": "View3D > Object > Collectionize", 11 | "description": "Create collections from object names", 12 | "category": "Object", 13 | } 14 | 15 | # This is pretty much what delete does, maybe later make it do more 16 | 17 | # class FlattenCollection(bpy.types.Operator): 18 | # """Unlink all objects from selected collection and its children, and link 19 | # them to the parent collection, if any""" 20 | 21 | # bl_idname = "object.flatten_collection" 22 | # bl_label = "Destroy collection, link all objects to scene" 23 | # bl_options = {"REGISTER", "UNDO"} 24 | 25 | # def execute(self, context): 26 | # col = context.collection 27 | # if col is None: 28 | # self.report({"WARNING"}, "No collection selected") 29 | # return {"CANCELLED"} 30 | 31 | # parent = context.scene.collection 32 | 33 | # for o in col.all_objects: 34 | # if o.name in col.objects: 35 | # col.objects.unlink(o) 36 | # parent.objects.link(o) 37 | 38 | # return {"FINISHED"} 39 | 40 | 41 | def get_collection_and_objects(self, context): 42 | # create a list of all selected objects 43 | selected_objects = [o for o in context.selected_objects] 44 | if not selected_objects: 45 | # if no objects are selected, use all objects in the active collection 46 | selected_objects = [o for o in context.collection.objects] 47 | if selected_objects: 48 | self.report({"WARNING"}, "No objects selected, using all objects in active collection") 49 | else: 50 | self.report({"WARNING"}, "No objects or collections selected") 51 | return None, None 52 | 53 | # get collection of selected object 54 | parent_collection = selected_objects[0].users_collection[0] 55 | 56 | # if all objects are in the same collection, use that as parent 57 | for o in selected_objects: 58 | if o.users_collection[0] != parent_collection: 59 | parent_collection = None 60 | break 61 | 62 | if parent_collection is None: 63 | parent_collection = context.scene.collection 64 | 65 | return parent_collection, selected_objects 66 | 67 | 68 | class CreateHierarchy(bpy.types.Operator): 69 | """Create object hierarchy with empties from object names, using a separator character 70 | to indicate hierarchy""" 71 | 72 | bl_idname = "object.empty_hierarchy_from_names" 73 | bl_label = "Create hierarchy from object names" 74 | bl_options = {"REGISTER", "UNDO"} 75 | 76 | separator: bpy.props.StringProperty( 77 | name="Separator", 78 | description="Separator character", 79 | default="_", 80 | maxlen=1, 81 | ) 82 | depth: bpy.props.IntProperty( 83 | name="Depth", 84 | description="Maximum depth of hierarchy", 85 | default=3, 86 | min=1, 87 | max=10, 88 | ) 89 | move_to_location: bpy.props.BoolProperty( 90 | name="Move to center", 91 | description="Move all empties to the first contained object's location", 92 | default=True, 93 | ) 94 | 95 | def execute(self, context): 96 | parent_collection, selected_objects = get_collection_and_objects(self, context) 97 | if selected_objects is None: 98 | return {"CANCELLED"} 99 | empties = {} 100 | for o in selected_objects: 101 | # skip all objects that already have a parent 102 | if o.parent is not None: 103 | continue 104 | 105 | parts = o.name.split(self.separator) 106 | parent = None 107 | for i in range(self.depth): 108 | if i >= len(parts): 109 | break 110 | 111 | if parts[i] not in empties: 112 | e = bpy.data.objects.new(parts[i], None) 113 | e.parent = parent 114 | empties[parts[i]] = e 115 | parent_collection.objects.link(e) 116 | 117 | parent = empties[parts[i]] 118 | 119 | o.parent = parent 120 | 121 | # move all empties to the center of the contained objects 122 | if self.move_to_location: 123 | for k, v in empties.items(): 124 | if v.parent is None or len(v.children) == 0: 125 | continue 126 | # csum = sum([o.location for o in v.children], mu.Vector((0, 0, 0))) 127 | # / len(v.children) 128 | # empties[k].location = csum 129 | csum = v.children[0].location.copy() 130 | empties[k].location = csum 131 | for c in v.children: 132 | c.location -= csum 133 | 134 | return {"FINISHED"} 135 | 136 | 137 | class Collectionize(bpy.types.Operator): 138 | """Create collections from object names, using a separator character to indicate hierarchy""" 139 | 140 | bl_idname = "object.collectionize" 141 | bl_label = "Collectionize" 142 | bl_options = {"REGISTER", "UNDO"} 143 | 144 | separator: bpy.props.StringProperty( 145 | name="Separator", 146 | description="Separator character", 147 | default="_", 148 | maxlen=1, 149 | ) 150 | depth: bpy.props.IntProperty( 151 | name="Depth", 152 | description="Depth of collection hierarchy", 153 | default=1, 154 | min=0, 155 | max=10, 156 | ) 157 | 158 | def execute(self, context): 159 | parent_collection, selected_objects = get_collection_and_objects(self, context) 160 | if selected_objects is None: 161 | return {"CANCELLED"} 162 | collections = defaultdict(list) 163 | for o in selected_objects: 164 | parts = o.name.split(self.separator) 165 | if self.depth >= len(parts): 166 | return {"CANCELLED"} 167 | 168 | collections[parts[self.depth]].append(o) 169 | 170 | for k, v in collections.items(): 171 | col = bpy.data.collections.new(k) 172 | parent_collection.children.link(col) 173 | for o in v: 174 | if o.name in parent_collection.objects: 175 | parent_collection.objects.unlink(o) 176 | col.objects.link(o) 177 | 178 | return {"FINISHED"} 179 | 180 | 181 | def menu_func(self, context): 182 | self.layout.separator() 183 | self.layout.operator(Collectionize.bl_idname) 184 | self.layout.operator(CreateHierarchy.bl_idname) 185 | 186 | 187 | # add operator to outliner context menu 188 | def outliner_menu_func(self, context): 189 | self.layout.separator() 190 | self.layout.operator(Collectionize.bl_idname) 191 | 192 | 193 | def register(): 194 | bpy.utils.register_class(Collectionize) 195 | bpy.utils.register_class(CreateHierarchy) 196 | bpy.types.VIEW3D_MT_object.append(menu_func) 197 | bpy.types.OUTLINER_MT_collection.append(outliner_menu_func) 198 | 199 | 200 | def unregister(): 201 | bpy.utils.unregister_class(Collectionize) 202 | bpy.utils.unregister_class(CreateHierarchy) 203 | bpy.types.VIEW3D_MT_object.remove(menu_func) 204 | bpy.types.OUTLINER_MT_collection.remove(outliner_menu_func) 205 | -------------------------------------------------------------------------------- /cursor_to_transform.py: -------------------------------------------------------------------------------- 1 | """ 2 | This program is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | This program is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with this program. If not, see . 14 | """ 15 | 16 | import bpy 17 | import bmesh 18 | import math 19 | import mathutils as mu 20 | import numpy as np 21 | 22 | OLD_DRAW = None 23 | OLD_OBJECT_ADD = None 24 | 25 | bl_info = { 26 | "name": "3D Cursor Enhancements", 27 | "author": "Tommi Hyppänen (ambi)", 28 | "version": (1, 0, 6), 29 | "blender": (2, 80, 0), 30 | "location": "View3D > Sidebar > View > 3D Cursor", 31 | "description": "Various tools to use 3D cursor rotation for more purposes.", 32 | "category": "Object", 33 | } 34 | 35 | 36 | def rotate_object(obj, q, point): 37 | R = q.to_matrix().to_4x4() 38 | T = mu.Matrix.Translation(point) 39 | M = T @ R @ T.inverted() 40 | obj.location = M @ obj.location 41 | obj.rotation_euler.rotate(M) 42 | 43 | 44 | class TFC_OT_SeriousTableFlip(bpy.types.Operator): 45 | bl_idname = "object.serious_table_flip" 46 | bl_label = "Serious table flip" 47 | bl_description = "Transform all selected objects according to the 3D cursor" 48 | bl_options = {"REGISTER", "UNDO"} 49 | 50 | def execute(self, context): 51 | mode = context.object.mode 52 | bpy.ops.object.mode_set(mode="OBJECT") 53 | 54 | to_qt = context.scene.cursor.rotation_euler.to_quaternion() 55 | to_qt.invert() 56 | 57 | to_tf = context.scene.cursor.location 58 | 59 | for obj in context.selected_objects: 60 | rotate_object(obj, to_qt, to_tf) 61 | obj.location -= to_tf 62 | 63 | context.scene.cursor.location = mu.Vector((0, 0, 0)) 64 | context.scene.cursor.rotation_euler = mu.Vector((0, 0, 0)) 65 | 66 | bpy.ops.object.mode_set(mode=mode) 67 | 68 | return {"FINISHED"} 69 | 70 | 71 | class TFC_OT_TransformFromCursor(bpy.types.Operator): 72 | bl_idname = "object.transform_from_cursor" 73 | bl_label = "Transform from cursor" 74 | bl_description = "Copy 3D cursor rotation to object transform rotation" 75 | bl_options = {"REGISTER", "UNDO"} 76 | 77 | def execute(self, context): 78 | mode = context.object.mode 79 | bpy.ops.object.mode_set(mode="OBJECT") 80 | 81 | bpy.ops.object.transform_apply(location=False, rotation=True, scale=False) 82 | # obj = context.active_object 83 | 84 | to_qt = context.scene.cursor.rotation_euler.to_quaternion() 85 | 86 | to_qt.invert() 87 | context.active_object.rotation_euler = to_qt.to_euler() 88 | bpy.ops.object.transform_apply(location=False, rotation=True, scale=False) 89 | to_qt.invert() 90 | 91 | context.active_object.rotation_euler = to_qt.to_euler() 92 | 93 | bpy.ops.object.mode_set(mode=mode) 94 | 95 | return {"FINISHED"} 96 | 97 | 98 | # classes = (TFC_OT_TransformFromCursor, TFC_OT_SeriousTableFlip) 99 | classes = (TFC_OT_TransformFromCursor, ) 100 | 101 | 102 | def register(): 103 | for c in classes: 104 | bpy.utils.register_class(c) 105 | 106 | bpy.types.Scene.new_objects_to_cursor = bpy.props.BoolProperty(default=False) 107 | bpy.types.Scene.new_objects_to_cursor_offset = bpy.props.FloatProperty(default=0.0, min=-100.0, max=100.0) 108 | 109 | # ----- HACK: 3D cursor panel additions ----- 110 | global OLD_DRAW 111 | if OLD_DRAW is None: 112 | OLD_DRAW = bpy.types.VIEW3D_PT_view3d_cursor.draw 113 | 114 | def draw(self, context): 115 | layout = self.layout 116 | 117 | cursor = context.scene.cursor 118 | 119 | layout.column().prop(cursor, "location", text="Location") 120 | rotation_mode = cursor.rotation_mode 121 | if rotation_mode == "QUATERNION": 122 | layout.column().prop(cursor, "rotation_quaternion", text="Rotation") 123 | elif rotation_mode == "AXIS_ANGLE": 124 | layout.column().prop(cursor, "rotation_axis_angle", text="Rotation") 125 | else: 126 | layout.column().prop(cursor, "rotation_euler", text="Rotation") 127 | 128 | layout.prop(cursor, "rotation_mode", text="") 129 | 130 | row = layout.row() 131 | row.operator(TFC_OT_TransformFromCursor.bl_idname, text="Copy to object") 132 | 133 | # row = layout.row() 134 | # row.operator(TFC_OT_SeriousTableFlip.bl_idname, text="Serious table flip") 135 | 136 | # row = layout.row() 137 | # row.prop(context.scene, "new_objects_to_cursor", text="Align new objects to cursor") 138 | 139 | # if context.scene.new_objects_to_cursor is True: 140 | # row = layout.row() 141 | # row.prop(context.scene, "new_objects_to_cursor_offset", text="Surface offset") 142 | 143 | bpy.types.VIEW3D_PT_view3d_cursor.draw = draw 144 | 145 | # # ----- HACK: Object add menu additions ----- 146 | # global OLD_OBJECT_ADD 147 | # if OLD_OBJECT_ADD is None: 148 | # OLD_OBJECT_ADD = bpy.types.VIEW3D_MT_mesh_add.draw 149 | 150 | # def draw(self, context): 151 | # layout = self.layout 152 | 153 | # layout.operator_context = "INVOKE_REGION_WIN" 154 | 155 | # if context.scene.new_objects_to_cursor is True: 156 | # oft = context.scene.new_objects_to_cursor_offset 157 | # step_1 = mu.Vector((0.0, 0.0, 1.0)) 158 | # cursor_rotation = context.scene.cursor.rotation_euler 159 | # cursor_matrix = cursor_rotation.to_matrix() 160 | 161 | # def op_sizes(): 162 | # sizes = { 163 | # "plane": 2, 164 | # "cube": 2, 165 | # "circle": 1, 166 | # "uv_sphere": 1, 167 | # "ico_sphere": 1, 168 | # "cylinder": 2, 169 | # "cone": 2, 170 | # "torus": 1, 171 | # "grid": 2, 172 | # "monkey": 2, 173 | # } 174 | 175 | # for entry in sizes: 176 | # op_name = "MESH_OT_primitive_" + entry + "_add" 177 | # op = bpy.data.window_managers[0].operator_properties_last(op_name) 178 | # if op is not None: 179 | # if entry in ["plane", "cube", "grid", "monkey"]: 180 | # sizes[entry] = op.size / sizes[entry] 181 | # if entry in ["circle", "uv_sphere", "ico_sphere"]: 182 | # sizes[entry] = op.radius / sizes[entry] 183 | # if entry in ["cylinder", "cone"]: 184 | # sizes[entry] = op.depth / sizes[entry] 185 | # if entry == "torus": 186 | # sizes[entry] = op.major_radius / sizes[entry] 187 | 188 | # return sizes 189 | 190 | # sz = op_sizes() 191 | # # print(sz) 192 | # # print(bpy.data.window_managers[0].operators.keys()) 193 | # # print(bpy.data.window_managers[0].operators) 194 | 195 | # op = layout.operator("mesh.primitive_plane_add", text="Plane", icon="MESH_PLANE") 196 | # op.rotation = cursor_rotation 197 | # op.location = context.scene.cursor.location + cursor_matrix @ step_1 * oft * sz["plane"] 198 | # op = layout.operator("mesh.primitive_cube_add", text="Cube", icon="MESH_CUBE") 199 | # op.rotation = cursor_rotation 200 | # op.location = context.scene.cursor.location + cursor_matrix @ step_1 * oft * sz["cube"] 201 | # op = layout.operator("mesh.primitive_circle_add", text="Circle", icon="MESH_CIRCLE") 202 | # op.rotation = cursor_rotation 203 | # op.location = context.scene.cursor.location + cursor_matrix @ step_1 * oft * sz["circle"] 204 | # op = layout.operator("mesh.primitive_uv_sphere_add", text="UV Sphere", icon="MESH_UVSPHERE") 205 | # op.rotation = cursor_rotation 206 | # op.location = context.scene.cursor.location + cursor_matrix @ step_1 * oft * sz["uv_sphere"] 207 | # op = layout.operator("mesh.primitive_ico_sphere_add", text="Ico Sphere", icon="MESH_ICOSPHERE") 208 | # op.rotation = cursor_rotation 209 | # op.location = context.scene.cursor.location + cursor_matrix @ step_1 * oft * sz["ico_sphere"] 210 | # op = layout.operator("mesh.primitive_cylinder_add", text="Cylinder", icon="MESH_CYLINDER") 211 | # op.rotation = cursor_rotation 212 | # op.location = context.scene.cursor.location + cursor_matrix @ step_1 * oft * sz["cylinder"] 213 | # op = layout.operator("mesh.primitive_cone_add", text="Cone", icon="MESH_CONE") 214 | # op.rotation = cursor_rotation 215 | # op.location = context.scene.cursor.location + cursor_matrix @ step_1 * oft * sz["cone"] 216 | # op = layout.operator("mesh.primitive_torus_add", text="Torus", icon="MESH_TORUS") 217 | # op.rotation = cursor_rotation 218 | # op.location = context.scene.cursor.location + cursor_matrix @ step_1 * oft * sz["torus"] 219 | 220 | # layout.separator() 221 | 222 | # op = layout.operator("mesh.primitive_grid_add", text="Grid", icon="MESH_GRID") 223 | # op.rotation = cursor_rotation 224 | # op.location = context.scene.cursor.location + cursor_matrix @ step_1 * oft * sz["grid"] 225 | # op = layout.operator("mesh.primitive_monkey_add", text="Monkey", icon="MESH_MONKEY") 226 | # op.rotation = cursor_rotation 227 | # op.location = context.scene.cursor.location + cursor_matrix @ step_1 * oft * sz["monkey"] 228 | 229 | # else: 230 | # op = layout.operator("mesh.primitive_plane_add", text="Plane", icon="MESH_PLANE") 231 | # op = layout.operator("mesh.primitive_cube_add", text="Cube", icon="MESH_CUBE") 232 | # op = layout.operator("mesh.primitive_circle_add", text="Circle", icon="MESH_CIRCLE") 233 | # op = layout.operator("mesh.primitive_uv_sphere_add", text="UV Sphere", icon="MESH_UVSPHERE") 234 | # op = layout.operator("mesh.primitive_ico_sphere_add", text="Ico Sphere", icon="MESH_ICOSPHERE") 235 | # op = layout.operator("mesh.primitive_cylinder_add", text="Cylinder", icon="MESH_CYLINDER") 236 | # op = layout.operator("mesh.primitive_cone_add", text="Cone", icon="MESH_CONE") 237 | # op = layout.operator("mesh.primitive_torus_add", text="Torus", icon="MESH_TORUS") 238 | 239 | # layout.separator() 240 | 241 | # op = layout.operator("mesh.primitive_grid_add", text="Grid", icon="MESH_GRID") 242 | # op = layout.operator("mesh.primitive_monkey_add", text="Monkey", icon="MESH_MONKEY") 243 | 244 | # bpy.types.VIEW3D_MT_mesh_add.draw = draw 245 | 246 | 247 | def unregister(): 248 | for c in reversed(classes): 249 | bpy.utils.unregister_class(c) 250 | 251 | del bpy.types.Scene.new_objects_to_cursor 252 | del bpy.types.Scene.new_objects_to_cursor_offset 253 | 254 | global OLD_DRAW 255 | if OLD_DRAW is not None: 256 | bpy.types.VIEW3D_PT_view3d_cursor.draw = OLD_DRAW 257 | 258 | # global OLD_OBJECT_ADD 259 | # if OLD_OBJECT_ADD is not None: 260 | # bpy.types.VIEW3D_MT_mesh_add.draw = OLD_OBJECT_ADD 261 | 262 | 263 | if __name__ == "__main__": 264 | register() 265 | -------------------------------------------------------------------------------- /fix_negative_scale.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "Fix Objects with Negative Scale", 3 | "author": "ambi", 4 | "version": (1, 0), 5 | "blender": (3, 1, 0), 6 | "location": "View3D > Object > Select > Fix Negative Scale", 7 | "description": "Selects all objects in the scene with negative scale and applies the transform", 8 | "category": "Object", 9 | } 10 | 11 | import bpy 12 | 13 | 14 | class OBJECT_OT_fix_negative_scale(bpy.types.Operator): 15 | bl_idname = "object.fix_negative_scale" 16 | bl_label = "Fix Negative Scale" 17 | bl_options = {"REGISTER", "UNDO"} 18 | 19 | def execute(self, context): 20 | mirrored_mesh = {} 21 | objs = bpy.context.scene.objects 22 | 23 | for obj in objs: 24 | count = int(obj.scale.x < 0) + int(obj.scale.y < 0) + int(obj.scale.z < 0) 25 | if count % 2 == 1: 26 | if obj.data not in mirrored_mesh: 27 | new_mesh = obj.data.copy() 28 | for v in new_mesh.vertices: 29 | v.co.x *= -1.0 30 | mirrored_mesh[obj.data] = new_mesh 31 | 32 | for obj in objs: 33 | count = int(obj.scale.x < 0) + int(obj.scale.y < 0) + int(obj.scale.z < 0) 34 | if count % 2 == 1: 35 | obj.data = mirrored_mesh[obj.data] 36 | obj.scale.x = -obj.scale.x 37 | obj.select_set(True) 38 | else: 39 | obj.select_set(False) 40 | 41 | if len(mirrored_mesh) > 0: 42 | self.report({"INFO"}, "Created new meshes: %d" % len(mirrored_mesh)) 43 | else: 44 | self.report({"INFO"}, "No negative scale objects found") 45 | 46 | return {"FINISHED"} 47 | 48 | 49 | def menu_func(self, context): 50 | self.layout.operator(OBJECT_OT_fix_negative_scale.bl_idname) 51 | 52 | 53 | def register(): 54 | bpy.utils.register_class(OBJECT_OT_fix_negative_scale) 55 | bpy.types.VIEW3D_MT_object.append(menu_func) 56 | 57 | 58 | def unregister(): 59 | bpy.utils.unregister_class(OBJECT_OT_fix_negative_scale) 60 | bpy.types.VIEW3D_MT_object.remove(menu_func) 61 | 62 | 63 | if __name__ == "__main__": 64 | register() 65 | -------------------------------------------------------------------------------- /old/bake_bevel.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import numpy 3 | import math 4 | 5 | print("bevelshader: hello") 6 | 7 | class BevelShader: 8 | def init_images(self, context): 9 | self.input_image = context.scene.seamless_input_image 10 | self.target_image = context.scene.seamless_generated_name 11 | 12 | print("Creating images...") 13 | self.size = bpy.data.images[self.input_image].size 14 | self.xs = self.size[0] 15 | self.ys = self.size[1] 16 | 17 | # copy image data into much more performant numpy arrays 18 | self.pixels = numpy.array(bpy.data.images[self.input_image].pixels) 19 | self.pixels = self.pixels.reshape((self.ys, self.xs, 4)) 20 | 21 | # if target image exists, change the size to fit 22 | if self.target_image in bpy.data.images: 23 | bpy.data.images[self.target_image].scale(self.xs, self.ys) 24 | self.image = bpy.data.images[self.target_image] 25 | else: 26 | self.image = bpy.data.images.new(self.target_image, width=self.xs, height=self.ys) 27 | 28 | self.pixels[:, :, 3] = 1.0 # alpha is always 1.0 everywhere 29 | 30 | def finish_images(self): 31 | self.image.pixels = self.pixels.flatten() 32 | #bpy.ops.image.invert(invert_r=False, invert_g=False, invert_b=False, invert_a=False) 33 | 34 | def fast_gaussian(self, s): 35 | d = int(2 ** s) 36 | tpx = self.pixels 37 | ystep = tpx.shape[1] 38 | while d > 1: 39 | tpx = (tpx * 2.0 + numpy.roll(tpx, -d * 4) + numpy.roll(tpx, d * 4)) / 4.0 40 | tpx = (tpx * 2.0 + numpy.roll(tpx, -d * (ystep * 4)) + numpy.roll(tpx, d * (ystep * 4))) / 4.0 41 | d = int(d / 2.0) 42 | self.pixels = tpx 43 | 44 | def convolution(self, intens, sfil): 45 | # source, intensity, convolution matrix 46 | ssp = self.pixels 47 | tpx = numpy.zeros(ssp.shape, dtype=float) 48 | tpx[:, :, 3] = 1.0 49 | ystep = int(4 * ssp.shape[1]) 50 | norms = 0 51 | for y in range(sfil.shape[0]): 52 | for x in range(sfil.shape[1]): 53 | tpx += numpy.roll(ssp, (x - int(sfil.shape[1] / 2)) * 4 + (y - int(sfil.shape[0] / 2)) * ystep) * sfil[y, x] 54 | norms += sfil[y, x] 55 | if norms > 0: 56 | tpx /= norms 57 | return ssp + (tpx - ssp) * intens 58 | 59 | def blur(self, s, intensity): 60 | self.pixels = self.convolution(intensity, numpy.ones((1 + s * 2, 1 + s * 2), dtype=float)) 61 | 62 | def inside_tri(self, pt, v1, v2, v3): 63 | def signn(p1, p2, p3): 64 | return (p1[0]-p3[0]) * (p2[1]-p3[1]) - (p2[0]-p3[0]) * (p1[1]-p3[1]) 65 | 66 | b1 = signn(pt, v1, v2) < 0.0 67 | b2 = signn(pt, v2, v3) < 0.0 68 | b3 = signn(pt, v3, v1) < 0.0 69 | 70 | return (b1 == b2) and (b2 == b3) 71 | 72 | def draw_point(self, tpx, xloc, yloc, cval, tri): 73 | #if self.inside_tri((xloc, yloc), tri[0], tri[1], tri[2]): 74 | tpx[yloc % self.ys, xloc % self.xs,:] = cval 75 | 76 | def draw_bevels(self): 77 | 78 | # ov.data.use_auto_smooth 79 | # ob.data.edges[0].use_edge_sharp 80 | 81 | print("draw bevels") 82 | tpx = self.pixels 83 | 84 | ob = bpy.context.object 85 | uv_layer = ob.data.uv_layers.active.data 86 | 87 | # map edge to index 88 | edgemap = {} 89 | for i, edge in enumerate(ob.data.edges): 90 | edgemap[(edge.vertices[0], edge.vertices[1])] = i #edge.use_edge_sharp 91 | 92 | # list of faces connected by an edge 93 | edgesharp = {} 94 | connecting_edges = {} 95 | for polyid, poly in enumerate(ob.data.polygons): 96 | for edge in poly.edge_keys: 97 | if not edge in connecting_edges: 98 | connecting_edges[edge] = [polyid] 99 | else: 100 | connecting_edges[edge].append(polyid) 101 | 102 | getedge = ob.data.edges[edgemap[edge] if edge in edgemap else edgemap[(edge[1], edge[0])]] 103 | edgesharp[edge] = getedge.use_edge_sharp 104 | 105 | step = 1.0/float(self.xs) 106 | for polyid, poly in enumerate(ob.data.polygons): 107 | uvs = [uv_layer[i].uv for i in poly.loop_indices] 108 | tri_center = (sum(i[0] for i in uvs)/len(uvs), sum(i[1] for i in uvs)/len(uvs)) 109 | for i, uv in enumerate(uvs): 110 | if edgesharp[poly.edge_keys[i]]: #poly.edge_keys[i] in edgesharp and edgesharp[poly.edge_keys[i]]: 111 | a = uv 112 | b = uvs[(i+1)%len(uvs)] 113 | 114 | new_tri = numpy.array([a, b, tri_center]) 115 | new_tri[:,0] *= self.xs 116 | new_tri[:,1] *= self.ys 117 | 118 | xdif = b[0]-a[0] 119 | ydif = b[1]-a[1] 120 | 121 | ablen = math.sqrt(xdif**2.0 + ydif**2.0) 122 | 123 | # generate edge normal 124 | faces = connecting_edges[poly.edge_keys[i]] 125 | cval = numpy.zeros((4), dtype=numpy.float16) 126 | for f in faces: 127 | cval[:3] += numpy.array(ob.data.polygons[f].normal) 128 | cval[:3] /= numpy.sqrt(cval[0]**2 + cval[1]**2 + cval[2]**2) 129 | cval[:3] = (cval[:3]+1.0)/2.0 130 | cval[3] = 1.0 131 | 132 | for l in numpy.arange(0, ablen, step): 133 | x = a[0] + xdif * float(l) / ablen 134 | y = a[1] + ydif * float(l) / ablen 135 | xloc = int(x*float(self.xs)) 136 | yloc = int(y*float(self.ys)) 137 | 138 | self.draw_point(tpx, xloc , yloc , cval, new_tri) 139 | self.draw_point(tpx, xloc+1, yloc , cval, new_tri) 140 | self.draw_point(tpx, xloc-1, yloc , cval, new_tri) 141 | self.draw_point(tpx, xloc , yloc+1, cval, new_tri) 142 | self.draw_point(tpx, xloc , yloc-1, cval, new_tri) 143 | 144 | self.pixels = tpx 145 | 146 | 147 | bshader = BevelShader() 148 | bshader.init_images(bpy.context) 149 | bshader.draw_bevels() 150 | bshader.blur(3, 1.0) 151 | bshader.draw_bevels() 152 | bshader.blur(2, 1.0) 153 | bshader.finish_images() 154 | 155 | print("bevelshader: goodbye") -------------------------------------------------------------------------------- /old/load_alphas.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Tommi Hyppänen 2 | # License: GPL 2 3 | 4 | bl_info = { 5 | "name": "Brush Texture Autoload", 6 | "category": "Paint", 7 | "description": "Autoloading of brush textures from a folder", 8 | "author": "Tommi Hyppänen (ambi)", 9 | "location": "3D view > Tool Shelf > Tools > Texture Autoload", 10 | "documentation": "community", 11 | "version": (0, 0, 1), 12 | "blender": (2, 76, 0) 13 | } 14 | 15 | import bpy 16 | import os 17 | 18 | class AlphasLoadOperator(bpy.types.Operator): 19 | """Alpha Autoloading Operator""" 20 | bl_idname = "texture.alphas_batch_load" 21 | bl_label = "Autoload Texture Alphas" 22 | 23 | @classmethod 24 | def poll(cls, context): 25 | return True 26 | 27 | def execute(self, context): 28 | print("path:"+context.scene.alphas_location) 29 | print("abspath:"+bpy.path.abspath(context.scene.alphas_location)) 30 | mypath = bpy.path.abspath(context.scene.alphas_location) 31 | dirfiles = [f for f in os.listdir(mypath) if os.path.isfile(os.path.join(mypath, f))] 32 | oktypes = set([".png", ".jpg"]) 33 | okfiles = [f for f in dirfiles if f[-4:].lower() in oktypes] 34 | print(okfiles) 35 | 36 | for item in okfiles: 37 | fullname = os.path.join(mypath, item) 38 | tex = bpy.data.textures.new(item[:50]+'.autoload', type='IMAGE') 39 | tex.image = bpy.data.images.load(fullname) 40 | tex.use_alpha = True 41 | 42 | return {'FINISHED'} 43 | 44 | class AlphasRemoveAllOperator(bpy.types.Operator): 45 | """Alpha Autoloading Remove All Operator""" 46 | bl_idname = "texture.alphas_batch_removeall" 47 | bl_label = "Autoremove Texture Alphas" 48 | 49 | @classmethod 50 | def poll(cls, context): 51 | return True 52 | 53 | def execute(self, context): 54 | remove_these = [i for i in bpy.data.textures.keys() if 'autoload' in i.split('.')] 55 | for item in remove_these: 56 | tex = bpy.data.textures[item] 57 | img = tex.image 58 | if not tex.users: 59 | bpy.data.textures.remove(tex) 60 | img.user_clear() 61 | if not img.users: 62 | bpy.data.images.remove(img) 63 | 64 | return {'FINISHED'} 65 | 66 | class TextureAutoloadPanel(bpy.types.Panel): 67 | """Creates a Panel for Texture Autoload addon""" 68 | bl_label = "Texture Autoload" 69 | bl_space_type = 'VIEW_3D' 70 | bl_region_type = 'TOOLS' 71 | bl_category = "Tools" 72 | 73 | def draw(self, context): 74 | layout = self.layout 75 | obj = context.object 76 | row = layout.row() 77 | row.prop(context.scene, "alphas_location", text="Alphas Location") 78 | row = layout.row() 79 | row.operator(AlphasLoadOperator.bl_idname) 80 | row = layout.row() 81 | row.operator(AlphasRemoveAllOperator.bl_idname) 82 | 83 | def register(): 84 | contexts = ["imagepaint", "sculpt_mode", "vertexpaint"] 85 | 86 | bpy.utils.register_class(AlphasLoadOperator) 87 | bpy.utils.register_class(AlphasRemoveAllOperator) 88 | 89 | for c in contexts: 90 | propdic = {"bl_idname": "texautoloadpanel.%s" % c, "bl_context": c,} 91 | MyPanel = type("TextureAutoloadPanel_%s" % c, (TextureAutoloadPanel,), propdic) 92 | bpy.utils.register_class(MyPanel) 93 | 94 | bpy.types.Scene.alphas_location = bpy.props.StringProperty( 95 | name="alphas_path", 96 | description="Alphas Location", 97 | subtype='DIR_PATH') 98 | 99 | def unregister(): 100 | contexts = ["imagepaint", "sculpt_mode", "vertexpaint"] 101 | 102 | bpy.utils.unregister_class(AlphasLoadOperator) 103 | bpy.utils.unregister_class(AlphasRemoveAllOperator) 104 | 105 | for c in contexts: 106 | propdic = {"bl_idname": "texautoloadpanel.%s" % c, 107 | "bl_context": c,} 108 | MyPanel = type("TextureAutoloadPanel_%s" % c, (TextureAutoloadPanel,), propdic) 109 | bpy.utils.unregister_class(MyPanel) 110 | 111 | del bpy.types.Scene.alphas_location 112 | 113 | 114 | if __name__ == "__main__": 115 | register() 116 | -------------------------------------------------------------------------------- /old/mesh_curves.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # ##### BEGIN GPL LICENSE BLOCK ##### 4 | # 5 | # This program is free software; you can redistribute it and/or 6 | # modify it under the terms of the GNU General Public License 7 | # as published by the Free Software Foundation; either version 2 8 | # of the License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software Foundation, 17 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | # 19 | # ##### END GPL LICENSE BLOCK ##### 20 | 21 | bl_info = { 22 | "name": "Curvature to vertex colors", 23 | "category": "Object", 24 | "description": "Set object vertex colors according to mesh curvature", 25 | "author": "Tommi Hyppänen (ambi)", 26 | "location": "3D View > Object menu > Curvature to vertex colors", 27 | "version": (0, 1, 7), 28 | "blender": (2, 79, 0) 29 | } 30 | 31 | import bpy 32 | import random 33 | from collections import defaultdict 34 | import mathutils as mu 35 | import math 36 | import bmesh 37 | import numpy as np 38 | import cProfile, pstats, io 39 | 40 | 41 | def read_verts(mesh): 42 | mverts_co = np.zeros((len(mesh.vertices)*3), dtype=np.float) 43 | mesh.vertices.foreach_get("co", mverts_co) 44 | return np.reshape(mverts_co, (len(mesh.vertices), 3)) 45 | 46 | 47 | def read_edges(mesh): 48 | fastedges = np.zeros((len(mesh.edges)*2), dtype=np.int) # [0.0, 0.0] * len(mesh.edges) 49 | mesh.edges.foreach_get("vertices", fastedges) 50 | return np.reshape(fastedges, (len(mesh.edges), 2)) 51 | 52 | 53 | def read_norms(mesh): 54 | mverts_no = np.zeros((len(mesh.vertices)*3), dtype=np.float) 55 | mesh.vertices.foreach_get("normal", mverts_no) 56 | return np.reshape(mverts_no, (len(mesh.vertices), 3)) 57 | 58 | 59 | def read_polygon_verts(mesh): 60 | polys = np.zeros((len(mesh.polygons)*4), dtype=np.uint32) 61 | mesh.polygons.foreach_get("vertices", faces) 62 | return polys 63 | 64 | 65 | def safe_bincount(data, weights, dts, conn): 66 | #for i, v in enumerate(edge_a): 67 | # vecsums[v] += totdot[i] 68 | # connections[v] += 1 69 | bc = np.bincount(data, weights) 70 | dts[:len(bc)] += bc 71 | bc = np.bincount(data) 72 | conn[:len(bc)] += bc 73 | return (dts, conn) 74 | 75 | 76 | def tri_area(a, b, c): 77 | ab = (b - a) 78 | ac = (c - a) 79 | angle = ab.angle(ac) 80 | return ab.length * ac.length * math.sin(angle)/2.0 81 | 82 | v = tri_area(mu.Vector((1,0,0)), mu.Vector((1,1,0)), mu.Vector((0,0,0))) 83 | assert(v > 0.499 and v < 0.501) 84 | 85 | 86 | """ 87 | LAPLACIAN 88 | for(int i : vertices) 89 | { 90 | for(int j : one_ring(i)) 91 | { 92 | for(int k : triangle_on_edge(i,j)) 93 | { 94 | L(i,j) = cot(angle(i,j,k)); 95 | L(i,i) -= cot(angle(i,j,k)); 96 | } 97 | } 98 | } 99 | 100 | for(triangle t : triangles) 101 | { 102 | for(edge i,j : t) 103 | { 104 | L(i,j) += cot(angle(i,j,k)); 105 | L(j,i) += cot(angle(i,j,k)); 106 | L(i,i) -= cot(angle(i,j,k)); 107 | L(j,j) -= cot(angle(i,j,k)); 108 | } 109 | } 110 | """ 111 | 112 | def edgewise_gaussian(mesh): 113 | """ Gaussian from edges """ 114 | bm = bmesh.from_edit_mesh(mesh) 115 | bm.faces.ensure_lookup_table() 116 | bm.edges.ensure_lookup_table() 117 | bm.verts.ensure_lookup_table() 118 | 119 | gaussian = [] 120 | for i, vi in enumerate(bm.verts): 121 | # generate one ring 122 | ring_verts = [e.other_vert(vi) for e in vi.link_edges] 123 | 124 | area = 0 125 | for ri in range(len(ring_verts)): 126 | rv = ring_verts[ri] 127 | area += tri_area(rv.co, ring_verts[(ri+1) % len(ring_verts)].co, vi.co) 128 | #area += (rv.co - vi.co).length 129 | 130 | aG = 0.0 131 | mT = 0.0 132 | for j, vj in enumerate(ring_verts): 133 | m=0.0 134 | next_v = ring_verts[(j+1) % len(ring_verts)] 135 | prev_v = ring_verts[(j-1) % len(ring_verts)] 136 | t0, t1 = next_v.co - vi.co, vj.co - vi.co 137 | 138 | m = (tri_area(vi.co, vj.co, next_v.co) + tri_area(vi.co, vj.co, prev_v.co))/2/area 139 | #m = t1.length / area 140 | mT += m 141 | aG += t0.dot(t1)*m 142 | 143 | #print(mT) 144 | # mT == 1.0 145 | #K = (2*math.pi - aG)/area 146 | K = (2*math.pi - aG)/(math.pi*4) 147 | gaussian.append(K) 148 | 149 | return np.array(gaussian) 150 | 151 | 152 | def laplace_beltrami(mesh): 153 | """ Laplace-Beltrami operator """ 154 | bm = bmesh.from_edit_mesh(mesh) 155 | bm.faces.ensure_lookup_table() 156 | bm.edges.ensure_lookup_table() 157 | bm.verts.ensure_lookup_table() 158 | 159 | print("lp: begin") 160 | 161 | def _cot(a): 162 | return 1.0/math.tan(a) if a!=0 else 1.0 163 | 164 | gaussian = [] 165 | k1 = [] 166 | k2 = [] 167 | rH = [] 168 | 169 | bork = 0 170 | 171 | #print(len(ring_verts), len(vi.link_edges)) 172 | 173 | # test validity 174 | # for rvi, rv in enumerate(ring_verts): 175 | # p_v = ring_verts[(rvi-1) % len(ring_verts)] 176 | # n_v = ring_verts[(rvi+1) % len(ring_verts)] 177 | 178 | # print(rv, rvi) 179 | # buddies = [e.other_vert(rv).index for e in rv.link_edges] 180 | 181 | # if p_v.index not in buddies: 182 | # print("P_V error!", buddies, "|", p_v.index, "| rv:",[e.index for e in ring_verts]) 183 | 184 | # if n_v.index not in buddies: 185 | # print("N_V error!", buddies, "|", n_v.index, "| rv:",[e.index for e in ring_verts]) 186 | 187 | # print("---") 188 | 189 | for i, vi in enumerate(bm.verts): 190 | # generate one ring 191 | ring_verts = [e.other_vert(vi) for e in vi.link_edges] 192 | 193 | if len(ring_verts) < 3: 194 | print("1-ring length error at", i) 195 | 196 | assert(vi in [e.other_vert(ring_verts[0]) for e in ring_verts[0].link_edges]) 197 | 198 | # one ring tri area 199 | area = 0 200 | for ri in range(len(ring_verts)): 201 | rv = ring_verts[ri] 202 | area += tri_area(rv.co, ring_verts[(ri+1) % len(ring_verts)].co, vi.co) 203 | 204 | # temporary (_tri_area doesn't work right) 205 | #area = sum(f.calc_area() for f in vi.link_faces) 206 | #area /= 3.0 207 | 208 | #area = sum(e.calc_length() for e in vi.link_edges) 209 | 210 | aG = 0.0 211 | #H = mu.Vector((0, 0, 0)) 212 | H = 0 213 | 214 | # plane equation for normal: n.dot(x-x0) = 0 215 | x0, y0, z0 = vi.co 216 | A, B, C = vi.normal 217 | D = -A*x0 -B*y0 -C*z0 218 | sqrABC = math.sqrt(A**2 + B**2 + C**2) 219 | 220 | # roll right 221 | # ring_verts = [ring_verts[-1]] + ring_verts[:-1] 222 | 223 | msum = 0 224 | for j, vj in enumerate(ring_verts): 225 | prev_v = ring_verts[(j-1) % len(ring_verts)] 226 | next_v = ring_verts[(j+1) % len(ring_verts)] 227 | 228 | x1, y1, z1 = vj.co 229 | sn = (A*x1 + B*y1 + C*z1 + D)/sqrABC 230 | 231 | t0, t1, t2 = next_v.co - vi.co, vj.co - vi.co, prev_v.co - vi.co 232 | 233 | a = (t2).angle((prev_v.co - vj.co)) 234 | b = (t0).angle((next_v.co - vj.co)) 235 | 236 | m = _cot(a) + _cot(b) 237 | 238 | #m = (_tri_area(vi.co, vj.co, next_v.co) + _tri_area(vi.co, vj.co, prev_v.co))/2/area 239 | fj = t1.length 240 | H += m * t1.length 241 | #H += m * vj.co 242 | 243 | msum += m 244 | 245 | #aG += t0.dot(t1) 246 | aG += t0.dot(t1)/t1.length 247 | 248 | 249 | #K = (2*math.pi - aG)/(area/3) 250 | K = 2*math.pi - aG 251 | gaussian.append(K) 252 | 253 | #H -= vi.co 254 | #H = H.length 255 | H = H/(area/3) 256 | t = H**2 - K 257 | if t<0: 258 | bork += 1 259 | t=0 260 | t = math.sqrt(t) 261 | 262 | k1.append(H + t) 263 | k2.append(H - t) 264 | 265 | rH.append(H) 266 | 267 | 268 | print("bork:",bork,"/",len(bm.verts)) 269 | 270 | print("lp: end") 271 | 272 | return np.array(gaussian), np.array(k1), np.array(k2), np.array(rH) 273 | 274 | 275 | def calc_normals(fastverts, fastnorms, fastedges): 276 | """ Calculates curvature for specified mesh """ 277 | edge_a, edge_b = fastedges[:,0], fastedges[:,1] 278 | 279 | tvec = fastverts[edge_b] - fastverts[edge_a] 280 | tvlen = np.linalg.norm(tvec, axis=1) 281 | 282 | # normalize vectors 283 | tvec = (tvec.T / tvlen).T 284 | 285 | # adjust the minimum of what is processed 286 | edgelength = tvlen * 100 287 | edgelength = np.where(edgelength < 1, 1.0, edgelength) 288 | 289 | vecsums = np.zeros(fastverts.shape[0], dtype=np.float) 290 | connections = np.zeros(fastverts.shape[0], dtype=np.float) 291 | 292 | # calculate normal differences to the edge vector in the first edge vertex 293 | totdot = (np.einsum('ij,ij->i', tvec, fastnorms[edge_a]))/edgelength 294 | safe_bincount(edge_a, totdot, vecsums, connections) 295 | 296 | # calculate normal differences to the edge vector in the second edge vertex 297 | totdot = (np.einsum('ij,ij->i', -tvec, fastnorms[edge_b]))/edgelength 298 | safe_bincount(edge_b, totdot, vecsums, connections) 299 | 300 | # (approximate gaussian) curvature is the average difference of 301 | # edge vectors to surface normals (from dot procuct cosine equation) 302 | #curve = 1.0 - np.arccos(vecsums/connections)/np.pi 303 | 304 | # 1 = max curvature, 0 = min curvature, 0.5 = zero curvature 305 | #curve -= 0.5 306 | #curve /= np.max([np.amax(curve), np.abs(np.amin(curve))]) 307 | #curve += 0.5 308 | return np.arccos(vecsums/connections)/np.pi 309 | 310 | 311 | def average_parameter_vector(data, fastverts, fastedges): 312 | """ Calculates normalized world-space vector that points into the direction of where parameter is higher on average """ 313 | vecsums = np.zeros((fastverts.shape[0], 3), dtype=np.float) 314 | connections = np.zeros(fastverts.shape[0], dtype=np.float) 315 | 316 | for i, e in enumerate(fastedges): 317 | vert_a, vert_b = e 318 | vecsums[vert_a] += (data[vert_b] - data[vert_a]) * (fastverts[vert_b] - fastverts[vert_a]) 319 | vecsums[vert_b] += (data[vert_a] - data[vert_b]) * (fastverts[vert_a] - fastverts[vert_b]) 320 | connections[vert_a] += 1 321 | connections[vert_b] += 1 322 | 323 | divr = connections * np.linalg.norm(vecsums, axis=1) * data 324 | 325 | vecsums[:,0] /= divr 326 | vecsums[:,1] /= divr 327 | vecsums[:,2] /= divr 328 | 329 | return vecsums 330 | 331 | 332 | def mesh_smooth_filter_variable(data, fastverts, fastedges, iterations): 333 | """ Smooths variables in data [-1, 1] over the mesh topology """ 334 | # vert indices of edges 335 | edge_a, edge_b = fastedges[:,0], fastedges[:,1] 336 | tvlen = np.linalg.norm(fastverts[edge_b] - fastverts[edge_a], axis=1) 337 | edgelength = np.where(tvlen<1, 1.0, tvlen) 338 | 339 | data_sums = np.zeros(fastverts.shape[0], dtype=np.float) 340 | connections = np.zeros(fastverts.shape[0], dtype=np.float) 341 | 342 | # longer the edge distance to datapoint, less it has influence 343 | 344 | for _ in range(iterations): 345 | # step 1 346 | data_sums = np.zeros(data_sums.shape) 347 | connections = np.zeros(connections.shape) 348 | 349 | per_vert = data[edge_b]/edgelength 350 | safe_bincount(edge_a, per_vert, data_sums, connections) 351 | eb_smooth = data_sums/connections 352 | 353 | per_vert = eb_smooth[edge_a]/edgelength 354 | safe_bincount(edge_b, per_vert, data_sums, connections) 355 | 356 | new_data = data_sums/connections 357 | 358 | # step 2 359 | data_sums = np.zeros(data_sums.shape) 360 | connections = np.zeros(connections.shape) 361 | 362 | per_vert = data[edge_a]/edgelength 363 | safe_bincount(edge_b, per_vert, data_sums, connections) 364 | ea_smooth = data_sums/connections 365 | 366 | per_vert = ea_smooth[edge_b]/edgelength 367 | safe_bincount(edge_a, per_vert, data_sums, connections) 368 | 369 | new_data += data_sums/connections 370 | 371 | # limit between -1 and 1 372 | #new_data /= np.max([np.amax(new_data), np.abs(np.amin(new_data))]) 373 | #data = new_data 374 | 375 | return new_data 376 | 377 | 378 | def write_colors(values, mesh): 379 | # Use 'curvature' vertex color entry for results 380 | if "Curvature" not in mesh.vertex_colors: 381 | mesh.vertex_colors.new(name="Curvature") 382 | 383 | color_layer = mesh.vertex_colors['Curvature'] 384 | mesh.vertex_colors["Curvature"].active = True 385 | 386 | print("writing vertex colors for array:", values.shape) 387 | 388 | # write vertex colors 389 | mloops = np.zeros((len(mesh.loops)), dtype=np.int) 390 | mesh.loops.foreach_get("vertex_index", mloops) 391 | color_layer.data.foreach_set("color", values[mloops].flatten()) 392 | 393 | 394 | class CurvatureOperator(bpy.types.Operator): 395 | """Curvature to vertex colors""" 396 | bl_idname = "object.vertex_colors_curve" 397 | bl_label = "Curvature to vertex colors" 398 | bl_options = {'REGISTER', 'UNDO'} 399 | 400 | typesel = bpy.props.EnumProperty( 401 | items=[ 402 | ("RED", "Red/Green", "", 1), 403 | ("GREY", "Grayscale", "", 2), 404 | ("GREYC", "Grayscale combined", "", 3), 405 | ("DIRECTION", "Direction", "", 4), 406 | ("THRESHOLD", "Threshold", "", 5), 407 | ("LAPLACIAN", "Laplace-Beltrami", "", 6), 408 | ], 409 | name="Output style", 410 | default="LAPLACIAN") 411 | 412 | concavity = bpy.props.BoolProperty( 413 | name="Concavity", 414 | default=True, 415 | options={'HIDDEN'}) 416 | convexity = bpy.props.BoolProperty( 417 | name="Convexity", 418 | default=True, 419 | options={'HIDDEN'}) 420 | 421 | def curveUpdate(self, context): 422 | if self.curvesel == "CAVITY": 423 | self.concavity = True 424 | self.convexity = False 425 | if self.curvesel == "VEXITY": 426 | self.concavity = False 427 | self.convexity = True 428 | if self.curvesel == "BOTH": 429 | self.concavity = True 430 | self.convexity = True 431 | 432 | curvesel = bpy.props.EnumProperty( 433 | items=[ 434 | ("CAVITY", "Concave", "", 1), 435 | ("VEXITY", "Convex", "", 2), 436 | ("BOTH", "Both", "", 3), 437 | ], 438 | name="Curvature type", 439 | default="BOTH", 440 | update=curveUpdate) 441 | 442 | intensity_multiplier = bpy.props.FloatProperty( 443 | name="Intensity Multiplier", 444 | min=0.0, 445 | default=1.0) 446 | 447 | smooth = bpy.props.IntProperty( 448 | name="Smoothing steps", 449 | min=0, 450 | max=200, 451 | default=2) 452 | 453 | invert = bpy.props.BoolProperty( 454 | name="Invert", 455 | default=False) 456 | 457 | @classmethod 458 | def poll(cls, context): 459 | ob = context.active_object 460 | return ob is not None and ob.mode == 'OBJECT' 461 | 462 | def set_colors(self, mesh, fvals): 463 | retvalues = np.ones((len(fvals), 4)) 464 | 465 | if self.typesel == "GREY": 466 | splitter = fvals>0.5 467 | a_part = splitter * (fvals*2-1)*self.concavity 468 | b_part = np.logical_not(splitter) * (1-fvals*2)*self.convexity 469 | fvals = a_part + b_part 470 | fvals *= self.intensity_multiplier 471 | if self.invert: 472 | fvals = 1.0 - fvals 473 | 474 | retvalues[:,0] = fvals 475 | retvalues[:,1] = fvals 476 | retvalues[:,2] = fvals 477 | 478 | if self.typesel == "GREYC": 479 | if not self.convexity: 480 | fvals = np.where(fvals<0.5, 0.5, fvals) 481 | if not self.concavity: 482 | fvals = np.where(fvals>0.5, 0.5, fvals) 483 | if not self.invert: 484 | fvals = 1.0 - fvals 485 | fvals = (fvals-0.5)*self.intensity_multiplier+0.5 486 | retvalues[:,0] = fvals 487 | retvalues[:,1] = fvals 488 | retvalues[:,2] = fvals 489 | 490 | if self.typesel == "RED": 491 | splitter = fvals>0.5 492 | a_part = splitter * (fvals*2-1)*self.concavity 493 | b_part = np.logical_not(splitter) * (1-fvals*2)*self.convexity 494 | if self.invert: 495 | retvalues[:,0] = 1.0 - a_part * self.intensity_multiplier 496 | retvalues[:,1] = 1.0 - b_part * self.intensity_multiplier 497 | else: 498 | retvalues[:,0] = a_part * self.intensity_multiplier 499 | retvalues[:,1] = b_part * self.intensity_multiplier 500 | retvalues[:,2] = np.zeros((len(fvals))) 501 | 502 | write_colors(retvalues, mesh) 503 | 504 | 505 | def execute(self, context): 506 | mesh = context.active_object.data 507 | fastverts = read_verts(mesh) 508 | fastedges = read_edges(mesh) 509 | fastnorms = read_norms(mesh) 510 | 511 | values = calc_normals(fastverts, fastnorms, fastedges) 512 | if self.smooth > 0: 513 | values = mesh_smooth_filter_variable(values, fastverts, fastedges, self.smooth) 514 | 515 | if self.typesel == "DIRECTION": 516 | values = average_parameter_vector(values, fastverts, fastedges) * self.intensity_multiplier 517 | values = (values+1)/2 518 | colors = np.ones((len(values), 4)) 519 | colors[:,0] = values[:,0] 520 | colors[:,1] = values[:,1] 521 | colors[:,2] = values[:,2] 522 | write_colors(colors, mesh) 523 | 524 | elif self.typesel == "THRESHOLD": 525 | values2 = calc_normals(fastverts, fastnorms, fastedges) 526 | if self.smooth > 0: 527 | values2 = mesh_smooth_filter_variable(values2, fastverts, fastedges, self.smooth * int(2.0*self.intensity_multiplier)) 528 | 529 | # make into 0=black 1=white 530 | values2 = 1.0-values2 531 | values = 1.0-values 532 | 533 | values = np.where(values > 0.52, values-values2, 0.0) 534 | values = np.where(values > 0.0, 1.0, 0.0) 535 | colors = np.ones((len(values), 4)) 536 | colors[:,0] = values 537 | colors[:,1] = values 538 | colors[:,2] = values 539 | write_colors(colors, mesh) 540 | 541 | elif self.typesel == "LAPLACIAN": 542 | bpy.ops.object.mode_set(mode='EDIT') 543 | # K, k1, k2, H = laplace_beltrami(mesh) 544 | 545 | # print("Gaussian:",np.min(K), np.max(K)) 546 | # print("H:",np.min(H), np.max(H)) 547 | # print("K1:",np.min(k1), np.max(k1)) 548 | # print("K2:",np.min(k2), np.max(k2)) 549 | 550 | colors = np.ones((len(values), 4)) 551 | # colors[:,0] = gaussian 552 | # colors[:,1] = gaussian 553 | # colors[:,2] = gaussian 554 | 555 | 556 | # k1 = H - sqrt(H**2 - K) 557 | # k2 = H + sqrt(H**2 - K) 558 | 559 | # divergence of surface normals is -2Hn => H = divg / (-2n) 560 | H = calc_normals(fastverts, fastnorms, fastedges) 561 | H = mesh_smooth_filter_variable(H, fastverts, fastedges, 1) 562 | K = edgewise_gaussian(mesh) 563 | K = mesh_smooth_filter_variable(K, fastverts, fastedges, 1) 564 | 565 | if np.min(H**2) < np.max(K): 566 | print("WARNING: normalized Gaussian") 567 | print("Gaussian:",np.min(K), np.max(K)) 568 | max_K = np.min(H**2) 569 | K /= np.max(K) 570 | K *= max_K 571 | 572 | k1 = H + np.sqrt(H**2 - K) 573 | k2 = H - np.sqrt(H**2 - K) 574 | 575 | print("Gaussian:",np.min(K), np.max(K)) 576 | print("H:",np.min(H), np.max(H)) 577 | print("K1:",np.min(k1), np.max(k1)) 578 | print("K2:",np.min(k2), np.max(k2)) 579 | 580 | K -= np.min(K) 581 | K /= np.max(K) 582 | K *= self.intensity_multiplier 583 | 584 | H -= np.min(H) 585 | H /= np.max(H) 586 | H *= self.intensity_multiplier 587 | 588 | k1 -= np.min(k1) 589 | k1 /= np.max(k1) 590 | k2 -= np.min(k2) 591 | k2 /= np.max(k2) 592 | 593 | k1 *= self.intensity_multiplier 594 | k2 *= self.intensity_multiplier 595 | 596 | colors[:,0] = K 597 | colors[:,1] = 0 598 | colors[:,2] = H 599 | bpy.ops.object.mode_set(mode='OBJECT') 600 | write_colors(colors, mesh) 601 | else: 602 | self.set_colors(mesh, values) 603 | 604 | return {'FINISHED'} 605 | 606 | 607 | def add_object_button(self, context): 608 | self.layout.operator( 609 | CurvatureOperator.bl_idname, 610 | text=CurvatureOperator.__doc__, 611 | icon='MESH_DATA') 612 | 613 | 614 | def register(): 615 | bpy.utils.register_class(CurvatureOperator) 616 | bpy.types.VIEW3D_MT_object.append(add_object_button) 617 | 618 | 619 | def unregister(): 620 | bpy.utils.unregister_class(CurvatureOperator) 621 | bpy.types.VIEW3D_MT_object.remove(add_object_button) 622 | 623 | 624 | def profile_debug(): 625 | pr = cProfile.Profile() 626 | pr.enable() 627 | bpy.ops.object.vertex_colors_curve() 628 | pr.disable() 629 | s = io.StringIO() 630 | sortby = 'cumulative' 631 | ps = pstats.Stats(pr, stream=s) 632 | ps.strip_dirs().sort_stats(sortby).print_stats() 633 | print(s.getvalue()) 634 | 635 | 636 | if __name__ == "__main__": 637 | #unregister() 638 | register() 639 | #profile_debug() 640 | 641 | -------------------------------------------------------------------------------- /old/triplanar_uv.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # ##### BEGIN GPL LICENSE BLOCK ##### 4 | # 5 | # This program is free software; you can redistribute it and/or 6 | # modify it under the terms of the GNU General Public License 7 | # as published by the Free Software Foundation; either version 2 8 | # of the License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software Foundation, 17 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | # 19 | # ##### END GPL LICENSE BLOCK ##### 20 | 21 | bl_info = { 22 | "name": "Triplanar UV mapping", 23 | "category": "UV", 24 | "description": "Generate triplanar UV mapping from an object", 25 | "author": "Tommi Hyppänen (ambi)", 26 | "location": "View3D > Edit mode > Unwrap menu > Triplanar UV mapping", 27 | "version": (0, 0, 2), 28 | "blender": (2, 76, 0) 29 | } 30 | 31 | 32 | import bpy 33 | import bmesh 34 | import math 35 | 36 | 37 | if bpy.app.version[0] < 2 or bpy.app.version[1] < 62: 38 | raise Exception("This Triplanar UV mapping addons works only in Blender 2.62 and above") 39 | 40 | 41 | def main(context): 42 | obj = context.active_object 43 | me = obj.data 44 | bm = bmesh.from_edit_mesh(me) 45 | 46 | uv_layer = bm.loops.layers.uv.verify() 47 | bm.faces.layers.tex.verify() # currently blender needs both layers. 48 | 49 | # adjust UVs 50 | for f in bm.faces: 51 | norm = f.normal 52 | ax, ay, az = abs(norm.x), abs(norm.y), abs(norm.z) 53 | axis = -1 54 | if ax > ay and ax > az: 55 | axis = 0 56 | if ay > ax and ay > az: 57 | axis = 1 58 | if az > ax and az > ay: 59 | axis = 2 60 | for l in f.loops: 61 | luv = l[uv_layer] 62 | if axis == 0: # x plane 63 | luv.uv.x = l.vert.co.y 64 | luv.uv.y = l.vert.co.z 65 | if axis == 1: # u plane 66 | luv.uv.x = l.vert.co.x 67 | luv.uv.y = l.vert.co.z 68 | if axis == 2: # z plane 69 | luv.uv.x = l.vert.co.x 70 | luv.uv.y = l.vert.co.y 71 | 72 | bmesh.update_edit_mesh(me) 73 | 74 | 75 | class UvOperator(bpy.types.Operator): 76 | """Triplanar UV mapping""" 77 | bl_idname = "uv.triplanar" 78 | bl_label = "Triplanar UV mapping" 79 | 80 | @classmethod 81 | def poll(cls, context): 82 | return (context.mode == 'EDIT_MESH') 83 | 84 | def execute(self, context): 85 | main(context) 86 | return {'FINISHED'} 87 | 88 | 89 | def menu_func(self, context): 90 | self.layout.operator("uv.triplanar") 91 | 92 | 93 | def register(): 94 | bpy.utils.register_class(UvOperator) 95 | bpy.types.VIEW3D_MT_uv_map.append(menu_func) 96 | 97 | 98 | def unregister(): 99 | bpy.utils.unregister_class(UvOperator) 100 | bpy.types.VIEW3D_MT_uv_map.remove(menu_func) 101 | 102 | 103 | if __name__ == "__main__": 104 | register() 105 | -------------------------------------------------------------------------------- /old/uv_area.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | bl_info = { 20 | "name": "UV Statistics", 21 | "category": "UV", 22 | "description": "Calculate relevant statistics on the active UV map", 23 | "author": "Tommi Hyppänen (ambi)", 24 | "location": "UV/Image Editor > Tools > Misc > UV Stats > Update Stats", 25 | "version": (0, 0, 7), 26 | "blender": (2, 80, 0) 27 | } 28 | 29 | # Calculate used UV area, works only for tri+quad meshes 30 | # doesn't account for UV overlap 31 | 32 | import bpy 33 | import math 34 | import bmesh 35 | from collections import defaultdict 36 | 37 | class UV(): 38 | def __init__(self, obj): 39 | self.obj = obj 40 | self.uv_layer = obj.data.uv_layers.active.data 41 | self.uv_esp = 5 42 | self.islands = [] 43 | 44 | print("Finding islands...") 45 | 46 | # all polys 47 | selfpolys = self.obj.data.polygons 48 | 49 | print("..polys") 50 | uvlr = self.uv_layer 51 | print(len(selfpolys)) 52 | 53 | _loc2poly = defaultdict(set) 54 | _poly2loc = defaultdict(set) 55 | for i, poly in enumerate(selfpolys): 56 | uvs = set((round(uvlr[i].uv[0], self.uv_esp), 57 | round(uvlr[i].uv[1], self.uv_esp)) for i in poly.loop_indices) 58 | for u in uvs: 59 | _loc2poly[u].add(poly.index) 60 | _poly2loc[poly.index].add(u) 61 | 62 | def _parse(poly): 63 | out = set() 64 | if poly in _poly2loc: 65 | out.add(poly) 66 | connected = _poly2loc[poly] 67 | del _poly2loc[poly] 68 | 69 | # while connections found 70 | while connected: 71 | new_connections = set() 72 | for c in connected: 73 | for p in _loc2poly[c]: 74 | out.add(p) 75 | new_connections |= _poly2loc[p] 76 | del _poly2loc[p] 77 | connected = new_connections 78 | 79 | return out 80 | 81 | # while unprocessed vertices remain... 82 | isles = [] 83 | while _poly2loc: 84 | # get random poly from existing points 85 | _poly = next(iter(_poly2loc.items()))[0] 86 | 87 | # repeat until no more connected vertices (remove connected vertices) 88 | isles.append(_parse(_poly)) 89 | 90 | print("..done") 91 | #self.islefaces = [] 92 | #for i in isles: 93 | # self.islefaces.append(list(i)) 94 | self.islandcount = len(isles) 95 | 96 | 97 | def get_uv_area(self): 98 | def triangle_area(verts): 99 | # Heron's formula 100 | a = (verts[1][0]-verts[0][0])**2.0 + (verts[1][1]-verts[0][1])**2.0 101 | b = (verts[2][0]-verts[1][0])**2.0 + (verts[2][1]-verts[1][1])**2.0 102 | c = (verts[0][0]-verts[2][0])**2.0 + (verts[0][1]-verts[2][1])**2.0 103 | cal = (2*a*b + 2*b*c + 2*c*a - a**2 - b**2 - c**2)/16 104 | if cal<0: cal=0 105 | return math.sqrt(cal) 106 | 107 | def quad_area(verts): 108 | return triangle_area(verts[:3]) + triangle_area(verts[2:]+[verts[0]]) 109 | 110 | total_area = 0.0 111 | for poly in self.obj.data.polygons: 112 | if len(poly.loop_indices) == 3: 113 | val = triangle_area([self.uv_layer[i].uv for i in poly.loop_indices]) 114 | if not math.isnan(val): 115 | total_area += val 116 | if len(poly.loop_indices) == 4: 117 | val = quad_area([self.uv_layer[i].uv for i in poly.loop_indices]) 118 | if not math.isnan(val): 119 | total_area += val 120 | 121 | return total_area 122 | 123 | class UVStatsOperator(bpy.types.Operator): 124 | """UV Statistics""" 125 | bl_idname = "uv.stats" 126 | bl_label = "UV Statistics" 127 | 128 | @classmethod 129 | def poll(cls, context): 130 | return bpy.context.active_object and bpy.context.active_object.mode == "EDIT" and len(context.object.data.uv_textures) > 0 131 | #return (context.object is not None and 132 | # context.object.type == 'MESH' and 133 | # len(context.object.data.uv_textures) > 0) 134 | 135 | def execute(self, context): 136 | prev_mode = bpy.context.object.mode 137 | bpy.ops.object.mode_set(mode='OBJECT') 138 | 139 | ob = bpy.context.object 140 | uv_layer = ob.data.uv_layers.active.data 141 | 142 | #me = ob.data 143 | #bm = bmesh.from_edit_mesh(me) 144 | 145 | #uv_layer = bm.loops.layers.uv.verify() 146 | #bm.faces.layers.tex.verify() 147 | 148 | #for f in bm.faces: 149 | # for l in f.loops: 150 | # luv = l[uv_layer] 151 | # print(luv.uv) 152 | uv = UV(ob) 153 | 154 | context.scene.uv_island_count = repr(uv.islandcount) 155 | context.scene.uv_area_size = repr(round(uv.get_uv_area()*100, 2))+"%" 156 | 157 | print(context.scene.uv_island_count + " approximate UV islands counted.") 158 | print(context.scene.uv_area_size + " UV area used.") 159 | 160 | bpy.ops.object.mode_set(mode=prev_mode) 161 | return {'FINISHED'} 162 | 163 | 164 | class UVStatsPanel(bpy.types.Panel): 165 | """UV Stats Panel""" 166 | bl_label = "UV Stats" 167 | bl_idname = "uv.statspanel" 168 | bl_space_type = 'IMAGE_EDITOR' 169 | bl_region_type = 'UI' 170 | bl_category = "Image" 171 | #bl_context = "imagepaint" 172 | 173 | def draw(self, context): 174 | layout = self.layout 175 | row = layout.row() 176 | row.label(text="Islands: "+context.scene.uv_island_count, icon='QUESTION') 177 | 178 | row = layout.row() 179 | row.label(text="Area: "+context.scene.uv_area_size, icon='QUESTION') 180 | 181 | row = layout.row() 182 | row.operator(UVStatsOperator.bl_idname, text="Update Stats") 183 | 184 | 185 | def register(): 186 | bpy.utils.register_class(UVStatsOperator) 187 | bpy.utils.register_class(UVStatsPanel) 188 | 189 | bpy.types.Scene.uv_island_count = bpy.props.StringProperty( 190 | name="uv_islands", description="UV Islands") 191 | 192 | bpy.types.Scene.uv_area_size = bpy.props.StringProperty( 193 | name="uv_area", description="UV Area") 194 | 195 | bpy.types.Scene.uv_distortion = bpy.props.StringProperty( 196 | name="uv_distortion", description="UV Distortion") 197 | 198 | def unregister(): 199 | bpy.utils.unregister_class(UVStatsOperator) 200 | bpy.utils.unregister_class(UVStatsPanel) 201 | 202 | 203 | if __name__ == "__main__": 204 | register() 205 | -------------------------------------------------------------------------------- /old/vdb_remesh.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Tommi Hyppänen, license: GNU General Public License v2.0 2 | 3 | bl_info = { 4 | "name": "OpenVDB remesh", 5 | "category": "Object", 6 | "description": "OpenVDB based remesher", 7 | "author": "Tommi Hyppänen (ambi)", 8 | "location": "3D View > Retopology > VDB remesh", 9 | "version": (0, 1, 5), 10 | "blender": (2, 79, 0) 11 | } 12 | 13 | try: 14 | import pyopenvdb as vdb 15 | except SystemError as e: 16 | print(e) 17 | import numpy as np 18 | import bpy 19 | import bmesh 20 | import random 21 | import cProfile, pstats, io 22 | 23 | DEBUG = True 24 | MIN_VOXEL_SIZE = 0.005 25 | MAX_VOXEL_SIZE = 0.5 26 | MAX_POLYGONS = 10000000 27 | 28 | MAX_SMOOTHING = 1000*500*500 29 | MAX_VOXEL_BB = 1500*1500*1500 30 | 31 | def write_slow(mesh, ve, tr, qu): 32 | print("vdb_remesh: write mesh (slow)") 33 | bm = bmesh.new() 34 | for co in ve.tolist(): 35 | bm.verts.new(co) 36 | 37 | bm.verts.ensure_lookup_table() 38 | 39 | for face_indices in tr.tolist() + qu.tolist(): 40 | bm.faces.new(tuple(bm.verts[index] for index in face_indices)) 41 | 42 | return bm 43 | 44 | 45 | def write_fast(ve, tr, qu): 46 | print("vdb_remesh: write mesh (fast)") 47 | me = bpy.data.meshes.new("testmesh") 48 | 49 | quadcount = len(qu) 50 | tricount = len(tr) 51 | 52 | me.vertices.add(count=len(ve)) 53 | 54 | loopcount = quadcount * 4 + tricount * 3 55 | facecount = quadcount + tricount 56 | 57 | me.loops.add(loopcount) 58 | me.polygons.add(facecount) 59 | 60 | face_lengths = np.zeros(facecount, dtype=np.int) 61 | face_lengths[:tricount] = 3 62 | face_lengths[tricount:] = 4 63 | 64 | loops = np.concatenate((np.arange(tricount) * 3, np.arange(quadcount) * 4 + tricount * 3)) 65 | 66 | # [::-1] makes normals consistent (from OpenVDB) 67 | v_out = np.concatenate((tr.ravel()[::-1], qu.ravel()[::-1])) 68 | 69 | me.vertices.foreach_set("co", ve.ravel()) 70 | me.polygons.foreach_set("loop_total", face_lengths) 71 | me.polygons.foreach_set("loop_start", loops) 72 | me.polygons.foreach_set("vertices", v_out) 73 | 74 | me.update(calc_edges=True) 75 | #me.validate(verbose=True) 76 | 77 | return me 78 | 79 | 80 | def read_verts(mesh): 81 | mverts_co = np.zeros((len(mesh.vertices)*3), dtype=np.float) 82 | mesh.vertices.foreach_get("co", mverts_co) 83 | return np.reshape(mverts_co, (len(mesh.vertices), 3)) 84 | 85 | 86 | def read_loops(mesh): 87 | loops = np.zeros((len(mesh.polygons)), dtype=np.int) 88 | mesh.polygons.foreach_get("loop_total", loops) 89 | return loops 90 | 91 | 92 | def read_edges(mesh): 93 | fastedges = np.zeros((len(mesh.edges)*2), dtype=np.int) # [0.0, 0.0] * len(mesh.edges) 94 | mesh.edges.foreach_get("vertices", fastedges) 95 | return np.reshape(fastedges, (len(mesh.edges), 2)) 96 | 97 | 98 | def read_norms(mesh): 99 | mverts_no = np.zeros((len(mesh.vertices)*3), dtype=np.float) 100 | mesh.vertices.foreach_get("normal", mverts_no) 101 | return np.reshape(mverts_no, (len(mesh.vertices), 3)) 102 | 103 | 104 | def read_polygon_verts(mesh): 105 | polys = np.zeros((len(mesh.polygons)*4), dtype=np.uint32) 106 | mesh.polygons.foreach_get("vertices", faces) 107 | return polys 108 | 109 | 110 | def safe_bincount(data, weights, dts, conn): 111 | bc = np.bincount(data, weights) 112 | dts[:len(bc)] += bc 113 | bc = np.bincount(data) 114 | conn[:len(bc)] += bc 115 | return (dts, conn) 116 | 117 | 118 | def calc_normals(fastverts, fastnorms, fastedges): 119 | """ Calculates curvature [0, 1] for specified mesh """ 120 | edge_a, edge_b = fastedges[:,0], fastedges[:,1] 121 | 122 | tvec = fastverts[edge_b] - fastverts[edge_a] 123 | tvlen = np.linalg.norm(tvec, axis=1) 124 | 125 | # normalize vectors 126 | tvec = (tvec.T / tvlen).T 127 | 128 | # adjust the minimum of what is processed 129 | edgelength = tvlen * 100 130 | edgelength = np.where(edgelength < 1, 1.0, edgelength) 131 | 132 | vecsums = np.zeros(fastverts.shape[0], dtype=np.float) 133 | connections = np.zeros(fastverts.shape[0], dtype=np.float) 134 | 135 | # calculate normal differences to the edge vector in the first edge vertex 136 | totdot = (np.einsum('ij,ij->i', tvec, fastnorms[edge_a]))/edgelength 137 | safe_bincount(edge_a, totdot, vecsums, connections) 138 | 139 | # calculate normal differences to the edge vector in the second edge vertex 140 | totdot = (np.einsum('ij,ij->i', -tvec, fastnorms[edge_b]))/edgelength 141 | safe_bincount(edge_b, totdot, vecsums, connections) 142 | 143 | # (approximate gaussian) curvature is the average difference of 144 | # edge vectors to surface normals (from dot procuct cosine equation) 145 | curve = 1.0 - np.arccos(vecsums/connections)/np.pi 146 | 147 | # 1 = max curvature, 0 = min curvature, 0.5 = zero curvature 148 | curve -= 0.5 149 | curve /= np.max([np.amax(curve), np.abs(np.amin(curve))]) 150 | curve += 0.5 151 | return curve 152 | 153 | 154 | def mesh_smooth_filter_variable(data, fastverts, fastedges, iterations): 155 | """ Smooths variables in data [-1, 1] over the mesh topology """ 156 | # vert indices of edges 157 | edge_a, edge_b = fastedges[:,0], fastedges[:,1] 158 | tvlen = np.linalg.norm(fastverts[edge_b] - fastverts[edge_a], axis=1) 159 | edgelength = np.where(tvlen<1, 1.0, tvlen) 160 | 161 | data_sums = np.zeros(fastverts.shape[0], dtype=np.float) 162 | connections = np.zeros(fastverts.shape[0], dtype=np.float) 163 | 164 | # longer the edge distance to datapoint, less it has influence 165 | 166 | for _ in range(iterations): 167 | # step 1 168 | per_vert = data[edge_b]/edgelength 169 | safe_bincount(edge_a, per_vert, data_sums, connections) 170 | eb_smooth = data_sums/connections 171 | 172 | per_vert = eb_smooth[edge_a]/edgelength 173 | safe_bincount(edge_b, per_vert, data_sums, connections) 174 | 175 | new_data = data_sums/connections 176 | 177 | # step 2 178 | data_sums = np.zeros(data_sums.shape) 179 | connections = np.zeros(connections.shape) 180 | 181 | per_vert = data[edge_a]/edgelength 182 | safe_bincount(edge_b, per_vert, data_sums, connections) 183 | ea_smooth = data_sums/connections 184 | 185 | per_vert = ea_smooth[edge_b]/edgelength 186 | safe_bincount(edge_a, per_vert, data_sums, connections) 187 | 188 | new_data += data_sums/connections 189 | 190 | # limit between -1 and 1 191 | new_data /= np.max([np.amax(new_data), np.abs(np.amin(new_data))]) 192 | data = new_data 193 | 194 | return new_data 195 | 196 | 197 | def vdb_remesh(verts, tris, quads, iso, adapt, only_quads, vxsize, filter_iterations, filter_width, filter_style, grid=None): 198 | 199 | iso *= vxsize 200 | 201 | def _read(verts, tris, quads, vxsize): 202 | print("vdb: read voxels from mesh") 203 | vtransform = vdb.createLinearTransform(voxelSize=vxsize) 204 | 205 | if len(tris) == 0: 206 | grid = vdb.FloatGrid.createLevelSetFromPolygons(verts, quads=quads, transform=vtransform) 207 | elif len(quads) == 0: 208 | grid = vdb.FloatGrid.createLevelSetFromPolygons(verts, triangles=tris, transform=vtransform) 209 | else: 210 | grid = vdb.FloatGrid.createLevelSetFromPolygons(verts, tris, quads, transform=vtransform) 211 | 212 | bb = grid.evalActiveVoxelBoundingBox() 213 | bb_size = (bb[1][0]-bb[0][0], bb[1][1]-bb[0][1], bb[1][2]-bb[0][2]) 214 | print("vdb_remesh: new grid {} voxels".format(bb_size)) 215 | 216 | return grid 217 | 218 | saved_grid = None 219 | if grid == None: 220 | grid = _read(verts, tris, quads, vxsize) 221 | else: 222 | saved_grid = grid 223 | grid = grid.deepCopy() 224 | 225 | def _filter_numpy(iterations, gr, fname): 226 | print("vdb: blur") 227 | bb = gr.evalActiveVoxelBoundingBox() 228 | bb_size = (bb[1][0]-bb[0][0], bb[1][1]-bb[0][1], bb[1][2]-bb[0][2]) 229 | print("Smoothing on {} bounding box".format(bb_size)) 230 | if bb_size[0] * bb_size[1] * bb_size[2] < MAX_SMOOTHING: 231 | array = np.empty(bb_size, dtype=np.float) 232 | gr.copyToArray(array, ijk=bb[0]) 233 | aavg = None 234 | for _ in range(iterations): 235 | 236 | if fname == "blur": 237 | surrounding = ( 238 | np.roll(array, -1, axis=0) + np.roll(array, 1, axis=0) + \ 239 | np.roll(array, -1, axis=1) + np.roll(array, 1, axis=1) + \ 240 | np.roll(array, -1, axis=2) + np.roll(array, 1, axis=2)) 241 | array = (array + surrounding) / 7 242 | 243 | if fname == "sharpen": 244 | surrounding = ( 245 | np.roll(array, -1, axis=0) + np.roll(array, 1, axis=0) + \ 246 | np.roll(array, -1, axis=1) + np.roll(array, 1, axis=1) + \ 247 | np.roll(array, -1, axis=2) + np.roll(array, 1, axis=2)) 248 | array = (array) * 0.5 + (array*7 - surrounding) * 0.5 249 | 250 | if fname == "edge": 251 | surrounding = ( 252 | np.roll(array, -1, axis=0) + np.roll(array, 1, axis=0) + \ 253 | np.roll(array, -1, axis=1) + np.roll(array, 1, axis=1) + \ 254 | np.roll(array, -1, axis=2) + np.roll(array, 1, axis=2)) 255 | array = surrounding - 4*array 256 | 257 | gr.copyFromArray(array, ijk=bb[0]) 258 | else: 259 | print("Smoothing bounding box exceeded maximum size. Skipping.") 260 | 261 | def _write(gr): 262 | print("vdb: write voxels to polygons") 263 | fit = filter_iterations if filter_iterations > 0 else 0 264 | verts, tris, quads = gr.convertToComplex(iso, adapt, fit, filter_width) 265 | 266 | return (verts, tris, quads) 267 | 268 | return (_write(grid), grid if saved_grid == None else saved_grid) 269 | 270 | def read_mesh(mesh): 271 | print("vdb_remesh: read mesh") 272 | verts = read_verts(mesh) 273 | 274 | qu, tr = [], [] 275 | for f in mesh.polygons: 276 | if len(f.vertices) == 4: 277 | qu.append([]) 278 | for idx in f.vertices: 279 | qu[-1].append(idx) 280 | if len(f.vertices) == 3: 281 | tr.append([]) 282 | for idx in f.vertices: 283 | tr[-1].append(idx) 284 | 285 | return (verts, np.array(tr), np.array(qu)) 286 | 287 | 288 | def read_bmesh(bmesh): 289 | print("vdb_remesh: read bmesh") 290 | bmesh.verts.ensure_lookup_table() 291 | bmesh.faces.ensure_lookup_table() 292 | 293 | verts = [(i.co[0], i.co[1], i.co[2]) for i in bmesh.verts] 294 | qu, tr = [], [] 295 | for f in bmesh.faces: 296 | if len(f.verts) == 4: 297 | qu.append([]) 298 | for v in f.verts: 299 | qu[-1].append(v.index) 300 | if len(f.verts) == 3: 301 | tr.append([]) 302 | for v in f.verts: 303 | tr[-1].append(v.index) 304 | 305 | return (np.array(verts), np.array(tr), np.array(qu)) 306 | 307 | 308 | def bmesh_update_normals(obj): 309 | # calc normals 310 | bm = bmesh.new() 311 | bm.from_mesh(obj.data) 312 | #bm.normal_update() 313 | bmesh.ops.recalc_face_normals(bm, faces=bm.faces) 314 | bm.to_mesh(obj.data) 315 | bm.free() 316 | 317 | def project_to_nearest(source, target): 318 | print("vdb_remesh: project to nearest") 319 | source.verts.ensure_lookup_table() 320 | 321 | res = target.closest_point_on_mesh(source.verts[0].co) 322 | 323 | if len(res) > 3: 324 | # new style 2.79 325 | rnum = 1 326 | else: 327 | # old style 2.76 328 | rnum = 0 329 | 330 | for i, vtx in enumerate(source.verts): 331 | res = target.closest_point_on_mesh(vtx.co) 332 | #if res[0]: 333 | source.verts[i].co = res[rnum] 334 | 335 | def project_to_nearest_numeric(source, target): 336 | print("vdb_remesh: project to nearest (numpy)") 337 | 338 | if len(source[0]) == 0: 339 | return 340 | 341 | res = target.closest_point_on_mesh(source[0][0].tolist()) 342 | 343 | if len(res) > 3: 344 | # new style 2.79 345 | rnum = 1 346 | else: 347 | # old style 2.76 348 | rnum = 0 349 | 350 | vfunc = target.closest_point_on_mesh 351 | for i, vtx in enumerate(source[0]): 352 | source[0][i] = vfunc(vtx)[rnum] 353 | 354 | 355 | class VDBRemeshOperator(bpy.types.Operator): 356 | """OpenVDB Remesh""" 357 | bl_idname = "object.vdbremesh_operator" 358 | bl_label = "OpenVDB remesh" 359 | bl_options = {'REGISTER', 'UNDO'} 360 | 361 | voxel_size_def = \ 362 | bpy.props.EnumProperty(items=[('relative', 'Relative', 'Voxel size is defined in relation to the object size'), 363 | ('absolute', 'Absolute', 'Voxel size is defined in world coordinates')], 364 | name="voxel_size_def", default='relative') 365 | 366 | voxel_size_world = bpy.props.FloatProperty( 367 | name="Voxel size", 368 | description="Voxel size defined in world coordinates", 369 | min=MIN_VOXEL_SIZE, max=MAX_VOXEL_SIZE, 370 | default=0.05) 371 | 372 | voxel_size_object = bpy.props.FloatProperty( 373 | name="Voxel size (relative)", 374 | description="Voxel size in relation to the objects longest bounding box edge", 375 | min=0.001, max=0.25, 376 | default=0.005) 377 | 378 | isovalue = bpy.props.FloatProperty( 379 | name="Isovalue", 380 | description="Isovalue", 381 | min=-3.0, max=3.0, 382 | default=0.0) 383 | 384 | adaptivity = bpy.props.FloatProperty( 385 | name="Adaptivity", 386 | description="Adaptivity", 387 | min=0.0, max=1.0, 388 | default=0.0001) 389 | 390 | filter_style = \ 391 | bpy.props.EnumProperty(items=[('blur', 'Blur', 'Blur voxels'), 392 | ('sharpen', 'Sharpen', 'Sharpen voxels'), 393 | ('edge', 'Edge', 'Edge enhance')], 394 | name="Filter style", default='blur') 395 | 396 | filter_iterations = bpy.props.IntProperty( 397 | name="Gaussian iterations", 398 | description="Gaussian iterations", 399 | min=0, max=10, 400 | default=0) 401 | 402 | filter_width = bpy.props.IntProperty( 403 | name="Gaussian width", 404 | description="Gaussian width", 405 | min=1, max=10, 406 | default=1) 407 | 408 | filter_quality = bpy.props.FloatProperty( 409 | name="Gaussian sharpness", 410 | description="Gaussian sharpness", 411 | min=2.0, max=10.0, 412 | default=5.0) 413 | 414 | only_quads = bpy.props.BoolProperty( 415 | name="Quads only", 416 | description="Construct the mesh using only quad topology", 417 | default=False) 418 | 419 | smooth = bpy.props.BoolProperty( 420 | name="Smooth", 421 | description="Smooth shading toggle", 422 | default=True) 423 | 424 | nearest = bpy.props.BoolProperty( 425 | name="Project to nearest", 426 | description="Project generated mesh points to nearest surface point", 427 | default=False) 428 | 429 | grid = None 430 | grid_voxelsize = None 431 | max_polys_reached = False 432 | 433 | @classmethod 434 | def poll(cls, context): 435 | return context.active_object is not None 436 | 437 | 438 | def execute(self, context): 439 | print("vdb_remesh: execute") 440 | if DEBUG: 441 | pr = cProfile.Profile() 442 | pr.enable() 443 | 444 | # voxel_def = context.scene.vdb_remesh_voxel_size_def 445 | # voxel_world = context.scene.vdb_remesh_voxel_size_world 446 | # voxel_object = context.scene.vdb_remesh_voxel_size_object 447 | 448 | voxel_size = 0.07 449 | 450 | if self.voxel_size_def == "relative": 451 | voxel_size = max(context.active_object.dimensions) * self.voxel_size_object 452 | else: 453 | voxel_size = self.voxel_size_world 454 | 455 | 456 | # apply modifiers for the active object before remeshing 457 | for mod in context.active_object.modifiers: 458 | try: 459 | bpy.ops.object.modifier_apply(modifier=mod.name) 460 | except RuntimeError as ex: 461 | print(ex) 462 | 463 | # start remesh 464 | me = context.active_object.data 465 | 466 | if self.grid == None or self.grid_voxelsize != voxel_size: 467 | # caching 468 | self.grid = None 469 | 470 | bm = bmesh.new() 471 | bm.from_mesh(me) 472 | 473 | loops = read_loops(me) 474 | if np.max(loops) > 4: 475 | print("Mesh has ngons! Triangulating...") 476 | bmesh.ops.triangulate(bm, faces=bm.faces[:], quad_method=0, ngon_method=0) 477 | 478 | self.grid_voxelsize = voxel_size 479 | startmesh = read_bmesh(bm) 480 | self.vert_0 = len(startmesh[0]) 481 | bm.free() 482 | else: 483 | startmesh = (None, None, None) 484 | 485 | #vdb_remesh(verts, tris, quads, iso, adapt, only_quads, vxsize, filter_iterations, filter_width, filter_style, grid=None): 486 | 487 | new_mesh, self.grid = vdb_remesh(startmesh[0], startmesh[1], startmesh[2], self.isovalue, \ 488 | self.adaptivity, self.only_quads, voxel_size, self.filter_iterations, self.filter_width, \ 489 | self.filter_style, grid=self.grid) 490 | 491 | print("vdb_remesh: new mesh {}".format([i.shape for i in new_mesh])) 492 | self.vert_1 = len(new_mesh[0]) 493 | self.face_1 = len(new_mesh[1])+len(new_mesh[2]) 494 | 495 | if self.face_1 < MAX_POLYGONS: 496 | self.max_polys_reached = False 497 | 498 | #if self.project_nearest: 499 | #context.scene.update() 500 | #project_to_nearest_numeric(new_mesh, context.active_object) 501 | 502 | remeshed = write_fast(*new_mesh) 503 | 504 | context.active_object.data = remeshed 505 | 506 | if self.nearest: 507 | def _project_wrap(): 508 | temp_object = bpy.data.objects.new("temp.remesher.947", me) 509 | temp_object.matrix_world = context.active_object.matrix_world 510 | 511 | 512 | bpy.ops.object.modifier_add(type='SHRINKWRAP') 513 | context.object.modifiers["Shrinkwrap"].target = temp_object 514 | 515 | for mod in context.active_object.modifiers: 516 | try: 517 | bpy.ops.object.modifier_apply(modifier=mod.name) 518 | except RuntimeError as ex: 519 | print(ex) 520 | 521 | objs = bpy.data.objects 522 | objs.remove(objs["temp.remesher.947"], True) 523 | 524 | _project_wrap() 525 | 526 | 527 | # calc normals 528 | #bmesh_update_normals(context.active_object) 529 | 530 | def _make_normals_consistent(): 531 | bpy.ops.object.mode_set(mode = 'EDIT') 532 | bpy.ops.mesh.normals_make_consistent() 533 | bpy.ops.object.mode_set(mode = 'OBJECT') 534 | 535 | #_make_normals_consistent() 536 | 537 | # flip normals 538 | """ 539 | obj = context.active_object 540 | norms = np.zeros((len(obj.data.polygons)*3), dtype=np.float) 541 | obj.data.polygons.foreach_get("normal", norms) 542 | norms = -norms 543 | obj.data.polygons.foreach_set("normal", norms) 544 | 545 | norms = np.zeros((len(obj.data.vertices)*3), dtype=np.float) 546 | obj.data.vertices.foreach_get("normal", norms) 547 | norms = -norms 548 | obj.data.vertices.foreach_set("normal", norms) 549 | """ 550 | 551 | if self.only_quads: 552 | def _decimate_flat_slow(): 553 | #obj.data.update() 554 | print("vdb_remesh: decimate flat: calculating curvature...") 555 | 556 | mesh = context.active_object.data 557 | fastverts = read_verts(mesh) 558 | fastedges = read_edges(mesh) 559 | fastnorms = read_norms(mesh) 560 | 561 | values = calc_normals(fastverts, fastnorms, fastedges) 562 | 563 | values = (values-0.5)*2.0 564 | values = mesh_smooth_filter_variable(values, fastverts, fastedges, 5) 565 | 566 | bm = bmesh.new() 567 | bm.from_mesh(context.active_object.data) 568 | bm.verts.ensure_lookup_table() 569 | c_verts = [] 570 | for i, v in enumerate(bm.verts): 571 | if abs(values[i]) < 0.01: 572 | c_verts.append(v) 573 | 574 | print("vdb_remesh: decimate flat: collapsing verts (bmesh.ops)") 575 | bmesh.ops.dissolve_verts(bm, verts=c_verts) #, use_face_split, use_boundary_tear) 576 | 577 | bm.to_mesh(context.active_object.data) 578 | bm.free() 579 | 580 | 581 | 582 | if self.smooth: 583 | bpy.ops.object.shade_smooth() 584 | 585 | else: 586 | self.max_polys_reached = True 587 | 588 | if DEBUG: 589 | pr.disable() 590 | s = io.StringIO() 591 | sortby = 'cumulative' 592 | ps = pstats.Stats(pr, stream=s) 593 | ps.strip_dirs().sort_stats(sortby).print_stats() 594 | print(s.getvalue()) 595 | 596 | print("vdb_remesh: exit") 597 | 598 | return {'FINISHED'} 599 | 600 | 601 | def draw(self, context): 602 | layout = self.layout 603 | col = layout.column() 604 | 605 | if self.max_polys_reached: 606 | row = col.row() 607 | row.label(text="Max poly count reached (>{})".format(MAX_POLYGONS)) 608 | row = col.row() 609 | row.label(text="Skipping writing to mesh.") 610 | 611 | row = layout.row() 612 | row.prop(self, "voxel_size_def", expand=True, text="Island margin quality/performance") 613 | 614 | row = layout.row() 615 | col = row.column(align=True) 616 | if self.voxel_size_def == "relative": 617 | col.prop(self, "voxel_size_object") 618 | else: 619 | col.prop(self, "voxel_size_world") 620 | 621 | row = layout.row() 622 | col = row.column(align=True) 623 | col.prop(self, "isovalue") 624 | 625 | col.prop(self, "adaptivity") 626 | 627 | #row = layout.row() 628 | #row.prop(self, "filter_style", expand=True, text="Type of filter to be iterated on the voxels") 629 | #row = layout.row() 630 | row = layout.row() 631 | col = row.column(align=True) 632 | 633 | col.prop(self, "filter_iterations") 634 | col.prop(self, "filter_width") 635 | col.prop(self, "filter_quality") 636 | 637 | row = layout.row() 638 | #row.prop(self, "only_quads") 639 | row.prop(self, "smooth") 640 | row.prop(self, "nearest") 641 | 642 | if hasattr(self, 'vert_0'): 643 | infotext = "Change: {:.2%}".format(self.vert_1/self.vert_0) 644 | row = layout.row() 645 | row.label(text=infotext) 646 | row = layout.row() 647 | row.label(text="Verts: {}, Polys: {}".format(self.vert_1, self.face_1)) 648 | 649 | row = layout.row() 650 | row.label(text="Cache: {} voxels".format(self.grid.activeVoxelCount())) 651 | 652 | 653 | 654 | 655 | class VDBRemeshPanel(bpy.types.Panel): 656 | """OpenVDB remesh operator panel""" 657 | bl_label = "VDB remesh" 658 | bl_idname = "object.vdbremesh_panel" 659 | bl_space_type = 'VIEW_3D' 660 | bl_region_type = 'TOOLS' 661 | bl_category = "Retopology" 662 | 663 | def draw(self, context): 664 | layout = self.layout 665 | 666 | """ 667 | row = layout.row() 668 | row.prop(context.scene, "vdb_remesh_voxel_size_def", expand=True, text="Island margin quality/performance") 669 | 670 | row = layout.row() 671 | col = row.column(align=True) 672 | if context.scene.vdb_remesh_voxel_size_def == "relative": 673 | col.prop(context.scene, "vdb_remesh_voxel_size_object") 674 | else: 675 | col.prop(context.scene, "vdb_remesh_voxel_size_world") 676 | 677 | col.prop(context.scene, "vdb_remesh_isovalue") 678 | col.prop(context.scene, "vdb_remesh_adaptivity") 679 | col.prop(context.scene, "vdb_remesh_blur") 680 | 681 | row = layout.row() 682 | row.prop(context.scene, "vdb_remesh_only_quads") 683 | row.prop(context.scene, "vdb_remesh_smooth") 684 | row.prop(context.scene, "vdb_remesh_project_nearest") 685 | """ 686 | 687 | row = layout.row() 688 | row.scale_y = 2.0 689 | row.operator(VDBRemeshOperator.bl_idname, text="OpenVDB remesh") 690 | 691 | 692 | def register(): 693 | bpy.utils.register_class(VDBRemeshOperator) 694 | bpy.utils.register_class(VDBRemeshPanel) 695 | 696 | """ 697 | save_to = bpy.types.Scene 698 | 699 | save_to.vdb_remesh_voxel_size_def = \ 700 | bpy.props.EnumProperty(items=[('relative', 'Relative', 'Voxel size is defined in relation to the object size'), 701 | ('absolute', 'Absolute', 'Voxel size is defined in world coordinates')], 702 | name="s_packing_marginquality", default='relative') 703 | 704 | save_to.vdb_remesh_voxel_size_world = bpy.props.FloatProperty( 705 | name="Voxel size", 706 | description="Voxel size defined in world coordinates", 707 | min=MIN_VOXEL_SIZE, max=MAX_VOXEL_SIZE, 708 | default=0.05) 709 | 710 | save_to.vdb_remesh_voxel_size_object = bpy.props.FloatProperty( 711 | name="Voxel size (relative)", 712 | description="Voxel size in relation to the objects longest bounding box edge", 713 | min=0.001, max=0.25, 714 | default=0.01) 715 | 716 | save_to.vdb_remesh_isovalue = bpy.props.FloatProperty( 717 | name="Isovalue", 718 | description="Isovalue", 719 | min=-3.0, max=3.0, 720 | default=0.0) 721 | 722 | save_to.vdb_remesh_adaptivity = bpy.props.FloatProperty( 723 | name="Adaptivity", 724 | description="Adaptivity", 725 | min=0.0, max=1.0, 726 | default=0.01) 727 | 728 | save_to.vdb_remesh_blur = bpy.props.IntProperty( 729 | name="Blur", 730 | description="Blur iterations", 731 | min=0, max=10, 732 | default=0) 733 | 734 | save_to.vdb_remesh_only_quads = bpy.props.BoolProperty( 735 | name="Quads only", 736 | description="Construct the mesh using only quad topology", 737 | default=False) 738 | 739 | save_to.vdb_remesh_smooth = bpy.props.BoolProperty( 740 | name="Smooth", 741 | description="Smooth shading toggle", 742 | default=True) 743 | 744 | save_to.vdb_remesh_project_nearest = bpy.props.BoolProperty( 745 | name="Project to nearest", 746 | description="Project generated mesh points to nearest surface point", 747 | default=False) 748 | """ 749 | 750 | 751 | def unregister(): 752 | bpy.utils.unregister_class(VDBRemeshOperator) 753 | bpy.utils.unregister_class(VDBRemeshPanel) 754 | 755 | 756 | if __name__ == "__main__": 757 | register() 758 | #bpy.ops.object.vdbremesh_operator() 759 | 760 | 761 | -------------------------------------------------------------------------------- /old/vdb_surf.py: -------------------------------------------------------------------------------- 1 | #====================== BEGIN GPL LICENSE BLOCK ====================== 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | #======================= END GPL LICENSE BLOCK ======================== 18 | 19 | bl_info = { 20 | "name": "Cube Surfer script with OpenVDB", 21 | "author": "Jean-Francois Gallant (PyroEvil), Tommi Hyppänen (ambi)", 22 | "version": (1, 1, 0), 23 | "blender": (2, 7, 9), 24 | "location": "Properties > Object Tab", 25 | "description": ("Cube Surfer script with OpenVDB"), 26 | "category": "Object"} 27 | 28 | import bpy 29 | from bpy.types import Operator,Panel, UIList 30 | from bpy.props import FloatVectorProperty,IntProperty,StringProperty,FloatProperty,BoolProperty, CollectionProperty 31 | from bpy_extras.object_utils import AddObjectHelper 32 | from mathutils import Vector 33 | import sys 34 | import bmesh 35 | import pyopenvdb as vdb 36 | import numpy as np 37 | import time 38 | 39 | 40 | MIN_VXSIZE = 0.005 41 | 42 | 43 | def add_isosurf(self, context): 44 | mesh = bpy.data.meshes.new(name="IsoSurface") 45 | obj = bpy.data.objects.new("IsoSurface", mesh) 46 | bpy.context.scene.objects.link(obj) 47 | obj['IsoSurfer'] = True 48 | obj.IsoSurf_res = True 49 | 50 | 51 | class OBJECT_OT_add_isosurf(Operator, AddObjectHelper): 52 | bl_idname = "mesh.add_isosurf" 53 | bl_label = "Add IsoSurface Object" 54 | bl_options = {'REGISTER', 'UNDO'} 55 | 56 | scale = FloatVectorProperty( 57 | name="scale", 58 | default=(1.0, 1.0, 1.0), 59 | subtype='TRANSLATION', 60 | description="scaling", 61 | ) 62 | 63 | def execute(self, context): 64 | 65 | add_isosurf(self, context) 66 | 67 | return {'FINISHED'} 68 | 69 | 70 | def add_isosurf_button(self, context): 71 | test_group = bpy.data.node_groups.new('testGroup', 'ShaderNodeTree') 72 | self.layout.operator( 73 | OBJECT_OT_add_isosurf.bl_idname, 74 | text="IsoSurface", 75 | icon='OUTLINER_DATA_META') 76 | 77 | 78 | def isosurf_prerender(context): 79 | scn = bpy.context.scene 80 | scn.IsoSurf_context = "RENDER" 81 | isosurf(context) 82 | 83 | 84 | def isosurf_postrender(context): 85 | scn = bpy.context.scene 86 | scn.IsoSurf_context = "WINDOW" 87 | 88 | 89 | def isosurf_frame(context): 90 | scn = bpy.context.scene 91 | if scn.IsoSurf_context == "WINDOW": 92 | isosurf(context) 93 | 94 | 95 | def write_fast(ve, tr, qu): 96 | me = bpy.data.meshes.new("testmesh") 97 | 98 | quadcount = len(qu) 99 | tricount = len(tr) 100 | 101 | me.vertices.add(count=len(ve)) 102 | 103 | loopcount = quadcount * 4 + tricount * 3 104 | facecount = quadcount + tricount 105 | 106 | me.loops.add(loopcount) 107 | me.polygons.add(facecount) 108 | 109 | face_lengths = np.zeros(facecount, dtype=np.int) 110 | face_lengths[:tricount] = 3 111 | face_lengths[tricount:] = 4 112 | 113 | loops = np.concatenate((np.arange(tricount) * 3, np.arange(quadcount) * 4 + tricount * 3)) 114 | 115 | # [::-1] makes normals consistent (from OpenVDB) 116 | v_out = np.concatenate((tr.ravel()[::-1], qu.ravel()[::-1])) 117 | 118 | me.vertices.foreach_set("co", ve.ravel()) 119 | me.polygons.foreach_set("loop_total", face_lengths) 120 | me.polygons.foreach_set("loop_start", loops) 121 | me.polygons.foreach_set("vertices", v_out) 122 | 123 | me.update(calc_edges=True) 124 | 125 | return me 126 | 127 | 128 | def isosurf(context): 129 | scn = bpy.context.scene 130 | 131 | stime = time.clock() 132 | SurfList = [] 133 | for i, obj in enumerate(bpy.context.scene.objects): 134 | if 'IsoSurfer' in obj: 135 | obsurf = obj 136 | mesurf = obj.data 137 | res = obj.IsoSurf_res 138 | 139 | SurfList.append([(obsurf, mesurf, res)]) 140 | 141 | for item in obj.IsoSurf: 142 | if item.active == True: 143 | if item.obj != '': 144 | if item.psys != '': 145 | SurfList[-1].append((item.obj, item.psys)) 146 | 147 | for surfobj in SurfList: 148 | print("Calculating isosurface, for frame:", bpy.context.scene.frame_current) 149 | 150 | for obj, psys in surfobj[1:]: 151 | psys = bpy.data.objects[obj].particle_systems[psys] 152 | 153 | ploc = [] 154 | stime = time.clock() 155 | 156 | palive = False 157 | for par in range(len(psys.particles)): 158 | if psys.particles[par].alive_state == 'ALIVE': 159 | ploc.append(psys.particles[par].location) 160 | palive = True 161 | 162 | if palive: 163 | print(' pack particles:',time.clock() - stime,'sec') 164 | 165 | vxsize = scn.isosurface_voxelsize 166 | sradius = scn.isosurface_sphereradius 167 | ssteps = scn.isosurface_smoothsteps 168 | 169 | vtransform = vdb.createLinearTransform(voxelSize=vxsize) 170 | grid = vdb.FloatGrid.createLevelSetFromPoints(np.array(ploc), transform=vtransform, radius=sradius) 171 | # iso, adaptivity, gaussian iterations, gaussian kernel size, gaussian sigma 172 | verts, tris, quads = grid.convertToComplex(0.0, 0.01, ssteps, 4, 0.8) 173 | 174 | print(' vdb remesh:',time.clock() - stime,'sec') 175 | stime = time.clock() 176 | 177 | # TODO: eats all memory & resets materials 178 | # obsurf.data = write_fast(verts, tris, quads) 179 | # bpy.ops.object.shade_smooth() 180 | # scn.update() 181 | 182 | bm = bmesh.new() 183 | 184 | bm.from_mesh(mesurf) 185 | bm.clear() 186 | 187 | for co in verts.tolist(): 188 | bm.verts.new(co) 189 | 190 | bm.verts.ensure_lookup_table() 191 | bm.faces.ensure_lookup_table() 192 | 193 | for face_indices in tris.tolist() + quads.tolist(): 194 | bm.faces.new(tuple(bm.verts[index] for index in face_indices[::-1])) 195 | 196 | for f in bm.faces: 197 | f.smooth = True 198 | 199 | bm.to_mesh(mesurf) 200 | bm.free() 201 | 202 | mesurf.calc_normals() 203 | 204 | scn.update() 205 | 206 | print(' write:',time.clock() - stime,'sec') 207 | 208 | 209 | class OBJECT_UL_IsoSurf(UIList): 210 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): 211 | split = layout.split(0.1) 212 | split.label(str(index)) 213 | split.prop(item, "name", text="", emboss=False, translate=False, icon='OUTLINER_OB_META') 214 | split.prop(item, "active", text = "") 215 | 216 | 217 | class IsoSurferPanel(Panel): 218 | bl_label = "VDB remesh particles" 219 | bl_idname = "OBJECT_PT_ui_list_example" 220 | bl_space_type = 'VIEW_3D' 221 | bl_region_type = 'TOOLS' 222 | bl_category = "Retopology" 223 | 224 | def draw(self, context): 225 | obj = context.object 226 | 227 | if 'IsoSurfer' in obj: 228 | layout = self.layout 229 | box = layout.box() 230 | 231 | #row = box.row() 232 | #row.prop(obj,"IsoSurf_res",text = "Voxel size:") 233 | row = box.row() 234 | col = row.column(align=True) 235 | col.prop(context.scene, "isosurface_voxelsize", text="Voxel size") 236 | col.prop(context.scene, "isosurface_sphereradius", text="Particle radius") 237 | col.prop(context.scene, "isosurface_smoothsteps", text="Smoothing steps") 238 | 239 | row = box.row() 240 | row.template_list("OBJECT_UL_IsoSurf", "", obj, "IsoSurf", obj, "IsoSurf_index") 241 | 242 | col = row.column(align=True) 243 | col.operator("op.isosurfer_item_add", icon="ZOOMIN", text="").add = True 244 | col.operator("op.isosurfer_item_add", icon="ZOOMOUT", text="").add = False 245 | 246 | if obj.IsoSurf and obj.IsoSurf_index < len(obj.IsoSurf): 247 | row = box.row() 248 | row.prop(obj.IsoSurf[obj.IsoSurf_index],"active",text = "Active") 249 | row = box.row() 250 | row.label('Object: ') 251 | row.prop_search(obj.IsoSurf[obj.IsoSurf_index], "obj",context.scene, "objects", text="") 252 | 253 | if obj.IsoSurf[obj.IsoSurf_index].obj != '': 254 | if bpy.data.objects[obj.IsoSurf[obj.IsoSurf_index].obj].type != 'MESH': 255 | obj.IsoSurf[obj.IsoSurf_index].obj = '' 256 | else: 257 | row = box.row() 258 | row.label('Particles: ') 259 | row.prop_search(obj.IsoSurf[obj.IsoSurf_index], "psys",bpy.data.objects[obj.IsoSurf[obj.IsoSurf_index].obj], "particle_systems", text="") 260 | 261 | else: 262 | layout = self.layout 263 | box = layout.box() 264 | row = box.row() 265 | row.label('Please select an isosurface object!', icon='ERROR') 266 | 267 | 268 | class OBJECT_OT_isosurfer_add(bpy.types.Operator): 269 | bl_label = "Add/Remove items from IsoSurf obj" 270 | bl_idname = "op.isosurfer_item_add" 271 | add = bpy.props.BoolProperty(default = True) 272 | 273 | def invoke(self, context, event): 274 | add = self.add 275 | ob = context.object 276 | if ob != None: 277 | item = ob.IsoSurf 278 | if add: 279 | item.add() 280 | l = len(item) 281 | item[-1].name = ("IsoSurf." +str(l)) 282 | item[-1].active = True 283 | #item[-1].res = 0.25 284 | item[-1].id = l 285 | else: 286 | index = ob.IsoSurf_index 287 | item.remove(index) 288 | 289 | return {'FINISHED'} 290 | 291 | 292 | class IsoSurf(bpy.types.PropertyGroup): 293 | active = BoolProperty() 294 | id = IntProperty() 295 | obj = StringProperty() 296 | psys = StringProperty() 297 | 298 | 299 | def register(): 300 | bpy.utils.register_class(OBJECT_OT_add_isosurf) 301 | bpy.utils.register_class(IsoSurferPanel) 302 | bpy.types.INFO_MT_mesh_add.append(add_isosurf_button) 303 | bpy.utils.register_module(__name__) 304 | bpy.types.Object.IsoSurf = CollectionProperty(type=IsoSurf) 305 | bpy.types.Object.IsoSurf_index = IntProperty() 306 | bpy.types.Object.IsoSurf_res = FloatProperty() 307 | bpy.types.Object.IsoSurf_preview = BoolProperty() 308 | bpy.types.Scene.IsoSurf_context = StringProperty(default = "WINDOW") 309 | 310 | bpy.types.Scene.isosurface_voxelsize = bpy.props.FloatProperty(name="isosurface_voxelsize", default=0.025, min=MIN_VXSIZE, max=1.0) 311 | bpy.types.Scene.isosurface_sphereradius = bpy.props.FloatProperty(name="isosurface_sphereradius", default=0.05, min=MIN_VXSIZE*2.0, max=2.0) 312 | bpy.types.Scene.isosurface_smoothsteps = bpy.props.IntProperty(name="isosurface_smoothsteps", default=2, min=0, max=20) 313 | 314 | 315 | 316 | def unregister(): 317 | bpy.utils.unregister_class(OBJECT_OT_add_isosurf) 318 | bpy.utils.unregister_class(IsoSurferPanel) 319 | bpy.types.INFO_MT_mesh_add.remove(add_isosurf_button) 320 | bpy.utils.unregister_module(__name__) 321 | del bpy.types.Object.IsoSurf 322 | 323 | 324 | if "isosurf_frame" not in [i.__name__ for i in bpy.app.handlers.frame_change_post]: 325 | print('create isosurfer handlers...') 326 | print(bpy.app.handlers.frame_change_post) 327 | bpy.app.handlers.persistent(isosurf_frame) 328 | bpy.app.handlers.frame_change_post.append(isosurf_frame) 329 | bpy.app.handlers.persistent(isosurf_prerender) 330 | bpy.app.handlers.render_pre.append(isosurf_prerender) 331 | bpy.app.handlers.persistent(isosurf_postrender) 332 | bpy.app.handlers.render_post.append(isosurf_postrender) 333 | bpy.app.handlers.render_cancel.append(isosurf_postrender) 334 | bpy.app.handlers.render_complete.append(isosurf_postrender) 335 | print('isosurfer handler created successfully!') 336 | 337 | -------------------------------------------------------------------------------- /view_active_in_outliner.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | bl_info = { 4 | "category": "Object", 5 | "name": "View active object in outliner", 6 | "blender": (3, 1, 0), 7 | "author": "ambi", 8 | "version": (1, 1), 9 | "location": "View3D > Object > View active object in outliner", 10 | "description": "View active object in outliner", 11 | } 12 | 13 | previous_selection = None 14 | mouse_in_view3d = False 15 | 16 | 17 | def view_act_object_in_outliner(): 18 | global previous_selection 19 | if bpy.context.active_object != previous_selection: 20 | previous_selection = bpy.context.active_object 21 | bpy.ops.wm.mouse_position("INVOKE_DEFAULT") 22 | if mouse_in_view3d: 23 | for screen in bpy.data.screens: 24 | for area in screen.areas: 25 | if area.type == "OUTLINER": 26 | region = next((r for r in area.regions if r.type == "WINDOW")) 27 | if region is not None: 28 | m = {"area": area, "region": region} 29 | bpy.ops.outliner.show_active(m) 30 | return 0.1 31 | 32 | 33 | def sync_toggle(self, context): 34 | if context.scene.sync_view3d_outliner: 35 | bpy.app.timers.register(view_act_object_in_outliner, first_interval=0.1) 36 | else: 37 | bpy.app.timers.unregister(view_act_object_in_outliner) 38 | 39 | 40 | def is_in_view3d_area(x, y): 41 | for area in bpy.context.screen.areas: 42 | if area.type == "VIEW_3D": 43 | x_in_range = x >= area.x and x <= area.x + area.width 44 | y_in_range = y >= area.y and y <= area.y + area.height 45 | return x_in_range and y_in_range 46 | return False 47 | 48 | 49 | class SimpleMouseOperator(bpy.types.Operator): 50 | bl_idname = "wm.mouse_position" 51 | bl_label = "Mouse location" 52 | 53 | x: bpy.props.IntProperty() 54 | y: bpy.props.IntProperty() 55 | 56 | def execute(self, context): 57 | global mouse_in_view3d 58 | mouse_in_view3d = is_in_view3d_area(self.x, self.y) 59 | # Mouse coords: self.x, self.y 60 | return {"FINISHED"} 61 | 62 | def invoke(self, context, event): 63 | self.x = event.mouse_x 64 | self.y = event.mouse_y 65 | return self.execute(context) 66 | 67 | 68 | # create function to add a toggle button to the 3d view header 69 | def draw_header(self, context): 70 | layout = self.layout 71 | if context.mode == "OBJECT": 72 | layout.label(text="Sync outliner") 73 | layout.prop(context.scene, "sync_view3d_outliner", text="") 74 | 75 | 76 | def register(): 77 | bpy.utils.register_class(SimpleMouseOperator) 78 | bpy.types.Scene.sync_view3d_outliner = bpy.props.BoolProperty( 79 | name="Sync View3D and Outliner", 80 | description="Sync View3D and Outliner", 81 | default=True, 82 | update=sync_toggle, 83 | ) 84 | bpy.types.VIEW3D_HT_header.append(draw_header) 85 | bpy.app.timers.register(view_act_object_in_outliner, first_interval=1.0) 86 | 87 | 88 | def unregister(): 89 | bpy.utils.unregister_class(SimpleMouseOperator) 90 | bpy.types.VIEW3D_HT_header.remove(draw_header) 91 | del bpy.types.Scene.sync_view3d_outliner 92 | bpy.app.timers.unregister(view_act_object_in_outliner) 93 | --------------------------------------------------------------------------------