From eeeb845d33e81afbc8ed127e6ab4ae7b18472a54 Mon Sep 17 00:00:00 2001 From: Folkert de Vries Date: Fri, 10 Jul 2015 21:57:23 +0900 Subject: 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 --- .../scripts/freestyle/modules/freestyle/utils.py | 180 +++++++-- .../scripts/freestyle/modules/parameter_editor.py | 401 +++++++++++++++++++-- .../scripts/startup/bl_ui/properties_freestyle.py | 100 +++++ 3 files changed, 620 insertions(+), 61 deletions(-) (limited to 'release') 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 -- cgit v1.2.3