This commit implements described in the #104573. The goal is to fix the confusion of the submodule hashes change, which are not ideal for any of the supported git-module configuration (they are either always visible causing confusion, or silently staged and committed, also causing confusion). This commit replaces submodules with a checkout of addons and addons_contrib, covered by the .gitignore, and locale and developer tools are moved to the main repository. This also changes the paths: - /release/scripts are moved to the /scripts - /source/tools are moved to the /tools - /release/datafiles/locale is moved to /locale This is done to avoid conflicts when using bisect, and also allow buildbot to automatically "recover" wgen building older or newer branches/patches. Running `make update` will initialize the local checkout to the changed repository configuration. Another aspect of the change is that the make update will support Github style of remote organization (origin remote pointing to thy fork, upstream remote pointing to the upstream blender/blender.git). Pull Request #104755
1248 lines
40 KiB
Python
1248 lines
40 KiB
Python
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
# Filename : shaders.py
|
|
# Authors : Fredo Durand, Stephane Grabli, Francois Sillion, Emmanuel Turquin
|
|
# Date : 11/08/2005
|
|
# Purpose : Stroke shaders to be used for creation of stylized strokes
|
|
|
|
"""
|
|
This module contains stroke shaders used for creation of stylized
|
|
strokes. It is also intended to be a collection of examples for
|
|
shader definition in Python.
|
|
|
|
User-defined stroke shaders inherit the
|
|
:class:`freestyle.types.StrokeShader` class.
|
|
"""
|
|
|
|
__all__ = (
|
|
"BackboneStretcherShader",
|
|
"BezierCurveShader",
|
|
"BlenderTextureShader",
|
|
"CalligraphicShader",
|
|
"ColorNoiseShader",
|
|
"ConstantColorShader",
|
|
"ConstantThicknessShader",
|
|
"ConstrainedIncreasingThicknessShader",
|
|
"GuidingLinesShader",
|
|
"IncreasingColorShader",
|
|
"IncreasingThicknessShader",
|
|
"PolygonalizationShader",
|
|
"RoundCapShader",
|
|
"SamplingShader",
|
|
"SmoothingShader",
|
|
"SpatialNoiseShader",
|
|
"SquareCapShader",
|
|
"StrokeTextureStepShader",
|
|
"ThicknessNoiseShader",
|
|
"TipRemoverShader",
|
|
"py2DCurvatureColorShader",
|
|
"pyBackboneStretcherNoCuspShader",
|
|
"pyBackboneStretcherShader",
|
|
"pyBluePrintCirclesShader",
|
|
"pyBluePrintDirectedSquaresShader",
|
|
"pyBluePrintEllipsesShader",
|
|
"pyBluePrintSquaresShader",
|
|
"pyConstantColorShader",
|
|
"pyConstantThicknessShader",
|
|
"pyConstrainedIncreasingThicknessShader",
|
|
"pyDecreasingThicknessShader",
|
|
"pyDepthDiscontinuityThicknessShader",
|
|
"pyDiffusion2Shader",
|
|
"pyFXSVaryingThicknessWithDensityShader",
|
|
"pyGuidingLineShader",
|
|
"pyHLRShader",
|
|
"pyImportance2DThicknessShader",
|
|
"pyImportance3DThicknessShader",
|
|
"pyIncreasingColorShader",
|
|
"pyIncreasingThicknessShader",
|
|
"pyInterpolateColorShader",
|
|
"pyLengthDependingBackboneStretcherShader",
|
|
"pyMaterialColorShader",
|
|
"pyModulateAlphaShader",
|
|
"pyNonLinearVaryingThicknessShader",
|
|
"pyPerlinNoise1DShader",
|
|
"pyPerlinNoise2DShader",
|
|
"pyRandomColorShader",
|
|
"pySLERPThicknessShader",
|
|
"pySamplingShader",
|
|
"pySinusDisplacementShader",
|
|
"pyTVertexRemoverShader",
|
|
"pyTVertexThickenerShader",
|
|
"pyTimeColorShader",
|
|
"pyTipRemoverShader",
|
|
"pyZDependingThicknessShader",
|
|
)
|
|
|
|
|
|
# module members
|
|
from _freestyle import (
|
|
BackboneStretcherShader,
|
|
BezierCurveShader,
|
|
BlenderTextureShader,
|
|
CalligraphicShader,
|
|
ColorNoiseShader,
|
|
ConstantColorShader,
|
|
ConstantThicknessShader,
|
|
ConstrainedIncreasingThicknessShader,
|
|
GuidingLinesShader,
|
|
IncreasingColorShader,
|
|
IncreasingThicknessShader,
|
|
PolygonalizationShader,
|
|
SamplingShader,
|
|
SmoothingShader,
|
|
SpatialNoiseShader,
|
|
StrokeTextureStepShader,
|
|
ThicknessNoiseShader,
|
|
TipRemoverShader,
|
|
)
|
|
|
|
# constructs for shader definition in Python
|
|
from freestyle.types import (
|
|
Interface0DIterator,
|
|
Nature,
|
|
Noise,
|
|
StrokeAttribute,
|
|
StrokeShader,
|
|
StrokeVertexIterator,
|
|
StrokeVertex,
|
|
)
|
|
from freestyle.functions import (
|
|
Curvature2DAngleF0D,
|
|
DensityF0D,
|
|
GetProjectedZF0D,
|
|
MaterialF0D,
|
|
Normal2DF0D,
|
|
Orientation2DF1D,
|
|
ZDiscontinuityF0D,
|
|
)
|
|
from freestyle.predicates import (
|
|
pyVertexNatureUP0D,
|
|
pyUEqualsUP0D,
|
|
)
|
|
|
|
from freestyle.utils import (
|
|
bound,
|
|
BoundingBox,
|
|
pairwise,
|
|
phase_to_direction,
|
|
)
|
|
|
|
from freestyle.utils import ContextFunctions as CF
|
|
|
|
import bpy
|
|
import random
|
|
|
|
from math import atan, cos, pi, sin, sinh, sqrt
|
|
from mathutils import Vector
|
|
from random import randint
|
|
|
|
|
|
# -- Thickness Stroke Shaders -- #
|
|
|
|
|
|
class pyDepthDiscontinuityThicknessShader(StrokeShader):
|
|
"""
|
|
Assigns a thickness to the stroke based on the stroke's distance
|
|
to the camera (Z-value).
|
|
"""
|
|
|
|
def __init__(self, min, max):
|
|
StrokeShader.__init__(self)
|
|
self.a = max - min
|
|
self.b = min
|
|
self.func = ZDiscontinuityF0D()
|
|
|
|
def shade(self, stroke):
|
|
it = Interface0DIterator(stroke)
|
|
for svert in it:
|
|
z = self.func(it)
|
|
thickness = self.a * z + self.b
|
|
svert.attribute.thickness = (thickness, thickness)
|
|
|
|
|
|
class pyConstantThicknessShader(StrokeShader):
|
|
"""
|
|
Assigns a constant thickness along the stroke.
|
|
"""
|
|
|
|
def __init__(self, thickness):
|
|
StrokeShader.__init__(self)
|
|
self._thickness = thickness / 2.0
|
|
|
|
def shade(self, stroke):
|
|
for svert in stroke:
|
|
svert.attribute.thickness = (self._thickness, self._thickness)
|
|
|
|
|
|
class pyFXSVaryingThicknessWithDensityShader(StrokeShader):
|
|
"""
|
|
Assigns thickness to a stroke based on the density of the diffuse map.
|
|
"""
|
|
|
|
def __init__(self, wsize, threshold_min, threshold_max, thicknessMin, thicknessMax):
|
|
StrokeShader.__init__(self)
|
|
self._func = DensityF0D(wsize)
|
|
self.threshold_min = threshold_min
|
|
self.threshold_max = threshold_max
|
|
self._thicknessMin = thicknessMin
|
|
self._thicknessMax = thicknessMax
|
|
|
|
def shade(self, stroke):
|
|
it = Interface0DIterator(stroke)
|
|
delta_threshold = self.threshold_max - self.threshold_min
|
|
delta_thickness = self._thicknessMax - self._thicknessMin
|
|
|
|
for svert in it:
|
|
c = self._func(it)
|
|
c = bound(self.threshold_min, c, self.threshold_max)
|
|
t = (self.threshold_max - c) / delta_threshold * delta_thickness + self._thicknessMin
|
|
svert.attribute.thickness = (t / 2.0, t / 2.0)
|
|
|
|
|
|
class pyIncreasingThicknessShader(StrokeShader):
|
|
"""
|
|
Increasingly thickens the stroke.
|
|
"""
|
|
|
|
def __init__(self, thicknessMin, thicknessMax):
|
|
StrokeShader.__init__(self)
|
|
self._thicknessMin = thicknessMin
|
|
self._thicknessMax = thicknessMax
|
|
|
|
def shade(self, stroke):
|
|
n = len(stroke)
|
|
for i, svert in enumerate(stroke):
|
|
c = i / n
|
|
if i < (n * 0.5):
|
|
t = (1.0 - c) * self._thicknessMin + c * self._thicknessMax
|
|
else:
|
|
t = (1.0 - c) * self._thicknessMax + c * self._thicknessMin
|
|
svert.attribute.thickness = (t / 2.0, t / 2.0)
|
|
|
|
|
|
class pyConstrainedIncreasingThicknessShader(StrokeShader):
|
|
"""
|
|
Increasingly thickens the stroke, constrained by a ratio of the
|
|
stroke's length.
|
|
"""
|
|
|
|
def __init__(self, thicknessMin, thicknessMax, ratio):
|
|
StrokeShader.__init__(self)
|
|
self._thicknessMin = thicknessMin
|
|
self._thicknessMax = thicknessMax
|
|
self._ratio = ratio
|
|
|
|
def shade(self, stroke):
|
|
n = len(stroke)
|
|
maxT = min(self._ratio * stroke.length_2d, self._thicknessMax)
|
|
|
|
for i, svert in enumerate(stroke):
|
|
c = i / n
|
|
if i < (n * 0.5):
|
|
t = (1.0 - c) * self._thicknessMin + c * maxT
|
|
else:
|
|
t = (1.0 - c) * maxT + c * self._thicknessMin
|
|
|
|
if i == (n - 1):
|
|
svert.attribute.thickness = (self._thicknessMin / 2.0, self._thicknessMin / 2.0)
|
|
else:
|
|
svert.attribute.thickness = (t / 2.0, t / 2.0)
|
|
|
|
|
|
class pyDecreasingThicknessShader(StrokeShader):
|
|
"""
|
|
Inverse of pyIncreasingThicknessShader, decreasingly thickens the stroke.
|
|
"""
|
|
|
|
def __init__(self, thicknessMin, thicknessMax):
|
|
StrokeShader.__init__(self)
|
|
self._thicknessMin = thicknessMin
|
|
self._thicknessMax = thicknessMax
|
|
|
|
def shade(self, stroke):
|
|
l = stroke.length_2d
|
|
n = len(stroke)
|
|
tMax = min(self._thicknessMax, 0.33 * l)
|
|
tMin = min(self._thicknessMin, 0.10 * l)
|
|
|
|
for i, svert in enumerate(stroke):
|
|
c = i / n
|
|
t = (1.0 - c) * tMax + c * tMin
|
|
svert.attribute.thickness = (t / 2.0, t / 2.0)
|
|
|
|
|
|
class pyNonLinearVaryingThicknessShader(StrokeShader):
|
|
"""
|
|
Assigns thickness to a stroke based on an exponential function.
|
|
"""
|
|
|
|
def __init__(self, thicknessExtremity, thicknessMiddle, exponent):
|
|
self._thicknessMin = thicknessMiddle
|
|
self._thicknessMax = thicknessExtremity
|
|
self._exp = exponent
|
|
StrokeShader.__init__(self)
|
|
|
|
def shade(self, stroke):
|
|
n = len(stroke)
|
|
for i, svert in enumerate(stroke):
|
|
c = (i / n) if (i < n / 2.0) else ((n - i) / n)
|
|
c = pow(c, self._exp) * pow(2.0, self._exp)
|
|
t = (1.0 - c) * self._thicknessMax + c * self._thicknessMin
|
|
svert.attribute.thickness = (t / 2.0, t / 2.0)
|
|
|
|
|
|
class pySLERPThicknessShader(StrokeShader):
|
|
"""
|
|
Assigns thickness to a stroke based on spherical linear interpolation.
|
|
"""
|
|
|
|
def __init__(self, thicknessMin, thicknessMax, omega=1.2):
|
|
StrokeShader.__init__(self)
|
|
self._thicknessMin = thicknessMin
|
|
self._thicknessMax = thicknessMax
|
|
self.omega = omega
|
|
|
|
def shade(self, stroke):
|
|
n = len(stroke)
|
|
maxT = min(self._thicknessMax, 0.33 * stroke.length_2d)
|
|
omega = self.omega
|
|
sinhyp = sinh(omega)
|
|
for i, svert in enumerate(stroke):
|
|
c = i / n
|
|
if i < (n * 0.5):
|
|
t = sin((1 - c) * omega) / sinhyp * self._thicknessMin + sin(c * omega) / sinhyp * maxT
|
|
else:
|
|
t = sin((1 - c) * omega) / sinhyp * maxT + sin(c * omega) / sinhyp * self._thicknessMin
|
|
svert.attribute.thickness = (t / 2.0, t / 2.0)
|
|
|
|
|
|
class pyTVertexThickenerShader(StrokeShader):
|
|
"""
|
|
Thickens TVertices (visual intersections between two edges).
|
|
"""
|
|
|
|
def __init__(self, a=1.5, n=3):
|
|
StrokeShader.__init__(self)
|
|
self._a = a
|
|
self._n = n
|
|
|
|
def shade(self, stroke):
|
|
n = self._n
|
|
a = self._a
|
|
|
|
term = (a - 1.0) / (n - 1.0)
|
|
|
|
if (stroke[0].nature & Nature.T_VERTEX):
|
|
for count, svert in zip(range(n), stroke):
|
|
r = term * (n / (count + 1.0) - 1.0) + 1.0
|
|
(tr, tl) = svert.attribute.thickness
|
|
svert.attribute.thickness = (r * tr, r * tl)
|
|
|
|
if (stroke[-1].nature & Nature.T_VERTEX):
|
|
for count, svert in zip(range(n), reversed(stroke)):
|
|
r = term * (n / (count + 1.0) - 1.0) + 1.0
|
|
(tr, tl) = svert.attribute.thickness
|
|
svert.attribute.thickness = (r * tr, r * tl)
|
|
|
|
|
|
class pyImportance2DThicknessShader(StrokeShader):
|
|
"""
|
|
Assigns thickness based on distance to a given point in 2D space.
|
|
the thickness is inverted, so the vertices closest to the
|
|
specified point have the lowest thickness.
|
|
"""
|
|
|
|
def __init__(self, x, y, w, kmin, kmax):
|
|
StrokeShader.__init__(self)
|
|
self._origin = Vector((x, y))
|
|
self._w = w
|
|
self._kmin, self._kmax = kmin, kmax
|
|
|
|
def shade(self, stroke):
|
|
for svert in stroke:
|
|
d = (svert.point_2d - self._origin).length
|
|
k = (self._kmin if (d > self._w) else
|
|
(self._kmax * (self._w - d) + self._kmin * d) / self._w)
|
|
|
|
(tr, tl) = svert.attribute.thickness
|
|
svert.attribute.thickness = (k * tr / 2.0, k * tl / 2.0)
|
|
|
|
|
|
class pyImportance3DThicknessShader(StrokeShader):
|
|
"""
|
|
Assigns thickness based on distance to a given point in 3D space.
|
|
"""
|
|
|
|
def __init__(self, x, y, z, w, kmin, kmax):
|
|
StrokeShader.__init__(self)
|
|
self._origin = Vector((x, y, z))
|
|
self._w = w
|
|
self._kmin, self._kmax = kmin, kmax
|
|
|
|
def shade(self, stroke):
|
|
for svert in stroke:
|
|
d = (svert.point_3d - self._origin).length
|
|
k = (self._kmin if (d > self._w) else
|
|
(self._kmax * (self._w - d) + self._kmin * d) / self._w)
|
|
|
|
(tr, tl) = svert.attribute.thickness
|
|
svert.attribute.thickness = (k * tr / 2.0, k * tl / 2.0)
|
|
|
|
|
|
class pyZDependingThicknessShader(StrokeShader):
|
|
"""
|
|
Assigns thickness based on an object's local Z depth (point
|
|
closest to camera is 1, point furthest from camera is zero).
|
|
"""
|
|
|
|
def __init__(self, min, max):
|
|
StrokeShader.__init__(self)
|
|
self.__min = min
|
|
self.__max = max
|
|
self.func = GetProjectedZF0D()
|
|
|
|
def shade(self, stroke):
|
|
it = Interface0DIterator(stroke)
|
|
z_indices = tuple(self.func(it) for _ in it)
|
|
z_min, z_max = min(1, *z_indices), max(0, *z_indices)
|
|
z_diff = 1 / (z_max - z_min)
|
|
|
|
for svert, z_index in zip(stroke, z_indices):
|
|
z = (z_index - z_min) * z_diff
|
|
thickness = (1 - z) * self.__max + z * self.__min
|
|
svert.attribute.thickness = (thickness, thickness)
|
|
|
|
|
|
# -- Color & Alpha Stroke Shaders -- #
|
|
|
|
|
|
class pyConstantColorShader(StrokeShader):
|
|
"""
|
|
Assigns a constant color to the stroke.
|
|
"""
|
|
|
|
def __init__(self, r, g, b, a=1):
|
|
StrokeShader.__init__(self)
|
|
self._color = (r, g, b)
|
|
self._a = a
|
|
|
|
def shade(self, stroke):
|
|
for svert in stroke:
|
|
svert.attribute.color = self._color
|
|
svert.attribute.alpha = self._a
|
|
|
|
|
|
class pyIncreasingColorShader(StrokeShader):
|
|
"""
|
|
Fades from one color to another along the stroke.
|
|
"""
|
|
|
|
def __init__(self, r1, g1, b1, a1, r2, g2, b2, a2):
|
|
StrokeShader.__init__(self)
|
|
# use 4d vector to simplify math
|
|
self._c1 = Vector((r1, g1, b1, a1))
|
|
self._c2 = Vector((r2, g2, b2, a2))
|
|
|
|
def shade(self, stroke):
|
|
n = len(stroke) - 1
|
|
|
|
for i, svert in enumerate(stroke):
|
|
c = i / n
|
|
color = (1 - c) * self._c1 + c * self._c2
|
|
svert.attribute.color = color[:3]
|
|
svert.attribute.alpha = color[3]
|
|
|
|
|
|
class pyInterpolateColorShader(StrokeShader):
|
|
"""
|
|
Fades from one color to another and back.
|
|
"""
|
|
|
|
def __init__(self, r1, g1, b1, a1, r2, g2, b2, a2):
|
|
StrokeShader.__init__(self)
|
|
# use 4d vector to simplify math
|
|
self._c1 = Vector((r1, g1, b1, a1))
|
|
self._c2 = Vector((r2, g2, b2, a2))
|
|
|
|
def shade(self, stroke):
|
|
n = len(stroke) - 1
|
|
for i, svert in enumerate(stroke):
|
|
c = 1.0 - 2.0 * abs((i / n) - 0.5)
|
|
color = (1.0 - c) * self._c1 + c * self._c2
|
|
svert.attribute.color = color[:3]
|
|
svert.attribute.alpha = color[3]
|
|
|
|
|
|
class pyModulateAlphaShader(StrokeShader):
|
|
"""
|
|
Limits the stroke's alpha between a min and max value.
|
|
"""
|
|
|
|
def __init__(self, min=0, max=1):
|
|
StrokeShader.__init__(self)
|
|
self.__min = min
|
|
self.__max = max
|
|
|
|
def shade(self, stroke):
|
|
for svert in stroke:
|
|
alpha = svert.attribute.alpha
|
|
alpha = bound(self.__min, alpha * svert.point.y * 0.0025, self.__max)
|
|
svert.attribute.alpha = alpha
|
|
|
|
|
|
class pyMaterialColorShader(StrokeShader):
|
|
"""
|
|
Assigns the color of the underlying material to the stroke.
|
|
"""
|
|
|
|
def __init__(self, threshold=50):
|
|
StrokeShader.__init__(self)
|
|
self._threshold = threshold
|
|
self._func = MaterialF0D()
|
|
|
|
def shade(self, stroke):
|
|
xn = 0.312713
|
|
yn = 0.329016
|
|
Yn = 1.0
|
|
un = 4.0 * xn / (-2.0 * xn + 12.0 * yn + 3.0)
|
|
vn = 9.0 * yn / (-2.0 * xn + 12.0 * yn + 3.0)
|
|
|
|
it = Interface0DIterator(stroke)
|
|
for svert in it:
|
|
mat = self._func(it)
|
|
|
|
r, g, b, *_ = mat.diffuse
|
|
|
|
X = 0.412453 * r + 0.35758 * g + 0.180423 * b
|
|
Y = 0.212671 * r + 0.71516 * g + 0.072169 * b
|
|
Z = 0.019334 * r + 0.11919 * g + 0.950227 * b
|
|
|
|
if not any((X, Y, Z)):
|
|
X = Y = Z = 0.01
|
|
|
|
u = 4.0 * X / (X + 15.0 * Y + 3.0 * Z)
|
|
v = 9.0 * Y / (X + 15.0 * Y + 3.0 * Z)
|
|
|
|
L = 116. * pow((Y / Yn), (1. / 3.)) - 16
|
|
U = 13. * L * (u - un)
|
|
V = 13. * L * (v - vn)
|
|
|
|
if L > self._threshold:
|
|
L /= 1.3
|
|
U += 10.
|
|
else:
|
|
L = L + 2.5 * (100 - L) * 0.2
|
|
U /= 3.0
|
|
V /= 3.0
|
|
|
|
u = U / (13.0 * L) + un
|
|
v = V / (13.0 * L) + vn
|
|
|
|
Y = Yn * pow(((L + 16.) / 116.), 3.)
|
|
X = -9. * Y * u / ((u - 4.) * v - u * v)
|
|
Z = (9. * Y - 15 * v * Y - v * X) / (3. * v)
|
|
|
|
r = 3.240479 * X - 1.53715 * Y - 0.498535 * Z
|
|
g = -0.969256 * X + 1.875991 * Y + 0.041556 * Z
|
|
b = 0.055648 * X - 0.204043 * Y + 1.057311 * Z
|
|
|
|
r = max(0, r)
|
|
g = max(0, g)
|
|
b = max(0, b)
|
|
|
|
svert.attribute.color = (r, g, b)
|
|
|
|
|
|
class pyRandomColorShader(StrokeShader):
|
|
"""
|
|
Assigns a color to the stroke based on given seed.
|
|
"""
|
|
|
|
def __init__(self, s=1):
|
|
StrokeShader.__init__(self)
|
|
random.seed = s
|
|
|
|
def shade(self, stroke):
|
|
c = (random.uniform(15, 75) * 0.01,
|
|
random.uniform(15, 75) * 0.01,
|
|
random.uniform(15, 75) * 0.01)
|
|
for svert in stroke:
|
|
svert.attribute.color = c
|
|
|
|
|
|
class py2DCurvatureColorShader(StrokeShader):
|
|
"""
|
|
Assigns a color (grayscale) to the stroke based on the curvature.
|
|
A higher curvature will yield a brighter color.
|
|
"""
|
|
|
|
def shade(self, stroke):
|
|
func = Curvature2DAngleF0D()
|
|
it = Interface0DIterator(stroke)
|
|
for svert in it:
|
|
c = func(it)
|
|
if c < 0 and bpy.app.debug_freestyle:
|
|
print("py2DCurvatureColorShader: negative 2D curvature")
|
|
color = 10.0 * c / pi
|
|
svert.attribute.color = (color, color, color)
|
|
|
|
|
|
class pyTimeColorShader(StrokeShader):
|
|
"""
|
|
Assigns a grayscale value that increases for every vertex.
|
|
The brightness will increase along the stroke.
|
|
"""
|
|
|
|
def __init__(self, step=0.01):
|
|
StrokeShader.__init__(self)
|
|
self._step = step
|
|
|
|
def shade(self, stroke):
|
|
for i, svert in enumerate(stroke):
|
|
c = i * self._step
|
|
svert.attribute.color = (c, c, c)
|
|
|
|
|
|
# -- Geometry Stroke Shaders -- #
|
|
|
|
|
|
class pySamplingShader(StrokeShader):
|
|
"""
|
|
Resamples the stroke, which gives the stroke the amount of
|
|
vertices specified.
|
|
"""
|
|
|
|
def __init__(self, sampling):
|
|
StrokeShader.__init__(self)
|
|
self._sampling = sampling
|
|
|
|
def shade(self, stroke):
|
|
stroke.resample(float(self._sampling))
|
|
stroke.update_length()
|
|
|
|
|
|
class pyBackboneStretcherShader(StrokeShader):
|
|
"""
|
|
Stretches the stroke's backbone by a given length (in pixels).
|
|
"""
|
|
|
|
def __init__(self, l):
|
|
StrokeShader.__init__(self)
|
|
self._l = l
|
|
|
|
def shade(self, stroke):
|
|
# get start and end points
|
|
v0, vn = stroke[0], stroke[-1]
|
|
p0, pn = v0.point, vn.point
|
|
# get the direction
|
|
d1 = (p0 - stroke[1].point).normalized()
|
|
dn = (pn - stroke[-2].point).normalized()
|
|
v0.point += d1 * self._l
|
|
vn.point += dn * self._l
|
|
stroke.update_length()
|
|
|
|
|
|
class pyLengthDependingBackboneStretcherShader(StrokeShader):
|
|
"""
|
|
Stretches the stroke's backbone proportional to the stroke's length
|
|
NOTE: you'll probably want an l somewhere between (0.5 - 0). A value that
|
|
is too high may yield unexpected results.
|
|
"""
|
|
|
|
def __init__(self, l):
|
|
StrokeShader.__init__(self)
|
|
self._l = l
|
|
|
|
def shade(self, stroke):
|
|
# get start and end points
|
|
v0, vn = stroke[0], stroke[-1]
|
|
p0, pn = v0.point, vn.point
|
|
# get the direction
|
|
d1 = (p0 - stroke[1].point).normalized()
|
|
dn = (pn - stroke[-2].point).normalized()
|
|
v0.point += d1 * self._l * stroke.length_2d
|
|
vn.point += dn * self._l * stroke.length_2d
|
|
stroke.update_length()
|
|
|
|
|
|
class pyGuidingLineShader(StrokeShader):
|
|
def shade(self, stroke):
|
|
# get the tangent direction
|
|
t = stroke[-1].point - stroke[0].point
|
|
# look for the stroke middle vertex
|
|
itmiddle = iter(stroke)
|
|
while itmiddle.object.u < 0.5:
|
|
itmiddle.increment()
|
|
center_vertex = itmiddle.object
|
|
# position all the vertices along the tangent for the right part
|
|
it = StrokeVertexIterator(itmiddle)
|
|
for svert in it:
|
|
svert.point = center_vertex.point + t * (svert.u - center_vertex.u)
|
|
# position all the vertices along the tangent for the left part
|
|
it = StrokeVertexIterator(itmiddle).reversed()
|
|
for svert in it:
|
|
svert.point = center_vertex.point - t * (center_vertex.u - svert.u)
|
|
stroke.update_length()
|
|
|
|
|
|
class pyBackboneStretcherNoCuspShader(StrokeShader):
|
|
"""
|
|
Stretches the stroke's backbone, excluding cusp vertices (end junctions).
|
|
"""
|
|
|
|
def __init__(self, l):
|
|
StrokeShader.__init__(self)
|
|
self._l = l
|
|
|
|
def shade(self, stroke):
|
|
|
|
v0, v1 = stroke[0], stroke[1]
|
|
vn, vn_1 = stroke[-1], stroke[-2]
|
|
|
|
if not (v0.nature & v1.nature & Nature.CUSP):
|
|
d1 = (v0.point - v1.point).normalized()
|
|
v0.point += d1 * self._l
|
|
|
|
if not (vn.nature & vn_1.nature & Nature.CUSP):
|
|
dn = (vn.point - vn_1.point).normalized()
|
|
vn.point += dn * self._l
|
|
|
|
stroke.update_length()
|
|
|
|
|
|
class pyDiffusion2Shader(StrokeShader):
|
|
"""
|
|
Iteratively adds an offset to the position of each stroke vertex
|
|
in the direction perpendicular to the stroke direction at the
|
|
point. The offset is scaled by the 2D curvature (i.e. how quickly
|
|
the stroke curve is) at the point.
|
|
"""
|
|
|
|
def __init__(self, lambda1, nbIter):
|
|
StrokeShader.__init__(self)
|
|
self._lambda = lambda1
|
|
self._nbIter = nbIter
|
|
self._normalInfo = Normal2DF0D()
|
|
self._curvatureInfo = Curvature2DAngleF0D()
|
|
|
|
def shade(self, stroke):
|
|
for _i in range(1, self._nbIter):
|
|
it = Interface0DIterator(stroke)
|
|
for svert in it:
|
|
svert.point += self._normalInfo(it) * self._lambda * self._curvatureInfo(it)
|
|
stroke.update_length()
|
|
|
|
|
|
class pyTipRemoverShader(StrokeShader):
|
|
"""
|
|
Removes the tips of the stroke.
|
|
"""
|
|
|
|
def __init__(self, l):
|
|
StrokeShader.__init__(self)
|
|
self._l = l
|
|
|
|
@staticmethod
|
|
def check_vertex(v, length):
|
|
# Returns True if the given strokevertex is less than self._l away
|
|
# from the stroke's tip and therefore should be removed.
|
|
return (v.curvilinear_abscissa < length or v.stroke_length - v.curvilinear_abscissa < length)
|
|
|
|
def shade(self, stroke):
|
|
n = len(stroke)
|
|
if n < 4:
|
|
return
|
|
|
|
verticesToRemove = tuple(svert for svert in stroke if self.check_vertex(svert, self._l))
|
|
# explicit conversion to StrokeAttribute is needed
|
|
oldAttributes = (StrokeAttribute(svert.attribute) for svert in stroke)
|
|
|
|
if n - len(verticesToRemove) < 2:
|
|
return
|
|
|
|
for sv in verticesToRemove:
|
|
stroke.remove_vertex(sv)
|
|
|
|
stroke.update_length()
|
|
stroke.resample(n)
|
|
if len(stroke) != n and bpy.app.debug_freestyle:
|
|
print("pyTipRemover: Warning: resampling problem")
|
|
|
|
for svert, a in zip(stroke, oldAttributes):
|
|
svert.attribute = a
|
|
stroke.update_length()
|
|
|
|
|
|
class pyTVertexRemoverShader(StrokeShader):
|
|
"""
|
|
Removes t-vertices from the stroke.
|
|
"""
|
|
|
|
def shade(self, stroke):
|
|
if len(stroke) < 4:
|
|
return
|
|
|
|
v0, vn = stroke[0], stroke[-1]
|
|
if (v0.nature & Nature.T_VERTEX):
|
|
stroke.remove_vertex(v0)
|
|
if (vn.nature & Nature.T_VERTEX):
|
|
stroke.remove_vertex(vn)
|
|
stroke.update_length()
|
|
|
|
|
|
class pyHLRShader(StrokeShader):
|
|
"""
|
|
Controls visibility based upon the quantitative invisibility (QI)
|
|
based on hidden line removal (HLR).
|
|
"""
|
|
|
|
def shade(self, stroke):
|
|
if len(stroke) < 4:
|
|
return
|
|
|
|
it = iter(stroke)
|
|
for v1, v2 in zip(it, it.incremented()):
|
|
if (v1.nature & Nature.VIEW_VERTEX):
|
|
visible = (v1.get_fedge(v2).viewedge.qi != 0)
|
|
v1.attribute.visible = not visible
|
|
|
|
|
|
class pySinusDisplacementShader(StrokeShader):
|
|
"""
|
|
Displaces the stroke in the shape of a sine wave.
|
|
"""
|
|
|
|
def __init__(self, f, a):
|
|
StrokeShader.__init__(self)
|
|
self._f = f
|
|
self._a = a
|
|
self._getNormal = Normal2DF0D()
|
|
|
|
def shade(self, stroke):
|
|
it = Interface0DIterator(stroke)
|
|
for svert in it:
|
|
normal = self._getNormal(it)
|
|
a = self._a * (1 - 2 * (abs(svert.u - 0.5)))
|
|
n = normal * a * cos(self._f * svert.u * 6.28)
|
|
svert.point += n
|
|
stroke.update_length()
|
|
|
|
|
|
class pyPerlinNoise1DShader(StrokeShader):
|
|
"""
|
|
Displaces the stroke using the curvilinear abscissa. This means
|
|
that lines with the same length and sampling interval will be
|
|
identically distorded.
|
|
"""
|
|
|
|
def __init__(self, freq=10, amp=10, oct=4, seed=-1):
|
|
StrokeShader.__init__(self)
|
|
self.__noise = Noise(seed)
|
|
self.__freq = freq
|
|
self.__amp = amp
|
|
self.__oct = oct
|
|
|
|
def shade(self, stroke):
|
|
for svert in stroke:
|
|
s = svert.projected_x + svert.projected_y
|
|
nres = self.__noise.turbulence1(s, self.__freq, self.__amp, self.__oct)
|
|
svert.point = (svert.projected_x + nres, svert.projected_y + nres)
|
|
stroke.update_length()
|
|
|
|
|
|
class pyPerlinNoise2DShader(StrokeShader):
|
|
"""
|
|
Displaces the stroke using the strokes coordinates. This means
|
|
that in a scene no strokes will be distorded identically.
|
|
|
|
More information on the noise shaders can be found at:
|
|
freestyleintegration.wordpress.com/2011/09/25/development-updates-on-september-25/
|
|
"""
|
|
|
|
def __init__(self, freq=10, amp=10, oct=4, seed=-1):
|
|
StrokeShader.__init__(self)
|
|
self.__noise = Noise(seed)
|
|
self.__freq = freq
|
|
self.__amp = amp
|
|
self.__oct = oct
|
|
|
|
def shade(self, stroke):
|
|
for svert in stroke:
|
|
nres = self.__noise.turbulence2(svert.point_2d, self.__freq, self.__amp, self.__oct)
|
|
svert.point = (svert.projected_x + nres, svert.projected_y + nres)
|
|
stroke.update_length()
|
|
|
|
|
|
class pyBluePrintCirclesShader(StrokeShader):
|
|
"""
|
|
Draws the silhouette of the object as a circle.
|
|
"""
|
|
|
|
def __init__(self, turns=1, random_radius=3, random_center=5):
|
|
StrokeShader.__init__(self)
|
|
self.__turns = turns
|
|
self.__random_center = random_center
|
|
self.__random_radius = random_radius
|
|
|
|
def shade(self, stroke):
|
|
# get minimum and maximum coordinates
|
|
p_min, p_max = BoundingBox.from_sequence(svert.point for svert in stroke).corners
|
|
|
|
stroke.resample(32 * self.__turns)
|
|
sv_nb = len(stroke) // self.__turns
|
|
center = (p_min + p_max) / 2
|
|
radius = (center.x - p_min.x + center.y - p_min.y) / 2
|
|
R = self.__random_radius
|
|
C = self.__random_center
|
|
|
|
# The directions (and phases) are calculated using a separate
|
|
# function decorated with an lru-cache. This guarantees that
|
|
# the directions (involving sin and cos) are calculated as few
|
|
# times as possible.
|
|
#
|
|
# This works because the phases and directions are only
|
|
# dependent on the stroke length, and the chance that
|
|
# stroke.resample() above produces strokes of the same length
|
|
# is quite high.
|
|
#
|
|
# In tests, the amount of calls to sin() and cos() went from
|
|
# over 21000 to just 32 times, yielding a speedup of over 100%
|
|
directions = phase_to_direction(sv_nb)
|
|
|
|
it = iter(stroke)
|
|
|
|
for _j in range(self.__turns):
|
|
prev_radius = radius
|
|
prev_center = center
|
|
radius += randint(-R, R)
|
|
center += Vector((randint(-C, C), randint(-C, C)))
|
|
|
|
for (phase, direction), svert in zip(directions, it):
|
|
r = prev_radius + (radius - prev_radius) * phase
|
|
c = prev_center + (center - prev_center) * phase
|
|
svert.point = c + r * direction
|
|
|
|
if not it.is_end:
|
|
it.increment()
|
|
for sv in tuple(it):
|
|
stroke.remove_vertex(sv)
|
|
|
|
stroke.update_length()
|
|
|
|
|
|
class pyBluePrintEllipsesShader(StrokeShader):
|
|
def __init__(self, turns=1, random_radius=3, random_center=5):
|
|
StrokeShader.__init__(self)
|
|
self.__turns = turns
|
|
self.__random_center = random_center
|
|
self.__random_radius = random_radius
|
|
|
|
def shade(self, stroke):
|
|
p_min, p_max = BoundingBox.from_sequence(svert.point for svert in stroke).corners
|
|
|
|
stroke.resample(32 * self.__turns)
|
|
sv_nb = len(stroke) // self.__turns
|
|
|
|
center = (p_min + p_max) / 2
|
|
radius = center - p_min
|
|
|
|
R = self.__random_radius
|
|
C = self.__random_center
|
|
|
|
# for description of the line below, see pyBluePrintCirclesShader
|
|
directions = phase_to_direction(sv_nb)
|
|
it = iter(stroke)
|
|
for _j in range(self.__turns):
|
|
prev_radius = radius
|
|
prev_center = center
|
|
radius = radius + Vector((randint(-R, R), randint(-R, R)))
|
|
center = center + Vector((randint(-C, C), randint(-C, C)))
|
|
|
|
for (phase, direction), svert in zip(directions, it):
|
|
r = prev_radius + (radius - prev_radius) * phase
|
|
c = prev_center + (center - prev_center) * phase
|
|
svert.point = (c.x + r.x * direction.x, c.y + r.y * direction.y)
|
|
|
|
# remove excess vertices
|
|
if not it.is_end:
|
|
it.increment()
|
|
for sv in tuple(it):
|
|
stroke.remove_vertex(sv)
|
|
|
|
stroke.update_length()
|
|
|
|
|
|
class pyBluePrintSquaresShader(StrokeShader):
|
|
def __init__(self, turns=1, bb_len=10, bb_rand=0):
|
|
StrokeShader.__init__(self)
|
|
self.__turns = turns # does not have any effect atm
|
|
self.__bb_len = bb_len
|
|
self.__bb_rand = bb_rand
|
|
|
|
def shade(self, stroke):
|
|
# this condition will lead to errors later, end now
|
|
if len(stroke) < 1:
|
|
return
|
|
|
|
# get minimum and maximum coordinates
|
|
p_min, p_max = BoundingBox.from_sequence(svert.point for svert in stroke).corners
|
|
|
|
stroke.resample(32 * self.__turns)
|
|
num_segments = len(stroke) // self.__turns
|
|
f = num_segments // 4
|
|
# indices of the vertices that will form corners
|
|
first, second, third, fourth = (f, f * 2, f * 3, num_segments)
|
|
|
|
# construct points of the backbone
|
|
bb_len = self.__bb_len
|
|
points = (
|
|
Vector((p_min.x - bb_len, p_min.y)),
|
|
Vector((p_max.x + bb_len, p_min.y)),
|
|
Vector((p_max.x, p_min.y - bb_len)),
|
|
Vector((p_max.x, p_max.y + bb_len)),
|
|
Vector((p_max.x + bb_len, p_max.y)),
|
|
Vector((p_min.x - bb_len, p_max.y)),
|
|
Vector((p_min.x, p_max.y + bb_len)),
|
|
Vector((p_min.x, p_min.y - bb_len)),
|
|
)
|
|
|
|
# add randomization to the points (if needed)
|
|
if self.__bb_rand:
|
|
R, r = self.__bb_rand, self.__bb_rand // 2
|
|
|
|
randomization_mat = (
|
|
Vector((randint(-R, R), randint(-r, r))),
|
|
Vector((randint(-R, R), randint(-r, r))),
|
|
Vector((randint(-r, r), randint(-R, R))),
|
|
Vector((randint(-r, r), randint(-R, R))),
|
|
Vector((randint(-R, R), randint(-r, r))),
|
|
Vector((randint(-R, R), randint(-r, r))),
|
|
Vector((randint(-r, r), randint(-R, R))),
|
|
Vector((randint(-r, r), randint(-R, R))),
|
|
)
|
|
|
|
# combine both tuples
|
|
points = tuple(p + rand for (p, rand) in zip(points, randomization_mat))
|
|
|
|
# subtract even from uneven; result is length four tuple of vectors
|
|
it = iter(points)
|
|
old_vecs = tuple(next(it) - current for current in it)
|
|
|
|
it = iter(stroke)
|
|
verticesToRemove = list()
|
|
for _j in range(self.__turns):
|
|
for i, svert in zip(range(num_segments), it):
|
|
if i < first:
|
|
svert.point = points[0] + old_vecs[0] * i / (first - 1)
|
|
svert.attribute.visible = (i != first - 1)
|
|
elif i < second:
|
|
svert.point = points[2] + old_vecs[1] * (i - first) / (second - first - 1)
|
|
svert.attribute.visible = (i != second - 1)
|
|
elif i < third:
|
|
svert.point = points[4] + old_vecs[2] * (i - second) / (third - second - 1)
|
|
svert.attribute.visible = (i != third - 1)
|
|
elif i < fourth:
|
|
svert.point = points[6] + old_vecs[3] * (i - third) / (fourth - third - 1)
|
|
svert.attribute.visible = (i != fourth - 1)
|
|
else:
|
|
# special case; remove these vertices
|
|
verticesToRemove.append(svert)
|
|
|
|
# remove excess vertices (if any)
|
|
if not it.is_end:
|
|
it.increment()
|
|
verticesToRemove += [svert for svert in it]
|
|
for sv in verticesToRemove:
|
|
stroke.remove_vertex(sv)
|
|
stroke.update_length()
|
|
|
|
|
|
class pyBluePrintDirectedSquaresShader(StrokeShader):
|
|
"""
|
|
Replaces the stroke with a directed square.
|
|
"""
|
|
|
|
def __init__(self, turns=1, bb_len=10, mult=1):
|
|
StrokeShader.__init__(self)
|
|
self.__mult = mult
|
|
self.__turns = turns
|
|
self.__bb_len = 1 + bb_len * 0.01
|
|
|
|
def shade(self, stroke):
|
|
stroke.resample(32 * self.__turns)
|
|
n = len(stroke)
|
|
|
|
p_mean = (1 / n) * sum((svert.point for svert in stroke), Vector((0.0, 0.0)))
|
|
p_var = Vector((0, 0))
|
|
p_var_xy = 0.0
|
|
for d in (svert.point - p_mean for svert in stroke):
|
|
p_var += Vector((d.x ** 2, d.y ** 2))
|
|
p_var_xy += d.x * d.y
|
|
|
|
# divide by number of vertices
|
|
p_var /= n
|
|
p_var_xy /= n
|
|
trace = p_var.x + p_var.y
|
|
det = p_var.x * p_var.y - pow(p_var_xy, 2)
|
|
|
|
sqrt_coeff = sqrt(trace * trace - 4 * det)
|
|
lambda1, lambda2 = (trace + sqrt_coeff) / 2, (trace - sqrt_coeff) / 2
|
|
# make sure those numbers aren't to small, if they are, rooting them will yield complex numbers
|
|
lambda1, lambda2 = max(1e-12, lambda1), max(1e-12, lambda2)
|
|
theta = atan(2 * p_var_xy / (p_var.x - p_var.y)) / 2
|
|
|
|
# Keep alignment for readability.
|
|
# autopep8: off
|
|
if p_var.y > p_var.x:
|
|
e1 = Vector((cos(theta + pi / 2), sin(theta + pi / 2))) * sqrt(lambda1) * self.__mult
|
|
e2 = Vector((cos(theta + pi ), sin(theta + pi ))) * sqrt(lambda2) * self.__mult
|
|
else:
|
|
e1 = Vector((cos(theta), sin(theta))) * sqrt(lambda1) * self.__mult
|
|
e2 = Vector((cos(theta + pi / 2), sin(theta + pi / 2))) * sqrt(lambda2) * self.__mult
|
|
# autopep8: on
|
|
|
|
# partition the stroke
|
|
num_segments = len(stroke) // self.__turns
|
|
f = num_segments // 4
|
|
# indices of the vertices that will form corners
|
|
first, second, third, fourth = (f, f * 2, f * 3, num_segments)
|
|
|
|
bb_len1 = self.__bb_len
|
|
bb_len2 = 1 + (bb_len1 - 1) * sqrt(lambda1 / lambda2)
|
|
points = (
|
|
p_mean - e1 - e2 * bb_len2,
|
|
p_mean - e1 * bb_len1 + e2,
|
|
p_mean + e1 + e2 * bb_len2,
|
|
p_mean + e1 * bb_len1 - e2,
|
|
)
|
|
|
|
old_vecs = (
|
|
e2 * bb_len2 * 2,
|
|
e1 * bb_len1 * 2,
|
|
-e2 * bb_len2 * 2,
|
|
-e1 * bb_len1 * 2,
|
|
)
|
|
|
|
it = iter(stroke)
|
|
verticesToRemove = list()
|
|
for _j in range(self.__turns):
|
|
for i, svert in zip(range(num_segments), it):
|
|
if i < first:
|
|
svert.point = points[0] + old_vecs[0] * i / (first - 1)
|
|
svert.attribute.visible = (i != first - 1)
|
|
elif i < second:
|
|
svert.point = points[1] + old_vecs[1] * (i - first) / (second - first - 1)
|
|
svert.attribute.visible = (i != second - 1)
|
|
elif i < third:
|
|
svert.point = points[2] + old_vecs[2] * (i - second) / (third - second - 1)
|
|
svert.attribute.visible = (i != third - 1)
|
|
elif i < fourth:
|
|
svert.point = points[3] + old_vecs[3] * (i - third) / (fourth - third - 1)
|
|
svert.attribute.visible = (i != fourth - 1)
|
|
else:
|
|
# special case; remove these vertices
|
|
verticesToRemove.append(svert)
|
|
|
|
# remove excess vertices
|
|
if not it.is_end:
|
|
it.increment()
|
|
verticesToRemove += [svert for svert in it]
|
|
for sv in verticesToRemove:
|
|
stroke.remove_vertex(sv)
|
|
stroke.update_length()
|
|
|
|
|
|
# -- various (used in the parameter editor) -- #
|
|
|
|
|
|
def iter_stroke_vertices(stroke, epsilon=1e-6):
|
|
yield stroke[0]
|
|
for prev, svert in pairwise(stroke):
|
|
if (prev.point - svert.point).length > epsilon:
|
|
yield svert
|
|
|
|
|
|
class RoundCapShader(StrokeShader):
|
|
def round_cap_thickness(self, x):
|
|
x = max(0.0, min(x, 1.0))
|
|
return pow(1.0 - (x ** 2.0), 0.5)
|
|
|
|
def shade(self, stroke):
|
|
# save the location and attribute of stroke vertices
|
|
buffer = tuple((Vector(sv.point), StrokeAttribute(sv.attribute))
|
|
for sv in iter_stroke_vertices(stroke))
|
|
nverts = len(buffer)
|
|
if nverts < 2:
|
|
return
|
|
# calculate the number of additional vertices to form caps
|
|
thickness_beg = sum(stroke[0].attribute.thickness)
|
|
nverts_beg = max(5, int(thickness_beg))
|
|
|
|
thickness_end = sum(stroke[-1].attribute.thickness)
|
|
nverts_end = max(5, int(thickness_end))
|
|
|
|
# adjust the total number of stroke vertices
|
|
stroke.resample(nverts + nverts_beg + nverts_end)
|
|
# restore the location and attribute of the original vertices
|
|
for i, (p, attr) in enumerate(buffer):
|
|
stroke[nverts_beg + i].point = p
|
|
stroke[nverts_beg + i].attribute = attr
|
|
# reshape the cap at the beginning of the stroke
|
|
q, attr = buffer[1]
|
|
p, attr = buffer[0]
|
|
direction = (p - q).normalized() * thickness_beg
|
|
n = 1.0 / nverts_beg
|
|
R, L = attr.thickness
|
|
for t, svert in zip(range(nverts_beg, 0, -1), stroke):
|
|
r = self.round_cap_thickness((t + 1) * n)
|
|
svert.point = p + direction * t * n
|
|
svert.attribute = attr
|
|
svert.attribute.thickness = (R * r, L * r)
|
|
# reshape the cap at the end of the stroke
|
|
q, attr = buffer[-2]
|
|
p, attr = buffer[-1]
|
|
direction = (p - q).normalized() * thickness_end
|
|
n = 1.0 / nverts_end
|
|
R, L = attr.thickness
|
|
for t, svert in zip(range(nverts_end, 0, -1), reversed(stroke)):
|
|
r = self.round_cap_thickness((t + 1) * n)
|
|
svert.point = p + direction * t * n
|
|
svert.attribute = attr
|
|
svert.attribute.thickness = (R * r, L * r)
|
|
# update the curvilinear 2D length of each vertex
|
|
stroke.update_length()
|
|
|
|
|
|
class SquareCapShader(StrokeShader):
|
|
def shade(self, stroke):
|
|
# save the location and attribute of stroke vertices
|
|
buffer = tuple((Vector(sv.point), StrokeAttribute(sv.attribute))
|
|
for sv in iter_stroke_vertices(stroke))
|
|
nverts = len(buffer)
|
|
if nverts < 2:
|
|
return
|
|
# calculate the number of additional vertices to form caps
|
|
caplen_beg = sum(stroke[0].attribute.thickness) / 2.0
|
|
nverts_beg = 1
|
|
|
|
caplen_end = sum(stroke[-1].attribute.thickness) / 2.0
|
|
nverts_end = 1
|
|
# adjust the total number of stroke vertices
|
|
stroke.resample(nverts + nverts_beg + nverts_end)
|
|
# restore the location and attribute of the original vertices
|
|
for i, (p, attr) in zip(range(nverts), buffer):
|
|
stroke[nverts_beg + i].point = p
|
|
stroke[nverts_beg + i].attribute = attr
|
|
# reshape the cap at the beginning of the stroke
|
|
q, attr = buffer[1]
|
|
p, attr = buffer[0]
|
|
stroke[0].point += (p - q).normalized() * caplen_beg
|
|
stroke[0].attribute = attr
|
|
# reshape the cap at the end of the stroke
|
|
q, attr = buffer[-2]
|
|
p, attr = buffer[-1]
|
|
stroke[-1].point += (p - q).normalized() * caplen_end
|
|
stroke[-1].attribute = attr
|
|
# update the curvilinear 2D length of each vertex
|
|
stroke.update_length()
|