initial commit: llamamod 0.2.0 – reverse-engineered level formats (wad, map, trak, text, stpc), python tools and notes, java mod manager (iso install, mod pipeline with exe patching, save editor, wine/windows launcher) with mods (test world, debug wireframe, mirror, randomizer, fog, cheats, intro skip, practice, free camera, boot to level, level projects) and the 3d level editor (geometry, objects, cross-world import, environment, textures, play-test, manual)
This commit is contained in:
commit
b36a366240
826 files changed
+105304
No files matched your search
@@ -0,0 +1,203 @@
|
||||
# Blender 3.0 (headless): level.obj aus geom_export (v x y z r g b, vt, usemtl page_NN/flat_N) laden,
|
||||
# in Spielkoordinaten (Blender x = Spiel-x, y = -Spiel-z, z = Spiel-y) rendern.
|
||||
# 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: # Meer
|
||||
bsdf.inputs['Roughness'].default_value = 0.25
|
||||
bsdf.inputs['Specular'].default_value = 0.5
|
||||
# leicht selbstleuchtend, damit die eingebackene Beleuchtung (Vertexfarben) sichtbar bleibt
|
||||
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)) # Spiel (x, y, -z) -> Blender (x, -spiel_z, y) = (x, z_obj, y_obj)
|
||||
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[::-1], ti[::-1], mat)) # Spiegelung -> Umlaufsinn umdrehen
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# --------------------------------------------------------------- Objekt-Marker (Strats aus 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)
|
||||
|
||||
# --------------------------------------------------------------- Licht, Welt
|
||||
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 liefert mit dem Debian-Blender 3.0.1 nur Schwarz -> Eevee (braucht X: eigenes Xvfb, z. B. :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 ohne OCIO-Konfiguration: nur 'Standard'
|
||||
|
||||
# Bereiche (Spielkoordinaten x, z, y) fuer die Beschriftung
|
||||
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: # von Suedwesten, schraeg oben
|
||||
shoot('schraeg', (10, -50, 27), (30.5, -21.5, 0), lens=33)
|
||||
if 'schraeg2' in views: # von Nordosten (hinter dem Ziel)
|
||||
shoot('schraeg2', (60, 2, 22), (29, -21, 0), lens=30)
|
||||
if 'oben' in views:
|
||||
shoot('oben', (31, -21.5, 80), (31, -21.4, 0), ortho=44)
|
||||
Reference in new issue
Block a user