├── LICENSE ├── README.md ├── sapling-randomizer-0.1.2.zip ├── sapling-randomizer └── __init__.py ├── sapling-randomizer_dialog_0.1.0.png └── sapling-randomizer_title_0.1.0.jpg /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 ThomasRadeke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sapling Randomizer 2 | This Blender add-on allows to create multiple random trees at the same time, based on the built-in "Sapling" tree generator. 3 | 4 | ## Download 5 | 1. Download: 6 | - [sapling-randomizer_0.1.2.zip](https://github.com/ThomasRadeke/sapling-randomizer/raw/master/sapling-randomizer-0.1.2.zip) (Blender 2.80) 7 | - [sapling-randomizer_0.1.0.zip](https://rahdick.at/projects/02_projects/2018-04-06_blender_addon_sapling_randomizer/sapling-randomizer_0.1.0.zip) (Blender 2.7x) 8 | - If the direct links don't work, please visit the [project website](https://rahdick.at/en/02_projects/2018-04-06_blender_addon_sapling_randomizer) and download from there. 9 | 2. In Blender, go to User Preferences > Add-Ons, click the "Install Add-on from File…" button on the bottom of the dialog and choose the downloaded ZIP file. 10 | 3. Find BOTH "Sapling Tree Gen" AND "Sapling Randomizer" and enable them. This add-on uses the built-in "Sapling Tree Gen" to generate the trees, so it must be enabled. 11 | 12 | ## Version 0.1.2 changes 13 | - Fixed Blender 2.80 Python API warnings 14 | - Added option for putting new objects into a collection 15 | - Added option for creating and assigning materials for trunks and leaves 16 | - Added option for preparing generated trees for use in particle systems 17 | - 3D cursor position is now taken into account 18 | 19 | ## Installation via GitHub 20 | 1. Download and unpack the thing. 21 | 2. Find the "sapling-randomizer" directory inside. It should only have a single .py file. 22 | 3. (A) Copy the whole directory (not just the Python file!) to your Blender Scripts directory or (B) pack the "sapling-randomizer" directory as a ZIP file again and install it via Blender's add-on manager. 23 | 4. In Blender's add-on manager, find BOTH "Sapling Tree Gen" AND "Sapling Randomizer" and enable them. This add-on uses the built-in "Sapling Tree Gen" to generate the trees, so it must be enabled. 24 | 25 | ## Usage 26 | When you have successfully enabled both add-ons, you can invoke the Sapling Randomizer using two methods: 27 | - 3D view > Tool Shelf > "Create" tab - you'll find a "Sapling Randomizer" button. Press it. 28 | - Using the quick menu (F3). Just type "Sapling Randomizer" and hit return. 29 | 30 | For more detail, head over to the [wiki](https://github.com/ThomasRadeke/sapling-randomizer/wiki). 31 | 32 | ## Known issues 33 | I'm not primarily a software developer, so please use the add-on with caution. 34 | - Some options are VERY SLOW, e.g. the "Maximum Branch Levels" control. Try with a low number of trees first. This depends on "Sapling"; can't do anything about it. Sorry! 35 | - Most options of the original "Sapling" add-on are not accessible through SR. However, you can save presets with "Sapling" and use them with SR. 36 | -------------------------------------------------------------------------------- /sapling-randomizer-0.1.2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasRadeke/sapling-randomizer/f7de52b4671d49df3d005d8223e9c22f33f32dac/sapling-randomizer-0.1.2.zip -------------------------------------------------------------------------------- /sapling-randomizer/__init__.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "Sapling Randomizer", 3 | "author": "Thomas Radeke", 4 | "version": (0, 1, 2), 5 | "blender": (2, 80, 0), 6 | "location": "View3D > Sidebar > Create", 7 | "description": "Generate multiple randomized \"Sapling\" curves at the same time.", 8 | "warning": "Needs the \"Sapling Tree Gen\" add-on to be activated.", 9 | "wiki_url": "https://github.com/ThomasRadeke/sapling-randomizer/wiki", 10 | "category": "Add Curve", 11 | } 12 | 13 | import os, bpy, random, math, add_curve_sapling 14 | from bpy.props import * 15 | 16 | class SaplingRandomizerOperator(bpy.types.Operator): 17 | bl_idname = "curve.sapling_randomizer" 18 | bl_label = "Sapling Randomizer" 19 | 20 | # Fix missing "bend" property in Sapling, which would otherwise prevent 21 | # execution of tree_add() 22 | add_curve_sapling.AddTree.bend = 0.0 23 | 24 | def getPresets(self, context): 25 | # get Sapling presets 26 | sapling_presets = [] 27 | for p in add_curve_sapling.getPresetpaths(): 28 | sapling_presets = sapling_presets + [a for a in os.listdir(p) if a[-3:] == '.py'] 29 | 30 | # Prepare enum item list from filenames 31 | preset_items = [] 32 | for s in sapling_presets: 33 | preset_items.append((s, s, 'Use "'+s+'" as preset')) 34 | return preset_items 35 | 36 | # Define class properties that will show up as UI elements on the dialog 37 | presets: EnumProperty(name="Preset", description="", items=getPresets) 38 | num_trees: IntProperty(name="Number of Saplings", description="Number of trees that will be generated by Sapling", default=10, min=1) 39 | spread: FloatProperty(name="Spread", description="Area in which Saplings will be created", unit='LENGTH') 40 | relative_spread: BoolProperty(name="Relative Spread", description="Make spread depend on number of Saplings", default=True) 41 | randomseed: IntProperty(name="Random Seed", description="Starting seed that will be passed to Sapling when generating trees", default=0) 42 | max_branch_levels: IntProperty(name="Maximum Branch Levels", description="Maximum branch levels to import from the preset", default=2) 43 | show_leaves: BoolProperty(name="Generate Leaves", description="Generate leaves for all generated Saplings", default=True) 44 | # leaf shape enum copied directly from Sapling. 45 | leaf_shape: EnumProperty(name="Leaf Shape", items=(('hex', 'Hexagonal', '0'), ('rect', 'Rectangular', '1'), 46 | ('dFace', 'DupliFaces', '2'), ('dVert', 'DupliVerts', '3')), default='hex') 47 | create_collection: BoolProperty(name="Create Collection", description="Create a new Collection for the new randomized objects", default=True) 48 | create_materials: BoolProperty(name="Create Materials", description="Create and assign tree trunk and leaf materials to all generated objects.", default=True) 49 | prepare_for_particles: BoolProperty(name="For Particle Systems", description="Prepare generated objects for use in particle systems.", default=False) 50 | 51 | # Run the actual code upon pressing "OK" on the dialog 52 | def execute(self, context): 53 | 54 | # decide whether "spread" will be relative or absolute 55 | if self.relative_spread: 56 | spread = math.sqrt(self.num_trees)*self.spread 57 | else: 58 | spread = self.spread 59 | 60 | # get window_manager context to make updating the progress indicator less code 61 | wm = bpy.context.window_manager 62 | 63 | # get list of current objects 64 | objects_before = bpy.data.objects.values() 65 | 66 | # start up the random generator with a new seed 67 | random.seed(self.randomseed) 68 | 69 | # start progress indicator 70 | wm.progress_begin(0, self.num_trees) 71 | 72 | # generate a number of trees 73 | for s in range(0, self.num_trees): 74 | #add_curve_sapling.ImportData.filename = 'japanese_maple.py' 75 | 76 | # use Sapling's own ImportData class to read the preset files into the "settings" list 77 | add_curve_sapling.ImportData.filename = self.presets 78 | add_curve_sapling.ImportData.execute(add_curve_sapling.ImportData, bpy.context) 79 | 80 | # have to override the preset values after reading them 81 | if add_curve_sapling.settings["levels"] > self.max_branch_levels: 82 | add_curve_sapling.settings["levels"] = self.max_branch_levels 83 | add_curve_sapling.settings["limitImport"] = False 84 | add_curve_sapling.settings["do_update"] = True 85 | add_curve_sapling.settings["bevel"] = True 86 | add_curve_sapling.settings["prune"] = False 87 | add_curve_sapling.settings["showLeaves"] = self.show_leaves 88 | add_curve_sapling.settings["leafShape"] = self.leaf_shape 89 | add_curve_sapling.settings["useArm"] = False 90 | add_curve_sapling.settings["seed"] = self.randomseed+s 91 | 92 | # run the actual tree generating code 93 | obj = bpy.ops.curve.tree_add( 94 | limitImport=False, 95 | do_update=True, 96 | bevel=True, 97 | prune=False, 98 | showLeaves=self.show_leaves, 99 | leafShape=self.leaf_shape, 100 | useArm=False, 101 | seed=self.randomseed+s 102 | ) 103 | # update the progress indicator after each tree 104 | wm.progress_update(s) 105 | 106 | # tell the progress indicator we're finished 107 | wm.progress_end() 108 | 109 | # make object list after generating 110 | objects_after = bpy.data.objects.values() 111 | newobjects = [object for object in objects_after if object not in objects_before] 112 | 113 | # set up basic materials 114 | if self.create_materials: 115 | # trunk 116 | trunk_material = bpy.data.materials.new("Sapling Trunk") 117 | trunk_material.diffuse_color = (0.0508761, 0.0450703, 0.0371111, 1) # dark brown 118 | trunk_material.roughness = 1.0 119 | trunk_material.specular_intensity = 0.0 120 | 121 | # leaves 122 | leaf_material = bpy.data.materials.new("Sapling Leaves") 123 | leaf_material.diffuse_color = (0.024514, 0.0508761, 0.0196054, 1) # darkish green 124 | leaf_material.roughness = 0.3 125 | leaf_material.specular_intensity = 0.5 126 | 127 | # since we cannot tell Sapling where to put its new trees, we have to iterate through all new "tree" objects 128 | # and move them randomly after generating them 129 | 130 | # also prepare a list of potentially joined meshes 131 | join_meshes = [] 132 | for obj in newobjects: 133 | 134 | # some operators require selections, so first deselect everything 135 | bpy.ops.object.select_all(action='DESELECT') 136 | 137 | # process trunks 138 | if obj.type == 'CURVE': 139 | cursor = bpy.context.scene.cursor.location 140 | x = (random.random() * spread) - spread/2 + cursor[0] 141 | y = (random.random() * spread) - spread/2 + cursor[1] 142 | obj.location = (x, y, cursor[2]) 143 | 144 | if self.create_materials: 145 | obj.data.materials.append(trunk_material) 146 | 147 | # if prepare_for_particles, convert curve to mesh and add mesh to list 148 | # of objects that need to be joined with their leaves 149 | if self.prepare_for_particles: 150 | obj.select_set(True) 151 | bpy.context.view_layer.objects.active = obj 152 | bpy.ops.object.convert(target='MESH') 153 | join_meshes.append(obj) 154 | 155 | # leaves 156 | if obj.type == 'MESH' and self.create_materials: 157 | obj.data.materials.append(leaf_material) 158 | 159 | # if prepare_for_particles, fix the wrongly-rotated axis by rotating the object by 160 | # 90° on X, applying the rotation and rotating it back by 90° 161 | if self.prepare_for_particles: 162 | obj.rotation_euler = (-1.5708,0,0) 163 | obj.select_set(True) 164 | bpy.context.view_layer.objects.active = obj 165 | bpy.ops.object.transform_apply(location=False,rotation=True,scale=False) 166 | obj.rotation_euler = (1.5708,0,0) 167 | 168 | # in case we want to prepare the trees for particle systems, go through the previously 169 | # prepared list of converted meshes, join their leaves to them and construct 170 | # a new list with just the final joined objects 171 | joined = [] 172 | if self.prepare_for_particles: 173 | for mesh in join_meshes: 174 | bpy.ops.object.select_all(action='DESELECT') 175 | child = mesh.children[0] 176 | child.select_set(True) 177 | mesh.select_set(True) 178 | bpy.context.view_layer.objects.active = mesh 179 | bpy.ops.object.join() 180 | joined.append(mesh) 181 | 182 | if self.create_collection: 183 | # make new collection 184 | newcol = bpy.data.collections.new("Randomized "+self.presets) 185 | bpy.context.scene.collection.children.link(newcol) 186 | 187 | # if the objects have been prepared for particle systems, the "newobjects" list 188 | # has now changed drastically. Replace it with the list of prepared meshes. 189 | if self.prepare_for_particles: 190 | newobjects = joined 191 | 192 | # move new objects into collection 193 | for objref in newobjects: 194 | #print(objref) 195 | # link new object to new collection 196 | newcol.objects.link(objref) 197 | # remove object from scene collection 198 | bpy.context.scene.collection.objects.unlink(objref) 199 | 200 | bpy.ops.object.select_all(action='DESELECT') 201 | return {'FINISHED'} 202 | 203 | # update the dialog when checkboxes are used 204 | #def check(self, context): 205 | # return True 206 | 207 | # set some defaults before popping up the dialog, then pop up the dialog 208 | def invoke(self, context, event): 209 | self.num_trees = 10 210 | self.randomseed = 0 211 | self.leaf_shape='hex' 212 | self.relative_spread = True 213 | self.spread = 3.0 214 | self.create_collection = True 215 | return context.window_manager.invoke_props_dialog(self) 216 | 217 | # register the operator so it can be called from the class below 218 | bpy.utils.register_class(SaplingRandomizerOperator) 219 | 220 | # make an entry in the UI so the randomizer can be called with a button 221 | # instead of just the quick menu 222 | class PANEL_PT_SaplingRandomizer(bpy.types.Panel): 223 | bl_label = "Sapling Randomizer" 224 | bl_space_type = "VIEW_3D" 225 | bl_region_type = 'TOOLS' if bpy.app.version < (2, 80) else 'UI' 226 | #bl_region_type = "UI" 227 | bl_category = "Create" 228 | bl_context = "objectmode" 229 | 230 | def draw(self, context): 231 | layout = self.layout 232 | 233 | row = layout.row() 234 | row.operator("curve.sapling_randomizer") 235 | 236 | def register(): 237 | bpy.utils.register_class(PANEL_PT_SaplingRandomizer) 238 | 239 | def unregister(): 240 | bpy.utils.unregister_class(PANEL_PT_SaplingRandomizer) 241 | 242 | if __name__ == "__main__": 243 | register() 244 | -------------------------------------------------------------------------------- /sapling-randomizer_dialog_0.1.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasRadeke/sapling-randomizer/f7de52b4671d49df3d005d8223e9c22f33f32dac/sapling-randomizer_dialog_0.1.0.png -------------------------------------------------------------------------------- /sapling-randomizer_title_0.1.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasRadeke/sapling-randomizer/f7de52b4671d49df3d005d8223e9c22f33f32dac/sapling-randomizer_title_0.1.0.jpg --------------------------------------------------------------------------------