1 # SPDX-License-Identifier: GPL-2.0-or-later
5 __author__
= "Nutti <nutti.metro@gmail.com>"
6 __status__
= "production"
8 __date__
= "6 Mar 2021"
10 from math
import pi
, cos
, tan
, sin
14 from mathutils
import Vector
15 from bpy_extras
import view3d_utils
16 from mathutils
.bvhtree
import BVHTree
17 from mathutils
.geometry
import barycentric_transform
18 from bpy
.props
import (
26 from ..utils
.bl_class_registry
import BlClassRegistry
27 from ..utils
.property_class_registry
import PropertyClassRegistry
28 from ..utils
import compatibility
as compat
31 if compat
.check_version(2, 80, 0) >= 0:
32 from ..lib
import bglx
as bgl
37 def _is_valid_context(context
):
38 objs
= common
.get_uv_editable_objects(context
)
42 # only edit mode is allowed to execute
43 if context
.object.mode
!= 'EDIT':
46 # only 'VIEW_3D' space is allowed to execute
47 if not common
.is_valid_space(context
, ['VIEW_3D']):
53 def _get_strength(p
, len_
, factor
):
62 return (len_
- p
) * f
/ len_
65 @PropertyClassRegistry()
70 def init_props(cls
, scene
):
72 return MUV_OT_UVSculpt
.is_running(bpy
.context
)
77 def update_func(_
, __
):
78 bpy
.ops
.uv
.muv_uv_sculpt('INVOKE_REGION_WIN')
80 scene
.muv_uv_sculpt_enabled
= BoolProperty(
82 description
="UV Sculpt is enabled",
85 scene
.muv_uv_sculpt_enable
= BoolProperty(
86 name
="UV Sculpt Showed",
87 description
="UV Sculpt is enabled",
93 scene
.muv_uv_sculpt_radius
= IntProperty(
95 description
="Radius of the brush",
100 scene
.muv_uv_sculpt_strength
= FloatProperty(
102 description
="How powerful the effect of the brush when applied",
107 scene
.muv_uv_sculpt_tools
= EnumProperty(
109 description
="Select Tools for the UV sculpt brushes",
111 ('GRAB', "Grab", "Grab UVs"),
112 ('RELAX', "Relax", "Relax UVs"),
113 ('PINCH', "Pinch", "Pinch UVs")
117 scene
.muv_uv_sculpt_show_brush
= BoolProperty(
119 description
="Show Brush",
122 scene
.muv_uv_sculpt_pinch_invert
= BoolProperty(
124 description
="Pinch UV to invert direction",
127 scene
.muv_uv_sculpt_relax_method
= EnumProperty(
129 description
="Algorithm used for relaxation",
131 ('HC', "HC", "Use HC method for relaxation"),
132 ('LAPLACIAN', "Laplacian",
133 "Use laplacian method for relaxation")
139 def del_props(cls
, scene
):
140 del scene
.muv_uv_sculpt_enabled
141 del scene
.muv_uv_sculpt_enable
142 del scene
.muv_uv_sculpt_radius
143 del scene
.muv_uv_sculpt_strength
144 del scene
.muv_uv_sculpt_tools
145 del scene
.muv_uv_sculpt_show_brush
146 del scene
.muv_uv_sculpt_pinch_invert
147 del scene
.muv_uv_sculpt_relax_method
150 def location_3d_to_region_2d_extra(region
, rv3d
, coord
):
151 coord_2d
= view3d_utils
.location_3d_to_region_2d(region
, rv3d
, coord
)
153 prj
= rv3d
.perspective_matrix
@ Vector(
154 (coord
[0], coord
[1], coord
[2], 1.0))
155 width_half
= region
.width
/ 2.0
156 height_half
= region
.height
/ 2.0
158 width_half
+ width_half
* (prj
.x
/ prj
.w
),
159 height_half
+ height_half
* (prj
.y
/ prj
.w
)
165 class MUV_OT_UVSculpt(bpy
.types
.Operator
):
167 Operation class: UV Sculpt in View3D
170 bl_idname
= "uv.muv_uv_sculpt"
171 bl_label
= "UV Sculpt"
172 bl_description
= "UV Sculpt in View3D"
173 bl_options
= {'REGISTER'}
179 def poll(cls
, context
):
180 # we can not get area/space/region from console
181 if common
.is_console_mode():
183 return _is_valid_context(context
)
186 def is_running(cls
, _
):
187 return 1 if cls
.__handle
else 0
190 def handle_add(cls
, obj
, context
):
192 sv
= bpy
.types
.SpaceView3D
193 cls
.__handle
= sv
.draw_handler_add(cls
.draw_brush
, (obj
, context
),
194 "WINDOW", "POST_PIXEL")
196 cls
.__timer
= context
.window_manager
.event_timer_add(
197 0.1, window
=context
.window
)
198 context
.window_manager
.modal_handler_add(obj
)
201 def handle_remove(cls
, context
):
203 sv
= bpy
.types
.SpaceView3D
204 sv
.draw_handler_remove(cls
.__handle
, "WINDOW")
207 context
.window_manager
.event_timer_remove(cls
.__timer
)
211 def draw_brush(cls
, obj
, context
):
213 user_prefs
= compat
.get_user_preferences(context
)
214 prefs
= user_prefs
.addons
["magic_uv"].preferences
217 theta
= 2 * pi
/ num_segment
220 color
= prefs
.uv_sculpt_brush_color
222 bgl
.glBegin(bgl
.GL_LINE_STRIP
)
223 bgl
.glColor4f(color
[0], color
[1], color
[2], color
[3])
224 x
= sc
.muv_uv_sculpt_radius
* cos(0.0)
225 y
= sc
.muv_uv_sculpt_radius
* sin(0.0)
226 for _
in range(num_segment
):
227 bgl
.glVertex2f(x
+ obj
.current_mco
.x
, y
+ obj
.current_mco
.y
)
237 self
.__loop
_info
= {} # { Object: loop_info }
238 self
.__stroking
= False
239 self
.current_mco
= Vector((0.0, 0.0))
240 self
.__initial
_mco
= Vector((0.0, 0.0))
242 def __stroke_init(self
, context
, _
):
245 self
.__initial
_mco
= self
.current_mco
247 objs
= common
.get_uv_editable_objects(context
)
250 self
.__loop
_info
= {}
252 world_mat
= obj
.matrix_world
253 bm
= bmesh
.from_edit_mesh(obj
.data
)
254 uv_layer
= bm
.loops
.layers
.uv
.verify()
255 _
, region
, space
= common
.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D')
257 self
.__loop
_info
[obj
] = []
261 for i
, l
in enumerate(f
.loops
):
262 loc_2d
= location_3d_to_region_2d_extra(
263 region
, space
.region_3d
,
264 compat
.matmul(world_mat
, l
.vert
.co
))
265 diff
= loc_2d
- self
.__initial
_mco
266 if diff
.length
< sc
.muv_uv_sculpt_radius
:
270 "initial_vco": l
.vert
.co
.copy(),
271 "initial_vco_2d": loc_2d
,
272 "initial_uv": l
[uv_layer
].uv
.copy(),
273 "strength": _get_strength(
274 diff
.length
, sc
.muv_uv_sculpt_radius
,
275 sc
.muv_uv_sculpt_strength
)
277 self
.__loop
_info
[obj
].append(info
)
279 def __stroke_apply(self
, context
, _
):
281 objs
= common
.get_uv_editable_objects(context
)
284 world_mat
= obj
.matrix_world
285 bm
= bmesh
.from_edit_mesh(obj
.data
)
286 uv_layer
= bm
.loops
.layers
.uv
.verify()
287 mco
= self
.current_mco
289 if sc
.muv_uv_sculpt_tools
== 'GRAB':
290 for info
in self
.__loop
_info
[obj
]:
291 diff_uv
= (mco
- self
.__initial
_mco
) * info
["strength"]
292 l
= bm
.faces
[info
["face_idx"]].loops
[info
["loop_idx"]]
293 l
[uv_layer
].uv
= info
["initial_uv"] + diff_uv
/ 100.0
295 elif sc
.muv_uv_sculpt_tools
== 'PINCH':
296 _
, region
, space
= common
.get_space(
297 'VIEW_3D', 'WINDOW', 'VIEW_3D')
302 for i
, l
in enumerate(f
.loops
):
303 loc_2d
= location_3d_to_region_2d_extra(
304 region
, space
.region_3d
,
305 compat
.matmul(world_mat
, l
.vert
.co
))
306 diff
= loc_2d
- self
.__initial
_mco
307 if diff
.length
< sc
.muv_uv_sculpt_radius
:
311 "initial_vco": l
.vert
.co
.copy(),
312 "initial_vco_2d": loc_2d
,
313 "initial_uv": l
[uv_layer
].uv
.copy(),
314 "strength": _get_strength(
315 diff
.length
, sc
.muv_uv_sculpt_radius
,
316 sc
.muv_uv_sculpt_strength
)
318 loop_info
.append(info
)
320 # mouse coordinate to UV coordinate
321 ray_vec
= view3d_utils
.region_2d_to_vector_3d(
322 region
, space
.region_3d
, mco
)
324 ray_orig
= view3d_utils
.region_2d_to_origin_3d(
325 region
, space
.region_3d
, mco
)
326 ray_tgt
= ray_orig
+ ray_vec
* 1000000.0
327 mwi
= world_mat
.inverted()
328 ray_orig_obj
= compat
.matmul(mwi
, ray_orig
)
329 ray_tgt_obj
= compat
.matmul(mwi
, ray_tgt
)
330 ray_dir_obj
= ray_tgt_obj
- ray_orig_obj
331 ray_dir_obj
.normalize()
332 tree
= BVHTree
.FromBMesh(bm
)
333 loc
, _
, fidx
, _
= tree
.ray_cast(ray_orig_obj
, ray_dir_obj
)
336 loops
= [l
for l
in bm
.faces
[fidx
].loops
]
337 uvs
= [Vector((l
[uv_layer
].uv
.x
, l
[uv_layer
].uv
.y
, 0.0))
339 target_uv
= barycentric_transform(
341 loops
[0].vert
.co
, loops
[1].vert
.co
, loops
[2].vert
.co
,
342 uvs
[0], uvs
[1], uvs
[2])
343 target_uv
= Vector((target_uv
.x
, target_uv
.y
))
345 # move to target UV coordinate
346 for info
in loop_info
:
347 l
= bm
.faces
[info
["face_idx"]].loops
[info
["loop_idx"]]
348 if sc
.muv_uv_sculpt_pinch_invert
:
350 (l
[uv_layer
].uv
- target_uv
) * info
["strength"]
353 (target_uv
- l
[uv_layer
].uv
) * info
["strength"]
354 l
[uv_layer
].uv
= l
[uv_layer
].uv
+ diff_uv
/ 10.0
356 elif sc
.muv_uv_sculpt_tools
== 'RELAX':
357 _
, region
, space
= common
.get_space(
358 'VIEW_3D', 'WINDOW', 'VIEW_3D')
360 # get vertex and loop relation
364 if l
.vert
in vert_db
:
365 vert_db
[l
.vert
]["loops"].append(l
)
367 vert_db
[l
.vert
] = {"loops": [l
]}
369 # get relaxation information
370 for k
in vert_db
.keys():
372 d
["uv_sum"] = Vector((0.0, 0.0))
376 ln
= l
.link_loop_next
377 lp
= l
.link_loop_prev
378 d
["uv_sum"] = d
["uv_sum"] + ln
[uv_layer
].uv
379 d
["uv_sum"] = d
["uv_sum"] + lp
[uv_layer
].uv
380 d
["uv_count"] = d
["uv_count"] + 2
381 d
["uv_p"] = d
["uv_sum"] / d
["uv_count"]
382 d
["uv_b"] = d
["uv_p"] - d
["loops"][0][uv_layer
].uv
383 for k
in vert_db
.keys():
385 d
["uv_sum_b"] = Vector((0.0, 0.0))
387 ln
= l
.link_loop_next
388 lp
= l
.link_loop_prev
389 dn
= vert_db
[ln
.vert
]
390 dp
= vert_db
[lp
.vert
]
391 d
["uv_sum_b"] = d
["uv_sum_b"] + dn
["uv_b"] + dp
["uv_b"]
397 for i
, l
in enumerate(f
.loops
):
398 loc_2d
= location_3d_to_region_2d_extra(
399 region
, space
.region_3d
,
400 compat
.matmul(world_mat
, l
.vert
.co
))
401 diff
= loc_2d
- self
.__initial
_mco
402 if diff
.length
>= sc
.muv_uv_sculpt_radius
:
405 strength
= _get_strength(diff
.length
,
406 sc
.muv_uv_sculpt_radius
,
407 sc
.muv_uv_sculpt_strength
)
409 base
= (1.0 - strength
) * l
[uv_layer
].uv
410 if sc
.muv_uv_sculpt_relax_method
== 'HC':
412 (db
["uv_b"] + db
["uv_sum_b"] / d
["uv_count"])
413 diff
= strength
* (db
["uv_p"] - t
)
414 target_uv
= base
+ diff
415 elif sc
.muv_uv_sculpt_relax_method
== 'LAPLACIAN':
416 diff
= strength
* db
["uv_p"]
417 target_uv
= base
+ diff
421 l
[uv_layer
].uv
= target_uv
423 bmesh
.update_edit_mesh(obj
.data
)
425 def __stroke_exit(self
, context
, _
):
427 objs
= common
.get_uv_editable_objects(context
)
430 bm
= bmesh
.from_edit_mesh(obj
.data
)
431 uv_layer
= bm
.loops
.layers
.uv
.verify()
432 mco
= self
.current_mco
434 if sc
.muv_uv_sculpt_tools
== 'GRAB':
435 for info
in self
.__loop
_info
[obj
]:
436 diff_uv
= (mco
- self
.__initial
_mco
) * info
["strength"]
437 l
= bm
.faces
[info
["face_idx"]].loops
[info
["loop_idx"]]
438 l
[uv_layer
].uv
= info
["initial_uv"] + diff_uv
/ 100.0
440 bmesh
.update_edit_mesh(obj
.data
)
442 def modal(self
, context
, event
):
444 context
.area
.tag_redraw()
446 if not MUV_OT_UVSculpt
.is_running(context
):
447 MUV_OT_UVSculpt
.handle_remove(context
)
450 self
.current_mco
= Vector((event
.mouse_region_x
, event
.mouse_region_y
))
458 if not common
.mouse_on_area(event
, 'VIEW_3D') or \
459 common
.mouse_on_regions(event
, 'VIEW_3D', region_types
):
460 return {'PASS_THROUGH'}
462 if event
.type == 'LEFTMOUSE':
463 if event
.value
== 'PRESS':
464 if not self
.__stroking
:
465 self
.__stroke
_init
(context
, event
)
466 self
.__stroking
= True
467 elif event
.value
== 'RELEASE':
469 self
.__stroke
_exit
(context
, event
)
470 self
.__stroking
= False
471 return {'RUNNING_MODAL'}
472 elif event
.type == 'MOUSEMOVE':
474 self
.__stroke
_apply
(context
, event
)
475 return {'RUNNING_MODAL'}
476 elif event
.type == 'TIMER':
478 self
.__stroke
_apply
(context
, event
)
479 return {'RUNNING_MODAL'}
481 return {'PASS_THROUGH'}
483 def invoke(self
, context
, _
):
485 context
.area
.tag_redraw()
487 if MUV_OT_UVSculpt
.is_running(context
):
488 MUV_OT_UVSculpt
.handle_remove(context
)
490 MUV_OT_UVSculpt
.handle_add(self
, context
)
492 return {'RUNNING_MODAL'}