1# ##### BEGIN GPL LICENSE BLOCK ##### 2# 3# This program is free software; you can redistribute it and/or 4# modify it under the terms of the GNU General Public License 5# as published by the Free Software Foundation; either version 2 6# of the License, or (at your option) any later version. 7# 8# This program is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with this program; if not, write to the Free Software Foundation, 15# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16# 17# ##### END GPL LICENSE BLOCK ##### 18 19# Filename : parameter_editor.py 20# Authors : Tamito Kajiyama 21# Date : 26/07/2010 22# Purpose : Interactive manipulation of stylization parameters 23 24from freestyle.types import ( 25 BinaryPredicate1D, 26 IntegrationType, 27 Interface0DIterator, 28 Nature, 29 Noise, 30 Operators, 31 StrokeAttribute, 32 UnaryPredicate0D, 33 UnaryPredicate1D, 34 TVertex, 35 Material, 36 ViewEdge, 37) 38from freestyle.chainingiterators import ( 39 ChainPredicateIterator, 40 ChainSilhouetteIterator, 41 pySketchyChainSilhouetteIterator, 42 pySketchyChainingIterator, 43) 44from freestyle.functions import ( 45 Curvature2DAngleF0D, 46 Normal2DF0D, 47 QuantitativeInvisibilityF1D, 48 VertexOrientation2DF0D, 49 CurveMaterialF0D, 50) 51from freestyle.predicates import ( 52 AndUP1D, 53 ContourUP1D, 54 ExternalContourUP1D, 55 FalseBP1D, 56 FalseUP1D, 57 Length2DBP1D, 58 NotBP1D, 59 NotUP1D, 60 OrUP1D, 61 QuantitativeInvisibilityUP1D, 62 TrueBP1D, 63 TrueUP1D, 64 WithinImageBoundaryUP1D, 65 pyNFirstUP1D, 66 pyNatureUP1D, 67 pyProjectedXBP1D, 68 pyProjectedYBP1D, 69 pyZBP1D, 70) 71from freestyle.shaders import ( 72 BackboneStretcherShader, 73 BezierCurveShader, 74 BlenderTextureShader, 75 ConstantColorShader, 76 GuidingLinesShader, 77 PolygonalizationShader, 78 pyBluePrintCirclesShader, 79 pyBluePrintEllipsesShader, 80 pyBluePrintSquaresShader, 81 RoundCapShader, 82 SamplingShader, 83 SpatialNoiseShader, 84 SquareCapShader, 85 StrokeShader, 86 StrokeTextureStepShader, 87 ThicknessNoiseShader as thickness_noise, 88 TipRemoverShader, 89) 90from freestyle.utils import ( 91 angle_x_normal, 92 bound, 93 BoundedProperty, 94 ContextFunctions, 95 curvature_from_stroke_vertex, 96 getCurrentScene, 97 iter_distance_along_stroke, 98 iter_distance_from_camera, 99 iter_distance_from_object, 100 iter_material_value, 101 iter_t2d_along_stroke, 102 normal_at_I0D, 103 pairwise, 104 simplify, 105 stroke_normal, 106) 107from _freestyle import ( 108 blendRamp, 109 evaluateColorRamp, 110 evaluateCurveMappingF, 111) 112 113import time 114import bpy 115import random 116 117from mathutils import Vector 118from math import pi, sin, cos, acos, radians, atan2 119from itertools import cycle, tee 120 121# WARNING: highly experimental, not a stable API 122# lists of callback functions 123# used by the render_freestyle_svg addon 124callbacks_lineset_pre = [] 125callbacks_modifiers_post = [] 126callbacks_lineset_post = [] 127 128 129class ColorRampModifier(StrokeShader): 130 """Primitive for the color modifiers.""" 131 132 def __init__(self, blend, influence, ramp): 133 StrokeShader.__init__(self) 134 self.blend = blend 135 self.influence = influence 136 self.ramp = ramp 137 138 def evaluate(self, t): 139 col = evaluateColorRamp(self.ramp, t) 140 return col.xyz # omit alpha 141 142 def blend_ramp(self, a, b): 143 return blendRamp(self.blend, a, self.influence, b) 144 145 146class ScalarBlendModifier(StrokeShader): 147 """Primitive for alpha and thickness modifiers.""" 148 149 def __init__(self, blend_type, influence): 150 StrokeShader.__init__(self) 151 self.blend_type = blend_type 152 self.influence = influence 153 154 def blend(self, v1, v2): 155 fac = self.influence 156 facm = 1.0 - fac 157 if self.blend_type == 'MIX': 158 v1 = facm * v1 + fac * v2 159 elif self.blend_type == 'ADD': 160 v1 += fac * v2 161 elif self.blend_type == 'MULTIPLY': 162 v1 *= facm + fac * v2 163 elif self.blend_type == 'SUBTRACT': 164 v1 -= fac * v2 165 elif self.blend_type == 'DIVIDE': 166 v1 = facm * v1 + fac * v1 / v2 if v2 != 0.0 else v1 167 elif self.blend_type == 'DIFFERENCE': 168 v1 = facm * v1 + fac * abs(v1 - v2) 169 elif self.blend_type == 'MININUM': 170 v1 = min(fac * v2, v1) 171 elif self.blend_type == 'MAXIMUM': 172 v1 = max(fac * v2, v1) 173 else: 174 raise ValueError("unknown curve blend type: " + self.blend_type) 175 return v1 176 177 178class CurveMappingModifier(ScalarBlendModifier): 179 def __init__(self, blend, influence, mapping, invert, curve): 180 ScalarBlendModifier.__init__(self, blend, influence) 181 assert mapping in {'LINEAR', 'CURVE'} 182 self.evaluate = getattr(self, mapping) 183 self.invert = invert 184 self.curve = curve 185 186 def LINEAR(self, t): 187 return (1.0 - t) if self.invert else t 188 189 def CURVE(self, t): 190 # deprecated: return evaluateCurveMappingF(self.curve, 0, t) 191 curve = self.curve 192 curve.initialize() 193 result = curve.evaluate(curve=curve.curves[0], position=t) 194 # float precision errors in t can give a very weird result for evaluate. 195 # therefore, bound the result by the curve's min and max values 196 return bound(curve.clip_min_y, result, curve.clip_max_y) 197 198 199class ThicknessModifierMixIn: 200 def __init__(self): 201 scene = getCurrentScene() 202 self.persp_camera = (scene.camera.data.type == 'PERSP') 203 204 def set_thickness(self, sv, outer, inner): 205 fe = sv.fedge 206 nature = fe.nature 207 if (nature & Nature.BORDER): 208 if self.persp_camera: 209 point = -sv.point_3d.normalized() 210 dir = point.dot(fe.normal_left) 211 else: 212 dir = fe.normal_left.z 213 if dir < 0.0: # the back side is visible 214 outer, inner = inner, outer 215 elif (nature & Nature.SILHOUETTE): 216 if fe.is_smooth: # TODO more tests needed 217 outer, inner = inner, outer 218 else: 219 outer = inner = (outer + inner) / 2 220 sv.attribute.thickness = (outer, inner) 221 222 223class ThicknessBlenderMixIn(ThicknessModifierMixIn): 224 def __init__(self, position, ratio): 225 ThicknessModifierMixIn.__init__(self) 226 self.position = position 227 self.ratio = ratio 228 229 def blend_thickness(self, svert, thickness, asymmetric=False): 230 """Blends and sets the thickness with respect to the position, blend mode and symmetry.""" 231 if asymmetric: 232 right, left = thickness 233 self.blend_thickness_asymmetric(svert, right, left) 234 else: 235 if type(thickness) not in {int, float}: 236 thickness = sum(thickness) 237 self.blend_thickness_symmetric(svert, thickness) 238 239 def blend_thickness_symmetric(self, svert, v): 240 """Blends and sets the thickness. Thickness is equal on each side of the backbone""" 241 outer, inner = svert.attribute.thickness 242 v = self.blend(outer + inner, v) 243 244 # Part 1: blend 245 if self.position == 'CENTER': 246 outer = inner = v * 0.5 247 elif self.position == 'INSIDE': 248 outer, inner = 0, v 249 elif self.position == 'OUTSIDE': 250 outer, inner = v, 0 251 elif self.position == 'RELATIVE': 252 outer, inner = v * self.ratio, v - (v * self.ratio) 253 else: 254 raise ValueError("unknown thickness position: " + position) 255 256 self.set_thickness(svert, outer, inner) 257 258 def blend_thickness_asymmetric(self, svert, right, left): 259 """Blends and sets the thickness. Thickness may be unequal on each side of the backbone""" 260 # blend the thickness values for both sides. This way, the blend mode is supported. 261 old = svert.attribute.thickness 262 new = (right, left) 263 right, left = (self.blend(*val) for val in zip(old, new)) 264 265 fe = svert.fedge 266 nature = fe.nature 267 if (nature & Nature.BORDER): 268 if self.persp_camera: 269 point = -svert.point_3d.normalized() 270 dir = point.dot(fe.normal_left) 271 else: 272 dir = fe.normal_left.z 273 if dir < 0.0: # the back side is visible 274 right, left = left, right 275 elif (nature & Nature.SILHOUETTE): 276 if fe.is_smooth: # TODO more tests needed 277 right, left = left, right 278 svert.attribute.thickness = (right, left) 279 280 281class BaseThicknessShader(StrokeShader, ThicknessModifierMixIn): 282 def __init__(self, thickness, position, ratio): 283 StrokeShader.__init__(self) 284 ThicknessModifierMixIn.__init__(self) 285 if position == 'CENTER': 286 self.outer = thickness * 0.5 287 self.inner = thickness - self.outer 288 elif position == 'INSIDE': 289 self.outer = 0 290 self.inner = thickness 291 elif position == 'OUTSIDE': 292 self.outer = thickness 293 self.inner = 0 294 elif position == 'RELATIVE': 295 self.outer = thickness * ratio 296 self.inner = thickness - self.outer 297 else: 298 raise ValueError("unknown thickness position: " + position) 299 300 def shade(self, stroke): 301 for svert in stroke: 302 self.set_thickness(svert, self.outer, self.inner) 303 304 305# Along Stroke modifiers 306 307class ColorAlongStrokeShader(ColorRampModifier): 308 """Maps a ramp to the color of the stroke, using the curvilinear abscissa (t).""" 309 310 def shade(self, stroke): 311 for svert, t in zip(stroke, iter_t2d_along_stroke(stroke)): 312 a = svert.attribute.color 313 b = self.evaluate(t) 314 svert.attribute.color = self.blend_ramp(a, b) 315 316 317class AlphaAlongStrokeShader(CurveMappingModifier): 318 """Maps a curve to the alpha/transparency of the stroke, using the curvilinear abscissa (t).""" 319 320 def shade(self, stroke): 321 for svert, t in zip(stroke, iter_t2d_along_stroke(stroke)): 322 a = svert.attribute.alpha 323 b = self.evaluate(t) 324 svert.attribute.alpha = self.blend(a, b) 325 326 327class ThicknessAlongStrokeShader(ThicknessBlenderMixIn, CurveMappingModifier): 328 """Maps a curve to the thickness of the stroke, using the curvilinear abscissa (t).""" 329 330 def __init__(self, thickness_position, thickness_ratio, 331 blend, influence, mapping, invert, curve, value_min, value_max): 332 ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio) 333 CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve) 334 self.value = BoundedProperty(value_min, value_max) 335 336 def shade(self, stroke): 337 for svert, t in zip(stroke, iter_t2d_along_stroke(stroke)): 338 b = self.value.min + self.evaluate(t) * self.value.delta 339 self.blend_thickness(svert, b) 340 341 342# -- Distance from Camera modifiers -- # 343 344class ColorDistanceFromCameraShader(ColorRampModifier): 345 """Picks a color value from a ramp based on the vertex' distance from the camera.""" 346 347 def __init__(self, blend, influence, ramp, range_min, range_max): 348 ColorRampModifier.__init__(self, blend, influence, ramp) 349 self.range = BoundedProperty(range_min, range_max) 350 351 def shade(self, stroke): 352 it = iter_distance_from_camera(stroke, *self.range) 353 for svert, t in it: 354 a = svert.attribute.color 355 b = self.evaluate(t) 356 svert.attribute.color = self.blend_ramp(a, b) 357 358 359class AlphaDistanceFromCameraShader(CurveMappingModifier): 360 """Picks an alpha value from a curve based on the vertex' distance from the camera""" 361 362 def __init__(self, blend, influence, mapping, invert, curve, range_min, range_max): 363 CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve) 364 self.range = BoundedProperty(range_min, range_max) 365 366 def shade(self, stroke): 367 it = iter_distance_from_camera(stroke, *self.range) 368 for svert, t in it: 369 a = svert.attribute.alpha 370 b = self.evaluate(t) 371 svert.attribute.alpha = self.blend(a, b) 372 373 374class ThicknessDistanceFromCameraShader(ThicknessBlenderMixIn, CurveMappingModifier): 375 """Picks a thickness value from a curve based on the vertex' distance from the camera.""" 376 377 def __init__(self, thickness_position, thickness_ratio, 378 blend, influence, mapping, invert, curve, range_min, range_max, value_min, value_max): 379 ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio) 380 CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve) 381 self.range = BoundedProperty(range_min, range_max) 382 self.value = BoundedProperty(value_min, value_max) 383 384 def shade(self, stroke): 385 for (svert, t) in iter_distance_from_camera(stroke, *self.range): 386 b = self.value.min + self.evaluate(t) * self.value.delta 387 self.blend_thickness(svert, b) 388 389 390# Distance from Object modifiers 391 392class ColorDistanceFromObjectShader(ColorRampModifier): 393 """Picks a color value from a ramp based on the vertex' distance from a given object.""" 394 395 def __init__(self, blend, influence, ramp, target, range_min, range_max): 396 ColorRampModifier.__init__(self, blend, influence, ramp) 397 if target is None: 398 raise ValueError("ColorDistanceFromObjectShader: target can't be None ") 399 self.range = BoundedProperty(range_min, range_max) 400 # construct a model-view matrix 401 matrix = getCurrentScene().camera.matrix_world.inverted() 402 # get the object location in the camera coordinate 403 self.loc = matrix @ target.location 404 405 def shade(self, stroke): 406 it = iter_distance_from_object(stroke, self.loc, *self.range) 407 for svert, t in it: 408 a = svert.attribute.color 409 b = self.evaluate(t) 410 svert.attribute.color = self.blend_ramp(a, b) 411 412 413class AlphaDistanceFromObjectShader(CurveMappingModifier): 414 """Picks an alpha value from a curve based on the vertex' distance from a given object.""" 415 416 def __init__(self, blend, influence, mapping, invert, curve, target, range_min, range_max): 417 CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve) 418 if target is None: 419 raise ValueError("AlphaDistanceFromObjectShader: target can't be None ") 420 self.range = BoundedProperty(range_min, range_max) 421 # construct a model-view matrix 422 matrix = getCurrentScene().camera.matrix_world.inverted() 423 # get the object location in the camera coordinate 424 self.loc = matrix @ target.location 425 426 def shade(self, stroke): 427 it = iter_distance_from_object(stroke, self.loc, *self.range) 428 for svert, t in it: 429 a = svert.attribute.alpha 430 b = self.evaluate(t) 431 svert.attribute.alpha = self.blend(a, b) 432 433 434class ThicknessDistanceFromObjectShader(ThicknessBlenderMixIn, CurveMappingModifier): 435 """Picks a thickness value from a curve based on the vertex' distance from a given object.""" 436 437 def __init__(self, thickness_position, thickness_ratio, 438 blend, influence, mapping, invert, curve, target, range_min, range_max, value_min, value_max): 439 ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio) 440 CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve) 441 if target is None: 442 raise ValueError("ThicknessDistanceFromObjectShader: target can't be None ") 443 self.range = BoundedProperty(range_min, range_max) 444 self.value = BoundedProperty(value_min, value_max) 445 # construct a model-view matrix 446 matrix = getCurrentScene().camera.matrix_world.inverted() 447 # get the object location in the camera coordinate 448 self.loc = matrix @ target.location 449 450 def shade(self, stroke): 451 it = iter_distance_from_object(stroke, self.loc, *self.range) 452 for svert, t in it: 453 b = self.value.min + self.evaluate(t) * self.value.delta 454 self.blend_thickness(svert, b) 455 456 457# Material modifiers 458class ColorMaterialShader(ColorRampModifier): 459 """Assigns a color to the vertices based on their underlying material.""" 460 461 def __init__(self, blend, influence, ramp, material_attribute, use_ramp): 462 ColorRampModifier.__init__(self, blend, influence, ramp) 463 self.attribute = material_attribute 464 self.use_ramp = use_ramp 465 self.func = CurveMaterialF0D() 466 467 def shade(self, stroke, attributes={'DIFF', 'SPEC', 'LINE'}): 468 it = Interface0DIterator(stroke) 469 if not self.use_ramp and self.attribute in attributes: 470 for svert in it: 471 material = self.func(it) 472 if self.attribute == 'LINE': 473 b = material.line[0:3] 474 elif self.attribute == 'DIFF': 475 b = material.diffuse[0:3] 476 else: 477 b = material.specular[0:3] 478 a = svert.attribute.color 479 svert.attribute.color = self.blend_ramp(a, b) 480 else: 481 for svert, value in iter_material_value(stroke, self.func, self.attribute): 482 a = svert.attribute.color 483 b = self.evaluate(value) 484 svert.attribute.color = self.blend_ramp(a, b) 485 486 487class AlphaMaterialShader(CurveMappingModifier): 488 """Assigns an alpha value to the vertices based on their underlying material.""" 489 490 def __init__(self, blend, influence, mapping, invert, curve, material_attribute): 491 CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve) 492 self.attribute = material_attribute 493 self.func = CurveMaterialF0D() 494 495 def shade(self, stroke): 496 for svert, value in iter_material_value(stroke, self.func, self.attribute): 497 a = svert.attribute.alpha 498 b = self.evaluate(value) 499 svert.attribute.alpha = self.blend(a, b) 500 501 502class ThicknessMaterialShader(ThicknessBlenderMixIn, CurveMappingModifier): 503 """Assigns a thickness value to the vertices based on their underlying material.""" 504 505 def __init__(self, thickness_position, thickness_ratio, 506 blend, influence, mapping, invert, curve, material_attribute, value_min, value_max): 507 ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio) 508 CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve) 509 self.attribute = material_attribute 510 self.value = BoundedProperty(value_min, value_max) 511 self.func = CurveMaterialF0D() 512 513 def shade(self, stroke): 514 for svert, value in iter_material_value(stroke, self.func, self.attribute): 515 b = self.value.min + self.evaluate(value) * self.value.delta 516 self.blend_thickness(svert, b) 517 518 519# Calligraphic thickness modifier 520 521class CalligraphicThicknessShader(ThicknessBlenderMixIn, ScalarBlendModifier): 522 """Thickness modifier for achieving a calligraphy-like effect.""" 523 524 def __init__(self, thickness_position, thickness_ratio, 525 blend_type, influence, orientation, thickness_min, thickness_max): 526 ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio) 527 ScalarBlendModifier.__init__(self, blend_type, influence) 528 self.orientation = Vector((cos(orientation), sin(orientation))) 529 self.thickness = BoundedProperty(thickness_min, thickness_max) 530 self.func = VertexOrientation2DF0D() 531 532 def shade(self, stroke): 533 it = Interface0DIterator(stroke) 534 for svert in it: 535 dir = self.func(it) 536 if dir.length != 0.0: 537 dir.normalize() 538 fac = abs(dir.orthogonal() @ self.orientation) 539 b = self.thickness.min + fac * self.thickness.delta 540 else: 541 b = self.thickness.min 542 self.blend_thickness(svert, b) 543 544 545# - Tangent Modifiers - # 546 547class TangentColorShader(ColorRampModifier): 548 """Color based on the direction of the stroke""" 549 550 def shade(self, stroke): 551 it = Interface0DIterator(stroke) 552 for svert in it: 553 angle = angle_x_normal(it) 554 fac = self.evaluate(angle / pi) 555 a = svert.attribute.color 556 svert.attribute.color = self.blend_ramp(a, fac) 557 558 559class TangentAlphaShader(CurveMappingModifier): 560 """Alpha transparency based on the direction of the stroke""" 561 562 def shade(self, stroke): 563 it = Interface0DIterator(stroke) 564 for svert in it: 565 angle = angle_x_normal(it) 566 fac = self.evaluate(angle / pi) 567 a = svert.attribute.alpha 568 svert.attribute.alpha = self.blend(a, fac) 569 570 571class TangentThicknessShader(ThicknessBlenderMixIn, CurveMappingModifier): 572 """Thickness based on the direction of the stroke""" 573 574 def __init__(self, thickness_position, thickness_ratio, blend, influence, mapping, invert, curve, 575 thickness_min, thickness_max): 576 ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio) 577 CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve) 578 self.thickness = BoundedProperty(thickness_min, thickness_max) 579 580 def shade(self, stroke): 581 it = Interface0DIterator(stroke) 582 for svert in it: 583 angle = angle_x_normal(it) 584 thickness = self.thickness.min + self.evaluate(angle / pi) * self.thickness.delta 585 self.blend_thickness(svert, thickness) 586 587 588# - Noise Modifiers - # 589 590class NoiseShader: 591 """Base class for noise shaders""" 592 593 def __init__(self, amplitude, period, seed=512): 594 self.amplitude = amplitude 595 self.scale = 1 / period / seed 596 self.seed = seed 597 598 def noisegen(self, stroke, n1=Noise(), n2=Noise()): 599 """Produces two noise values per StrokeVertex for every vertex in the stroke""" 600 initU1 = stroke.length_2d * self.seed + n1.rand(512) * self.seed 601 initU2 = stroke.length_2d * self.seed + n2.rand() * self.seed 602 603 for svert in stroke: 604 a = n1.turbulence_smooth(self.scale * svert.curvilinear_abscissa + initU1, 2) 605 b = n2.turbulence_smooth(self.scale * svert.curvilinear_abscissa + initU2, 2) 606 yield (svert, a, b) 607 608 609class ThicknessNoiseShader(ThicknessBlenderMixIn, ScalarBlendModifier, NoiseShader): 610 """Thickness based on pseudo-noise""" 611 612 def __init__(self, thickness_position, thickness_ratio, blend_type, 613 influence, amplitude, period, seed=512, asymmetric=True): 614 ScalarBlendModifier.__init__(self, blend_type, influence) 615 ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio) 616 NoiseShader.__init__(self, amplitude, period, seed) 617 self.asymmetric = asymmetric 618 619 def shade(self, stroke): 620 for svert, noiseval1, noiseval2 in self.noisegen(stroke): 621 (r, l) = svert.attribute.thickness 622 l += noiseval1 * self.amplitude 623 r += noiseval2 * self.amplitude 624 self.blend_thickness(svert, (r, l), self.asymmetric) 625 626 627class ColorNoiseShader(ColorRampModifier, NoiseShader): 628 """Color based on pseudo-noise""" 629 630 def __init__(self, blend, influence, ramp, amplitude, period, seed=512): 631 ColorRampModifier.__init__(self, blend, influence, ramp) 632 NoiseShader.__init__(self, amplitude, period, seed) 633 634 def shade(self, stroke): 635 for svert, noiseval1, noiseval2 in self.noisegen(stroke): 636 position = abs(noiseval1 + noiseval2) 637 svert.attribute.color = self.blend_ramp(svert.attribute.color, self.evaluate(position)) 638 639 640class AlphaNoiseShader(CurveMappingModifier, NoiseShader): 641 """Alpha transparency on based pseudo-noise""" 642 643 def __init__(self, blend, influence, mapping, invert, curve, amplitude, period, seed=512): 644 CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve) 645 NoiseShader.__init__(self, amplitude, period, seed) 646 647 def shade(self, stroke, n1=Noise(), n2=Noise()): 648 for svert, noiseval1, noiseval2 in self.noisegen(stroke): 649 position = abs(noiseval1 + noiseval2) 650 svert.attribute.alpha = self.blend(svert.attribute.alpha, self.evaluate(position)) 651 652 653# - Crease Angle Modifiers - # 654 655def crease_angle(svert): 656 """Returns the crease angle between the StrokeVertex' two adjacent faces (in radians)""" 657 fe = svert.fedge 658 if not fe or fe.is_smooth or not (fe.nature & Nature.CREASE): 659 return None 660 # make sure that the input is within the domain of the acos function 661 product = bound(-1.0, -fe.normal_left.dot(fe.normal_right), 1.0) 662 return acos(product) 663 664 665class CreaseAngleColorShader(ColorRampModifier): 666 """Color based on the crease angle between two adjacent faces on the underlying geometry""" 667 668 def __init__(self, blend, influence, ramp, angle_min, angle_max): 669 ColorRampModifier.__init__(self, blend, influence, ramp) 670 # angles are (already) in radians 671 self.angle = BoundedProperty(angle_min, angle_max) 672 673 def shade(self, stroke): 674 for svert in stroke: 675 angle = crease_angle(svert) 676 if angle is None: 677 continue 678 t = self.angle.interpolate(angle) 679 svert.attribute.color = self.blend_ramp(svert.attribute.color, self.evaluate(t)) 680 681 682class CreaseAngleAlphaShader(CurveMappingModifier): 683 """Alpha transparency based on the crease angle between two adjacent faces on the underlying geometry""" 684 685 def __init__(self, blend, influence, mapping, invert, curve, angle_min, angle_max): 686 CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve) 687 # angles are (already) in radians 688 self.angle = BoundedProperty(angle_min, angle_max) 689 690 def shade(self, stroke): 691 for svert in stroke: 692 angle = crease_angle(svert) 693 if angle is None: 694 continue 695 t = self.angle.interpolate(angle) 696 svert.attribute.alpha = self.blend(svert.attribute.alpha, self.evaluate(t)) 697 698 699class CreaseAngleThicknessShader(ThicknessBlenderMixIn, CurveMappingModifier): 700 """Thickness based on the crease angle between two adjacent faces on the underlying geometry""" 701 702 def __init__(self, thickness_position, thickness_ratio, blend, influence, mapping, invert, curve, 703 angle_min, angle_max, thickness_min, thickness_max): 704 ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio) 705 CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve) 706 # angles are (already) in radians 707 self.angle = BoundedProperty(angle_min, angle_max) 708 self.thickness = BoundedProperty(thickness_min, thickness_max) 709 710 def shade(self, stroke): 711 for svert in stroke: 712 angle = crease_angle(svert) 713 if angle is None: 714 continue 715 t = self.angle.interpolate(angle) 716 thickness = self.thickness.min + self.evaluate(t) * self.thickness.delta 717 self.blend_thickness(svert, thickness) 718 719 720# - Curvature3D Modifiers - # 721 722def normalized_absolute_curvature(svert, bounded_curvature): 723 """ 724 Gives the absolute curvature in range [0, 1]. 725 726 The actual curvature (Kr) value can be anywhere in the range [-inf, inf], where convex curvature 727 yields a positive value, and concave a negative one. These shaders only look for the magnitude 728 of the 3D curvature, hence the abs() 729 """ 730 curvature = curvature_from_stroke_vertex(svert) 731 if curvature is None: 732 return 0.0 733 return bounded_curvature.interpolate(abs(curvature)) 734 735 736class Curvature3DColorShader(ColorRampModifier): 737 """Color based on the 3D curvature of the underlying geometry""" 738 739 def __init__(self, blend, influence, ramp, curvature_min, curvature_max): 740 ColorRampModifier.__init__(self, blend, influence, ramp) 741 self.curvature = BoundedProperty(curvature_min, curvature_max) 742 743 def shade(self, stroke): 744 for svert in stroke: 745 t = normalized_absolute_curvature(svert, self.curvature) 746 a = svert.attribute.color 747 b = self.evaluate(t) 748 svert.attribute.color = self.blend_ramp(a, b) 749 750 751class Curvature3DAlphaShader(CurveMappingModifier): 752 """Alpha based on the 3D curvature of the underlying geometry""" 753 754 def __init__(self, blend, influence, mapping, invert, curve, curvature_min, curvature_max): 755 CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve) 756 self.curvature = BoundedProperty(curvature_min, curvature_max) 757 758 def shade(self, stroke): 759 for svert in stroke: 760 t = normalized_absolute_curvature(svert, self.curvature) 761 a = svert.attribute.alpha 762 b = self.evaluate(t) 763 svert.attribute.alpha = self.blend(a, b) 764 765 766class Curvature3DThicknessShader(ThicknessBlenderMixIn, CurveMappingModifier): 767 """Alpha based on the 3D curvature of the underlying geometry""" 768 769 def __init__(self, thickness_position, thickness_ratio, blend, influence, mapping, invert, curve, 770 curvature_min, curvature_max, thickness_min, thickness_max): 771 ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio) 772 CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve) 773 self.curvature = BoundedProperty(curvature_min, curvature_max) 774 self.thickness = BoundedProperty(thickness_min, thickness_max) 775 776 def shade(self, stroke): 777 for svert in stroke: 778 t = normalized_absolute_curvature(svert, self.curvature) 779 thickness = self.thickness.min + self.evaluate(t) * self.thickness.delta 780 self.blend_thickness(svert, thickness) 781 782 783# Geometry modifiers 784 785class SimplificationShader(StrokeShader): 786 """Simplifies a stroke by merging points together""" 787 788 def __init__(self, tolerance): 789 StrokeShader.__init__(self) 790 self.tolerance = tolerance 791 792 def shade(self, stroke): 793 points = tuple(svert.point for svert in stroke) 794 points_simplified = simplify(points, tolerance=self.tolerance) 795 796 it = iter(stroke) 797 for svert, point in zip(it, points_simplified): 798 svert.point = point 799 800 for svert in tuple(it): 801 stroke.remove_vertex(svert) 802 803 804class SinusDisplacementShader(StrokeShader): 805 """Displaces the stroke in a sine wave-like shape.""" 806 807 def __init__(self, wavelength, amplitude, phase): 808 StrokeShader.__init__(self) 809 self.wavelength = wavelength 810 self.amplitude = amplitude 811 self.phase = phase / wavelength * 2 * pi 812 813 def shade(self, stroke): 814 # normals are stored in a tuple, so they don't update when we reposition vertices. 815 normals = tuple(stroke_normal(stroke)) 816 distances = iter_distance_along_stroke(stroke) 817 coeff = 1 / self.wavelength * 2 * pi 818 for svert, distance, normal in zip(stroke, distances, normals): 819 n = normal * self.amplitude * cos(distance * coeff + self.phase) 820 svert.point += n 821 stroke.update_length() 822 823 824class PerlinNoise1DShader(StrokeShader): 825 """ 826 Displaces the stroke using the curvilinear abscissa. This means 827 that lines with the same length and sampling interval will be 828 identically distorded. 829 """ 830 831 def __init__(self, freq=10, amp=10, oct=4, angle=radians(45), seed=-1): 832 StrokeShader.__init__(self) 833 self.noise = Noise(seed) 834 self.freq = freq 835 self.amp = amp 836 self.oct = oct 837 self.dir = Vector((cos(angle), sin(angle))) 838 839 def shade(self, stroke): 840 length = stroke.length_2d 841 for svert in stroke: 842 nres = self.noise.turbulence1(length * svert.u, self.freq, self.amp, self.oct) 843 svert.point += nres * self.dir 844 stroke.update_length() 845 846 847class PerlinNoise2DShader(StrokeShader): 848 """ 849 Displaces the stroke using the strokes coordinates. This means 850 that in a scene no strokes will be distorted identically. 851 852 More information on the noise shaders can be found at: 853 freestyleintegration.wordpress.com/2011/09/25/development-updates-on-september-25/ 854 """ 855 856 def __init__(self, freq=10, amp=10, oct=4, angle=radians(45), seed=-1): 857 StrokeShader.__init__(self) 858 self.noise = Noise(seed) 859 self.freq = freq 860 self.amp = amp 861 self.oct = oct 862 self.dir = Vector((cos(angle), sin(angle))) 863 864 def shade(self, stroke): 865 for svert in stroke: 866 projected = Vector((svert.projected_x, svert.projected_y)) 867 nres = self.noise.turbulence2(projected, self.freq, self.amp, self.oct) 868 svert.point += nres * self.dir 869 stroke.update_length() 870 871 872class Offset2DShader(StrokeShader): 873 """Offsets the stroke by a given amount.""" 874 875 def __init__(self, start, end, x, y): 876 StrokeShader.__init__(self) 877 self.start = start 878 self.end = end 879 self.xy = Vector((x, y)) 880 881 def shade(self, stroke): 882 # normals are stored in a tuple, so they don't update when we reposition vertices. 883 normals = tuple(stroke_normal(stroke)) 884 for svert, normal in zip(stroke, normals): 885 a = self.start + svert.u * (self.end - self.start) 886 svert.point += (normal * a) + self.xy 887 stroke.update_length() 888 889 890class Transform2DShader(StrokeShader): 891 """Transforms the stroke (scale, rotation, location) around a given pivot point """ 892 893 def __init__(self, pivot, scale_x, scale_y, angle, pivot_u, pivot_x, pivot_y): 894 StrokeShader.__init__(self) 895 self.pivot = pivot 896 self.scale = Vector((scale_x, scale_y)) 897 self.cos_theta = cos(angle) 898 self.sin_theta = sin(angle) 899 self.pivot_u = pivot_u 900 self.pivot_x = pivot_x 901 self.pivot_y = pivot_y 902 if pivot not in {'START', 'END', 'CENTER', 'ABSOLUTE', 'PARAM'}: 903 raise ValueError("expected pivot in {'START', 'END', 'CENTER', 'ABSOLUTE', 'PARAM'}, not" + pivot) 904 905 def shade(self, stroke): 906 # determine the pivot of scaling and rotation operations 907 if self.pivot == 'START': 908 pivot = stroke[0].point 909 elif self.pivot == 'END': 910 pivot = stroke[-1].point 911 elif self.pivot == 'CENTER': 912 # minor rounding errors here, because 913 # given v = Vector(a, b), then (v / n) != Vector(v.x / n, v.y / n) 914 pivot = (1 / len(stroke)) * sum((svert.point for svert in stroke), Vector((0.0, 0.0))) 915 elif self.pivot == 'ABSOLUTE': 916 pivot = Vector((self.pivot_x, self.pivot_y)) 917 elif self.pivot == 'PARAM': 918 if self.pivot_u < stroke[0].u: 919 pivot = stroke[0].point 920 else: 921 for prev, svert in pairwise(stroke): 922 if self.pivot_u < svert.u: 923 break 924 pivot = svert.point + (svert.u - self.pivot_u) * (prev.point - svert.point) 925 926 # apply scaling and rotation operations 927 for svert in stroke: 928 p = (svert.point - pivot) 929 x = p.x * self.scale.x 930 y = p.y * self.scale.y 931 p.x = x * self.cos_theta - y * self.sin_theta 932 p.y = x * self.sin_theta + y * self.cos_theta 933 svert.point = p + pivot 934 stroke.update_length() 935 936 937# Predicates and helper functions 938 939class QuantitativeInvisibilityRangeUP1D(UnaryPredicate1D): 940 def __init__(self, qi_start, qi_end): 941 UnaryPredicate1D.__init__(self) 942 self.getQI = QuantitativeInvisibilityF1D() 943 self.qi_start = qi_start 944 self.qi_end = qi_end 945 946 def __call__(self, inter): 947 qi = self.getQI(inter) 948 return self.qi_start <= qi <= self.qi_end 949 950 951def getQualifiedObjectName(ob): 952 if ob.library is not None: 953 return ob.library.filepath + '/' + ob.name 954 return ob.name 955 956 957class ObjectNamesUP1D(UnaryPredicate1D): 958 def __init__(self, names, negative): 959 UnaryPredicate1D.__init__(self) 960 self.names = names 961 self.negative = negative 962 963 def getViewShapeName(self, vs): 964 if vs.library_path is not None and len(vs.library_path): 965 return vs.library_path + '/' + vs.name 966 return vs.name 967 968 def __call__(self, viewEdge): 969 found = self.getViewShapeName(viewEdge.viewshape) in self.names 970 if self.negative: 971 return not found 972 return found 973 974 975# -- Split by dashed line pattern -- # 976 977class SplitPatternStartingUP0D(UnaryPredicate0D): 978 def __init__(self, controller): 979 UnaryPredicate0D.__init__(self) 980 self.controller = controller 981 982 def __call__(self, inter): 983 return self.controller.start() 984 985 986class SplitPatternStoppingUP0D(UnaryPredicate0D): 987 def __init__(self, controller): 988 UnaryPredicate0D.__init__(self) 989 self.controller = controller 990 991 def __call__(self, inter): 992 return self.controller.stop() 993 994 995class SplitPatternController: 996 def __init__(self, pattern, sampling): 997 self.sampling = float(sampling) 998 k = len(pattern) // 2 999 n = k * 2 1000 self.start_pos = [pattern[i] + pattern[i + 1] for i in range(0, n, 2)] 1001 self.stop_pos = [pattern[i] for i in range(0, n, 2)] 1002 self.init() 1003 1004 def init(self): 1005 self.start_len = 0.0 1006 self.start_idx = 0 1007 self.stop_len = self.sampling 1008 self.stop_idx = 0 1009 1010 def start(self): 1011 self.start_len += self.sampling 1012 if abs(self.start_len - self.start_pos[self.start_idx]) < self.sampling / 2.0: 1013 self.start_len = 0.0 1014 self.start_idx = (self.start_idx + 1) % len(self.start_pos) 1015 return True 1016 return False 1017 1018 def stop(self): 1019 if self.start_len > 0.0: 1020 self.init() 1021 self.stop_len += self.sampling 1022 if abs(self.stop_len - self.stop_pos[self.stop_idx]) < self.sampling / 2.0: 1023 self.stop_len = self.sampling 1024 self.stop_idx = (self.stop_idx + 1) % len(self.stop_pos) 1025 return True 1026 return False 1027 1028 1029# Dashed line 1030 1031class DashedLineShader(StrokeShader): 1032 def __init__(self, pattern): 1033 StrokeShader.__init__(self) 1034 self.pattern = pattern 1035 1036 def shade(self, stroke): 1037 start = 0.0 # 2D curvilinear length 1038 visible = True 1039 # The extra 'sampling' term is added below, because the 1040 # visibility attribute of the i-th vertex refers to the 1041 # visibility of the stroke segment between the i-th and 1042 # (i+1)-th vertices. 1043 sampling = 1.0 1044 it = stroke.stroke_vertices_begin(sampling) 1045 pattern_cycle = cycle(self.pattern) 1046 pattern = next(pattern_cycle) 1047 for svert in it: 1048 pos = it.t # curvilinear abscissa 1049 1050 if pos - start + sampling > pattern: 1051 start = pos 1052 pattern = next(pattern_cycle) 1053 visible = not visible 1054 1055 if not visible: 1056 it.object.attribute.visible = False 1057 1058 1059# predicates for chaining 1060 1061class AngleLargerThanBP1D(BinaryPredicate1D): 1062 def __init__(self, angle): 1063 BinaryPredicate1D.__init__(self) 1064 self.angle = angle 1065 1066 def __call__(self, i1, i2): 1067 sv1a = i1.first_fedge.first_svertex.point_2d 1068 sv1b = i1.last_fedge.second_svertex.point_2d 1069 sv2a = i2.first_fedge.first_svertex.point_2d 1070 sv2b = i2.last_fedge.second_svertex.point_2d 1071 if (sv1a - sv2a).length < 1e-6: 1072 dir1 = sv1a - sv1b 1073 dir2 = sv2b - sv2a 1074 elif (sv1b - sv2b).length < 1e-6: 1075 dir1 = sv1b - sv1a 1076 dir2 = sv2a - sv2b 1077 elif (sv1a - sv2b).length < 1e-6: 1078 dir1 = sv1a - sv1b 1079 dir2 = sv2a - sv2b 1080 elif (sv1b - sv2a).length < 1e-6: 1081 dir1 = sv1b - sv1a 1082 dir2 = sv2b - sv2a 1083 else: 1084 return False 1085 denom = dir1.length * dir2.length 1086 if denom < 1e-6: 1087 return False 1088 x = (dir1 * dir2) / denom 1089 return acos(bound(-1.0, x, 1.0)) > self.angle 1090 1091 1092# predicates for selection 1093 1094class LengthThresholdUP1D(UnaryPredicate1D): 1095 def __init__(self, length_min=None, length_max=None): 1096 UnaryPredicate1D.__init__(self) 1097 self.length_min = length_min 1098 self.length_max = length_max 1099 1100 def __call__(self, inter): 1101 length = inter.length_2d 1102 if self.length_min is not None and length < self.length_min: 1103 return False 1104 if self.length_max is not None and length > self.length_max: 1105 return False 1106 return True 1107 1108 1109class FaceMarkBothUP1D(UnaryPredicate1D): 1110 def __call__(self, inter: ViewEdge): 1111 fe = inter.first_fedge 1112 while fe is not None: 1113 if fe.is_smooth: 1114 if fe.face_mark: 1115 return True 1116 elif (fe.nature & Nature.BORDER): 1117 if fe.face_mark_left: 1118 return True 1119 else: 1120 if fe.face_mark_right and fe.face_mark_left: 1121 return True 1122 fe = fe.next_fedge 1123 return False 1124 1125 1126class FaceMarkOneUP1D(UnaryPredicate1D): 1127 def __call__(self, inter: ViewEdge): 1128 fe = inter.first_fedge 1129 while fe is not None: 1130 if fe.is_smooth: 1131 if fe.face_mark: 1132 return True 1133 elif (fe.nature & Nature.BORDER): 1134 if fe.face_mark_left: 1135 return True 1136 else: 1137 if fe.face_mark_right or fe.face_mark_left: 1138 return True 1139 fe = fe.next_fedge 1140 return False 1141 1142 1143# predicates for splitting 1144 1145class MaterialBoundaryUP0D(UnaryPredicate0D): 1146 def __call__(self, it): 1147 # can't use only it.is_end here, see commit rBeb8964fb7f19 1148 if it.is_begin or it.at_last or it.is_end: 1149 return False 1150 it.decrement() 1151 prev, v, succ = next(it), next(it), next(it) 1152 fe = v.get_fedge(prev) 1153 idx1 = fe.material_index if fe.is_smooth else fe.material_index_left 1154 fe = v.get_fedge(succ) 1155 idx2 = fe.material_index if fe.is_smooth else fe.material_index_left 1156 return idx1 != idx2 1157 1158 1159class Curvature2DAngleThresholdUP0D(UnaryPredicate0D): 1160 def __init__(self, angle_min=None, angle_max=None): 1161 UnaryPredicate0D.__init__(self) 1162 self.angle_min = angle_min 1163 self.angle_max = angle_max 1164 self.func = Curvature2DAngleF0D() 1165 1166 def __call__(self, inter): 1167 angle = pi - self.func(inter) 1168 if self.angle_min is not None and angle < self.angle_min: 1169 return True 1170 if self.angle_max is not None and angle > self.angle_max: 1171 return True 1172 return False 1173 1174 1175class Length2DThresholdUP0D(UnaryPredicate0D): 1176 def __init__(self, length_limit): 1177 UnaryPredicate0D.__init__(self) 1178 self.length_limit = length_limit 1179 self.t = 0.0 1180 1181 def __call__(self, inter): 1182 t = inter.t # curvilinear abscissa 1183 if t < self.t: 1184 self.t = 0.0 1185 return False 1186 if t - self.t < self.length_limit: 1187 return False 1188 self.t = t 1189 return True 1190 1191 1192# Seed for random number generation 1193 1194class Seed: 1195 def __init__(self): 1196 self.t_max = 2 ** 15 1197 self.t = int(time.time()) % self.t_max 1198 1199 def get(self, seed): 1200 if seed < 0: 1201 self.t = (self.t + 1) % self.t_max 1202 return self.t 1203 return seed 1204 1205 1206_seed = Seed() 1207 1208 1209def get_dashed_pattern(linestyle): 1210 """Extracts the dashed pattern from the various UI options """ 1211 pattern = [] 1212 if linestyle.dash1 > 0 and linestyle.gap1 > 0: 1213 pattern.append(linestyle.dash1) 1214 pattern.append(linestyle.gap1) 1215 if linestyle.dash2 > 0 and linestyle.gap2 > 0: 1216 pattern.append(linestyle.dash2) 1217 pattern.append(linestyle.gap2) 1218 if linestyle.dash3 > 0 and linestyle.gap3 > 0: 1219 pattern.append(linestyle.dash3) 1220 pattern.append(linestyle.gap3) 1221 return pattern 1222 1223 1224def get_grouped_objects(group): 1225 for ob in group.objects: 1226 if ob.instance_type == 'COLLECTION' and ob.instance_collection is not None: 1227 for dupli in get_grouped_objects(ob.instance_collection): 1228 yield dupli 1229 else: 1230 yield ob 1231 1232 1233integration_types = { 1234 'MEAN': IntegrationType.MEAN, 1235 'MIN': IntegrationType.MIN, 1236 'MAX': IntegrationType.MAX, 1237 'FIRST': IntegrationType.FIRST, 1238 'LAST': IntegrationType.LAST} 1239 1240 1241# main function for parameter processing 1242def process(layer_name, lineset_name): 1243 scene = getCurrentScene() 1244 layer = scene.view_layers[layer_name] 1245 lineset = layer.freestyle_settings.linesets[lineset_name] 1246 linestyle = lineset.linestyle 1247 1248 # execute line set pre-processing callback functions 1249 for fn in callbacks_lineset_pre: 1250 fn(scene, layer, lineset) 1251 1252 selection_criteria = [] 1253 # prepare selection criteria by visibility 1254 if lineset.select_by_visibility: 1255 if lineset.visibility == 'VISIBLE': 1256 selection_criteria.append( 1257 QuantitativeInvisibilityUP1D(0)) 1258 elif lineset.visibility == 'HIDDEN': 1259 selection_criteria.append( 1260 NotUP1D(QuantitativeInvisibilityUP1D(0))) 1261 elif lineset.visibility == 'RANGE': 1262 selection_criteria.append( 1263 QuantitativeInvisibilityRangeUP1D(lineset.qi_start, lineset.qi_end)) 1264 # prepare selection criteria by edge types 1265 if lineset.select_by_edge_types: 1266 edge_type_criteria = [] 1267 if lineset.select_silhouette: 1268 upred = pyNatureUP1D(Nature.SILHOUETTE) 1269 edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_silhouette else upred) 1270 if lineset.select_border: 1271 upred = pyNatureUP1D(Nature.BORDER) 1272 edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_border else upred) 1273 if lineset.select_crease: 1274 upred = pyNatureUP1D(Nature.CREASE) 1275 edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_crease else upred) 1276 if lineset.select_ridge_valley: 1277 upred = pyNatureUP1D(Nature.RIDGE) 1278 edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_ridge_valley else upred) 1279 if lineset.select_suggestive_contour: 1280 upred = pyNatureUP1D(Nature.SUGGESTIVE_CONTOUR) 1281 edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_suggestive_contour else upred) 1282 if lineset.select_material_boundary: 1283 upred = pyNatureUP1D(Nature.MATERIAL_BOUNDARY) 1284 edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_material_boundary else upred) 1285 if lineset.select_edge_mark: 1286 upred = pyNatureUP1D(Nature.EDGE_MARK) 1287 edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_edge_mark else upred) 1288 if lineset.select_contour: 1289 upred = ContourUP1D() 1290 edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_contour else upred) 1291 if lineset.select_external_contour: 1292 upred = ExternalContourUP1D() 1293 edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_external_contour else upred) 1294 if edge_type_criteria: 1295 if lineset.edge_type_combination == 'OR': 1296 upred = OrUP1D(*edge_type_criteria) 1297 else: 1298 upred = AndUP1D(*edge_type_criteria) 1299 if lineset.edge_type_negation == 'EXCLUSIVE': 1300 upred = NotUP1D(upred) 1301 selection_criteria.append(upred) 1302 # prepare selection criteria by face marks 1303 if lineset.select_by_face_marks: 1304 if lineset.face_mark_condition == 'BOTH': 1305 upred = FaceMarkBothUP1D() 1306 else: 1307 upred = FaceMarkOneUP1D() 1308 1309 if lineset.face_mark_negation == 'EXCLUSIVE': 1310 upred = NotUP1D(upred) 1311 selection_criteria.append(upred) 1312 # prepare selection criteria by group of objects 1313 if lineset.select_by_collection: 1314 if lineset.collection is not None: 1315 names = {getQualifiedObjectName(ob): True for ob in get_grouped_objects(lineset.collection)} 1316 upred = ObjectNamesUP1D(names, lineset.collection_negation == 'EXCLUSIVE') 1317 selection_criteria.append(upred) 1318 # prepare selection criteria by image border 1319 if lineset.select_by_image_border: 1320 upred = WithinImageBoundaryUP1D(*ContextFunctions.get_border()) 1321 selection_criteria.append(upred) 1322 # select feature edges 1323 if selection_criteria: 1324 upred = AndUP1D(*selection_criteria) 1325 else: 1326 upred = TrueUP1D() 1327 Operators.select(upred) 1328 # join feature edges to form chains 1329 if linestyle.use_chaining: 1330 if linestyle.chaining == 'PLAIN': 1331 if linestyle.use_same_object: 1332 Operators.bidirectional_chain(ChainSilhouetteIterator(), NotUP1D(upred)) 1333 else: 1334 Operators.bidirectional_chain(ChainPredicateIterator(upred, TrueBP1D()), NotUP1D(upred)) 1335 elif linestyle.chaining == 'SKETCHY': 1336 if linestyle.use_same_object: 1337 Operators.bidirectional_chain(pySketchyChainSilhouetteIterator(linestyle.rounds)) 1338 else: 1339 Operators.bidirectional_chain(pySketchyChainingIterator(linestyle.rounds)) 1340 else: 1341 Operators.chain(ChainPredicateIterator(FalseUP1D(), FalseBP1D()), NotUP1D(upred)) 1342 # split chains 1343 if linestyle.material_boundary: 1344 Operators.sequential_split(MaterialBoundaryUP0D()) 1345 if linestyle.use_angle_min or linestyle.use_angle_max: 1346 angle_min = linestyle.angle_min if linestyle.use_angle_min else None 1347 angle_max = linestyle.angle_max if linestyle.use_angle_max else None 1348 Operators.sequential_split(Curvature2DAngleThresholdUP0D(angle_min, angle_max)) 1349 if linestyle.use_split_length: 1350 Operators.sequential_split(Length2DThresholdUP0D(linestyle.split_length), 1.0) 1351 if linestyle.use_split_pattern: 1352 pattern = [] 1353 if linestyle.split_dash1 > 0 and linestyle.split_gap1 > 0: 1354 pattern.append(linestyle.split_dash1) 1355 pattern.append(linestyle.split_gap1) 1356 if linestyle.split_dash2 > 0 and linestyle.split_gap2 > 0: 1357 pattern.append(linestyle.split_dash2) 1358 pattern.append(linestyle.split_gap2) 1359 if linestyle.split_dash3 > 0 and linestyle.split_gap3 > 0: 1360 pattern.append(linestyle.split_dash3) 1361 pattern.append(linestyle.split_gap3) 1362 if len(pattern) > 0: 1363 sampling = 1.0 1364 controller = SplitPatternController(pattern, sampling) 1365 Operators.sequential_split(SplitPatternStartingUP0D(controller), 1366 SplitPatternStoppingUP0D(controller), 1367 sampling) 1368 # sort selected chains 1369 if linestyle.use_sorting: 1370 integration = integration_types.get(linestyle.integration_type, IntegrationType.MEAN) 1371 if linestyle.sort_key == 'DISTANCE_FROM_CAMERA': 1372 bpred = pyZBP1D(integration) 1373 elif linestyle.sort_key == '2D_LENGTH': 1374 bpred = Length2DBP1D() 1375 elif linestyle.sort_key == 'PROJECTED_X': 1376 bpred = pyProjectedXBP1D(integration) 1377 elif linestyle.sort_key == 'PROJECTED_Y': 1378 bpred = pyProjectedYBP1D(integration) 1379 if linestyle.sort_order == 'REVERSE': 1380 bpred = NotBP1D(bpred) 1381 Operators.sort(bpred) 1382 # select chains 1383 if linestyle.use_length_min or linestyle.use_length_max: 1384 length_min = linestyle.length_min if linestyle.use_length_min else None 1385 length_max = linestyle.length_max if linestyle.use_length_max else None 1386 Operators.select(LengthThresholdUP1D(length_min, length_max)) 1387 if linestyle.use_chain_count: 1388 Operators.select(pyNFirstUP1D(linestyle.chain_count)) 1389 # prepare a list of stroke shaders 1390 shaders_list = [] 1391 for m in linestyle.geometry_modifiers: 1392 if not m.use: 1393 continue 1394 if m.type == 'SAMPLING': 1395 shaders_list.append(SamplingShader( 1396 m.sampling)) 1397 elif m.type == 'BEZIER_CURVE': 1398 shaders_list.append(BezierCurveShader( 1399 m.error)) 1400 elif m.type == 'SIMPLIFICATION': 1401 shaders_list.append(SimplificationShader(m.tolerance)) 1402 elif m.type == 'SINUS_DISPLACEMENT': 1403 shaders_list.append(SinusDisplacementShader( 1404 m.wavelength, m.amplitude, m.phase)) 1405 elif m.type == 'SPATIAL_NOISE': 1406 shaders_list.append(SpatialNoiseShader( 1407 m.amplitude, m.scale, m.octaves, m.smooth, m.use_pure_random)) 1408 elif m.type == 'PERLIN_NOISE_1D': 1409 shaders_list.append(PerlinNoise1DShader( 1410 m.frequency, m.amplitude, m.octaves, m.angle, _seed.get(m.seed))) 1411 elif m.type == 'PERLIN_NOISE_2D': 1412 shaders_list.append(PerlinNoise2DShader( 1413 m.frequency, m.amplitude, m.octaves, m.angle, _seed.get(m.seed))) 1414 elif m.type == 'BACKBONE_STRETCHER': 1415 shaders_list.append(BackboneStretcherShader( 1416 m.backbone_length)) 1417 elif m.type == 'TIP_REMOVER': 1418 shaders_list.append(TipRemoverShader( 1419 m.tip_length)) 1420 elif m.type == 'POLYGONIZATION': 1421 shaders_list.append(PolygonalizationShader( 1422 m.error)) 1423 elif m.type == 'GUIDING_LINES': 1424 shaders_list.append(GuidingLinesShader( 1425 m.offset)) 1426 elif m.type == 'BLUEPRINT': 1427 if m.shape == 'CIRCLES': 1428 shaders_list.append(pyBluePrintCirclesShader( 1429 m.rounds, m.random_radius, m.random_center)) 1430 elif m.shape == 'ELLIPSES': 1431 shaders_list.append(pyBluePrintEllipsesShader( 1432 m.rounds, m.random_radius, m.random_center)) 1433 elif m.shape == 'SQUARES': 1434 shaders_list.append(pyBluePrintSquaresShader( 1435 m.rounds, m.backbone_length, m.random_backbone)) 1436 elif m.type == '2D_OFFSET': 1437 shaders_list.append(Offset2DShader( 1438 m.start, m.end, m.x, m.y)) 1439 elif m.type == '2D_TRANSFORM': 1440 shaders_list.append(Transform2DShader( 1441 m.pivot, m.scale_x, m.scale_y, m.angle, m.pivot_u, m.pivot_x, m.pivot_y)) 1442 # -- Base color, alpha and thickness -- # 1443 if (not linestyle.use_chaining) or (linestyle.chaining == 'PLAIN' and linestyle.use_same_object): 1444 thickness_position = linestyle.thickness_position 1445 else: 1446 thickness_position = 'CENTER' 1447 import bpy 1448 if bpy.app.debug_freestyle: 1449 print("Warning: Thickness position options are applied when chaining is disabled\n" 1450 " or the Plain chaining is used with the Same Object option enabled.") 1451 shaders_list.append(ConstantColorShader(*(linestyle.color), alpha=linestyle.alpha)) 1452 shaders_list.append(BaseThicknessShader(linestyle.thickness, thickness_position, 1453 linestyle.thickness_ratio)) 1454 # -- Modifiers -- # 1455 for m in linestyle.color_modifiers: 1456 if not m.use: 1457 continue 1458 if m.type == 'ALONG_STROKE': 1459 shaders_list.append(ColorAlongStrokeShader( 1460 m.blend, m.influence, m.color_ramp)) 1461 elif m.type == 'DISTANCE_FROM_CAMERA': 1462 shaders_list.append(ColorDistanceFromCameraShader( 1463 m.blend, m.influence, m.color_ramp, 1464 m.range_min, m.range_max)) 1465 elif m.type == 'DISTANCE_FROM_OBJECT': 1466 if m.target is not None: 1467 shaders_list.append(ColorDistanceFromObjectShader( 1468 m.blend, m.influence, m.color_ramp, m.target, 1469 m.range_min, m.range_max)) 1470 elif m.type == 'MATERIAL': 1471 shaders_list.append(ColorMaterialShader( 1472 m.blend, m.influence, m.color_ramp, m.material_attribute, 1473 m.use_ramp)) 1474 elif m.type == 'TANGENT': 1475 shaders_list.append(TangentColorShader( 1476 m.blend, m.influence, m.color_ramp)) 1477 elif m.type == 'CREASE_ANGLE': 1478 shaders_list.append(CreaseAngleColorShader( 1479 m.blend, m.influence, m.color_ramp, 1480 m.angle_min, m.angle_max)) 1481 elif m.type == 'CURVATURE_3D': 1482 shaders_list.append(Curvature3DColorShader( 1483 m.blend, m.influence, m.color_ramp, 1484 m.curvature_min, m.curvature_max)) 1485 elif m.type == 'NOISE': 1486 shaders_list.append(ColorNoiseShader( 1487 m.blend, m.influence, m.color_ramp, 1488 m.amplitude, m.period, m.seed)) 1489 for m in linestyle.alpha_modifiers: 1490 if not m.use: 1491 continue 1492 if m.type == 'ALONG_STROKE': 1493 shaders_list.append(AlphaAlongStrokeShader( 1494 m.blend, m.influence, m.mapping, m.invert, m.curve)) 1495 elif m.type == 'DISTANCE_FROM_CAMERA': 1496 shaders_list.append(AlphaDistanceFromCameraShader( 1497 m.blend, m.influence, m.mapping, m.invert, m.curve, 1498 m.range_min, m.range_max)) 1499 elif m.type == 'DISTANCE_FROM_OBJECT': 1500 if m.target is not None: 1501 shaders_list.append(AlphaDistanceFromObjectShader( 1502 m.blend, m.influence, m.mapping, m.invert, m.curve, m.target, 1503 m.range_min, m.range_max)) 1504 elif m.type == 'MATERIAL': 1505 shaders_list.append(AlphaMaterialShader( 1506 m.blend, m.influence, m.mapping, m.invert, m.curve, 1507 m.material_attribute)) 1508 elif m.type == 'TANGENT': 1509 shaders_list.append(TangentAlphaShader( 1510 m.blend, m.influence, m.mapping, m.invert, m.curve,)) 1511 elif m.type == 'CREASE_ANGLE': 1512 shaders_list.append(CreaseAngleAlphaShader( 1513 m.blend, m.influence, m.mapping, m.invert, m.curve, 1514 m.angle_min, m.angle_max)) 1515 elif m.type == 'CURVATURE_3D': 1516 shaders_list.append(Curvature3DAlphaShader( 1517 m.blend, m.influence, m.mapping, m.invert, m.curve, 1518 m.curvature_min, m.curvature_max)) 1519 elif m.type == 'NOISE': 1520 shaders_list.append(AlphaNoiseShader( 1521 m.blend, m.influence, m.mapping, m.invert, m.curve, 1522 m.amplitude, m.period, m.seed)) 1523 for m in linestyle.thickness_modifiers: 1524 if not m.use: 1525 continue 1526 if m.type == 'ALONG_STROKE': 1527 shaders_list.append(ThicknessAlongStrokeShader( 1528 thickness_position, linestyle.thickness_ratio, 1529 m.blend, m.influence, m.mapping, m.invert, m.curve, 1530 m.value_min, m.value_max)) 1531 elif m.type == 'DISTANCE_FROM_CAMERA': 1532 shaders_list.append(ThicknessDistanceFromCameraShader( 1533 thickness_position, linestyle.thickness_ratio, 1534 m.blend, m.influence, m.mapping, m.invert, m.curve, 1535 m.range_min, m.range_max, m.value_min, m.value_max)) 1536 elif m.type == 'DISTANCE_FROM_OBJECT': 1537 if m.target is not None: 1538 shaders_list.append(ThicknessDistanceFromObjectShader( 1539 thickness_position, linestyle.thickness_ratio, 1540 m.blend, m.influence, m.mapping, m.invert, m.curve, m.target, 1541 m.range_min, m.range_max, m.value_min, m.value_max)) 1542 elif m.type == 'MATERIAL': 1543 shaders_list.append(ThicknessMaterialShader( 1544 thickness_position, linestyle.thickness_ratio, 1545 m.blend, m.influence, m.mapping, m.invert, m.curve, 1546 m.material_attribute, m.value_min, m.value_max)) 1547 elif m.type == 'CALLIGRAPHY': 1548 shaders_list.append(CalligraphicThicknessShader( 1549 thickness_position, linestyle.thickness_ratio, 1550 m.blend, m.influence, 1551 m.orientation, m.thickness_min, m.thickness_max)) 1552 elif m.type == 'TANGENT': 1553 shaders_list.append(TangentThicknessShader( 1554 thickness_position, linestyle.thickness_ratio, 1555 m.blend, m.influence, m.mapping, m.invert, m.curve, 1556 m.thickness_min, m.thickness_max)) 1557 elif m.type == 'NOISE': 1558 shaders_list.append(ThicknessNoiseShader( 1559 thickness_position, linestyle.thickness_ratio, 1560 m.blend, m.influence, 1561 m.amplitude, m.period, m.seed, m.use_asymmetric)) 1562 elif m.type == 'CREASE_ANGLE': 1563 shaders_list.append(CreaseAngleThicknessShader( 1564 thickness_position, linestyle.thickness_ratio, 1565 m.blend, m.influence, m.mapping, m.invert, m.curve, 1566 m.angle_min, m.angle_max, m.thickness_min, m.thickness_max)) 1567 elif m.type == 'CURVATURE_3D': 1568 shaders_list.append(Curvature3DThicknessShader( 1569 thickness_position, linestyle.thickness_ratio, 1570 m.blend, m.influence, m.mapping, m.invert, m.curve, 1571 m.curvature_min, m.curvature_max, m.thickness_min, m.thickness_max)) 1572 else: 1573 raise RuntimeError("No Thickness modifier with type", type(m), m) 1574 # -- Textures -- # 1575 has_tex = False 1576 if linestyle.use_nodes and linestyle.node_tree: 1577 shaders_list.append(BlenderTextureShader(linestyle.node_tree)) 1578 has_tex = True 1579 if has_tex: 1580 shaders_list.append(StrokeTextureStepShader(linestyle.texture_spacing)) 1581 1582 # execute post-base stylization callbacks 1583 for fn in callbacks_modifiers_post: 1584 shaders_list.extend(fn(scene, layer, lineset)) 1585 1586 # -- Stroke caps -- # 1587 if linestyle.caps == 'ROUND': 1588 shaders_list.append(RoundCapShader()) 1589 elif linestyle.caps == 'SQUARE': 1590 shaders_list.append(SquareCapShader()) 1591 1592 # -- Dashed line -- # 1593 if linestyle.use_dashed_line: 1594 pattern = get_dashed_pattern(linestyle) 1595 if len(pattern) > 0: 1596 shaders_list.append(DashedLineShader(pattern)) 1597 1598 # create strokes using the shaders list 1599 Operators.create(TrueUP1D(), shaders_list) 1600 1601 # execute line set post-processing callback functions 1602 for fn in callbacks_lineset_post: 1603 fn(scene, layer, lineset) 1604