493 lines
17 KiB
Python
493 lines
17 KiB
Python
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||
|
|
|
||
|
|
__author__ = "Nutti <nutti.metro@gmail.com>"
|
||
|
|
__status__ = "production"
|
||
|
|
__version__ = "6.7"
|
||
|
|
__date__ = "9 Sep 2022"
|
||
|
|
|
||
|
|
from math import pi, cos, tan, sin
|
||
|
|
|
||
|
|
import bpy
|
||
|
|
import bmesh
|
||
|
|
from mathutils import Vector
|
||
|
|
from bpy_extras import view3d_utils
|
||
|
|
from mathutils.bvhtree import BVHTree
|
||
|
|
from mathutils.geometry import barycentric_transform
|
||
|
|
from bpy.props import (
|
||
|
|
BoolProperty,
|
||
|
|
IntProperty,
|
||
|
|
EnumProperty,
|
||
|
|
FloatProperty,
|
||
|
|
)
|
||
|
|
|
||
|
|
from .. import common
|
||
|
|
from ..utils.bl_class_registry import BlClassRegistry
|
||
|
|
from ..utils.property_class_registry import PropertyClassRegistry
|
||
|
|
from ..utils import compatibility as compat
|
||
|
|
|
||
|
|
import gpu
|
||
|
|
from gpu_extras.batch import batch_for_shader
|
||
|
|
|
||
|
|
|
||
|
|
def _is_valid_context(context):
|
||
|
|
# only 'VIEW_3D' space is allowed to execute
|
||
|
|
if not common.is_valid_space(context, ['VIEW_3D']):
|
||
|
|
return False
|
||
|
|
|
||
|
|
objs = common.get_uv_editable_objects(context)
|
||
|
|
if not objs:
|
||
|
|
return False
|
||
|
|
|
||
|
|
# only edit mode is allowed to execute
|
||
|
|
if context.object.mode != 'EDIT':
|
||
|
|
return False
|
||
|
|
|
||
|
|
return True
|
||
|
|
|
||
|
|
|
||
|
|
def _get_strength(p, len_, factor):
|
||
|
|
f = factor
|
||
|
|
|
||
|
|
if p > len_:
|
||
|
|
return 0.0
|
||
|
|
|
||
|
|
if p < 0.0:
|
||
|
|
return f
|
||
|
|
|
||
|
|
return (len_ - p) * f / len_
|
||
|
|
|
||
|
|
|
||
|
|
@PropertyClassRegistry()
|
||
|
|
class _Properties:
|
||
|
|
idname = "uv_sculpt"
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def init_props(cls, scene):
|
||
|
|
def get_func(_):
|
||
|
|
return MUV_OT_UVSculpt.is_running(bpy.context)
|
||
|
|
|
||
|
|
def set_func(_, __):
|
||
|
|
pass
|
||
|
|
|
||
|
|
def update_func(_, __):
|
||
|
|
bpy.ops.uv.muv_uv_sculpt('INVOKE_REGION_WIN')
|
||
|
|
|
||
|
|
scene.muv_uv_sculpt_enabled = BoolProperty(
|
||
|
|
name="UV Sculpt",
|
||
|
|
description="UV Sculpt is enabled",
|
||
|
|
default=False
|
||
|
|
)
|
||
|
|
scene.muv_uv_sculpt_enable = BoolProperty(
|
||
|
|
name="UV Sculpt Showed",
|
||
|
|
description="UV Sculpt is enabled",
|
||
|
|
default=False,
|
||
|
|
get=get_func,
|
||
|
|
set=set_func,
|
||
|
|
update=update_func
|
||
|
|
)
|
||
|
|
scene.muv_uv_sculpt_radius = IntProperty(
|
||
|
|
name="Radius",
|
||
|
|
description="Radius of the brush",
|
||
|
|
min=1,
|
||
|
|
max=500,
|
||
|
|
default=30
|
||
|
|
)
|
||
|
|
scene.muv_uv_sculpt_strength = FloatProperty(
|
||
|
|
name="Strength",
|
||
|
|
description="How powerful the effect of the brush when applied",
|
||
|
|
min=0.0,
|
||
|
|
max=1.0,
|
||
|
|
default=0.03,
|
||
|
|
)
|
||
|
|
scene.muv_uv_sculpt_tools = EnumProperty(
|
||
|
|
name="Tools",
|
||
|
|
description="Select Tools for the UV sculpt brushes",
|
||
|
|
items=[
|
||
|
|
('GRAB', "Grab", "Grab UVs"),
|
||
|
|
('RELAX', "Relax", "Relax UVs"),
|
||
|
|
('PINCH', "Pinch", "Pinch UVs")
|
||
|
|
],
|
||
|
|
default='GRAB'
|
||
|
|
)
|
||
|
|
scene.muv_uv_sculpt_show_brush = BoolProperty(
|
||
|
|
name="Show Brush",
|
||
|
|
description="Show Brush",
|
||
|
|
default=True
|
||
|
|
)
|
||
|
|
scene.muv_uv_sculpt_pinch_invert = BoolProperty(
|
||
|
|
name="Invert",
|
||
|
|
description="Pinch UV to invert direction",
|
||
|
|
default=False
|
||
|
|
)
|
||
|
|
scene.muv_uv_sculpt_relax_method = EnumProperty(
|
||
|
|
name="Method",
|
||
|
|
description="Algorithm used for relaxation",
|
||
|
|
items=[
|
||
|
|
('HC', "HC", "Use HC method for relaxation"),
|
||
|
|
('LAPLACIAN', "Laplacian",
|
||
|
|
"Use laplacian method for relaxation")
|
||
|
|
],
|
||
|
|
default='HC'
|
||
|
|
)
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def del_props(cls, scene):
|
||
|
|
del scene.muv_uv_sculpt_enabled
|
||
|
|
del scene.muv_uv_sculpt_enable
|
||
|
|
del scene.muv_uv_sculpt_radius
|
||
|
|
del scene.muv_uv_sculpt_strength
|
||
|
|
del scene.muv_uv_sculpt_tools
|
||
|
|
del scene.muv_uv_sculpt_show_brush
|
||
|
|
del scene.muv_uv_sculpt_pinch_invert
|
||
|
|
del scene.muv_uv_sculpt_relax_method
|
||
|
|
|
||
|
|
|
||
|
|
def location_3d_to_region_2d_extra(region, rv3d, coord):
|
||
|
|
coord_2d = view3d_utils.location_3d_to_region_2d(region, rv3d, coord)
|
||
|
|
if coord_2d is None:
|
||
|
|
prj = rv3d.perspective_matrix @ Vector(
|
||
|
|
(coord[0], coord[1], coord[2], 1.0))
|
||
|
|
width_half = region.width / 2.0
|
||
|
|
height_half = region.height / 2.0
|
||
|
|
coord_2d = Vector((
|
||
|
|
width_half + width_half * (prj.x / prj.w),
|
||
|
|
height_half + height_half * (prj.y / prj.w)
|
||
|
|
))
|
||
|
|
return coord_2d
|
||
|
|
|
||
|
|
|
||
|
|
@BlClassRegistry()
|
||
|
|
class MUV_OT_UVSculpt(bpy.types.Operator):
|
||
|
|
"""
|
||
|
|
Operation class: UV Sculpt in View3D
|
||
|
|
"""
|
||
|
|
|
||
|
|
bl_idname = "uv.muv_uv_sculpt"
|
||
|
|
bl_label = "UV Sculpt"
|
||
|
|
bl_description = "UV Sculpt in View3D"
|
||
|
|
bl_options = {'REGISTER'}
|
||
|
|
|
||
|
|
__handle = None
|
||
|
|
__timer = None
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def poll(cls, context):
|
||
|
|
# we can not get area/space/region from console
|
||
|
|
if common.is_console_mode():
|
||
|
|
return False
|
||
|
|
return _is_valid_context(context)
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def is_running(cls, _):
|
||
|
|
return 1 if cls.__handle else 0
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def handle_add(cls, obj, context):
|
||
|
|
if not cls.__handle:
|
||
|
|
sv = bpy.types.SpaceView3D
|
||
|
|
cls.__handle = sv.draw_handler_add(cls.draw_brush, (obj, context),
|
||
|
|
"WINDOW", "POST_PIXEL")
|
||
|
|
if not cls.__timer:
|
||
|
|
cls.__timer = context.window_manager.event_timer_add(
|
||
|
|
0.1, window=context.window)
|
||
|
|
context.window_manager.modal_handler_add(obj)
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def handle_remove(cls, context):
|
||
|
|
if cls.__handle:
|
||
|
|
sv = bpy.types.SpaceView3D
|
||
|
|
sv.draw_handler_remove(cls.__handle, "WINDOW")
|
||
|
|
cls.__handle = None
|
||
|
|
if cls.__timer:
|
||
|
|
context.window_manager.event_timer_remove(cls.__timer)
|
||
|
|
cls.__timer = None
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def draw_brush(cls, obj, context):
|
||
|
|
sc = context.scene
|
||
|
|
user_prefs = compat.get_user_preferences(context)
|
||
|
|
prefs = user_prefs.addons["magic_uv"].preferences
|
||
|
|
|
||
|
|
num_segment = 180
|
||
|
|
theta = 2 * pi / num_segment
|
||
|
|
fact_t = tan(theta)
|
||
|
|
fact_r = cos(theta)
|
||
|
|
|
||
|
|
shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||
|
|
shader.bind()
|
||
|
|
shader.uniform_float("color", prefs.uv_sculpt_brush_color)
|
||
|
|
|
||
|
|
x = sc.muv_uv_sculpt_radius * cos(0.0)
|
||
|
|
y = sc.muv_uv_sculpt_radius * sin(0.0)
|
||
|
|
coords = []
|
||
|
|
for _ in range(num_segment):
|
||
|
|
coords.append([x + obj.current_mco.x, y + obj.current_mco.y])
|
||
|
|
tx = -y
|
||
|
|
ty = x
|
||
|
|
x = x + tx * fact_t
|
||
|
|
y = y + ty * fact_t
|
||
|
|
x = x * fact_r
|
||
|
|
y = y * fact_r
|
||
|
|
|
||
|
|
batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": coords})
|
||
|
|
batch.draw(shader)
|
||
|
|
del batch
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
self.__loop_info = {} # { Object: loop_info }
|
||
|
|
self.__stroking = False
|
||
|
|
self.current_mco = Vector((0.0, 0.0))
|
||
|
|
self.__initial_mco = Vector((0.0, 0.0))
|
||
|
|
|
||
|
|
def __stroke_init(self, context, _):
|
||
|
|
sc = context.scene
|
||
|
|
|
||
|
|
self.__initial_mco = self.current_mco
|
||
|
|
|
||
|
|
objs = common.get_uv_editable_objects(context)
|
||
|
|
|
||
|
|
# get influenced UV
|
||
|
|
self.__loop_info = {}
|
||
|
|
for obj in objs:
|
||
|
|
world_mat = obj.matrix_world
|
||
|
|
bm = bmesh.from_edit_mesh(obj.data)
|
||
|
|
uv_layer = bm.loops.layers.uv.verify()
|
||
|
|
_, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D')
|
||
|
|
|
||
|
|
self.__loop_info[obj] = []
|
||
|
|
for f in bm.faces:
|
||
|
|
if not f.select:
|
||
|
|
continue
|
||
|
|
for i, l in enumerate(f.loops):
|
||
|
|
loc_2d = location_3d_to_region_2d_extra(
|
||
|
|
region, space.region_3d,
|
||
|
|
compat.matmul(world_mat, l.vert.co))
|
||
|
|
diff = loc_2d - self.__initial_mco
|
||
|
|
if diff.length < sc.muv_uv_sculpt_radius:
|
||
|
|
info = {
|
||
|
|
"face_idx": f.index,
|
||
|
|
"loop_idx": i,
|
||
|
|
"initial_vco": l.vert.co.copy(),
|
||
|
|
"initial_vco_2d": loc_2d,
|
||
|
|
"initial_uv": l[uv_layer].uv.copy(),
|
||
|
|
"strength": _get_strength(
|
||
|
|
diff.length, sc.muv_uv_sculpt_radius,
|
||
|
|
sc.muv_uv_sculpt_strength)
|
||
|
|
}
|
||
|
|
self.__loop_info[obj].append(info)
|
||
|
|
|
||
|
|
def __stroke_apply(self, context, _):
|
||
|
|
sc = context.scene
|
||
|
|
objs = common.get_uv_editable_objects(context)
|
||
|
|
|
||
|
|
for obj in objs:
|
||
|
|
world_mat = obj.matrix_world
|
||
|
|
bm = bmesh.from_edit_mesh(obj.data)
|
||
|
|
uv_layer = bm.loops.layers.uv.verify()
|
||
|
|
mco = self.current_mco
|
||
|
|
|
||
|
|
if sc.muv_uv_sculpt_tools == 'GRAB':
|
||
|
|
for info in self.__loop_info[obj]:
|
||
|
|
diff_uv = (mco - self.__initial_mco) * info["strength"]
|
||
|
|
l = bm.faces[info["face_idx"]].loops[info["loop_idx"]]
|
||
|
|
l[uv_layer].uv = info["initial_uv"] + diff_uv / 100.0
|
||
|
|
|
||
|
|
elif sc.muv_uv_sculpt_tools == 'PINCH':
|
||
|
|
_, region, space = common.get_space(
|
||
|
|
'VIEW_3D', 'WINDOW', 'VIEW_3D')
|
||
|
|
loop_info = []
|
||
|
|
for f in bm.faces:
|
||
|
|
if not f.select:
|
||
|
|
continue
|
||
|
|
for i, l in enumerate(f.loops):
|
||
|
|
loc_2d = location_3d_to_region_2d_extra(
|
||
|
|
region, space.region_3d,
|
||
|
|
compat.matmul(world_mat, l.vert.co))
|
||
|
|
diff = loc_2d - self.__initial_mco
|
||
|
|
if diff.length < sc.muv_uv_sculpt_radius:
|
||
|
|
info = {
|
||
|
|
"face_idx": f.index,
|
||
|
|
"loop_idx": i,
|
||
|
|
"initial_vco": l.vert.co.copy(),
|
||
|
|
"initial_vco_2d": loc_2d,
|
||
|
|
"initial_uv": l[uv_layer].uv.copy(),
|
||
|
|
"strength": _get_strength(
|
||
|
|
diff.length, sc.muv_uv_sculpt_radius,
|
||
|
|
sc.muv_uv_sculpt_strength)
|
||
|
|
}
|
||
|
|
loop_info.append(info)
|
||
|
|
|
||
|
|
# mouse coordinate to UV coordinate
|
||
|
|
ray_vec = view3d_utils.region_2d_to_vector_3d(
|
||
|
|
region, space.region_3d, mco)
|
||
|
|
ray_vec.normalize()
|
||
|
|
ray_orig = view3d_utils.region_2d_to_origin_3d(
|
||
|
|
region, space.region_3d, mco)
|
||
|
|
ray_tgt = ray_orig + ray_vec * 1000000.0
|
||
|
|
mwi = world_mat.inverted()
|
||
|
|
ray_orig_obj = compat.matmul(mwi, ray_orig)
|
||
|
|
ray_tgt_obj = compat.matmul(mwi, ray_tgt)
|
||
|
|
ray_dir_obj = ray_tgt_obj - ray_orig_obj
|
||
|
|
ray_dir_obj.normalize()
|
||
|
|
tree = BVHTree.FromBMesh(bm)
|
||
|
|
loc, _, fidx, _ = tree.ray_cast(ray_orig_obj, ray_dir_obj)
|
||
|
|
if not loc:
|
||
|
|
return
|
||
|
|
loops = [l for l in bm.faces[fidx].loops]
|
||
|
|
uvs = [Vector((l[uv_layer].uv.x, l[uv_layer].uv.y, 0.0))
|
||
|
|
for l in loops]
|
||
|
|
target_uv = barycentric_transform(
|
||
|
|
loc,
|
||
|
|
loops[0].vert.co, loops[1].vert.co, loops[2].vert.co,
|
||
|
|
uvs[0], uvs[1], uvs[2])
|
||
|
|
target_uv = Vector((target_uv.x, target_uv.y))
|
||
|
|
|
||
|
|
# move to target UV coordinate
|
||
|
|
for info in loop_info:
|
||
|
|
l = bm.faces[info["face_idx"]].loops[info["loop_idx"]]
|
||
|
|
if sc.muv_uv_sculpt_pinch_invert:
|
||
|
|
diff_uv = \
|
||
|
|
(l[uv_layer].uv - target_uv) * info["strength"]
|
||
|
|
else:
|
||
|
|
diff_uv = \
|
||
|
|
(target_uv - l[uv_layer].uv) * info["strength"]
|
||
|
|
l[uv_layer].uv = l[uv_layer].uv + diff_uv / 10.0
|
||
|
|
|
||
|
|
elif sc.muv_uv_sculpt_tools == 'RELAX':
|
||
|
|
_, region, space = common.get_space(
|
||
|
|
'VIEW_3D', 'WINDOW', 'VIEW_3D')
|
||
|
|
|
||
|
|
# get vertex and loop relation
|
||
|
|
vert_db = {}
|
||
|
|
for f in bm.faces:
|
||
|
|
for l in f.loops:
|
||
|
|
if l.vert in vert_db:
|
||
|
|
vert_db[l.vert]["loops"].append(l)
|
||
|
|
else:
|
||
|
|
vert_db[l.vert] = {"loops": [l]}
|
||
|
|
|
||
|
|
# get relaxation information
|
||
|
|
for k in vert_db.keys():
|
||
|
|
d = vert_db[k]
|
||
|
|
d["uv_sum"] = Vector((0.0, 0.0))
|
||
|
|
d["uv_count"] = 0
|
||
|
|
|
||
|
|
for l in d["loops"]:
|
||
|
|
ln = l.link_loop_next
|
||
|
|
lp = l.link_loop_prev
|
||
|
|
d["uv_sum"] = d["uv_sum"] + ln[uv_layer].uv
|
||
|
|
d["uv_sum"] = d["uv_sum"] + lp[uv_layer].uv
|
||
|
|
d["uv_count"] = d["uv_count"] + 2
|
||
|
|
d["uv_p"] = d["uv_sum"] / d["uv_count"]
|
||
|
|
d["uv_b"] = d["uv_p"] - d["loops"][0][uv_layer].uv
|
||
|
|
for k in vert_db.keys():
|
||
|
|
d = vert_db[k]
|
||
|
|
d["uv_sum_b"] = Vector((0.0, 0.0))
|
||
|
|
for l in d["loops"]:
|
||
|
|
ln = l.link_loop_next
|
||
|
|
lp = l.link_loop_prev
|
||
|
|
dn = vert_db[ln.vert]
|
||
|
|
dp = vert_db[lp.vert]
|
||
|
|
d["uv_sum_b"] = d["uv_sum_b"] + dn["uv_b"] + dp["uv_b"]
|
||
|
|
|
||
|
|
# apply
|
||
|
|
for f in bm.faces:
|
||
|
|
if not f.select:
|
||
|
|
continue
|
||
|
|
for i, l in enumerate(f.loops):
|
||
|
|
loc_2d = location_3d_to_region_2d_extra(
|
||
|
|
region, space.region_3d,
|
||
|
|
compat.matmul(world_mat, l.vert.co))
|
||
|
|
diff = loc_2d - self.__initial_mco
|
||
|
|
if diff.length >= sc.muv_uv_sculpt_radius:
|
||
|
|
continue
|
||
|
|
db = vert_db[l.vert]
|
||
|
|
strength = _get_strength(diff.length,
|
||
|
|
sc.muv_uv_sculpt_radius,
|
||
|
|
sc.muv_uv_sculpt_strength)
|
||
|
|
|
||
|
|
base = (1.0 - strength) * l[uv_layer].uv
|
||
|
|
if sc.muv_uv_sculpt_relax_method == 'HC':
|
||
|
|
t = 0.5 * \
|
||
|
|
(db["uv_b"] + db["uv_sum_b"] / d["uv_count"])
|
||
|
|
diff = strength * (db["uv_p"] - t)
|
||
|
|
target_uv = base + diff
|
||
|
|
elif sc.muv_uv_sculpt_relax_method == 'LAPLACIAN':
|
||
|
|
diff = strength * db["uv_p"]
|
||
|
|
target_uv = base + diff
|
||
|
|
else:
|
||
|
|
continue
|
||
|
|
|
||
|
|
l[uv_layer].uv = target_uv
|
||
|
|
|
||
|
|
bmesh.update_edit_mesh(obj.data)
|
||
|
|
|
||
|
|
def __stroke_exit(self, context, _):
|
||
|
|
sc = context.scene
|
||
|
|
objs = common.get_uv_editable_objects(context)
|
||
|
|
|
||
|
|
for obj in objs:
|
||
|
|
bm = bmesh.from_edit_mesh(obj.data)
|
||
|
|
uv_layer = bm.loops.layers.uv.verify()
|
||
|
|
mco = self.current_mco
|
||
|
|
|
||
|
|
if sc.muv_uv_sculpt_tools == 'GRAB':
|
||
|
|
for info in self.__loop_info[obj]:
|
||
|
|
diff_uv = (mco - self.__initial_mco) * info["strength"]
|
||
|
|
l = bm.faces[info["face_idx"]].loops[info["loop_idx"]]
|
||
|
|
l[uv_layer].uv = info["initial_uv"] + diff_uv / 100.0
|
||
|
|
|
||
|
|
bmesh.update_edit_mesh(obj.data)
|
||
|
|
|
||
|
|
def modal(self, context, event):
|
||
|
|
if context.area:
|
||
|
|
context.area.tag_redraw()
|
||
|
|
|
||
|
|
if not MUV_OT_UVSculpt.is_running(context):
|
||
|
|
MUV_OT_UVSculpt.handle_remove(context)
|
||
|
|
return {'FINISHED'}
|
||
|
|
|
||
|
|
self.current_mco = Vector((event.mouse_region_x, event.mouse_region_y))
|
||
|
|
|
||
|
|
region_types = [
|
||
|
|
'HEADER',
|
||
|
|
'UI',
|
||
|
|
'TOOLS',
|
||
|
|
'TOOL_PROPS',
|
||
|
|
]
|
||
|
|
if not common.mouse_on_area(event, 'VIEW_3D') or \
|
||
|
|
common.mouse_on_regions(event, 'VIEW_3D', region_types):
|
||
|
|
return {'PASS_THROUGH'}
|
||
|
|
|
||
|
|
if event.type == 'LEFTMOUSE':
|
||
|
|
if event.value == 'PRESS':
|
||
|
|
if not self.__stroking:
|
||
|
|
self.__stroke_init(context, event)
|
||
|
|
self.__stroking = True
|
||
|
|
elif event.value == 'RELEASE':
|
||
|
|
if self.__stroking:
|
||
|
|
self.__stroke_exit(context, event)
|
||
|
|
self.__stroking = False
|
||
|
|
return {'RUNNING_MODAL'}
|
||
|
|
elif event.type == 'MOUSEMOVE':
|
||
|
|
if self.__stroking:
|
||
|
|
self.__stroke_apply(context, event)
|
||
|
|
return {'RUNNING_MODAL'}
|
||
|
|
elif event.type == 'TIMER':
|
||
|
|
if self.__stroking:
|
||
|
|
self.__stroke_apply(context, event)
|
||
|
|
return {'RUNNING_MODAL'}
|
||
|
|
|
||
|
|
return {'PASS_THROUGH'}
|
||
|
|
|
||
|
|
def invoke(self, context, _):
|
||
|
|
if context.area:
|
||
|
|
context.area.tag_redraw()
|
||
|
|
|
||
|
|
if MUV_OT_UVSculpt.is_running(context):
|
||
|
|
MUV_OT_UVSculpt.handle_remove(context)
|
||
|
|
else:
|
||
|
|
MUV_OT_UVSculpt.handle_add(self, context)
|
||
|
|
|
||
|
|
return {'RUNNING_MODAL'}
|