1- import bpy
2- from bpy .types import (Panel , Operator , PropertyGroup )
3- from bpy .props import (EnumProperty , PointerProperty )
4- import pathlib
5- import os
6-
7- from . import infer
8- if "bpy" in locals ():
9- import importlib
10- importlib .reload (infer )
11-
121bl_info = {
132 'name' : 'DeepBump' ,
143 'description' : 'Generates normal maps from image textures' ,
154 'author' : 'Hugo Tini' ,
16- 'version' : (0 , 1 , 0 ),
17- 'blender' : (2 , 82 , 0 ),
5+ 'version' : (2 , 0 , 0 ),
6+ 'blender' : (3 , 0 , 0 ),
187 'location' : 'Node Editor > DeepBump' ,
198 'category' : 'Material' ,
20- 'warning' : 'Extra Python dependencies must be installed first, check the readme.'
9+ 'warning' : 'Requires installation of dependencies' ,
10+ 'doc_url' : 'https://github.com/HugoTini/DeepBump/blob/master/readme.md'
2111}
2212
13+
14+ import bpy
15+ from bpy .types import (Panel , Operator , PropertyGroup )
16+ from bpy .props import (EnumProperty , PointerProperty )
17+ import pathlib
18+ import os
19+ import subprocess
20+ import sys
21+ import importlib
22+ from collections import namedtuple
23+
24+
2325# ------------------------------------------------------------------------
24- # Scene Properties
26+ # Dependencies management utils
27+ # ------------------------------------------------------------------------
28+
29+
30+ # Python dependencies management helpers from :
31+ # https://github.com/robertguetzkow/blender-python-examples/tree/master/add_ons/install_dependencies
32+ Dependency = namedtuple ('Dependency' , ['module' , 'package' , 'name' ])
33+ dependencies = (Dependency (module = 'onnxruntime' , package = None , name = 'ort' ),
34+ Dependency (module = 'numpy' , package = None , name = 'np' ))
35+ dependencies_installed = False
36+
37+
38+ def import_module (module_name , global_name = None , reload = True ):
39+ if global_name is None :
40+ global_name = module_name
41+ if global_name in globals ():
42+ importlib .reload (globals ()[global_name ])
43+ else :
44+ globals ()[global_name ] = importlib .import_module (module_name )
45+
46+
47+ def install_pip ():
48+ try :
49+ # Check if pip is already installed
50+ subprocess .run ([sys .executable , '-m' , 'pip' , '--version' ], check = True )
51+ except subprocess .CalledProcessError :
52+ import ensurepip
53+ ensurepip .bootstrap ()
54+ os .environ .pop ("PIP_REQ_TRACKER" , None )
55+
56+
57+ def install_and_import_module (module_name , package_name = None , global_name = None ):
58+ if package_name is None :
59+ package_name = module_name
60+ if global_name is None :
61+ global_name = module_name
62+ # Create a copy of the environment variables and modify them for the subprocess call
63+ environ_copy = dict (os .environ )
64+ environ_copy ['PYTHONNOUSERSITE' ] = '1'
65+ subprocess .run ([sys .executable , '-m' , 'pip' , 'install' , package_name ], check = True , env = environ_copy )
66+ # The installation succeeded, attempt to import the module again
67+ import_module (module_name , global_name )
68+
69+
70+ # ------------------------------------------------------------------------
71+ # Scene properties
2572# ------------------------------------------------------------------------
2673
2774
@@ -35,120 +82,115 @@ class DeepBumpProperties(PropertyGroup):
3582 ('BIG' , 'Big' , 'Big overlap between tiles.' )]
3683 )
3784
85+
3886# ------------------------------------------------------------------------
3987# Operators
4088# ------------------------------------------------------------------------
4189
4290
43- class WM_OT_DeepBumpOperator (Operator ):
44- '''Generates a normal map from an image. Settings in DeepBump panel (Node Editor)'''
45-
91+ class DEEPBUMP_OT_DeepBumpOperator (Operator ):
92+ bl_idname = 'deepbump.operator'
4693 bl_label = 'DeepBump'
47- bl_idname = 'wm.deep_bump'
94+ bl_description = ('Generates a normal map from an image.'
95+ 'Settings are in DeepBump panel (Node Editor)' )
96+
4897 progress_started = False
4998 ort_session = None
5099
51100 @classmethod
52101 def poll (self , context ):
53- selected_node_type = context .active_node .bl_idname
54- return (context .area .type == 'NODE_EDITOR' ) and (selected_node_type == 'ShaderNodeTexImage' )
102+ if context .active_node is not None :
103+ selected_node_type = context .active_node .bl_idname
104+ return (context .area .type == 'NODE_EDITOR' ) and (selected_node_type == 'ShaderNodeTexImage' )
105+ return False
55106
56107 def progress_print (self , current , total ):
57108 wm = bpy .context .window_manager
58109 if self .progress_started :
59110 wm .progress_update (current )
60- print ('{ }/{}' . format ( current , total ) )
111+ print (f'DeepBump : { current } /{ total } ' )
61112 else :
62113 wm .progress_begin (0 , total )
63114 self .progress_started = True
64115
65116 def execute (self , context ):
66- # make sure dependencies are installed
67- try :
68- import numpy as np
69- import onnxruntime as ort
70- from . import infer
71- except ImportError as e :
72- self .report ({'WARNING' }, 'Dependencies missing, check readme.' )
73- print (e )
74- return {'CANCELLED' }
75-
76- # get input image from selected node
117+ # Get input image from selected node
77118 input_node = context .active_node
78119 input_img = input_node .image
79- if input_img == None :
120+ if input_img is None :
80121 self .report (
81122 {'WARNING' }, 'Selected image node must have an image assigned to it.' )
82123 return {'CANCELLED' }
83124
84- # convert to C,H,W numpy array
125+ # Convert to C,H,W numpy array
85126 width = input_img .size [0 ]
86127 height = input_img .size [1 ]
87128 channels = input_img .channels
88129 img = np .array (input_img .pixels )
89130 img = np .reshape (img , (channels , width , height ), order = 'F' )
90131 img = np .transpose (img , (0 , 2 , 1 ))
91- # flip height
132+ # Flip height
92133 img = np .flip (img , axis = 1 )
93134
94- # remove alpha & convert to grayscale
135+ # Remove alpha & convert to grayscale
95136 img = np .mean (img [0 :3 ], axis = 0 , keepdims = True )
96137
97- # split image in tiles
138+ # Split image in tiles
98139 tile_size = (256 , 256 )
99140 OVERLAP = context .scene .deep_bump_tool .tiles_overlap_enum
100141 overlaps = {'SMALL' : 20 , 'MEDIUM' : 50 , 'BIG' : 124 }
101142 stride_size = (tile_size [0 ]- overlaps [OVERLAP ],
102143 tile_size [1 ]- overlaps [OVERLAP ])
103- print ('tilling' )
144+ print ('DeepBump : tilling' )
104145 tiles , paddings = infer .tiles_split (img , tile_size , stride_size )
105146
106- # load model (if not already loaded)
107- if self .ort_session == None :
108- print ('loading model' )
147+ # Load model (if not already loaded)
148+ if self .ort_session is None :
149+ print ('DeepBump : loading model' )
109150 addon_path = str (pathlib .Path (__file__ ).parent .absolute ())
110151 self .ort_session = ort .InferenceSession (
111152 addon_path + '/deepbump256.onnx' )
153+ self .ort_session
112154
113- # predict normal map for each tile
114- print ('generating' )
155+ # Predict normal map for each tile
156+ print ('DeepBump : generating' )
115157 self .progress_started = False
116158 pred_tiles = infer .tiles_infer (
117159 tiles , self .ort_session , progress_callback = self .progress_print )
118160
119- # merge tiles
120- print ('merging' )
161+ # Merge tiles
162+ print ('DeepBump : merging' )
121163 pred_img = infer .tiles_merge (
122164 pred_tiles , stride_size , (3 , img .shape [1 ], img .shape [2 ]), paddings )
123165
124- # normalize each pixel to unit vector
166+ # Normalize each pixel to unit vector
125167 pred_img = infer .normalize (pred_img )
126168
127- # create new image datablock
169+ # Create new image datablock
128170 img_name = os .path .splitext (input_img .name )
129171 normal_name = img_name [0 ] + '_normal' + img_name [1 ]
130172 normal_img = bpy .data .images .new (
131173 normal_name , width = width , height = height )
132174 normal_img .colorspace_settings .name = 'Non-Color'
133175
134- # flip height
176+ # Flip height
135177 pred_img = np .flip (pred_img , axis = 1 )
136- # add alpha channel
178+ # Add alpha channel
137179 pred_img = np .concatenate (
138180 [pred_img , np .ones ((1 , height , width ))], axis = 0 )
139- # flatten to array
181+ # Flatten to array
140182 pred_img = np .transpose (pred_img , (0 , 2 , 1 )).flatten ('F' )
141- # write to image block
183+ # Write to image block
142184 normal_img .pixels = pred_img
143185
144- # create new node for normal map
186+ # Create new node for normal map
145187 normal_node = context .material .node_tree .nodes .new (
146188 type = 'ShaderNodeTexImage' )
147189 normal_node .location = input_node .location
148190 normal_node .location [1 ] -= input_node .width * 1.2
149191 normal_node .image = normal_img
150192
151- # create normal vector node & link nodes
193+ # Create normal vector node & link nodes
152194 normal_vec_node = context .material .node_tree .nodes .new (
153195 type = 'ShaderNodeNormalMap' )
154196 normal_vec_node .location = normal_node .location
@@ -157,24 +199,57 @@ def execute(self, context):
157199 links .new (normal_node .outputs ['Color' ],
158200 normal_vec_node .inputs ['Color' ])
159201
160- # if input image was linked to a BSDF, link to BSDF normal slot
202+ # If input image was linked to a BSDF, link to BSDF normal slot
161203 if input_node .outputs ['Color' ].is_linked :
162204 if len (input_node .outputs ['Color' ].links ) == 1 :
163205 to_node = input_node .outputs ['Color' ].links [0 ].to_node
164206 if to_node .bl_idname == 'ShaderNodeBsdfPrincipled' :
165207 links .new (
166208 normal_vec_node .outputs ['Normal' ], to_node .inputs ['Normal' ])
167209
210+ print ('DeepBump : done' )
168211 return {'FINISHED' }
169212
213+
214+ class DEEPBUMP_OT_install_dependencies (bpy .types .Operator ):
215+ bl_idname = 'deepbump.install_dependencies'
216+ bl_label = 'Install dependencies'
217+ bl_description = 'Downloads and installs the required python packages for this add-on.'
218+ bl_options = {'REGISTER' , 'INTERNAL' }
219+
220+ @classmethod
221+ def poll (self , context ):
222+ # Deactivate when dependencies have been installed
223+ return not dependencies_installed
224+
225+ def execute (self , context ):
226+ try :
227+ install_pip ()
228+ for dependency in dependencies :
229+ install_and_import_module (module_name = dependency .module ,
230+ package_name = dependency .package ,
231+ global_name = dependency .name )
232+ except (subprocess .CalledProcessError , ImportError ) as err :
233+ self .report ({'ERROR' }, str (err ))
234+ return {'CANCELLED' }
235+
236+ global dependencies_installed
237+ dependencies_installed = True
238+
239+ # Register the panels, operators, etc. since dependencies are installed
240+ register_functionality ()
241+
242+ return {"FINISHED" }
243+
244+
170245# ------------------------------------------------------------------------
171- # Panel in Object Mode
246+ # UI (DeepBump panel & addon install dependencies button)
172247# ------------------------------------------------------------------------
173248
174249
175- class OBJECT_PT_DeepBumpPanel (Panel ):
250+ class DEEPBUMP_PT_DeepBumpPanel (Panel ):
251+ bl_idname = 'DEEPBUMP_PT_DeepBumpPanel'
176252 bl_label = 'DeepBump'
177- bl_idname = 'OBJECT_PT_DeepBumpPanel'
178253 bl_space_type = 'NODE_EDITOR'
179254 bl_region_type = 'UI'
180255 bl_category = 'DeepBump'
@@ -192,31 +267,75 @@ def draw(self, context):
192267 layout .prop (deep_bump_tool , 'tiles_overlap_enum' , text = '' )
193268
194269 layout .separator ()
195- layout .operator ('wm.deep_bump' , text = 'Generate Normal Map' )
270+ layout .operator ('deepbump.operator' , text = 'Generate Normal Map' )
271+
272+
273+ class DEEPBUMP_preferences (bpy .types .AddonPreferences ):
274+ bl_idname = __name__
275+
276+ def draw (self , context ):
277+ layout = self .layout
278+ if dependencies_installed :
279+ layout .label (text = 'Required dependencies are installed' , icon = 'CHECKMARK' )
280+ else :
281+ layout .label (text = 'Installing dependencies requires internet and might take a few minutes' ,
282+ icon = 'INFO' )
283+ layout .operator (DEEPBUMP_OT_install_dependencies .bl_idname , icon = 'CONSOLE' )
284+
196285
197286# ------------------------------------------------------------------------
198287# Registration
199288# ------------------------------------------------------------------------
200289
201290
291+ # Classes for the addon actual functionality
202292classes = (
203293 DeepBumpProperties ,
204- WM_OT_DeepBumpOperator ,
205- OBJECT_PT_DeepBumpPanel
294+ DEEPBUMP_OT_DeepBumpOperator ,
295+ DEEPBUMP_PT_DeepBumpPanel
296+ )
297+ # Classes for downloading & installing dependencies
298+ preference_classes = (
299+ DEEPBUMP_OT_install_dependencies ,
300+ DEEPBUMP_preferences
206301)
207302
208303
209304def register ():
210- from bpy .utils import register_class
305+ global dependencies_installed
306+ dependencies_installed = False
307+
308+ for cls in preference_classes :
309+ bpy .utils .register_class (cls )
310+
311+ try :
312+ for dependency in dependencies :
313+ import_module (module_name = dependency .module , global_name = dependency .name )
314+ except ModuleNotFoundError :
315+ # Don't register other panels, operators etc.
316+ return
317+
318+ dependencies_installed = True
319+ register_functionality ()
320+
321+
322+ def register_functionality ():
211323 for cls in classes :
212- register_class (cls )
324+ bpy . utils . register_class (cls )
213325 bpy .types .Scene .deep_bump_tool = PointerProperty (type = DeepBumpProperties )
326+ from . import infer
327+ # Disable MS telemetry
328+ ort .disable_telemetry_events ()
214329
215330
216331def unregister ():
217- from bpy .utils import unregister_class
218- for cls in reversed (classes ):
219- unregister_class (cls )
332+ for cls in preference_classes :
333+ bpy .utils .unregister_class (cls )
334+
335+ if dependencies_installed :
336+ for cls in classes :
337+ bpy .utils .unregister_class (cls )
338+
220339 del bpy .types .Scene .deep_bump_tool
221340
222341
0 commit comments