Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFolkert de Vries <flokkievids@gmail.com>2015-07-10 15:57:23 +0300
committerTamito Kajiyama <rd6t-kjym@asahi-net.or.jp>2015-07-10 17:15:56 +0300
commiteeeb845d33e81afbc8ed127e6ab4ae7b18472a54 (patch)
tree78f8f3ecf9c4dd3bf4383ccc73520bcd2c6b2c7b /release
parent7837f0e8332f3726e0322b0c48b0da4d7c2d5813 (diff)
Freestyle: new stroke modifiers
This patch introduces a couple new stroke modifiers. The ones currently implemented are based on prototypes by @kjym3 and myself. The new modifiers: - Tangent - Thickness noise - Crease Angle - Simplification - Curvature 3D The documentation for these new modifier types can be found [[ http://www.blender.org/manual/render/freestyle/parameter_editor/index.html | in the manual ]]: {F134441} (left: AnisotropicThicknessShader, right: NoiseThicknessShader) {F140499} (left: Curvature 3D, right: Simplification) Author: Folkert de Vries (flokkievids) Reviewers: kjym3 Subscribers: #user_interface, plasmasolutions, kjym3 Projects: #bf_blender Differential Revision: https://developer.blender.org/D963
Diffstat (limited to 'release')
-rw-r--r--release/scripts/freestyle/modules/freestyle/utils.py180
-rw-r--r--release/scripts/freestyle/modules/parameter_editor.py401
-rw-r--r--release/scripts/startup/bl_ui/properties_freestyle.py100
3 files changed, 620 insertions, 61 deletions
diff --git a/release/scripts/freestyle/modules/freestyle/utils.py b/release/scripts/freestyle/modules/freestyle/utils.py
index 41d2297f723..c66426824c0 100644
--- a/release/scripts/freestyle/modules/freestyle/utils.py
+++ b/release/scripts/freestyle/modules/freestyle/utils.py
@@ -39,9 +39,11 @@ __all__ = (
"iter_material_value",
"iter_t2d_along_stroke",
"material_from_fedge",
+ "normal_at_I0D",
"pairwise",
"phase_to_direction",
"rgb_to_bw",
+ "simplify",
"stroke_curvature",
"stroke_normal",
"StrokeCollector",
@@ -66,8 +68,22 @@ from freestyle.types import (
from mathutils import Vector
from functools import lru_cache, namedtuple
-from math import cos, sin, pi
-from itertools import tee
+from math import cos, sin, pi, atan2
+from itertools import tee, compress
+
+# -- types -- #
+
+# A named tuple primitive used for storing data that has an upper and
+# lower bound (e.g., thickness, range and certain other values)
+class BoundedProperty(namedtuple("BoundedProperty", ["min", "max", "delta"])):
+ def __new__(cls, minimum, maximum, delta=None):
+ if delta is None:
+ delta = abs(maximum - minimum)
+ return super().__new__(cls, minimum, maximum, delta)
+
+ def interpolate(self, val):
+ result = (self.max - val) / self.delta
+ return 1.0 - bound(0, result, 1)
# -- real utility functions -- #
@@ -76,7 +92,6 @@ def rgb_to_bw(r, g, b):
"""Method to convert rgb to a bw intensity value."""
return 0.35 * r + 0.45 * g + 0.2 * b
-
def bound(lower, x, higher):
"""Returns x bounded by a maximum and minimum value. Equivalent to:
return min(max(x, lower), higher)
@@ -84,7 +99,6 @@ def bound(lower, x, higher):
# this is about 50% quicker than min(max(x, lower), higher)
return (lower if x <= lower else higher if x >= higher else x)
-
def get_strokes():
"""Get all strokes that are currently available"""
return tuple(map(Operators().get_stroke_from_index, range(Operators().get_strokes_size())))
@@ -118,8 +132,60 @@ def material_from_fedge(fe):
material = right if (right.priority > left.priority) else left
return material
-# -- General helper functions -- #
+def bounding_box(stroke):
+ """
+ Returns the maximum and minimum coordinates (the bounding box) of the stroke's vertices
+ """
+ x, y = zip(*(svert.point for svert in stroke))
+ return (Vector((min(x), min(y))), Vector((max(x), max(y))))
+def normal_at_I0D(it: Interface0DIterator) -> Vector:
+ """Normal at an Interface0D object. In contrast to Normal2DF0D this
+ function uses the actual data instead of underlying Fedge objects.
+ """
+ if it.at_last and it.is_begin:
+ # corner-case
+ return Vector((0, 0))
+ elif it.at_last:
+ it.decrement()
+ a, b = it.object, next(it)
+ elif it.is_begin:
+ a, b = it.object, next(it)
+ # give iterator back in original state
+ it.decrement()
+ elif it.is_end:
+ # just fail hard: this shouldn not happen
+ raise StopIteration()
+ else:
+ # this case sometimes has a small difference with Normal2DF0D (1e-3 -ish)
+ it.decrement()
+ a = it.object
+ curr, b = next(it), next(it)
+ # give iterator back in original state
+ it.decrement()
+ return (b.point - a.point).orthogonal().normalized()
+
+def angle_x_normal(it: Interface0DIterator):
+ """unsigned angle between a Point's normal and the X axis, in radians"""
+ normal = normal_at_I0D(it)
+ return abs(atan2(normal[1], normal[0]))
+
+def curvature_from_stroke_vertex(svert):
+ """The 3D curvature of an stroke vertex' underlying geometry
+ The result is None or in the range [-inf, inf]"""
+ c1 = svert.first_svertex.curvatures
+ c2 = svert.second_svertex.curvatures
+ if c1 is None and c2 is None:
+ Kr = None
+ elif c1 is None:
+ Kr = c2[4]
+ elif c2 is None:
+ Kr = c1[4]
+ else:
+ Kr = c1[4] + svert.t2d * (c2[4] - c1[4])
+ return Kr
+
+# -- General helper functions -- #
@lru_cache(maxsize=32)
def phase_to_direction(length):
@@ -134,9 +200,74 @@ def phase_to_direction(length):
results.append((phase, Vector((cos(2 * pi * phase), sin(2 * pi * phase)))))
return results
-# A named tuple primitive used for storing data that has an upper and
-# lower bound (e.g., thickness, range and certain values)
-BoundedProperty = namedtuple("BoundedProperty", ["min", "max", "delta"])
+
+
+# -- simplification of a set of points; based on simplify.js by Vladimir Agafonkin --
+# https://mourner.github.io/simplify-js/
+
+def getSquareSegmentDistance(p, p1, p2):
+ """
+ Square distance between point and a segment
+ """
+ x, y = p1
+
+ dx, dy = (p2 - p1)
+
+ if dx or dy:
+ t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy)
+
+ if t > 1:
+ x, y = p2
+ elif t > 0:
+ x += dx * t
+ y += dy * t
+
+ dx, dy = p.x - x, p.y - y
+ return dx * dx + dy * dy
+
+
+def simplifyDouglasPeucker(points, tolerance):
+ length = len(points)
+ markers = [0] * length
+
+ first = 0
+ last = length - 1
+
+ first_stack = []
+ last_stack = []
+
+ new_points = []
+
+ markers[first] = 1
+ markers[last] = 1
+
+ while last:
+ max_sqdist = 0
+
+ for i in range(first, last):
+ sqdist = getSquareSegmentDistance(points[i], points[first], points[last])
+
+ if sqdist > max_sqdist:
+ index = i
+ max_sqdist = sqdist
+
+ if max_sqdist > tolerance:
+ markers[index] = 1
+
+ first_stack.append(first)
+ last_stack.append(index)
+
+ first_stack.append(index)
+ last_stack.append(last)
+
+ first = first_stack.pop() if first_stack else None
+ last = last_stack.pop() if last_stack else None
+
+ return tuple(compress(points, markers))
+
+def simplify(points, tolerance):
+ """Simplifies a set of points"""
+ return simplifyDouglasPeucker(points, tolerance * tolerance)
class BoundingBox:
@@ -346,7 +477,6 @@ def iter_distance_along_stroke(stroke):
# -- mathematical operations -- #
-
def stroke_curvature(it):
"""
Compute the 2D curvature at the stroke vertex pointed by the iterator 'it'.
@@ -390,21 +520,23 @@ def stroke_normal(stroke):
for use in geometry modifiers it is advised to
cast this generator function to a tuple or list
"""
- n = len(stroke) - 1
-
- for i, svert in enumerate(stroke):
- if i == 0:
- e = stroke[i + 1].point - svert.point
- yield Vector((e[1], -e[0])).normalized()
- elif i == n:
- e = svert.point - stroke[i - 1].point
- yield Vector((e[1], -e[0])).normalized()
- else:
- e1 = stroke[i + 1].point - svert.point
- e2 = svert.point - stroke[i - 1].point
- n1 = Vector((e1[1], -e1[0])).normalized()
- n2 = Vector((e2[1], -e2[0])).normalized()
- yield (n1 + n2).normalized()
+ # n = len(stroke) - 1
+ it = iter(stroke)
+ yield from (normal_at_I0D(it) for _ in it)
+
+ #for i, svert in enumerate(stroke):
+ # if i == 0:
+ # e = stroke[i + 1].point - svert.point
+ # yield Vector((e[1], -e[0])).normalized()
+ # elif i == n:
+ # e = svert.point - stroke[i - 1].point
+ # yield Vector((e[1], -e[0])).normalized()
+ # else:
+ # e1 = stroke[i + 1].point - svert.point
+ # e2 = svert.point - stroke[i - 1].point
+ # n1 = Vector((e1[1], -e1[0])).normalized()
+ # n2 = Vector((e2[1], -e2[0])).normalized()
+ # yield (n1 + n2).normalized()
def get_test_stroke():
diff --git a/release/scripts/freestyle/modules/parameter_editor.py b/release/scripts/freestyle/modules/parameter_editor.py
index 3c11c33a39d..a1d0528e117 100644
--- a/release/scripts/freestyle/modules/parameter_editor.py
+++ b/release/scripts/freestyle/modules/parameter_editor.py
@@ -75,29 +75,34 @@ from freestyle.shaders import (
ConstantColorShader,
GuidingLinesShader,
PolygonalizationShader,
- SamplingShader,
- SpatialNoiseShader,
- StrokeShader,
- StrokeTextureStepShader,
- TipRemoverShader,
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_t2d_along_stroke,
iter_distance_from_camera,
iter_distance_from_object,
iter_material_value,
- stroke_normal,
- bound,
+ iter_t2d_along_stroke,
+ normal_at_I0D,
pairwise,
- BoundedProperty,
+ simplify,
+ stroke_normal,
)
from _freestyle import (
blendRamp,
@@ -106,12 +111,16 @@ from _freestyle import (
)
import time
+import bpy
+import random
+
from mathutils import Vector
-from math import pi, sin, cos, acos, radians
+from math import pi, sin, cos, acos, radians, atan2
from itertools import cycle, tee
-# lists of callback functions
# 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 = []
@@ -176,7 +185,13 @@ class CurveMappingModifier(ScalarBlendModifier):
return (1.0 - t) if self.invert else t
def CURVE(self, t):
- return evaluateCurveMappingF(self.curve, 0, 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:
@@ -209,10 +224,20 @@ class ThicknessBlenderMixIn(ThicknessModifierMixIn):
self.position = position
self.ratio = ratio
- def blend_thickness(self, svert, v):
- """Blends and sets the thickness."""
+ 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
- fe = svert.fedge
v = self.blend(outer + inner, v)
# Part 1: blend
@@ -227,21 +252,29 @@ class ThicknessBlenderMixIn(ThicknessModifierMixIn):
else:
raise ValueError("unknown thickness position: " + position)
- # Part 2: set
- if (fe.nature & Nature.BORDER):
+ 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
- outer, inner = inner, outer
- elif (fe.nature & Nature.SILHOUETTE):
+ right, left = left, right
+ elif (nature & Nature.SILHOUETTE):
if fe.is_smooth: # TODO more tests needed
- outer, inner = inner, outer
- else:
- outer = inner = (outer + inner) / 2
- svert.attribute.thickness = (outer, inner)
+ right, left = left, right
+ svert.attribute.thickness = (right, left)
class BaseThicknessShader(StrokeShader, ThicknessModifierMixIn):
@@ -294,7 +327,7 @@ class ThicknessAlongStrokeShader(ThicknessBlenderMixIn, CurveMappingModifier):
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, value_max - value_min)
+ self.value = BoundedProperty(value_min, value_max)
def shade(self, stroke):
for svert, t in zip(stroke, iter_t2d_along_stroke(stroke)):
@@ -308,7 +341,7 @@ 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, range_max - range_min)
+ self.range = BoundedProperty(range_min, range_max)
def shade(self, stroke):
it = iter_distance_from_camera(stroke, *self.range)
@@ -322,7 +355,7 @@ 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, range_max - range_min)
+ self.range = BoundedProperty(range_min, range_max)
def shade(self, stroke):
it = iter_distance_from_camera(stroke, *self.range)
@@ -338,8 +371,8 @@ class ThicknessDistanceFromCameraShader(ThicknessBlenderMixIn, CurveMappingModif
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, range_max - range_min)
- self.value = BoundedProperty(value_min, value_max, value_max - value_min)
+ 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):
@@ -355,7 +388,7 @@ class ColorDistanceFromObjectShader(ColorRampModifier):
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, range_max - range_min)
+ 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
@@ -375,7 +408,7 @@ class AlphaDistanceFromObjectShader(CurveMappingModifier):
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, range_max - range_min)
+ 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
@@ -397,8 +430,8 @@ class ThicknessDistanceFromObjectShader(ThicknessBlenderMixIn, CurveMappingModif
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, range_max - range_min)
- self.value = BoundedProperty(value_min, value_max, value_max - value_min)
+ 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
@@ -459,7 +492,7 @@ class ThicknessMaterialShader(ThicknessBlenderMixIn, CurveMappingModifier):
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, value_max - value_min)
+ self.value = BoundedProperty(value_min, value_max)
self.func = CurveMaterialF0D()
def shade(self, stroke):
@@ -478,7 +511,7 @@ class CalligraphicThicknessShader(ThicknessBlenderMixIn, ScalarBlendModifier):
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, thickness_max - thickness_min)
+ self.thickness = BoundedProperty(thickness_min, thickness_max)
self.func = VertexOrientation2DF0D()
def shade(self, stroke):
@@ -493,11 +526,252 @@ class CalligraphicThicknessShader(ThicknessBlenderMixIn, ScalarBlendModifier):
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 sinewave-like shape."""
+ """Displaces the stroke in a sine wave-like shape."""
def __init__(self, wavelength, amplitude, phase):
StrokeShader.__init__(self)
self.wavelength = wavelength
@@ -540,7 +814,7 @@ class PerlinNoise1DShader(StrokeShader):
class PerlinNoise2DShader(StrokeShader):
"""
Displaces the stroke using the strokes coordinates. This means
- that in a scene no strokes will be distorded identically.
+ 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/
@@ -883,7 +1157,6 @@ class Seed:
_seed = Seed()
-
def get_dashed_pattern(linestyle):
"""Extracts the dashed pattern from the various UI options """
pattern = []
@@ -1066,6 +1339,8 @@ def process(layer_name, lineset_name):
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))
@@ -1137,6 +1412,21 @@ def process(layer_name, lineset_name):
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
@@ -1155,6 +1445,21 @@ def process(layer_name, lineset_name):
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
@@ -1183,6 +1488,28 @@ def process(layer_name, lineset_name):
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 scene.render.use_shading_nodes:
diff --git a/release/scripts/startup/bl_ui/properties_freestyle.py b/release/scripts/startup/bl_ui/properties_freestyle.py
index 995136b0d97..f58a698d194 100644
--- a/release/scripts/startup/bl_ui/properties_freestyle.py
+++ b/release/scripts/startup/bl_ui/properties_freestyle.py
@@ -283,6 +283,10 @@ class RENDERLAYER_PT_freestyle_linestyle(RenderLayerFreestyleEditorButtonsPanel,
sub.operator("scene.freestyle_modifier_move", icon='TRIA_DOWN', text="").direction = 'DOWN'
sub.operator("scene.freestyle_modifier_remove", icon='X', text="")
+ def draw_modifier_box_error(self, box, modifier, message):
+ row = box.row()
+ row.label(text=message, icon="ERROR")
+
def draw_modifier_common(self, box, modifier):
row = box.row()
row.prop(modifier, "blend", text="")
@@ -351,6 +355,33 @@ class RENDERLAYER_PT_freestyle_linestyle(RenderLayerFreestyleEditorButtonsPanel,
if show_ramp:
self.draw_modifier_color_ramp_common(box, modifier, False)
+ elif modifier.type == 'TANGENT':
+ self.draw_modifier_color_ramp_common(box, modifier, False)
+
+ elif modifier.type == 'NOISE':
+ self.draw_modifier_color_ramp_common(box, modifier, False)
+ row = box.row(align=False)
+ row.prop(modifier, "amplitude")
+ row.prop(modifier, "period")
+ row.prop(modifier, "seed")
+
+ elif modifier.type == 'CREASE_ANGLE':
+ self.draw_modifier_color_ramp_common(box, modifier, False)
+ row = box.row(align=True)
+ row.prop(modifier, "angle_min")
+ row.prop(modifier, "angle_max")
+
+ elif modifier.type == 'CURVATURE_3D':
+ self.draw_modifier_color_ramp_common(box, modifier, False)
+ row = box.row(align=True)
+ row.prop(modifier, "curvature_min")
+ row.prop(modifier, "curvature_max")
+ freestyle = context.scene.render.layers.active.freestyle_settings
+ if not freestyle.use_smoothness:
+ message = "Enable Face Smoothness to use this modifier"
+ self.draw_modifier_box_error(col.box(), modifier, message)
+
+
def draw_alpha_modifier(self, context, modifier):
layout = self.layout
@@ -380,6 +411,32 @@ class RENDERLAYER_PT_freestyle_linestyle(RenderLayerFreestyleEditorButtonsPanel,
box.prop(modifier, "material_attribute", text="")
self.draw_modifier_curve_common(box, modifier, False, False)
+ elif modifier.type == 'TANGENT':
+ self.draw_modifier_curve_common(box, modifier, False, False)
+
+ elif modifier.type == 'NOISE':
+ self.draw_modifier_curve_common(box, modifier, False, False)
+ row = box.row(align=False)
+ row.prop(modifier, "amplitude")
+ row.prop(modifier, "period")
+ row.prop(modifier, "seed")
+
+ elif modifier.type == 'CREASE_ANGLE':
+ self.draw_modifier_curve_common(box, modifier, False, False)
+ row = box.row(align=True)
+ row.prop(modifier, "angle_min")
+ row.prop(modifier, "angle_max")
+
+ elif modifier.type == 'CURVATURE_3D':
+ self.draw_modifier_curve_common(box, modifier, False, False)
+ row = box.row(align=True)
+ row.prop(modifier, "curvature_min")
+ row.prop(modifier, "curvature_max")
+ freestyle = context.scene.render.layers.active.freestyle_settings
+ if not freestyle.use_smoothness:
+ message = "Enable Face Smoothness to use this modifier"
+ self.draw_modifier_box_error(col.box(), modifier, message)
+
def draw_thickness_modifier(self, context, modifier):
layout = self.layout
@@ -415,6 +472,45 @@ class RENDERLAYER_PT_freestyle_linestyle(RenderLayerFreestyleEditorButtonsPanel,
row.prop(modifier, "thickness_min")
row.prop(modifier, "thickness_max")
+ elif modifier.type == 'TANGENT':
+ self.draw_modifier_curve_common(box, modifier, False, False)
+ self.mapping = 'CURVE'
+ row = box.row(align=True)
+ row.prop(modifier, "thickness_min")
+ row.prop(modifier, "thickness_max")
+
+ elif modifier.type == 'NOISE':
+ row = box.row(align=False)
+ row.prop(modifier, "amplitude")
+ row.prop(modifier, "period")
+ row = box.row(align=False)
+ row.prop(modifier, "seed")
+ row.prop(modifier, "use_asymmetric")
+
+ elif modifier.type == 'CREASE_ANGLE':
+ self.draw_modifier_curve_common(box, modifier, False, False)
+ row = box.row(align=True)
+ row.prop(modifier, "thickness_min")
+ row.prop(modifier, "thickness_max")
+ row = box.row(align=True)
+ row.prop(modifier, "angle_min")
+ row.prop(modifier, "angle_max")
+
+
+ elif modifier.type == 'CURVATURE_3D':
+ self.draw_modifier_curve_common(box, modifier, False, False)
+ row = box.row(align=True)
+ row.prop(modifier, "thickness_min")
+ row.prop(modifier, "thickness_max")
+ row = box.row(align=True)
+ row.prop(modifier, "curvature_min")
+ row.prop(modifier, "curvature_max")
+ freestyle = context.scene.render.layers.active.freestyle_settings
+ if not freestyle.use_smoothness:
+ message = "Enable Face Smoothness to use this modifier"
+ self.draw_modifier_box_error(col.box(), modifier, message)
+
+
def draw_geometry_modifier(self, context, modifier):
layout = self.layout
@@ -512,6 +608,10 @@ class RENDERLAYER_PT_freestyle_linestyle(RenderLayerFreestyleEditorButtonsPanel,
row.prop(modifier, "scale_y")
box.prop(modifier, "angle")
+ elif modifier.type == 'SIMPLIFICATION':
+ box.prop(modifier, "tolerance")
+
+
def draw(self, context):
layout = self.layout