diff options
Diffstat (limited to 'release/scripts/freestyle/style_modules/parameter_editor.py')
-rw-r--r-- | release/scripts/freestyle/style_modules/parameter_editor.py | 1301 |
1 files changed, 1301 insertions, 0 deletions
diff --git a/release/scripts/freestyle/style_modules/parameter_editor.py b/release/scripts/freestyle/style_modules/parameter_editor.py new file mode 100644 index 00000000000..75a948b8623 --- /dev/null +++ b/release/scripts/freestyle/style_modules/parameter_editor.py @@ -0,0 +1,1301 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +import Freestyle +import math +import mathutils +import time + +from freestyle_init import * +from logical_operators import * +from ChainingIterators import * +from shaders import * + +class ColorRampModifier(StrokeShader): + def __init__(self, blend, influence, ramp): + StrokeShader.__init__(self) + self.__blend = blend + self.__influence = influence + self.__ramp = ramp + def evaluate(self, t): + col = Freestyle.evaluateColorRamp(self.__ramp, t) + col = col.xyz # omit alpha + return col + def blend_ramp(self, a, b): + return Freestyle.blendRamp(self.__blend, a, self.__influence, b) + +class ScalarBlendModifier(StrokeShader): + def __init__(self, blend, influence): + StrokeShader.__init__(self) + self.__blend = blend + self.__influence = influence + def blend(self, v1, v2): + fac = self.__influence + facm = 1.0 - fac + if self.__blend == "MIX": + v1 = facm * v1 + fac * v2 + elif self.__blend == "ADD": + v1 += fac * v2 + elif self.__blend == "MULTIPLY": + v1 *= facm + fac * v2; + elif self.__blend == "SUBTRACT": + v1 -= fac * v2 + elif self.__blend == "DIVIDE": + if v2 != 0.0: + v1 = facm * v1 + fac * v1 / v2 + elif self.__blend == "DIFFERENCE": + v1 = facm * v1 + fac * abs(v1 - v2) + elif self.__blend == "MININUM": + tmp = fac * v1 + if v1 > tmp: + v1 = tmp + elif self.__blend == "MAXIMUM": + tmp = fac * v1 + if v1 < tmp: + v1 = tmp + else: + raise ValueError("unknown curve blend type: " + self.__blend) + return v1 + +class CurveMappingModifier(ScalarBlendModifier): + def __init__(self, blend, influence, mapping, invert, curve): + ScalarBlendModifier.__init__(self, blend, influence) + assert mapping in ("LINEAR", "CURVE") + self.__mapping = getattr(self, mapping) + self.__invert = invert + self.__curve = curve + def LINEAR(self, t): + if self.__invert: + return 1.0 - t + return t + def CURVE(self, t): + return Freestyle.evaluateCurveMappingF(self.__curve, 0, t) + def evaluate(self, t): + return self.__mapping(t) + +class ThicknessModifierMixIn: + def __init__(self): + scene = Freestyle.getCurrentScene() + self.__persp_camera = (scene.camera.data.type == "PERSP") + def set_thickness(self, sv, outer, inner): + fe = sv.first_svertex.get_fedge(sv.second_svertex) + nature = fe.nature + if (nature & Nature.BORDER): + if self.__persp_camera: + point = -sv.point_3d.copy() + point.normalize() + 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 (nature & Nature.SILHOUETTE): + if fe.is_smooth: # TODO more tests needed + outer, inner = inner, outer + else: + outer = inner = (outer + inner) / 2 + sv.attribute.thickness = (outer, inner) + +class ThicknessBlenderMixIn(ThicknessModifierMixIn): + def __init__(self, position, ratio): + ThicknessModifierMixIn.__init__(self) + self.__position = position + self.__ratio = ratio + def blend_thickness(self, outer, inner, v): + if self.__position == "CENTER": + outer = self.blend(outer, v / 2) + inner = self.blend(inner, v / 2) + elif self.__position == "INSIDE": + outer = self.blend(outer, 0) + inner = self.blend(inner, v) + elif self.__position == "OUTSIDE": + outer = self.blend(outer, v) + inner = self.blend(inner, 0) + elif self.__position == "RELATIVE": + outer = self.blend(outer, v * self.__ratio) + inner = self.blend(inner, v * (1 - self.__ratio)) + else: + raise ValueError("unknown thickness position: " + self.__position) + return outer, inner + +class BaseColorShader(ConstantColorShader): + def getName(self): + return "BaseColorShader" + +class BaseThicknessShader(StrokeShader, ThicknessModifierMixIn): + def __init__(self, thickness, position, ratio): + StrokeShader.__init__(self) + ThicknessModifierMixIn.__init__(self) + if position == "CENTER": + self.__outer = thickness / 2 + self.__inner = thickness / 2 + elif position == "INSIDE": + self.__outer = 0 + self.__inner = thickness + elif position == "OUTSIDE": + self.__outer = thickness + self.__inner = 0 + elif position == "RELATIVE": + self.__outer = thickness * ratio + self.__inner = thickness * (1 - ratio) + else: + raise ValueError("unknown thickness position: " + self.position) + def getName(self): + return "BaseThicknessShader" + def shade(self, stroke): + it = stroke.stroke_vertices_begin() + while not it.is_end: + sv = it.object + self.set_thickness(sv, self.__outer, self.__inner) + it.increment() + +# Along Stroke modifiers + +def iter_t2d_along_stroke(stroke): + total = stroke.length_2d + distance = 0.0 + it = stroke.stroke_vertices_begin() + while not it.is_end: + p = it.object.point + if not it.is_begin: + distance += (prev - p).length + prev = p + t = min(distance / total, 1.0) + yield it, t + it.increment() + +class ColorAlongStrokeShader(ColorRampModifier): + def getName(self): + return "ColorAlongStrokeShader" + def shade(self, stroke): + for it, t in iter_t2d_along_stroke(stroke): + sv = it.object + a = sv.attribute.color + b = self.evaluate(t) + sv.attribute.color = self.blend_ramp(a, b) + +class AlphaAlongStrokeShader(CurveMappingModifier): + def getName(self): + return "AlphaAlongStrokeShader" + def shade(self, stroke): + for it, t in iter_t2d_along_stroke(stroke): + sv = it.object + a = sv.attribute.alpha + b = self.evaluate(t) + sv.attribute.alpha = self.blend(a, b) + +class ThicknessAlongStrokeShader(ThicknessBlenderMixIn, CurveMappingModifier): + def __init__(self, thickness_position, thickness_ratio, + 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_min = value_min + self.__value_max = value_max + def getName(self): + return "ThicknessAlongStrokeShader" + def shade(self, stroke): + for it, t in iter_t2d_along_stroke(stroke): + sv = it.object + a = sv.attribute.thickness + b = self.__value_min + self.evaluate(t) * (self.__value_max - self.__value_min) + c = self.blend_thickness(a[0], a[1], b) + self.set_thickness(sv, c[0], c[1]) + +# Distance from Camera modifiers + +def iter_distance_from_camera(stroke, range_min, range_max): + normfac = range_max - range_min # normalization factor + it = stroke.stroke_vertices_begin() + while not it.is_end: + p = it.object.point_3d # in the camera coordinate + distance = p.length + if distance < range_min: + t = 0.0 + elif distance > range_max: + t = 1.0 + else: + t = (distance - range_min) / normfac + yield it, t + it.increment() + +class ColorDistanceFromCameraShader(ColorRampModifier): + def __init__(self, blend, influence, ramp, range_min, range_max): + ColorRampModifier.__init__(self, blend, influence, ramp) + self.__range_min = range_min + self.__range_max = range_max + def getName(self): + return "ColorDistanceFromCameraShader" + def shade(self, stroke): + for it, t in iter_distance_from_camera(stroke, self.__range_min, self.__range_max): + sv = it.object + a = sv.attribute.color + b = self.evaluate(t) + sv.attribute.color = self.blend_ramp(a, b) + +class AlphaDistanceFromCameraShader(CurveMappingModifier): + def __init__(self, blend, influence, mapping, invert, curve, range_min, range_max): + CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve) + self.__range_min = range_min + self.__range_max = range_max + def getName(self): + return "AlphaDistanceFromCameraShader" + def shade(self, stroke): + for it, t in iter_distance_from_camera(stroke, self.__range_min, self.__range_max): + sv = it.object + a = sv.attribute.alpha + b = self.evaluate(t) + sv.attribute.alpha = self.blend(a, b) + +class ThicknessDistanceFromCameraShader(ThicknessBlenderMixIn, CurveMappingModifier): + def __init__(self, thickness_position, thickness_ratio, + 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_min = range_min + self.__range_max = range_max + self.__value_min = value_min + self.__value_max = value_max + def getName(self): + return "ThicknessDistanceFromCameraShader" + def shade(self, stroke): + for it, t in iter_distance_from_camera(stroke, self.__range_min, self.__range_max): + sv = it.object + a = sv.attribute.thickness + b = self.__value_min + self.evaluate(t) * (self.__value_max - self.__value_min) + c = self.blend_thickness(a[0], a[1], b) + self.set_thickness(sv, c[0], c[1]) + +# Distance from Object modifiers + +def iter_distance_from_object(stroke, object, range_min, range_max): + scene = Freestyle.getCurrentScene() + mv = scene.camera.matrix_world.copy() # model-view matrix + mv.invert() + loc = mv * object.location # loc in the camera coordinate + normfac = range_max - range_min # normalization factor + it = stroke.stroke_vertices_begin() + while not it.is_end: + p = it.object.point_3d # in the camera coordinate + distance = (p - loc).length + if distance < range_min: + t = 0.0 + elif distance > range_max: + t = 1.0 + else: + t = (distance - range_min) / normfac + yield it, t + it.increment() + +class ColorDistanceFromObjectShader(ColorRampModifier): + def __init__(self, blend, influence, ramp, target, range_min, range_max): + ColorRampModifier.__init__(self, blend, influence, ramp) + self.__target = target + self.__range_min = range_min + self.__range_max = range_max + def getName(self): + return "ColorDistanceFromObjectShader" + def shade(self, stroke): + if self.__target is None: + return + for it, t in iter_distance_from_object(stroke, self.__target, self.__range_min, self.__range_max): + sv = it.object + a = sv.attribute.color + b = self.evaluate(t) + sv.attribute.color = self.blend_ramp(a, b) + +class AlphaDistanceFromObjectShader(CurveMappingModifier): + def __init__(self, blend, influence, mapping, invert, curve, target, range_min, range_max): + CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve) + self.__target = target + self.__range_min = range_min + self.__range_max = range_max + def getName(self): + return "AlphaDistanceFromObjectShader" + def shade(self, stroke): + if self.__target is None: + return + for it, t in iter_distance_from_object(stroke, self.__target, self.__range_min, self.__range_max): + sv = it.object + a = sv.attribute.alpha + b = self.evaluate(t) + sv.attribute.alpha = self.blend(a, b) + +class ThicknessDistanceFromObjectShader(ThicknessBlenderMixIn, CurveMappingModifier): + def __init__(self, thickness_position, thickness_ratio, + blend, influence, mapping, invert, curve, target, range_min, range_max, value_min, value_max): + ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio) + CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve) + self.__target = target + self.__range_min = range_min + self.__range_max = range_max + self.__value_min = value_min + self.__value_max = value_max + def getName(self): + return "ThicknessDistanceFromObjectShader" + def shade(self, stroke): + if self.__target is None: + return + for it, t in iter_distance_from_object(stroke, self.__target, self.__range_min, self.__range_max): + sv = it.object + a = sv.attribute.thickness + b = self.__value_min + self.evaluate(t) * (self.__value_max - self.__value_min) + c = self.blend_thickness(a[0], a[1], b) + self.set_thickness(sv, c[0], c[1]) + +# Material modifiers + +def iter_material_color(stroke, material_attr): + func = CurveMaterialF0D() + it = stroke.stroke_vertices_begin() + while not it.is_end: + material = func(it.cast_to_interface0diterator()) + if material_attr == "DIFF": + color = (material.diffuse[0], + material.diffuse[1], + material.diffuse[2]) + elif material_attr == "SPEC": + color = (material.specular[0], + material.specular[1], + material.specular[2]) + else: + raise ValueError("unexpected material attribute: " + material_attr) + yield it, color + it.increment() + +def iter_material_value(stroke, material_attr): + func = CurveMaterialF0D() + it = stroke.stroke_vertices_begin() + while not it.is_end: + material = func(it.cast_to_interface0diterator()) + if material_attr == "DIFF": + r = material.diffuse[0] + g = material.diffuse[1] + b = material.diffuse[2] + t = 0.35 * r + 0.45 * r + 0.2 * b + elif material_attr == "DIFF_R": + t = material.diffuse[0] + elif material_attr == "DIFF_G": + t = material.diffuse[1] + elif material_attr == "DIFF_B": + t = material.diffuse[2] + elif material_attr == "SPEC": + r = material.specular[0] + g = material.specular[1] + b = material.specular[2] + t = 0.35 * r + 0.45 * r + 0.2 * b + elif material_attr == "SPEC_R": + t = material.specular[0] + elif material_attr == "SPEC_G": + t = material.specular[1] + elif material_attr == "SPEC_B": + t = material.specular[2] + elif material_attr == "SPEC_HARDNESS": + t = material.shininess + elif material_attr == "ALPHA": + t = material.diffuse[3] + else: + raise ValueError("unexpected material attribute: " + material_attr) + yield it, t + it.increment() + +class ColorMaterialShader(ColorRampModifier): + def __init__(self, blend, influence, ramp, material_attr, use_ramp): + ColorRampModifier.__init__(self, blend, influence, ramp) + self.__material_attr = material_attr + self.__use_ramp = use_ramp + def getName(self): + return "ColorMaterialShader" + def shade(self, stroke): + if self.__material_attr in ["DIFF", "SPEC"] and not self.__use_ramp: + for it, b in iter_material_color(stroke, self.__material_attr): + sv = it.object + a = sv.attribute.color + sv.attribute.color = self.blend_ramp(a, b) + else: + for it, t in iter_material_value(stroke, self.__material_attr): + sv = it.object + a = sv.attribute.color + b = self.evaluate(t) + sv.attribute.color = self.blend_ramp(a, b) + +class AlphaMaterialShader(CurveMappingModifier): + def __init__(self, blend, influence, mapping, invert, curve, material_attr): + CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve) + self.__material_attr = material_attr + def getName(self): + return "AlphaMaterialShader" + def shade(self, stroke): + for it, t in iter_material_value(stroke, self.__material_attr): + sv = it.object + a = sv.attribute.alpha + b = self.evaluate(t) + sv.attribute.alpha = self.blend(a, b) + +class ThicknessMaterialShader(ThicknessBlenderMixIn, CurveMappingModifier): + def __init__(self, thickness_position, thickness_ratio, + blend, influence, mapping, invert, curve, material_attr, value_min, value_max): + ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio) + CurveMappingModifier.__init__(self, blend, influence, mapping, invert, curve) + self.__material_attr = material_attr + self.__value_min = value_min + self.__value_max = value_max + def getName(self): + return "ThicknessMaterialShader" + def shade(self, stroke): + for it, t in iter_material_value(stroke, self.__material_attr): + sv = it.object + a = sv.attribute.thickness + b = self.__value_min + self.evaluate(t) * (self.__value_max - self.__value_min) + c = self.blend_thickness(a[0], a[1], b) + self.set_thickness(sv, c[0], c[1]) + +# Calligraphic thickness modifier + +class CalligraphicThicknessShader(ThicknessBlenderMixIn, ScalarBlendModifier): + def __init__(self, thickness_position, thickness_ratio, + blend, influence, orientation, min_thickness, max_thickness): + ThicknessBlenderMixIn.__init__(self, thickness_position, thickness_ratio) + ScalarBlendModifier.__init__(self, blend, influence) + self.__orientation = mathutils.Vector((math.cos(orientation), math.sin(orientation))) + self.__min_thickness = min_thickness + self.__max_thickness = max_thickness + def shade(self, stroke): + func = VertexOrientation2DF0D() + it = stroke.stroke_vertices_begin() + while not it.is_end: + dir = func(it.cast_to_interface0diterator()) + orthDir = mathutils.Vector((-dir.y, dir.x)) + orthDir.normalize() + fac = abs(orthDir * self.__orientation) + sv = it.object + a = sv.attribute.thickness + b = self.__min_thickness + fac * (self.__max_thickness - self.__min_thickness) + b = max(b, 0.0) + c = self.blend_thickness(a[0], a[1], b) + self.set_thickness(sv, c[0], c[1]) + it.increment() + +# Geometry modifiers + +def iter_distance_along_stroke(stroke): + distance = 0.0 + it = stroke.stroke_vertices_begin() + while not it.is_end: + p = it.object.point + if not it.is_begin: + distance += (prev - p).length + prev = p + yield it, distance + it.increment() + +class SinusDisplacementShader(StrokeShader): + def __init__(self, wavelength, amplitude, phase): + StrokeShader.__init__(self) + self._wavelength = wavelength + self._amplitude = amplitude + self._phase = phase / wavelength * 2 * math.pi + self._getNormal = Normal2DF0D() + def getName(self): + return "SinusDisplacementShader" + def shade(self, stroke): + for it, distance in iter_distance_along_stroke(stroke): + v = it.object + n = self._getNormal(it.cast_to_interface0diterator()) + n = n * self._amplitude * math.cos(distance / self._wavelength * 2 * math.pi + self._phase) + v.point = v.point + n + stroke.update_length() + +class PerlinNoise1DShader(StrokeShader): + def __init__(self, freq = 10, amp = 10, oct = 4, angle = math.radians(45), seed = -1): + StrokeShader.__init__(self) + self.__noise = Noise(seed) + self.__freq = freq + self.__amp = amp + self.__oct = oct + self.__dir = Vector([cos(angle), sin(angle)]) + def getName(self): + return "PerlinNoise1DShader" + def shade(self, stroke): + length = stroke.length_2d + it = stroke.stroke_vertices_begin() + while not it.is_end: + v = it.object + nres = self.__noise.turbulence1(length * v.u, self.__freq, self.__amp, self.__oct) + v.point = v.point + nres * self.__dir + it.increment() + stroke.update_length() + +class PerlinNoise2DShader(StrokeShader): + def __init__(self, freq = 10, amp = 10, oct = 4, angle = math.radians(45), seed = -1): + StrokeShader.__init__(self) + self.__noise = Noise(seed) + self.__freq = freq + self.__amp = amp + self.__oct = oct + self.__dir = Vector([cos(angle), sin(angle)]) + def getName(self): + return "PerlinNoise2DShader" + def shade(self, stroke): + it = stroke.stroke_vertices_begin() + while not it.is_end: + v = it.object + vec = Vector([v.projected_x, v.projected_y]) + nres = self.__noise.turbulence2(vec, self.__freq, self.__amp, self.__oct) + v.point = v.point + nres * self.__dir + it.increment() + stroke.update_length() + +class Offset2DShader(StrokeShader): + def __init__(self, start, end, x, y): + StrokeShader.__init__(self) + self.__start = start + self.__end = end + self.__xy = Vector([x, y]) + self.__getNormal = Normal2DF0D() + def getName(self): + return "Offset2DShader" + def shade(self, stroke): + it = stroke.stroke_vertices_begin() + while not it.is_end: + v = it.object + u = v.u + a = self.__start + u * (self.__end - self.__start) + n = self.__getNormal(it.cast_to_interface0diterator()) + n = n * a + v.point = v.point + n + self.__xy + it.increment() + stroke.update_length() + +class Transform2DShader(StrokeShader): + def __init__(self, pivot, scale_x, scale_y, angle, pivot_u, pivot_x, pivot_y): + StrokeShader.__init__(self) + self.__pivot = pivot + self.__scale_x = scale_x + self.__scale_y = scale_y + self.__angle = angle + self.__pivot_u = pivot_u + self.__pivot_x = pivot_x + self.__pivot_y = pivot_y + def getName(self): + return "Transform2DShader" + def shade(self, stroke): + # determine the pivot of scaling and rotation operations + if self.__pivot == "START": + it = stroke.stroke_vertices_begin() + pivot = it.object.point + elif self.__pivot == "END": + it = stroke.stroke_vertices_end() + it.decrement() + pivot = it.object.point + elif self.__pivot == "PARAM": + p = None + it = stroke.stroke_vertices_begin() + while not it.is_end: + prev = p + v = it.object + p = v.point + u = v.u + if self.__pivot_u < u: + break + it.increment() + if prev is None: + pivot = p + else: + delta = u - self.__pivot_u + pivot = p + delta * (prev - p) + elif self.__pivot == "CENTER": + pivot = Vector([0.0, 0.0]) + n = 0 + it = stroke.stroke_vertices_begin() + while not it.is_end: + p = it.object.point + pivot = pivot + p + n = n + 1 + it.increment() + pivot.x = pivot.x / n + pivot.y = pivot.y / n + elif self.__pivot == "ABSOLUTE": + pivot = Vector([self.__pivot_x, self.__pivot_y]) + # apply scaling and rotation operations + cos_theta = math.cos(self.__angle) + sin_theta = math.sin(self.__angle) + it = stroke.stroke_vertices_begin() + while not it.is_end: + v = it.object + p = v.point + p = p - pivot + x = p.x * self.__scale_x + y = p.y * self.__scale_y + p.x = x * cos_theta - y * sin_theta + p.y = x * sin_theta + y * cos_theta + v.point = p + pivot + it.increment() + stroke.update_length() + +# Predicates and helper functions + +class QuantitativeInvisibilityRangeUP1D(UnaryPredicate1D): + def __init__(self, qi_start, qi_end): + UnaryPredicate1D.__init__(self) + self.__getQI = QuantitativeInvisibilityF1D() + self.__qi_start = qi_start + self.__qi_end = qi_end + def getName(self): + return "QuantitativeInvisibilityRangeUP1D" + def __call__(self, inter): + qi = self.__getQI(inter) + return self.__qi_start <= qi <= self.__qi_end + +def join_unary_predicates(upred_list, bpred): + if not upred_list: + return None + upred = upred_list[0] + for p in upred_list[1:]: + upred = bpred(upred, p) + return upred + +class ObjectNamesUP1D(UnaryPredicate1D): + def __init__(self, names, negative): + UnaryPredicate1D.__init__(self) + self._names = names + self._negative = negative + def getName(self): + return "ObjectNamesUP1D" + def __call__(self, viewEdge): + found = viewEdge.viewshape.name in self._names + if self._negative: + return not found + return found + +# Stroke caps + +def iter_stroke_vertices(stroke): + it = stroke.stroke_vertices_begin() + prev_p = None + while not it.is_end: + sv = it.object + p = sv.point + if prev_p is None or (prev_p - p).length > 1e-6: + yield sv + prev_p = p + it.increment() + +class RoundCapShader(StrokeShader): + def round_cap_thickness(self, x): + x = max(0.0, min(x, 1.0)) + return math.sqrt(1.0 - (x ** 2)) + def shade(self, stroke): + # save the location and attribute of stroke vertices + buffer = [] + for sv in iter_stroke_vertices(stroke): + buffer.append((Vector(sv.point), StrokeAttribute(sv.attribute))) + nverts = len(buffer) + if nverts < 2: + return + # calculate the number of additional vertices to form caps + R, L = stroke[0].attribute.thickness + caplen_beg = (R + L) / 2.0 + nverts_beg = max(5, int(R + L)) + R, L = stroke[-1].attribute.thickness + caplen_end = (R + L) / 2.0 + nverts_end = max(5, int(R + L)) + # 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 in range(nverts): + p, attr = buffer[i] + 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] + d = p - q + d = d / d.length * caplen_beg + n = 1.0 / nverts_beg + R, L = attr.thickness + for i in range(nverts_beg): + t = (nverts_beg - i) * n + stroke[i].point = p + d * t + r = self.round_cap_thickness((nverts_beg - i + 1) * n) + stroke[i].attribute = attr + stroke[i].attribute.thickness = (R * r, L * r) + # reshape the cap at the end of the stroke + q, attr = buffer[-2] + p, attr = buffer[-1] + d = p - q + d = d / d.length * caplen_end + n = 1.0 / nverts_end + R, L = attr.thickness + for i in range(nverts_end): + t = (nverts_end - i) * n + stroke[-i-1].point = p + d * t + r = self.round_cap_thickness((nverts_end - i + 1) * n) + stroke[-i-1].attribute = attr + stroke[-i-1].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 = [] + for sv in iter_stroke_vertices(stroke): + buffer.append((Vector(sv.point), StrokeAttribute(sv.attribute))) + nverts = len(buffer) + if nverts < 2: + return + # calculate the number of additional vertices to form caps + R, L = stroke[0].attribute.thickness + caplen_beg = (R + L) / 2.0 + nverts_beg = 1 + R, L = stroke[-1].attribute.thickness + caplen_end = (R + L) / 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 in range(nverts): + p, attr = buffer[i] + 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] + d = p - q + stroke[0].point = p + d / d.length * caplen_beg + stroke[0].attribute = attr + # reshape the cap at the end of the stroke + q, attr = buffer[-2] + p, attr = buffer[-1] + d = p - q + stroke[-1].point = p + d / d.length * caplen_beg + stroke[-1].attribute = attr + # update the curvilinear 2D length of each vertex + stroke.update_length() + +# Split by dashed line pattern + +class SplitPatternStartingUP0D(UnaryPredicate0D): + def __init__(self, controller): + UnaryPredicate0D.__init__(self) + self._controller = controller + def __call__(self, inter): + return self._controller.start() + +class SplitPatternStoppingUP0D(UnaryPredicate0D): + def __init__(self, controller): + UnaryPredicate0D.__init__(self) + self._controller = controller + def __call__(self, inter): + return self._controller.stop() + +class SplitPatternController: + def __init__(self, pattern, sampling): + self.sampling = float(sampling) + k = len(pattern) // 2 + n = k * 2 + self.start_pos = [pattern[i] + pattern[i+1] for i in range(0, n, 2)] + self.stop_pos = [pattern[i] for i in range(0, n, 2)] + self.init() + def init(self): + self.start_len = 0.0 + self.start_idx = 0 + self.stop_len = self.sampling + self.stop_idx = 0 + def start(self): + self.start_len += self.sampling + if abs(self.start_len - self.start_pos[self.start_idx]) < self.sampling / 2.0: + self.start_len = 0.0 + self.start_idx = (self.start_idx + 1) % len(self.start_pos) + return True + return False + def stop(self): + if self.start_len > 0.0: + self.init() + self.stop_len += self.sampling + if abs(self.stop_len - self.stop_pos[self.stop_idx]) < self.sampling / 2.0: + self.stop_len = self.sampling + self.stop_idx = (self.stop_idx + 1) % len(self.stop_pos) + return True + return False + +# Dashed line + +class DashedLineShader(StrokeShader): + def __init__(self, pattern): + StrokeShader.__init__(self) + self._pattern = pattern + def getName(self): + return "DashedLineShader" + def shade(self, stroke): + index = 0 # pattern index + start = 0.0 # 2D curvilinear length + visible = True + sampling = 1.0 + it = stroke.stroke_vertices_begin(sampling) + while not it.is_end: + pos = it.t # curvilinear abscissa + # The extra 'sampling' term is added below, because the + # visibility attribute of the i-th vertex refers to the + # visibility of the stroke segment between the i-th and + # (i+1)-th vertices. + if pos - start + sampling > self._pattern[index]: + start = pos + index += 1 + if index == len(self._pattern): + index = 0 + visible = not visible + it.object.attribute.visible = visible + it.increment() + +# predicates for chaining + +class AngleLargerThanBP1D(BinaryPredicate1D): + def __init__(self, angle): + BinaryPredicate1D.__init__(self) + self._angle = angle + def getName(self): + return "AngleLargerThanBP1D" + def __call__(self, i1, i2): + sv1a = i1.first_fedge.first_svertex.point_2d + sv1b = i1.last_fedge.second_svertex.point_2d + sv2a = i2.first_fedge.first_svertex.point_2d + sv2b = i2.last_fedge.second_svertex.point_2d + if (sv1a - sv2a).length < 1e-6: + dir1 = sv1a - sv1b + dir2 = sv2b - sv2a + elif (sv1b - sv2b).length < 1e-6: + dir1 = sv1b - sv1a + dir2 = sv2a - sv2b + elif (sv1a - sv2b).length < 1e-6: + dir1 = sv1a - sv1b + dir2 = sv2a - sv2b + elif (sv1b - sv2a).length < 1e-6: + dir1 = sv1b - sv1a + dir2 = sv2b - sv2a + else: + return False + denom = dir1.length * dir2.length + if denom < 1e-6: + return False + x = (dir1 * dir2) / denom + return math.acos(min(max(x, -1.0), 1.0)) > self._angle + +class AndBP1D(BinaryPredicate1D): + def __init__(self, pred1, pred2): + BinaryPredicate1D.__init__(self) + self.__pred1 = pred1 + self.__pred2 = pred2 + def getName(self): + return "AndBP1D" + def __call__(self, i1, i2): + return self.__pred1(i1, i2) and self.__pred2(i1, i2) + +# predicates for selection + +class LengthThresholdUP1D(UnaryPredicate1D): + def __init__(self, min_length=None, max_length=None): + UnaryPredicate1D.__init__(self) + self._min_length = min_length + self._max_length = max_length + def getName(self): + return "LengthThresholdUP1D" + def __call__(self, inter): + length = inter.length_2d + if self._min_length is not None and length < self._min_length: + return False + if self._max_length is not None and length > self._max_length: + return False + return True + +class FaceMarkBothUP1D(UnaryPredicate1D): + def __call__(self, inter): # ViewEdge + fe = inter.first_fedge + while fe is not None: + if fe.is_smooth: + if fe.face_mark: + return True + else: + if fe.face_mark_right and fe.face_mark_left: + return True + fe = fe.next_fedge + return False + +class FaceMarkOneUP1D(UnaryPredicate1D): + def __call__(self, inter): # ViewEdge + fe = inter.first_fedge + while fe is not None: + if fe.is_smooth: + if fe.face_mark: + return True + else: + if fe.face_mark_right or fe.face_mark_left: + return True + fe = fe.next_fedge + return False + +# predicates for splitting + +class MaterialBoundaryUP0D(UnaryPredicate0D): + def getName(self): + return "MaterialBoundaryUP0D" + def __call__(self, it): + if it.is_begin: + return False + it_prev = Interface0DIterator(it) + it_prev.decrement() + v = it.object + it.increment() + if it.is_end: + return False + fe = v.get_fedge(it_prev.object) + idx1 = fe.material_index if fe.is_smooth else fe.material_index_left + fe = v.get_fedge(it.object) + idx2 = fe.material_index if fe.is_smooth else fe.material_index_left + return idx1 != idx2 + +class Curvature2DAngleThresholdUP0D(UnaryPredicate0D): + def __init__(self, min_angle=None, max_angle=None): + UnaryPredicate0D.__init__(self) + self._min_angle = min_angle + self._max_angle = max_angle + self._func = Curvature2DAngleF0D() + def getName(self): + return "Curvature2DAngleThresholdUP0D" + def __call__(self, inter): + angle = math.pi - self._func(inter) + if self._min_angle is not None and angle < self._min_angle: + return True + if self._max_angle is not None and angle > self._max_angle: + return True + return False + +class Length2DThresholdUP0D(UnaryPredicate0D): + def __init__(self, length_limit): + UnaryPredicate0D.__init__(self) + self._length_limit = length_limit + self._t = 0.0 + def getName(self): + return "Length2DThresholdUP0D" + def __call__(self, inter): + t = inter.t # curvilinear abscissa + if t < self._t: + self._t = 0.0 + return False + if t - self._t < self._length_limit: + return False + self._t = t + return True + +# Seed for random number generation + +class Seed: + def __init__(self): + self.t_max = 2 ** 15 + self.t = int(time.time()) % self.t_max + def get(self, seed): + if seed < 0: + self.t = (self.t + 1) % self.t_max + return self.t + return seed + +_seed = Seed() + +# main function for parameter processing + +def process(layer_name, lineset_name): + scene = Freestyle.getCurrentScene() + layer = scene.render.layers[layer_name] + lineset = layer.freestyle_settings.linesets[lineset_name] + linestyle = lineset.linestyle + + selection_criteria = [] + # prepare selection criteria by visibility + if lineset.select_by_visibility: + if lineset.visibility == "VISIBLE": + selection_criteria.append( + QuantitativeInvisibilityUP1D(0)) + elif lineset.visibility == "HIDDEN": + selection_criteria.append( + NotUP1D(QuantitativeInvisibilityUP1D(0))) + elif lineset.visibility == "RANGE": + selection_criteria.append( + QuantitativeInvisibilityRangeUP1D(lineset.qi_start, lineset.qi_end)) + # prepare selection criteria by edge types + if lineset.select_by_edge_types: + edge_type_criteria = [] + if lineset.select_silhouette: + upred = pyNatureUP1D(Nature.SILHOUETTE) + edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_silhouette else upred) + if lineset.select_border: + upred = pyNatureUP1D(Nature.BORDER) + edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_border else upred) + if lineset.select_crease: + upred = pyNatureUP1D(Nature.CREASE) + edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_crease else upred) + if lineset.select_ridge_valley: + upred = pyNatureUP1D(Nature.RIDGE) + edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_ridge_valley else upred) + if lineset.select_suggestive_contour: + upred = pyNatureUP1D(Nature.SUGGESTIVE_CONTOUR) + edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_suggestive_contour else upred) + if lineset.select_material_boundary: + upred = pyNatureUP1D(Nature.MATERIAL_BOUNDARY) + edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_material_boundary else upred) + if lineset.select_edge_mark: + upred = pyNatureUP1D(Nature.EDGE_MARK) + edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_edge_mark else upred) + if lineset.select_contour: + upred = ContourUP1D() + edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_contour else upred) + if lineset.select_external_contour: + upred = ExternalContourUP1D() + edge_type_criteria.append(NotUP1D(upred) if lineset.exclude_external_contour else upred) + if lineset.edge_type_combination == "OR": + upred = join_unary_predicates(edge_type_criteria, OrUP1D) + else: + upred = join_unary_predicates(edge_type_criteria, AndUP1D) + if upred is not None: + if lineset.edge_type_negation == "EXCLUSIVE": + upred = NotUP1D(upred) + selection_criteria.append(upred) + # prepare selection criteria by face marks + if lineset.select_by_face_marks: + if lineset.face_mark_condition == "BOTH": + upred = FaceMarkBothUP1D() + else: + upred = FaceMarkOneUP1D() + if lineset.face_mark_negation == "EXCLUSIVE": + upred = NotUP1D(upred) + selection_criteria.append(upred) + # prepare selection criteria by group of objects + if lineset.select_by_group: + if lineset.group is not None: + names = dict((ob.name, True) for ob in lineset.group.objects) + upred = ObjectNamesUP1D(names, lineset.group_negation == 'EXCLUSIVE') + selection_criteria.append(upred) + # prepare selection criteria by image border + if lineset.select_by_image_border: + w = scene.render.resolution_x + h = scene.render.resolution_y + if scene.render.use_border: + xmin = scene.render.border_min_x * w + xmax = scene.render.border_max_x * w + ymin = scene.render.border_min_y * h + ymax = scene.render.border_max_y * h + else: + xmin, xmax = 0.0, float(w) + ymin, ymax = 0.0, float(h) + upred = WithinImageBoundaryUP1D(xmin, ymin, xmax, ymax) + selection_criteria.append(upred) + # select feature edges + upred = join_unary_predicates(selection_criteria, AndUP1D) + if upred is None: + upred = TrueUP1D() + Operators.select(upred) + # join feature edges to form chains + if linestyle.use_chaining: + if linestyle.chaining == "PLAIN": + if linestyle.same_object: + Operators.bidirectionalChain(ChainSilhouetteIterator(), NotUP1D(upred)) + else: + Operators.bidirectionalChain(ChainPredicateIterator(upred, TrueBP1D()), NotUP1D(upred)) + elif linestyle.chaining == "SKETCHY": + if linestyle.same_object: + Operators.bidirectionalChain(pySketchyChainSilhouetteIterator(linestyle.rounds)) + else: + Operators.bidirectionalChain(pySketchyChainingIterator(linestyle.rounds)) + else: + Operators.chain(ChainPredicateIterator(FalseUP1D(), FalseBP1D()), NotUP1D(upred)) + # split chains + if linestyle.material_boundary: + Operators.sequentialSplit(MaterialBoundaryUP0D()) + if linestyle.use_min_angle or linestyle.use_max_angle: + min_angle = linestyle.min_angle if linestyle.use_min_angle else None + max_angle = linestyle.max_angle if linestyle.use_max_angle else None + Operators.sequentialSplit(Curvature2DAngleThresholdUP0D(min_angle, max_angle)) + if linestyle.use_split_length: + Operators.sequentialSplit(Length2DThresholdUP0D(linestyle.split_length), 1.0) + if linestyle.use_split_pattern: + pattern = [] + if linestyle.split_dash1 > 0 and linestyle.split_gap1 > 0: + pattern.append(linestyle.split_dash1) + pattern.append(linestyle.split_gap1) + if linestyle.split_dash2 > 0 and linestyle.split_gap2 > 0: + pattern.append(linestyle.split_dash2) + pattern.append(linestyle.split_gap2) + if linestyle.split_dash3 > 0 and linestyle.split_gap3 > 0: + pattern.append(linestyle.split_dash3) + pattern.append(linestyle.split_gap3) + if len(pattern) > 0: + sampling = 1.0 + controller = SplitPatternController(pattern, sampling) + Operators.sequentialSplit(SplitPatternStartingUP0D(controller), + SplitPatternStoppingUP0D(controller), + sampling) + # select chains + if linestyle.use_min_length or linestyle.use_max_length: + min_length = linestyle.min_length if linestyle.use_min_length else None + max_length = linestyle.max_length if linestyle.use_max_length else None + Operators.select(LengthThresholdUP1D(min_length, max_length)) + # prepare a list of stroke shaders + shaders_list = [] + for m in linestyle.geometry_modifiers: + if not m.use: + continue + if m.type == "SAMPLING": + shaders_list.append(SamplingShader( + m.sampling)) + elif m.type == "BEZIER_CURVE": + shaders_list.append(BezierCurveShader( + m.error)) + elif m.type == "SINUS_DISPLACEMENT": + shaders_list.append(SinusDisplacementShader( + m.wavelength, m.amplitude, m.phase)) + elif m.type == "SPATIAL_NOISE": + shaders_list.append(SpatialNoiseShader( + m.amplitude, m.scale, m.octaves, m.smooth, m.pure_random)) + elif m.type == "PERLIN_NOISE_1D": + shaders_list.append(PerlinNoise1DShader( + m.frequency, m.amplitude, m.octaves, m.angle, _seed.get(m.seed))) + elif m.type == "PERLIN_NOISE_2D": + shaders_list.append(PerlinNoise2DShader( + m.frequency, m.amplitude, m.octaves, m.angle, _seed.get(m.seed))) + elif m.type == "BACKBONE_STRETCHER": + shaders_list.append(BackboneStretcherShader( + m.backbone_length)) + elif m.type == "TIP_REMOVER": + shaders_list.append(TipRemoverShader( + m.tip_length)) + elif m.type == "POLYGONIZATION": + shaders_list.append(PolygonalizationShader( + m.error)) + elif m.type == "GUIDING_LINES": + shaders_list.append(GuidingLinesShader( + m.offset)) + elif m.type == "BLUEPRINT": + if m.shape == "CIRCLES": + shaders_list.append(pyBluePrintCirclesShader( + m.rounds, m.random_radius, m.random_center)) + elif m.shape == "ELLIPSES": + shaders_list.append(pyBluePrintEllipsesShader( + m.rounds, m.random_radius, m.random_center)) + elif m.shape == "SQUARES": + shaders_list.append(pyBluePrintSquaresShader( + m.rounds, m.backbone_length, m.random_backbone)) + elif m.type == "2D_OFFSET": + shaders_list.append(Offset2DShader( + m.start, m.end, m.x, m.y)) + elif m.type == "2D_TRANSFORM": + shaders_list.append(Transform2DShader( + m.pivot, m.scale_x, m.scale_y, m.angle, m.pivot_u, m.pivot_x, m.pivot_y)) + color = linestyle.color + if (not linestyle.use_chaining) or (linestyle.chaining == "PLAIN" and linestyle.same_object): + thickness_position = linestyle.thickness_position + else: + thickness_position = "CENTER" + import bpy + if bpy.app.debug_freestyle: + print("Warning: Thickness position options are applied when chaining is disabled") + print(" or the Plain chaining is used with the Same Object option enabled.") + shaders_list.append(BaseColorShader(color.r, color.g, color.b, linestyle.alpha)) + shaders_list.append(BaseThicknessShader(linestyle.thickness, thickness_position, + linestyle.thickness_ratio)) + for m in linestyle.color_modifiers: + if not m.use: + continue + if m.type == "ALONG_STROKE": + shaders_list.append(ColorAlongStrokeShader( + m.blend, m.influence, m.color_ramp)) + elif m.type == "DISTANCE_FROM_CAMERA": + shaders_list.append(ColorDistanceFromCameraShader( + m.blend, m.influence, m.color_ramp, + m.range_min, m.range_max)) + elif m.type == "DISTANCE_FROM_OBJECT": + shaders_list.append(ColorDistanceFromObjectShader( + m.blend, m.influence, m.color_ramp, m.target, + m.range_min, m.range_max)) + elif m.type == "MATERIAL": + shaders_list.append(ColorMaterialShader( + m.blend, m.influence, m.color_ramp, m.material_attr, + m.use_ramp)) + for m in linestyle.alpha_modifiers: + if not m.use: + continue + if m.type == "ALONG_STROKE": + shaders_list.append(AlphaAlongStrokeShader( + m.blend, m.influence, m.mapping, m.invert, m.curve)) + elif m.type == "DISTANCE_FROM_CAMERA": + shaders_list.append(AlphaDistanceFromCameraShader( + m.blend, m.influence, m.mapping, m.invert, m.curve, + m.range_min, m.range_max)) + elif m.type == "DISTANCE_FROM_OBJECT": + shaders_list.append(AlphaDistanceFromObjectShader( + m.blend, m.influence, m.mapping, m.invert, m.curve, m.target, + m.range_min, m.range_max)) + elif m.type == "MATERIAL": + shaders_list.append(AlphaMaterialShader( + m.blend, m.influence, m.mapping, m.invert, m.curve, + m.material_attr)) + for m in linestyle.thickness_modifiers: + if not m.use: + continue + if m.type == "ALONG_STROKE": + shaders_list.append(ThicknessAlongStrokeShader( + thickness_position, linestyle.thickness_ratio, + m.blend, m.influence, m.mapping, m.invert, m.curve, + m.value_min, m.value_max)) + elif m.type == "DISTANCE_FROM_CAMERA": + shaders_list.append(ThicknessDistanceFromCameraShader( + thickness_position, linestyle.thickness_ratio, + m.blend, m.influence, m.mapping, m.invert, m.curve, + m.range_min, m.range_max, m.value_min, m.value_max)) + elif m.type == "DISTANCE_FROM_OBJECT": + shaders_list.append(ThicknessDistanceFromObjectShader( + thickness_position, linestyle.thickness_ratio, + m.blend, m.influence, m.mapping, m.invert, m.curve, m.target, + m.range_min, m.range_max, m.value_min, m.value_max)) + elif m.type == "MATERIAL": + shaders_list.append(ThicknessMaterialShader( + thickness_position, linestyle.thickness_ratio, + m.blend, m.influence, m.mapping, m.invert, m.curve, + m.material_attr, m.value_min, m.value_max)) + elif m.type == "CALLIGRAPHY": + shaders_list.append(CalligraphicThicknessShader( + thickness_position, linestyle.thickness_ratio, + m.blend, m.influence, + m.orientation, m.min_thickness, m.max_thickness)) + if linestyle.caps == "ROUND": + shaders_list.append(RoundCapShader()) + elif linestyle.caps == "SQUARE": + shaders_list.append(SquareCapShader()) + if linestyle.use_dashed_line: + pattern = [] + if linestyle.dash1 > 0 and linestyle.gap1 > 0: + pattern.append(linestyle.dash1) + pattern.append(linestyle.gap1) + if linestyle.dash2 > 0 and linestyle.gap2 > 0: + pattern.append(linestyle.dash2) + pattern.append(linestyle.gap2) + if linestyle.dash3 > 0 and linestyle.gap3 > 0: + pattern.append(linestyle.dash3) + pattern.append(linestyle.gap3) + if len(pattern) > 0: + shaders_list.append(DashedLineShader(pattern)) + # create strokes using the shaders list + Operators.create(TrueUP1D(), shaders_list) |