Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions plugins/blender/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ later):
| No One Lives Forever 2, Contract J.A.C.K., Tron 2.0 | `.ltb`, model file versions 18 to 25 | DTX version -5 |
| No One Lives Forever (PS2) | `.ltb`, file type 2, version 16 | DTX version -5, 32P (palette alpha as the PS2 draws it) |
| Die Hard: Nakatomi Plaza | wrapped `.ltb`, ABC or D3D body, import and conversion | DTX version -5 |
| Shogo, Blood 2, KISS Psycho Circus (LithTech 1) | `.abc`, version 6, vertex animation as shape keys (`lithtech/abc6.py`, `export_abc6.py`; `tests/blender_abc6.py`, `tests/blender_abc6_edit.py`) | (DTX -2 and -3: `lithtech/dtx.py` as for the others) |

meant to grow to every LithTech-era game.

Expand All @@ -25,11 +26,13 @@ meant to grow to every LithTech-era game.
`roundtrip_dtx.py` read and write back every shipped model and texture
(`extract_abc.py` and `extract_ltb.py` fill the asset cache first; the PS2
files come from the discs' `LITH_PS2.REZ`, out/assets/ps2), and
`roundtrip_ps2_dtx.py` every PS2 texture. `blender_roundtrip.py`,
`roundtrip_ps2_dtx.py` every PS2 texture. `roundtrip_abc6.py` does the
same for the LithTech 1 `.abc` (v6) under the folders it's given. `blender_roundtrip.py`,
`blender_roundtrip_ltb.py`, `blender_roundtrip_ps2ltb.py` and
`blender_roundtrip_ps2_dtx.py` (run inside Blender) import and export them
through the extension, and `blender_rebuild_ps2ltb.py` checks the PS2
exporter's rebuilding of edited meshes. `build_ps2ltb_names.py` makes the
exporter's rebuilding of edited meshes. `blender_abc6.py` and
`blender_abc6_edit.py` do the same for LithTech 1's `.abc` v6. `build_ps2ltb_names.py` makes the
PS2 models' name list.

Game data is never committed; tests read it from the installed games
Expand Down
43 changes: 39 additions & 4 deletions plugins/blender/docs/BLENDER.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# The Blender extension

`io_scene_lithtech` imports and exports LithTech models (`.abc` versions 9
to 12: NOLF, AvP2; `.ltb` model file versions 18 to 25: NOLF2, Contract
J.A.C.K., Tron 2.0) and textures (`.dtx`, version -5) in Blender 5.2 LTS
and later.
to 12: NOLF, AvP2; `.abc` version 6: Shogo, Blood 2, KISS Psycho Circus;
`.ltb` model file versions 18 to 25: NOLF2, Contract J.A.C.K., Tron 2.0)
and textures (`.dtx`, version -5) in Blender 5.2 LTS and later.

Die Hard: Nakatomi Plaza's wrapped `.ltb` files can also be imported through
the LTB importer (wrapper version 3, model versions 9–13, ABC or D3D body).
Expand Down Expand Up @@ -224,9 +224,44 @@ the same triangles and weights). Nothing exported here has been run on a
PS2. Skins are found as for `.abc` (SKINS beside MODELS, SKINS_PV beside
MODELS_PV, in any letter case); PS2 skins are 8-bit palette DTX files.

## LithTech 1 models (Shogo, Blood 2, KISS Psycho Circus)

**File > Import / Export > LithTech Model (.abc)** reads and writes `.abc`
version 6 too; *Version* "As Imported" writes a v6 model back as v6, and
"12" converts it. `lithtech/abc6.py` has the format. The layout in Blender
is the `.abc` one. What v6 adds:

| v6 | Blender |
|---|---|
| one mesh, each vertex on one node | one mesh object, each vertex in one vertex group |
| no bind pose: the first key of the first animation | the rest pose |
| vertex animation: a deformation node's vertices keyed per keyframe as three bytes each | shape keys `<animation>\|<key>` (as for `.ltb`), at the bind pose, keyed in the action's KEY slot so they play with the bones |
| quaternions as stored, some not unit length | the action's `lt_v6_keys` |
| bounds, the reduced-detail vertices, the vertex bytes, the raw vertex and face values | `lt_v6_*` properties and attributes |
| chunks v6 doesn't define (KISS's `TransformInfo`, in every one of its models) | `lt_v6_extra_chunks_json`, written back as stored |

**Export.** Each value the import kept is written back while what Blender
holds still matches it, so an unchanged model exports byte for byte:
`tests/blender_abc6.py` checks every v6 model of Shogo's and Blood 2's
demos and the LithTech 2.42 SDK's samples, 414. Outside Blender,
`tests/roundtrip_abc6.py` reads and writes back the shipped games' v6
models: all 1,645 of Blood 2's (with The Nightmare Levels) and KISS's come
back byte-identical. An edit changes what it
touches: `tests/blender_abc6_edit.py` moves an animated vertex in one frame
(three bytes change), a bone at one key (that key changes, written unit
length), a vertex, a face, and makes vertex animation on a model that had
none, and checks each file against what Blender shows. v6 holds one node per
vertex (the heaviest of several is taken), triangles only, and no skins; a
node with a vertex that moves in a frame lists every vertex on it, as every
such node in the shipped files does. The first key of the first animation
is LithTech 1's bind, so keep it the rest pose (export warns when it is
not). The shape keys are found by their action's name, so rename an action
and its keys together. Blender interpolates normalized quaternions where LithTech 1
interpolates the stored ones, so a few Blood 2 animations with very long
keys play differently between keys in Blender; the file is unaffected.

## Not yet

- Shogo/Blood 2-era `.abc` (its own module beside `lithtech/abc.py`).
- Matrix-palette skeletal meshes are read and written but no shipped model
uses them, so they're untested.
- World files (`.dat`; the PS2's world `.ltb` files).
Expand Down
42 changes: 37 additions & 5 deletions plugins/blender/io_scene_lithtech/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# --------------------------------------------------------------------- #

class IMPORT_SCENE_OT_lithtech_abc(bpy.types.Operator, ImportHelper):
"""Import LithTech models (.abc): NOLF, AvP2 and AvP2 Primal Hunt"""
"""Import LithTech models (.abc): NOLF, AvP2 and AvP2 Primal Hunt, and LithTech 1's v6 (Shogo, Blood 2, KISS)"""
bl_idname = "import_scene.lithtech_abc"
bl_label = "Import LithTech Model"
bl_options = {'REGISTER', 'UNDO', 'PRESET'}
Expand Down Expand Up @@ -94,14 +94,22 @@ def draw(self, context):


class EXPORT_SCENE_OT_lithtech_abc(bpy.types.Operator, ExportHelper):
"""Export an armature and its meshes as a LithTech model (.abc, version 12)"""
"""Export an armature and its meshes as a LithTech model (.abc: version 12, or LithTech 1's version 6)"""
bl_idname = "export_scene.lithtech_abc"
bl_label = "Export LithTech Model"
bl_options = {'PRESET'}

filename_ext = ".abc"
filter_glob: StringProperty(default="*.abc", options={'HIDDEN'})

abc_version: EnumProperty(
name="Version",
items=[('AUTO', "As Imported", "Version 6 for a model imported from LithTech 1's .abc v6, "
"else version 12"),
('12', "12", "NOLF, AvP2 and AvP2 Primal Hunt"),
('6', "6 (LithTech 1)", "Shogo, Blood 2, KISS Psycho Circus: one mesh, one node per "
"vertex, and vertex animation from the shape keys \"<action>|<key>\"")],
default='AUTO')
scale: FloatProperty(
name="Scale", default=1.0, min=1e-6, soft_min=0.001, soft_max=1000.0,
description="LithTech units per Blender unit (set from the scale the model was "
Expand Down Expand Up @@ -138,8 +146,26 @@ def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(arm.name, self.filename_ext)
return super().invoke(context, event)

def _version(self, context):
if self.abc_version != 'AUTO':
return int(self.abc_version)
from . import export_abc
arm = export_abc.find_armature(context)
return 6 if arm is not None and arm.get("lt_source_version") == 6 else 12

def execute(self, context):
from . import export_abc, textures
if self._version(context) == 6:
from . import export_abc6
try:
_, written, _notes = export_abc6.save(
context, self.filepath, scale=self.scale, use_selection=self.use_selection,
export_animations=self.export_animations, report=self.report)
except export_abc.ExportError as e:
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
self.report({'INFO'}, "Wrote " + ", ".join(os.path.basename(p) for p in written) + " (v6)")
return {'FINISHED'}
try:
_, written = export_abc.save(
context, self.filepath, scale=self.scale, use_selection=self.use_selection,
Expand All @@ -157,13 +183,19 @@ def execute(self, context):
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.prop(self, "abc_version")
v12 = self._version(context) != 6
layout.prop(self, "scale")
layout.prop(self, "use_selection")
layout.prop(self, "export_animations")
layout.prop(self, "normalize_weights")
layout.prop(self, "export_textures")
# v6 holds one node per vertex, and LithTech 1's skins are a DTX
# version the texture writer doesn't write.
sub = layout.column()
sub.active = v12
sub.prop(self, "normalize_weights")
sub.prop(self, "export_textures")
sub = layout.column()
sub.active = self.export_textures
sub.active = v12 and self.export_textures
sub.prop(self, "texture_format")


Expand Down
36 changes: 36 additions & 0 deletions plugins/blender/io_scene_lithtech/animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,41 @@ def assign(arm, action):
slot = action.slots.new(id_type='OBJECT', name=arm.name)
ad.action = action
ad.action_slot = slot
assign_shape_keys(arm, action)


def vertex_animated_keys(arm):
"""The shape keys (Key datablocks) of the meshes this armature deforms
whose vertex animation the importer keyed (LithTech 1's .abc v6; see
import_abc._add_v6_vertex_animation)."""
out = []
for o in bpy.data.objects:
if o.type != 'MESH' or o.get("lt_vertex_animation") != "abc6" or o.data.shape_keys is None:
continue
if o.parent == arm or any(m.type == 'ARMATURE' and m.object == arm for m in o.modifiers):
out.append(o.data.shape_keys)
return out


def assign_shape_keys(arm, action):
"""Give each vertex-animated mesh's shape keys the action's KEY slot, so
the frames play with the bones; with none (or no action), take the
action off them and set every frame's key to 0, the first pose."""
for key in vertex_animated_keys(arm):
slots = [] if action is None else [s for s in action.slots if s.target_id_type == 'KEY']
slot = next((s for s in slots if s.name_display == key.name), None)
if slot is None and len(slots) == 1:
slot = slots[0]
if slot is None:
if key.animation_data:
key.animation_data.action = None
for kb in key.key_blocks:
if "|" in kb.name:
kb.value = 0.0
continue
ad = key.animation_data or key.animation_data_create()
ad.action = action
ad.action_slot = slot


def rest_pose(arm):
Expand All @@ -40,6 +75,7 @@ def rest_pose(arm):
track.mute = True
for bone in arm.pose.bones:
bone.matrix_basis = Matrix.Identity(4)
assign_shape_keys(arm, None)


def select_action(context, arm, action):
Expand Down
11 changes: 10 additions & 1 deletion plugins/blender/io_scene_lithtech/export_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,19 @@ def _mesh_objects(arm_obj, objects):


def _evaluated_mesh(obj, depsgraph):
"""The object's mesh with its modifiers except Armature ones."""
"""The object's mesh with its modifiers except Armature ones, and
without its vertex-animation frames (shape keys "<animation>|<key>"):
playing an animation sets their values, and the mesh is written at
rest."""
toggled = []
for m in obj.modifiers:
if m.type == 'ARMATURE' and m.show_viewport:
m.show_viewport = False
toggled.append(m)
keys = obj.data.shape_keys if obj.type == 'MESH' else None
muted = [kb for kb in keys.key_blocks if "|" in kb.name and not kb.mute] if keys else []
for kb in muted:
kb.mute = True
try:
depsgraph.update()
ev = obj.evaluated_get(depsgraph)
Expand All @@ -132,6 +139,8 @@ def _evaluated_mesh(obj, depsgraph):
finally:
for m in toggled:
m.show_viewport = True
for kb in muted:
kb.mute = False
return me


Expand Down
Loading
Loading