534 lines
18 KiB
Python
534 lines
18 KiB
Python
'''
|
|
Copyright (C) 2021 - 2023 Akaneyu
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHAmathNTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
'''
|
|
|
|
import ctypes
|
|
import math
|
|
import bpy
|
|
import mathutils
|
|
import numpy as np
|
|
from . import app
|
|
from . import utils
|
|
|
|
class SYMMETRIZE_TEXTURE_OT_use_3d_brush(bpy.types.Operator):
|
|
"""Symmetrize the texture by using 3D brush"""
|
|
bl_idname = "symmetrize_texture.use_3d_brush"
|
|
bl_label = "Use 3D Brush"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def __init__(self):
|
|
self.native_lib = None
|
|
self.lmb = False
|
|
self.image = None
|
|
self.image_pixels = None
|
|
self.eval_object = None
|
|
self.eval_mesh = None
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.mode == 'OBJECT'
|
|
|
|
def modal(self, context, event):
|
|
session = app.get_session()
|
|
|
|
context.area.tag_redraw()
|
|
|
|
# for updating the 3D view
|
|
context.tool_settings.image_paint.canvas = context.tool_settings.image_paint.canvas
|
|
|
|
region_pos = [event.mouse_region_x, event.mouse_region_y]
|
|
|
|
if session.resizing_brush:
|
|
session.brush_position = self.resize_origin
|
|
else:
|
|
session.brush_position = region_pos
|
|
|
|
if event.type == 'MOUSEMOVE':
|
|
if session.resizing_brush:
|
|
session.brush_size = max(self.initial_brush_size
|
|
+ region_pos[0] - self.resize_origin[0], 1.0)
|
|
else:
|
|
if self.lmb:
|
|
self.process(context)
|
|
elif event.type == 'LEFTMOUSE':
|
|
if event.value == 'PRESS':
|
|
self.lmb = True
|
|
|
|
if session.resizing_brush:
|
|
session.resizing_brush = False
|
|
else:
|
|
self.process(context)
|
|
elif event.value == 'RELEASE':
|
|
self.lmb = False
|
|
|
|
elif event.type in ['F']:
|
|
if not session.resizing_brush:
|
|
self.resize_origin = region_pos
|
|
self.initial_brush_size = session.brush_size
|
|
|
|
session.resizing_brush = True
|
|
elif event.type in ['RIGHTMOUSE', 'ESC', 'RET']:
|
|
if event.value == 'PRESS':
|
|
if session.resizing_brush:
|
|
session.brush_size = self.initial_brush_size
|
|
|
|
session.resizing_brush = False
|
|
else:
|
|
self.eval_object.to_mesh_clear()
|
|
|
|
session.brush_position = None
|
|
session.brush_active = False
|
|
session.resizing_brush = False
|
|
|
|
bpy.types.SpaceView3D.draw_handler_remove(session.draw_handler, 'WINDOW')
|
|
|
|
self.native_lib.SYMTEX_free()
|
|
|
|
app.unload_native_library(self.native_lib)
|
|
|
|
return {'CANCELLED'}
|
|
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def invoke(self, context, event):
|
|
session = app.get_session()
|
|
|
|
wm = context.window_manager
|
|
props = wm.symmetrizetexture_properties
|
|
|
|
if context.area.type != 'VIEW_3D':
|
|
return {'CANCELLED'}
|
|
|
|
self.native_lib = app.load_native_library()
|
|
|
|
self.native_lib.SYMTEX_init(0) # 3D processor
|
|
|
|
session.brush_position = None
|
|
session.brush_active = True
|
|
|
|
obj = context.object
|
|
if not obj or obj.type != 'MESH':
|
|
return {'CANCELLED'}
|
|
|
|
img = bpy.data.images.get(props.image_preview)
|
|
if not img or img.use_view_as_render:
|
|
return {'CANCELLED'}
|
|
|
|
self.image = img
|
|
|
|
depsgraph = context.evaluated_depsgraph_get()
|
|
|
|
self.eval_object = obj.evaluated_get(depsgraph)
|
|
self.eval_mesh = self.eval_object.to_mesh()
|
|
|
|
obj_mat = obj.matrix_world
|
|
obj_mat_inv = obj_mat.inverted()
|
|
|
|
region_3d = context.area.spaces.active.region_3d
|
|
|
|
self.native_lib.SYMTEX_setOrthogonal(int(region_3d.view_perspective == 'ORTHO'))
|
|
|
|
pers_mat = region_3d.perspective_matrix
|
|
pers_mat = pers_mat @ obj_mat
|
|
|
|
self.native_lib.SYMTEX_setPerspectiveMatrix((ctypes.c_float * 16)
|
|
(*[x for row in pers_mat for x in row]))
|
|
|
|
self.native_lib.SYMTEX_setRegionSize(context.region.width, context.region.height)
|
|
|
|
view_mat_inv = region_3d.view_matrix.inverted()
|
|
|
|
view_pos = obj_mat_inv.to_3x3() @ view_mat_inv.col[3].to_3d() \
|
|
+ obj_mat_inv.col[3].to_3d()
|
|
|
|
view_dir = obj_mat_inv.to_3x3() @ view_mat_inv.to_3x3() @ mathutils.Vector((0, 0, 1.0))
|
|
view_dir.normalize()
|
|
|
|
self.native_lib.SYMTEX_setViewPosition((ctypes.c_float * 3)
|
|
(view_pos[0], view_pos[1], view_pos[2]))
|
|
self.native_lib.SYMTEX_setViewDirection((ctypes.c_float * 3)
|
|
(view_dir[0], view_dir[1], view_dir[2]))
|
|
|
|
num_verts = len(self.eval_mesh.vertices)
|
|
vert_coords = np.empty(num_verts * 3, dtype=np.float32)
|
|
vert_norms = np.empty(num_verts * 3, dtype=np.float32)
|
|
|
|
for i, vert in enumerate(self.eval_mesh.vertices):
|
|
vert_coords[i * 3] = vert.co[0]
|
|
vert_coords[i * 3 + 1] = vert.co[1]
|
|
vert_coords[i * 3 + 2] = vert.co[2]
|
|
|
|
vert_norms[i * 3] = vert.normal[0]
|
|
vert_norms[i * 3 + 1] = vert.normal[1]
|
|
vert_norms[i * 3 + 2] = vert.normal[2]
|
|
|
|
self.native_lib.SYMTEX_setVertexCoords(vert_coords.ctypes.data_as(
|
|
ctypes.POINTER(ctypes.c_float)), num_verts)
|
|
self.native_lib.SYMTEX_setVertexNormals(vert_norms.ctypes.data_as(
|
|
ctypes.POINTER(ctypes.c_float)), num_verts)
|
|
|
|
num_indices = len(self.eval_mesh.loops)
|
|
vert_indices = np.empty(num_indices, dtype=np.int32)
|
|
|
|
for i, mesh_loop in enumerate(self.eval_mesh.loops):
|
|
vert_indices[i] = mesh_loop.vertex_index
|
|
|
|
self.native_lib.SYMTEX_setVertexIndices(vert_indices.ctypes.data_as(
|
|
ctypes.POINTER(ctypes.c_int)), num_indices)
|
|
|
|
num_uv_coords = len(self.eval_mesh.uv_layers.active.data)
|
|
uv_coords = np.empty(num_uv_coords * 2, dtype=np.float32)
|
|
|
|
for i, loop_uv in enumerate(self.eval_mesh.uv_layers.active.data):
|
|
uv_coords[i * 2] = loop_uv.uv[0]
|
|
uv_coords[i * 2 + 1] = loop_uv.uv[1]
|
|
|
|
self.native_lib.SYMTEX_setUVCoords(uv_coords.ctypes.data_as(
|
|
ctypes.POINTER(ctypes.c_float)), num_uv_coords)
|
|
|
|
self.eval_mesh.calc_loop_triangles()
|
|
|
|
num_triangles = len(self.eval_mesh.loop_triangles)
|
|
tri_indices = np.empty(num_triangles * 3, dtype=np.int32)
|
|
|
|
for i, loop_tri in enumerate(self.eval_mesh.loop_triangles):
|
|
tri_indices[i * 3] = loop_tri.loops[0]
|
|
tri_indices[i * 3 + 1] = loop_tri.loops[1]
|
|
tri_indices[i * 3 + 2] = loop_tri.loops[2]
|
|
|
|
self.native_lib.SYMTEX_setTriangles(tri_indices.ctypes.data_as(
|
|
ctypes.POINTER(ctypes.c_int)), num_triangles)
|
|
|
|
img_width, img_height = self.image.size
|
|
self.image_pixels = utils.read_pixels_from_image(self.image)
|
|
|
|
self.native_lib.SYMTEX_setImageSize(img_width, img_height)
|
|
self.native_lib.SYMTEX_setImagePixels(self.image_pixels.ctypes.data_as(
|
|
ctypes.POINTER(ctypes.c_float)))
|
|
|
|
if props.image_mirror_axis == 'x_axis':
|
|
mirror_axis = 0
|
|
else:
|
|
mirror_axis = 1
|
|
|
|
self.native_lib.SYMTEX_setMirrorAxis(mirror_axis)
|
|
|
|
self.native_lib.SYMTEX_prepare()
|
|
|
|
session.draw_handler = bpy.types.SpaceView3D.draw_handler_add(
|
|
app.draw_handler, (), 'WINDOW', 'POST_PIXEL')
|
|
|
|
context.window_manager.modal_handler_add(self)
|
|
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def process(self, context):
|
|
session = app.get_session()
|
|
|
|
wm = context.window_manager
|
|
props = wm.symmetrizetexture_properties
|
|
|
|
self.native_lib.SYMTEX_setBrushSize(ctypes.c_float(session.brush_size))
|
|
self.native_lib.SYMTEX_setBrushStrength(ctypes.c_float(props.brush_strength))
|
|
|
|
if props.brush_falloff == 'smooth':
|
|
falloff_type = 0
|
|
else:
|
|
falloff_type = 9
|
|
|
|
self.native_lib.SYMTEX_setBrushFalloffType(falloff_type)
|
|
|
|
pos = session.brush_position
|
|
|
|
self.native_lib.SYMTEX_processStroke((ctypes.c_float * 2)(*pos))
|
|
|
|
utils.write_pixels_to_image(self.image, self.image_pixels, False)
|
|
|
|
self.image.update()
|
|
|
|
class SYMMETRIZE_TEXTURE_OT_save_image(bpy.types.Operator):
|
|
"""Save the image"""
|
|
bl_idname = "image_layers_node.save_image"
|
|
bl_label = "Save Image"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.mode == 'OBJECT'
|
|
|
|
def execute(self, context):
|
|
wm = context.window_manager
|
|
props = wm.symmetrizetexture_properties
|
|
|
|
img = bpy.data.images.get(props.image_preview)
|
|
if not img or img.use_view_as_render:
|
|
return {'CANCELLED'}
|
|
|
|
if img.packed_files:
|
|
img.pack()
|
|
elif img.filepath:
|
|
img.save()
|
|
|
|
return {'FINISHED'}
|
|
|
|
class SYMMETRIZE_TEXTURE_OT_mirrored_copy(bpy.types.Operator):
|
|
"""Create a Mirrored copy of image"""
|
|
bl_idname = "symmetrize_texture.mirrored_copy"
|
|
bl_label = "Mirrored Copy"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def __init__(self):
|
|
pass
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.area.spaces.active.mode != 'UV' \
|
|
and context.area.spaces.active.image != None \
|
|
and not context.area.spaces.active.image.use_view_as_render
|
|
|
|
def modal(self, context, event):
|
|
session = app.get_session()
|
|
|
|
wm = context.window_manager
|
|
props = wm.symmetrizetexture_properties
|
|
|
|
context.area.tag_redraw()
|
|
#context.tool_settings.image_paint.canvas = context.tool_settings.image_paint.canvas
|
|
|
|
region_pos = [event.mouse_region_x, event.mouse_region_y]
|
|
|
|
target_x, target_y = context.region.view2d.region_to_view(*region_pos)
|
|
|
|
if props.image_mirror_axis == 'x_axis':
|
|
session.direction = target_x - 0.5
|
|
else:
|
|
session.direction = target_y - 0.5
|
|
|
|
if event.type in ['LEFTMOUSE']:
|
|
self.process(context)
|
|
session.selecting_direction = False
|
|
|
|
bpy.types.SpaceImageEditor.draw_handler_remove(session.draw_handler, 'WINDOW')
|
|
|
|
return {'FINISHED'}
|
|
elif event.type in ['RIGHTMOUSE', 'ESC', 'RET']:
|
|
session.selecting_direction = False
|
|
|
|
bpy.types.SpaceImageEditor.draw_handler_remove(session.draw_handler, 'WINDOW')
|
|
|
|
return {'CANCELLED'}
|
|
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def invoke(self, context, event):
|
|
session = app.get_session()
|
|
|
|
if context.area.type != 'IMAGE_EDITOR':
|
|
return {'CANCELLED'}
|
|
|
|
session.selecting_direction = True
|
|
|
|
session.draw_handler = bpy.types.SpaceImageEditor.draw_handler_add(
|
|
app.draw_handler, (), 'WINDOW', 'POST_PIXEL')
|
|
|
|
context.window_manager.modal_handler_add(self)
|
|
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def process(self, context):
|
|
session = app.get_session()
|
|
|
|
wm = context.window_manager
|
|
props = wm.symmetrizetexture_properties
|
|
|
|
img = context.area.spaces.active.image
|
|
|
|
img_width, img_height = img.size
|
|
|
|
img_pixels = utils.read_pixels_from_image(img)
|
|
|
|
half_img_width = img_width / 2
|
|
half_img_height = img_height / 2
|
|
x_mid_1, x_mid_2 = int(math.floor(half_img_width)), int(math.ceil(half_img_width))
|
|
y_mid_1, y_mid_2 = int(math.floor(half_img_height)), int(math.ceil(half_img_height))
|
|
|
|
if props.image_mirror_axis == 'x_axis':
|
|
if session.direction > 0:
|
|
mirror_pixels = img_pixels[:, :x_mid_1]
|
|
img_pixels[:, x_mid_2:] = np.fliplr(mirror_pixels)
|
|
else:
|
|
mirror_pixels = img_pixels[:, x_mid_2:]
|
|
img_pixels[:, :x_mid_1] = np.fliplr(mirror_pixels)
|
|
else:
|
|
if session.direction > 0:
|
|
mirror_pixels = img_pixels[:y_mid_1]
|
|
img_pixels[y_mid_2:] = np.flipud(mirror_pixels)
|
|
else:
|
|
mirror_pixels = img_pixels[y_mid_2:]
|
|
img_pixels[:y_mid_1] = np.flipud(mirror_pixels)
|
|
|
|
utils.write_pixels_to_image(img, img_pixels, False)
|
|
|
|
img.update()
|
|
|
|
class SYMMETRIZE_TEXTURE_OT_use_2d_brush(bpy.types.Operator):
|
|
"""Symmetrize the texture by using 2D brush"""
|
|
bl_idname = "symmetrize_texture.use_2d_brush"
|
|
bl_label = "Use 2D Brush"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def __init__(self):
|
|
self.native_lib = None
|
|
self.lmb = False
|
|
self.image = None
|
|
self.image_pixels = None
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.area.spaces.active.mode != 'UV' \
|
|
and context.area.spaces.active.image != None \
|
|
and not context.area.spaces.active.image.use_view_as_render
|
|
|
|
def modal(self, context, event):
|
|
session = app.get_session()
|
|
|
|
context.area.tag_redraw()
|
|
#context.tool_settings.image_paint.canvas = context.tool_settings.image_paint.canvas
|
|
|
|
region_pos = [event.mouse_region_x, event.mouse_region_y]
|
|
|
|
if session.resizing_brush:
|
|
session.brush_position = self.resize_origin
|
|
else:
|
|
session.brush_position = region_pos
|
|
|
|
if event.type == 'MOUSEMOVE':
|
|
if session.resizing_brush:
|
|
session.brush_size = max(self.initial_brush_size
|
|
+ region_pos[0] - self.resize_origin[0], 1.0)
|
|
else:
|
|
if self.lmb:
|
|
self.process(context)
|
|
else:
|
|
pass
|
|
#self.process(context, False)
|
|
elif event.type == 'LEFTMOUSE':
|
|
if event.value == 'PRESS':
|
|
self.lmb = True
|
|
|
|
if session.resizing_brush:
|
|
session.resizing_brush = False
|
|
else:
|
|
self.process(context)
|
|
elif event.value == 'RELEASE':
|
|
self.lmb = False
|
|
|
|
elif event.type in ['F']:
|
|
if not session.resizing_brush:
|
|
self.resize_origin = region_pos
|
|
self.initial_brush_size = session.brush_size
|
|
|
|
session.resizing_brush = True
|
|
elif event.type in ['RIGHTMOUSE', 'ESC', 'RET']:
|
|
if event.value == 'PRESS':
|
|
if session.resizing_brush:
|
|
session.brush_size = self.initial_brush_size
|
|
|
|
session.resizing_brush = False
|
|
else:
|
|
session.brush_position = None
|
|
session.brush_active = False
|
|
session.resizing_brush = False
|
|
|
|
bpy.types.SpaceImageEditor.draw_handler_remove(session.draw_handler, 'WINDOW')
|
|
|
|
self.native_lib.SYMTEX_free()
|
|
|
|
app.unload_native_library(self.native_lib)
|
|
|
|
return {'CANCELLED'}
|
|
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def invoke(self, context, event):
|
|
session = app.get_session()
|
|
|
|
wm = context.window_manager
|
|
props = wm.symmetrizetexture_properties
|
|
|
|
if context.area.type != 'IMAGE_EDITOR':
|
|
return {'CANCELLED'}
|
|
|
|
self.native_lib = app.load_native_library()
|
|
|
|
self.native_lib.SYMTEX_init(1) # 2D processor
|
|
|
|
session.brush_position = None
|
|
session.brush_active = True
|
|
|
|
self.image = context.area.spaces.active.image
|
|
|
|
img_width, img_height = self.image.size
|
|
self.image_pixels = utils.read_pixels_from_image(self.image)
|
|
|
|
self.native_lib.SYMTEX_setImageSize(img_width, img_height)
|
|
self.native_lib.SYMTEX_setImagePixels(self.image_pixels.ctypes.data_as(
|
|
ctypes.POINTER(ctypes.c_float)))
|
|
|
|
if props.image_mirror_axis == 'x_axis':
|
|
mirror_axis = 0
|
|
else:
|
|
mirror_axis = 1
|
|
|
|
self.native_lib.SYMTEX_setMirrorAxis(mirror_axis)
|
|
|
|
self.native_lib.SYMTEX_prepare()
|
|
|
|
session.draw_handler = bpy.types.SpaceImageEditor.draw_handler_add(
|
|
app.draw_handler, (), 'WINDOW', 'POST_PIXEL')
|
|
|
|
context.window_manager.modal_handler_add(self)
|
|
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def process(self, context):
|
|
session = app.get_session()
|
|
|
|
wm = context.window_manager
|
|
props = wm.symmetrizetexture_properties
|
|
|
|
view_x, view_y = context.region.view2d.region_to_view(*session.brush_position)
|
|
|
|
radius_x1, dummy = context.region.view2d.region_to_view(0, 0)
|
|
radius_x2, dummy = context.region.view2d.region_to_view(session.brush_size, 0)
|
|
radius = radius_x2 - radius_x1
|
|
|
|
self.native_lib.SYMTEX_setBrushSize(ctypes.c_float(radius))
|
|
self.native_lib.SYMTEX_setBrushStrength(ctypes.c_float(props.brush_strength))
|
|
|
|
if props.brush_falloff == 'smooth':
|
|
falloff_type = 0
|
|
else:
|
|
falloff_type = 9
|
|
|
|
self.native_lib.SYMTEX_setBrushFalloffType(falloff_type)
|
|
|
|
self.native_lib.SYMTEX_processStroke((ctypes.c_float * 2)(view_x, view_y))
|
|
|
|
utils.write_pixels_to_image(self.image, self.image_pixels, False)
|
|
|
|
self.image.update()
|