├── .gitignore
├── .vscode
└── settings.json
├── README.md
├── __init__.py
├── op_grow.py
├── panel.py
├── settings.py
└── symlink.sh
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | *.log
3 | .eslintcache
4 | .DS_Store
5 | out/
6 | .tmp*
7 | *.tsbuildinfo
8 | __pycache__
9 |
10 | *.blend
11 | *.blend1
12 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "search.useIgnoreFiles": false
3 | }
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Differential Growth for Blender
2 |
3 | [Latest release](https://github.com/inca/blender-differential-growth/releases/latest) · _Please see [the orginal blog post](https://boris.okunskiy.name/posts/blender-differential-growth) for a formal introduction_.
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 | Differential growth is a generative algorithm inspired by the growth occurring in living organisms such as lichens, algae, poriferae, corals and other kinds of organic forms.
16 |
17 | ## Installation
18 |
19 | 1. Download a zip file from [Releases](https://github.com/inca/blender-differential-growth/releases/latest)
20 | 2. Install the addon in Blender by going to Edit > Preferences > Addons > Install
21 |
22 | ### Important notes
23 |
24 | - The algorithm is "destructive" (i.e. it will modify the mesh in-place). It is recommended to set the Undo Steps to maximum (Preferences > System > Undo Steps) to be able to revert to previous results.
25 |
26 | - Some combination of parameters may result in [combinatorial explosion](https://en.wikipedia.org/wiki/Combinatorial_explosion) causing Blender to hang. It is recommended to save often.
27 |
28 | ## Development
29 |
30 | It can be painful to develop in Blender's text editor.
31 |
32 | One good solution is to symlink the repo to the Blender's addon location, then develop using your favorite editor.
33 |
34 | Then use F3 -> Reload Scripts every time the change is made.
35 |
36 | ### Mac/Linux
37 |
38 | Locate the addons directory, then replace `ADDONS_DIR` in `symlink.sh` and run it.
39 |
40 | ### Windows
41 |
42 | Run `cmd.exe` as Administrator (Start menu, search 'cmd', right click).
43 |
44 | Then run (correct the paths as necessary):
45 |
46 | ```
47 | mklink /D "C:\Program Files (x86)\Steam\steamapps\common\Blender\2.93\scripts\addons\diffgrow" "C:\Users\boris\3d\blender\diffgrowth"
48 | ```
49 |
50 | ## Credits
51 |
52 | This work is heavily inspired by the work of others. Here's a non-exhaustive list of resources used in creation of this addon:
53 |
54 | - [Anders Hoff · inconvergent.net](https://inconvergent.net/)
55 | - [Floraform by Nervous System](https://n-e-r-v-o-u-s.com/projects/sets/floraform/)
56 | - [Kaspar Ravel Blog](https://www.kaspar.wtf/code-poems/differential-growth)
57 | - [Jason Webb 2D differential growth](https://medium.com/@jason.webb/2d-differential-growth-in-js-1843fd51b0ce)
58 | - [Sheltron's amazing collection of math art](https://nshelton.github.io/about/)
59 |
60 | ## License
61 |
62 | Differential Growth Addon uses [Blender License](https://www.blender.org/about/license/).
63 |
64 | Free to Use. Free to Change. Free to Share. Free to Sell Your Work.
65 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | # Support script reload for imports
2 | if 'bpy' in locals():
3 | import importlib
4 | importlib.reload(panel)
5 | importlib.reload(op_grow)
6 | else:
7 | from . import panel
8 | from . import op_grow
9 | from . import settings
10 |
11 | import bpy
12 |
13 | bl_info = {
14 | 'name': 'Differential Growth',
15 | 'description': 'Grow mesh into nature-inspired wavy forms',
16 | 'version': (2, 2, 0),
17 | 'author': 'Boris Okunskiy',
18 | 'tracker_url': 'https://github.com/inca/blender-differential-growth/issues',
19 | 'location': 'Properties > Object > Differential Growth',
20 | 'blender': (4, 30, 0),
21 | 'category': 'Object'
22 | }
23 |
24 | def register():
25 | bpy.utils.register_class(settings.DiffGrowthSettings)
26 | bpy.utils.register_class(op_grow.DiffGrowthStepOperator)
27 | bpy.utils.register_class(panel.DiffGrowthPanel)
28 |
29 | bpy.types.Object.diff_growth_settings = \
30 | bpy.props.PointerProperty(type=settings.DiffGrowthSettings)
31 |
32 | print('Differential Growth Registered')
33 |
34 | def unregister():
35 | bpy.utils.unregister_class(panel.DiffGrowthPanel)
36 | bpy.utils.unregister_class(op_grow.DiffGrowthStepOperator)
37 | bpy.utils.unregister_class(settings.DiffGrowthSettings)
38 |
39 | print('Differential Growth Unregistered')
40 |
41 | if __name__ == '__main__':
42 | register()
43 |
--------------------------------------------------------------------------------
/op_grow.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import bmesh
3 | import math
4 | from mathutils import Vector, kdtree, noise
5 |
6 | class DiffGrowthStepOperator(bpy.types.Operator):
7 | bl_label = "Diff Growth Step"
8 | bl_idname="object.diff_growth_step"
9 | bl_options = {"REGISTER", "UNDO"}
10 |
11 | def execute(self, context):
12 | obj = context.object
13 |
14 | if obj is None or obj.type != 'MESH':
15 | self.report({ "WARNING" }, "Active object must be a mesh")
16 | return { "CANCELLED" }
17 |
18 | if obj.vertex_groups.active_index == -1:
19 | self.report({ "WARNING" }, "A vertex group is required; switch to Weight Paint and define the growth area")
20 | return { "CANCELLED" }
21 |
22 | settings = obj.diff_growth_settings
23 | bm = bmesh.new()
24 | bm.from_mesh(obj.data)
25 |
26 | grow_step(
27 | obj,
28 | bm,
29 | settings
30 | )
31 |
32 | bm.normal_update()
33 | bm.to_mesh(obj.data)
34 | bm.free()
35 | obj.data.update()
36 | return { "FINISHED" }
37 |
38 |
39 | def grow_step(
40 | obj,
41 | bm,
42 | settings,
43 | ):
44 | group_index = obj.vertex_groups.active_index
45 | seed_vector = Vector((0, 0, 1)) * settings.seed
46 | scale = Vector(settings.scale)
47 |
48 | # Collect vertices with weights
49 | edges = set()
50 |
51 | kd = kdtree.KDTree(len(bm.verts))
52 | for i, vert in enumerate(bm.verts):
53 | kd.insert(vert.co, i)
54 | kd.balance()
55 |
56 | for vert in bm.verts:
57 | w = get_vertex_weight(bm, vert, group_index)
58 | if w == 0:
59 | continue
60 |
61 | # Remember edge for subsequent subdivision
62 | for edge in vert.link_edges:
63 | edges.add(edge)
64 |
65 | # Calculate forces
66 | f_attr = calc_vert_attraction(vert)
67 | f_rep = calc_vert_repulsion(vert, kd, settings.repulsion_radius)
68 | f_noise = noise.noise_vector(vert.co * settings.noise_scale + seed_vector)
69 | growth_vec = Vector((0, 0, 1))
70 | if settings.growth_dir_obj:
71 | growth_vec = (settings.growth_dir_obj.location - vert.co).normalized()
72 | force = \
73 | settings.fac_attr * f_attr + \
74 | settings.fac_rep * f_rep + \
75 | settings.fac_noise * f_noise + \
76 | settings.fac_growth_dir * growth_vec;
77 | offset = force * settings.dt * settings.dt * w;
78 | vert.co += offset * scale
79 |
80 | # Readjust weights
81 | if settings.inhibit_base > 0:
82 | if not vert.is_boundary:
83 | w = w ** (1 + settings.inhibit_base) - 0.01;
84 | if settings.inhibit_shell > 0:
85 | sh = vert.calc_shell_factor()
86 | w = w * pow(sh, -1 * settings.inhibit_shell)
87 | set_vertex_weight(bm, vert, group_index, w)
88 |
89 | # Subdivide
90 | edges_to_subdivide = []
91 | for edge in edges:
92 | avg_weight = calc_avg_edge_weight(bm, [edge], group_index)
93 | if avg_weight == 0:
94 | continue
95 | l = edge.calc_length()
96 | if (l / settings.split_radius) > (1 / avg_weight):
97 | edges_to_subdivide.append(edge)
98 |
99 | if len(edges_to_subdivide) > 0:
100 | print("Subdividing %i" % len(edges_to_subdivide))
101 | bmesh.ops.subdivide_edges(
102 | bm,
103 | edges=edges_to_subdivide,
104 | smooth=1.0,
105 | cuts=1,
106 | use_grid_fill=True,
107 | use_single_edge=True)
108 | # Triangulate adjacent faces
109 | adjacent_faces = set()
110 | for edge in edges_to_subdivide:
111 | adjacent_faces.update(edge.link_faces)
112 | bmesh.ops.triangulate(
113 | bm,
114 | faces=list(adjacent_faces))
115 | # Update normals
116 | bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
117 |
118 | def get_vertex_weight(bm, vert, group_index):
119 | weight_layer = bm.verts.layers.deform.active
120 | weights = vert[weight_layer]
121 | return weights[group_index] if group_index in weights else 0
122 |
123 | def set_vertex_weight(bm, vert, group_index, weight):
124 | weight_layer = bm.verts.layers.deform.active
125 | weights = vert[weight_layer]
126 | weights[group_index] = weight
127 |
128 | def calc_avg_edge_length(edges):
129 | sum = 0.0
130 | for edge in edges:
131 | sum += edge.calc_length()
132 | return sum / len(edges)
133 |
134 | def calc_min_edge_length(edges):
135 | val = 100000
136 | for edge in edges:
137 | val = min(edge.calc_length(), val)
138 | return val
139 |
140 | def calc_avg_edge_weight(bm, edges, group_index):
141 | sum = 0.0
142 | n = 0
143 | for edge in edges:
144 | for vert in edge.verts:
145 | sum += get_vertex_weight(bm, vert, group_index)
146 | n += 1
147 | return sum / n
148 |
149 | def calc_vert_attraction(vert):
150 | result = Vector()
151 | for edge in vert.link_edges:
152 | other = edge.other_vert(vert)
153 | if other == None:
154 | continue
155 | result += other.co - vert.co
156 | return result
157 |
158 | def calc_vert_repulsion(vert, kd, radius):
159 | result = Vector()
160 | for (co, index, distance) in kd.find_range(vert.co, radius):
161 | if (index == vert.index):
162 | continue;
163 | direction = (vert.co - co).normalized()
164 | # magnitude = radius / distance - 1
165 | magnitude = math.exp(-1 * (distance / radius) + 1) - 1
166 | result += direction * magnitude
167 | return result;
168 |
--------------------------------------------------------------------------------
/panel.py:
--------------------------------------------------------------------------------
1 | import bpy
2 |
3 | class DiffGrowthPanel(bpy.types.Panel):
4 | bl_label = "Differential Growth"
5 | bl_idname = "OBJECT_PT_diffgrow_panel"
6 | bl_space_type = 'PROPERTIES'
7 | bl_region_type = 'WINDOW'
8 | bl_context = 'object'
9 |
10 | def draw(self, context):
11 | layout = self.layout
12 | obj = context.object
13 | settings = obj.diff_growth_settings
14 |
15 | box = layout.box()
16 | box.label(text='Basics')
17 | row = box.row()
18 | row.prop(settings, 'split_radius')
19 | row = box.row()
20 | row.prop(settings, 'repulsion_radius')
21 | row = box.row()
22 | row.prop(settings, 'dt')
23 | row = box.row()
24 | row.prop(settings, 'scale')
25 |
26 | box = layout.box()
27 | box.label(text='Forces')
28 | row = box.row()
29 | row.prop(settings, 'fac_attr')
30 | row = box.row()
31 | row.prop(settings, 'fac_rep')
32 | row = box.row()
33 | row.prop(settings, 'fac_noise')
34 | row = box.row()
35 | row.prop(settings, 'noise_scale')
36 | row = box.row()
37 | row.prop(settings, 'seed')
38 |
39 | box = layout.box()
40 | box.label(text='Growth Direction')
41 | row = box.row()
42 | row.prop(settings, 'growth_dir_obj')
43 | row = box.row()
44 | row.prop(settings, 'fac_growth_dir')
45 |
46 | box = layout.box()
47 | box.label(text='Growth Inhibitors')
48 | row = box.row()
49 | row.prop(settings, 'inhibit_base')
50 | row = box.row()
51 | row.prop(settings, 'inhibit_shell')
52 |
53 | row = layout.row()
54 | row.operator('object.diff_growth_step')
55 |
--------------------------------------------------------------------------------
/settings.py:
--------------------------------------------------------------------------------
1 | import bpy
2 |
3 | class DiffGrowthSettings(bpy.types.PropertyGroup):
4 |
5 | split_radius: bpy.props.FloatProperty(
6 | name="Split Radius",
7 | description="Edges above this radius will be subdivided",
8 | default=.5,
9 | min=.01,
10 | max=10,
11 | )
12 |
13 | repulsion_radius: bpy.props.FloatProperty(
14 | name="Repulsion Radius",
15 | description="",
16 | default=1,
17 | min=0,
18 | max=10,
19 | )
20 |
21 | dt: bpy.props.FloatProperty(
22 | name="Step size",
23 | description="How much growth to apply on each step",
24 | default=.1,
25 | min=.001,
26 | max=10,
27 | )
28 |
29 | scale: bpy.props.FloatVectorProperty(
30 | name="Step Scale",
31 | description="Applies per-component scale to each movement; can be used to limit motion to specific axis",
32 | default=(1.0,1.0,1.0),
33 | size=3,
34 | subtype='XYZ'
35 | )
36 |
37 | seed: bpy.props.IntProperty(
38 | name="Seed",
39 | description="Noise seed",
40 | default=1,
41 | min=1,
42 | max=1000,
43 | )
44 |
45 | noise_scale: bpy.props.FloatProperty(
46 | name="Noise Scale",
47 | description="Higher value produce high frequency noise",
48 | default=2,
49 | min=0.01,
50 | max=100,
51 | )
52 |
53 | growth_dir_obj: bpy.props.PointerProperty(
54 | name="Growth Direction Object",
55 | description="The object towards which to grow; if not specified, +Z is used",
56 | type=bpy.types.Object
57 | )
58 |
59 | fac_attr: bpy.props.FloatProperty(
60 | name="Attraction Factor",
61 | description="Attraction Factor",
62 | default=0,
63 | min=0,
64 | max=1000,
65 | )
66 |
67 | fac_rep: bpy.props.FloatProperty(
68 | name="Repulsion Factor",
69 | description="Repulsion Factor",
70 | default=1,
71 | min=0,
72 | max=1000,
73 | )
74 |
75 | fac_noise: bpy.props.FloatProperty(
76 | name="Noise Factor",
77 | description="Noise Factor",
78 | default=1,
79 | min=0,
80 | max=1000,
81 | )
82 |
83 | fac_growth_dir: bpy.props.FloatProperty(
84 | name="Growth Direction Factor",
85 | description="Growth Direction Factor",
86 | default=0,
87 | min=-1000,
88 | max=1000,
89 | )
90 |
91 | inhibit_base: bpy.props.FloatProperty(
92 | name="Base Factor",
93 | description="Inhibit non-boundary growth",
94 | default=1,
95 | min=0,
96 | max=10,
97 | )
98 |
99 | inhibit_shell: bpy.props.FloatProperty(
100 | name="Shell Factor",
101 | description="Inhibit growth based on vertex sharpness",
102 | default=0,
103 | min=0,
104 | max=10,
105 | )
106 |
--------------------------------------------------------------------------------
/symlink.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # ADDONS_DIR="/Applications/Blender.app/Contents/Resources/4.3/scripts/addons"
4 | ADDONS_DIR="/Users/inca/Library/Application Support/Blender/4.3/scripts/addons"
5 | ln -s "$(pwd)" "$ADDONS_DIR/diffgrowth"
6 |
--------------------------------------------------------------------------------