├── .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 | Foliose lichen mesh 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 | --------------------------------------------------------------------------------