Created
June 20, 2016 15:04
-
-
Save oznogon/f7e3031b7fa351c106add5052c297d25 to your computer and use it in GitHub Desktop.
spaceship_generator.py modified for EE
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# spaceship_generator.py | |
# | |
# This is a Blender script that uses procedural generation to create | |
# textured 3D spaceship models. Tested with Blender 2.77a. | |
# | |
# michael@spaceduststudios.com | |
# https://github.com/a1studmuffin/SpaceshipGenerator | |
# | |
import sys | |
import os | |
import os.path | |
import bpy | |
import bmesh | |
import datetime | |
from math import sqrt, radians, pi, cos, sin | |
from mathutils import Vector, Matrix | |
from random import random, seed, uniform, randint, randrange | |
from enum import IntEnum | |
from colorsys import hls_to_rgb | |
DIR = os.path.dirname(os.path.abspath(__file__)) | |
def resource_path(*path_components): | |
return os.path.join(DIR, *path_components) | |
# Deletes all existing spaceships and unused materials from the scene | |
def reset_scene(): | |
for item in bpy.data.objects: | |
item.select = item.name.startswith('Spaceship') | |
bpy.ops.object.delete() | |
for material in bpy.data.materials: | |
if not material.users: | |
bpy.data.materials.remove(material) | |
for texture in bpy.data.textures: | |
if not texture.users: | |
bpy.data.textures.remove(texture) | |
# Extrudes a face along its normal by translate_forwards units. | |
# Returns the new face, and optionally fills out extruded_face_list | |
# with all the additional side faces created from the extrusion. | |
def extrude_face(bm, face, translate_forwards=0.0, extruded_face_list=None): | |
new_faces = bmesh.ops.extrude_discrete_faces(bm, faces=[face])['faces'] | |
if extruded_face_list != None: | |
extruded_face_list += new_faces[:] | |
new_face = new_faces[0] | |
bmesh.ops.translate(bm, | |
vec=new_face.normal * translate_forwards, | |
verts=new_face.verts) | |
return new_face | |
# Similar to extrude_face, except corrigates the geometry to create "ribs". | |
# Returns the new face. | |
def ribbed_extrude_face(bm, face, translate_forwards, num_ribs=3, rib_scale=0.9): | |
translate_forwards_per_rib = translate_forwards / float(num_ribs) | |
new_face = face | |
for i in range(num_ribs): | |
new_face = extrude_face(bm, new_face, translate_forwards_per_rib * 0.25) | |
new_face = extrude_face(bm, new_face, 0.0) | |
scale_face(bm, new_face, rib_scale, rib_scale, rib_scale) | |
new_face = extrude_face(bm, new_face, translate_forwards_per_rib * 0.5) | |
new_face = extrude_face(bm, new_face, 0.0) | |
scale_face(bm, new_face, 1 / rib_scale, 1 / rib_scale, 1 / rib_scale) | |
new_face = extrude_face(bm, new_face, translate_forwards_per_rib * 0.25) | |
return new_face | |
# Scales a face in local face space. Ace! | |
def scale_face(bm, face, scale_x, scale_y, scale_z): | |
face_space = get_face_matrix(face) | |
face_space.invert() | |
bmesh.ops.scale(bm, | |
vec=Vector((scale_x, scale_y, scale_z)), | |
space=face_space, | |
verts=face.verts) | |
# Returns a rough 4x4 transform matrix for a face (doesn't handle | |
# distortion/shear) with optional position override. | |
def get_face_matrix(face, pos=None): | |
x_axis = (face.verts[1].co - face.verts[0].co).normalized() | |
z_axis = -face.normal | |
y_axis = z_axis.cross(x_axis) | |
if not pos: | |
pos = face.calc_center_bounds() | |
# Construct a 4x4 matrix from axes + position: | |
# http://i.stack.imgur.com/3TnQP.png | |
mat = Matrix() | |
mat[0][0] = x_axis.x | |
mat[1][0] = x_axis.y | |
mat[2][0] = x_axis.z | |
mat[3][0] = 0 | |
mat[0][1] = y_axis.x | |
mat[1][1] = y_axis.y | |
mat[2][1] = y_axis.z | |
mat[3][1] = 0 | |
mat[0][2] = z_axis.x | |
mat[1][2] = z_axis.y | |
mat[2][2] = z_axis.z | |
mat[3][2] = 0 | |
mat[0][3] = pos.x | |
mat[1][3] = pos.y | |
mat[2][3] = pos.z | |
mat[3][3] = 1 | |
return mat | |
# Returns the rough length and width of a quad face. | |
# Assumes a perfect rectangle, but close enough. | |
def get_face_width_and_height(face): | |
if not face.is_valid or len(face.verts[:]) < 4: | |
return -1, -1 | |
width = (face.verts[0].co - face.verts[1].co).length | |
height = (face.verts[2].co - face.verts[1].co).length | |
return width, height | |
# Returns the rough aspect ratio of a face. Always >= 1. | |
def get_aspect_ratio(face): | |
if not face.is_valid: | |
return 1.0 | |
face_aspect_ratio = max(0.01, face.edges[0].calc_length() / face.edges[1].calc_length()) | |
if face_aspect_ratio < 1.0: | |
face_aspect_ratio = 1.0 / face_aspect_ratio | |
return face_aspect_ratio | |
# Returns true if this face is pointing behind the ship | |
def is_rear_face(face): | |
return face.normal.x < -0.95 | |
# Given a face, splits it into a uniform grid and extrudes each grid face | |
# out and back in again, making an exhaust shape. | |
def add_exhaust_to_face(bm, face, file, ee_generate_modeldata): | |
if not face.is_valid: | |
return | |
# The more square the face is, the more grid divisions it might have | |
num_cuts = randint(1, int(4 - get_aspect_ratio(face))) | |
result = bmesh.ops.subdivide_edges(bm, | |
edges=face.edges[:], | |
cuts=num_cuts, | |
fractal=0.02, | |
use_grid_fill=True) | |
exhaust_length = uniform(0.1, 0.2) | |
scale_outer = 1 / uniform(1.3, 1.6) | |
scale_inner = 1 / uniform(1.05, 1.1) | |
for face in result['geom']: | |
if isinstance(face, bmesh.types.BMFace): | |
if is_rear_face(face): | |
face.material_index = Material.hull_dark | |
face = extrude_face(bm, face, exhaust_length) | |
scale_face(bm, face, scale_outer, scale_outer, scale_outer) | |
extruded_face_list = [] | |
face = extrude_face(bm, face, -exhaust_length * 0.9, extruded_face_list) | |
for extruded_face in extruded_face_list: | |
extruded_face.material_index = Material.exhaust_burn | |
# TODO: Get and convert material color and size. | |
if ee_generate_modeldata: | |
ee_modeldata_engine_vector = extruded_face.calc_center_bounds() | |
file.write('model:addEngineEmitter(' + ('%.6f' % ee_modeldata_engine_vector.x) + ', ' + ('%.6f' % ee_modeldata_engine_vector.y) + ', ' + ('%.6f' % ee_modeldata_engine_vector.z) + ', 1.0, 0.2, 0.2, 1.5)\n') | |
scale_face(bm, face, scale_inner, scale_inner, scale_inner) | |
# Given a face, splits it up into a smaller uniform grid and extrudes each grid cell. | |
def add_grid_to_face(bm, face): | |
if not face.is_valid: | |
return | |
result = bmesh.ops.subdivide_edges(bm, | |
edges=face.edges[:], | |
cuts=randint(2, 4), | |
fractal=0.02, | |
use_grid_fill=True, | |
use_single_edge=False) | |
grid_length = uniform(0.025, 0.15) | |
scale = 0.8 | |
for face in result['geom']: | |
if isinstance(face, bmesh.types.BMFace): | |
material_index = Material.hull_lights if random() > 0.5 else Material.hull | |
extruded_face_list = [] | |
face = extrude_face(bm, face, grid_length, extruded_face_list) | |
for extruded_face in extruded_face_list: | |
if abs(face.normal.z) < 0.707: # side face | |
extruded_face.material_index = material_index | |
scale_face(bm, face, scale, scale, scale) | |
# Given a face, adds some cylinders along it in a grid pattern. | |
def add_cylinders_to_face(bm, face): | |
if not face.is_valid or len(face.verts[:]) < 4: | |
return | |
horizontal_step = randint(1, 3) | |
vertical_step = randint(1, 3) | |
num_segments = randint(6, 12) | |
face_width, face_height = get_face_width_and_height(face) | |
cylinder_depth = 1.3 * min(face_width / (horizontal_step + 2), | |
face_height / (vertical_step + 2)) | |
cylinder_size = cylinder_depth * 0.5 | |
for h in range(horizontal_step): | |
top = face.verts[0].co.lerp( | |
face.verts[1].co, (h + 1) / float(horizontal_step + 1)) | |
bottom = face.verts[3].co.lerp( | |
face.verts[2].co, (h + 1) / float(horizontal_step + 1)) | |
for v in range(vertical_step): | |
pos = top.lerp(bottom, (v + 1) / float(vertical_step + 1)) | |
cylinder_matrix = get_face_matrix(face, pos) * \ | |
Matrix.Rotation(radians(90), 3, 'X').to_4x4() | |
bmesh.ops.create_cone(bm, | |
cap_ends=True, | |
cap_tris=False, | |
segments=num_segments, | |
diameter1=cylinder_size, | |
diameter2=cylinder_size, | |
depth=cylinder_depth, | |
matrix=cylinder_matrix) | |
# Given a face, adds some weapon turrets to it in a grid pattern. | |
# Each turret will have a random orientation. | |
def add_weapons_to_face(bm, face, file, ee_generate_modeldata): | |
if not face.is_valid or len(face.verts[:]) < 4: | |
return | |
horizontal_step = randint(1, 2) | |
vertical_step = randint(1, 2) | |
num_segments = 16 | |
face_width, face_height = get_face_width_and_height(face) | |
weapon_size = 0.5 * min(face_width / (horizontal_step + 2), | |
face_height / (vertical_step + 2)) | |
weapon_depth = weapon_size * 0.2 | |
for h in range(horizontal_step): | |
top = face.verts[0].co.lerp( | |
face.verts[1].co, (h + 1) / float(horizontal_step + 1)) | |
bottom = face.verts[3].co.lerp( | |
face.verts[2].co, (h + 1) / float(horizontal_step + 1)) | |
for v in range(vertical_step): | |
pos = top.lerp(bottom, (v + 1) / float(vertical_step + 1)) | |
if ee_generate_modeldata: | |
ee_modeldata_beam_vector = pos | |
file.write('model:addBeamPosition(' + ('%.6f' % ee_modeldata_beam_vector.x) + ',' + ('%.6f' % ee_modeldata_beam_vector.y) + ',' + ('%.6f' % ee_modeldata_beam_vector.z) + ')\n') | |
file.write('model:addTubePosition(' + ('%.6f' % ee_modeldata_beam_vector.x) + ',' + ('%.6f' % ee_modeldata_beam_vector.y) + ',' + ('%.6f' % ee_modeldata_beam_vector.z) + ')\n') | |
face_matrix = get_face_matrix(face, pos + face.normal * weapon_depth * 0.5) * \ | |
Matrix.Rotation(radians(uniform(0, 90)), 3, 'Z').to_4x4() | |
# Turret foundation | |
bmesh.ops.create_cone(bm, | |
cap_ends=True, | |
cap_tris=False, | |
segments=num_segments, | |
diameter1=weapon_size * 0.9, | |
diameter2=weapon_size, | |
depth=weapon_depth, | |
matrix=face_matrix) | |
# Turret left guard | |
left_guard_mat = face_matrix * \ | |
Matrix.Rotation(radians(90), 3, 'Y').to_4x4() * \ | |
Matrix.Translation(Vector((0, 0, weapon_size * 0.6))).to_4x4() | |
bmesh.ops.create_cone(bm, | |
cap_ends=True, | |
cap_tris=False, | |
segments=num_segments, | |
diameter1=weapon_size * 0.6, | |
diameter2=weapon_size * 0.5, | |
depth=weapon_depth * 2, | |
matrix=left_guard_mat) | |
# Turret right guard | |
right_guard_mat = face_matrix * \ | |
Matrix.Rotation(radians(90), 3, 'Y').to_4x4() * \ | |
Matrix.Translation(Vector((0, 0, weapon_size * -0.6))).to_4x4() | |
bmesh.ops.create_cone(bm, | |
cap_ends=True, | |
cap_tris=False, | |
segments=num_segments, | |
diameter1=weapon_size * 0.5, | |
diameter2=weapon_size * 0.6, | |
depth=weapon_depth * 2, | |
matrix=right_guard_mat) | |
# Turret housing | |
upward_angle = uniform(0, 45) | |
turret_house_mat = face_matrix * \ | |
Matrix.Rotation(radians(upward_angle), 3, 'X').to_4x4() * \ | |
Matrix.Translation(Vector((0, weapon_size * -0.4, 0))).to_4x4() | |
bmesh.ops.create_cone(bm, | |
cap_ends=True, | |
cap_tris=False, | |
segments=8, | |
diameter1=weapon_size * 0.4, | |
diameter2=weapon_size * 0.4, | |
depth=weapon_depth * 5, | |
matrix=turret_house_mat) | |
# Turret barrels L + R | |
bmesh.ops.create_cone(bm, | |
cap_ends=True, | |
cap_tris=False, | |
segments=8, | |
diameter1=weapon_size * 0.1, | |
diameter2=weapon_size * 0.1, | |
depth=weapon_depth * 6, | |
matrix=turret_house_mat * \ | |
Matrix.Translation(Vector((weapon_size * 0.2, 0, -weapon_size))).to_4x4()) | |
bmesh.ops.create_cone(bm, | |
cap_ends=True, | |
cap_tris=False, | |
segments=8, | |
diameter1=weapon_size * 0.1, | |
diameter2=weapon_size * 0.1, | |
depth=weapon_depth * 6, | |
matrix=turret_house_mat * \ | |
Matrix.Translation(Vector((weapon_size * -0.2, 0, -weapon_size))).to_4x4()) | |
# Given a face, adds a sphere on the surface, partially inset. | |
def add_sphere_to_face(bm, face): | |
if not face.is_valid: | |
return | |
face_width, face_height = get_face_width_and_height(face) | |
sphere_size = uniform(0.4, 1.0) * min(face_width, face_height) | |
sphere_matrix = get_face_matrix(face, | |
face.calc_center_bounds() - face.normal * \ | |
uniform(0, sphere_size * 0.5)) | |
result = bmesh.ops.create_icosphere(bm, | |
subdivisions=3, | |
diameter=sphere_size, | |
matrix=sphere_matrix) | |
for vert in result['verts']: | |
for face in vert.link_faces: | |
face.material_index = Material.hull | |
# Given a face, adds some pointy intimidating antennas. | |
def add_surface_antenna_to_face(bm, face): | |
if not face.is_valid or len(face.verts[:]) < 4: | |
return | |
horizontal_step = randint(4, 10) | |
vertical_step = randint(4, 10) | |
for h in range(horizontal_step): | |
top = face.verts[0].co.lerp( | |
face.verts[1].co, (h + 1) / float(horizontal_step + 1)) | |
bottom = face.verts[3].co.lerp( | |
face.verts[2].co, (h + 1) / float(horizontal_step + 1)) | |
for v in range(vertical_step): | |
if random() > 0.9: | |
pos = top.lerp(bottom, (v + 1) / float(vertical_step + 1)) | |
face_size = sqrt(face.calc_area()) | |
depth = uniform(0.1, 1.5) * face_size | |
depth_short = depth * uniform(0.02, 0.15) | |
base_diameter = uniform(0.005, 0.05) | |
material_index = Material.hull if random() > 0.5 else Material.hull_dark | |
# Spire | |
num_segments = uniform(3, 6) | |
result = bmesh.ops.create_cone(bm, | |
cap_ends=False, | |
cap_tris=False, | |
segments=num_segments, | |
diameter1=0, | |
diameter2=base_diameter, | |
depth=depth, | |
matrix=get_face_matrix(face, pos + face.normal * depth * 0.5)) | |
for vert in result['verts']: | |
for vert_face in vert.link_faces: | |
vert_face.material_index = material_index | |
# Base | |
result = bmesh.ops.create_cone(bm, | |
cap_ends=True, | |
cap_tris=False, | |
segments=num_segments, | |
diameter1=base_diameter * uniform(1, 1.5), | |
diameter2=base_diameter * uniform(1.5, 2), | |
depth=depth_short, | |
matrix=get_face_matrix(face, pos + face.normal * depth_short * 0.45)) | |
for vert in result['verts']: | |
for vert_face in vert.link_faces: | |
vert_face.material_index = material_index | |
# Given a face, adds a glowing "landing pad" style disc. | |
def add_disc_to_face(bm, face): | |
if not face.is_valid: | |
return | |
face_width, face_height = get_face_width_and_height(face) | |
depth = 0.125 * min(face_width, face_height) | |
bmesh.ops.create_cone(bm, | |
cap_ends=True, | |
cap_tris=False, | |
segments=32, | |
diameter1=depth * 3, | |
diameter2=depth * 4, | |
depth=depth, | |
matrix=get_face_matrix(face, face.calc_center_bounds() + face.normal * depth * 0.5)) | |
result = bmesh.ops.create_cone(bm, | |
cap_ends=False, | |
cap_tris=False, | |
segments=32, | |
diameter1=depth * 1.25, | |
diameter2=depth * 2.25, | |
depth=0.0, | |
matrix=get_face_matrix(face, face.calc_center_bounds() + face.normal * depth * 1.05)) | |
for vert in result['verts']: | |
for face in vert.link_faces: | |
face.material_index = Material.glow_disc | |
class Material(IntEnum): | |
hull = 0 # Plain spaceship hull | |
hull_lights = 1 # Spaceship hull with emissive windows | |
hull_dark = 2 # Plain Spaceship hull, darkened | |
exhaust_burn = 3 # Emissive engine burn material | |
glow_disc = 4 # Emissive landing pad disc material | |
# Creates a texture given a texture name, texture type, and filename. | |
# Uses an image cache dictionary to prevent loading the same asset from disk twice. | |
# Returns the texture. | |
img_cache = {} | |
def create_texture(name, tex_type, filename, use_alpha=True): | |
if filename in img_cache: | |
# Image has been cached already, so just use that. | |
img = img_cache[(filename, use_alpha)] | |
else: | |
# We haven't cached this asset yet, so load it from disk. | |
try: | |
img = bpy.data.images.load(filename) | |
except: | |
raise IOError("Cannot load image: %s" % filename) | |
img.use_alpha = use_alpha | |
img.pack() | |
# Cache the asset | |
img_cache[(filename, use_alpha)] = img | |
# Create and return a new texture using img | |
tex = bpy.data.textures.new(name, tex_type) | |
tex.image = img | |
return tex | |
# Adds a hull normal map texture slot to a material. | |
def add_hull_normal_map(mat, hull_normal_colortex): | |
mtex = mat.texture_slots.add() | |
mtex.texture = hull_normal_colortex | |
mtex.texture_coords = 'GLOBAL' # global UVs, yolo | |
mtex.mapping = 'CUBE' | |
mtex.use_map_color_diffuse = False | |
mtex.use_map_normal = True | |
mtex.normal_factor = 1 | |
mtex.bump_method = 'BUMP_BEST_QUALITY' | |
# Sets some basic properties for a hull material. | |
def set_hull_mat_basics(mat, color, hull_normal_colortex): | |
mat.specular_intensity = 0.1 | |
mat.diffuse_color = color | |
add_hull_normal_map(mat, hull_normal_colortex) | |
# Creates all our materials and returns them as a list. | |
def create_materials(): | |
ret = [] | |
for material in Material: | |
ret.append(bpy.data.materials.new(material.name)) | |
# Choose a base color for the spaceship hull | |
hull_base_color = hls_to_rgb( | |
random(), uniform(0.05, 0.5), uniform(0, 0.25)) | |
# Load up the hull normal map | |
hull_normal_colortex = create_texture( | |
'ColorTex', 'IMAGE', resource_path('textures', 'hull_normal.png')) | |
hull_normal_colortex.use_normal_map = True | |
# Build the hull texture | |
mat = ret[Material.hull] | |
set_hull_mat_basics(mat, hull_base_color, hull_normal_colortex) | |
# Build the hull_lights texture | |
mat = ret[Material.hull_lights] | |
set_hull_mat_basics(mat, hull_base_color, hull_normal_colortex) | |
# Add a diffuse layer that sets the window color | |
mtex = mat.texture_slots.add() | |
mtex.texture = create_texture( | |
'ColorTex', 'IMAGE', resource_path('textures', 'hull_lights_diffuse.png')) | |
mtex.texture_coords = 'GLOBAL' | |
mtex.mapping = 'CUBE' | |
mtex.blend_type = 'ADD' | |
mtex.use_map_color_diffuse = True | |
mtex.use_rgb_to_intensity = True | |
mtex.color = hls_to_rgb(random(), uniform(0.5, 1), uniform(0, 0.5)) | |
# Add an emissive layer that lights up the windows | |
mtex = mat.texture_slots.add() | |
mtex.texture = create_texture( | |
'ColorTex', 'IMAGE', resource_path('textures', 'hull_lights_emit.png'), False) | |
mtex.texture_coords = 'GLOBAL' | |
mtex.mapping = 'CUBE' | |
mtex.use_map_emit = True | |
mtex.emit_factor = 2.0 | |
mtex.blend_type = 'ADD' | |
mtex.use_map_color_diffuse = False | |
# Build the hull_dark texture | |
mat = ret[Material.hull_dark] | |
set_hull_mat_basics(mat, [0.3 * x for x in hull_base_color], hull_normal_colortex) | |
# Choose a glow color for the exhaust + glow discs | |
glow_color = hls_to_rgb(random(), uniform(0.5, 1), 1) | |
# Build the exhaust_burn texture | |
mat = ret[Material.exhaust_burn] | |
mat.diffuse_color = glow_color | |
mat.emit = 1.0 | |
# Build the glow_disc texture | |
mat = ret[Material.glow_disc] | |
mat.diffuse_color = glow_color | |
mat.emit = 1.0 | |
return ret | |
# Generates a textured spaceship mesh and returns the object. | |
# Just uses global cube texture coordinates rather than generating UVs. | |
# Takes an optional random seed value to generate a specific spaceship. | |
# Allows overriding of some parameters that affect generation. | |
def generate_spaceship(random_seed='', | |
num_hull_segments_min=3, | |
num_hull_segments_max=6, | |
create_asymmetry_segments=True, | |
num_asymmetry_segments_min=1, | |
num_asymmetry_segments_max=5, | |
create_face_detail=True, | |
allow_horizontal_symmetry=True, | |
allow_vertical_symmetry=False, | |
apply_bevel_modifier=True, | |
assign_materials=True, | |
ee_generate_uvbake=True, | |
ee_generate_modeldata=True): | |
if not random_seed: | |
random_seed = randint(0, sys.maxsize) | |
seed(random_seed) | |
seed_used = str(random_seed) | |
ee_timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S') | |
ee_model_name = seed_used + '_' + ee_timestamp | |
ee_obj = os.path.join(os.path.dirname(bpy.data.filepath), ee_model_name + ".obj") | |
if ee_generate_modeldata: | |
ee_modeldata_file = open(os.path.join(os.path.dirname(bpy.data.filepath), ee_model_name + "_model_data.lua"), 'w') | |
ee_modeldata_file.write('model = ModelData()\n') | |
ee_modeldata_file.write('model:setName("' + ee_model_name + '")\n') | |
ee_modeldata_file.write('model:setMesh("' + ee_model_name + '.obj")\n') | |
ee_modeldata_file.write('model:setTexture("' + ee_model_name + '-baked.png")\n') | |
ee_modeldata_file.write('model:setSpecular("' + ee_model_name + '-specular.png")\n') | |
ee_modeldata_file.write('model:setIllumination("' + ee_model_name + '-illumination.png")\n') | |
ee_modeldata_file.write('model:setScale(48)\n') | |
ee_modeldata_file.write('model:setRadius(100)\n') | |
if ee_generate_uvbake: | |
ee_uvbake_texture_file = os.path.join(os.path.dirname(bpy.data.filepath), "_" + ee_model_name + "-baked.png") | |
ee_uvbake_specular_file = os.path.join(os.path.dirname(bpy.data.filepath), "_" + ee_model_name + "-specular.png") | |
ee_uvbake_illumination_file = os.path.join(os.path.dirname(bpy.data.filepath), "_" + ee_model_name + "-illumination.png") | |
# Let's start with a unit BMesh cube scaled randomly | |
bm = bmesh.new() | |
bmesh.ops.create_cube(bm, size=1) | |
scale_vector = Vector( | |
(uniform(0.75, 2.0), uniform(0.75, 2.0), uniform(0.75, 2.0))) | |
bmesh.ops.scale(bm, vec=scale_vector, verts=bm.verts) | |
# Extrude out the hull along the X axis, adding some semi-random perturbations | |
for face in bm.faces[:]: | |
if abs(face.normal.x) > 0.5: | |
hull_segment_length = uniform(0.3, 1) | |
num_hull_segments = randrange(num_hull_segments_min, num_hull_segments_max) | |
hull_segment_range = range(num_hull_segments) | |
for i in hull_segment_range: | |
is_last_hull_segment = i == hull_segment_range[-1] | |
val = random() | |
if val > 0.1: | |
# Most of the time, extrude out the face with some random deviations | |
face = extrude_face(bm, face, hull_segment_length) | |
if random() > 0.75: | |
face = extrude_face( | |
bm, face, hull_segment_length * 0.25) | |
# Maybe apply some scaling | |
if random() > 0.5: | |
sy = uniform(1.2, 1.5) | |
sz = uniform(1.2, 1.5) | |
if is_last_hull_segment or random() > 0.5: | |
sy = 1 / sy | |
sz = 1 / sz | |
scale_face(bm, face, 1, sy, sz) | |
# Maybe apply some sideways translation | |
if random() > 0.5: | |
sideways_translation = Vector( | |
(0, 0, uniform(0.1, 0.4) * scale_vector.z * hull_segment_length)) | |
if random() > 0.5: | |
sideways_translation = -sideways_translation | |
bmesh.ops.translate(bm, | |
vec=sideways_translation, | |
verts=face.verts) | |
# Maybe add some rotation around Y axis | |
if random() > 0.5: | |
angle = 5 | |
if random() > 0.5: | |
angle = -angle | |
bmesh.ops.rotate(bm, | |
verts=face.verts, | |
cent=(0, 0, 0), | |
matrix=Matrix.Rotation(radians(angle), 3, 'Y')) | |
else: | |
# Rarely, create a ribbed section of the hull | |
rib_scale = uniform(0.75, 0.95) | |
face = ribbed_extrude_face( | |
bm, face, hull_segment_length, randint(2, 4), rib_scale) | |
# Add some large asynmmetrical sections of the hull that stick out | |
if create_asymmetry_segments: | |
for face in bm.faces[:]: | |
# Skip any long thin faces as it'll probably look stupid | |
if get_aspect_ratio(face) > 4: | |
continue | |
if random() > 0.85: | |
hull_piece_length = uniform(0.1, 0.4) | |
for i in range(randrange(num_asymmetry_segments_min, num_asymmetry_segments_max)): | |
face = extrude_face(bm, face, hull_piece_length) | |
# Maybe apply some scaling | |
if random() > 0.25: | |
s = 1 / uniform(1.1, 1.5) | |
scale_face(bm, face, s, s, s) | |
# Now the basic hull shape is built, let's categorize + add detail to all the faces | |
if create_face_detail: | |
engine_faces = [] | |
grid_faces = [] | |
antenna_faces = [] | |
weapon_faces = [] | |
sphere_faces = [] | |
disc_faces = [] | |
cylinder_faces = [] | |
for face in bm.faces[:]: | |
# Skip any long thin faces as it'll probably look stupid | |
if get_aspect_ratio(face) > 3: | |
continue | |
# Spin the wheel! Let's categorize + assign some materials | |
val = random() | |
if is_rear_face(face): # rear face | |
if not engine_faces or val > 0.75: | |
engine_faces.append(face) | |
elif val > 0.5: | |
cylinder_faces.append(face) | |
elif val > 0.25: | |
grid_faces.append(face) | |
else: | |
face.material_index = Material.hull_lights | |
elif face.normal.x > 0.9: # front face | |
if face.normal.dot(face.calc_center_bounds()) > 0 and val > 0.7: | |
antenna_faces.append(face) # front facing antenna | |
face.material_index = Material.hull_lights | |
elif val > 0.4: | |
grid_faces.append(face) | |
else: | |
face.material_index = Material.hull_lights | |
elif face.normal.z > 0.9: # top face | |
if face.normal.dot(face.calc_center_bounds()) > 0 and val > 0.7: | |
antenna_faces.append(face) # top facing antenna | |
elif val > 0.6: | |
grid_faces.append(face) | |
elif val > 0.3: | |
cylinder_faces.append(face) | |
elif face.normal.z < -0.9: # bottom face | |
if val > 0.75: | |
disc_faces.append(face) | |
elif val > 0.5: | |
grid_faces.append(face) | |
elif val > 0.25: | |
weapon_faces.append(face) | |
elif abs(face.normal.y) > 0.9: # side face | |
if not weapon_faces or val > 0.75: | |
weapon_faces.append(face) | |
elif val > 0.6: | |
grid_faces.append(face) | |
elif val > 0.4: | |
sphere_faces.append(face) | |
else: | |
face.material_index = Material.hull_lights | |
# Now we've categorized, let's actually add the detail | |
for face in engine_faces: | |
add_exhaust_to_face(bm, face, ee_modeldata_file, ee_generate_modeldata) | |
for face in grid_faces: | |
add_grid_to_face(bm, face) | |
for face in antenna_faces: | |
add_surface_antenna_to_face(bm, face) | |
for face in weapon_faces: | |
add_weapons_to_face(bm, face, ee_modeldata_file, ee_generate_modeldata) | |
for face in sphere_faces: | |
add_sphere_to_face(bm, face) | |
for face in disc_faces: | |
add_disc_to_face(bm, face) | |
for face in cylinder_faces: | |
add_cylinders_to_face(bm, face) | |
# Apply horizontal symmetry sometimes | |
if allow_horizontal_symmetry and random() > 0.5: | |
bmesh.ops.symmetrize(bm, input=bm.verts[:] + bm.edges[:] + bm.faces[:], direction=1) | |
# Apply vertical symmetry sometimes - this can cause spaceship "islands", so disabled by default | |
if allow_vertical_symmetry and random() > 0.5: | |
bmesh.ops.symmetrize(bm, input=bm.verts[:] + bm.edges[:] + bm.faces[:], direction=2) | |
# Finish up, write the bmesh into a new mesh | |
me = bpy.data.meshes.new('Mesh') | |
bm.to_mesh(me) | |
bm.free() | |
# Add the mesh to the scene | |
scene = bpy.context.scene | |
obj = bpy.data.objects.new('Spaceship', me) | |
scene.objects.link(obj) | |
# Select and make active | |
scene.objects.active = obj | |
obj.select = True | |
# Recenter the object to its center of mass | |
bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_MASS') | |
ob = bpy.context.object | |
ob.location = (0, 0, 0) | |
# Add a fairly broad bevel modifier to angularize shape | |
if apply_bevel_modifier: | |
bpy.ops.object.modifier_add(type='BEVEL') | |
ob.modifiers["Bevel"].width = uniform(5, 20) | |
ob.modifiers["Bevel"].offset_type = 'PERCENT' | |
ob.modifiers["Bevel"].segments = 2 | |
ob.modifiers["Bevel"].profile = 0.25 | |
ob.modifiers["Bevel"].limit_method = 'NONE' | |
# Add materials to the spaceship | |
me = ob.data | |
materials = create_materials() | |
for mat in materials: | |
if assign_materials: | |
me.materials.append(mat) | |
else: | |
me.materials.append(bpy.data.materials.new(name="Material")) | |
if ee_generate_uvbake: | |
# Switch to edit mode. | |
bpy.context.window.screen = bpy.data.screens['Default'] | |
bpy.context.area.type = 'VIEW_3D' | |
bpy.ops.object.mode_set(mode='EDIT') | |
# Select everything. | |
bpy.ops.mesh.select_all(action='SELECT') | |
# Unwrap the mesh. | |
bpy.ops.uv.smart_project() | |
# Create a new image. | |
# Bake materials into textures. | |
ee_uvbake_illumination_image = bpy.ops.image.new(name=ee_model_name + '-illumination') | |
# Can't figure out how to get the bake function to select an image. | |
# In the UI, this is as simple as selecting the image from a dropdown. | |
# The bake function complains about every type of invocation, without a | |
# clear explanation of the problem, so we have to bake manually. Stupid | |
# bpy.ops.object.bake(type='COMBINED', use_selected_to_active=True) | |
# bpy.ops.object.bake(type='SPECULAR', use_selected_to_active=True) | |
# bpy.ops.object.bake(type='EMIT', use_selected_to_active=True) | |
bpy.data.scenes["Scene"].render.bake_type = 'FULL' | |
bpy.data.scenes["Scene"].render.use_bake_selected_to_active = True | |
bpy.data.scenes["Scene"].render.use_bake_to_vertex_color = False | |
bpy.data.scenes["Scene"].render.use_bake_clear = True | |
bpy.data.scenes["Scene"].render.bake_distance = 0.000 | |
bpy.data.scenes["Scene"].render.bake_margin = 16 | |
bpy.data.scenes["Scene"].render.bake_bias = 0.001 | |
bpy.data.scenes["Scene"].render.bake_quad_split = 'AUTO' | |
ee_uvbake_texture_file = os.path.join(os.path.dirname(bpy.data.filepath), ee_model_name + "-baked.png") | |
ee_uvbake_specular_file = os.path.join(os.path.dirname(bpy.data.filepath), ee_model_name + "-specular.png") | |
ee_uvbake_illumination_file = os.path.join(os.path.dirname(bpy.data.filepath), ee_model_name + "-illumination.png") | |
# Baking really sucks in bpy! | |
# Switch to image editor and bake. | |
# bpy.context.area.type = 'IMAGE_EDITOR' | |
# bpy.ops.image.new(name=ee_model_name + '-baked') | |
# bpy.ops.object.bake_image() | |
# bpy.ops.image.save_as(filepath=ee_uvbake_texture_file) | |
# bpy.ops.image.new(name=ee_model_name + '-specular') | |
# bpy.data.scenes["Scene"].render.bake_type = 'SPEC_INTENSITY' | |
# bpy.ops.object.bake_image() | |
# bpy.ops.image.save_as(filepath=ee_uvbake_specular_file) | |
# bpy.ops.image.new(name=ee_model_name + '-illumination') | |
# bpy.data.scenes["Scene"].render.bake_type = 'EMIT' | |
# bpy.ops.object.bake_image() | |
# bpy.ops.image.save_as(filepath=ee_uvbake_illumination_file) | |
if ee_generate_modeldata: | |
ee_modeldata_file.close() | |
# Export OBJ. | |
bpy.ops.export_scene.obj(filepath=ee_obj, axis_forward='Z', use_materials=False) | |
return obj | |
if __name__ == "__main__": | |
# When true, this script will generate a single spaceship in the scene. | |
# When false, this script will render multiple movie frames showcasing lots of ships. | |
generate_single_spaceship = True | |
if generate_single_spaceship: | |
# Reset the scene, generate a single spaceship and focus on it | |
reset_scene() | |
customseed = '' # add anything here to generate the same spaceship | |
obj = generate_spaceship(customseed) | |
# View the selected object in all views | |
for area in bpy.context.screen.areas: | |
if area.type == 'VIEW_3D': | |
ctx = bpy.context.copy() | |
ctx['area'] = area | |
ctx['region'] = area.regions[-1] | |
bpy.ops.view3d.view_selected(ctx) | |
else: | |
# Export a movie showcasing many different kinds of ships | |
# Settings | |
output_path = '' # leave empty to use script folder | |
total_movie_duration = 16 | |
total_spaceship_duration = 1 | |
yaw_rate = 45 # degrees/sec | |
yaw_offset = 220 # degrees/sec | |
camera_pole_rate = 1 | |
camera_pole_pitch_min = 15 # degrees | |
camera_pole_pitch_max = 30 # degrees | |
camera_pole_pitch_offset = 0 # degrees | |
camera_pole_length = 10 | |
camera_refocus_object_every_frame = False | |
fov = 60 # degrees | |
fps = 30 | |
res_x = 1920 | |
res_y = 1080 | |
# Batch render the movie frames | |
inv_fps = 1/float(fps) | |
movie_duration = 0 | |
spaceship_duration = total_spaceship_duration | |
scene = bpy.data.scenes["Scene"] | |
scene.render.resolution_x = res_x | |
scene.render.resolution_y = res_y | |
scene.camera.rotation_mode = 'XYZ' | |
scene.camera.data.angle = radians(fov) | |
frame = 0 | |
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S') | |
while movie_duration < total_movie_duration: | |
movie_duration += inv_fps | |
spaceship_duration += inv_fps | |
if spaceship_duration >= total_spaceship_duration: | |
spaceship_duration -= total_spaceship_duration | |
# Generate a new spaceship | |
reset_scene() | |
obj = generate_spaceship() | |
# look for a mirror plane in the scene, and position it just underneath the ship if found | |
lowest_z = centre = min((Vector(b).z for b in obj.bound_box)) | |
plane_obj = bpy.data.objects['Plane'] if 'Plane' in bpy.data.objects else None | |
if plane_obj: | |
plane_obj.location.z = lowest_z - 0.3 | |
# Position and orient the camera | |
rad = radians(yaw_offset + (yaw_rate * movie_duration)) | |
camera_pole_pitch_lerp = 0.5 * (1 + cos(camera_pole_rate * movie_duration)) # 0-1 | |
camera_pole_pitch = camera_pole_pitch_max * camera_pole_pitch_lerp + \ | |
camera_pole_pitch_min * (1 - camera_pole_pitch_lerp) | |
scene.camera.rotation_euler = (radians(90 - camera_pole_pitch + camera_pole_pitch_offset), 0, rad) | |
scene.camera.location = (sin(rad) * camera_pole_length, | |
cos(rad) * -camera_pole_length, | |
sin(radians(camera_pole_pitch))*camera_pole_length) | |
if camera_refocus_object_every_frame: | |
bpy.ops.view3d.camera_to_view_selected() | |
# Render the scene to disk | |
script_path = bpy.context.space_data.text.filepath if bpy.context.space_data else __file__ | |
folder = output_path if output_path else os.path.split(os.path.realpath(script_path))[0] | |
filename = os.path.join('renders', timestamp, timestamp + '_' + str(frame).zfill(5) + '.png') | |
bpy.data.scenes['Scene'].render.filepath = os.path.join(folder, filename) | |
print('Rendering frame ' + str(frame) + '...') | |
bpy.ops.render.render(write_still=True) | |
frame += 1 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment