diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b958328
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/build*/
+__pycache__/
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..457cdca
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,13 @@
+cmake_minimum_required(VERSION 3.11)
+
+set(LIB_NAME symmetrize_texture)
+
+project(${LIB_NAME})
+
+add_library(${LIB_NAME} SHARED
+ src/symtex_processor.cpp
+ src/math_util.cpp
+
+ src/symtex_processor.h
+ src/math_util.h
+)
diff --git a/addon/__init__.py b/addon/__init__.py
new file mode 100644
index 0000000..7623a7d
--- /dev/null
+++ b/addon/__init__.py
@@ -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 .
+'''
+
+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()
diff --git a/addon/app.py b/addon/app.py
new file mode 100644
index 0000000..07b0990
--- /dev/null
+++ b/addon/app.py
@@ -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 .
+'''
+
+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()
diff --git a/addon/icons/mirror_copy.png b/addon/icons/mirror_copy.png
new file mode 100644
index 0000000..a0c738c
Binary files /dev/null and b/addon/icons/mirror_copy.png differ
diff --git a/addon/icons/mirror_x.png b/addon/icons/mirror_x.png
new file mode 100644
index 0000000..7927a09
Binary files /dev/null and b/addon/icons/mirror_x.png differ
diff --git a/addon/icons/mirror_y.png b/addon/icons/mirror_y.png
new file mode 100644
index 0000000..668d928
Binary files /dev/null and b/addon/icons/mirror_y.png differ
diff --git a/addon/icons/sym_brush.png b/addon/icons/sym_brush.png
new file mode 100644
index 0000000..53a227e
Binary files /dev/null and b/addon/icons/sym_brush.png differ
diff --git a/addon/operators.py b/addon/operators.py
new file mode 100644
index 0000000..2eb1987
--- /dev/null
+++ b/addon/operators.py
@@ -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 .
+'''
+
+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()
diff --git a/addon/ui.py b/addon/ui.py
new file mode 100644
index 0000000..545cb05
--- /dev/null
+++ b/addon/ui.py
@@ -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 .
+'''
+
+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)
diff --git a/addon/ui_renderer.py b/addon/ui_renderer.py
new file mode 100644
index 0000000..7d009de
--- /dev/null
+++ b/addon/ui_renderer.py
@@ -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 .
+'''
+
+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')
diff --git a/addon/utils.py b/addon/utils.py
new file mode 100644
index 0000000..0110156
--- /dev/null
+++ b/addon/utils.py
@@ -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 .
+'''
+
+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()
diff --git a/src/math_util.cpp b/src/math_util.cpp
new file mode 100644
index 0000000..ebfa25c
--- /dev/null
+++ b/src/math_util.cpp
@@ -0,0 +1,179 @@
+/*
+ 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 .
+*/
+
+#include "math_util.h"
+
+#include
+#include
+
+using namespace std;
+
+void multiplyMatrix4fVector4f(float *out, float *mat, float *v)
+{
+ float x = v[0];
+ float y = v[1];
+ float z = v[2];
+
+ out[0] = x * mat[0] + y * mat[1] + z * mat[2] + mat[3] * v[3];
+ out[1] = x * mat[4] + y * mat[5] + z * mat[6] + mat[7] * v[3];
+ out[2] = x * mat[8] + y * mat[9] + z * mat[10] + mat[11] * v[3];
+ out[3] = x * mat[12] + y * mat[13] + z * mat[14] + mat[15] * v[3];
+}
+
+void multiplyMatrix4fVector3f(float *out, float *mat, float *v)
+{
+ float x = v[0];
+ float y = v[1];
+ float z = v[2];
+
+ out[0] = x * mat[0] + y * mat[1] + z * mat[2] + mat[3];
+ out[1] = x * mat[4] + y * mat[5] + z * mat[6] + mat[7];
+ out[2] = x * mat[8] + y * mat[9] + z * mat[10] + mat[11];
+}
+
+void multiplyVector4fValue(float *out, float *v, float val)
+{
+ out[0] = v[0] * val;
+ out[1] = v[1] * val;
+ out[2] = v[2] * val;
+ out[3] = v[3] * val;
+}
+
+void multiplyVector3fValue(float *out, float *v, float val)
+{
+ out[0] = v[0] * val;
+ out[1] = v[1] * val;
+ out[2] = v[2] * val;
+}
+
+void copyVector4f(float *out, float *v)
+{
+ out[0] = v[0];
+ out[1] = v[1];
+ out[2] = v[2];
+ out[3] = v[3];
+}
+
+void copyVector4fValue(float *out, float val)
+{
+ out[0] = val;
+ out[1] = val;
+ out[2] = val;
+ out[3] = val;
+}
+
+void copyVector3f(float *out, float *v)
+{
+ out[0] = v[0];
+ out[1] = v[1];
+ out[2] = v[2];
+}
+
+void copyVector3fValue(float *out, float val)
+{
+ out[0] = val;
+ out[1] = val;
+ out[2] = val;
+}
+
+void copyVector3i(int *out, int *v)
+{
+ out[0] = v[0];
+ out[1] = v[1];
+ out[2] = v[2];
+}
+
+void copyVector3iValue(int *out, int val)
+{
+ out[0] = val;
+ out[1] = val;
+ out[2] = val;
+}
+
+void copyVector2f(float *out, float *v)
+{
+ out[0] = v[0];
+ out[1] = v[1];
+}
+
+void copyVector2i(int *out, int *v)
+{
+ out[0] = v[0];
+ out[1] = v[1];
+}
+
+void copyVector2iValue(int *out, int val)
+{
+ out[0] = val;
+ out[1] = val;
+}
+
+void subtractVector3f(float *out, float *v1, float *v2)
+{
+ out[0] = v1[0] - v2[0];
+ out[1] = v1[1] - v2[1];
+ out[2] = v1[2] - v2[2];
+}
+
+void normalizeVector3f(float *v)
+{
+ float d = (float) sqrt(dotVector3f(v, v));
+
+ if (d == 0) {
+ copyVector3fValue(v, 0);
+ } else {
+ multiplyVector3fValue(v, v, 1.0f / d);
+ }
+}
+
+float dotVector3f(float *v1, float *v2)
+{
+ return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2];
+}
+
+float crossTriVector2f(float *v1, float *v2, float *v3)
+{
+ return (v1[0] - v2[0]) * (v2[1] - v3[1]) + (v1[1] - v2[1]) * (v3[0] - v2[0]);
+}
+
+float lenSquaredVector2f(float *v1, float *v2)
+{
+ float dx = v2[0] - v1[0];
+ float dy = v2[1] - v1[1];
+
+ return dx * dx + dy * dy;
+}
+
+void printVector2f(float *v) {
+ cout << "(" << v[0] << ", " << v[1] << ")" << endl;
+}
+
+void printVector4f(float *v) {
+ cout << "(" << v[0] << ", " << v[1] << ", " << v[2] << ", " << v[3] << ")" << endl;
+}
+
+void printMatrix4f(float *mat) {
+ int i;
+
+ cout << "[" << endl;
+
+ for (i = 0; i < 16; i += 4) {
+ cout << mat[i] << ", " << mat[i + 1] << ", " << mat[i + 2] << ", " << mat[i + 3] << endl;
+ }
+
+ cout << "]" << endl;
+}
diff --git a/src/math_util.h b/src/math_util.h
new file mode 100644
index 0000000..305f109
--- /dev/null
+++ b/src/math_util.h
@@ -0,0 +1,45 @@
+/*
+ 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 .
+*/
+
+#ifndef MATH_UTIL_H_
+#define MATH_UTIL_H_
+
+const float PI = 3.1415927f;
+
+void multiplyMatrix4fVector4f(float *out, float *mat, float *v);
+void multiplyMatrix4fVector3f(float *out, float *mat, float *v);
+void multiplyVector4fValue(float *out, float *v, float val);
+void multiplyVector3fValue(float *out, float *v, float val);
+void copyVector4f(float *out, float *v);
+void copyVector4fValue(float *out, float val);
+void copyVector3f(float *out, float *v);
+void copyVector3fValue(float *out, float val);
+void copyVector3i(int *out, int *v);
+void copyVector3iValue(int *out, int val);
+void copyVector2f(float *out, float *v);
+void copyVector2i(int *out, int *v);
+void copyVector2iValue(int *out, int val);
+void subtractVector3f(float *out, float *v1, float *v2);
+void normalizeVector3f(float *v);
+float dotVector3f(float *v1, float *v2);
+float crossTriVector2f(float *v1, float *v2, float *v3);
+float lenSquaredVector2f(float *v1, float *v2);
+void printVector2f(float *v);
+void printVector4f(float *v);
+void printMatrix4f(float *mat);
+
+#endif // MATH_UTIL_H_
diff --git a/src/symtex_processor.cpp b/src/symtex_processor.cpp
new file mode 100644
index 0000000..5b6e71b
--- /dev/null
+++ b/src/symtex_processor.cpp
@@ -0,0 +1,739 @@
+/*
+ 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 .
+*/
+
+#include "symtex_processor.h"
+
+#include
+#include
+#include
+#include
+
+#include "math_util.h"
+
+using namespace std;
+
+Processor *g_processor;
+
+int SYMTEX_init(int type)
+{
+ g_processor = new Processor(type);
+
+ return 0;
+}
+
+void SYMTEX_free()
+{
+ delete g_processor;
+}
+
+void SYMTEX_setRegionSize(int width, int height)
+{
+ g_processor->setRegionSize(width, height);
+}
+
+void SYMTEX_setOrthogonal(int isOrtho)
+{
+ g_processor->setOrthogonal(isOrtho != 0);
+}
+
+void SYMTEX_setPerspectiveMatrix(float *mat)
+{
+ g_processor->setPerspectiveMatrix(mat);
+}
+
+void SYMTEX_setViewPosition(float *pos)
+{
+ g_processor->setViewPosition(pos);
+}
+
+void SYMTEX_setViewDirection(float *dir)
+{
+ g_processor->setViewDirection(dir);
+}
+
+void SYMTEX_setVertexCoords(float *coords, int numCoords)
+{
+ int i;
+
+ for (i = 0; i < numCoords; i ++) {
+ g_processor->addVertexCoord(coords + 3 * i);
+ }
+}
+
+void SYMTEX_setVertexNormals(float *normals, int numNormals)
+{
+ int i;
+
+ for (i = 0; i < numNormals; i ++) {
+ g_processor->addVertexNormal(normals + 3 * i);
+ }
+}
+
+void SYMTEX_setVertexIndices(int *indices, int numIndices)
+{
+ int i;
+
+ for (i = 0; i < numIndices; i++) {
+ g_processor->addVertexIndex(indices[i]);
+ }
+}
+
+void SYMTEX_setUVCoords(float *coords, int numCoords)
+{
+ int i;
+
+ for (i = 0; i < numCoords; i++) {
+ g_processor->addUVCoord(coords + 2 * i);
+ }
+}
+
+void SYMTEX_setTriangles(int *indices, int numTriangles)
+{
+ int i;
+
+ for (i = 0; i < numTriangles; i++) {
+ g_processor->addTriangle(indices + 3 * i);
+ }
+}
+
+void SYMTEX_setImageSize(int width, int height)
+{
+ g_processor->setImageSize(width, height);
+}
+
+void SYMTEX_setImagePixels(float *pixels)
+{
+ g_processor->setImagePixels(pixels);
+}
+
+void SYMTEX_setMirrorAxis(int axis)
+{
+ g_processor->setMirrorAxis(axis);
+}
+
+void SYMTEX_setBrushSize(float size)
+{
+ g_processor->setBrushSize(size);
+}
+
+void SYMTEX_setBrushStrength(float strength)
+{
+ g_processor->setBrushStrength(strength);
+}
+
+void SYMTEX_setBrushFalloffType(int type)
+{
+ g_processor->setBrushFalloffType(type);
+}
+
+void SYMTEX_prepare()
+{
+ g_processor->prepare();
+}
+
+void SYMTEX_processStroke(float *pos)
+{
+ g_processor->processStroke(pos);
+}
+
+static float checkLinePointSide2d(float *line1, float *line2, float *pt)
+{
+ return ((line1[0] - pt[0]) * (line2[1] - pt[1]))
+ - ((line2[0] - pt[0]) * (line1[1] - pt[1]));
+}
+
+static bool checkIntersectPointPolygon2d(float *pt, float **verts, int numVerts)
+{
+ int i;
+
+ if (checkLinePointSide2d(verts[numVerts - 1], verts[0], pt) < 0) {
+ return false;
+ }
+
+ for (i = 1; i < numVerts; i++) {
+ if (checkLinePointSide2d(verts[i - 1], verts[i], pt) < 0) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static void calcBarycentricWeights(float *weights, float *v1, float *v2, float *v3, float *coord)
+{
+ weights[0] = crossTriVector2f(v2, v3, coord);
+ weights[1] = crossTriVector2f(v3, v1, coord);
+ weights[2] = crossTriVector2f(v1, v2, coord);
+
+ float totalWeight = weights[0] + weights[1] + weights[2];
+
+ if (totalWeight == 0) {
+ copyVector3fValue(weights, 1.0f / 3.0f);
+ } else {
+ multiplyVector3fValue(weights, weights, 1.0f / totalWeight);
+ }
+}
+
+static void interpolateWeights3d(float *out, float *v1, float *v2, float *v3, float *weights)
+{
+ out[0] = v1[0] * weights[0] + v2[0] * weights[1] + v3[0] * weights[2];
+ out[1] = v1[1] * weights[0] + v2[1] * weights[1] + v3[1] * weights[2];
+ out[2] = v1[2] * weights[0] + v2[2] * weights[1] + v3[2] * weights[2];
+}
+
+static void calcScreenCoordOrthogonal(float *scrCoord, float *uv,
+ float *v1ScrCoord, float *v2ScrCoord, float *v3ScrCoord,
+ float *uv1Coord, float *uv2Coord, float *uv3Coord)
+{
+ float weights[3];
+
+ calcBarycentricWeights(weights, uv1Coord, uv2Coord, uv3Coord, uv);
+ interpolateWeights3d(scrCoord, v1ScrCoord, v2ScrCoord, v3ScrCoord, weights);
+}
+
+static void calcScreenCoordPerspective(float *scrCoord, float *uv,
+ float *v1ScrCoord, float *v2ScrCoord, float *v3ScrCoord,
+ float *uv1Coord, float *uv2Coord, float *uv3Coord)
+{
+ float weights[3];
+
+ calcBarycentricWeights(weights, uv1Coord, uv2Coord, uv3Coord, uv);
+
+ float weightsTemp[3];
+ weightsTemp[0] = weights[0] * v1ScrCoord[3];
+ weightsTemp[1] = weights[1] * v2ScrCoord[3];
+ weightsTemp[2] = weights[2] * v3ScrCoord[3];
+
+ float totalWeight = weightsTemp[0] + weightsTemp[1] + weightsTemp[2];
+
+ if (totalWeight > 0) {
+ float totalWeightInv = 1.0f / totalWeight;
+ multiplyVector3fValue(weightsTemp, weightsTemp, totalWeightInv);
+ } else {
+ copyVector3fValue(weights, 1.0f / 3.0f);
+ copyVector3fValue(weightsTemp, 1.0f / 3.0f);
+ }
+
+ interpolateWeights3d(scrCoord, v1ScrCoord, v2ScrCoord, v3ScrCoord, weightsTemp);
+}
+
+Triangle::Triangle()
+{
+ copyVector3iValue(m_indices, 0);
+}
+
+Triangle::Triangle(int *indices)
+{
+ copyVector3i(m_indices, indices);
+}
+
+Triangle::~Triangle()
+{
+}
+
+PixelState::PixelState()
+{
+ copyVector4fValue(m_screenCoord, 0);
+ copyVector2iValue(m_imageCoord, 0);
+}
+
+PixelState::~PixelState()
+{
+}
+
+void PixelState::setScreenCoord(float *coord)
+{
+ copyVector4f(m_screenCoord, coord);
+}
+
+void PixelState::setImageCoord(int *coord)
+{
+ copyVector2i(m_imageCoord, coord);
+}
+
+Processor::Processor(int type) :
+ m_processType(type),
+ m_regionWidth(0),
+ m_regionHeight(0),
+ m_orthogonal(false),
+ m_imageWidth(0),
+ m_imageHeight(0),
+ m_imagePixels(NULL),
+ m_originalImagePixels(NULL),
+ m_imageAlpha(NULL),
+ m_mirrorAxis(0),
+ m_brushSize(0),
+ m_brushStrength(1.0f),
+ m_brushFalloffType(0)
+{
+ int i;
+
+ for (i = 0; i < 16; i++) {
+ m_perspectiveMatrix[i] = 0;
+ }
+
+ copyVector3fValue(m_viewPosition, 0);
+ copyVector3fValue(m_viewDirection, 0);
+}
+
+Processor::~Processor()
+{
+ int i;
+
+ for (i = 0; i < (int) m_vertexCoords.size(); i++) {
+ delete [] m_vertexCoords[i];
+ }
+
+ for (i = 0; i < (int) m_vertexNormals.size(); i++) {
+ delete [] m_vertexNormals[i];
+ }
+
+ for (i = 0; i < (int) m_screenCoords.size(); i++) {
+ delete [] m_screenCoords[i];
+ }
+
+ for (i = 0; i < (int) m_uvCoords.size(); i++) {
+ delete [] m_uvCoords[i];
+ }
+
+ for (i = 0; i < (int) m_triangles.size(); i++) {
+ delete m_triangles[i];
+ }
+
+ if (m_originalImagePixels != NULL) {
+ delete [] m_originalImagePixels;
+ }
+
+ if (m_imageAlpha != NULL) {
+ delete [] m_imageAlpha;
+ }
+
+ for (i = 0; i < (int) m_pixelStates.size(); i++) {
+ delete m_pixelStates[i];
+ }
+}
+
+void Processor::setRegionSize(int width, int height)
+{
+ m_regionWidth = width;
+ m_regionHeight = height;
+}
+
+void Processor::setPerspectiveMatrix(float *mat)
+{
+ int i;
+
+ for (i = 0; i < 16; i++) {
+ m_perspectiveMatrix[i] = mat[i];
+ }
+}
+
+void Processor::setViewPosition(float *pos)
+{
+ copyVector3f(m_viewPosition, pos);
+}
+
+void Processor::setViewDirection(float *dir)
+{
+ copyVector3f(m_viewDirection, dir);
+}
+
+void Processor::addVertexCoord(float *coord)
+{
+ float *coordEntry = new float[3];
+ copyVector3f(coordEntry, coord);
+
+ m_vertexCoords.push_back(coordEntry);
+}
+
+void Processor::addVertexNormal(float *norm)
+{
+ float *normEntry = new float[3];
+ copyVector3f(normEntry, norm);
+
+ m_vertexNormals.push_back(normEntry);
+}
+
+void Processor::addVertexIndex(int index)
+{
+ m_vertexIndices.push_back(index);
+}
+
+void Processor::addUVCoord(float *coord)
+{
+ float *coordEntry = new float[2];
+ copyVector2f(coordEntry, coord);
+
+ m_uvCoords.push_back(coordEntry);
+}
+
+void Processor::addTriangle(int *indices)
+{
+ Triangle *tri = new Triangle(indices);
+ m_triangles.push_back(tri);
+}
+
+void Processor::setImageSize(int width, int height)
+{
+ m_imageWidth = width;
+ m_imageHeight = height;
+}
+
+void Processor::prepare()
+{
+ int i, j, x, y;
+
+ long numPixels = m_imageWidth * m_imageHeight;
+ long pixelBuffSize = numPixels * 4;
+
+ m_originalImagePixels = new float[pixelBuffSize];
+ memcpy(m_originalImagePixels, m_imagePixels, sizeof(float) * pixelBuffSize);
+
+ m_imageAlpha = new unsigned short[numPixels];
+ memset(m_imageAlpha, 0, sizeof(unsigned short) * numPixels);
+
+ if (m_processType != 0) {
+ return;
+ }
+
+ //cout << m_regionWidth << ", " << m_regionHeight << endl;
+ //printMatrix4f(m_perspectiveMatrix);
+
+ const float normalAngleInner = 80.0f;
+ const float normalAngle = (normalAngleInner + 90.0f) / 2.0f;
+ const float normalAngleCos = (float) cos(normalAngle * PI / 180.0f);
+
+ float viewDirPersp[3];
+
+ for (i = 0; i < (int) m_vertexCoords.size(); i++) {
+ float *vertCoord = m_vertexCoords[i];
+
+ float *scrCoord = new float[4];
+ if (m_orthogonal) {
+ multiplyMatrix4fVector3f(scrCoord, m_perspectiveMatrix, vertCoord);
+
+ scrCoord[0] = (m_regionWidth * 0.5f) + (m_regionWidth * 0.5f) * scrCoord[0];
+ scrCoord[1] = (m_regionHeight * 0.5f) + (m_regionHeight * 0.5f) * scrCoord[1];
+ } else {
+ copyVector3f(scrCoord, vertCoord);
+ scrCoord[3] = 1.0f;
+ multiplyMatrix4fVector4f(scrCoord, m_perspectiveMatrix, scrCoord);
+
+ scrCoord[0] = m_regionWidth * 0.5f
+ + m_regionWidth * 0.5f * scrCoord[0] / scrCoord[3];
+ scrCoord[1] = m_regionHeight * 0.5f
+ + m_regionHeight * 0.5f * scrCoord[1] / scrCoord[3];
+ scrCoord[2] = scrCoord[2] / scrCoord[3];
+ }
+
+ m_screenCoords.push_back(scrCoord);
+
+ float *vertNorm = m_vertexNormals[i];
+
+ int vertState = 0;
+ if (m_orthogonal) {
+ if (dotVector3f(m_viewDirection, vertNorm) <= normalAngleCos) {
+ vertState |= 1;
+ }
+ } else {
+ subtractVector3f(viewDirPersp, m_viewPosition, vertCoord);
+ normalizeVector3f(viewDirPersp);
+
+ if (dotVector3f(viewDirPersp, vertNorm) <= normalAngleCos) {
+ vertState |= 1;
+ }
+ }
+
+ m_vertexStates.push_back(vertState);
+ }
+
+ int triVertIndices[3];
+ float *triUvCoords[3];
+ float minUvCoord[2];
+ float maxUvCoord[2];
+ float uvCoord[2];
+ int minImgRect[2];
+ int maxImgRect[2];
+
+ for (i = 0; i < (int) m_triangles.size(); i++) {
+ Triangle *tri = m_triangles[i];
+ int *triIndices = tri->getIndices();
+
+ triVertIndices[0] = m_vertexIndices[triIndices[0]];
+ triVertIndices[1] = m_vertexIndices[triIndices[1]];
+ triVertIndices[2] = m_vertexIndices[triIndices[2]];
+
+ bool culled = true;
+ for (j = 0; j < 3; j++) {
+ int vertState = m_vertexStates[triVertIndices[j]];
+ if ((vertState & 1) == 0) {
+ culled = false;
+ break;
+ }
+ }
+
+ if (culled) {
+ continue;
+ }
+
+ float *v1ScrCoord = m_screenCoords[triVertIndices[0]];
+ float *v2ScrCoord = m_screenCoords[triVertIndices[1]];
+ float *v3ScrCoord = m_screenCoords[triVertIndices[2]];
+
+ //cout << "Triangle #" << i << endl;
+
+ //printVector4f(v1ScrCoord);
+ //printVector4f(v2ScrCoord);
+ //printVector4f(v3ScrCoord);
+
+ triUvCoords[0] = m_uvCoords[triIndices[0]];
+ triUvCoords[1] = m_uvCoords[triIndices[1]];
+ triUvCoords[2] = m_uvCoords[triIndices[2]];
+
+ //printVector2f(triUvCoords[0]);
+ //printVector2f(triUvCoords[1]);
+ //printVector2f(triUvCoords[2]);
+
+ minUvCoord[0] = numeric_limits::max();
+ minUvCoord[1] = numeric_limits::max();
+ maxUvCoord[0] = -numeric_limits::max();
+ maxUvCoord[1] = -numeric_limits::max();
+
+ for (j = 0; j < 3; j++) {
+ minUvCoord[0] = triUvCoords[j][0] < minUvCoord[0] ? triUvCoords[j][0] : minUvCoord[0];
+ minUvCoord[1] = triUvCoords[j][1] < minUvCoord[1] ? triUvCoords[j][1] : minUvCoord[1];
+ maxUvCoord[0] = triUvCoords[j][0] > maxUvCoord[0] ? triUvCoords[j][0] : maxUvCoord[0];
+ maxUvCoord[1] = triUvCoords[j][1] > maxUvCoord[1] ? triUvCoords[j][1] : maxUvCoord[1];
+ }
+
+ minImgRect[0] = (int) (m_imageWidth * minUvCoord[0]);
+ minImgRect[1] = (int) (m_imageHeight * minUvCoord[1]);
+ maxImgRect[0] = (int) (m_imageWidth * maxUvCoord[0] + 1);
+ maxImgRect[1] = (int) (m_imageHeight * maxUvCoord[1] + 1);
+
+ //cout << minImgRect[0] << ", " << minImgRect[1] << endl;
+ //cout << maxImgRect[0] << ", " << maxImgRect[1] << endl;
+
+ for (y = minImgRect[1]; y < maxImgRect[1]; y++) {
+ uvCoord[1] = y / (float) m_imageHeight;
+
+ for (x = minImgRect[0]; x < maxImgRect[0]; x++) {
+ uvCoord[0] = x / (float) m_imageWidth;
+
+ preparePixel(x, y, uvCoord, v1ScrCoord, v2ScrCoord, v3ScrCoord, triUvCoords);
+ }
+ }
+ }
+}
+
+void Processor::processStroke(float *pos)
+{
+ if (m_processType == 0) {
+ processStroke3d(pos);
+ } else {
+ processStroke2d(pos);
+ }
+}
+
+void Processor::preparePixel(int x, int y, float *uvCoord,
+ float *v1ScrCoord, float *v2ScrCoord, float *v3ScrCoord, float **triUvCoords)
+{
+ if (!checkIntersectPointPolygon2d(uvCoord, triUvCoords, 3)) {
+ return;
+ }
+
+ float pixelScrCoord[4];
+ if (m_orthogonal) {
+ calcScreenCoordOrthogonal(pixelScrCoord,
+ uvCoord, v1ScrCoord, v2ScrCoord, v3ScrCoord,
+ triUvCoords[0], triUvCoords[1], triUvCoords[2]);
+ } else {
+ calcScreenCoordPerspective(pixelScrCoord,
+ uvCoord, v1ScrCoord, v2ScrCoord, v3ScrCoord,
+ triUvCoords[0], triUvCoords[1], triUvCoords[2]);
+ }
+
+ x = x % m_imageWidth;
+ y = y % m_imageHeight;
+
+ int imgCoord[] = {x, y};
+
+ PixelState *pixelState = new PixelState();
+ pixelState->setScreenCoord(pixelScrCoord);
+ pixelState->setImageCoord(imgCoord);
+
+ m_pixelStates.push_back(pixelState);
+}
+
+void Processor::processStroke3d(float *pos)
+{
+ int i;
+
+ float brushRadiusSq = m_brushSize * m_brushSize;
+
+ for (i = 0; i < (int) m_pixelStates.size(); i++) {
+ PixelState *pixelState = m_pixelStates[i];
+
+ float distSq = lenSquaredVector2f(pos, pixelState->getScreenCoord());
+
+ if (distSq >= brushRadiusSq) {
+ continue;
+ }
+
+ int *imgCoord = pixelState->getImageCoord();
+
+ float dist = (float) sqrt(distSq);
+ float falloff = calcBrushFalloff(dist, m_brushSize);
+
+ processStrokePixel(imgCoord[0], imgCoord[1], falloff);
+ }
+}
+
+void Processor::processStroke2d(float *pos)
+{
+ int x, y;
+
+ int px = (int) (m_imageWidth * pos[0]);
+ int py = (int) (m_imageHeight * pos[1]);
+
+ int brushRadius = (int) (m_imageWidth * m_brushSize);
+
+ int x1 = px - brushRadius;
+ int y1 = py - brushRadius;
+ int x2 = px + brushRadius;
+ int y2 = py + brushRadius;
+
+ x1 = max(min(x1, m_imageWidth), 0);
+ y1 = max(min(y1, m_imageHeight), 0);
+ x2 = max(min(x2, m_imageWidth), 0);
+ y2 = max(min(y2, m_imageHeight), 0);
+
+ float brushRadiusSq = brushRadius * (float) brushRadius;
+
+ for (y = y1; y < y2; y++) {
+ for (x = x1; x < x2; x++) {
+ float distSq = (px - x) * (float) (px - x) + (py - y) * (float) (py - y);
+
+ if (distSq >= brushRadiusSq) {
+ continue;
+ }
+
+ float dist = (float) sqrt(distSq);
+ float falloff = calcBrushFalloff(dist, (float) brushRadius);
+
+ processStrokePixel(x, y, falloff);
+ }
+ }
+}
+
+void Processor::processStrokePixel(int x, int y, float falloff)
+{
+ long alphaOffset = m_imageWidth * y + x;
+
+ float alphaTemp = falloff * m_brushStrength;
+
+ float alphaSaved = m_imageAlpha[alphaOffset] / 65535.0f;
+ float alpha = (alphaTemp - alphaSaved * falloff) + alphaSaved;
+
+ if (alpha > 1.0f) {
+ alpha = 1.0f;
+ }
+
+ if (alpha > alphaSaved) {
+ m_imageAlpha[alphaOffset] = (unsigned short) (alpha * 65535.0f);
+ }
+ else {
+ return;
+ }
+
+ if (alpha <= 0) {
+ return;
+ }
+
+ long pixelOffset = (m_imageWidth * y + x) * 4;
+
+ float *addr = m_imagePixels + pixelOffset;
+ float *origAddr = m_originalImagePixels + pixelOffset;
+
+ int mirrorOffset = 0;
+ if (m_mirrorAxis == 0) {
+ mirrorOffset = (m_imageWidth * y + (m_imageWidth - x)) * 4;
+ } else {
+ mirrorOffset = (m_imageWidth * (m_imageHeight - y - 1) + x) * 4;
+ }
+
+ float *mirrorAddr = m_imagePixels + mirrorOffset;
+
+ float srcColor[4];
+ float destColor[4];
+
+ srcColor[0] = *mirrorAddr;
+ srcColor[1] = *(mirrorAddr + 1);
+ srcColor[2] = *(mirrorAddr + 2);
+ srcColor[3] = *(mirrorAddr + 3);
+
+ //srcColor[0] = 1.0f;
+ //srcColor[1] = 0;
+ //srcColor[2] = 0;
+ //srcColor[3] = 1.0f;
+
+ destColor[0] = *origAddr;
+ destColor[1] = *(origAddr + 1);
+ destColor[2] = *(origAddr + 2);
+ destColor[3] = *(origAddr + 3);
+
+ multiplyVector4fValue(srcColor, srcColor, alpha);
+ //srcColor[3] = alpha;
+
+ if (srcColor[3] > 0) {
+ float t = srcColor[3];
+ float tComp = 1.0f - t;
+
+ destColor[0] = tComp * destColor[0] + srcColor[0];
+ destColor[1] = tComp * destColor[1] + srcColor[1];
+ destColor[2] = tComp * destColor[2] + srcColor[2];
+ destColor[3] = tComp * destColor[3] + t;
+
+ *addr = destColor[0];
+ *(addr + 1) = destColor[1];
+ *(addr + 2) = destColor[2];
+ *(addr + 3) = destColor[3];
+ }
+}
+
+float Processor::calcBrushFalloff(float dist, float len)
+{
+ float falloff = 1.0f; // default: constant
+
+ if (dist >= len) {
+ return 0;
+ }
+
+ float p = 1.0f - dist / len;
+
+ if (m_brushFalloffType == 0) { // smooth
+ falloff = 3.0f * p * p - 2.0f * p * p * p;
+ }
+
+ if (falloff < 0) {
+ falloff = 0;
+ } else if (falloff > 1.0f) {
+ falloff = 1.0f;
+ }
+
+ return falloff;
+}
diff --git a/src/symtex_processor.h b/src/symtex_processor.h
new file mode 100644
index 0000000..a409541
--- /dev/null
+++ b/src/symtex_processor.h
@@ -0,0 +1,141 @@
+/*
+ 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 .
+*/
+
+#ifndef SYMTEX_PROCESSOR_H_
+#define SYMTEX_PROCESSOR_H_
+
+#include
+
+#if defined(_MSC_VER)
+ #define EXPORT __declspec(dllexport)
+#else
+ #define EXPORT
+#endif
+
+extern "C" {
+
+EXPORT int SYMTEX_init(int type);
+EXPORT void SYMTEX_free();
+EXPORT void SYMTEX_setRegionSize(int width, int height);
+EXPORT void SYMTEX_setOrthogonal(int isOrtho);
+EXPORT void SYMTEX_setPerspectiveMatrix(float *mat);
+EXPORT void SYMTEX_setViewPosition(float *pos);
+EXPORT void SYMTEX_setViewDirection(float *dir);
+EXPORT void SYMTEX_setVertexCoords(float *coords, int numCoords);
+EXPORT void SYMTEX_setVertexNormals(float *normals, int numNormals);
+EXPORT void SYMTEX_setVertexIndices(int *indices, int numIndices);
+EXPORT void SYMTEX_setUVCoords(float *coords, int numCoords);
+EXPORT void SYMTEX_setTriangles(int *indices, int numTriangles);
+EXPORT void SYMTEX_setImageSize(int width, int height);
+EXPORT void SYMTEX_setImagePixels(float *pixels);
+EXPORT void SYMTEX_setMirrorAxis(int axis);
+EXPORT void SYMTEX_setBrushSize(float size);
+EXPORT void SYMTEX_setBrushStrength(float strength);
+EXPORT void SYMTEX_setBrushFalloffType(int type);
+EXPORT void SYMTEX_prepare();
+EXPORT void SYMTEX_processStroke(float *pos);
+
+}
+
+class Triangle
+{
+public:
+ Triangle();
+ Triangle(int *indices);
+ virtual ~Triangle();
+ int *getIndices() { return m_indices; }
+
+private:
+ int m_indices[3];
+};
+
+class PixelState
+{
+public:
+ PixelState();
+ virtual ~PixelState();
+ float *getScreenCoord() { return m_screenCoord; }
+ void setScreenCoord(float *coord);
+ int *getImageCoord() { return m_imageCoord; }
+ void setImageCoord(int *coord);
+
+private:
+ float m_screenCoord[4];
+ int m_imageCoord[2];
+};
+
+// brush position & size
+// 3D: screen coordinate, 2D: uv coordinate (0 - 1.0)
+
+class Processor
+{
+public:
+ Processor(int type);
+ virtual ~Processor();
+ void setRegionSize(int width, int height);
+ void setOrthogonal(bool isOrtho) { m_orthogonal = isOrtho; }
+ void setPerspectiveMatrix(float *mat);
+ void setViewPosition(float *pos);
+ void setViewDirection(float *dir);
+ void addVertexCoord(float *coord);
+ void addVertexNormal(float *norm);
+ void addVertexIndex(int index);
+ void addUVCoord(float *coord);
+ void addTriangle(int *indices);
+ void setImageSize(int width, int height);
+ void setImagePixels(float *pixels) { m_imagePixels = pixels; }
+ void setMirrorAxis(int axis) { m_mirrorAxis = axis; }
+ void setBrushSize(float size) { m_brushSize = size; }
+ void setBrushStrength(float strength) { m_brushStrength = strength; }
+ void setBrushFalloffType(int type) { m_brushFalloffType = type; }
+ void prepare();
+ void processStroke(float *pos);
+
+private:
+ void preparePixel(int x, int y, float *uvCoord,
+ float *v1ScrCoord, float *v2ScrCoord, float *v3ScrCoord, float **triUvCoords);
+ void processStroke3d(float *pos);
+ void processStroke2d(float *pos);
+ void processStrokePixel(int x, int y, float falloff);
+ float calcBrushFalloff(float dist, float len);
+ int m_processType;
+ int m_regionWidth;
+ int m_regionHeight;
+ bool m_orthogonal;
+ float m_perspectiveMatrix[16];
+ float m_viewPosition[3];
+ float m_viewDirection[3];
+ std::vector m_vertexCoords;
+ std::vector m_vertexNormals;
+ std::vector m_screenCoords;
+ std::vector m_vertexIndices;
+ std::vector m_uvCoords;
+ std::vector m_triangles;
+ std::vector m_vertexStates;
+ int m_imageWidth;
+ int m_imageHeight;
+ float *m_imagePixels;
+ float *m_originalImagePixels;
+ unsigned short *m_imageAlpha;
+ std::vector m_pixelStates;
+ int m_mirrorAxis;
+ float m_brushSize;
+ float m_brushStrength;
+ int m_brushFalloffType;
+};
+
+#endif // SYMTEX_PROCESSOR_H_