kopia lustrzana https://github.com/a1studmuffin/SpaceshipGenerator
Added source code and textures
rodzic
14bc162cc6
commit
06bef8cd48
|
@ -0,0 +1,814 @@
|
|||
#
|
||||
# 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 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
|
||||
|
||||
# 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):
|
||||
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
|
||||
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):
|
||||
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))
|
||||
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):
|
||||
global img_cache
|
||||
img = None
|
||||
if filename in img_cache:
|
||||
# Image has been cached already, so just use that.
|
||||
img = img_cache[filename]
|
||||
else:
|
||||
# We haven't cached this asset yet, so load it from disk.
|
||||
|
||||
# Figure out the script path depending on our context (command-line or in-editor)
|
||||
script_path = bpy.context.space_data.text.filepath if bpy.context.space_data else __file__
|
||||
|
||||
# Get the folder the script lives in. If it lives in a .blend file, strip that off too.
|
||||
script_folder = os.path.split(os.path.realpath(script_path))[0]
|
||||
if script_folder.endswith('.blend'):
|
||||
script_folder = os.path.split(script_folder)[0]
|
||||
|
||||
filepath = os.path.join(script_folder, filename)
|
||||
try:
|
||||
img = bpy.data.images.load(filepath)
|
||||
except:
|
||||
raise NameError("Cannot load image %s" % filepath)
|
||||
|
||||
# Cache the asset
|
||||
img_cache[filename] = img
|
||||
|
||||
# Create and return a new texture using img
|
||||
tex = bpy.data.textures.new(name, tex_type)
|
||||
tex.image = img
|
||||
tex.image.use_alpha = use_alpha
|
||||
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', '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', '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', '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):
|
||||
if random_seed:
|
||||
seed(random_seed)
|
||||
|
||||
# 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)
|
||||
|
||||
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)
|
||||
|
||||
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"))
|
||||
|
||||
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()
|
||||
seed = '' # add anything here to generate the same spaceship
|
||||
obj = generate_spaceship(seed)
|
||||
|
||||
# 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 = '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
|
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 97 KiB |
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 366 KiB |
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 5.4 MiB |
Ładowanie…
Reference in New Issue