Initial commit

This commit is contained in:
2024-03-05 21:33:30 +09:00
parent fd516d9e0e
commit 995bf859b5
16 changed files with 2341 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
'''
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
MERCHANTABILITY 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/>.
'''
bl_info = {
"name": "Symmetrize Texture",
"author": "akaneyu",
"version": (1, 1, 3),
"blender": (2, 93, 0),
"location": "View3D",
"warning": "",
"description": "",
"wiki_url": "",
"tracker_url": "",
"category": "3D View"}
if "bpy" in locals():
import importlib
importlib.reload(app)
importlib.reload(operators)
importlib.reload(ui)
importlib.reload(ui_renderer)
importlib.reload(utils)
import bpy
from . import app
from . import operators
from . import ui
classes = [
app.SYMMETRIZE_TEXTURE_PropertyGroup,
operators.SYMMETRIZE_TEXTURE_OT_use_3d_brush,
operators.SYMMETRIZE_TEXTURE_OT_mirrored_copy,
operators.SYMMETRIZE_TEXTURE_OT_use_2d_brush,
operators.SYMMETRIZE_TEXTURE_OT_save_image,
ui.SYMMETRIZE_TEXTURE_MT_menu_3d,
ui.SYMMETRIZE_TEXTURE_MT_menu_2d,
ui.SYMMETRIZE_TEXTURE_PT_panel_3d,
ui.SYMMETRIZE_TEXTURE_PT_panel_2d
]
def register():
app.load_icons()
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.VIEW3D_MT_object.append(ui.menu_func_3d)
bpy.types.IMAGE_MT_image.append(ui.menu_func_2d)
wm = bpy.types.WindowManager
wm.symmetrizetexture_properties = \
bpy.props.PointerProperty(type=app.SYMMETRIZE_TEXTURE_PropertyGroup)
app.SYMMETRIZE_TEXTURE_PropertyGroup.image_mirror_axis = \
bpy.props.EnumProperty(items=(
('x_axis', 'X', 'X Axis', ui.get_icon_id('mirror_x'), 0),
('y_axis', 'Y', 'Y Axis', ui.get_icon_id('mirror_y'), 1)))
def unregister():
app.dispose_icons()
for cls in classes:
bpy.utils.unregister_class(cls)
bpy.types.VIEW3D_MT_object.remove(ui.menu_func_3d)
bpy.types.IMAGE_MT_image.remove(ui.menu_func_2d)
wm = bpy.types.WindowManager
del wm.symmetrizetexture_properties
if __name__ == "__main__":
register()
+166
View File
@@ -0,0 +1,166 @@
'''
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
MERCHANTABILITY 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 sys
import os
import ctypes
import math
import bpy
import bpy.utils.previews
import blf
import numpy as np
from . ui_renderer import UIRenderer as UIRenderer
from . import utils
class Session:
def __init__(self):
self.icons = None
self.ui_renderer = None
self.draw_handler = None
self.previous_object = None
self.brush_position = None
self.brush_size = 50.0
self.selecting_direction = False
self.brush_active = False
self.resizing_brush = False
def get_session():
global session
return session
def draw_handler():
global session
context = bpy.context
wm = context.window_manager
props = wm.symmetrizetexture_properties
mirror_axis = props.image_mirror_axis
info_text = None
if not session.ui_renderer:
session.ui_renderer = UIRenderer()
# direction setup
if session.selecting_direction:
if mirror_axis == 'x_axis':
border_pos1 = context.region.view2d.view_to_region(0.5, 0, clip=False)
border_pos2 = context.region.view2d.view_to_region(0.5, 1.0, clip=False)
else:
border_pos1 = context.region.view2d.view_to_region(0, 0.5, clip=False)
border_pos2 = context.region.view2d.view_to_region(1.0, 0.5, clip=False)
session.ui_renderer.render_border(border_pos1, border_pos2)
center = context.region.view2d.view_to_region(0.5, 0.5, clip=False)
if mirror_axis == 'x_axis':
arrow_angle = 0 if session.direction > 0 else np.pi
else:
arrow_angle = np.pi / 2.0 if session.direction > 0 else np.pi * 1.5
session.ui_renderer.render_arrow(center, arrow_angle)
info_text = "LMB: Perform\n" \
+ "RMB: Cancel"
# brush
if session.brush_active and session.brush_position:
session.ui_renderer.render_brush_frame(session.brush_position, session.brush_size)
info_text = "LMB: Perform\n" \
+ "RMB: Finish\n" \
+ "F: Change brush size"
area_height = context.area.height
# info text
if info_text:
blf.enable(0, blf.WORD_WRAP)
blf.word_wrap(0, 200)
blf.color(0, 1.0, 1.0, 1.0, 1.0)
if bpy.context.area.type == 'VIEW_3D':
blf.position(0, 85, area_height - 150, 0)
else:
blf.position(0, 85, area_height - 70, 0)
blf.size(0, 14, 72)
blf.draw(0, info_text)
blf.disable(0, blf.WORD_WRAP)
def load_icons():
global session
script_dir = os.path.dirname(os.path.realpath(__file__))
icons = bpy.utils.previews.new()
icons_dir = os.path.join(script_dir, "icons")
for file_name in os.listdir(icons_dir):
icon_name = os.path.splitext(file_name)[0]
icons.load(icon_name, os.path.join(icons_dir, file_name), 'IMAGE')
session.icons = icons
def dispose_icons():
global session
bpy.utils.previews.remove(session.icons)
def load_native_library():
script_dir = os.path.dirname(os.path.realpath(__file__))
if os.name == 'nt':
lib_file_name = 'symmetrize_texture.dll'
else:
lib_file_name = 'libsymmetrize_texture.so'
lib = ctypes.CDLL(os.path.join(script_dir, lib_file_name))
return lib
def unload_native_library(lib):
if os.name == 'nt':
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
kernel32.FreeLibrary.argtypes = [ctypes.c_void_p]
kernel32.FreeLibrary(lib._handle)
else:
stdlib = ctypes.CDLL("")
stdlib.dlclose.argtypes = [ctypes.c_void_p]
stdlib.dlclose(lib._handle)
def get_image_previews(self, context):
image_previews = []
for i, img in enumerate(bpy.data.images):
image_previews.append((img.name, img.name, img.name, bpy.types.UILayout.icon(img), i))
return image_previews
class SYMMETRIZE_TEXTURE_PropertyGroup(bpy.types.PropertyGroup):
image_preview: bpy.props.EnumProperty(items=get_image_previews, options={'LIBRARY_EDITABLE'})
# created in the register(): image_mirror_axis
brush_strength: bpy.props.FloatProperty(name='Strength', default=1.0, min=0, max=1.0, precision=3)
brush_falloff: bpy.props.EnumProperty(items=(
('smooth', 'Smooth', 'Smooth', 'SMOOTHCURVE', 0),
('constant', 'Constant', 'Constant', 'NOCURVE', 1)))
session = Session()
Binary file not shown.

After

Width:  |  Height:  |  Size: 793 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 926 B

+533
View File
@@ -0,0 +1,533 @@
'''
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()
+189
View File
@@ -0,0 +1,189 @@
'''
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
MERCHANTABILITY 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 bpy
from . import operators
from . import app
def get_icon_id(icon_name):
session = app.get_session()
if icon_name in session.icons:
return session.icons[icon_name].icon_id
else:
return 0
def menu_func_3d(self, context):
layout = self.layout
layout.separator()
layout.menu(SYMMETRIZE_TEXTURE_MT_menu_3d.bl_idname, text='Symmetrize Texture')
def menu_func_2d(self, context):
layout = self.layout
if context.area.spaces.active.mode != 'UV' \
and context.area.spaces.active.image != None \
and not context.area.spaces.active.image.use_view_as_render:
layout.separator()
layout.menu(SYMMETRIZE_TEXTURE_MT_menu_2d.bl_idname, text='Symmetrize Texture')
class SYMMETRIZE_TEXTURE_MT_menu_3d(bpy.types.Menu):
bl_idname = "SYMMETRIZE_TEXTURE_MT_menu_3d"
bl_label = "Symmetrize Texture"
def draw(self, context):
layout = self.layout
layout.operator(operators.SYMMETRIZE_TEXTURE_OT_use_3d_brush.bl_idname, text='3D Brush',
icon_value=get_icon_id('sym_brush'))
layout.operator(operators.SYMMETRIZE_TEXTURE_OT_save_image.bl_idname, text='Save Image',
icon="FILE_TICK")
class SYMMETRIZE_TEXTURE_MT_menu_2d(bpy.types.Menu):
bl_idname = "SYMMETRIZE_TEXTURE_MT_menu_2d"
bl_label = "Symmetrize Texture"
def draw(self, context):
layout = self.layout
layout.operator(operators.SYMMETRIZE_TEXTURE_OT_mirrored_copy.bl_idname, text='Mirrored Copy',
icon_value=get_icon_id('mirror_copy'))
layout.operator(operators.SYMMETRIZE_TEXTURE_OT_use_2d_brush.bl_idname, text='2D Brush',
icon_value=get_icon_id('sym_brush'))
class SYMMETRIZE_TEXTURE_PT_panel_3d(bpy.types.Panel):
bl_label = "Symmetrize Texture"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Symmetrize Texture"
@classmethod
def poll(cls, context):
return context.mode == 'OBJECT'
def draw(self, context):
session = app.get_session()
wm = context.window_manager
props = wm.symmetrizetexture_properties
if context.object != session.previous_object:
imgs = self.find_texture_images(context)
if imgs:
props.image_preview = imgs[0].name
session.previous_object = context.object
layout = self.layout
row = layout.row()
op = row.operator(operators.SYMMETRIZE_TEXTURE_OT_use_3d_brush.bl_idname, text='3D Brush',
icon_value=get_icon_id('sym_brush'))
row = layout.row()
row.label(text='Texture:')
row = layout.row()
row.prop(wm.symmetrizetexture_properties, 'image_preview', text='')
row = layout.row()
op = row.operator(operators.SYMMETRIZE_TEXTURE_OT_save_image.bl_idname, text='Save Image',
icon="FILE_TICK")
row = layout.row()
row.label(text='Brush:')
row = layout.split(align=True)
row.alignment = 'RIGHT'
row.label(text='Strength')
row.prop(props, "brush_strength", text='', slider=True)
row = layout.split(align=True)
row.alignment = 'RIGHT'
row.label(text='Falloff')
row.prop(props, "brush_falloff", text='')
row = layout.row()
row.label(text='Image Mirror Axis:')
row = layout.row()
row.prop(props, "image_mirror_axis", expand=True)
def find_texture_images(self, context):
imgs = []
for mat_slot in context.object.material_slots:
self.find_texture_images_from_node_tree(imgs, mat_slot.material.node_tree)
return imgs
def find_texture_images_from_node_tree(self, imgs, node_tree):
for node in node_tree.nodes:
if node.bl_idname == 'ShaderNodeTexImage':
if node.image not in imgs:
imgs.append(node.image)
elif node.bl_idname == 'ShaderNodeGroup':
self.find_texture_images_from_node_tree(imgs, node.node_tree)
class SYMMETRIZE_TEXTURE_PT_panel_2d(bpy.types.Panel):
bl_label = "Symmetrize Texture"
bl_space_type = "IMAGE_EDITOR"
bl_region_type = "UI"
bl_category = "Symmetrize Texture"
@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 draw(self, context):
session = app.get_session()
wm = context.window_manager
props = wm.symmetrizetexture_properties
layout = self.layout
row = layout.row()
op = row.operator(operators.SYMMETRIZE_TEXTURE_OT_mirrored_copy.bl_idname, text='Mirrored Copy',
icon_value=get_icon_id('mirror_copy'))
row = layout.row()
op = row.operator(operators.SYMMETRIZE_TEXTURE_OT_use_2d_brush.bl_idname, text='2D Brush',
icon_value=get_icon_id('sym_brush'))
row = layout.row()
row.label(text='Brush:')
row = layout.split(align=True)
row.alignment = 'RIGHT'
row.label(text='Strength')
row.prop(props, "brush_strength", text='', slider=True)
row = layout.split(align=True)
row.alignment = 'RIGHT'
row.label(text='Falloff')
row.prop(props, "brush_falloff", text='')
row = layout.row()
row.label(text='Image Mirror Axis:')
row = layout.row()
row.prop(props, "image_mirror_axis", expand=True)
+208
View File
@@ -0,0 +1,208 @@
'''
Copyright (C) 2020 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
MERCHANTABILITY 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 time
import bpy
import bgl
import blf
import gpu
from gpu_extras.batch import batch_for_shader
from mathutils import Matrix
import numpy as np
default_vertex_shader = '''
uniform mat4 ModelViewProjectionMatrix;
in vec2 pos;
void main()
{
gl_Position = ModelViewProjectionMatrix * vec4(pos, 0, 1.0);
}
'''
default_fragment_shader = '''
uniform vec4 color;
out vec4 fragColor;
void main()
{
fragColor = color;
}
'''
dotted_line_vertex_shader = '''
uniform mat4 ModelViewProjectionMatrix;
in vec2 pos;
in float arcLength;
out float arcLengthInter;
void main()
{
arcLengthInter = arcLength;
gl_Position = ModelViewProjectionMatrix * vec4(pos, 0, 1.0);
}
'''
dotted_line_fragment_shader = '''
uniform float scale;
uniform float offset;
uniform vec4 color1;
uniform vec4 color2;
in float arcLengthInter;
out vec4 fragColor;
void main()
{
if (step(sin((arcLengthInter + offset) * scale), 0.5) == 1) {
fragColor = color1;
} else {
fragColor = color2;
}
}
'''
class UIRenderer:
def __init__(self):
self.default_shader = gpu.types.GPUShader(default_vertex_shader,
default_fragment_shader)
self.default_shader_u_color = self.default_shader.uniform_from_name("color")
self.dotted_line_shader = gpu.types.GPUShader(dotted_line_vertex_shader,
dotted_line_fragment_shader)
self.dotted_line_shader_u_color1 = self.dotted_line_shader.uniform_from_name("color1")
self.dotted_line_shader_u_color2 = self.dotted_line_shader.uniform_from_name("color2")
def render_border(self, pos1, pos2):
bgl.glEnable(bgl.GL_BLEND)
bgl.glLineWidth(1.0)
batch = batch_for_shader(self.default_shader, 'LINES',
{"pos": [pos1, pos2]})
self.default_shader.bind()
self.default_shader.uniform_vector_float(self.default_shader_u_color,
np.array([1.0, 0.0, 1.0, 1.0], 'f'), 4)
batch.draw(self.default_shader)
err = bgl.glGetError()
if err != bgl.GL_NO_ERROR:
print('render_border')
print('OpenGL error:', err)
def render_arrow(self, center, angle):
bgl.glEnable(bgl.GL_BLEND)
bgl.glLineWidth(1.0)
gpu.matrix.load_identity()
with gpu.matrix.push_pop():
gpu.matrix.translate(center)
gpu.matrix.multiply_matrix(
Matrix.Rotation(angle, 4, 'Z'))
verts = [
(0, -50),
(100, -50),
(0, 50),
(100, 50),
(100, 0),
(200, 0),
(100, 100),
(100, -100)
]
indices = [
(0, 1, 2),
(2, 1, 3),
(4, 5, 6),
(4, 5, 7)
]
batch = batch_for_shader(self.default_shader, 'TRIS',
{"pos": verts}, indices=indices)
self.default_shader.bind()
self.default_shader.uniform_vector_float(self.default_shader_u_color,
np.array([1.0, 1.0, 1.0, 0.5], 'f'), 4)
batch.draw(self.default_shader)
err = bgl.glGetError()
if err != bgl.GL_NO_ERROR:
print('render_arrow')
print('OpenGL error:', err)
def render_brush_frame(self, pos, radius):
bgl.glEnable(bgl.GL_BLEND)
bgl.glLineWidth(2.0)
verts = self.create_brush_frame_vertices(pos, radius)
arc_lengths = [0]
for a, b in zip(verts[:-1], verts[1:]):
arc_lengths.append(arc_lengths[-1] + np.linalg.norm(a - b))
batch = batch_for_shader(self.dotted_line_shader, 'LINE_STRIP',
{"pos": verts, "arcLength": arc_lengths})
self.dotted_line_shader.bind()
self.dotted_line_shader.uniform_float("scale", 0.6)
self.dotted_line_shader.uniform_float("offset", 0)
self.dotted_line_shader.uniform_vector_float(self.dotted_line_shader_u_color1,
np.array([1.0, 1.0, 1.0, 0.5], 'f'), 4)
self.dotted_line_shader.uniform_vector_float(self.dotted_line_shader_u_color2,
np.array([0.0, 0.0, 0.0, 0.5], 'f'), 4)
batch.draw(self.dotted_line_shader)
err = bgl.glGetError()
if err != bgl.GL_NO_ERROR:
print('render_brush_frame')
print('OpenGL error:', err)
def create_brush_frame_vertices(self, pos, radius):
segs = 32
theta = 2.0 * np.pi / segs
c = np.cos(theta)
s = np.sin(theta)
x = radius
y = 0
verts = []
for i in range(segs):
verts.append((x + pos[0], y + pos[1]))
t = x
x = c * x - s * y
y = s * t + c * y
verts.append(verts[0])
return np.array(verts, 'f')
+38
View File
@@ -0,0 +1,38 @@
'''
Copyright (C) 2020 - 2022 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
MERCHANTABILITY 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 bpy
import numpy as np
def read_pixels_from_image(img):
width, height = img.size[0], img.size[1]
if bpy.app.version >= (2, 83, 0):
pixels = np.empty(len(img.pixels), dtype=np.float32);
img.pixels.foreach_get(pixels)
return np.reshape(pixels, (height, width, 4))
else:
return np.reshape(img.pixels[:], (height, width, 4))
def write_pixels_to_image(img, pixels, update_preview=True):
if bpy.app.version >= (2, 83, 0):
img.pixels.foreach_set(np.reshape(pixels, -1))
else:
img.pixels = np.reshape(pixels, -1)
if update_preview and img.preview:
img.preview.reload()