├── .gitignore ├── README.md ├── slap_it.py └── slap_it_alternate.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.blend 2 | *.blend1 3 | *.pyc 4 | /video 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slap it! 2 | A simple decals addon for Blender 2.8+ 3 | 4 | Demo: https://www.youtube.com/watch?v=meq--NcBFFM 5 | 6 | ## Download and installation 7 | 8 | 1. Download latest `slap_it.py` from https://github.com/artempoletsky/blender_slap_it/releases 9 | 2. In Blenders preferences install the addon from the file 10 | 3. Enable built-in Blenders addon `Import images as planes` 11 | 12 | ## Usage 13 | 14 | 1. In object mode press shift + A > Image > Images as planes. Add decal image you want to scene. 15 | 2. In snapping settings enable snapping, Snap to `Face`, Snap with `Active`, Align Rotation to Target. Use material preview shading in the viewport. Snap image to place you want. 16 | 3. With image selected select target object. Open context menu (W or Right Click). Press `Slap it!`. 17 | 4. Adjust appearance of the decal via shader editor. Adjust height offset of the decal via strength of the displace modifier. 18 | 19 | The addon uses blenders knife project feature. So you can use it not only on plains but on any object that can be projected. If there are issues with the knife project, they appear in the script. 20 | 21 | You can support me on Gumroad, if you like my work. https://gum.co/litSq 22 | 23 | -------------------------------------------------------------------------------- /slap_it.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "Slap it!", 3 | "author": "Artem Poletsky", 4 | "version": (1, 1, 0), 5 | "blender": (3, 0, 3), 6 | # "location": "View3D > Add > Mesh > New Object", 7 | "description": "A simple decals addon", 8 | "warning": "", 9 | "wiki_url": "", 10 | "category": "Mesh", 11 | } 12 | 13 | import bpy 14 | 15 | def view3d_find( return_area = False ): 16 | # returns first 3d view, normally we get from context 17 | for area in bpy.context.window.screen.areas: 18 | if area.type == 'VIEW_3D': 19 | v3d = area.spaces[0] 20 | rv3d = v3d.region_3d 21 | for region in area.regions: 22 | if region.type == 'WINDOW': 23 | if return_area: return region, rv3d, v3d, area 24 | return region, rv3d, v3d 25 | return None, None 26 | 27 | def knife_override(selected, edit): 28 | 29 | 30 | region, rv3d, v3d, area = view3d_find(True) 31 | 32 | # Define context override dictionary for overriding the knife_project operator's context 33 | override = { 34 | 'scene' : bpy.context.scene, 35 | 'region' : region, 36 | 'area' : area, 37 | 'space' : v3d, 38 | 'active_object' : bpy.context.object, 39 | 'window' : bpy.context.window, 40 | 'screen' : bpy.context.screen, 41 | 'selected_objects' : selected, 42 | 'edit_object' : edit 43 | } 44 | 45 | return override 46 | 47 | 48 | class SlapItOperator(bpy.types.Operator): 49 | """Slap decal on mesh""" 50 | bl_idname = "object.slap_it_operator" 51 | bl_label = "Slap it!" 52 | 53 | @classmethod 54 | def poll(cls, context): 55 | return len(context.selected_objects) == 2 56 | 57 | def execute(self, context): 58 | 59 | C = bpy.context 60 | target_object = C.object 61 | selected = C.selected_objects 62 | 63 | selected.remove(target_object) 64 | source_decal_object = selected[0] 65 | source_decal_object.select_set(False) 66 | 67 | C.view_layer.objects.active = target_object 68 | bpy.ops.object.duplicate() 69 | decal_object = C.object 70 | bpy.ops.object.convert(target='MESH') 71 | 72 | bpy.ops.object.select_all(action='DESELECT') 73 | 74 | source_decal_object.select_set(True) 75 | C.view_layer.objects.active = source_decal_object 76 | bpy.ops.object.editmode_toggle() 77 | bpy.ops.mesh.select_all(action='SELECT') 78 | bpy.ops.object.editmode_toggle() 79 | 80 | override = knife_override([source_decal_object, decal_object], decal_object) 81 | 82 | old_camera = C.scene.camera 83 | 84 | bpy.ops.object.camera_add() 85 | camera = C.object 86 | C.scene.camera = camera 87 | bpy.ops.view3d.camera_to_view() 88 | camera.select_set(False) 89 | 90 | # move the camera to the top decal view 91 | source_decal_object.select_set(True) 92 | C.view_layer.objects.active = source_decal_object 93 | bpy.ops.view3d.view_axis(type='TOP', align_active=True, relative=False) 94 | bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) # redraw hack 95 | 96 | # prepare selection for the knife project 97 | source_decal_object.select_set(False) 98 | decal_object.select_set(True) 99 | C.view_layer.objects.active = decal_object 100 | bpy.ops.object.editmode_toggle() 101 | source_decal_object.select_set(True) 102 | 103 | bpy.ops.mesh.knife_project(override); 104 | 105 | 106 | bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT') 107 | bpy.ops.mesh.select_all(action='INVERT') 108 | #bpy.ops.mesh.select_linked_pick(deselect=True, delimit=set(), index=1) 109 | bpy.ops.mesh.delete(type='VERT') 110 | bpy.ops.object.editmode_toggle() 111 | C.view_layer.objects.active = source_decal_object 112 | decal_object.select_set(True) 113 | bpy.ops.object.make_links_data(type='MATERIAL') 114 | 115 | C.view_layer.objects.active = decal_object 116 | source_decal_object.select_set(False) 117 | bpy.ops.object.editmode_toggle() 118 | bpy.ops.mesh.select_all(action='SELECT') 119 | 120 | override = {'area': override['area'], 'region': override['region'], 'edit_object': decal_object} 121 | bpy.ops.uv.project_from_view(override, camera_bounds = False, scale_to_bounds=True, correct_aspect=True) 122 | 123 | bpy.ops.object.editmode_toggle() 124 | bpy.ops.object.select_all(action='DESELECT') 125 | 126 | 127 | decal_object.name = 'Slap ' + source_decal_object.name 128 | 129 | decal_object.select_set(True) 130 | C.view_layer.objects.active = decal_object 131 | 132 | 133 | bpy.ops.object.modifier_add(type='DISPLACE') 134 | 135 | mod = decal_object.modifiers["Displace"] 136 | mod.strength = 0.001 137 | 138 | bpy.ops.object.modifier_add(type='DATA_TRANSFER') 139 | 140 | decal_object.data.use_auto_smooth = True 141 | 142 | decal_object.visible_shadow = False #turns off shadow on the decal in Cycles 143 | decal_object.active_material.shadow_method = 'NONE' #in Eevee 144 | 145 | mod = decal_object.modifiers["DataTransfer"] 146 | mod.object = target_object 147 | mod.use_loop_data = True 148 | mod.data_types_loops = {'CUSTOM_NORMAL'} 149 | mod.loop_mapping = 'POLYINTERP_NEAREST' 150 | 151 | decal_object.select_set(False) 152 | 153 | camera.select_set(True) 154 | C.view_layer.objects.active = camera 155 | 156 | # bpy.ops.action.view_frame(override) 157 | bpy.ops.view3d.view_camera(override) 158 | bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) 159 | bpy.ops.object.delete() 160 | if old_camera: 161 | C.scene.camera = old_camera 162 | source_decal_object.select_set(True) 163 | C.view_layer.objects.active = source_decal_object 164 | 165 | return {'FINISHED'} 166 | 167 | def menu_func(self, context): 168 | layout = self.layout 169 | layout.separator() 170 | 171 | layout.operator_context = "INVOKE_DEFAULT" 172 | layout.operator('object.slap_it_operator', text='Slap it!') 173 | 174 | 175 | def register(): 176 | bpy.utils.register_class(SlapItOperator) 177 | 178 | bpy.types.VIEW3D_MT_object_context_menu.append(menu_func) 179 | 180 | 181 | def unregister(): 182 | bpy.utils.unregister_class(SlapItOperator) 183 | 184 | bpy.types.VIEW3D_MT_object_context_menu.remove(menu_func) 185 | 186 | if __name__ == "__main__": 187 | register() 188 | -------------------------------------------------------------------------------- /slap_it_alternate.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "Slap it!", 3 | "author": "Artem Poletsky", 4 | "version": (1, 0), 5 | "blender": (2, 80, 0), 6 | "location": "View3D > Add > Mesh > New Object", 7 | "description": "A simple decals addon", 8 | "warning": "", 9 | "wiki_url": "", 10 | "category": "Mesh", 11 | } 12 | 13 | import bpy 14 | 15 | class SlapItOperator(bpy.types.Operator): 16 | """Slap decal on mesh""" 17 | bl_idname = "object.slap_it_operator" 18 | bl_label = "Slap it!" 19 | 20 | @classmethod 21 | def poll(cls, context): 22 | return len(context.selected_objects) == 2 23 | 24 | def execute(self, context): 25 | 26 | C = bpy.context 27 | target_object = C.object 28 | selected = C.selected_objects 29 | 30 | selected.remove(target_object) 31 | source_decal_object = selected[0] 32 | target_object.select_set(False) 33 | 34 | C.view_layer.objects.active = source_decal_object 35 | bpy.ops.object.duplicate() 36 | 37 | active_object = C.object 38 | C.object.name = 'Slap ' + source_decal_object.name 39 | 40 | bpy.ops.object.modifier_add(type='SUBSURF') 41 | 42 | mod = active_object.modifiers["Subdivision"] 43 | mod.render_levels = 5 44 | mod.levels = 5 45 | mod.subdivision_type = 'SIMPLE' 46 | 47 | 48 | bpy.ops.object.modifier_add(type='SHRINKWRAP') 49 | 50 | mod = active_object.modifiers["Shrinkwrap"] 51 | mod.offset = 0.005 52 | mod.target = target_object 53 | mod.wrap_method = 'TARGET_PROJECT' 54 | mod.wrap_mode = 'ABOVE_SURFACE' 55 | 56 | bpy.ops.object.modifier_add(type='DECIMATE') 57 | 58 | mod = active_object.modifiers["Decimate"] 59 | mod.decimate_type = 'DISSOLVE' 60 | mod.angle_limit = 0.1 61 | 62 | bpy.ops.object.modifier_add(type='DATA_TRANSFER') 63 | 64 | active_object.data.use_auto_smooth = True 65 | mod = active_object.modifiers["DataTransfer"] 66 | mod.object = target_object 67 | mod.use_loop_data = True 68 | mod.data_types_loops = {'CUSTOM_NORMAL'} 69 | mod.loop_mapping = 'POLYINTERP_NEAREST' 70 | 71 | active_object.select_set(False) 72 | source_decal_object.select_set(True) 73 | C.view_layer.objects.active = source_decal_object 74 | 75 | return {'FINISHED'} 76 | 77 | def menu_func(self, context): 78 | layout = self.layout 79 | layout.separator() 80 | 81 | layout.operator_context = "INVOKE_DEFAULT" 82 | layout.operator('object.slap_it_operator', text='Slap it!') 83 | 84 | 85 | def register(): 86 | bpy.utils.register_class(SlapItOperator) 87 | 88 | bpy.types.VIEW3D_MT_object_context_menu.append(menu_func) 89 | 90 | 91 | def unregister(): 92 | bpy.utils.unregister_class(SlapItOperator) 93 | 94 | bpy.types.VIEW3D_MT_object_context_menu.remove(menu_func) 95 | 96 | if __name__ == "__main__": 97 | register() 98 | --------------------------------------------------------------------------------