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:

committed by
Tamito Kajiyama

parent
7837f0e833
commit
eeeb845d33
@@ -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():
|
||||
|
@@ -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:
|
||||
|
Reference in New Issue
Block a user