parameter_editor.py 61 KB

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