204 lines
8.8 KiB
Python
204 lines
8.8 KiB
Python
# Blender 3.0 (headless): load level.obj from geom_export (v x y z r g b, vt, usemtl page_NN/flat_N),
|
|
# render in game coordinates (Blender x = game x, y = -game z, z = game y).
|
|
# blender -b -P render_level.py -- OBJDIR OUTPREFIX [samples] [scale%]
|
|
import bpy, sys, os, json, math
|
|
from mathutils import Vector
|
|
from bpy_extras.object_utils import world_to_camera_view
|
|
|
|
argv = sys.argv[sys.argv.index('--') + 1:]
|
|
objdir, outp = argv[0], argv[1]
|
|
samples = int(argv[2]) if len(argv) > 2 else 64
|
|
scale = int(argv[3]) if len(argv) > 3 else 100
|
|
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
sc = bpy.context.scene
|
|
|
|
# --------------------------------------------------------------- MTL
|
|
mats = {}
|
|
cur = None
|
|
for ln in open(os.path.join(objdir, 'level.mtl')):
|
|
p = ln.split()
|
|
if not p:
|
|
continue
|
|
if p[0] == 'newmtl':
|
|
cur = p[1]; mats[cur] = {}
|
|
elif p[0] == 'Kd':
|
|
mats[cur]['kd'] = tuple(float(v) for v in p[1:4])
|
|
elif p[0] == 'map_Kd':
|
|
mats[cur]['img'] = p[1]
|
|
|
|
|
|
def make_mat(name, d):
|
|
m = bpy.data.materials.new(name)
|
|
m.use_nodes = True
|
|
nt = m.node_tree; N = nt.nodes; Lk = nt.links
|
|
N.clear()
|
|
out = N.new('ShaderNodeOutputMaterial')
|
|
bsdf = N.new('ShaderNodeBsdfPrincipled')
|
|
bsdf.inputs['Roughness'].default_value = 0.9
|
|
bsdf.inputs['Specular'].default_value = 0.15
|
|
vc = N.new('ShaderNodeVertexColor'); vc.layer_name = 'Col'
|
|
mul = N.new('ShaderNodeMixRGB'); mul.blend_type = 'MULTIPLY'; mul.inputs[0].default_value = 1.0
|
|
Lk.new(vc.outputs['Color'], mul.inputs[2])
|
|
if 'img' in d:
|
|
tex = N.new('ShaderNodeTexImage')
|
|
tex.image = bpy.data.images.load(os.path.join(objdir, d['img']))
|
|
tex.interpolation = 'Closest'
|
|
Lk.new(tex.outputs['Color'], mul.inputs[1])
|
|
Lk.new(tex.outputs['Alpha'], bsdf.inputs['Alpha'])
|
|
m.blend_method = 'CLIP'
|
|
else:
|
|
kd = d.get('kd', (1, 1, 1))
|
|
mul.inputs[1].default_value = (*kd, 1)
|
|
if kd[2] > 0.5 and kd[0] < 0.3: # sea
|
|
bsdf.inputs['Roughness'].default_value = 0.25
|
|
bsdf.inputs['Specular'].default_value = 0.5
|
|
# slightly emissive so the baked lighting (vertex colours) stays visible
|
|
Lk.new(mul.outputs[0], bsdf.inputs['Base Color'])
|
|
Lk.new(mul.outputs[0], bsdf.inputs['Emission'])
|
|
bsdf.inputs['Emission Strength'].default_value = 0.25
|
|
Lk.new(bsdf.outputs[0], out.inputs[0])
|
|
return m
|
|
|
|
|
|
# --------------------------------------------------------------- OBJ
|
|
V, C, VT = [], [], []
|
|
faces = [] # (vidx, vtidx, mat)
|
|
mat = None
|
|
for ln in open(os.path.join(objdir, 'level.obj')):
|
|
if ln.startswith('v '):
|
|
p = ln.split()
|
|
x, y, z = float(p[1]), float(p[2]), float(p[3])
|
|
V.append((x, -z, y)) # OBJ = game space (right-handed, y up) -> Blender z up
|
|
C.append((float(p[4]), float(p[5]), float(p[6])) if len(p) >= 7 else (1, 1, 1))
|
|
elif ln.startswith('vt '):
|
|
p = ln.split(); VT.append((float(p[1]), float(p[2])))
|
|
elif ln.startswith('usemtl'):
|
|
mat = ln.split()[1]
|
|
elif ln.startswith('f '):
|
|
vi, ti = [], []
|
|
for t in ln.split()[1:]:
|
|
q = t.split('/')
|
|
vi.append(int(q[0]) - 1)
|
|
ti.append(int(q[1]) - 1 if len(q) > 1 and q[1] else -1)
|
|
faces.append((vi, ti, mat))
|
|
|
|
matnames = sorted(set(f[2] for f in faces))
|
|
mesh = bpy.data.meshes.new('level')
|
|
mesh.from_pydata(V, [], [f[0] for f in faces])
|
|
for n in matnames:
|
|
mesh.materials.append(make_mat(n, mats.get(n, {})))
|
|
mi = {n: i for i, n in enumerate(matnames)}
|
|
uvl = mesh.uv_layers.new(name='UV')
|
|
col = mesh.vertex_colors.new(name='Col')
|
|
for poly, (vi, ti, m) in zip(mesh.polygons, faces):
|
|
poly.material_index = mi[m]
|
|
for k, li in enumerate(poly.loop_indices):
|
|
t = ti[k]
|
|
uvl.data[li].uv = VT[t] if t >= 0 else (0, 0)
|
|
c = C[vi[k]]
|
|
col.data[li].color = (c[0], c[1], c[2], 1)
|
|
mesh.update()
|
|
ob = bpy.data.objects.new('level', mesh)
|
|
sc.collection.objects.link(ob)
|
|
|
|
|
|
# --------------------------------------------------------------- object markers (strats from strats.json)
|
|
def flatmat(name, rgb, emit=0.6):
|
|
m = bpy.data.materials.new(name); m.use_nodes = True
|
|
b = m.node_tree.nodes['Principled BSDF']
|
|
b.inputs['Base Color'].default_value = (*rgb, 1)
|
|
b.inputs['Emission'].default_value = (*rgb, 1); b.inputs['Emission Strength'].default_value = emit
|
|
b.inputs['Roughness'].default_value = 0.4
|
|
return m
|
|
|
|
|
|
sj = os.environ.get('STRATS')
|
|
if sj and os.path.exists(sj):
|
|
ROWZ = [22.5, 25.2, 27.9, 30.6]
|
|
rowcol = [(1.0, 0.8, 0.1), (0.75, 0.75, 0.78), (0.2, 0.45, 1.0), (0.95, 0.15, 0.12)]
|
|
M = {k: flatmat(k, c) for k, c in [('coin', (1.0, 0.75, 0.05)), ('lama', (1.0, 0.3, 0.05)),
|
|
('cp', (1.0, 0.55, 0.0)), ('tuer', (0.75, 0.2, 1.0)),
|
|
('tier', (0.95, 0.95, 0.9))]}
|
|
RM = [flatmat('row%d' % i, c) for i, c in enumerate(rowcol)]
|
|
for d in json.load(open(sj)):
|
|
n, x, y, z = d['name'], d['x'], d['y'], d['z']
|
|
loc = (x, -z, y)
|
|
if n == 'Coin' and not any(abs(z - r) < 0.3 for r in ROWZ):
|
|
bpy.ops.mesh.primitive_cylinder_add(radius=0.18, depth=0.05, location=(x, -z, y + 0.35),
|
|
rotation=(math.radians(90), 0, math.radians(90)))
|
|
mat = M['coin']
|
|
elif n == 'Llama':
|
|
bpy.ops.mesh.primitive_cone_add(radius1=0.3, depth=0.9, location=(x, -z, y + 0.45)); mat = M['lama']
|
|
elif n == 'CheckPointPost' and not any(abs(z - r) < 0.3 for r in ROWZ):
|
|
bpy.ops.mesh.primitive_cylinder_add(radius=0.1, depth=1.2, location=(x, -z, y + 0.6)); mat = M['cp']
|
|
elif n == 'LevelDoor':
|
|
bpy.ops.mesh.primitive_torus_add(major_radius=0.4, minor_radius=0.07, location=(x, -z, y + 0.6),
|
|
rotation=(0, math.radians(90), 0)); mat = M['tuer']
|
|
elif n in ('Bunny', 'Ladybird', 'FlockOfBirds', 'VillagerHerder'):
|
|
bpy.ops.mesh.primitive_uv_sphere_add(radius=0.25, location=(x, -z, y + 0.3)); mat = M['tier']
|
|
else:
|
|
r = min(range(4), key=lambda i: abs(z - ROWZ[i]))
|
|
bpy.ops.mesh.primitive_ico_sphere_add(radius=0.22, subdivisions=1, location=(x, -z, y + 0.4))
|
|
mat = RM[r]
|
|
o = bpy.context.active_object; o.data.materials.append(mat)
|
|
|
|
# --------------------------------------------------------------- light, world
|
|
w = bpy.data.worlds.new('w'); sc.world = w; w.use_nodes = True
|
|
bg = w.node_tree.nodes['Background']
|
|
bg.inputs[0].default_value = (0.55, 0.72, 0.95, 1); bg.inputs[1].default_value = 0.9
|
|
sun = bpy.data.lights.new('sun', 'SUN'); sun.energy = 2.6; sun.angle = math.radians(6)
|
|
so = bpy.data.objects.new('sun', sun); sc.collection.objects.link(so)
|
|
so.rotation_euler = (math.radians(42), 0, math.radians(-140))
|
|
|
|
# Cycles renders only black with Debian Blender 3.0.1 -> Eevee (needs X: a separate Xvfb, e.g. :7)
|
|
sc.render.engine = 'BLENDER_EEVEE'
|
|
ee = sc.eevee
|
|
ee.taa_render_samples = samples
|
|
ee.use_gtao = True; ee.gtao_distance = 1.5
|
|
ee.use_soft_shadows = True
|
|
ee.shadow_cascade_size = '4096'
|
|
sun.shadow_cascade_max_distance = 120
|
|
sun.shadow_cascade_count = 4
|
|
try:
|
|
sc.view_settings.view_transform = 'Filmic'
|
|
sc.view_settings.look = 'Medium High Contrast'
|
|
except TypeError:
|
|
pass # Debian Blender without OCIO config: only 'Standard'
|
|
|
|
# areas (game coordinates x, z, y) for the labels; keys are the label text (German, as in the level)
|
|
LABELS = {'Start': (18, 15.5, 2.2), 'Galerie': (23.5, 26.5, 1.2), 'Gehege': (37.5, 25.4, 1.2),
|
|
'Parkour': (33.0, 15.5, 2.6), 'Ziel': (50.0, 15.5, 6.0)}
|
|
|
|
|
|
def shoot(name, loc, look, lens=35, ortho=None, res=(1600, 1000)):
|
|
cam = bpy.data.cameras.new(name)
|
|
co = bpy.data.objects.new(name, cam); sc.collection.objects.link(co)
|
|
co.location = loc
|
|
d = Vector(look) - Vector(loc)
|
|
co.rotation_euler = d.to_track_quat('-Z', 'Y').to_euler()
|
|
if ortho:
|
|
cam.type = 'ORTHO'; cam.ortho_scale = ortho
|
|
else:
|
|
cam.lens = lens
|
|
cam.clip_end = 500
|
|
sc.camera = co
|
|
sc.render.resolution_x, sc.render.resolution_y = res
|
|
sc.render.resolution_percentage = scale
|
|
sc.render.filepath = outp + '_' + name + '.png'
|
|
bpy.ops.render.render(write_still=True)
|
|
lab = {}
|
|
for k, (x, z, y) in LABELS.items():
|
|
v = world_to_camera_view(sc, co, Vector((x, -z, y)))
|
|
lab[k] = (v.x, 1 - v.y)
|
|
json.dump(lab, open(outp + '_' + name + '.json', 'w'))
|
|
|
|
|
|
views = os.environ.get('VIEWS', 'schraeg,oben').split(',')
|
|
if 'schraeg' in views: # 'schraeg' = oblique: from the south-west, above
|
|
shoot('schraeg', (10, -50, 27), (30.5, -21.5, 0), lens=33)
|
|
if 'schraeg2' in views: # from the north-east (behind the goal)
|
|
shoot('schraeg2', (60, 2, 22), (29, -21, 0), lens=30)
|
|
if 'oben' in views: # 'oben' = top view
|
|
shoot('oben', (31, -21.5, 80), (31, -21.4, 0), ortho=44)
|