Implement normal map generator
This commit is contained in:
parent
d5797c313c
commit
6fa5629403
|
@ -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,
|
||||||
|
|
|
@ -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'}
|
||||||
|
|
118
addon/ui.py
118
addon/ui.py
|
@ -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'))
|
||||||
|
|
Loading…
Reference in New Issue