Files
llamamod/tools/vorschau/render_level.py
T

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)