From 6fa56294030dd97a0abae7bdaa178552ec75f234 Mon Sep 17 00:00:00 2001 From: akaneyu Date: Fri, 24 May 2024 00:37:08 +0900 Subject: [PATCH] Implement normal map generator --- addon/__init__.py | 4 +- addon/operators.py | 63 ++++++++++++++++++++++++ addon/ui.py | 118 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 184 insertions(+), 1 deletion(-) diff --git a/addon/__init__.py b/addon/__init__.py index 174a58f..b43c7cf 100644 --- a/addon/__init__.py +++ b/addon/__init__.py @@ -18,7 +18,7 @@ bl_info = { "name": "Image Editor Plus", "author": "akaneyu", - "version": (1, 8, 1), + "version": (1, 9, 0), "blender": (3, 3, 0), "location": "Image", "warning": "", @@ -80,6 +80,7 @@ classes = [ operators.IMAGE_EDITOR_PLUS_OT_add_noise, operators.IMAGE_EDITOR_PLUS_OT_pixelize, operators.IMAGE_EDITOR_PLUS_OT_make_seamless, + operators.IMAGE_EDITOR_PLUS_OT_normal_map, ui.IMAGE_EDITOR_PLUS_OT_scale_dialog, ui.IMAGE_EDITOR_PLUS_OT_change_canvas_size_dialog, ui.IMAGE_EDITOR_PLUS_OffsetPropertyGroup, @@ -94,6 +95,7 @@ classes = [ ui.IMAGE_EDITOR_PLUS_OT_add_noise_dialog, ui.IMAGE_EDITOR_PLUS_OT_pixelize_dialog, ui.IMAGE_EDITOR_PLUS_OT_make_seamless_dialog, + ui.IMAGE_EDITOR_PLUS_OT_normal_map_dialog, ui.IMAGE_EDITOR_PLUS_UL_layer_list, ui.IMAGE_EDITOR_PLUS_MT_edit_menu, ui.IMAGE_EDITOR_PLUS_MT_layers_menu, diff --git a/addon/operators.py b/addon/operators.py index bee9e2c..3ed921b 100644 --- a/addon/operators.py +++ b/addon/operators.py @@ -1633,3 +1633,66 @@ class IMAGE_EDITOR_PLUS_OT_make_seamless(bpy.types.Operator): app.refresh_image(context) return {'FINISHED'} + +class IMAGE_EDITOR_PLUS_OT_normal_map(bpy.types.Operator): + """Apply settings""" + bl_idname = 'image_editor_plus.normal_map' + bl_label = "Normal Map" + scale: bpy.props.FloatProperty() + flip_x: bpy.props.BoolProperty() + flip_y: bpy.props.BoolProperty() + full_z: bpy.props.BoolProperty() + + def execute(self, context): + scale = self.scale + flip_x = self.flip_x + flip_y = self.flip_y + full_z = self.full_z + + pixels, hsl = app.get_image_cache() + if pixels is None: + return {'CANCELLED'} + + selection = app.get_target_selection(context) + if selection: + target_hsl = hsl[selection[0][1]:selection[1][1], + selection[0][0]:selection[1][0]] + elif selection == []: + return {'CANCELLED'} + else: + target_hsl = hsl + + target_width, target_height = target_hsl.shape[1], target_hsl.shape[0] + + target_hsl = np.pad(target_hsl, ((1, 1), (1, 1), (0, 0)), 'edge') + + new_pixels = np.zeros((target_height, target_width, 4)) + nx = (target_hsl[1:target_height + 1, 0:target_width, 2] + - target_hsl[1:target_height + 1, 2:target_width + 2, 2]) * scale + ny = (target_hsl[0:target_height, 1:target_width + 1, 2] + - target_hsl[2:target_height + 2, 1:target_width + 1, 2]) * scale + nz = 1.0 / np.sqrt(nx * nx + ny * ny + 1.0) + + nx *= nz + ny *= nz + + new_pixels = np.dstack([ + 0.5 + (-0.5 if flip_x else 0.5) * nx, + 0.5 + (-0.5 if flip_y else 0.5) * ny, + (0 if full_z else 0.5) + (1.0 if full_z else 0.5) * nz, + ]) + + new_pixels = np.clip(new_pixels, 0, None) + + if selection: + pixels[selection[0][1]:selection[1][1], + selection[0][0]:selection[1][0], 0:3] = new_pixels + else: + pixels[:,:,0:3] = new_pixels + + img = app.get_target_image(context) + utils.write_pixels_to_image(img, pixels) + + app.refresh_image(context) + + return {'FINISHED'} diff --git a/addon/ui.py b/addon/ui.py index c277e37..2368336 100644 --- a/addon/ui.py +++ b/addon/ui.py @@ -1253,6 +1253,117 @@ class IMAGE_EDITOR_PLUS_OT_make_seamless_dialog(bpy.types.Operator): row.column() row.prop(self, 'preview', text='Preview', toggle=True, icon='VIEWZOOM') +def preview_normal_map_properties(self, context): + if self.preview: + update_normal_map_properties(self, context) + else: + img = app.get_target_image(context) + if img: + app.revert_image_cache(img) + app.refresh_image(context) + +def reset_normal_map_properties(self, context): + if self.reset: + self.property_unset('scale') + self.property_unset('flip_x') + self.property_unset('flip_y') + self.property_unset('full_z') + update_normal_map_properties(self, context) + + self.reset = False + +def update_normal_map_properties(self, context): + if self.preview: + bpy.ops.image_editor_plus.normal_map('EXEC_DEFAULT', False, + scale=self.scale, + flip_x=self.flip_x, + flip_y=self.flip_y, + full_z=self.full_z) + +class IMAGE_EDITOR_PLUS_OT_normal_map_dialog(bpy.types.Operator): + """Generate a normal map""" + bl_idname = 'image_editor_plus.normal_map_dialog' + bl_label = "Normal Map" + preview: bpy.props.BoolProperty(default=True, options={'SKIP_SAVE'}, + update=preview_normal_map_properties) + reset: bpy.props.BoolProperty(options={'SKIP_SAVE'}, update=reset_normal_map_properties) + scale: bpy.props.FloatProperty(name='Scale', subtype='FACTOR', + min=0, soft_max=250.0, default=10.0, options={'SKIP_SAVE'}, + update=update_normal_map_properties) + flip_x: bpy.props.BoolProperty(name='Flip X', default=False, options={'SKIP_SAVE'}, + update=update_normal_map_properties) + flip_y: bpy.props.BoolProperty(name='Flip Y', default=False, options={'SKIP_SAVE'}, + update=update_normal_map_properties) + full_z: bpy.props.BoolProperty(name='Full Range for Z', default=False, options={'SKIP_SAVE'}, + update=update_normal_map_properties) + + def invoke(self, context, event): + session = app.get_session() + + wm = context.window_manager + + img = app.get_target_image(context) + if not img: + return {'CANCELLED'} + + app.cache_image(img, need_hsl=True) + + layer = app.get_active_layer(context) + if layer: + session.cached_layer_location = layer.location[:] + + update_normal_map_properties(self, context) + + return wm.invoke_props_dialog(self) + + def execute(self, context): + bpy.ops.image_editor_plus.normal_map('EXEC_DEFAULT', False, + scale=self.scale, + flip_x=self.flip_x, + flip_y=self.flip_y, + full_z=self.full_z) + + return {'FINISHED'} + + def cancel(self, context): + img = app.get_target_image(context) + if not img: + return {'CANCELLED'} + + app.revert_image_cache(img) + app.clear_image_cache() + app.refresh_image(context) + + def draw(self, context): + layout = self.layout + + row = layout.split(align=True) + row.column() + row.prop(self, 'preview', text='Preview', toggle=True, icon='VIEWZOOM') + + layout.separator() + + row = layout.split(align=True) + row.alignment = 'RIGHT' + row.label(text='Scale') + row.prop(self, 'scale', text='') + + row = layout.split(align=True) + row.column() + row.prop(self, 'flip_x') + + row = layout.split(align=True) + row.column() + row.prop(self, 'flip_y') + + row = layout.split(align=True) + row.column() + row.prop(self, 'full_z') + + row = layout.split(factor=0.7) + row.column() + row.prop(self, 'reset', text='Reset', toggle=True) + class IMAGE_EDITOR_PLUS_UL_layer_list(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): icon_value = 0 @@ -1432,6 +1543,9 @@ class IMAGE_EDITOR_PLUS_MT_filter_menu(bpy.types.Menu): layout.operator(IMAGE_EDITOR_PLUS_OT_make_seamless_dialog.bl_idname, text='Make Seamless...', icon_value=get_icon_id('filter')) + layout.operator(IMAGE_EDITOR_PLUS_OT_normal_map_dialog.bl_idname, text='Normal Map...', + icon_value=get_icon_id('filter')) + class IMAGE_EDITOR_PLUS_PT_select_panel(bpy.types.Panel): bl_label = "Select" bl_space_type = "IMAGE_EDITOR" @@ -1793,3 +1907,7 @@ class IMAGE_EDITOR_PLUS_PT_filter_panel(bpy.types.Panel): row = layout.row() row.operator(IMAGE_EDITOR_PLUS_OT_make_seamless_dialog.bl_idname, text='Make Seamless...', icon_value=get_icon_id('filter')) + + row = layout.row() + row.operator(IMAGE_EDITOR_PLUS_OT_normal_map_dialog.bl_idname, text='Normal Map...', + icon_value=get_icon_id('filter'))