123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569 |
- # ##### BEGIN GPL LICENSE BLOCK #####
- #
- # This program is free software; you can redistribute it and/or
- # modify it under the terms of the GNU General Public License
- # as published by the Free Software Foundation; either version 2
- # of the License, or (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program; if not, write to the Free Software Foundation,
- # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- #
- # ##### END GPL LICENSE BLOCK #####
- # Filename : parameter_editor.py
- # Authors : Tamito Kajiyama
- # Date : 26/07/2010
- # Purpose : Interactive manipulation of stylization parameters
- from freestyle.types import (
- BinaryPredicate1D,
- IntegrationType,
- Interface0DIterator,
- Nature,
- Noise,
- Operators,
- StrokeAttribute,
- UnaryPredicate0D,
- UnaryPredicate1D,
- TVertex,
- Material,
- ViewEdge,
- )
- from freestyle.chainingiterators import (
- ChainPredicateIterator,
- ChainSilhouetteIterator,
- pySketchyChainSilhouetteIterator,
- pySketchyChainingIterator,
- )
- from freestyle.functions import (
- Curvature2DAngleF0D,
- Normal2DF0D,
- QuantitativeInvisibilityF1D,
- VertexOrientation2DF0D,
- CurveMaterialF0D,
- )
- from freestyle.predicates import (
- AndUP1D,
- ContourUP1D,
- ExternalContourUP1D,
- FalseBP1D,
- FalseUP1D,
- Length2DBP1D,
- NotBP1D,
- NotUP1D,
- OrUP1D,
- QuantitativeInvisibilityUP1D,
- TrueBP1D,
- TrueUP1D,
- WithinImageBoundaryUP1D,
- pyNFirstUP1D,
- pyNatureUP1D,
- pyProjectedXBP1D,
- pyProjectedYBP1D,
- pyZBP1D,
- )
- from freestyle.shaders import (
- BackboneStretcherShader,
- BezierCurveShader,
- BlenderTextureShader,
- ConstantColorShader,
- GuidingLinesShader,
- PolygonalizationShader,
- pyBluePrintCirclesShader,
- pyBluePrintEllipsesShader,
- pyBluePrintSquaresShader,
- RoundCapShader,
- SamplingShader,
- SpatialNoiseShader,
- SquareCapShader,
- StrokeShader,
- StrokeTextureStepShader,
- ThicknessNoiseShader as thickness_noise,
- TipRemoverShader,
- )
- from freestyle.utils import (
- angle_x_normal,
- bound,
- BoundedProperty,
- ContextFunctions,
- curvature_from_stroke_vertex,
- getCurrentScene,
- iter_distance_along_stroke,
- iter_distance_from_camera,
- iter_distance_from_object,
- iter_material_value,
- iter_t2d_along_stroke,
- normal_at_I0D,
- pairwise,
- simplify,
- stroke_normal,
- )
- from _freestyle import (
- blendRamp,
- evaluateColorRamp,
- evaluateCurveMappingF,
- )
- import time
- import bpy
- import random
- from mathutils import Vector
- from math import pi, sin, cos, acos, radians, atan2
- from itertools import cycle, tee
- # WARNING: highly experimental, not a stable API
- # lists of callback functions
- # used by the render_freestyle_svg addon
- callbacks_lineset_pre = []
- callbacks_modifiers_post = []
- callbacks_lineset_post = []
- class ColorRampModifier(StrokeShader):
- """Primitive for the color modifiers."""
- def __init__(self, blend, influence, ramp):
- StrokeShader.__init__(self)
- self.blend = blend
- self.influence = influence
- self.ramp = ramp
- def evaluate(self, t):
- col = evaluateColorRamp(self.ramp, t)
- return col.xyz # omit alpha
- def blend_ramp(self, a, b):
- return blendRamp(self.blend, a, self.influence, b)
- class ScalarBlendModifier(StrokeShader):
- """Primitive for alpha and thickness modifiers."""
- def __init__(self, blend_type, influence):
- StrokeShader.__init__(self)
- self.blend_type = blend_type
- self.influence = influence
- def blend(self, v1, v2):
- fac = self.influence
- facm = 1.0 - fac
- if self.blend_type == 'MIX':
- v1 = facm * v1 + fac * v2
- elif self.blend_type == 'ADD':
- v1 += fac * v2
- elif self.blend_type == 'MULTIPLY':
- v1 *= facm + fac * v2
- elif self.blend_type == 'SUBTRACT':
- v1 -= fac * v2
- elif self.blend_type == 'DIVIDE':
- v1 = facm * v1 + fac * v1 / v2 if v2 != 0.0 else v1
- elif self.blend_type == 'DIFFERENCE':
- v1 = facm * v1 + fac * abs(v1 - v2)
- elif self.blend_type == 'MININUM':
- v1 = min(fac * v2, v1)
- elif self.blend_type == 'MAXIMUM':
- v1 = max(fac * v2, v1)
- else:
- raise ValueError("unknown curve blend type: " + self.blend_type)
- return v1
- class CurveMappingModifier(ScalarBlendModifier):
- def __init__(self, blend, influence, mapping, invert, curve):
- ScalarBlendModifier.__init__(self, blend, influence)
- assert mapping in {'LINEAR', 'CURVE'}
- self.evaluate = getattr(self, mapping)
- self.invert = invert
- self.curve = curve
- def LINEAR(self, t):
- return (1.0 - t) if self.invert else t
- def CURVE(self, t):
- # deprecated: return evaluateCurveMappingF(self.curve, 0, t)
- curve = self.curve
- curve.initialize()
- result = curve.curves[0].evaluate(t)
- # float precision errors in t can give a very weird result for evaluate.
- # therefore, bound the result by the curve's min and max values
- return bound(curve.clip_min_y, result, curve.clip_max_y)
- class ThicknessModifierMixIn:
- def __init__(self):
- scene = getCurrentScene()
- self.persp_camera = (scene.camera.data.type == 'PERSP')
- def set_thickness(self, sv, outer, inner):
- fe = sv.fedge
- nature = fe.nature
- if (nature & Nature.BORDER):
- if self.persp_camera:
- point = -sv.point_3d.normalized()
- dir = point.dot(fe.normal_left)
- else:
- dir = fe.normal_left.z
- if dir < 0.0: # the back side is visible
- outer, inner = inner, outer
- elif (nature & Nature.SILHOUETTE):
- if fe.is_smooth: # TODO more tests needed
- outer, inner = inner, outer
- else:
- outer = inner = (outer + inner) / 2
- sv.attribute.thickness = (outer, inner)
- class ThicknessBlenderMixIn(ThicknessModifierMixIn):
- def __init__(self, position, ratio):
- ThicknessModifierMixIn.__init__(self)
- self.position = position
- self.ratio = ratio
- def blend_thickness(self, svert, thickness, asymmetric=False):
- """Blends and sets the thickness with respect to the position, blend mode and symmetry."""
- if asymmetric:
- right, left = thickness
- self.blend_thickness_asymmetric(svert, right, left)
- else:
- if type(thickness) not in {int, float}:
- thickness = sum(thickness)
- self.blend_thickness_symmetric(svert, thickness)
- def blend_thickness_symmetric(self, svert, v):
- """Blends and sets the thickness. Thickness is equal on each side of the backbone"""
- outer, inner = svert.attribute.thickness
- v = self.blend(outer + inner, v)
- # Part 1: blend
- if self.position == 'CENTER':
- outer = inner = v * 0.5
- elif self.position == 'INSIDE':
- outer, inner = 0, v
- elif self.position == 'OUTSIDE':
- outer, inner = v, 0
- elif self.position == 'RELATIVE':
- outer, inner = v * self.ratio, v - (v * self.ratio)
- else:
- raise ValueError("unknown thickness position: " + position)
- self.set_thickness(svert, outer, inner)
- def blend_thickness_asymmetric(self, svert, right, left):
- """Blends and sets the thickness. Thickness may be unequal on each side of the backbone"""
- # blend the thickness values for both sides. This way, the blend mode is supported.
- old = svert.attribute.thickness
- new = (right, left)
- right, left = (self.blend(*val) for val in zip(old, new))
- fe = svert.fedge
- nature = fe.nature
- if (nature & Nature.BORDER):
- if self.persp_camera:
- point = -svert.point_3d.normalized()
- dir = point.dot(fe.normal_left)
- else:
- dir = fe.normal_left.z
- if dir < 0.0: # the back side is visible
- right, left = left, right
- elif (nature & Nature.SILHOUETTE):
- if fe.is_smooth: # TODO more tests needed
- right, left = left, right
- svert.attribute.thickness = (right, left)
- class BaseThicknessShader(StrokeShader, ThicknessModifierMixIn):
- def __init__(self, thickness, position, ratio):
- StrokeShader.__init__(self)
- ThicknessModifierMixIn.__init__(self)
- if position == 'CENTER':
- self.outer = thickness * 0.5
- self.inner = thickness - self.outer
- elif position == 'INSIDE':
- self.outer = 0
- self.inner = thickness
- elif position == 'OUTSIDE':
- self.outer = thickness
- self.inner = 0
- elif position == 'RELATIVE':
- self.outer = thickness * ratio
- self.inner = thickness - self.outer
- else:
- raise ValueError("unknown thickness position: " + position)
- def shade(self, stroke):
- for svert in stroke:
- self.set_thickness(svert, self.outer, self.inner)
- # Along Stroke modifiers
- class ColorAlongStrokeShader(ColorRampModifier):
- """Maps a ramp to the color of the stroke, using the curvilinear abscissa (t)."""
- def shade(self, stroke):
- for svert, t in zip(stroke, iter_t2d_along_stroke(stroke)):
- a = svert.attribute.color
- b = self.evaluate(t)
- svert.attribute.color = self.blend_ramp(a, b)
- class AlphaAlongStrokeShader(CurveMappingModifier):
- """Maps a curve to the alpha/transparency of the stroke, using the curvilinear abscissa (t)."""
- def shade(self, stroke):
- for svert, t in zip(stroke, iter_t2d_along_stroke(stroke)):
- a = svert.attribute.alpha
- b = self.evaluate(t)
- svert.attribute.alpha = self.blend(a, b)
- class ThicknessAlongStrokeShader(ThicknessBlenderMixIn, CurveMappingModifier):
- """Maps a curve to the thickness of the stroke, using the curvilinear abscissa (t)."""
- def __init__(self, thickness_position, thickness_ratio,
- blend, influence, mapping, invert, curve, value_min, value_max):
- ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio)
- CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
- self.value = BoundedProperty(value_min, value_max)
- def shade(self, stroke):
- for svert, t in zip(stroke, iter_t2d_along_stroke(stroke)):
- b = self.value.min + self.evaluate(t) * self.value.delta
- self.blend_thickness(svert, b)
- # -- Distance from Camera modifiers -- #
- class ColorDistanceFromCameraShader(ColorRampModifier):
- """Picks a color value from a ramp based on the vertex' distance from the camera."""
- def __init__(self, blend, influence, ramp, range_min, range_max):
- ColorRampModifier.__init__(self, blend, influence, ramp)
- self.range = BoundedProperty(range_min, range_max)
- def shade(self, stroke):
- it = iter_distance_from_camera(stroke, *self.range)
- for svert, t in it:
- a = svert.attribute.color
- b = self.evaluate(t)
- svert.attribute.color = self.blend_ramp(a, b)
- class AlphaDistanceFromCameraShader(CurveMappingModifier):
- """Picks an alpha value from a curve based on the vertex' distance from the camera"""
- def __init__(self, blend, influence, mapping, invert, curve, range_min, range_max):
- CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
- self.range = BoundedProperty(range_min, range_max)
- def shade(self, stroke):
- it = iter_distance_from_camera(stroke, *self.range)
- for svert, t in it:
- a = svert.attribute.alpha
- b = self.evaluate(t)
- svert.attribute.alpha = self.blend(a, b)
- class ThicknessDistanceFromCameraShader(ThicknessBlenderMixIn, CurveMappingModifier):
- """Picks a thickness value from a curve based on the vertex' distance from the camera."""
- def __init__(self, thickness_position, thickness_ratio,
- blend, influence, mapping, invert, curve, range_min, range_max, value_min, value_max):
- ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio)
- CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
- self.range = BoundedProperty(range_min, range_max)
- self.value = BoundedProperty(value_min, value_max)
- def shade(self, stroke):
- for (svert, t) in iter_distance_from_camera(stroke, *self.range):
- b = self.value.min + self.evaluate(t) * self.value.delta
- self.blend_thickness(svert, b)
- # Distance from Object modifiers
- class ColorDistanceFromObjectShader(ColorRampModifier):
- """Picks a color value from a ramp based on the vertex' distance from a given object."""
- def __init__(self, blend, influence, ramp, target, range_min, range_max):
- ColorRampModifier.__init__(self, blend, influence, ramp)
- if target is None:
- raise ValueError("ColorDistanceFromObjectShader: target can't be None ")
- self.range = BoundedProperty(range_min, range_max)
- # construct a model-view matrix
- matrix = getCurrentScene().camera.matrix_world.inverted()
- # get the object location in the camera coordinate
- self.loc = matrix @ target.location
- def shade(self, stroke):
- it = iter_distance_from_object(stroke, self.loc, *self.range)
- for svert, t in it:
- a = svert.attribute.color
- b = self.evaluate(t)
- svert.attribute.color = self.blend_ramp(a, b)
- class AlphaDistanceFromObjectShader(CurveMappingModifier):
- """Picks an alpha value from a curve based on the vertex' distance from a given object."""
- def __init__(self, blend, influence, mapping, invert, curve, target, range_min, range_max):
- CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
- if target is None:
- raise ValueError("AlphaDistanceFromObjectShader: target can't be None ")
- self.range = BoundedProperty(range_min, range_max)
- # construct a model-view matrix
- matrix = getCurrentScene().camera.matrix_world.inverted()
- # get the object location in the camera coordinate
- self.loc = matrix @ target.location
- def shade(self, stroke):
- it = iter_distance_from_object(stroke, self.loc, *self.range)
- for svert, t in it:
- a = svert.attribute.alpha
- b = self.evaluate(t)
- svert.attribute.alpha = self.blend(a, b)
- class ThicknessDistanceFromObjectShader(ThicknessBlenderMixIn, CurveMappingModifier):
- """Picks a thickness value from a curve based on the vertex' distance from a given object."""
- def __init__(self, thickness_position, thickness_ratio,
- blend, influence, mapping, invert, curve, target, range_min, range_max, value_min, value_max):
- ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio)
- CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
- if target is None:
- raise ValueError("ThicknessDistanceFromObjectShader: target can't be None ")
- self.range = BoundedProperty(range_min, range_max)
- self.value = BoundedProperty(value_min, value_max)
- # construct a model-view matrix
- matrix = getCurrentScene().camera.matrix_world.inverted()
- # get the object location in the camera coordinate
- self.loc = matrix @ target.location
- def shade(self, stroke):
- it = iter_distance_from_object(stroke, self.loc, *self.range)
- for svert, t in it:
- b = self.value.min + self.evaluate(t) * self.value.delta
- self.blend_thickness(svert, b)
- # Material modifiers
- class ColorMaterialShader(ColorRampModifier):
- """Assigns a color to the vertices based on their underlying material."""
- def __init__(self, blend, influence, ramp, material_attribute, use_ramp):
- ColorRampModifier.__init__(self, blend, influence, ramp)
- self.attribute = material_attribute
- self.use_ramp = use_ramp
- self.func = CurveMaterialF0D()
- def shade(self, stroke, attributes={'DIFF', 'SPEC', 'LINE'}):
- it = Interface0DIterator(stroke)
- if not self.use_ramp and self.attribute in attributes:
- for svert in it:
- material = self.func(it)
- if self.attribute == 'LINE':
- b = material.line[0:3]
- elif self.attribute == 'DIFF':
- b = material.diffuse[0:3]
- else:
- b = material.specular[0:3]
- a = svert.attribute.color
- svert.attribute.color = self.blend_ramp(a, b)
- else:
- for svert, value in iter_material_value(stroke, self.func, self.attribute):
- a = svert.attribute.color
- b = self.evaluate(value)
- svert.attribute.color = self.blend_ramp(a, b)
- class AlphaMaterialShader(CurveMappingModifier):
- """Assigns an alpha value to the vertices based on their underlying material."""
- def __init__(self, blend, influence, mapping, invert, curve, material_attribute):
- CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
- self.attribute = material_attribute
- self.func = CurveMaterialF0D()
- def shade(self, stroke):
- for svert, value in iter_material_value(stroke, self.func, self.attribute):
- a = svert.attribute.alpha
- b = self.evaluate(value)
- svert.attribute.alpha = self.blend(a, b)
- class ThicknessMaterialShader(ThicknessBlenderMixIn, CurveMappingModifier):
- """Assigns a thickness value to the vertices based on their underlying material."""
- def __init__(self, thickness_position, thickness_ratio,
- blend, influence, mapping, invert, curve, material_attribute, value_min, value_max):
- ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio)
- CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
- self.attribute = material_attribute
- self.value = BoundedProperty(value_min, value_max)
- self.func = CurveMaterialF0D()
- def shade(self, stroke):
- for svert, value in iter_material_value(stroke, self.func, self.attribute):
- b = self.value.min + self.evaluate(value) * self.value.delta
- self.blend_thickness(svert, b)
- # Calligraphic thickness modifier
- class CalligraphicThicknessShader(ThicknessBlenderMixIn, ScalarBlendModifier):
- """Thickness modifier for achieving a calligraphy-like effect."""
- def __init__(self, thickness_position, thickness_ratio,
- blend_type, influence, orientation, thickness_min, thickness_max):
- ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio)
- ScalarBlendModifier.__init__(self, blend_type, influence)
- self.orientation = Vector((cos(orientation), sin(orientation)))
- self.thickness = BoundedProperty(thickness_min, thickness_max)
- self.func = VertexOrientation2DF0D()
- def shade(self, stroke):
- it = Interface0DIterator(stroke)
- for svert in it:
- dir = self.func(it)
- if dir.length != 0.0:
- dir.normalize()
- fac = abs(dir.orthogonal() @ self.orientation)
- b = self.thickness.min + fac * self.thickness.delta
- else:
- b = self.thickness.min
- self.blend_thickness(svert, b)
- # - Tangent Modifiers - #
- class TangentColorShader(ColorRampModifier):
- """Color based on the direction of the stroke"""
- def shade(self, stroke):
- it = Interface0DIterator(stroke)
- for svert in it:
- angle = angle_x_normal(it)
- fac = self.evaluate(angle / pi)
- a = svert.attribute.color
- svert.attribute.color = self.blend_ramp(a, fac)
- class TangentAlphaShader(CurveMappingModifier):
- """Alpha transparency based on the direction of the stroke"""
- def shade(self, stroke):
- it = Interface0DIterator(stroke)
- for svert in it:
- angle = angle_x_normal(it)
- fac = self.evaluate(angle / pi)
- a = svert.attribute.alpha
- svert.attribute.alpha = self.blend(a, fac)
- class TangentThicknessShader(ThicknessBlenderMixIn, CurveMappingModifier):
- """Thickness based on the direction of the stroke"""
- def __init__(self, thickness_position, thickness_ratio, blend, influence, mapping, invert, curve,
- thickness_min, thickness_max):
- ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio)
- CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
- self.thickness = BoundedProperty(thickness_min, thickness_max)
- def shade(self, stroke):
- it = Interface0DIterator(stroke)
- for svert in it:
- angle = angle_x_normal(it)
- thickness = self.thickness.min + self.evaluate(angle / pi) * self.thickness.delta
- self.blend_thickness(svert, thickness)
- # - Noise Modifiers - #
- class NoiseShader:
- """Base class for noise shaders"""
- def __init__(self, amplitude, period, seed=512):
- self.amplitude = amplitude
- self.scale = 1 / period / seed
- self.seed = seed
- def noisegen(self, stroke, n1=Noise(), n2=Noise()):
- """Produces two noise values per StrokeVertex for every vertex in the stroke"""
- initU1 = stroke.length_2d * self.seed + n1.rand(512) * self.seed
- initU2 = stroke.length_2d * self.seed + n2.rand() * self.seed
- for svert in stroke:
- a = n1.turbulence_smooth(self.scale * svert.curvilinear_abscissa + initU1, 2)
- b = n2.turbulence_smooth(self.scale * svert.curvilinear_abscissa + initU2, 2)
- yield (svert, a, b)
- class ThicknessNoiseShader(ThicknessBlenderMixIn, ScalarBlendModifier, NoiseShader):
- """Thickness based on pseudo-noise"""
- def __init__(self, thickness_position, thickness_ratio, blend_type, influence, amplitude, period, seed=512, asymmetric=True):
- ScalarBlendModifier.__init__(self, blend_type, influence)
- ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio)
- NoiseShader.__init__(self, amplitude, period, seed)
- self.asymmetric = asymmetric
- def shade(self, stroke):
- for svert, noiseval1, noiseval2 in self.noisegen(stroke):
- (r, l) = svert.attribute.thickness
- l += noiseval1 * self.amplitude
- r += noiseval2 * self.amplitude
- self.blend_thickness(svert, (r, l), self.asymmetric)
- class ColorNoiseShader(ColorRampModifier, NoiseShader):
- """Color based on pseudo-noise"""
- def __init__(self, blend, influence, ramp, amplitude, period, seed=512):
- ColorRampModifier.__init__(self, blend, influence, ramp)
- NoiseShader.__init__(self, amplitude, period, seed)
- def shade(self, stroke):
- for svert, noiseval1, noiseval2 in self.noisegen(stroke):
- position = abs(noiseval1 + noiseval2)
- svert.attribute.color = self.blend_ramp(svert.attribute.color, self.evaluate(position))
- class AlphaNoiseShader(CurveMappingModifier, NoiseShader):
- """Alpha transparency on based pseudo-noise"""
- def __init__(self, blend, influence, mapping, invert, curve, amplitude, period, seed=512):
- CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
- NoiseShader.__init__(self, amplitude, period, seed)
- def shade(self, stroke, n1=Noise(), n2=Noise()):
- for svert, noiseval1, noiseval2 in self.noisegen(stroke):
- position = abs(noiseval1 + noiseval2)
- svert.attribute.alpha = self.blend(svert.attribute.alpha, self.evaluate(position))
- # - Crease Angle Modifiers - #
- def crease_angle(svert):
- """Returns the crease angle between the StrokeVertex' two adjacent faces (in radians)"""
- fe = svert.fedge
- if not fe or fe.is_smooth or not (fe.nature & Nature.CREASE):
- return None
- # make sure that the input is within the domain of the acos function
- product = bound(-1.0, -fe.normal_left.dot(fe.normal_right), 1.0)
- return acos(product)
- class CreaseAngleColorShader(ColorRampModifier):
- """Color based on the crease angle between two adjacent faces on the underlying geometry"""
- def __init__(self, blend, influence, ramp, angle_min, angle_max):
- ColorRampModifier.__init__(self, blend, influence, ramp)
- # angles are (already) in radians
- self.angle = BoundedProperty(angle_min, angle_max)
- def shade(self, stroke):
- for svert in stroke:
- angle = crease_angle(svert)
- if angle is None:
- continue
- t = self.angle.interpolate(angle)
- svert.attribute.color = self.blend_ramp(svert.attribute.color, self.evaluate(t))
- class CreaseAngleAlphaShader(CurveMappingModifier):
- """Alpha transparency based on the crease angle between two adjacent faces on the underlying geometry"""
- def __init__(self, blend, influence, mapping, invert, curve, angle_min, angle_max):
- CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
- # angles are (already) in radians
- self.angle = BoundedProperty(angle_min, angle_max)
- def shade(self, stroke):
- for svert in stroke:
- angle = crease_angle(svert)
- if angle is None:
- continue
- t = self.angle.interpolate(angle)
- svert.attribute.alpha = self.blend(svert.attribute.alpha, self.evaluate(t))
- class CreaseAngleThicknessShader(ThicknessBlenderMixIn, CurveMappingModifier):
- """Thickness based on the crease angle between two adjacent faces on the underlying geometry"""
- def __init__(self, thickness_position, thickness_ratio, blend, influence, mapping, invert, curve,
- angle_min, angle_max, thickness_min, thickness_max):
- ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio)
- CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
- # angles are (already) in radians
- self.angle = BoundedProperty(angle_min, angle_max)
- self.thickness = BoundedProperty(thickness_min, thickness_max)
- def shade(self, stroke):
- for svert in stroke:
- angle = crease_angle(svert)
- if angle is None:
- continue
- t = self.angle.interpolate(angle)
- thickness = self.thickness.min + self.evaluate(t) * self.thickness.delta
- self.blend_thickness(svert, thickness)
- # - Curvature3D Modifiers - #
- def normalized_absolute_curvature(svert, bounded_curvature):
- """
- Gives the absolute curvature in range [0, 1].
- The actual curvature (Kr) value can be anywhere in the range [-inf, inf], where convex curvature
- yields a positive value, and concave a negative one. These shaders only look for the magnitude
- of the 3D curvature, hence the abs()
- """
- curvature = curvature_from_stroke_vertex(svert)
- if curvature is None:
- return 0.0
- return bounded_curvature.interpolate(abs(curvature))
- class Curvature3DColorShader(ColorRampModifier):
- """Color based on the 3D curvature of the underlying geometry"""
- def __init__(self, blend, influence, ramp, curvature_min, curvature_max):
- ColorRampModifier.__init__(self, blend, influence, ramp)
- self.curvature = BoundedProperty(curvature_min, curvature_max)
- def shade(self, stroke):
- for svert in stroke:
- t = normalized_absolute_curvature(svert, self.curvature)
- a = svert.attribute.color
- b = self.evaluate(t)
- svert.attribute.color = self.blend_ramp(a, b)
- class Curvature3DAlphaShader(CurveMappingModifier):
- """Alpha based on the 3D curvature of the underlying geometry"""
- def __init__(self, blend, influence, mapping, invert, curve, curvature_min, curvature_max):
- CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
- self.curvature = BoundedProperty(curvature_min, curvature_max)
- def shade(self, stroke):
- for svert in stroke:
- t = normalized_absolute_curvature(svert, self.curvature)
- a = svert.attribute.alpha
- b = self.evaluate(t)
- svert.attribute.alpha = self.blend(a, b)
- class Curvature3DThicknessShader(ThicknessBlenderMixIn, CurveMappingModifier):
- """Alpha based on the 3D curvature of the underlying geometry"""
- def __init__(self, thickness_position, thickness_ratio, blend, influence, mapping, invert, curve,
- curvature_min, curvature_max, thickness_min, thickness_max):
- ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio)
- CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve)
- self.curvature = BoundedProperty(curvature_min, curvature_max)
- self.thickness = BoundedProperty(thickness_min, thickness_max)
- def shade(self, stroke):
- for svert in stroke:
- t = normalized_absolute_curvature(svert, self.curvature)
- thickness = self.thickness.min + self.evaluate(t) * self.thickness.delta
- self.blend_thickness(svert, thickness)
- # Geometry modifiers
- class SimplificationShader(StrokeShader):
- """Simplifies a stroke by merging points together"""
- def __init__(self, tolerance):
- StrokeShader.__init__(self)
- self.tolerance = tolerance
- def shade(self, stroke):
- points = tuple(svert.point for svert in stroke)
- points_simplified = simplify(points, tolerance=self.tolerance)
- it = iter(stroke)
- for svert, point in zip(it, points_simplified):
- svert.point = point
- for svert in tuple(it):
- stroke.remove_vertex(svert)
- class SinusDisplacementShader(StrokeShader):
- """Displaces the stroke in a sine wave-like shape."""
- def __init__(self, wavelength, amplitude, phase):
- StrokeShader.__init__(self)
- self.wavelength = wavelength
- self.amplitude = amplitude
- self.phase = phase / wavelength * 2 * pi
- def shade(self, stroke):
- # normals are stored in a tuple, so they don't update when we reposition vertices.
- normals = tuple(stroke_normal(stroke))
- distances = iter_distance_along_stroke(stroke)
- coeff = 1 / self.wavelength * 2 * pi
- for svert, distance, normal in zip(stroke, distances, normals):
- n = normal * self.amplitude * cos(distance * coeff + self.phase)
- svert.point += n
- stroke.update_length()
- class PerlinNoise1DShader(StrokeShader):
- """
- Displaces the stroke using the curvilinear abscissa. This means
- that lines with the same length and sampling interval will be
- identically distorded.
- """
- def __init__(self, freq=10, amp=10, oct=4, angle=radians(45), seed=-1):
- StrokeShader.__init__(self)
- self.noise = Noise(seed)
- self.freq = freq
- self.amp = amp
- self.oct = oct
- self.dir = Vector((cos(angle), sin(angle)))
- def shade(self, stroke):
- length = stroke.length_2d
- for svert in stroke:
- nres = self.noise.turbulence1(length * svert.u, self.freq, self.amp, self.oct)
- svert.point += nres * self.dir
- stroke.update_length()
- class PerlinNoise2DShader(StrokeShader):
- """
- Displaces the stroke using the strokes coordinates. This means
- that in a scene no strokes will be distorted identically.
- More information on the noise shaders can be found at:
- freestyleintegration.wordpress.com/2011/09/25/development-updates-on-september-25/
- """
- def __init__(self, freq=10, amp=10, oct=4, angle=radians(45), seed=-1):
- StrokeShader.__init__(self)
- self.noise = Noise(seed)
- self.freq = freq
- self.amp = amp
- self.oct = oct
- self.dir = Vector((cos(angle), sin(angle)))
- def shade(self, stroke):
- for svert in stroke:
- projected = Vector((svert.projected_x, svert.projected_y))
- nres = self.noise.turbulence2(projected, self.freq, self.amp, self.oct)
- svert.point += nres * self.dir
- stroke.update_length()
- class Offset2DShader(StrokeShader):
- """Offsets the stroke by a given amount."""
- def __init__(self, start, end, x, y):
- StrokeShader.__init__(self)
- self.start = start
- self.end = end
- self.xy = Vector((x, y))
- def shade(self, stroke):
- # normals are stored in a tuple, so they don't update when we reposition vertices.
- normals = tuple(stroke_normal(stroke))
- for svert, normal in zip(stroke, normals):
- a = self.start + svert.u * (self.end - self.start)
- svert.point += (normal * a) + self.xy
- stroke.update_length()
- class Transform2DShader(StrokeShader):
- """Transforms the stroke (scale, rotation, location) around a given pivot point """
- def __init__(self, pivot, scale_x, scale_y, angle, pivot_u, pivot_x, pivot_y):
- StrokeShader.__init__(self)
- self.pivot = pivot
- self.scale = Vector((scale_x, scale_y))
- self.cos_theta = cos(angle)
- self.sin_theta = sin(angle)
- self.pivot_u = pivot_u
- self.pivot_x = pivot_x
- self.pivot_y = pivot_y
- if pivot not in {'START', 'END', 'CENTER', 'ABSOLUTE', 'PARAM'}:
- raise ValueError("expected pivot in {'START', 'END', 'CENTER', 'ABSOLUTE', 'PARAM'}, not" + pivot)
- def shade(self, stroke):
- # determine the pivot of scaling and rotation operations
- if self.pivot == 'START':
- pivot = stroke[0].point
- elif self.pivot == 'END':
- pivot = stroke[-1].point
- elif self.pivot == 'CENTER':
- # minor rounding errors here, because
- # given v = Vector(a, b), then (v / n) != Vector(v.x / n, v.y / n)
- pivot = (1 / len(stroke)) * sum((svert.point for svert in stroke), Vector((0.0, 0.0)))
- elif self.pivot == 'ABSOLUTE':
- pivot = Vector((self.pivot_x, self.pivot_y))
- elif self.pivot == 'PARAM':
- if self.pivot_u < stroke[0].u:
- pivot = stroke[0].point
- else:
- for prev, svert in pairwise(stroke):
- if self.pivot_u < svert.u:
- break
- pivot = svert.point + (svert.u - self.pivot_u) * (prev.point - svert.point)
- # apply scaling and rotation operations
- for svert in stroke:
- p = (svert.point - pivot)
- x = p.x * self.scale.x
- y = p.y * self.scale.y
- p.x = x * self.cos_theta - y * self.sin_theta
- p.y = x * self.sin_theta + y * self.cos_theta
- svert.point = p + pivot
- stroke.update_length()
- # Predicates and helper functions
- class QuantitativeInvisibilityRangeUP1D(UnaryPredicate1D):
- def __init__(self, qi_start, qi_end):
- UnaryPredicate1D.__init__(self)
- self.getQI = QuantitativeInvisibilityF1D()
- self.qi_start = qi_start
- self.qi_end = qi_end
- def __call__(self, inter):
- qi = self.getQI(inter)
- return self.qi_start <= qi <= self.qi_end
- def getQualifiedObjectName(ob):
- if ob.library is not None:
- return ob.library.filepath + '/' + ob.name
- return ob.name
- class ObjectNamesUP1D(UnaryPredicate1D):
- def __init__(self, names, negative):
- UnaryPredicate1D.__init__(self)
- self.names = names
- self.negative = negative
- def getViewShapeName(self, vs):
- if vs.library_path is not None and len(vs.library_path):
- return vs.library_path + '/' + vs.name
- return vs.name
- def __call__(self, viewEdge):
- found = self.getViewShapeName(viewEdge.viewshape) in self.names
- if self.negative:
- return not found
- return found
- # -- Split by dashed line pattern -- #
- class SplitPatternStartingUP0D(UnaryPredicate0D):
- def __init__(self, controller):
- UnaryPredicate0D.__init__(self)
- self.controller = controller
- def __call__(self, inter):
- return self.controller.start()
- class SplitPatternStoppingUP0D(UnaryPredicate0D):
- def __init__(self, controller):
- UnaryPredicate0D.__init__(self)
- self.controller = controller
- def __call__(self, inter):
- return self.controller.stop()
- class SplitPatternController:
- def __init__(self, pattern, sampling):
- self.sampling = float(sampling)
- k = len(pattern) // 2
- n = k * 2
- self.start_pos = [pattern[i] + pattern[i + 1] for i in range(0, n, 2)]
- self.stop_pos = [pattern[i] for i in range(0, n, 2)]
- self.init()
- def init(self):
- self.start_len = 0.0
- self.start_idx = 0
- self.stop_len = self.sampling
- self.stop_idx = 0
- def start(self):
- self.start_len += self.sampling
- if abs(self.start_len - self.start_pos[self.start_idx]) < self.sampling / 2.0:
- self.start_len = 0.0
- self.start_idx = (self.start_idx + 1) % len(self.start_pos)
- return True
- return False
- def stop(self):
- if self.start_len > 0.0:
- self.init()
- self.stop_len += self.sampling
- if abs(self.stop_len - self.stop_pos[self.stop_idx]) < self.sampling / 2.0:
- self.stop_len = self.sampling
- self.stop_idx = (self.stop_idx + 1) % len(self.stop_pos)
- return True
- return False
- # Dashed line
- class DashedLineShader(StrokeShader):
- def __init__(self, pattern):
- StrokeShader.__init__(self)
- self.pattern = pattern
- def shade(self, stroke):
- start = 0.0 # 2D curvilinear length
- visible = True
- # The extra 'sampling' term is added below, because the
- # visibility attribute of the i-th vertex refers to the
- # visibility of the stroke segment between the i-th and
- # (i+1)-th vertices.
- sampling = 1.0
- it = stroke.stroke_vertices_begin(sampling)
- pattern_cycle = cycle(self.pattern)
- pattern = next(pattern_cycle)
- for svert in it:
- pos = it.t # curvilinear abscissa
- if pos - start + sampling > pattern:
- start = pos
- pattern = next(pattern_cycle)
- visible = not visible
- if not visible:
- it.object.attribute.visible = False
- # predicates for chaining
- class AngleLargerThanBP1D(BinaryPredicate1D):
- def __init__(self, angle):
- BinaryPredicate1D.__init__(self)
- self.angle = angle
- def __call__(self, i1, i2):
- sv1a = i1.first_fedge.first_svertex.point_2d
- sv1b = i1.last_fedge.second_svertex.point_2d
- sv2a = i2.first_fedge.first_svertex.point_2d
- sv2b = i2.last_fedge.second_svertex.point_2d
- if (sv1a - sv2a).length < 1e-6:
- dir1 = sv1a - sv1b
- dir2 = sv2b - sv2a
- elif (sv1b - sv2b).length < 1e-6:
- dir1 = sv1b - sv1a
- dir2 = sv2a - sv2b
- elif (sv1a - sv2b).length < 1e-6:
- dir1 = sv1a - sv1b
- dir2 = sv2a - sv2b
- elif (sv1b - sv2a).length < 1e-6:
- dir1 = sv1b - sv1a
- dir2 = sv2b - sv2a
- else:
- return False
- denom = dir1.length * dir2.length
- if denom < 1e-6:
- return False
- x = (dir1 * dir2) / denom
- return acos(bound(-1.0, x, 1.0)) > self.angle
- # predicates for selection
- class LengthThresholdUP1D(UnaryPredicate1D):
- def __init__(self, length_min=None, length_max=None):
- UnaryPredicate1D.__init__(self)
- self.length_min = length_min
- self.length_max = length_max
- def __call__(self, inter):
- length = inter.length_2d
- if self.length_min is not None and length < self.length_min:
- return False
- if self.length_max is not None and length > self.length_max:
- return False
- return True
- class FaceMarkBothUP1D(UnaryPredicate1D):
- def __call__(self, inter: ViewEdge):
- fe = inter.first_fedge
- while fe is not None:
- if fe.is_smooth:
- if fe.face_mark:
- return True
- elif (fe.nature & Nature.BORDER):
- if fe.face_mark_left:
- return True
- else:
- if fe.face_mark_right and fe.face_mark_left:
- return True
- fe = fe.next_fedge
- return False
- class FaceMarkOneUP1D(UnaryPredicate1D):
- def __call__(self, inter: ViewEdge):
- fe = inter.first_fedge
- while fe is not None:
- if fe.is_smooth:
- if fe.face_mark:
- return True
- elif (fe.nature & Nature.BORDER):
- if fe.face_mark_left:
- return True
- else:
- if fe.face_mark_right or fe.face_mark_left:
- return True
- fe = fe.next_fedge
- return False
- # predicates for splitting
- class MaterialBoundaryUP0D(UnaryPredicate0D):
- def __call__(self, it):
- # can't use only it.is_end here, see commit rBeb8964fb7f19
- if it.is_begin or it.at_last or it.is_end:
- return False
- it.decrement()
- prev, v, succ = next(it), next(it), next(it)
- fe = v.get_fedge(prev)
- idx1 = fe.material_index if fe.is_smooth else fe.material_index_left
- fe = v.get_fedge(succ)
- idx2 = fe.material_index if fe.is_smooth else fe.material_index_left
- return idx1 != idx2
- class Curvature2DAngleThresholdUP0D(UnaryPredicate0D):
- def __init__(self, angle_min=None, angle_max=None):
- UnaryPredicate0D.__init__(self)
- self.angle_min = angle_min
- self.angle_max = angle_max
- self.func = Curvature2DAngleF0D()
- def __call__(self, inter):
- angle = pi - self.func(inter)
- if self.angle_min is not None and angle < self.angle_min:
- return True
- if self.angle_max is not None and angle > self.angle_max:
- return True
- return False
- class Length2DThresholdUP0D(UnaryPredicate0D):
- def __init__(self, length_limit):
- UnaryPredicate0D.__init__(self)
- self.length_limit = length_limit
- self.t = 0.0
- def __call__(self, inter):
- t = inter.t # curvilinear abscissa
- if t < self.t:
- self.t = 0.0
- return False
- if t - self.t < self.length_limit:
- return False
- self.t = t
- return True
- # Seed for random number generation
- class Seed:
- def __init__(self):
- self.t_max = 2 ** 15
- self.t = int(time.time()) % self.t_max
- def get(self, seed):
- if seed < 0:
- self.t = (self.t + 1) % self.t_max
- return self.t
- return seed
- _seed = Seed()
- def get_dashed_pattern(linestyle):
- """Extracts the dashed pattern from the various UI options """
- pattern = []
- if linestyle.dash1 > 0 and linestyle.gap1 > 0:
- pattern.append(linestyle.dash1)
- pattern.append(linestyle.gap1)
- if linestyle.dash2 > 0 and linestyle.gap2 > 0:
- pattern.append(linestyle.dash2)
- pattern.append(linestyle.gap2)
- if linestyle.dash3 > 0 and linestyle.gap3 > 0:
- pattern.append(linestyle.dash3)
- pattern.append(linestyle.gap3)
- return pattern
- def get_grouped_objects(group):
- for ob in group.objects:
- if ob.instance_type == 'COLLECTION' and ob.instance_collection is not None:
- for dupli in get_grouped_objects(ob.instance_collection):
- yield dupli
- else:
- yield ob
- integration_types = {
- 'MEAN': IntegrationType.MEAN,
- 'MIN': IntegrationType.MIN,
- 'MAX': IntegrationType.MAX,
- 'FIRST': IntegrationType.FIRST,
- 'LAST': IntegrationType.LAST}
- # main function for parameter processing
- def process(layer_name, lineset_name):
- scene = getCurrentScene()
- layer = scene.view_layers[layer_name]
- lineset = layer.freestyle_settings.linesets[lineset_name]
- linestyle = lineset.linestyle
- # execute line set pre-processing callback functions
- for fn in callbacks_lineset_pre:
- fn(scene, layer, lineset)
- selection_criteria = []
- # prepare selection criteria by visibility
- if lineset.select_by_visibility:
- if lineset.visibility == 'VISIBLE':
- selection_criteria.append(
- QuantitativeInvisibilityUP1D(0))
- elif lineset.visibility == 'HIDDEN':
- selection_criteria.append(
- NotUP1D(QuantitativeInvisibilityUP1D(0)))
- elif lineset.visibility == 'RANGE':
- selection_criteria.append(
- QuantitativeInvisibilityRangeUP1D(lineset.qi_start, lineset.qi_end))
- # prepare selection criteria by edge types
- if lineset.select_by_edge_types:
- edge_type_criteria = []
- if lineset.select_silhouette:
- upred = pyNatureUP1D(Nature.SILHOUETTE)
- edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_silhouette else upred)
- if lineset.select_border:
- upred = pyNatureUP1D(Nature.BORDER)
- edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_border else upred)
- if lineset.select_crease:
- upred = pyNatureUP1D(Nature.CREASE)
- edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_crease else upred)
- if lineset.select_ridge_valley:
- upred = pyNatureUP1D(Nature.RIDGE)
- edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_ridge_valley else upred)
- if lineset.select_suggestive_contour:
- upred = pyNatureUP1D(Nature.SUGGESTIVE_CONTOUR)
- edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_suggestive_contour else upred)
- if lineset.select_material_boundary:
- upred = pyNatureUP1D(Nature.MATERIAL_BOUNDARY)
- edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_material_boundary else upred)
- if lineset.select_edge_mark:
- upred = pyNatureUP1D(Nature.EDGE_MARK)
- edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_edge_mark else upred)
- if lineset.select_contour:
- upred = ContourUP1D()
- edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_contour else upred)
- if lineset.select_external_contour:
- upred = ExternalContourUP1D()
- edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_external_contour else upred)
- if edge_type_criteria:
- if lineset.edge_type_combination == 'OR':
- upred = OrUP1D(*edge_type_criteria)
- else:
- upred = AndUP1D(*edge_type_criteria)
- if lineset.edge_type_negation == 'EXCLUSIVE':
- upred = NotUP1D(upred)
- selection_criteria.append(upred)
- # prepare selection criteria by face marks
- if lineset.select_by_face_marks:
- if lineset.face_mark_condition == 'BOTH':
- upred = FaceMarkBothUP1D()
- else:
- upred = FaceMarkOneUP1D()
- if lineset.face_mark_negation == 'EXCLUSIVE':
- upred = NotUP1D(upred)
- selection_criteria.append(upred)
- # prepare selection criteria by group of objects
- if lineset.select_by_collection:
- if lineset.collection is not None:
- names = {getQualifiedObjectName(ob): True for ob in get_grouped_objects(lineset.collection)}
- upred = ObjectNamesUP1D(names, lineset.collection_negation == 'EXCLUSIVE')
- selection_criteria.append(upred)
- # prepare selection criteria by image border
- if lineset.select_by_image_border:
- upred = WithinImageBoundaryUP1D(*ContextFunctions.get_border())
- selection_criteria.append(upred)
- # select feature edges
- if selection_criteria:
- upred = AndUP1D(*selection_criteria)
- else:
- upred = TrueUP1D()
- Operators.select(upred)
- # join feature edges to form chains
- if linestyle.use_chaining:
- if linestyle.chaining == 'PLAIN':
- if linestyle.use_same_object:
- Operators.bidirectional_chain(ChainSilhouetteIterator(), NotUP1D(upred))
- else:
- Operators.bidirectional_chain(ChainPredicateIterator(upred, TrueBP1D()), NotUP1D(upred))
- elif linestyle.chaining == 'SKETCHY':
- if linestyle.use_same_object:
- Operators.bidirectional_chain(pySketchyChainSilhouetteIterator(linestyle.rounds))
- else:
- Operators.bidirectional_chain(pySketchyChainingIterator(linestyle.rounds))
- else:
- Operators.chain(ChainPredicateIterator(FalseUP1D(), FalseBP1D()), NotUP1D(upred))
- # split chains
- if linestyle.material_boundary:
- Operators.sequential_split(MaterialBoundaryUP0D())
- if linestyle.use_angle_min or linestyle.use_angle_max:
- angle_min = linestyle.angle_min if linestyle.use_angle_min else None
- angle_max = linestyle.angle_max if linestyle.use_angle_max else None
- Operators.sequential_split(Curvature2DAngleThresholdUP0D(angle_min, angle_max))
- if linestyle.use_split_length:
- Operators.sequential_split(Length2DThresholdUP0D(linestyle.split_length), 1.0)
- if linestyle.use_split_pattern:
- pattern = []
- if linestyle.split_dash1 > 0 and linestyle.split_gap1 > 0:
- pattern.append(linestyle.split_dash1)
- pattern.append(linestyle.split_gap1)
- if linestyle.split_dash2 > 0 and linestyle.split_gap2 > 0:
- pattern.append(linestyle.split_dash2)
- pattern.append(linestyle.split_gap2)
- if linestyle.split_dash3 > 0 and linestyle.split_gap3 > 0:
- pattern.append(linestyle.split_dash3)
- pattern.append(linestyle.split_gap3)
- if len(pattern) > 0:
- sampling = 1.0
- controller = SplitPatternController(pattern, sampling)
- Operators.sequential_split(SplitPatternStartingUP0D(controller),
- SplitPatternStoppingUP0D(controller),
- sampling)
- # sort selected chains
- if linestyle.use_sorting:
- integration = integration_types.get(linestyle.integration_type, IntegrationType.MEAN)
- if linestyle.sort_key == 'DISTANCE_FROM_CAMERA':
- bpred = pyZBP1D(integration)
- elif linestyle.sort_key == '2D_LENGTH':
- bpred = Length2DBP1D()
- elif linestyle.sort_key == 'PROJECTED_X':
- bpred = pyProjectedXBP1D(integration)
- elif linestyle.sort_key == 'PROJECTED_Y':
- bpred = pyProjectedYBP1D(integration)
- if linestyle.sort_order == 'REVERSE':
- bpred = NotBP1D(bpred)
- Operators.sort(bpred)
- # select chains
- if linestyle.use_length_min or linestyle.use_length_max:
- length_min = linestyle.length_min if linestyle.use_length_min else None
- length_max = linestyle.length_max if linestyle.use_length_max else None
- Operators.select(LengthThresholdUP1D(length_min, length_max))
- if linestyle.use_chain_count:
- Operators.select(pyNFirstUP1D(linestyle.chain_count))
- # prepare a list of stroke shaders
- shaders_list = []
- for m in linestyle.geometry_modifiers:
- if not m.use:
- continue
- if m.type == 'SAMPLING':
- shaders_list.append(SamplingShader(
- m.sampling))
- elif m.type == 'BEZIER_CURVE':
- shaders_list.append(BezierCurveShader(
- m.error))
- elif m.type == 'SIMPLIFICATION':
- shaders_list.append(SimplificationShader(m.tolerance))
- elif m.type == 'SINUS_DISPLACEMENT':
- shaders_list.append(SinusDisplacementShader(
- m.wavelength, m.amplitude, m.phase))
- elif m.type == 'SPATIAL_NOISE':
- shaders_list.append(SpatialNoiseShader(
- m.amplitude, m.scale, m.octaves, m.smooth, m.use_pure_random))
- elif m.type == 'PERLIN_NOISE_1D':
- shaders_list.append(PerlinNoise1DShader(
- m.frequency, m.amplitude, m.octaves, m.angle, _seed.get(m.seed)))
- elif m.type == 'PERLIN_NOISE_2D':
- shaders_list.append(PerlinNoise2DShader(
- m.frequency, m.amplitude, m.octaves, m.angle, _seed.get(m.seed)))
- elif m.type == 'BACKBONE_STRETCHER':
- shaders_list.append(BackboneStretcherShader(
- m.backbone_length))
- elif m.type == 'TIP_REMOVER':
- shaders_list.append(TipRemoverShader(
- m.tip_length))
- elif m.type == 'POLYGONIZATION':
- shaders_list.append(PolygonalizationShader(
- m.error))
- elif m.type == 'GUIDING_LINES':
- shaders_list.append(GuidingLinesShader(
- m.offset))
- elif m.type == 'BLUEPRINT':
- if m.shape == 'CIRCLES':
- shaders_list.append(pyBluePrintCirclesShader(
- m.rounds, m.random_radius, m.random_center))
- elif m.shape == 'ELLIPSES':
- shaders_list.append(pyBluePrintEllipsesShader(
- m.rounds, m.random_radius, m.random_center))
- elif m.shape == 'SQUARES':
- shaders_list.append(pyBluePrintSquaresShader(
- m.rounds, m.backbone_length, m.random_backbone))
- elif m.type == '2D_OFFSET':
- shaders_list.append(Offset2DShader(
- m.start, m.end, m.x, m.y))
- elif m.type == '2D_TRANSFORM':
- shaders_list.append(Transform2DShader(
- m.pivot, m.scale_x, m.scale_y, m.angle, m.pivot_u, m.pivot_x, m.pivot_y))
- # -- Base color, alpha and thickness -- #
- if (not linestyle.use_chaining) or (linestyle.chaining == 'PLAIN' and linestyle.use_same_object):
- thickness_position = linestyle.thickness_position
- else:
- thickness_position = 'CENTER'
- import bpy
- if bpy.app.debug_freestyle:
- print("Warning: Thickness position options are applied when chaining is disabled\n"
- " or the Plain chaining is used with the Same Object option enabled.")
- shaders_list.append(ConstantColorShader(*(linestyle.color), alpha=linestyle.alpha))
- shaders_list.append(BaseThicknessShader(linestyle.thickness, thickness_position,
- linestyle.thickness_ratio))
- # -- Modifiers -- #
- for m in linestyle.color_modifiers:
- if not m.use:
- continue
- if m.type == 'ALONG_STROKE':
- shaders_list.append(ColorAlongStrokeShader(
- m.blend, m.influence, m.color_ramp))
- elif m.type == 'DISTANCE_FROM_CAMERA':
- shaders_list.append(ColorDistanceFromCameraShader(
- m.blend, m.influence, m.color_ramp,
- m.range_min, m.range_max))
- elif m.type == 'DISTANCE_FROM_OBJECT':
- if m.target is not None:
- shaders_list.append(ColorDistanceFromObjectShader(
- m.blend, m.influence, m.color_ramp, m.target,
- m.range_min, m.range_max))
- elif m.type == 'MATERIAL':
- shaders_list.append(ColorMaterialShader(
- m.blend, m.influence, m.color_ramp, m.material_attribute,
- m.use_ramp))
- elif m.type == 'TANGENT':
- shaders_list.append(TangentColorShader(
- m.blend, m.influence, m.color_ramp))
- elif m.type == 'CREASE_ANGLE':
- shaders_list.append(CreaseAngleColorShader(
- m.blend, m.influence, m.color_ramp,
- m.angle_min, m.angle_max))
- elif m.type == 'CURVATURE_3D':
- shaders_list.append(Curvature3DColorShader(
- m.blend, m.influence, m.color_ramp,
- m.curvature_min, m.curvature_max))
- elif m.type == 'NOISE':
- shaders_list.append(ColorNoiseShader(
- m.blend, m.influence, m.color_ramp,
- m.amplitude, m.period, m.seed))
- for m in linestyle.alpha_modifiers:
- if not m.use:
- continue
- if m.type == 'ALONG_STROKE':
- shaders_list.append(AlphaAlongStrokeShader(
- m.blend, m.influence, m.mapping, m.invert, m.curve))
- elif m.type == 'DISTANCE_FROM_CAMERA':
- shaders_list.append(AlphaDistanceFromCameraShader(
- m.blend, m.influence, m.mapping, m.invert, m.curve,
- m.range_min, m.range_max))
- elif m.type == 'DISTANCE_FROM_OBJECT':
- if m.target is not None:
- shaders_list.append(AlphaDistanceFromObjectShader(
- m.blend, m.influence, m.mapping, m.invert, m.curve, m.target,
- m.range_min, m.range_max))
- elif m.type == 'MATERIAL':
- shaders_list.append(AlphaMaterialShader(
- m.blend, m.influence, m.mapping, m.invert, m.curve,
- m.material_attribute))
- elif m.type == 'TANGENT':
- shaders_list.append(TangentAlphaShader(
- m.blend, m.influence, m.mapping, m.invert, m.curve,))
- elif m.type == 'CREASE_ANGLE':
- shaders_list.append(CreaseAngleAlphaShader(
- m.blend, m.influence, m.mapping, m.invert, m.curve,
- m.angle_min, m.angle_max))
- elif m.type == 'CURVATURE_3D':
- shaders_list.append(Curvature3DAlphaShader(
- m.blend, m.influence, m.mapping, m.invert, m.curve,
- m.curvature_min, m.curvature_max))
- elif m.type == 'NOISE':
- shaders_list.append(AlphaNoiseShader(
- m.blend, m.influence, m.mapping, m.invert, m.curve,
- m.amplitude, m.period, m.seed))
- for m in linestyle.thickness_modifiers:
- if not m.use:
- continue
- if m.type == 'ALONG_STROKE':
- shaders_list.append(ThicknessAlongStrokeShader(
- thickness_position, linestyle.thickness_ratio,
- m.blend, m.influence, m.mapping, m.invert, m.curve,
- m.value_min, m.value_max))
- elif m.type == 'DISTANCE_FROM_CAMERA':
- shaders_list.append(ThicknessDistanceFromCameraShader(
- thickness_position, linestyle.thickness_ratio,
- m.blend, m.influence, m.mapping, m.invert, m.curve,
- m.range_min, m.range_max, m.value_min, m.value_max))
- elif m.type == 'DISTANCE_FROM_OBJECT':
- if m.target is not None:
- shaders_list.append(ThicknessDistanceFromObjectShader(
- thickness_position, linestyle.thickness_ratio,
- m.blend, m.influence, m.mapping, m.invert, m.curve, m.target,
- m.range_min, m.range_max, m.value_min, m.value_max))
- elif m.type == 'MATERIAL':
- shaders_list.append(ThicknessMaterialShader(
- thickness_position, linestyle.thickness_ratio,
- m.blend, m.influence, m.mapping, m.invert, m.curve,
- m.material_attribute, m.value_min, m.value_max))
- elif m.type == 'CALLIGRAPHY':
- shaders_list.append(CalligraphicThicknessShader(
- thickness_position, linestyle.thickness_ratio,
- m.blend, m.influence,
- m.orientation, m.thickness_min, m.thickness_max))
- elif m.type == 'TANGENT':
- shaders_list.append(TangentThicknessShader(
- thickness_position, linestyle.thickness_ratio,
- m.blend, m.influence, m.mapping, m.invert, m.curve,
- m.thickness_min, m.thickness_max))
- elif m.type == 'NOISE':
- shaders_list.append(ThicknessNoiseShader(
- thickness_position, linestyle.thickness_ratio,
- m.blend, m.influence,
- m.amplitude, m.period, m.seed, m.use_asymmetric))
- elif m.type == 'CREASE_ANGLE':
- shaders_list.append(CreaseAngleThicknessShader(
- thickness_position, linestyle.thickness_ratio,
- m.blend, m.influence, m.mapping, m.invert, m.curve,
- m.angle_min, m.angle_max, m.thickness_min, m.thickness_max))
- elif m.type == 'CURVATURE_3D':
- shaders_list.append(Curvature3DThicknessShader(
- thickness_position, linestyle.thickness_ratio,
- m.blend, m.influence, m.mapping, m.invert, m.curve,
- m.curvature_min, m.curvature_max, m.thickness_min, m.thickness_max))
- else:
- raise RuntimeError("No Thickness modifier with type", type(m), m)
- # -- Textures -- #
- has_tex = False
- if linestyle.use_nodes and linestyle.node_tree:
- shaders_list.append(BlenderTextureShader(linestyle.node_tree))
- has_tex = True
- if has_tex:
- shaders_list.append(StrokeTextureStepShader(linestyle.texture_spacing))
- # execute post-base stylization callbacks
- for fn in callbacks_modifiers_post:
- shaders_list.extend(fn(scene, layer, lineset))
- # -- Stroke caps -- #
- if linestyle.caps == 'ROUND':
- shaders_list.append(RoundCapShader())
- elif linestyle.caps == 'SQUARE':
- shaders_list.append(SquareCapShader())
- # -- Dashed line -- #
- if linestyle.use_dashed_line:
- pattern = get_dashed_pattern(linestyle)
- if len(pattern) > 0:
- shaders_list.append(DashedLineShader(pattern))
- # create strokes using the shaders list
- Operators.create(TrueUP1D(), shaders_list)
- # execute line set post-processing callback functions
- for fn in callbacks_lineset_post:
- fn(scene, layer, lineset)
|