Update!
Firstly, the creation script that I added to this post has been edited to fix an issue with vertex normals.
There seems to be a bug in flatten_strong
with the .fbx models in particular, where it applies the scale on the GeomNode to its vertex normals instead of preserving their unit-length. (A bug report has already been filed here.) The edited script works around this issue by setting a new format on the vertex data, which seems to fix it.
The biggest change however, is in the way “plates” – let’s just call them “parts” in general from now on, as they are understood to be the most atomic elements of the starship – of the main hull are generated! Up till now, they were all nicely generated in succession, while we were patiently(?) waiting for the whole procedure to finish. After watching that for a while, it gets predictable, repetitive and thus quite boring. To spice things up a bit, I’ve moved up the generation start of those parts that can be considered to make up the next “section” of the main starship hull. Such a section is like a vertical slice of the hull, or a “ring of plates”.
Specifically, when 2 parts have been generated for the current section, the first part of the next section gets generated, together with the third part of the current section. This again decreases the total generation time.
Furthermore, the generation time has been randomized a bit for more variety in the process.
The parts themselves have now become separate, temporary nodes, so it is possible to apply all kinds of effects to them without affecting the actual starship model.
And last but certainly not least: I made a builder-bot model in Blender! (So maybe it looks more like a vacuum-cleaner than a robot, but that’s OK. )
models.zip (1.4 MB)
Seriously though, this is the kind of look I was going for – at least for the “ground-bots”, which are responsible for generating the bottom parts of the ship. For the upper parts, I’d like to have aerial, drone-like bots.
Here is a completely new main script, showcasing the builder-bots shooting red energy beams onto the generated parts as the camera slowly orbits around the ship:
from direct.showbase.ShowBase import ShowBase
from panda3d.core import *
from random import random
import array
base = ShowBase()
def create_beam():
from math import pi, sin, cos
data = array.array("f", [])
segs = 6
vert_count = 0
for i in range(segs + 1):
angle = pi * 2 / segs * i
x = sin(angle)
z = -cos(angle)
data.extend((x, 0., z, x, 1., z))
vert_count += 2
vertex_format = GeomVertexFormat.get_v3()
v_data = GeomVertexData("data", vertex_format, GeomEnums.UH_static)
data_array = v_data.modify_array(0)
data_array.unclean_set_num_rows(vert_count)
view = memoryview(data_array).cast("B").cast("f")
view[:] = data
prim = GeomTriangles(GeomEnums.UH_static)
for i in range(segs):
i1 = i * 2
i2 = i1 + 1
i3 = i2 + 1
i4 = i3 + 1
prim.add_vertices(i1, i2, i3)
prim.add_vertices(i2, i4, i3)
geom = Geom(v_data)
geom.add_primitive(prim)
node = GeomNode("beam")
node.add_geom(geom)
beam = NodePath(node)
beam.set_light_off()
beam.set_color(1., 0., 0., 1.)
return beam
class BuilderBot:
def __init__(self, model, beam):
self.model = model
self.beam = beam.copy_to(self.model)
beam.set_pos(0., 0., 2.3)
beam.set_sy(.1)
def set_part(self, part):
if part:
x, y, z = part.center
self.model.set_pos(x, y, 0.)
dist = (part.center - self.beam.get_pos(base.render)).length()
self.beam.set_sy(dist)
self.beam.look_at(base.render, part.center)
else:
self.beam.set_sy(.1)
class Section:
def __init__(self, primitives):
self.primitives = primitives
self.generation_count = 0
def __bool__(self):
return True if self.primitives else False
def __len__(self):
return len(self.primitives)
def generate(self, vertex_data):
if not self.primitives:
return
self.generation_count += 1
prim = self.primitives.pop(0)
self.part = Part(vertex_data, prim)
return self.part
class Part:
def __init__(self, vertex_data, primitive):
geom = Geom(vertex_data)
geom.add_primitive(primitive)
self.primitive = primitive
node = GeomNode("part")
node.add_geom(geom)
self.model = NodePath(node)
self.model.set_transparency(TransparencyAttrib.M_alpha)
self.model.set_color(1., 1., 0., 1.)
self.model.set_alpha_scale(0.)
p_min, p_max = self.model.get_tight_bounds()
self.center = p_min + (p_max - p_min) * .5
def destroy(self):
self.model.detach_node()
self.model = None
def solidify(self, task, duration, finalizer):
self.model.set_alpha_scale(task.time / duration)
if task.time < duration:
return task.cont
finalizer(self.primitive)
self.destroy()
class Demo:
def __init__(self):
base.disableMouse()
# set up a light source
p_light = PointLight("point_light")
p_light.set_color((1., 1., 1., 1.))
self.light = base.camera.attach_new_node(p_light)
self.light.set_pos(5., -100., 7.)
base.render.set_light(self.light)
self.model = base.loader.load_model("starship.bam")
self.model.reparent_to(base.render)
p_min, p_max = self.model.get_tight_bounds()
ship_size = (p_max - p_min).length()
self.cam_dist = ship_size# * 1.5
self.cam_heading = 180.
self.cam_target = base.render.attach_new_node("cam_target")
x, y, z = self.model.get_pos()
self.cam_target.set_pos(x, y, 10.)
self.cam_target.set_h(self.cam_heading)
base.camera.reparent_to(self.cam_target)
base.camera.set_y(-self.cam_dist)
beam = create_beam()
beam.set_scale(.1)
self.builder_bots = []
self.active_builder_bots = []
offset = (p_max - p_min).y / 10.
for i in range(6):
model = base.loader.load_model("builder_bot.fbx")
model.reparent_to(base.render)
model.set_pos(0., p_max.y - offset * i, 0.)
bot = BuilderBot(model, beam)
self.builder_bots.append(bot)
bounds = self.model.node().get_bounds()
geom = self.model.node().modify_geom(0)
self.vertex_data = geom.get_vertex_data()
new_prim = GeomTriangles(GeomEnums.UH_static)
new_prim.set_index_type(GeomEnums.NT_uint32)
self.prim_count = 0
primitives = [prim for prim in geom.primitives]
primitives.pop() # remove the last, uncolored ship part
# Divide the hull into (predefined) sections.
# Each section will be generated with one bot for the bottom half and
# one drone for the upper half.
self.sections = []
self.sections.append(Section(primitives[:1])) # rear polygon
del primitives[:1]
for _ in range(8): # 8 sections each containing 10 hull plates
self.sections.append(Section(primitives[:10]))
del primitives[:10]
self.sections.append(Section(primitives[:7])) # section with 7 plates
del primitives[:7]
self.sections.append(Section(primitives[:10])) # section with 10 plates
del primitives[:10]
self.sections.append(Section(primitives))
# prune any invalid sections
self.sections = [s for s in self.sections if s]
section = self.sections.pop(0)
bot = self.builder_bots.pop(0)
self.active_builder_bots.append(bot)
geom.clear_primitives()
geom.add_primitive(new_prim)
self.model.node().set_bounds(bounds)
self.model.node().set_final(True)
base.task_mgr.add(self.__move_camera, "move_camera")
build_task = lambda task: self.__build_section(task, section, bot)
base.task_mgr.add(build_task, "build_section", delay=0.5)
def __move_camera(self, task):
self.cam_heading -= 1.75 * globalClock.get_dt()
self.cam_target.set_h(self.cam_heading)
return task.cont
def __add_primitive(self, prim):
prim_array = prim.get_vertices()
prim_view = memoryview(prim_array).cast("B").cast("I")
geom = self.model.node().modify_geom(0)
new_prim = geom.modify_primitive(0)
new_prim_array = new_prim.modify_vertices()
old_size = new_prim_array.get_num_rows()
new_prim_array.set_num_rows(old_size + len(prim_view))
new_prim_view = memoryview(new_prim_array).cast("B").cast("I")
new_prim_view[old_size:] = prim_view[:]
def __build_section(self, task, section, bot):
part = section.generate(self.vertex_data)
part.model.reparent_to(base.render)
solidify_task = lambda task: part.solidify(task, 1.5, self.__add_primitive)
base.task_mgr.add(solidify_task, "solidify")
deactivation_task = lambda task: bot.set_part(None)
base.task_mgr.add(deactivation_task, "deactivate_beam", delay=1.5)
bot.set_part(part)
if section.generation_count == min(len(section) + 1, 2) and self.sections:
next_section = self.sections.pop(0)
next_bot = self.builder_bots.pop(0)
self.active_builder_bots.append(next_bot)
next_build_task = lambda task: self.__build_section(task, next_section, next_bot)
delay = 1.6 + random()
base.task_mgr.add(next_build_task, "build_section", delay=delay)
if section:
task.delayTime = 1.6 + random()
return task.again
self.active_builder_bots.remove(bot)
self.builder_bots.append(bot)
game = Demo()
base.run()
At the moment, the bots just “teleport” all over the place instead of actually moving. This will be quite a bit of work to get right, using intervals or tasks. Will see what we can do.
Note that I made some mistakes when coloring the parts near the front of the ship in Blender, resulting in a couple of unintended holes – just ignore.
Anyway, I hope you will enjoy this update, as it is already far closer to what I originally imagined.