[
  {
    "path": "LICENSE",
    "content": "================\nSOFTWARE LICENSE\n================\n\nThe MIT License (MIT)\n\nCopyright (c) 2016 Michael Davies\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n==============================================================\nLICENSE FOR CONTENT PROCEDURALLY GENERATED USING THIS SOFTWARE\n==============================================================\n\nAll content procedurally generated by this software and its permutations\nare licensed under Creative Commons Attribution By 3.0:\n\nhttps://creativecommons.org/licenses/by/3.0/\n\nIf in doubt, please get in touch. As a rule of thumb I'm fine with anyone\nusing these spaceships in commercial works, but I am less okay with people\nselling the spaceship models directly for commercial gain. Use common sense.\n"
  },
  {
    "path": "README.md",
    "content": "# Spaceship Generator\n\nA Blender script to procedurally generate 3D spaceships from a random seed.\n\n![Spaceship screenshots](https://raw.githubusercontent.com/a1studmuffin/SpaceshipGenerator/master/screenshots/spaceships_grid.jpg)\n\nUsage\n-----\n* Install Blender 2.80 or greater: http://blender.org/download/\n* Download newest `add_mesh_SpaceshipGenerator.zip` from the [Releases](https://github.com/a1studmuffin/SpaceshipGenerator/releases) section\n* Under Edit > Preferences... > Add-ons > Install... open the downloaded ZIP file\n* Under Edit > Preferences... > Add-ons enable the \"Add Mesh: Spaceship Generator\" script (search for \"spaceship\")\n* Add a spaceship in the 3D View under Add > Mesh > Spaceship\n* Expand the Spaceship tab that appears in the bottom left of the viewport to adjust procedural generation settings\n\nHow it works\n------------\n\n![Step-by-step animation](https://raw.githubusercontent.com/a1studmuffin/SpaceshipGenerator/master/screenshots/step-by-step-animation.gif)\n\nWatch on YouTube: https://www.youtube.com/watch?v=xJZyXqJ6nog\n\n* Start with a box.\n* Build the hull: Extrude the front/rear faces several times, adding random translation/scaling/rotation along the way.\n* Add asymmetry to the hull: Pick random faces and extrude them out in a similar manner, reducing in scale each time.\n* Add detail to the hull: Categorize each face by its orientation and generate details on it such as engines, antenna, weapon turrets, lights etc.\n* Sometimes apply horizontal symmetry.\n* Add a Bevel modifier to angularize the shape a bit.\n* Apply materials to the final result.\n* Take over the universe with your new infinite fleet of spaceships.\n\nExtreme examples\n----------------\nThe following screenshots were created using extreme values for the number of hull segments and asymmetry segments to show how the algorithm works.\n\n![Extreme spaceship screenshots](https://raw.githubusercontent.com/a1studmuffin/SpaceshipGenerator/master/screenshots/extreme_examples.jpg)\n\nTips and Tricks\n---------------\n* By default the script will delete all objects starting with `Spaceship` before generating a new spaceship. To disable this feature, remove or comment out the call to `reset_scene()` around line 735 in the main function.\n* You can provide a seed to the `generate_spaceship()` function to always generate the same spaceship. For example, `generate_spaceship('michael')`.\n* The `generate_spaceship()` function takes many more parameters that affect the generation process. Try playing with them!\n* You can replace the textures with your own ones. All textures are applied using global-space cube UVs. `hull_normal.png` is a normal map that adds extra surface \"greebles\". `hull_lights_diffuse.png` is an additive diffuse texture to set the color of the window lights. `hull_lights_emit.png` is an emissive texture to make the windows glow in darkness.\n\nCredits\n-------\nWritten for fun as part of the [/r/proceduralgeneration](https://www.reddit.com/r/proceduralgeneration/) June 2016 [monthly challenge](https://www.reddit.com/r/proceduralgeneration/comments/4mn9gj/monthly_challenge_7_june_2016_procedural/).\n\nReleased under the [MIT License].\n\nAuthored and maintained by Michael Davies.\n\n> GitHub [@a1studmuffin](https://github.com/a1studmuffin)\n> Twitter [@butterparty](https://twitter.com/butterparty)\n\nSpecial thanks to [@panzi](https://github.com/panzi) for bugfixes, a proper GUI and build script. Also to [@mjrthemes](https://github.com/mjrthemes) for bugfixing, and [@LendoK](https://github.com/LendoK) for the 2.80 port.\n\n[MIT License]: http://mit-license.org/\n"
  },
  {
    "path": "__init__.py",
    "content": "bl_info = {\n    \"name\": \"Spaceship Generator\",\n    \"author\": \"Michael Davies\",\n    \"version\": (1, 1, 3),\n    \"blender\": (2, 80, 0),\n    \"location\": \"View3D > Add > Mesh\",\n    \"description\": \"Procedurally generate 3D spaceships from a random seed.\",\n    \"wiki_url\": \"https://github.com/a1studmuffin/SpaceshipGenerator/blob/master/README.md\",\n    \"tracker_url\": \"https://github.com/a1studmuffin/SpaceshipGenerator/issues\",\n    \"category\": \"Add Mesh\"\n}\n\nif \"bpy\" in locals():\n    # reload logic (magic)\n    import importlib\n    importlib.reload(spaceship_generator)\nelse:\n    from . import spaceship_generator\n\nimport bpy\nfrom bpy.props import StringProperty, BoolProperty, IntProperty\nfrom bpy.types import Operator\n\nclass GenerateSpaceship(Operator):\n    \"\"\"Procedurally generate 3D spaceships from a random seed.\"\"\"\n    bl_idname = \"mesh.generate_spaceship\"\n    bl_label = \"Spaceship\"\n    bl_options = {'REGISTER', 'UNDO'}\n\n    random_seed : StringProperty(default='', name='Seed')\n    num_hull_segments_min      : IntProperty (default=3, min=0, soft_max=16, name='Min. Hull Segments')\n    num_hull_segments_max      : IntProperty (default=6, min=0, soft_max=16, name='Max. Hull Segments')\n    create_asymmetry_segments  : BoolProperty(default=True, name='Create Asymmetry Segments')\n    num_asymmetry_segments_min : IntProperty (default=1, min=1, soft_max=16, name='Min. Asymmetry Segments')\n    num_asymmetry_segments_max : IntProperty (default=5, min=1, soft_max=16, name='Max. Asymmetry Segments')\n    create_face_detail         : BoolProperty(default=True,  name='Create Face Detail')\n    allow_horizontal_symmetry  : BoolProperty(default=True,  name='Allow Horizontal Symmetry')\n    allow_vertical_symmetry    : BoolProperty(default=False, name='Allow Vertical Symmetry')\n    apply_bevel_modifier       : BoolProperty(default=True,  name='Apply Bevel Modifier')\n    assign_materials           : BoolProperty(default=True,  name='Assign Materials')\n\n    def execute(self, context):\n        spaceship_generator.generate_spaceship(\n            self.random_seed,\n            self.num_hull_segments_min,\n            self.num_hull_segments_max,\n            self.create_asymmetry_segments,\n            self.num_asymmetry_segments_min,\n            self.num_asymmetry_segments_max,\n            self.create_face_detail,\n            self.allow_horizontal_symmetry,\n            self.allow_vertical_symmetry,\n            self.apply_bevel_modifier,\n            self.assign_materials)\n        return {'FINISHED'}\n\ndef menu_func(self, context):\n    self.layout.operator(GenerateSpaceship.bl_idname, text=\"Spaceship\")\n\ndef register():\n    bpy.utils.register_class(GenerateSpaceship)\n    bpy.types.VIEW3D_MT_mesh_add.append(menu_func)\n\ndef unregister():\n    bpy.utils.unregister_class(GenerateSpaceship)\n    bpy.types.VIEW3D_MT_mesh_add.remove(menu_func)\n\nif __name__ == \"__main__\":\n    register()\n"
  },
  {
    "path": "build.py",
    "content": "#!/usr/bin/env python\n\nfrom os.path import abspath, dirname, join as pjoin\nimport zipfile\n\nSRC_DIR = dirname(abspath(__file__))\n\nwith zipfile.ZipFile('add_mesh_SpaceshipGenerator.zip', 'w', zipfile.ZIP_DEFLATED) as arch:\n    for filename in [\n            '__init__.py',\n            'spaceship_generator.py',\n            'textures/hull_normal.png',\n            'textures/hull_lights_emit.png',\n            'textures/hull_lights_diffuse.png']:\n        arch.write(pjoin(SRC_DIR, filename), 'add_mesh_SpaceshipGenerator/'+filename)\n\nprint('created file: add_mesh_SpaceshipGenerator.zip')\n"
  },
  {
    "path": "spaceship_generator.py",
    "content": "#\n# spaceship_generator.py\n#\n# This is a Blender script that uses procedural generation to create\n# textured 3D spaceship models. Tested with Blender 2.77a.\n#\n# michael@spaceduststudios.com\n# https://github.com/a1studmuffin/SpaceshipGenerator\n#\n\nimport sys\nimport os\nimport os.path\nimport bpy\nimport bmesh\nimport datetime\nfrom math import sqrt, radians, pi, cos, sin\nfrom mathutils import Vector, Matrix\nfrom random import random, seed, uniform, randint, randrange\nfrom enum import IntEnum\nfrom colorsys import hls_to_rgb\n\nDIR = os.path.dirname(os.path.abspath(__file__))\n\ndef resource_path(*path_components):\n    return os.path.join(DIR, *path_components)\n\n# Deletes all existing spaceships and unused materials from the scene\ndef reset_scene():\n    for item in bpy.data.objects:\n        item.select = item.name.startswith('Spaceship')\n    bpy.ops.object.delete()\n    for material in bpy.data.materials:\n        if not material.users:\n            bpy.data.materials.remove(material)\n    for texture in bpy.data.textures:\n        if not texture.users:\n            bpy.data.textures.remove(texture)\n\n# Extrudes a face along its normal by translate_forwards units.\n# Returns the new face, and optionally fills out extruded_face_list\n# with all the additional side faces created from the extrusion.\ndef extrude_face(bm, face, translate_forwards=0.0, extruded_face_list=None):\n    new_faces = bmesh.ops.extrude_discrete_faces(bm, faces=[face])['faces']\n    if extruded_face_list != None:\n        extruded_face_list += new_faces[:]\n    new_face = new_faces[0]\n    bmesh.ops.translate(bm,\n                        vec=new_face.normal * translate_forwards,\n                        verts=new_face.verts)\n    return new_face\n\n# Similar to extrude_face, except corrigates the geometry to create \"ribs\".\n# Returns the new face.\ndef ribbed_extrude_face(bm, face, translate_forwards, num_ribs=3, rib_scale=0.9):\n    translate_forwards_per_rib = translate_forwards / float(num_ribs)\n    new_face = face\n    for i in range(num_ribs):\n        new_face = extrude_face(bm, new_face, translate_forwards_per_rib * 0.25)\n        new_face = extrude_face(bm, new_face, 0.0)\n        scale_face(bm, new_face, rib_scale, rib_scale, rib_scale)\n        new_face = extrude_face(bm, new_face, translate_forwards_per_rib * 0.5)\n        new_face = extrude_face(bm, new_face, 0.0)\n        scale_face(bm, new_face, 1 / rib_scale, 1 / rib_scale, 1 / rib_scale)\n        new_face = extrude_face(bm, new_face, translate_forwards_per_rib * 0.25)\n    return new_face\n\n# Scales a face in local face space. Ace!\ndef scale_face(bm, face, scale_x, scale_y, scale_z):\n    face_space = get_face_matrix(face)\n    face_space.invert()\n    bmesh.ops.scale(bm,\n                    vec=Vector((scale_x, scale_y, scale_z)),\n                    space=face_space,\n                    verts=face.verts)\n\n# Returns a rough 4x4 transform matrix for a face (doesn't handle\n# distortion/shear) with optional position override.\ndef get_face_matrix(face, pos=None):\n    x_axis = (face.verts[1].co - face.verts[0].co).normalized()\n    z_axis = -face.normal\n    y_axis = z_axis.cross(x_axis)\n    if not pos:\n        pos = face.calc_center_bounds()\n\n    # Construct a 4x4 matrix from axes + position:\n    # http://i.stack.imgur.com/3TnQP.png\n    mat = Matrix()\n    mat[0][0] = x_axis.x\n    mat[1][0] = x_axis.y\n    mat[2][0] = x_axis.z\n    mat[3][0] = 0\n    mat[0][1] = y_axis.x\n    mat[1][1] = y_axis.y\n    mat[2][1] = y_axis.z\n    mat[3][1] = 0\n    mat[0][2] = z_axis.x\n    mat[1][2] = z_axis.y\n    mat[2][2] = z_axis.z\n    mat[3][2] = 0\n    mat[0][3] = pos.x\n    mat[1][3] = pos.y\n    mat[2][3] = pos.z\n    mat[3][3] = 1\n    return mat\n\n# Returns the rough length and width of a quad face.\n# Assumes a perfect rectangle, but close enough.\ndef get_face_width_and_height(face):\n    if not face.is_valid or len(face.verts[:]) < 4:\n        return -1, -1\n    width = (face.verts[0].co - face.verts[1].co).length\n    height = (face.verts[2].co - face.verts[1].co).length\n    return width, height\n\n# Returns the rough aspect ratio of a face. Always >= 1.\ndef get_aspect_ratio(face):\n    if not face.is_valid:\n        return 1.0\n    face_aspect_ratio = max(0.01, face.edges[0].calc_length() / face.edges[1].calc_length())\n    if face_aspect_ratio < 1.0:\n        face_aspect_ratio = 1.0 / face_aspect_ratio\n    return face_aspect_ratio\n\n# Returns true if this face is pointing behind the ship\ndef is_rear_face(face):\n    return face.normal.x < -0.95\n\n# Given a face, splits it into a uniform grid and extrudes each grid face\n# out and back in again, making an exhaust shape.\ndef add_exhaust_to_face(bm, face):\n    if not face.is_valid:\n        return\n\n    # The more square the face is, the more grid divisions it might have\n    num_cuts = randint(1, int(4 - get_aspect_ratio(face)))\n    result = bmesh.ops.subdivide_edges(bm,\n                                    edges=face.edges[:],\n                                    cuts=num_cuts,\n                                    fractal=0.02,\n                                    use_grid_fill=True)\n\n    exhaust_length = uniform(0.1, 0.2)\n    scale_outer = 1 / uniform(1.3, 1.6)\n    scale_inner = 1 / uniform(1.05, 1.1)\n    for face in result['geom']:\n        if isinstance(face, bmesh.types.BMFace):\n            if is_rear_face(face):\n                face.material_index = Material.hull_dark\n                face = extrude_face(bm, face, exhaust_length)\n                scale_face(bm, face, scale_outer, scale_outer, scale_outer)\n                extruded_face_list = []\n                face = extrude_face(bm, face, -exhaust_length * 0.9, extruded_face_list)\n                for extruded_face in extruded_face_list:\n                    extruded_face.material_index = Material.exhaust_burn\n                scale_face(bm, face, scale_inner, scale_inner, scale_inner)\n\n# Given a face, splits it up into a smaller uniform grid and extrudes each grid cell.\ndef add_grid_to_face(bm, face):\n    if not face.is_valid:\n        return\n    result = bmesh.ops.subdivide_edges(bm,\n                                    edges=face.edges[:],\n                                    cuts=randint(2, 4),\n                                    fractal=0.02,\n                                    use_grid_fill=True,\n                                    use_single_edge=False)\n    grid_length = uniform(0.025, 0.15)\n    scale = 0.8\n    for face in result['geom']:\n        if isinstance(face, bmesh.types.BMFace):\n            material_index = Material.hull_lights if random() > 0.5 else Material.hull\n            extruded_face_list = []\n            face = extrude_face(bm, face, grid_length, extruded_face_list)\n            for extruded_face in extruded_face_list:\n                if abs(face.normal.z) < 0.707: # side face\n                    extruded_face.material_index = material_index\n            scale_face(bm, face, scale, scale, scale)\n\n# Given a face, adds some cylinders along it in a grid pattern.\ndef add_cylinders_to_face(bm, face):\n    if not face.is_valid or len(face.verts[:]) < 4:\n        return\n    horizontal_step = randint(1, 3)\n    vertical_step = randint(1, 3)\n    num_segments = randint(6, 12)\n    face_width, face_height = get_face_width_and_height(face)\n    cylinder_depth = 1.3 * min(face_width / (horizontal_step + 2),\n                               face_height / (vertical_step + 2))\n    cylinder_size = cylinder_depth * 0.5\n    for h in range(horizontal_step):\n        top = face.verts[0].co.lerp(\n            face.verts[1].co, (h + 1) / float(horizontal_step + 1))\n        bottom = face.verts[3].co.lerp(\n            face.verts[2].co, (h + 1) / float(horizontal_step + 1))\n        for v in range(vertical_step):\n            pos = top.lerp(bottom, (v + 1) / float(vertical_step + 1))\n            cylinder_matrix = get_face_matrix(face, pos) @ \\\n                Matrix.Rotation(radians(90), 3, 'X').to_4x4()\n            bmesh.ops.create_cone(bm,\n                                  cap_ends=True,\n                                  cap_tris=False,\n                                  segments=num_segments,\n                                  diameter1=cylinder_size,\n                                  diameter2=cylinder_size,\n                                  depth=cylinder_depth,\n                                  matrix=cylinder_matrix)\n\n# Given a face, adds some weapon turrets to it in a grid pattern.\n# Each turret will have a random orientation.\ndef add_weapons_to_face(bm, face):\n    if not face.is_valid or len(face.verts[:]) < 4:\n        return\n    horizontal_step = randint(1, 2)\n    vertical_step = randint(1, 2)\n    num_segments = 16\n    face_width, face_height = get_face_width_and_height(face)\n    weapon_size = 0.5 * min(face_width / (horizontal_step + 2),\n                            face_height / (vertical_step + 2))\n    weapon_depth = weapon_size * 0.2\n    for h in range(horizontal_step):\n        top = face.verts[0].co.lerp(\n            face.verts[1].co, (h + 1) / float(horizontal_step + 1))\n        bottom = face.verts[3].co.lerp(\n            face.verts[2].co, (h + 1) / float(horizontal_step + 1))\n        for v in range(vertical_step):\n            pos = top.lerp(bottom, (v + 1) / float(vertical_step + 1))\n            face_matrix = get_face_matrix(face, pos + face.normal * weapon_depth * 0.5) @ \\\n                Matrix.Rotation(radians(uniform(0, 90)), 3, 'Z').to_4x4()\n\n            # Turret foundation\n            bmesh.ops.create_cone(bm,\n                                  cap_ends=True,\n                                  cap_tris=False,\n                                  segments=num_segments,\n                                  diameter1=weapon_size * 0.9,\n                                  diameter2=weapon_size,\n                                  depth=weapon_depth,\n                                  matrix=face_matrix)\n\n            # Turret left guard\n            left_guard_mat = face_matrix @ \\\n                Matrix.Rotation(radians(90), 3, 'Y').to_4x4() @ \\\n                Matrix.Translation(Vector((0, 0, weapon_size * 0.6))).to_4x4()\n            bmesh.ops.create_cone(bm,\n                                  cap_ends=True,\n                                  cap_tris=False,\n                                  segments=num_segments,\n                                  diameter1=weapon_size * 0.6,\n                                  diameter2=weapon_size * 0.5,\n                                  depth=weapon_depth * 2,\n                                  matrix=left_guard_mat)\n\n            # Turret right guard\n            right_guard_mat = face_matrix @ \\\n                Matrix.Rotation(radians(90), 3, 'Y').to_4x4() @ \\\n                Matrix.Translation(Vector((0, 0, weapon_size * -0.6))).to_4x4()\n            bmesh.ops.create_cone(bm,\n                                  cap_ends=True,\n                                  cap_tris=False,\n                                  segments=num_segments,\n                                  diameter1=weapon_size * 0.5,\n                                  diameter2=weapon_size * 0.6,\n                                  depth=weapon_depth * 2,\n                                  matrix=right_guard_mat)\n\n            # Turret housing\n            upward_angle = uniform(0, 45)\n            turret_house_mat = face_matrix @ \\\n                Matrix.Rotation(radians(upward_angle), 3, 'X').to_4x4() @ \\\n                Matrix.Translation(Vector((0, weapon_size * -0.4, 0))).to_4x4()\n            bmesh.ops.create_cone(bm,\n                                  cap_ends=True,\n                                  cap_tris=False,\n                                  segments=8,\n                                  diameter1=weapon_size * 0.4,\n                                  diameter2=weapon_size * 0.4,\n                                  depth=weapon_depth * 5,\n                                  matrix=turret_house_mat)\n\n            # Turret barrels L + R\n            bmesh.ops.create_cone(bm,\n                                  cap_ends=True,\n                                  cap_tris=False,\n                                  segments=8,\n                                  diameter1=weapon_size * 0.1,\n                                  diameter2=weapon_size * 0.1,\n                                  depth=weapon_depth * 6,\n                                  matrix=turret_house_mat @ \\\n                                         Matrix.Translation(Vector((weapon_size * 0.2, 0, -weapon_size))).to_4x4())\n            bmesh.ops.create_cone(bm,\n                                  cap_ends=True,\n                                  cap_tris=False,\n                                  segments=8,\n                                  diameter1=weapon_size * 0.1,\n                                  diameter2=weapon_size * 0.1,\n                                  depth=weapon_depth * 6,\n                                  matrix=turret_house_mat @ \\\n                                         Matrix.Translation(Vector((weapon_size * -0.2, 0, -weapon_size))).to_4x4())\n\n# Given a face, adds a sphere on the surface, partially inset.\ndef add_sphere_to_face(bm, face):\n    if not face.is_valid:\n        return\n    face_width, face_height = get_face_width_and_height(face)\n    sphere_size = uniform(0.4, 1.0) * min(face_width, face_height)\n    sphere_matrix = get_face_matrix(face,\n                                    face.calc_center_bounds() - face.normal * \\\n                                    uniform(0, sphere_size * 0.5))\n    result = bmesh.ops.create_icosphere(bm,\n                                        subdivisions=3,\n                                        diameter=sphere_size,\n                                        matrix=sphere_matrix)\n    for vert in result['verts']:\n        for face in vert.link_faces:\n            face.material_index = Material.hull\n\n# Given a face, adds some pointy intimidating antennas.\ndef add_surface_antenna_to_face(bm, face):\n    if not face.is_valid or len(face.verts[:]) < 4:\n        return\n    horizontal_step = randint(4, 10)\n    vertical_step = randint(4, 10)\n    for h in range(horizontal_step):\n        top = face.verts[0].co.lerp(\n            face.verts[1].co, (h + 1) / float(horizontal_step + 1))\n        bottom = face.verts[3].co.lerp(\n            face.verts[2].co, (h + 1) / float(horizontal_step + 1))\n        for v in range(vertical_step):\n            if random() > 0.9:\n                pos = top.lerp(bottom, (v + 1) / float(vertical_step + 1))\n                face_size = sqrt(face.calc_area())\n                depth = uniform(0.1, 1.5) * face_size\n                depth_short = depth * uniform(0.02, 0.15)\n                base_diameter = uniform(0.005, 0.05)\n\n                material_index = Material.hull if random() > 0.5 else Material.hull_dark\n\n                # Spire\n                num_segments = uniform(3, 6)\n                result = bmesh.ops.create_cone(bm,\n                                               cap_ends=False,\n                                               cap_tris=False,\n                                               segments=num_segments,\n                                               diameter1=0,\n                                               diameter2=base_diameter,\n                                               depth=depth,\n                                               matrix=get_face_matrix(face, pos + face.normal * depth * 0.5))\n                for vert in result['verts']:\n                    for vert_face in vert.link_faces:\n                        vert_face.material_index = material_index\n\n                # Base\n                result = bmesh.ops.create_cone(bm,\n                                               cap_ends=True,\n                                               cap_tris=False,\n                                               segments=num_segments,\n                                               diameter1=base_diameter * uniform(1, 1.5),\n                                               diameter2=base_diameter * uniform(1.5, 2),\n                                               depth=depth_short,\n                                               matrix=get_face_matrix(face, pos + face.normal * depth_short * 0.45))\n                for vert in result['verts']:\n                    for vert_face in vert.link_faces:\n                        vert_face.material_index = material_index\n\n# Given a face, adds a glowing \"landing pad\" style disc.\ndef add_disc_to_face(bm, face):\n    if not face.is_valid:\n        return\n    face_width, face_height = get_face_width_and_height(face)\n    depth = 0.125 * min(face_width, face_height)\n    bmesh.ops.create_cone(bm,\n                          cap_ends=True,\n                          cap_tris=False,\n                          segments=32,\n                          diameter1=depth * 3,\n                          diameter2=depth * 4,\n                          depth=depth,\n                          matrix=get_face_matrix(face, face.calc_center_bounds() + face.normal * depth * 0.5))\n    result = bmesh.ops.create_cone(bm,\n                                   cap_ends=False,\n                                   cap_tris=False,\n                                   segments=32,\n                                   diameter1=depth * 1.25,\n                                   diameter2=depth * 2.25,\n                                   depth=0.0,\n                                   matrix=get_face_matrix(face, face.calc_center_bounds() + face.normal * depth * 1.05))\n    for vert in result['verts']:\n        for face in vert.link_faces:\n            face.material_index = Material.glow_disc\n\nclass Material(IntEnum):\n    hull = 0            # Plain spaceship hull\n    hull_lights = 1     # Spaceship hull with emissive windows\n    hull_dark = 2       # Plain Spaceship hull, darkened\n    exhaust_burn = 3    # Emissive engine burn material\n    glow_disc = 4       # Emissive landing pad disc material\n\n\n# Returns shader node\ndef getShaderNode(mat):\n    ntree = mat.node_tree\n    node_out = ntree.get_output_node('EEVEE')\n    shader_node = node_out.inputs['Surface'].links[0].from_node\n    return shader_node\n\ndef getShaderInput(mat, name):\n    shaderNode = getShaderNode(mat)\n    return shaderNode.inputs[name]\n\n# Adds a hull normal map texture slot to a material.\ndef add_hull_normal_map(mat, hull_normal_map):\n    ntree = mat.node_tree\n    shader = getShaderNode(mat)\n    links = ntree.links\n\n    teximage_node = ntree.nodes.new('ShaderNodeTexImage')\n    teximage_node.image = hull_normal_map\n    teximage_node.image.colorspace_settings.name = 'Raw'\n    teximage_node.projection ='BOX'\n    tex_coords_node = ntree.nodes.new('ShaderNodeTexCoord')\n    links.new(tex_coords_node.outputs['Object'], teximage_node.inputs['Vector'])\n    normalMap_node = ntree.nodes.new('ShaderNodeNormalMap')\n    links.new(teximage_node.outputs[0], normalMap_node.inputs['Color'])\n    links.new(normalMap_node.outputs['Normal'], shader.inputs['Normal'])\n    return tex_coords_node\n\n\n\n# Sets some basic properties for a hull material.\ndef set_hull_mat_basics(mat, color, hull_normal_map):\n    shader_node = getShaderNode(mat)\n    shader_node.inputs[\"Specular\"].default_value = 0.1\n    shader_node.inputs[\"Base Color\"].default_value = color\n\n    return add_hull_normal_map(mat, hull_normal_map)\n\n# Creates all our materials and returns them as a list.\ndef create_materials():\n    ret = []\n\n    for material in Material:\n        mat = bpy.data.materials.new(name=material.name)\n        mat.use_nodes = True\n        ret.append(mat)\n\n    # Choose a base color for the spaceship hull\n    hull_base_color = hls_to_rgb(\n        random(), uniform(0.05, 0.5), uniform(0, 0.25))\n    hull_base_color = (hull_base_color[0], hull_base_color[1], hull_base_color[2], 1.0)\n\n    # Load up the hull normal map\n    hull_normal_map = bpy.data.images.load(resource_path('textures', 'hull_normal.png'), check_existing=True)\n\n\n    # Build the hull texture\n    mat = ret[Material.hull]\n    set_hull_mat_basics(mat, hull_base_color, hull_normal_map)\n\n    # Build the hull_lights texture\n    mat = ret[Material.hull_lights]\n    tex_coords_node = set_hull_mat_basics(mat, hull_base_color, hull_normal_map)\n    ntree = mat.node_tree\n    shader_node = getShaderNode(mat)\n    links = ntree.links\n\n    # Add a diffuse layer that sets the window color\n    hull_lights_diffuse_map = bpy.data.images.load(resource_path('textures', 'hull_lights_diffuse.png'), check_existing=True)\n    teximage_diff_node = ntree.nodes.new('ShaderNodeTexImage')\n    teximage_diff_node.image = hull_lights_diffuse_map\n    teximage_diff_node.projection ='BOX'\n    links.new(tex_coords_node.outputs['Object'], teximage_diff_node.inputs['Vector'])\n    RGB_node = ntree.nodes.new('ShaderNodeRGB')\n    RGB_node.outputs[0].default_value = hull_base_color\n    mix_node = ntree.nodes.new('ShaderNodeMixRGB')\n    links.new(RGB_node.outputs[0], mix_node.inputs[1])\n    links.new(teximage_diff_node.outputs[0], mix_node.inputs[2])\n    links.new(teximage_diff_node.outputs[1], mix_node.inputs[0])\n    links.new(mix_node.outputs[0], shader_node.inputs[\"Base Color\"])\n\n\n\n    # Add an emissive layer that lights up the windows\n    hull_lights_emessive_map = bpy.data.images.load(resource_path('textures', 'hull_lights_emit.png'), check_existing=True)\n    teximage_emit_node = ntree.nodes.new('ShaderNodeTexImage')\n    teximage_emit_node.image = hull_lights_emessive_map\n    teximage_emit_node.projection ='BOX'\n    links.new(tex_coords_node.outputs['Object'], teximage_emit_node.inputs['Vector'])\n    links.new(teximage_emit_node.outputs[0], shader_node.inputs[\"Emission\"])\n\n\n\n    # Build the hull_dark texture\n    mat = ret[Material.hull_dark]\n    set_hull_mat_basics(mat, [0.3 * x for x in hull_base_color], hull_normal_map)\n\n    # Choose a glow color for the exhaust + glow discs\n    glow_color = hls_to_rgb(random(), uniform(0.5, 1), 1)\n    glow_color = (glow_color[0], glow_color[1], glow_color[2], 1.0)\n\n    # # Build the exhaust_burn texture\n    mat = ret[Material.exhaust_burn]\n    shader_node = getShaderNode(mat)\n    shader_node.inputs[\"Emission\"].default_value = glow_color\n\n    # # Build the glow_disc texture\n    mat = ret[Material.glow_disc]\n    shader_node = getShaderNode(mat)\n    shader_node.inputs[\"Emission\"].default_value = glow_color\n\n    return ret\n\n# Generates a textured spaceship mesh and returns the object.\n# Just uses global cube texture coordinates rather than generating UVs.\n# Takes an optional random seed value to generate a specific spaceship.\n# Allows overriding of some parameters that affect generation.\ndef generate_spaceship(random_seed='',\n                       num_hull_segments_min=3,\n                       num_hull_segments_max=6,\n                       create_asymmetry_segments=True,\n                       num_asymmetry_segments_min=1,\n                       num_asymmetry_segments_max=5,\n                       create_face_detail=True,\n                       allow_horizontal_symmetry=True,\n                       allow_vertical_symmetry=False,\n                       apply_bevel_modifier=True,\n                       assign_materials=True):\n    if random_seed:\n        seed(random_seed)\n\n    # Let's start with a unit BMesh cube scaled randomly\n    bm = bmesh.new()\n    bmesh.ops.create_cube(bm, size=1)\n    scale_vector = Vector(\n        (uniform(0.75, 2.0), uniform(0.75, 2.0), uniform(0.75, 2.0)))\n    bmesh.ops.scale(bm, vec=scale_vector, verts=bm.verts)\n\n    # Extrude out the hull along the X axis, adding some semi-random perturbations\n    for face in bm.faces[:]:\n        if abs(face.normal.x) > 0.5:\n            hull_segment_length = uniform(0.3, 1)\n            num_hull_segments = randrange(num_hull_segments_min, num_hull_segments_max)\n            hull_segment_range = range(num_hull_segments)\n            for i in hull_segment_range:\n                is_last_hull_segment = i == hull_segment_range[-1]\n                val = random()\n                if val > 0.1:\n                    # Most of the time, extrude out the face with some random deviations\n                    face = extrude_face(bm, face, hull_segment_length)\n                    if random() > 0.75:\n                        face = extrude_face(\n                            bm, face, hull_segment_length * 0.25)\n\n                    # Maybe apply some scaling\n                    if random() > 0.5:\n                        sy = uniform(1.2, 1.5)\n                        sz = uniform(1.2, 1.5)\n                        if is_last_hull_segment or random() > 0.5:\n                            sy = 1 / sy\n                            sz = 1 / sz\n                        scale_face(bm, face, 1, sy, sz)\n\n                    # Maybe apply some sideways translation\n                    if random() > 0.5:\n                        sideways_translation = Vector(\n                            (0, 0, uniform(0.1, 0.4) * scale_vector.z * hull_segment_length))\n                        if random() > 0.5:\n                            sideways_translation = -sideways_translation\n                        bmesh.ops.translate(bm,\n                                            vec=sideways_translation,\n                                            verts=face.verts)\n\n                    # Maybe add some rotation around Y axis\n                    if random() > 0.5:\n                        angle = 5\n                        if random() > 0.5:\n                            angle = -angle\n                        bmesh.ops.rotate(bm,\n                                         verts=face.verts,\n                                         cent=(0, 0, 0),\n                                         matrix=Matrix.Rotation(radians(angle), 3, 'Y'))\n                else:\n                    # Rarely, create a ribbed section of the hull\n                    rib_scale = uniform(0.75, 0.95)\n                    face = ribbed_extrude_face(\n                        bm, face, hull_segment_length, randint(2, 4), rib_scale)\n\n    # Add some large asymmetrical sections of the hull that stick out\n    if create_asymmetry_segments:\n        for face in bm.faces[:]:\n            # Skip any long thin faces as it'll probably look stupid\n            if get_aspect_ratio(face) > 4:\n                continue\n            if random() > 0.85:\n                hull_piece_length = uniform(0.1, 0.4)\n                for i in range(randrange(num_asymmetry_segments_min, num_asymmetry_segments_max)):\n                    face = extrude_face(bm, face, hull_piece_length)\n\n                    # Maybe apply some scaling\n                    if random() > 0.25:\n                        s = 1 / uniform(1.1, 1.5)\n                        scale_face(bm, face, s, s, s)\n\n    # Now the basic hull shape is built, let's categorize + add detail to all the faces\n    if create_face_detail:\n        engine_faces = []\n        grid_faces = []\n        antenna_faces = []\n        weapon_faces = []\n        sphere_faces = []\n        disc_faces = []\n        cylinder_faces = []\n        for face in bm.faces[:]:\n            # Skip any long thin faces as it'll probably look stupid\n            if get_aspect_ratio(face) > 3:\n                continue\n\n            # Spin the wheel! Let's categorize + assign some materials\n            val = random()\n            if is_rear_face(face):  # rear face\n                if not engine_faces or val > 0.75:\n                    engine_faces.append(face)\n                elif val > 0.5:\n                    cylinder_faces.append(face)\n                elif val > 0.25:\n                    grid_faces.append(face)\n                else:\n                    face.material_index = Material.hull_lights\n            elif face.normal.x > 0.9:  # front face\n                if face.normal.dot(face.calc_center_bounds()) > 0 and val > 0.7:\n                    antenna_faces.append(face)  # front facing antenna\n                    face.material_index = Material.hull_lights\n                elif val > 0.4:\n                    grid_faces.append(face)\n                else:\n                    face.material_index = Material.hull_lights\n            elif face.normal.z > 0.9:  # top face\n                if face.normal.dot(face.calc_center_bounds()) > 0 and val > 0.7:\n                    antenna_faces.append(face)  # top facing antenna\n                elif val > 0.6:\n                    grid_faces.append(face)\n                elif val > 0.3:\n                    cylinder_faces.append(face)\n            elif face.normal.z < -0.9:  # bottom face\n                if val > 0.75:\n                    disc_faces.append(face)\n                elif val > 0.5:\n                    grid_faces.append(face)\n                elif val > 0.25:\n                    weapon_faces.append(face)\n            elif abs(face.normal.y) > 0.9:  # side face\n                if not weapon_faces or val > 0.75:\n                    weapon_faces.append(face)\n                elif val > 0.6:\n                    grid_faces.append(face)\n                elif val > 0.4:\n                    sphere_faces.append(face)\n                else:\n                    face.material_index = Material.hull_lights\n\n        # Now we've categorized, let's actually add the detail\n        for face in engine_faces:\n            add_exhaust_to_face(bm, face)\n\n        for face in grid_faces:\n            add_grid_to_face(bm, face)\n\n        for face in antenna_faces:\n            add_surface_antenna_to_face(bm, face)\n\n        for face in weapon_faces:\n            add_weapons_to_face(bm, face)\n\n        for face in sphere_faces:\n            add_sphere_to_face(bm, face)\n\n        for face in disc_faces:\n            add_disc_to_face(bm, face)\n\n        for face in cylinder_faces:\n            add_cylinders_to_face(bm, face)\n\n    # Apply horizontal symmetry sometimes\n    if allow_horizontal_symmetry and random() > 0.5:\n        bmesh.ops.symmetrize(bm, input=bm.verts[:] + bm.edges[:] + bm.faces[:], direction=\"Y\")\n\n    # Apply vertical symmetry sometimes - this can cause spaceship \"islands\", so disabled by default\n    if allow_vertical_symmetry and random() > 0.5:\n        bmesh.ops.symmetrize(bm, input=bm.verts[:] + bm.edges[:] + bm.faces[:], direction=\"Z\")\n\n    # Finish up, write the bmesh into a new mesh\n    me = bpy.data.meshes.new('Mesh')\n    bm.to_mesh(me)\n    bm.free()\n\n    # Add the mesh to the scene\n    scene = bpy.context.scene\n    obj = bpy.data.objects.new('Spaceship', me)\n    # scene.objects.link(obj)\n    scene.collection.objects.link(obj)\n\n    # Select and make active\n    bpy.context.view_layer.objects.active = obj\n    obj.select_set(True)\n    # scene.objects.active = obj\n    # obj.select = True\n\n    # Recenter the object to its center of mass\n    bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_MASS')\n    ob = bpy.context.object\n    ob.location = (0, 0, 0)\n\n    # Add a fairly broad bevel modifier to angularize shape\n    if apply_bevel_modifier:\n        bevel_modifier = ob.modifiers.new('Bevel', 'BEVEL')\n        bevel_modifier.width = uniform(5, 20)\n        bevel_modifier.offset_type = 'PERCENT'\n        bevel_modifier.segments = 2\n        bevel_modifier.profile = 0.25\n        bevel_modifier.limit_method = 'NONE'\n\n    # Add materials to the spaceship\n    me = ob.data\n    materials = create_materials()\n    # materials = []\n    for mat in materials:\n        if assign_materials:\n            me.materials.append(mat)\n        else:\n            me.materials.append(bpy.data.materials.new(name=\"Material\"))\n\n    return obj\n\nif __name__ == \"__main__\":\n\n    # When true, this script will generate a single spaceship in the scene.\n    # When false, this script will render multiple movie frames showcasing lots of ships.\n    generate_single_spaceship = True\n\n    if generate_single_spaceship:\n        # Reset the scene, generate a single spaceship and focus on it\n        reset_scene()\n        customseed = '' # add anything here to generate the same spaceship\n        obj = generate_spaceship(customseed)\n\n        # View the selected object in all views\n        for area in bpy.context.screen.areas:\n            if area.type == 'VIEW_3D':\n                ctx = bpy.context.copy()\n                ctx['area'] = area\n                ctx['region'] = area.regions[-1]\n                bpy.ops.view3d.view_selected(ctx)\n    else:\n        # Export a movie showcasing many different kinds of ships\n\n        # Settings\n        output_path = '' # leave empty to use script folder\n        total_movie_duration = 16\n        total_spaceship_duration = 1\n        yaw_rate = 45 # degrees/sec\n        yaw_offset = 220 # degrees/sec\n        camera_pole_rate = 1\n        camera_pole_pitch_min = 15 # degrees\n        camera_pole_pitch_max = 30 # degrees\n        camera_pole_pitch_offset = 0 # degrees\n        camera_pole_length = 10\n        camera_refocus_object_every_frame = False\n        fov = 60 # degrees\n        fps = 30\n        res_x = 1920\n        res_y = 1080\n\n        # Batch render the movie frames\n        inv_fps = 1/float(fps)\n        movie_duration = 0\n        spaceship_duration = total_spaceship_duration\n        scene = bpy.data.scenes[\"Scene\"]\n        scene.render.resolution_x = res_x\n        scene.render.resolution_y = res_y\n        scene.camera.rotation_mode = 'XYZ'\n        scene.camera.data.angle = radians(fov)\n        frame = 0\n        timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')\n        while movie_duration < total_movie_duration:\n            movie_duration += inv_fps\n            spaceship_duration += inv_fps\n            if spaceship_duration >= total_spaceship_duration:\n                spaceship_duration -= total_spaceship_duration\n\n                # Generate a new spaceship\n                reset_scene()\n                obj = generate_spaceship()\n\n                # look for a mirror plane in the scene, and position it just underneath the ship if found\n                lowest_z = centre = min((Vector(b).z for b in obj.bound_box))\n                plane_obj = bpy.data.objects['Plane'] if 'Plane' in bpy.data.objects else None\n                if plane_obj:\n                    plane_obj.location.z = lowest_z - 0.3\n\n            # Position and orient the camera\n            rad = radians(yaw_offset + (yaw_rate * movie_duration))\n            camera_pole_pitch_lerp = 0.5 * (1 + cos(camera_pole_rate * movie_duration)) # 0-1\n            camera_pole_pitch = camera_pole_pitch_max * camera_pole_pitch_lerp + \\\n                                camera_pole_pitch_min * (1 - camera_pole_pitch_lerp)\n            scene.camera.rotation_euler = (radians(90 - camera_pole_pitch + camera_pole_pitch_offset), 0, rad)\n            scene.camera.location = (sin(rad) * camera_pole_length,\n                                     cos(rad) * -camera_pole_length,\n                                     sin(radians(camera_pole_pitch))*camera_pole_length)\n            if camera_refocus_object_every_frame:\n                bpy.ops.view3d.camera_to_view_selected()\n\n            # Render the scene to disk\n            script_path = bpy.context.space_data.text.filepath if bpy.context.space_data else __file__\n            folder = output_path if output_path else os.path.split(os.path.realpath(script_path))[0]\n            filename = os.path.join('renders', timestamp, timestamp + '_' + str(frame).zfill(5) + '.png')\n            bpy.data.scenes['Scene'].render.filepath = os.path.join(folder, filename)\n            print('Rendering frame ' + str(frame) + '...')\n            bpy.ops.render.render(write_still=True)\n            frame += 1\n"
  }
]