diff --git a/addons/godotvmf/godotmdl/ani/index.gd b/addons/godotvmf/godotmdl/ani/index.gd new file mode 100644 index 0000000..86d92fe --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/index.gd @@ -0,0 +1,301 @@ +class_name ANIReader extends RefCounted + +## Animation flags from studio.h +const STUDIO_ANIM_RAWPOS = 0x01 ## Vector48 — static position for all frames +const STUDIO_ANIM_RAWROT = 0x02 ## Quaternion48 — static rotation for all frames +const STUDIO_ANIM_ANIMPOS = 0x04 ## mstudioanim_valueptr_t — per-frame position +const STUDIO_ANIM_ANIMROT = 0x08 ## mstudioanim_valueptr_t — per-frame rotation +const STUDIO_ANIM_DELTA = 0x10 ## values are deltas added to the rest pose +const STUDIO_ANIM_RAWROT2 = 0x20 ## Quaternion64 — static rotation for all frames + +const SOURCE_TO_GODOT_DYNAMIC := Basis( + Vector3( -1, 0, 0), + Vector3( 0, 0, 1), + Vector3( 0, 1, 0) +); + +const SOURCE_TO_GODOT_STATIC := Basis( + Vector3( 0, 0,-1), + Vector3(-1, 0, 0), + Vector3( 0, 1, 0) +); +var file: FileAccess +var mdl: MDLReader +var library: AnimationLibrary +var _scale: float = 1.0 + +func _init(filepath: String, _mdl: MDLReader): + mdl = _mdl; + if FileAccess.file_exists(filepath): + file = FileAccess.open(filepath, FileAccess.READ); + parse_local_animations(); + +## Returns the coordinate-system conversion basis for this model. +## Mirrors MDLCombiner.get_model_transform_basis() but without PHY dependency. +func get_model_transform_basis() -> Basis: + return SOURCE_TO_GODOT_DYNAMIC; + +## Quaternion from Z-up to Y-up +func _transform_quaternion(q: Quaternion) -> Quaternion: + return q + +func _transform_vector(v: Vector3) -> Vector3: + return v * _scale; + +## Builds an AnimationLibrary from the inline animation descriptors in the MDL file. +## scale: override the model scale (defaults to VMFConfig.import.scale). +func parse_local_animations(scale: float = -1.0) -> AnimationLibrary: + library = AnimationLibrary.new(); + _scale = scale if scale > 0.0 else VMFConfig.import.scale; + + if not mdl or not mdl.header or not mdl.file: + return library; + + var anim_count: int = mdl.header.anim_count; + if anim_count <= 0: + return library; + + var anim_descs: Array = ByteReader.read_array(mdl.file, mdl.header, "anim_offset", "anim_count", MStudioAnimDesc_t); + + for anim_desc in anim_descs: + _process_anim_desc(anim_desc); + + return library; + +# ── Per-animation processing ─────────────────────────────────────────────── + +func _process_anim_desc(anim_desc: MStudioAnimDesc_t) -> void: + var anim_name: String = anim_desc.pszName; + if anim_name.is_empty(): + return; + + if anim_desc.numframes <= 0: + push_warning("ANIReader: skipping '%s' — invalid numframes %d" % [anim_name, anim_desc.numframes]); + return; + + var num_frames: int = min(anim_desc.numframes, 10000); + if anim_desc.numframes > 10000: + push_warning("ANIReader: clamping '%s' numframes %d → 10000" % [anim_name, anim_desc.numframes]); + + var bone_count: int = mdl.bones.size(); + var bone_rots: Array = []; # [bone_idx][frame] = Quaternion + var bone_poss: Array = []; # [bone_idx][frame] = Vector3 + bone_rots.resize(bone_count); + bone_poss.resize(bone_count); + + # Fill each bone with its rest-pose value for every frame + for bi in range(bone_count): + var bone: MDLReader.MDLBone = mdl.bones[bi]; + bone_rots[bi] = []; + bone_poss[bi] = []; + var rest_q := _transform_quaternion(bone.quat); + var rest_p := _transform_vector(bone.pos); + for _f in range(num_frames): + bone_rots[bi].append(rest_q); + bone_poss[bi].append(rest_p); + + # Read animation data from MDL file (animblock==0 means inline) + if anim_desc.animblock == 0 or anim_desc.animblock == -1: + if anim_desc.sectionindex != 0 and anim_desc.sectionframes > 0: + _read_sectioned(anim_desc, num_frames, bone_rots, bone_poss); + elif anim_desc.animindex != 0: + _read_anim_block( + mdl.file, + anim_desc.address + anim_desc.animindex, + num_frames, 0, + bone_rots, bone_poss + ); + # External animblocks (animblock > 0) require the .ani file — skipped for now. + + # Build the Godot Animation resource + var animation := Animation.new(); + var fps := anim_desc.fps if anim_desc.fps > 0.0 else 30.0; + animation.length = float(num_frames - 1) / fps; + animation.loop_mode = Animation.LOOP_LINEAR if (anim_desc.flags & 0x1) else Animation.LOOP_NONE; + + for bi in range(bone_count): + var bone: MDLReader.MDLBone = mdl.bones[bi]; + var rot_track: int = animation.add_track(Animation.TYPE_ROTATION_3D); + var pos_track: int = animation.add_track(Animation.TYPE_POSITION_3D); + animation.track_set_path(rot_track, "skeleton:" + bone.name); + animation.track_set_path(pos_track, "skeleton:" + bone.name); + for f in range(num_frames): + var t := float(f) / fps; + animation.rotation_track_insert_key(rot_track, t, bone_rots[bi][f]); + animation.position_track_insert_key(pos_track, t, bone_poss[bi][f]); + + var safe_name: String = anim_name.replace("/", "_").replace("@", "").replace(" ", "_"); + if safe_name.is_empty(): + safe_name = "anim_%d" % anim_desc.address; + if library.has_animation(safe_name): + safe_name = safe_name + "_%d" % anim_desc.address; + library.add_animation(safe_name, animation); + +# ── Sectioned animation ──────────────────────────────────────────────────── + +func _read_sectioned(anim_desc: MStudioAnimDesc_t, num_frames: int, bone_rots: Array, bone_poss: Array) -> void: + var sectionframes: int = anim_desc.sectionframes; + # +1 for the terminator section + var num_sections: int = (num_frames + sectionframes - 1) / sectionframes + 1; + + mdl.file.seek(anim_desc.address + anim_desc.sectionindex); + var sections: Array = []; + for _i in range(num_sections): + sections.append(ByteReader.read_by_structure(mdl.file, MStudioAnimSections_t)); + + for si in range(num_sections - 1): # skip the terminator + var section: MStudioAnimSections_t = sections[si]; + if section == null: + continue; + var frame_start: int = si * sectionframes; + var frame_count: int = min(sectionframes, num_frames - frame_start); + if frame_count <= 0: + break; + + var src_file: FileAccess = _get_source_file(section.animblock); + if src_file and section.animindex != 0: + _read_anim_block(src_file, anim_desc.address + section.animindex, frame_count, frame_start, bone_rots, bone_poss); + +# ── Bone-linked-list walking ─────────────────────────────────────────────── + +func _get_source_file(animblock: int) -> FileAccess: + if animblock == 0 or animblock == -1: + return mdl.file; + return file; # external .ani file + +func _read_anim_block(src: FileAccess, start_offset: int, num_frames: int, frame_offset: int, bone_rots: Array, bone_poss: Array) -> void: + if not src or start_offset <= 0: + return; + + var bone_count: int = mdl.bones.size(); + var visited: Dictionary = {}; + var max_iters: int = bone_count + 1; + + src.seek(start_offset); + + for _iter in range(max_iters): + var anim_address: int = src.get_position(); + var anim: MStudioAnim_t = ByteReader.read_by_structure(src, MStudioAnim_t); + if not anim: + break; + + var bone_idx: int = anim.bone; + if bone_idx >= bone_count: + break; + + if bone_idx in visited: + push_warning("ANIReader: infinite loop in anim linked list at bone %d" % bone_idx); + break; + visited[bone_idx] = true; + + _decode_bone_data(src, anim, anim_address, bone_idx, num_frames, frame_offset, bone_rots, bone_poss); + + if anim.nextoffset == 0: + break; + src.seek(anim_address + anim.nextoffset); + +# ── Per-bone data decoding ───────────────────────────────────────────────── + +func _decode_bone_data(src: FileAccess, anim: MStudioAnim_t, anim_address: int, bone_idx: int, num_frames: int, frame_offset: int, bone_rots: Array, bone_poss: Array) -> void: + var bone: MDLReader.MDLBone = mdl.bones[bone_idx]; + var flags: int = anim.flags; + var is_delta: bool = (flags & STUDIO_ANIM_DELTA) != 0; + # Data begins right after the 4-byte mstudioanim_t header + var data_start: int = anim_address + 4; + + # ── Rotation ────────────────────────────────────────────────────────── + var rot_data_size: int = 0; + + if flags & STUDIO_ANIM_RAWROT: + src.seek(data_start); + var q := MStudioAnimDecoder.read_quaternion48(src); + if is_delta: + q = q * bone.quat; + var gq := _transform_quaternion(q); + for f in range(frame_offset, frame_offset + num_frames): + if f < bone_rots[bone_idx].size(): + bone_rots[bone_idx][f] = gq; + rot_data_size = 6; + + elif flags & STUDIO_ANIM_RAWROT2: + src.seek(data_start); + var q := MStudioAnimDecoder.read_quaternion64(src); + if is_delta: + q = q * bone.quat; + var gq := _transform_quaternion(q); + for f in range(frame_offset, frame_offset + num_frames): + if f < bone_rots[bone_idx].size(): + bone_rots[bone_idx][f] = gq; + rot_data_size = 8; + + elif flags & STUDIO_ANIM_ANIMROT: + var vptr_addr := data_start; + src.seek(vptr_addr); + var vptr: MStudioAnimValuePtr_t = ByteReader.read_by_structure(src, MStudioAnimValuePtr_t); + + var rx := _decode_axis(src, vptr_addr, vptr.offset[0], num_frames); + var ry := _decode_axis(src, vptr_addr, vptr.offset[1], num_frames); + var rz := _decode_axis(src, vptr_addr, vptr.offset[2], num_frames); + + for fi in range(num_frames): + var angles := Vector3( + _axis_value(rx, fi, bone.rot.x, bone.rot_scale.x), + _axis_value(ry, fi, bone.rot.y, bone.rot_scale.y), + _axis_value(rz, fi, bone.rot.z, bone.rot_scale.z) + ); + var q := MStudioAnimDecoder.angle_quaternion(angles); + if is_delta: + q = q * bone.quat; + var f := frame_offset + fi; + if f < bone_rots[bone_idx].size(): + bone_rots[bone_idx][f] = _transform_quaternion(q); + rot_data_size = 6; + + # ── Position ────────────────────────────────────────────────────────── + var pos_start: int = data_start + rot_data_size; + + if flags & STUDIO_ANIM_RAWPOS: + src.seek(pos_start); + var p := MStudioAnimDecoder.read_vector48(src); + if is_delta: + p = p + bone.pos; + var gp := _transform_vector(p); + for f in range(frame_offset, frame_offset + num_frames): + if f < bone_poss[bone_idx].size(): + bone_poss[bone_idx][f] = gp; + + elif flags & STUDIO_ANIM_ANIMPOS: + var vptr_addr := pos_start; + src.seek(vptr_addr); + var vptr: MStudioAnimValuePtr_t = ByteReader.read_by_structure(src, MStudioAnimValuePtr_t); + + var px := _decode_axis(src, vptr_addr, vptr.offset[0], num_frames); + var py := _decode_axis(src, vptr_addr, vptr.offset[1], num_frames); + var pz := _decode_axis(src, vptr_addr, vptr.offset[2], num_frames); + + for fi in range(num_frames): + var p := Vector3( + _axis_value(px, fi, bone.pos.x, bone.pos_scale.x), + _axis_value(py, fi, bone.pos.y, bone.pos_scale.y), + _axis_value(pz, fi, bone.pos.z, bone.pos_scale.z) + ); + if is_delta: + p = p + bone.pos; + var f := frame_offset + fi; + if f < bone_poss[bone_idx].size(): + bone_poss[bone_idx][f] = _transform_vector(p); + +# ── Helpers ──────────────────────────────────────────────────────────────── + +## Decodes animation values for one axis. Returns empty array when offset <= 0 (use rest). +func _decode_axis(src: FileAccess, vptr_addr: int, offset: int, num_frames: int) -> Array[int]: + if offset <= 0: + return []; + return MStudioAnimDecoder.decode_anim_values(src, vptr_addr + offset, num_frames); + +## Returns the animated value for a single axis/frame, or the rest-pose value if no data. +func _axis_value(values: Array[int], fi: int, rest: float, scale: float) -> float: + if values.is_empty(): + return rest; + var v: int = values[fi] if fi < values.size() else values[-1]; + return float(v) * scale; diff --git a/addons/godotvmf/godotmdl/ani/index.gd.uid b/addons/godotvmf/godotmdl/ani/index.gd.uid new file mode 100644 index 0000000..ad3dfe8 --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/index.gd.uid @@ -0,0 +1 @@ +uid://c3y01l14lawdc diff --git a/addons/godotvmf/godotmdl/ani/mstudioactivitymodifier_t.gd b/addons/godotvmf/godotmdl/ani/mstudioactivitymodifier_t.gd new file mode 100644 index 0000000..8bbefe8 --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/mstudioactivitymodifier_t.gd @@ -0,0 +1,13 @@ +class_name MStudioActivityModifier_t extends RefCounted + +static var scheme: + get: return { + sznameindex = ByteReader.Type.INT, + } + +var sznameindex: int + +var address: int = 0 + +func _to_string(): + return "MStudioActivityModifier_t: {sznameindex: %d}" % [sznameindex] diff --git a/addons/godotvmf/godotmdl/ani/mstudioactivitymodifier_t.gd.uid b/addons/godotvmf/godotmdl/ani/mstudioactivitymodifier_t.gd.uid new file mode 100644 index 0000000..7471998 --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/mstudioactivitymodifier_t.gd.uid @@ -0,0 +1 @@ +uid://bxwbkoubc46mv diff --git a/addons/godotvmf/godotmdl/ani/mstudioanim_t.gd b/addons/godotvmf/godotmdl/ani/mstudioanim_t.gd new file mode 100644 index 0000000..bc391ce --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/mstudioanim_t.gd @@ -0,0 +1,17 @@ +class_name MStudioAnim_t extends RefCounted + +static var scheme: + get: return { + bone = ByteReader.Type.BYTE, + flags = ByteReader.Type.BYTE, + nextoffset = ByteReader.Type.SHORT, + } + +var bone: int +var flags: int +var nextoffset: int + +var address: int = 0 + +func _to_string(): + return "MStudioAnim_t: {bone: %d, flags: %d, nextoffset: %d}" % [bone, flags, nextoffset] diff --git a/addons/godotvmf/godotmdl/ani/mstudioanim_t.gd.uid b/addons/godotvmf/godotmdl/ani/mstudioanim_t.gd.uid new file mode 100644 index 0000000..8f3ac04 --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/mstudioanim_t.gd.uid @@ -0,0 +1 @@ +uid://kaqs4exc85j7 diff --git a/addons/godotvmf/godotmdl/ani/mstudioanimdecoder.gd b/addons/godotvmf/godotmdl/ani/mstudioanimdecoder.gd new file mode 100644 index 0000000..047a3df --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/mstudioanimdecoder.gd @@ -0,0 +1,107 @@ +class_name MStudioAnimDecoder extends RefCounted + +static func decode_float16(val: int) -> float: + var sign: int = (val >> 15) & 1 + var exp: int = (val >> 10) & 0x1F + var mant: int = val & 0x3FF + + if exp == 0: + if mant == 0: + return 0.0 if sign == 0 else -0.0 + else: + return (1.0 if sign == 0 else -1.0) * pow(2.0, -14.0) * (float(mant) / 1024.0) + elif exp == 31: + if mant == 0: + return INF if sign == 0 else -INF + else: + return NAN + else: + return (1.0 if sign == 0 else -1.0) * pow(2.0, exp - 15.0) * (1.0 + float(mant) / 1024.0) + +static func read_vector48(file: FileAccess) -> Vector3: + return Vector3( + decode_float16(file.get_16()), + decode_float16(file.get_16()), + decode_float16(file.get_16()) + ) + +static func read_quaternion48(file: FileAccess) -> Quaternion: + var x_val = file.get_16() + var y_val = file.get_16() + var z_wneg = file.get_16() + + var x = (float(x_val) - 32768.0) / 32768.0 + var y = (float(y_val) - 32768.0) / 32768.0 + var z = (float(z_wneg & 0x7FFF) - 16384.0) / 16384.0 + var wneg = (z_wneg >> 15) & 1 + + var w_sq = 1.0 - x*x - y*y - z*z + var w = sqrt(max(0.0, w_sq)) + if wneg: + w = -w + + return Quaternion(x, y, z, w) + +static func read_quaternion64(file: FileAccess) -> Quaternion: + var v1 = file.get_32() + var v2 = file.get_32() + + var x_val = v1 & 0x1FFFFF + var y_val = (v1 >> 21) | ((v2 & 0x3FF) << 11) + var z_val = (v2 >> 10) & 0x1FFFFF + var wneg = (v2 >> 31) & 1 + + var x = (float(x_val) - 1048576.0) / 1048576.5 + var y = (float(y_val) - 1048576.0) / 1048576.5 + var z = (float(z_val) - 1048576.0) / 1048576.5 + + var w_sq = 1.0 - x*x - y*y - z*z + var w = sqrt(max(0.0, w_sq)) + if wneg: + w = -w + + return Quaternion(x, y, z, w) + +## Converts Source Engine RadianEuler (angles.x=pitch, angles.y=yaw, angles.z=roll) +## to a Quaternion using the same formula as Source SDK's AngleQuaternion(RadianEuler). +static func angle_quaternion(angles: Vector3) -> Quaternion: + var sy = sin(angles.z * 0.5) + var cy = cos(angles.z * 0.5) + var sp = sin(angles.x * 0.5) + var cp = cos(angles.x * 0.5) + var sr = sin(angles.y * 0.5) + var cr = cos(angles.y * 0.5) + return Quaternion( + sr*cp*cy - cr*sp*sy, + cr*sp*cy + sr*cp*sy, + cr*cp*sy - sr*sp*cy, + cr*cp*cy + sr*sp*sy + ) + +static func decode_anim_values(file: FileAccess, start_address: int, num_frames: int) -> Array[int]: + var values: Array[int] = [] + if start_address == 0: + return values + + file.seek(start_address) + var k = 0 + while k < num_frames: + var valid = file.get_8() + var total = file.get_8() + if total == 0: + break + + var valid_values = [] + for j in range(valid): + valid_values.append(ByteReader.read_signed_short(file)) + + for j in range(total): + if j < valid: + values.append(valid_values[j]) + else: + if valid_values.size() > 0: + values.append(valid_values[-1]) + else: + values.append(0) + k += total + return values diff --git a/addons/godotvmf/godotmdl/ani/mstudioanimdecoder.gd.uid b/addons/godotvmf/godotmdl/ani/mstudioanimdecoder.gd.uid new file mode 100644 index 0000000..b548f04 --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/mstudioanimdecoder.gd.uid @@ -0,0 +1 @@ +uid://clec3me0qdnow diff --git a/addons/godotvmf/godotmdl/ani/mstudioanimdesc_t.gd b/addons/godotvmf/godotmdl/ani/mstudioanimdesc_t.gd new file mode 100644 index 0000000..e5278ed --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/mstudioanimdesc_t.gd @@ -0,0 +1,53 @@ +class_name MStudioAnimDesc_t extends RefCounted + +static var scheme: + get: return { + baseptr = ByteReader.Type.INT, + pszName = ByteReader.Type.INLINE_STRING, + fps = ByteReader.Type.FLOAT, + flags = ByteReader.Type.INT, + numframes = ByteReader.Type.INT, + nummovements = ByteReader.Type.INT, + movementindex = ByteReader.Type.INT, + unused1 = [ByteReader.Type.INT, 6], + animblock = ByteReader.Type.INT, + animindex = ByteReader.Type.INT, + numikrules = ByteReader.Type.INT, + ikruleindex = ByteReader.Type.INT, + animblockikruleindex = ByteReader.Type.INT, + numlocalhierarchy = ByteReader.Type.INT, + localhierarchyindex = ByteReader.Type.INT, + sectionindex = ByteReader.Type.INT, + sectionframes = ByteReader.Type.INT, + zeroframespan = ByteReader.Type.SHORT, + zeroframecount = ByteReader.Type.SHORT, + zeroframeindex = ByteReader.Type.INT, + zeroframestalltime = ByteReader.Type.FLOAT, +} + +var baseptr: int +var pszName: String +var fps: float +var flags: int +var numframes: int +var nummovements: int +var movementindex: int +var unused1: Array[int] +var animblock: int +var animindex: int +var numikrules: int +var ikruleindex: int +var animblockikruleindex: int +var numlocalhierarchy: int +var localhierarchyindex: int +var sectionindex: int +var sectionframes: int +var zeroframespan: int +var zeroframecount: int +var zeroframeindex: int +var zeroframestalltime: float + +var address: int = 0 + +func _to_string(): + return "MStudioAnimDesc_t: {baseptr: %d, pszName: %s, fps: %f, flags: %d, numframes: %d, nummovements: %d, movementindex: %d, unused1: %s, animblock: %d, animindex: %d, numikrules: %d, ikruleindex: %d, animblockikruleindex: %d, numlocalhierarchy: %d, localhierarchyindex: %d, sectionindex: %d, sectionframes: %d, zeroframespan: %d, zeroframecount: %d, zeroframeindex: %d, zeroframestalltime: %f}" % [baseptr, pszName, fps, flags, numframes, nummovements, movementindex, unused1, animblock, animindex, numikrules, ikruleindex, animblockikruleindex, numlocalhierarchy, localhierarchyindex, sectionindex, sectionframes, zeroframespan, zeroframecount, zeroframeindex, zeroframestalltime] diff --git a/addons/godotvmf/godotmdl/ani/mstudioanimdesc_t.gd.uid b/addons/godotvmf/godotmdl/ani/mstudioanimdesc_t.gd.uid new file mode 100644 index 0000000..35feee6 --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/mstudioanimdesc_t.gd.uid @@ -0,0 +1 @@ +uid://36n1clirwqq diff --git a/addons/godotvmf/godotmdl/ani/mstudioanimsections_t.gd b/addons/godotvmf/godotmdl/ani/mstudioanimsections_t.gd new file mode 100644 index 0000000..0a15289 --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/mstudioanimsections_t.gd @@ -0,0 +1,15 @@ +class_name MStudioAnimSections_t extends RefCounted + +static var scheme: + get: return { + animblock = ByteReader.Type.INT, + animindex = ByteReader.Type.INT, + } + +var animblock: int +var animindex: int + +var address: int = 0 + +func _to_string(): + return "MStudioAnimSections_t: {animblock: %d, animindex: %d}" % [animblock, animindex] diff --git a/addons/godotvmf/godotmdl/ani/mstudioanimsections_t.gd.uid b/addons/godotvmf/godotmdl/ani/mstudioanimsections_t.gd.uid new file mode 100644 index 0000000..0c10019 --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/mstudioanimsections_t.gd.uid @@ -0,0 +1 @@ +uid://5cvbnnkfu636 diff --git a/addons/godotvmf/godotmdl/ani/mstudioanimvalue_t.gd b/addons/godotvmf/godotmdl/ani/mstudioanimvalue_t.gd new file mode 100644 index 0000000..6cdfc6f --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/mstudioanimvalue_t.gd @@ -0,0 +1,22 @@ +class_name MStudioAnimValue_t extends RefCounted + +static var scheme: + get: return { + valid = ByteReader.Type.BYTE, + total = ByteReader.Type.BYTE, + } + +var valid: int +var total: int + +var address: int = 0 + +var value: int: + get: + var val = (total << 8) | valid + if val > 32767: + val -= 65536 + return val + +func _to_string(): + return "MStudioAnimValue_t: {valid: %d, total: %d, value: %d}" % [valid, total, value] diff --git a/addons/godotvmf/godotmdl/ani/mstudioanimvalue_t.gd.uid b/addons/godotvmf/godotmdl/ani/mstudioanimvalue_t.gd.uid new file mode 100644 index 0000000..3ccd1a9 --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/mstudioanimvalue_t.gd.uid @@ -0,0 +1 @@ +uid://dcyfbfo6cs5io diff --git a/addons/godotvmf/godotmdl/ani/mstudioanimvalueptr_t.gd b/addons/godotvmf/godotmdl/ani/mstudioanimvalueptr_t.gd new file mode 100644 index 0000000..adb1636 --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/mstudioanimvalueptr_t.gd @@ -0,0 +1,13 @@ +class_name MStudioAnimValuePtr_t extends RefCounted + +static var scheme: + get: return { + offset = [ByteReader.Type.SHORT, 3], + } + +var offset: Array[int] + +var address: int = 0 + +func _to_string(): + return "MStudioAnimValuePtr_t: {offset: %s}" % [offset] diff --git a/addons/godotvmf/godotmdl/ani/mstudioanimvalueptr_t.gd.uid b/addons/godotvmf/godotmdl/ani/mstudioanimvalueptr_t.gd.uid new file mode 100644 index 0000000..21337a4 --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/mstudioanimvalueptr_t.gd.uid @@ -0,0 +1 @@ +uid://c5n0rd2alkewk diff --git a/addons/godotvmf/godotmdl/ani/mstudioautolayer_t.gd b/addons/godotvmf/godotmdl/ani/mstudioautolayer_t.gd new file mode 100644 index 0000000..933019a --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/mstudioautolayer_t.gd @@ -0,0 +1,25 @@ +class_name MStudioAutoLayer_t extends RefCounted + +static var scheme: + get: return { + iSequence = ByteReader.Type.SHORT, + iPose = ByteReader.Type.SHORT, + flags = ByteReader.Type.INT, + start = ByteReader.Type.FLOAT, + peak = ByteReader.Type.FLOAT, + tail = ByteReader.Type.FLOAT, + end = ByteReader.Type.FLOAT, + } + +var iSequence: int +var iPose: int +var flags: int +var start: float +var peak: float +var tail: float +var end: float + +var address: int = 0 + +func _to_string(): + return "MStudioAutoLayer_t: {iSequence: %d, iPose: %d, flags: %d, start: %f, peak: %f, tail: %f, end: %f}" % [iSequence, iPose, flags, start, peak, tail, end] diff --git a/addons/godotvmf/godotmdl/ani/mstudioautolayer_t.gd.uid b/addons/godotvmf/godotmdl/ani/mstudioautolayer_t.gd.uid new file mode 100644 index 0000000..56b1b50 --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/mstudioautolayer_t.gd.uid @@ -0,0 +1 @@ +uid://ci3m8gfsqd5eg diff --git a/addons/godotvmf/godotmdl/ani/mstudiocompressedikerror_t.gd b/addons/godotvmf/godotmdl/ani/mstudiocompressedikerror_t.gd new file mode 100644 index 0000000..6a594f3 --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/mstudiocompressedikerror_t.gd @@ -0,0 +1,15 @@ +class_name MStudioCompressedIKError_t extends RefCounted + +static var scheme: + get: return { + scale = [ByteReader.Type.FLOAT, 6], + offset = [ByteReader.Type.SHORT, 6], + } + +var scale: Array[float] +var offset: Array[int] + +var address: int = 0 + +func _to_string(): + return "MStudioCompressedIKError_t: {scale: %s, offset: %s}" % [scale, offset] diff --git a/addons/godotvmf/godotmdl/ani/mstudiocompressedikerror_t.gd.uid b/addons/godotvmf/godotmdl/ani/mstudiocompressedikerror_t.gd.uid new file mode 100644 index 0000000..75ab259 --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/mstudiocompressedikerror_t.gd.uid @@ -0,0 +1 @@ +uid://ni01jjoi7ovh diff --git a/addons/godotvmf/godotmdl/ani/mstudioevent_t.gd b/addons/godotvmf/godotmdl/ani/mstudioevent_t.gd new file mode 100644 index 0000000..89258f7 --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/mstudioevent_t.gd @@ -0,0 +1,21 @@ +class_name MStudioEvent_t extends RefCounted + +static var scheme: + get: return { + cycle = ByteReader.Type.FLOAT, + event = ByteReader.Type.INT, + type = ByteReader.Type.INT, + options = [ByteReader.Type.STRING, 64], + szeventindex = ByteReader.Type.INT, + } + +var cycle: float +var event: int +var type: int +var options: String +var szeventindex: int + +var address: int = 0 + +func _to_string(): + return "MStudioEvent_t: {cycle: %f, event: %d, type: %d, options: %s, szeventindex: %d}" % [cycle, event, type, options, szeventindex] diff --git a/addons/godotvmf/godotmdl/ani/mstudioevent_t.gd.uid b/addons/godotvmf/godotmdl/ani/mstudioevent_t.gd.uid new file mode 100644 index 0000000..74f2b73 --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/mstudioevent_t.gd.uid @@ -0,0 +1 @@ +uid://b1bpev2ek04db diff --git a/addons/godotvmf/godotmdl/ani/mstudiolocalhierarchy_t.gd b/addons/godotvmf/godotmdl/ani/mstudiolocalhierarchy_t.gd new file mode 100644 index 0000000..ce98fb0 --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/mstudiolocalhierarchy_t.gd @@ -0,0 +1,29 @@ +class_name MStudioLocalHierarchy_t extends RefCounted + +static var scheme: + get: return { + iBone = ByteReader.Type.INT, + iNewParent = ByteReader.Type.INT, + start = ByteReader.Type.FLOAT, + peak = ByteReader.Type.FLOAT, + tail = ByteReader.Type.FLOAT, + end = ByteReader.Type.FLOAT, + iStart = ByteReader.Type.INT, + localanimindex = ByteReader.Type.INT, + unused = [ByteReader.Type.INT, 4], + } + +var iBone: int +var iNewParent: int +var start: float +var peak: float +var tail: float +var end: float +var iStart: int +var localanimindex: int +var unused: Array[int] + +var address: int = 0 + +func _to_string(): + return "MStudioLocalHierarchy_t: {iBone: %d, iNewParent: %d, start: %f, peak: %f, tail: %f, end: %f, iStart: %d, localanimindex: %d}" % [iBone, iNewParent, start, peak, tail, end, iStart, localanimindex] diff --git a/addons/godotvmf/godotmdl/ani/mstudiolocalhierarchy_t.gd.uid b/addons/godotvmf/godotmdl/ani/mstudiolocalhierarchy_t.gd.uid new file mode 100644 index 0000000..5c7f7d7 --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/mstudiolocalhierarchy_t.gd.uid @@ -0,0 +1 @@ +uid://y26wpcgr6rda diff --git a/addons/godotvmf/godotmdl/ani/mstudioseqdesc_t.gd b/addons/godotvmf/godotmdl/ani/mstudioseqdesc_t.gd new file mode 100644 index 0000000..dca565f --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/mstudioseqdesc_t.gd @@ -0,0 +1,99 @@ +class_name MStudioSeqDesc_t extends RefCounted + +static var scheme: + get: return { + baseptr = ByteReader.Type.INT, + szlabelindex = ByteReader.Type.INT, + szactivitynameindex = ByteReader.Type.INT, + flags = ByteReader.Type.INT, + activity = ByteReader.Type.INT, + actweight = ByteReader.Type.INT, + numevents = ByteReader.Type.INT, + eventindex = ByteReader.Type.INT, + bbmin = ByteReader.Type.VECTOR3, + bbmax = ByteReader.Type.VECTOR3, + numblends = ByteReader.Type.INT, + animindexindex = ByteReader.Type.INT, + movementindex = ByteReader.Type.INT, + groupsize = [ByteReader.Type.INT, 2], + paramindex = [ByteReader.Type.INT, 2], + paramstart = [ByteReader.Type.FLOAT, 2], + paramend = [ByteReader.Type.FLOAT, 2], + paramparent = ByteReader.Type.INT, + fadeintime = ByteReader.Type.FLOAT, + fadeouttime = ByteReader.Type.FLOAT, + localentrynode = ByteReader.Type.INT, + localexitnode = ByteReader.Type.INT, + nodeflags = ByteReader.Type.INT, + entryphase = ByteReader.Type.FLOAT, + exitphase = ByteReader.Type.FLOAT, + lastframe = ByteReader.Type.FLOAT, + nextseq = ByteReader.Type.INT, + pose = ByteReader.Type.INT, + numikrules = ByteReader.Type.INT, + numautolayers = ByteReader.Type.INT, + autolayerindex = ByteReader.Type.INT, + weightlistindex = ByteReader.Type.INT, + posekeyindex = ByteReader.Type.INT, + numiklocks = ByteReader.Type.INT, + iklockindex = ByteReader.Type.INT, + keyvalueindex = ByteReader.Type.INT, + keyvaluesize = ByteReader.Type.INT, + cycleposeindex = ByteReader.Type.INT, + activitymodifierindex = ByteReader.Type.INT, + numactivitymodifiers = ByteReader.Type.INT, + animtagindex = ByteReader.Type.INT, + numanimtags = ByteReader.Type.INT, + rootDriverIndex = ByteReader.Type.INT, + unused = [ByteReader.Type.INT, 2], + } + +var baseptr: int +var szlabelindex: int +var szactivitynameindex: int +var flags: int +var activity: int +var actweight: int +var numevents: int +var eventindex: int +var bbmin: Vector3 +var bbmax: Vector3 +var numblends: int +var animindexindex: int +var movementindex: int +var groupsize: Array[int] +var paramindex: Array[int] +var paramstart: Array[float] +var paramend: Array[float] +var paramparent: int +var fadeintime: float +var fadeouttime: float +var localentrynode: int +var localexitnode: int +var nodeflags: int +var entryphase: float +var exitphase: float +var lastframe: float +var nextseq: int +var pose: int +var numikrules: int +var numautolayers: int +var autolayerindex: int +var weightlistindex: int +var posekeyindex: int +var numiklocks: int +var iklockindex: int +var keyvalueindex: int +var keyvaluesize: int +var cycleposeindex: int +var activitymodifierindex: int +var numactivitymodifiers: int +var animtagindex: int +var numanimtags: int +var rootDriverIndex: int +var unused: Array[int] + +var address: int = 0 + +func _to_string(): + return "MStudioSeqDesc_t: {baseptr: %d, flags: %d, activity: %d, numblends: %d}" % [baseptr, flags, activity, numblends] diff --git a/addons/godotvmf/godotmdl/ani/mstudioseqdesc_t.gd.uid b/addons/godotvmf/godotmdl/ani/mstudioseqdesc_t.gd.uid new file mode 100644 index 0000000..fde76a3 --- /dev/null +++ b/addons/godotvmf/godotmdl/ani/mstudioseqdesc_t.gd.uid @@ -0,0 +1 @@ +uid://dbrv3ghmj36sk diff --git a/addons/godotvmf/godotmdl/import.gd b/addons/godotvmf/godotmdl/import.gd index 337a0f4..dc239f1 100644 --- a/addons/godotvmf/godotmdl/import.gd +++ b/addons/godotvmf/godotmdl/import.gd @@ -90,14 +90,15 @@ func _import(mdl_path: String, save_path: String, options: Dictionary, _platform var vtx_path = mdl_path.replace(".mdl", ".vtx"); var vvd_path = mdl_path.replace(".mdl", ".vvd"); var phy_path = mdl_path.replace(".mdl", ".phy"); - # TODO: Add support for animations - # var ani_path = mdl_path.replace(".mdl", ".ani"); - # var ani = ANIReader.new(mdl_path, mdl.header); + var ani_path = mdl_path.replace(".mdl", ".ani"); var mdl = MDLReader.new(mdl_path); var vtx = VTXReader.new(vtx_path, mdl.header.version); var vvd = VVDReader.new(vvd_path); - var phy = PHYReader.new(phy_path); + var phy = PHYReader.new(phy_path, mdl); + # var ani := ANIReader.new(ani_path, mdl); + + mdl.close(); var model_name = mdl_path.get_file().get_basename().replace(".mdl", ""); @@ -111,7 +112,9 @@ func _import(mdl_path: String, save_path: String, options: Dictionary, _platform return ERR_PARSE_ERROR; var path_to_save = save_path + '.' + _get_save_extension(); - var combiner = MDLCombiner.new(mdl, vtx, vvd, phy, options); + var combiner = MDLCombiner.new(mdl, vtx, vvd, phy, null, options); + + var scn = pack_model(combiner.mesh_instance, model_name); var error = ResourceSaver.save(scn, path_to_save, ResourceSaver.FLAG_COMPRESS); diff --git a/addons/godotvmf/godotmdl/mdl_combiner.gd b/addons/godotvmf/godotmdl/mdl_combiner.gd index 8327a46..d3391ab 100644 --- a/addons/godotvmf/godotmdl/mdl_combiner.gd +++ b/addons/godotvmf/godotmdl/mdl_combiner.gd @@ -11,6 +11,19 @@ var vtx: VTXReader; var vvd: VVDReader; var phy: PHYReader; +## Basis from x y z -> y z -x and rotated by 90 degrees around y-axis +const SOURCE_TO_GODOT_DYNAMIC := Basis( + Vector3( -1, 0, 0), + Vector3( 0, 0, 1), + Vector3( 0, 1, 0) +); + +const SOURCE_TO_GODOT_STATIC := Basis( + Vector3( 0, 0,-1), + Vector3(-1, 0, 0), + Vector3( 0, 1, 0) +); + var is_static_body: bool: get: return (mdl.header.flags & mdl.MDLFlag.STATIC_PROP) != 0; @@ -23,6 +36,9 @@ var rotation_radians: Vector3: var additional_basis: Basis: get: return Basis.from_euler(rotation_radians); +var mdl_version: int: + get: return mdl.header.version; + var is_static_prop: bool: get: return (mdl.header.flags & mdl.MDLFlag.STATIC_PROP) != 0; @@ -38,7 +54,7 @@ static func apply_skin(mesh_instance: MeshInstance3D, skin_id: int, directly: bo else: mesh_instance.set_surface_override_material(surface_idx, materials[surface_idx]); -func _init(mdl: MDLReader, vtx: VTXReader, vvd: VVDReader, phy: PHYReader, options: Dictionary): +func _init(mdl: MDLReader, vtx: VTXReader, vvd: VVDReader, phy: PHYReader, ani: ANIReader, options: Dictionary): self.options = options; self.mdl = mdl; self.vtx = vtx; @@ -53,6 +69,29 @@ func _init(mdl: MDLReader, vtx: VTXReader, vvd: VVDReader, phy: PHYReader, optio create_occluder(); assign_materials(); + if ani: add_animations(ani.library); + +func get_model_transform_basis() -> Basis: + if is_static_body: + return SOURCE_TO_GODOT_STATIC; + + var solid_count = phy.solid_count; + + if mdl.header.version == 48: + return SOURCE_TO_GODOT_DYNAMIC; + + if (mdl.header.version == 47 && solid_count == 1) or mdl.header.version >= 49: + return SOURCE_TO_GODOT_DYNAMIC.rotated(Vector3.RIGHT, -PI / 2); + + return SOURCE_TO_GODOT_DYNAMIC; + +func transform_vector(vec: Vector3) -> Vector3: + return get_model_transform_basis() * vec * scale; + +func transform_basis(basis: Basis) -> Basis: + var target_basis := get_model_transform_basis(); + return target_basis * basis * target_basis.inverse(); + func setup_mesh_instance(): var scale = options.scale if not options.use_global_scale else VMFConfig.import.scale; @@ -96,12 +135,12 @@ func process_mesh(mesh: VTXReader.VTXMesh, body_part_index: int, model_index: in var vert := vvd.vertices[vid]; var tangent := vvd.tangents[vid]; - st.set_normal(vert.normal * additional_basis); + st.set_normal(transform_vector(vert.normal)); st.set_tangent(tangent); st.set_uv(vert.uv); st.set_bones(vert.bone_weight.bone_bytes); st.set_weights(vert.bone_weight.weight_bytes); - st.add_vertex(vert.position * additional_basis.scaled(Vector3.ONE * scale)); + st.add_vertex(transform_vector(vert.position)); for indice in strip_group.indices: if indice > strip_group.vertices.size() - 1: break; @@ -188,6 +227,17 @@ func generate_collision(): for solid in surface.solids: # NOTE: Skip the last solid since it's a fullbody collision shape if solid_index == surface.solids.size() - 1 and surface.solids.size() > 1: break; + var target_bone_index := mdl.bones.find_custom(func(bone: MDLReader.MDLBone): return bone.id == solid.bone_index); + var target_bone: MDLReader.MDLBone; + var bone_transform: Transform3D; + + if target_bone_index != -1: + target_bone = mdl.bones[target_bone_index]; + + bone_transform = Transform3D( + Basis(target_bone.quat), + target_bone.pos, + ); if not is_static_body: static_body = StaticBody3D.new(); @@ -202,20 +252,20 @@ func generate_collision(): var shape: ConvexPolygonShape3D = ConvexPolygonShape3D.new(); collision.name = "collision_" + str(surface_index) + "_" + str(solid_index); - static_body.basis *= additional_basis; var vertices = []; for face in solid.faces: - var v1 = surface.vertices[face.v1] * additional_basis.scaled(Vector3.ONE * scale); - var v2 = surface.vertices[face.v2] * additional_basis.scaled(Vector3.ONE * scale); - var v3 = surface.vertices[face.v3] * additional_basis.scaled(Vector3.ONE * scale); + var v1 = transform_vector(phy.transform_vertex(surface.vertices[face.v1], solid.bone_index)); + var v2 = transform_vector(phy.transform_vertex(surface.vertices[face.v2], solid.bone_index)); + var v3 = transform_vector(phy.transform_vertex(surface.vertices[face.v3], solid.bone_index)); vertices.append_array([v1, v2, v3]); shape.points = PackedVector3Array(vertices); collision.shape = shape; + if not is_static_body: var bone_attachment: BoneAttachment3D = BoneAttachment3D.new(); bone_attachment.name = "bone_attachment_" + str(surface_index) + "_" + str(solid_index); @@ -225,10 +275,15 @@ func generate_collision(): bone_attachment.set_owner(mesh_instance); static_body.set_owner(mesh_instance); else: - # NOTE: We don't need bone attachment for static bodies since they has only one bone - if static_body.get_parent() != mesh_instance: - mesh_instance.add_child(static_body); - static_body.set_owner(mesh_instance); + var bone_attachment: BoneAttachment3D = mesh_instance.get_node_or_null("static_body_attachment"); + + if not bone_attachment: + bone_attachment = BoneAttachment3D.new(); + bone_attachment.name = "static_body_attachment" + mesh_instance.add_child(bone_attachment); + bone_attachment.set_owner(mesh_instance); + bone_attachment.add_child(static_body); + static_body.set_owner(mesh_instance); static_body.add_child(collision); collision.set_owner(mesh_instance); @@ -238,33 +293,20 @@ func generate_collision(): surface_index += 1; func setup_skeleton(): - if is_static_prop: return; - if Engine.get_version_info().minor < 4: return; - if is_static_body: return; skeleton.basis = additional_basis.inverse(); for bone in mdl.bones: skeleton.add_bone(bone.name); - for bone in mdl.bones: + for bone in mdl.bones as Array[MDLReader.MDLBone]: if bone.parent != -1: skeleton.set_bone_parent(bone.id, bone.parent); - var parent_bone = mdl.bones[bone.parent]; - var parent_transform = parent_bone.pos_to_bone if parent_bone else Transform3D.IDENTITY; - var target_transform = bone.pos_to_bone * parent_transform; - var basis = additional_basis if bone.parent == -1 else Basis.IDENTITY; - var transform = Transform3D(Basis(bone.quat), bone.pos); - transform = transform.scaled(Vector3.ONE * scale); - - skeleton.set_bone_global_pose_override(bone.id, target_transform, 1.0); - skeleton.set_bone_pose_position(bone.id, transform.origin); - skeleton.set_bone_pose_rotation(bone.id, transform.basis.get_rotation_quaternion()); + var rest_basis = transform_basis(Basis(bone.quat)); + var rest_transform = Transform3D(rest_basis, transform_vector(bone.pos)); - var target_rest_pose = skeleton.get_bone_pose(bone.id); - - skeleton.set_bone_rest(bone.id, target_rest_pose); + skeleton.set_bone_rest(bone.id, rest_transform); skeleton.reset_bone_pose(bone.id); var skin = skeleton.create_skin_from_rest_transforms(); @@ -278,7 +320,6 @@ func assign_materials(): var materials = []; var skin = 0; var mesh = mesh_instance.mesh; - for tex in mdl.textures: for dir in mdl.textureDirs: @@ -330,3 +371,13 @@ func generate_lods(): mesh.set_meta(meta, mesh.get_meta(meta)); mesh_instance.set_mesh(mesh); + +func add_animations(library: AnimationLibrary): + if not library or library.get_animation_list().is_empty(): return; + + var anim_player := AnimationPlayer.new(); + anim_player.name = "animation_player"; + anim_player.add_animation_library("", library); + + mesh_instance.add_child(anim_player); + anim_player.set_owner(mesh_instance); diff --git a/addons/godotvmf/godotmdl/mdl_reader.gd b/addons/godotvmf/godotmdl/mdl_reader.gd index cc688c9..99756b4 100644 --- a/addons/godotvmf/godotmdl/mdl_reader.gd +++ b/addons/godotvmf/godotmdl/mdl_reader.gd @@ -3,28 +3,28 @@ class_name MDLReader extends RefCounted # Structure classes #region classes enum MDLFlag { - AUTOGENERATED_HITBOX = 0x0001, - USES_ENV_CUBEMAP = 0x0002, - FORCE_OPAQUE = 0x0004, - TRANSLUCENT_TWOPASS = 0x0008, - STATIC_PROP = 0x0010, - USES_FB_TEXTURE = 0x0020, - HAS_SHADOWLOD = 0x0040, - USES_BUMPMAPPING = 0x0080, - USE_SHADOWLOD_MATERIALS = 0x0100, - OBSOLETE = 0x0200, - UNUSED = 0x0400, - NO_FORCED_FADE = 0x0800, - FORCE_PHONEME_CROSSFADE = 0x1000, - CONSTANT_DIRECTIONAL_LIGHT_DOT = 0x2000, - FLEXES_CONVERTED = 0x4000, - BUILT_IN_PREVIEW_MODE = 0x8000, - AMBIENT_BOOST = 0x10000, - DO_NOT_CAST_SHADOWS = 0x20000, - CAST_TEXTURE_SHADOWS = 0x40000, - NA1 = 0x80000, - NA2 = 0x100000, - VERT_ANIM_FIXED_POINT_SCALE = 0x200000, + AUTOGENERATED_HITBOX = 0x0001, # binary 0b0000000000000001 + USES_ENV_CUBEMAP = 0x0002, # binary 0b0000000000000010 + FORCE_OPAQUE = 0x0004, # binary 0b0000000000000100 + TRANSLUCENT_TWOPASS = 0x0008, # binary 0b0000000000001000 + STATIC_PROP = 0x0010, # binary 0b0000000000010000 + USES_FB_TEXTURE = 0x0020, # binary 0b0000000000100000 + HAS_SHADOWLOD = 0x0040, # binary 0b0000000001000000 + USES_BUMPMAPPING = 0x0080, # binary 0b0000000010000000 + USE_SHADOWLOD_MATERIALS = 0x0100, # binary 0b0000000100000000 + OBSOLETE = 0x0200, # binary 0b0000001000000000 + UNUSED = 0x0400, # binary 0b0000010000000000 + NO_FORCED_FADE = 0x0800, # binary 0b0000100000000000 + FORCE_PHONEME_CROSSFADE = 0x1000, # binary 0b0001000000000000 + CONSTANT_DIRECTIONAL_LIGHT_DOT = 0x2000, # binary 0b0010000000000000 + FLEXES_CONVERTED = 0x4000, # binary 0b0100000000000000 + BUILT_IN_PREVIEW_MODE = 0x8000, # binary 0b1000000000000000 + AMBIENT_BOOST = 0x10000, # binary 0b0000000100000000000000 + DO_NOT_CAST_SHADOWS = 0x20000, # binary 0b0000001000000000000000 + CAST_TEXTURE_SHADOWS = 0x40000, # binary 0b0000010000000000000000 + NA1 = 0x80000, # binary 0b0000100000000000000000 + NA2 = 0x100000, # binary 0b0001000000000000000000 + VERT_ANIM_FIXED_POINT_SCALE = 0x200000, # binary 0b0010000000000000000000 } class MDLHeader: @@ -478,7 +478,8 @@ var file: FileAccess; var materials: Array = []; var skin_families: Array = []; var source_path: String = ""; -var is_static_prop: bool = false; +var is_static_prop: bool: + get: return header.flags & MDLFlag.STATIC_PROP != 0; var model_name: get: return source_path.get_file().split(".")[0]; @@ -496,7 +497,7 @@ func _init(source_path: String): _read_body_parts(); _read_skin_families(); - file.close(); +func close(): file.close(); func _read_skin_families(): file.seek(header.skin_offset); @@ -541,8 +542,6 @@ func _read_bones(): bone.id = bone_index; bone_index += 1; - is_static_prop = bone.name == "static_prop"; - bone_controllers = ByteReader.read_array(file, header, "bone_controller_offset", "bone_controller_count", MDLBoneController); func get_possible_material_paths(): diff --git a/addons/godotvmf/godotmdl/phy_reader.gd b/addons/godotvmf/godotmdl/phy_reader.gd index ec1dbc8..ad54543 100644 --- a/addons/godotvmf/godotmdl/phy_reader.gd +++ b/addons/godotvmf/godotmdl/phy_reader.gd @@ -1,12 +1,13 @@ class_name PHYReader extends RefCounted + class PHYHeader: static var scheme: get: return { - size = ByteReader.Type.INT, - id = ByteReader.Type.INT, - solid_count = ByteReader.Type.INT, - checksum = ByteReader.Type.INT, + size = ByteReader.Type.INT, + id = ByteReader.Type.INT, + solid_count = ByteReader.Type.INT, + checksum = ByteReader.Type.INT, }; var size: int; @@ -22,15 +23,15 @@ class PHYHeader: class PHYSurfaceHeader: static var scheme: get: return { - size = ByteReader.Type.INT, - id = [ByteReader.Type.STRING, 4], - version = ByteReader.Type.SHORT, - model_type = ByteReader.Type.SHORT, - surface_size = ByteReader.Type.INT, - drag_axis_areas = ByteReader.Type.VECTOR3, - axis_map_size = ByteReader.Type.INT, - unused1 = [ByteReader.Type.INT, 11], - ivps = [ByteReader.Type.STRING, 4], + size = ByteReader.Type.INT, + id = [ByteReader.Type.STRING, 4], + version = ByteReader.Type.SHORT, + model_type = ByteReader.Type.SHORT, + surface_size = ByteReader.Type.INT, + drag_axis_areas = ByteReader.Type.VECTOR3, + axis_map_size = ByteReader.Type.INT, + unused1 = [ByteReader.Type.INT, 11], + ivps = [ByteReader.Type.STRING, 4], }; var size: int; @@ -54,13 +55,13 @@ class PHYSurfaceHeader: class PHYLegacySurfaceHeader: static var scheme: get: return { - mass_center = ByteReader.Type.VECTOR3, - rotation_inertia = ByteReader.Type.VECTOR3, - upper_limit_radius = ByteReader.Type.FLOAT, - max_deviation = ByteReader.Type.INT, - byte_size = ByteReader.Type.INT, - dummy = [ByteReader.Type.INT, 2], - ivps = [ByteReader.Type.STRING, 4], + mass_center = ByteReader.Type.VECTOR3, + rotation_inertia = ByteReader.Type.VECTOR3, + upper_limit_radius = ByteReader.Type.FLOAT, + max_deviation = ByteReader.Type.INT, + byte_size = ByteReader.Type.INT, + dummy = [ByteReader.Type.INT, 2], + ivps = [ByteReader.Type.STRING, 4], }; var mass_center: Vector3; @@ -80,10 +81,10 @@ class PHYLegacySurfaceHeader: class PHYSolidHeader: static var scheme: get: return { - vertices_offset = ByteReader.Type.INT, - bone_index = ByteReader.Type.INT, - flags = ByteReader.Type.INT, - face_count = ByteReader.Type.INT, + vertices_offset = ByteReader.Type.INT, + bone_index = ByteReader.Type.INT, + flags = ByteReader.Type.INT, + face_count = ByteReader.Type.INT, }; var vertices_offset: int; @@ -104,15 +105,15 @@ class PHYSolidHeader: class PHYTriangleData: static var scheme: get: return { - vertex_index = ByteReader.Type.BYTE, - unused1 = ByteReader.Type.BYTE, - unused2 = ByteReader.Type.UNSIGNED_SHORT, - v1 = ByteReader.Type.SHORT, - unused3 = ByteReader.Type.SHORT, - v2 = ByteReader.Type.SHORT, - unused4 = ByteReader.Type.SHORT, - v3 = ByteReader.Type.SHORT, - unused5 = ByteReader.Type.SHORT, + vertex_index = ByteReader.Type.BYTE, + unused1 = ByteReader.Type.BYTE, + unused2 = ByteReader.Type.UNSIGNED_SHORT, + v1 = ByteReader.Type.SHORT, + unused3 = ByteReader.Type.SHORT, + v2 = ByteReader.Type.SHORT, + unused4 = ByteReader.Type.SHORT, + v3 = ByteReader.Type.SHORT, + unused5 = ByteReader.Type.SHORT, }; var vertex_index: int; @@ -135,8 +136,42 @@ class PHYTriangleData: var header: PHYHeader; var surfaces: Array[PHYSurfaceHeader] = []; var legacy_surfaces: Array[PHYLegacySurfaceHeader] = []; - -func _init(source_file: String): +var mdl: MDLReader; +var solid_count: int = 0; ## Somethimes the info the header is wrong so we'll count the solids count manually :C (airboat.mdl) + +const TO_INCHES = 39.3700787402; +const ROT_X_NEG90 := Basis(Vector3(1, 0, 0), Vector3(0, 0, -1), Vector3(0, 1, 0)) +const ROT_X_POS90_FLIP_Y := Basis(Vector3(1, 0, 0), Vector3(0, 0, 1), Vector3(0, 1, 0)) +const ROT_Y_NEG90_FLIP_Y := Basis(Vector3(0, 0, 1), Vector3(0, -1, 0), Vector3(-1, 0, 0)) +const ROT_XZ := Basis(Vector3(0.0, 1.0, 0.0), Vector3(0.0, 0.0, 1.0), Vector3(1.0, 0.0, 0.0)) + +## Method to convert physics vertices to Source Engine's coordinate system. +## It is different for each model, so we need to check the model's version and flags +## to determine how to transform the vertices. +## +## The formula derived through experiments and tested on models up to version 49. +## I hope it won't break :'( +func transform_vertex(vec: Vector3, bone_idx: int) -> Vector3: + vec = vec * TO_INCHES + + var bone: MDLReader.MDLBone = mdl.bones[bone_idx] if bone_idx < mdl.bones.size() else null + var is_autogenerated := mdl.header.flags & MDLReader.MDLFlag.AUTOGENERATED_HITBOX != 0 + + if mdl.is_static_prop or (mdl.header.version <= 48 and solid_count > 1): + return ROT_X_NEG90 * vec + elif mdl.header.version > 48 and is_autogenerated and solid_count > 1: + return ROT_X_POS90_FLIP_Y * vec + elif solid_count == 1 && mdl.header.version == 48: + return bone.pos_to_bone * (ROT_XZ * vec); + else: + return bone.pos_to_bone * (ROT_Y_NEG90_FLIP_Y * vec) \ + if bone != null \ + else ROT_Y_NEG90_FLIP_Y * vec; + + return vec + +func _init(source_file: String, _mdl: MDLReader): + mdl = _mdl; var file = FileAccess.open(source_file, FileAccess.READ); if file == null: return; @@ -158,6 +193,8 @@ func _init(source_file: String): while file.get_position() < vertices_start: var solid_header = ByteReader.read_by_structure(file, PHYSolidHeader) as PHYSolidHeader; + solid_count += 1; + vertices_start = min(solid_header.address + solid_header.vertices_offset, vertices_start); surface_header.solids.append(solid_header); @@ -169,11 +206,8 @@ func _init(source_file: String): vertices_count = max(vertices_count, triangle_data.v1, triangle_data.v2, triangle_data.v3); for j in range(vertices_count + 1): - var vertex = ByteReader._read_data(file, ByteReader.Type.VECTOR3); - var w = ByteReader._read_data(file, ByteReader.Type.FLOAT); - - # NOTE: For some reason all collision solids are converted from inch to m. Converting them back to inch - vertex = Vector3(vertex.x, vertex.z, -vertex.y) / 0.0254; + var vertex := ByteReader._read_data(file, ByteReader.Type.VECTOR3) as Vector3; + var w := ByteReader._read_data(file, ByteReader.Type.FLOAT) as float; surface_header.vertices.append(vertex); @@ -181,4 +215,3 @@ func _init(source_file: String): # NOTE: +4 means that size of header starts after the size byte file.seek((surface_header.address + 4) + surface_header.size); - diff --git a/addons/godotvmf/godotmdl/reader.gd b/addons/godotvmf/godotmdl/reader.gd index 66423da..ff0cb12 100644 --- a/addons/godotvmf/godotmdl/reader.gd +++ b/addons/godotvmf/godotmdl/reader.gd @@ -18,6 +18,7 @@ enum Type { MAT3X4 = 15, EULER_VECTOR = 16, VECTOR4 = 17, + INLINE_STRING = 18, } static func read_array(file: FileAccess, root_structure, offset_field: String, count_field: String, Clazz): @@ -33,6 +34,19 @@ static func read_array(file: FileAccess, root_structure, offset_field: String, c return result; +static func read_inline_string(file: FileAccess, address: int): + var offset = read_signed_int(file); + var result = ""; + + if offset == 0: + result = ""; + else: + var pos = file.get_position(); + result = read_string(file, address + offset); + file.seek(pos); + + return result; + static func read_by_structure(file: FileAccess, Clazz, read_from = -1): var result = Clazz.new(); @@ -43,8 +57,9 @@ static func read_by_structure(file: FileAccess, Clazz, read_from = -1): push_error("ByteReader: Scheme not found in class: " + Clazz.name); return null; + var address = file.get_position(); if "address" in result: - result.address = file.get_position(); + result.address = address; for key in Clazz.scheme.keys(): var type = Clazz.scheme[key]; @@ -59,12 +74,18 @@ static func read_by_structure(file: FileAccess, Clazz, read_from = -1): continue; if elements_count < 0: - result[key] = _read_data(file, type); + if type == Type.INLINE_STRING: + result[key] = read_inline_string(file, address); + else: + result[key] = _read_data(file, type); else: var buffer = ""; for i in range(elements_count): if type != Type.STRING: - result[key].append(_read_data(file, type)); + if type == Type.INLINE_STRING: + result[key] = read_inline_string(file, address); + else: + result[key].append(_read_data(file, type)); else: buffer += _read_data(file, type); @@ -93,67 +114,59 @@ static func _read_data(file: FileAccess, type: Type): Type.STRING: return read_char(file); Type.CHAR: return read_char(file); Type.VECTOR2: return Vector2(file.get_float(), file.get_float()); + Type.VECTOR3: return Vector3(file.get_float(), file.get_float(), file.get_float()); Type.VECTOR4: return Vector4(file.get_float(), file.get_float(), file.get_float(), file.get_float()); + Type.TANGENT: return Plane(file.get_float(), file.get_float(), file.get_float(), file.get_float()); + Type.QUATERNION: return Quaternion(file.get_float(), file.get_float(), file.get_float(), file.get_float()); Type.STRING_NULL_TERMINATED: return read_string(file); - Type.QUATERNION: return read_quaternion(file); - Type.VECTOR3: return read_vector(file); - Type.TANGENT: return read_plane(file); Type.MAT3X4: return read_transform_3d(file); Type.EULER_VECTOR: return read_euler_vector(file); _: return type; -static func read_signed_short(file: FileAccess): +static func read_signed_short(file: FileAccess) -> int: var value = file.get_16(); if value > 32767: value -= 65536; return value; -static func read_signed_int(file: FileAccess): +static func read_signed_int(file: FileAccess) -> int: var value = file.get_32(); if value > 2147483647: value -= 4294967296; return value; ## Matrix 3x4 to Transform3D -static func read_transform_3d(file: FileAccess): - var transform = Transform3D(); - var yup_transform = Transform3D(Vector3(1, 0, 0), Vector3(0, 0, 1), Vector3(0, -1, 0), Vector3(0, 0, 0)); - - var x = Vector3(file.get_float(), file.get_float(), file.get_float()); - var y = Vector3(file.get_float(), file.get_float(), file.get_float()); - var z = Vector3(file.get_float(), file.get_float(), file.get_float()); - var t = Vector3(file.get_float(), file.get_float(), file.get_float()); - - transform.basis = Basis( - Vector3(x.x, x.y, x.z), - Vector3(y.x, y.y, y.z), - Vector3(z.x, z.y, z.z) +static func read_transform_3d(file: FileAccess) -> Transform3D: + var transform := Transform3D(); + + var source := [ + [file.get_float(), file.get_float(), file.get_float(), file.get_float()], + [file.get_float(), file.get_float(), file.get_float(), file.get_float()], + [file.get_float(), file.get_float(), file.get_float(), file.get_float()], + ]; + + var basis := Basis( + Vector3(source[0][0], source[0][1], source[0][2]), + Vector3(source[1][0], source[1][1], source[1][2]), + Vector3(source[2][0], source[2][1], source[2][2]) ); - transform.origin = Vector3(t.x, t.y, t.z); + var origin := Vector3(source[0][3], source[1][3], source[2][3]); - return (transform * yup_transform).orthonormalized(); + return Transform3D(basis, origin); -## Converts euler vector from z-up to y-up -static func read_euler_vector(file: FileAccess): +static func read_euler_vector(file: FileAccess) -> Vector3: return Vector3(file.get_float(), file.get_float(), file.get_float()); -## Converts plane from z-up to y-up -static func read_plane(file: FileAccess): - var plane = Plane(file.get_float(), file.get_float(), file.get_float(), file.get_float()); - return Plane(plane.normal.x, plane.normal.z, plane.normal.y, plane.d); - -static func read_vector(file: FileAccess): - var vector = Vector3(file.get_float(), file.get_float(), file.get_float()); +static func read_plane(file: FileAccess) -> Plane: + return Plane(file.get_float(), file.get_float(), file.get_float(), file.get_float()); - return Vector3(vector.x, vector.z, -vector.y); - -static func read_quaternion(file: FileAccess): - var q = Quaternion(file.get_float(), file.get_float(), file.get_float(), file.get_float()); +static func read_vector(file: FileAccess) -> Vector3: + return Vector3(file.get_float(), file.get_float(), file.get_float()); - # Convert quaternion from z-up to y-up - return Quaternion(q.x, q.z, -q.y, q.w); +static func read_quaternion(file: FileAccess) -> Quaternion: + return Quaternion(file.get_float(), file.get_float(), file.get_float(), file.get_float()); ## Reads string of the file till null character static func read_string(file: FileAccess, offset: int = -1): diff --git a/addons/godotvmf/godotmdl/vvd_reader.gd b/addons/godotvmf/godotmdl/vvd_reader.gd index 26338b6..e82b6b4 100644 --- a/addons/godotvmf/godotmdl/vvd_reader.gd +++ b/addons/godotvmf/godotmdl/vvd_reader.gd @@ -1,7 +1,5 @@ class_name VVDReader extends RefCounted -const MAX_NUM_LODS = 8; - class VVDHeader: static var scheme: get: return { @@ -9,7 +7,7 @@ class VVDHeader: version = ByteReader.Type.INT, checksum = ByteReader.Type.INT, num_lods = ByteReader.Type.INT, - num_lods_vertexes = [ByteReader.Type.INT, MAX_NUM_LODS], + num_lods_vertexes = [ByteReader.Type.INT, 8], num_fixups = ByteReader.Type.INT, fixup_table_offset = ByteReader.Type.INT, vertex_data_offset = ByteReader.Type.INT,