Implement normal map generator

This commit is contained in:
akaneyu 2024-05-24 00:37:08 +09:00
parent d5797c313c
commit 6fa5629403
3 changed files with 184 additions and 1 deletions

View File

@ -18,7 +18,7 @@
bl_info = { bl_info = {
"name": "Image Editor Plus", "name": "Image Editor Plus",
"author": "akaneyu", "author": "akaneyu",
"version": (1, 8, 1), "version": (1, 9, 0),
"blender": (3, 3, 0), "blender": (3, 3, 0),
"location": "Image", "location": "Image",
"warning": "", "warning": "",
@ -80,6 +80,7 @@ classes = [
operators.IMAGE_EDITOR_PLUS_OT_add_noise, operators.IMAGE_EDITOR_PLUS_OT_add_noise,
operators.IMAGE_EDITOR_PLUS_OT_pixelize, operators.IMAGE_EDITOR_PLUS_OT_pixelize,
operators.IMAGE_EDITOR_PLUS_OT_make_seamless, 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_scale_dialog,
ui.IMAGE_EDITOR_PLUS_OT_change_canvas_size_dialog, ui.IMAGE_EDITOR_PLUS_OT_change_canvas_size_dialog,
ui.IMAGE_EDITOR_PLUS_OffsetPropertyGroup, ui.IMAGE_EDITOR_PLUS_OffsetPropertyGroup,
@ -94,6 +95,7 @@ classes = [
ui.IMAGE_EDITOR_PLUS_OT_add_noise_dialog, ui.IMAGE_EDITOR_PLUS_OT_add_noise_dialog,
ui.IMAGE_EDITOR_PLUS_OT_pixelize_dialog, ui.IMAGE_EDITOR_PLUS_OT_pixelize_dialog,
ui.IMAGE_EDITOR_PLUS_OT_make_seamless_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_UL_layer_list,
ui.IMAGE_EDITOR_PLUS_MT_edit_menu, ui.IMAGE_EDITOR_PLUS_MT_edit_menu,
ui.IMAGE_EDITOR_PLUS_MT_layers_menu, ui.IMAGE_EDITOR_PLUS_MT_layers_menu,

View File

@ -1633,3 +1633,66 @@ class IMAGE_EDITOR_PLUS_OT_make_seamless(bpy.types.Operator):
app.refresh_image(context) app.refresh_image(context)
return {'FINISHED'} 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'}

View File

@ -1253,6 +1253,117 @@ class IMAGE_EDITOR_PLUS_OT_make_seamless_dialog(bpy.types.Operator):
row.column() row.column()
row.prop(self, 'preview', text='Preview', toggle=True, icon='VIEWZOOM') 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): class IMAGE_EDITOR_PLUS_UL_layer_list(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname): def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
icon_value = 0 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...', layout.operator(IMAGE_EDITOR_PLUS_OT_make_seamless_dialog.bl_idname, text='Make Seamless...',
icon_value=get_icon_id('filter')) 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): class IMAGE_EDITOR_PLUS_PT_select_panel(bpy.types.Panel):
bl_label = "Select" bl_label = "Select"
bl_space_type = "IMAGE_EDITOR" bl_space_type = "IMAGE_EDITOR"
@ -1793,3 +1907,7 @@ class IMAGE_EDITOR_PLUS_PT_filter_panel(bpy.types.Panel):
row = layout.row() row = layout.row()
row.operator(IMAGE_EDITOR_PLUS_OT_make_seamless_dialog.bl_idname, text='Make Seamless...', row.operator(IMAGE_EDITOR_PLUS_OT_make_seamless_dialog.bl_idname, text='Make Seamless...',
icon_value=get_icon_id('filter')) 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'))