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
This commit is contained in:
Folkert de Vries
2015-07-10 21:57:23 +09:00
committed by Tamito Kajiyama
parent 7837f0e833
commit eeeb845d33
12 changed files with 1513 additions and 63 deletions

View File

@@ -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
# 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()
#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():

View File

@@ -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: