aboutsummaryrefslogtreecommitdiff
path: root/game/src
diff options
context:
space:
mode:
author hop311 <hop3114@gmail.com>2024-05-05 19:36:21 +0200
committer hop311 <hop3114@gmail.com>2024-05-07 23:06:38 +0200
commit2ac43ba7df3b2c3dc40c6b87c2bc57c4b02ffa42 (patch)
treef251adec091f704d889360a0eb32bd4ad018192d /game/src
parentc29cc0dabe3e3c7d03280e74d2d10fc3cc479c7f (diff)
Add GDScript XAC and XSM loaders
Diffstat (limited to 'game/src')
-rw-r--r--game/src/Game/Model/FileAccessUtils.gd88
-rw-r--r--game/src/Game/Model/UnitModel.gd160
-rw-r--r--game/src/Game/Model/XACLoader.gd1127
-rw-r--r--game/src/Game/Model/XSMLoader.gd363
-rw-r--r--game/src/Game/Model/flag.gdshader26
-rw-r--r--game/src/Game/Model/flag_mat.tres7
-rw-r--r--game/src/Game/Model/unit_colours.gdshader31
-rw-r--r--game/src/Game/Model/unit_colours_mat.tres9
8 files changed, 1811 insertions, 0 deletions
diff --git a/game/src/Game/Model/FileAccessUtils.gd b/game/src/Game/Model/FileAccessUtils.gd
new file mode 100644
index 0000000..f5e02e1
--- /dev/null
+++ b/game/src/Game/Model/FileAccessUtils.gd
@@ -0,0 +1,88 @@
+class_name FileAccessUtils
+
+static func read_vec2(file : FileAccess) -> Vector2:
+ return Vector2(file.get_float(), file.get_float())
+
+static func read_vec3(file : FileAccess) -> Vector3:
+ return Vector3(file.get_float(), file.get_float(), file.get_float())
+
+static func read_pos(file : FileAccess) -> Vector3:
+ var pos : Vector3 = read_vec3(file)
+ pos.x = -pos.x
+ return pos
+
+static func read_vec4(file : FileAccess) -> Vector4:
+ return Vector4(file.get_float(), file.get_float(), file.get_float(), file.get_float())
+
+# Because paradox may or may not be consistent with the xsm spec depending on if its Tuesday or not
+static func read_quat(file : FileAccess, int16 : bool = false) -> Quaternion:
+ if int16:
+ return Quaternion(read_f16(file), -read_f16(file), -read_f16(file), read_f16(file))
+ else:
+ return Quaternion(file.get_float(), -file.get_float(), -file.get_float(), file.get_float())
+
+static func read_f16(file : FileAccess) -> float:
+ # 32767 or 0x7FFF is the max magnitude of a signed int16
+ return float(read_int16(file)) / 32767.0
+
+static func replace_chars(string : String) -> String:
+ return string.replace(":", "_").replace("\\", "_").replace("/", "_")
+
+static func read_xac_str(file : FileAccess) -> String:
+ var length : int = file.get_32()
+ var buffer : PackedByteArray = file.get_buffer(length)
+ return buffer.get_string_from_ascii()
+
+static func read_int32(file : FileAccess) -> int:
+ var bytes : int = file.get_32()
+ var negative : bool = bytes >> 31
+ var val : int = bytes & 0x7FFFFFFF
+ if negative:
+ val = -((val ^ 0x7FFFFFFF) + 1)
+ return val
+
+static func read_int16(file : FileAccess) -> int:
+ var bytes : int = file.get_16()
+ var negative : bool = bytes >> 15
+ var val : int = bytes & 0x7FFF
+ if negative:
+ val = -((val ^ 0x7FFF) + 1)
+ return val
+
+static func read_Color32(file : FileAccess) -> Color:
+ return Color8(file.get_8(), file.get_8(), file.get_8(), file.get_8())
+
+static func read_Color128(file : FileAccess) -> Color:
+ return Color(
+ file.get_32() / 0xFFFFFFFF,
+ file.get_32() / 0xFFFFFFFF,
+ file.get_32() / 0xFFFFFFFF,
+ file.get_32() / 0xFFFFFFFF
+ )
+
+static func read_mat4x4(file : FileAccess) -> xac_mat4x4:
+ return xac_mat4x4.new(read_vec4(file), read_vec4(file), read_vec4(file), read_vec4(file))
+
+# This datatype is only ever used to hold a transform for nodes (bones)
+class xac_mat4x4:
+ var col1 : Vector4
+ var col2 : Vector4
+ var col3 : Vector4
+ var col4 : Vector4
+
+ func _init(col1 : Vector4, col2 : Vector4, col3 : Vector4, col4 : Vector4) -> void:
+ self.col1 = col1
+ self.col2 = col2
+ self.col3 = col3
+ self.col4 = col4
+
+ func debugPrint() -> void:
+ print("\t\tMat4x4 col1:", col1, " col2:", col2, " col3:", col3, " col4:", col4)
+
+ func getAsTransform() -> Transform3D: # godot wants 3x4 matrix
+ return Transform3D(
+ Vector3(col1.x, col1.y, col1.z),
+ Vector3(col2.x, col2.y, col2.z),
+ Vector3(col3.x, col3.y, col3.z),
+ Vector3(col4.x, col4.y, col4.z)
+ )
diff --git a/game/src/Game/Model/UnitModel.gd b/game/src/Game/Model/UnitModel.gd
new file mode 100644
index 0000000..0a4fe2f
--- /dev/null
+++ b/game/src/Game/Model/UnitModel.gd
@@ -0,0 +1,160 @@
+class_name UnitModel
+extends Node3D
+
+var skeleton : Skeleton3D = null
+var anim_player : AnimationPlayer = null
+var anim_lib : AnimationLibrary = null
+var sub_units : Array[UnitModel]
+var meshes : Array[MeshInstance3D]
+
+# COLOUR VARIABLES
+@export_group("Colors")
+@export var primary_colour : Color:
+ set(col_in):
+ primary_colour = col_in
+ change_colour_prop(&"colour_primary", primary_colour)
+ for unit : UnitModel in sub_units:
+ unit.primary_colour = col_in
+
+@export var secondary_colour: Color:
+ set(col_in):
+ secondary_colour = col_in
+ change_colour_prop(&"colour_secondary", secondary_colour)
+ for unit : UnitModel in sub_units:
+ unit.secondary_colour = col_in
+
+@export var tertiary_colour : Color:
+ set(col_in):
+ tertiary_colour = col_in
+ change_colour_prop(&"colour_tertiary", tertiary_colour)
+ for unit : UnitModel in sub_units:
+ unit.tertiary_colour = col_in
+
+# ANIMATION VARIABLES
+@export_group("Animation")
+@export var idle_anim : Animation:
+ set(anim_in):
+ load_animation("idle", anim_in)
+ idle_anim = anim_in
+
+@export var move_anim : Animation:
+ set(anim_in):
+ load_animation("move", anim_in)
+ move_anim = anim_in
+
+@export var attack_anim : Animation:
+ set(anim_in):
+ load_animation("attack", anim_in)
+ attack_anim = anim_in
+
+enum Anim { NONE, IDLE, MOVE, ATTACK }
+
+const ANIMATION_LIBRARY : StringName = &"default_lib"
+const ANIMATION_IDLE : String = ANIMATION_LIBRARY + "/idle"
+const ANIMATION_MOVE : String = ANIMATION_LIBRARY + "/move"
+const ANIMATION_ATTACK : String = ANIMATION_LIBRARY + "/attack"
+
+@export var current_anim : Anim:
+ set(anim_in):
+ for unit : UnitModel in sub_units:
+ unit.current_anim = anim_in
+
+ if anim_player:
+ match anim_in:
+ Anim.IDLE:
+ if idle_anim:
+ anim_player.set_current_animation(ANIMATION_IDLE)
+ current_anim = Anim.IDLE
+ return
+ Anim.MOVE:
+ if move_anim:
+ anim_player.set_current_animation(ANIMATION_MOVE)
+ current_anim = Anim.MOVE
+ return
+ Anim.ATTACK:
+ if attack_anim:
+ anim_player.set_current_animation(ANIMATION_ATTACK)
+ current_anim = Anim.ATTACK
+ return
+ _: #None
+ pass
+
+ anim_player.stop()
+
+ current_anim = Anim.NONE
+
+# TEXTURE SCROLL SPEEDS (TANKS TRACKS AND SMOKE)
+@export_subgroup("Texture_Scroll")
+@export var scroll_speed_idle : float = 0.0
+@export var scroll_speed_move : float = 0.0
+@export var scroll_speed_attack : float = 0.0
+
+func unit_init() -> void:
+ for child : Node in get_children():
+ if child is MeshInstance3D:
+ meshes.append(child)
+ elif child is Skeleton3D:
+ skeleton = child
+
+func add_anim_player() -> void:
+ anim_player = AnimationPlayer.new()
+ anim_player.name = "anim_player"
+
+ anim_lib = AnimationLibrary.new()
+ anim_lib.resource_name = ANIMATION_LIBRARY
+ anim_player.add_animation_library(ANIMATION_LIBRARY, anim_lib)
+
+ add_child(anim_player)
+
+func has_bone(bone_name : String) -> bool:
+ return skeleton and skeleton.find_bone(bone_name) > -1
+
+func attach_model(bone_name : String, model : Node3D) -> Error:
+ if not model:
+ push_error("Cannot attach null model to bone \"", bone_name, "\" of UnitModel ", get_name())
+ return FAILED
+
+ if not skeleton:
+ push_error("Cannot attach model \"", model.get_name(), "\" to bone \"", bone_name, "\" of UnitModel ", get_name(), " - has no skeleton!")
+ return FAILED
+
+ var bone_idx : int = skeleton.find_bone(bone_name)
+ if bone_idx < 0 or bone_idx >= skeleton.get_bone_count():
+ push_error("Invalid bone \"", bone_name, "\" (index ", bone_idx, ") for attachment \"", model.get_name(), "\" to UnitModel \"", get_name(), "\"")
+ return FAILED
+
+ var bone_attachment := BoneAttachment3D.new()
+ bone_attachment.name = bone_name
+ bone_attachment.bone_idx = bone_idx
+ bone_attachment.add_child(model)
+ skeleton.add_child(bone_attachment)
+
+ if model is UnitModel:
+ sub_units.push_back(model)
+ model.current_anim = current_anim
+ model.primary_colour = primary_colour
+ model.secondary_colour = secondary_colour
+ model.tertiary_colour = tertiary_colour
+
+ return OK
+
+func _set_tex_scroll(speed : float) -> void:
+ for mesh : MeshInstance3D in meshes:
+ if mesh.get_active_material(0) is ShaderMaterial:
+ mesh.set_instance_shader_parameter(&"scroll", Vector2(0, speed))
+
+func set_flag_index(index : int) -> void:
+ for mesh : MeshInstance3D in meshes:
+ mesh.set_instance_shader_parameter(&"flag_index", index)
+
+func change_colour_prop(prop_name : StringName, prop_val : Color) -> void:
+ for mesh : MeshInstance3D in meshes:
+ if mesh.get_active_material(0) is ShaderMaterial:
+ mesh.set_instance_shader_parameter(prop_name, prop_val)
+
+func load_animation(prop_name : String, animIn : Animation) -> void:
+ if not animIn:
+ return
+ if not anim_player:
+ add_anim_player()
+ anim_lib.add_animation(prop_name,animIn)
diff --git a/game/src/Game/Model/XACLoader.gd b/game/src/Game/Model/XACLoader.gd
new file mode 100644
index 0000000..330384b
--- /dev/null
+++ b/game/src/Game/Model/XACLoader.gd
@@ -0,0 +1,1127 @@
+class_name XACLoader
+
+static var shader : ShaderMaterial = preload("res://src/Game/Model/unit_colours_mat.tres")
+const MAX_UNIT_TEXTURES : int = 32 # max number of textures supported by the shader
+static var added_textures_spec : PackedStringArray
+static var added_textures_diffuse : PackedStringArray
+
+static var flag_shader : ShaderMaterial = preload("res://src/Game/Model/flag_mat.tres")
+
+static func setup_flag_shader() -> void:
+ flag_shader.set_shader_parameter(&"flag_dims", GameSingleton.get_flag_dims())
+ flag_shader.set_shader_parameter(&"texture_flag_sheet_diffuse", GameSingleton.get_flag_sheet_texture())
+
+# Keys: source_file (String)
+# Values: loaded model (UnitModel or Node3D) or LOAD_FAILED_MARKER (StringName)
+static var xac_cache : Dictionary
+
+const LOAD_FAILED_MARKER : StringName = &"XAC LOAD FAILED"
+
+static func get_xac_model(source_file : String, is_unit : bool) -> Node3D:
+ var cached : Variant = xac_cache.get(source_file)
+ if not cached:
+ cached = _load_xac_model(source_file, is_unit)
+ if cached:
+ xac_cache[source_file] = cached
+ else:
+ xac_cache[source_file] = LOAD_FAILED_MARKER
+ push_error("Failed to get XAC model \"", source_file, "\" (current load failed)")
+ return null
+
+ if not cached is Node3D:
+ push_error("Failed to get XAC model \"", source_file, "\" (previous load failed)")
+ return null
+
+ var node : Node3D = cached.duplicate()
+ if node is UnitModel:
+ node.unit_init()
+ return node
+
+static func _load_xac_model(source_file : String, is_unit : bool) -> Node3D:
+ var source_path : String = GameSingleton.lookup_file_path(source_file)
+ var file : FileAccess = FileAccess.open(source_path, FileAccess.READ)
+ if file == null:
+ push_error("Failed to load XAC ", source_file, " from looked up path ", source_path)
+ return null
+
+ var metaDataChunk : MetadataChunk
+ var nodeHierarchyChunk : NodeHierarchyChunk
+ var materialTotalsChunk : MaterialTotalsChunk
+ var materialDefinitionChunks : Array[MaterialDefinitionChunk] = []
+ var mesh_chunks : Array[MeshChunk] = []
+ var skinningChunks : Array[SkinningChunk] = []
+ var chunkType6s : Array[ChunkType6] = []
+ var nodeChunks : Array[NodeChunk] = []
+ var chunkType4s : Array[ChunkTypeUnknown] = []
+ var chunkUnknowns : Array[ChunkTypeUnknown] = []
+
+ readHeader(file)
+
+ while file.get_position() < file.get_length():
+ var type : int = FileAccessUtils.read_int32(file)
+ var length : int = FileAccessUtils.read_int32(file)
+ var version : int = FileAccessUtils.read_int32(file)
+ match type:
+ 0x7:
+ metaDataChunk = readMetaDataChunk(file)
+ 0xB:
+ nodeHierarchyChunk = readNodeHierarchyChunk(file)
+ 0xD:
+ materialTotalsChunk = readMaterialTotalsChunk(file)
+ 0x3:
+ # Ver=1 Appears on old version of format
+ var chunk : MaterialDefinitionChunk = readMaterialDefinitionChunk(file, version==1)
+ if chunk.has_specular():
+ is_unit = true
+ materialDefinitionChunks.push_back(chunk)
+ 0x1:
+ mesh_chunks.push_back(readMeshChunk(file))
+ 0x2:
+ skinningChunks.push_back(readSkinningChunk(file,mesh_chunks, version==2))
+ 0x6:
+ chunkType6s.push_back(readChunkType6(file))
+ 0x0: # Appears on old version of format
+ nodeChunks.push_back(readNodeChunk(file))
+ 0xA: # Appears on old version of format
+ chunkUnknowns.push_back(readChunkTypeUnknown(file, length))
+ 0x4: # Appears on old version of format
+ chunkType4s.push_back(readChunkTypeUnknown(file, length))
+ 0x8:
+ push_warning("XAC model ", source_file, " contains junk data chunk 0x8 (skipping)")
+ break
+ _:
+ push_error(">> INVALID XAC CHUNK TYPE %s in model %s" % [type, source_file])
+ break
+
+ #BUILD THE GODOT MATERIALS
+ var materials : Array[MaterialDefinition] = make_materials(materialDefinitionChunks)
+
+ #BUILD THE MESH
+ var node : Node3D = null
+
+ if is_unit:
+ node = UnitModel.new()
+ else:
+ node = Node3D.new()
+
+ node.name = metaDataChunk.origFileName.replace("\\", "/").split("/", false)[-1].get_slice(".", 0)
+
+ var skeleton : Skeleton3D = null
+
+ # build the skeleton hierarchy
+ if nodeHierarchyChunk:
+ skeleton = build_armature(nodeHierarchyChunk)
+ elif not nodeChunks.is_empty():
+ skeleton = build_armature_chunk0(nodeChunks)
+
+ if skeleton:
+ node.add_child(skeleton)
+ else:
+ push_warning("MODEL HAS NO SKELETON: ", source_file)
+
+ var st : SurfaceTool = SurfaceTool.new()
+
+ for chunk : MeshChunk in mesh_chunks:
+ var mesh_chunk_name : String
+ if nodeHierarchyChunk:
+ mesh_chunk_name = nodeHierarchyChunk.nodes[chunk.nodeId].name
+ elif not nodeChunks.is_empty():
+ mesh_chunk_name = nodeChunks[chunk.nodeId].name
+
+ const INVALID_MESHES : PackedStringArray = ["polySurface95"]
+ if mesh_chunk_name in INVALID_MESHES:
+ push_warning("Skipping unused mesh \"", mesh_chunk_name, "\" in model \"", node.name, "\"")
+ continue
+
+ var mesh : ArrayMesh = null
+ var verts : PackedVector3Array
+ var normals : PackedVector3Array
+ var tangents : Array[Vector4]
+ var uvs : Array[PackedVector2Array] = []
+ var influenceRangeInd : PackedInt64Array
+
+ # vert attributes could be in any order, so search for them
+ for vertAttrib : VerticesAttribute in chunk.VerticesAttributes:
+ match vertAttrib.type:
+ 0: # position
+ verts = vertAttrib.data
+ 1: # normals vec3
+ normals = vertAttrib.data
+ 2: # tangents vec4
+ tangents = vertAttrib.data
+ 3: # uv coords vec2
+ uvs.push_back(vertAttrib.data) #can have multiple sets of uv data
+ 5: # influence range, uint32
+ influenceRangeInd = vertAttrib.data
+ _: # type 4 32bit colours and type 6 128bit colours aren't used
+ pass
+
+ if chunk.bIsCollisionMesh or mesh_chunk_name == "pCube1":
+ var ar3d : Area3D = Area3D.new()
+ node.add_child(ar3d)
+ ar3d.owner = node
+ for submesh : SubMesh in chunk.SubMeshes:
+ var shape : ConvexPolygonShape3D = ConvexPolygonShape3D.new()
+ shape.points = verts
+
+ var col : CollisionShape3D = CollisionShape3D.new()
+ col.shape = shape
+
+ ar3d.add_child(col)
+ col.owner = node
+ continue
+
+ #TODO will this produce correct results?
+ var applyVertexWeights : bool = true
+ var skinning_chunk_ind : int = 0
+ for skin : SkinningChunk in skinningChunks:
+ if skin.nodeId == chunk.nodeId:
+ break
+ skinning_chunk_ind += 1
+ if skinning_chunk_ind >= len(skinningChunks):
+ skinning_chunk_ind = 1
+ applyVertexWeights = false
+
+ var meshInstance : MeshInstance3D = MeshInstance3D.new()
+ node.add_child(meshInstance)
+ meshInstance.owner = node
+
+ if mesh_chunk_name:
+ meshInstance.name = mesh_chunk_name
+
+ if skeleton:
+ meshInstance.skeleton = meshInstance.get_path_to(skeleton)
+
+ if not verts.is_empty():
+ var vert_total : int = 0
+ var surfaceIndex : int = 0
+
+ for submesh : SubMesh in chunk.SubMeshes:
+ st.begin(Mesh.PRIMITIVE_TRIANGLES)
+
+ for i : int in submesh.relativeIndices.size():
+ var rel_index : int = vert_total + submesh.relativeIndices[i]
+
+ if not normals.is_empty():
+ st.set_normal(normals[rel_index])
+
+ if not tangents.is_empty():
+ st.set_tangent(Plane(
+ -tangents[rel_index].x,
+ tangents[rel_index].y,
+ tangents[rel_index].z,
+ tangents[rel_index].w
+ ))
+
+ if not uvs.is_empty():
+ st.set_uv(uvs[0][rel_index])
+
+ if not influenceRangeInd.is_empty() and not skinningChunks.is_empty() and applyVertexWeights:
+ #TODO: Which skinning Chunk?
+ # likely look at the skinning chunk's nodeId, see if it matches our mesh's id
+ var vert_inf_range_ind : int = influenceRangeInd[rel_index]
+ var skin_chunk : SkinningChunk = skinningChunks[skinning_chunk_ind]
+ var influenceRange : InfluenceRange = skin_chunk.influenceRange[vert_inf_range_ind]
+ var boneWeights : Array[InfluenceData] = skinningChunks[skinning_chunk_ind].influenceData.slice(
+ influenceRange.firstInfluenceIndex,
+ influenceRange.firstInfluenceIndex + influenceRange.numInfluences
+ )
+ if len(boneWeights) > 4:
+ push_error("num BONE WEIGHTS WAS > 4, GODOT DOESNT LIKE THIS")
+ # TODO: Less hacky fix?
+ boneWeights = boneWeights.slice(0,4)
+
+ var godotBoneIds : PackedInt32Array = PackedInt32Array()
+ var godotBoneWeights : PackedFloat32Array = PackedFloat32Array()
+ godotBoneIds.resize(4)
+ godotBoneIds.fill(0)
+ godotBoneWeights.resize(4)
+ godotBoneWeights.fill(0)
+
+ var index : int = 0
+ for bone : InfluenceData in boneWeights:
+ godotBoneIds.set(index, bone.boneId)
+ godotBoneWeights.set(index, bone.fWeight)
+ index += 1
+
+ if skeleton:
+ st.set_bones(godotBoneIds)
+ st.set_weights(godotBoneWeights)
+
+ st.add_vertex(verts[rel_index])
+
+ vert_total += submesh.numVertices
+
+ mesh = st.commit(mesh) # add a new surface to the mesh
+ meshInstance.mesh = mesh
+
+ st.clear()
+
+ mesh.surface_set_material(surfaceIndex, materials[submesh.materialId].mat)
+ surfaceIndex += 1
+
+ if materials[submesh.materialId].spec_index != -1:
+ meshInstance.set_instance_shader_parameter(&"tex_index_specular", materials[submesh.materialId].spec_index)
+
+ if materials[submesh.materialId].diffuse_index != -1:
+ meshInstance.set_instance_shader_parameter(&"tex_index_diffuse", materials[submesh.materialId].diffuse_index)
+
+ return node
+
+# Information needed to set up a material
+# Leave the indices -1 if not using the unit shader
+class MaterialDefinition:
+ var spec_index : int = -1
+ var diffuse_index : int = -1
+ var normal_index : int = -1
+ var mat : Material
+
+ func _init(mat : Material, diffuse_ind : int = -1, spec_ind : int = -1, normal_ind : int = -1) -> void:
+ self.mat = mat
+ self.diffuse_index = diffuse_ind
+ self.spec_index = spec_ind
+ self.normal_index = normal_ind
+
+static func make_materials(materialDefinitionChunks : Array[MaterialDefinitionChunk]) -> Array[MaterialDefinition]:
+ const TEXTURES_PATH : String = "gfx/anims/%s.dds"
+
+ var materials : Array[MaterialDefinition] = []
+
+ for matdef : MaterialDefinitionChunk in materialDefinitionChunks:
+ var diffuse_name : String
+ var specular_name : String
+ var normal_name : String
+
+ # Find important textures
+ for layer : Layer in matdef.Layers:
+ if layer.texture in ["nospec", "unionjacksquare", "test256texture"]:
+ continue
+
+ match layer.mapType:
+ 2: # diffuse
+ if not diffuse_name:
+ diffuse_name = layer.texture
+ else:
+ push_error("Multiple diffuse layers in material: ", diffuse_name, " and ", layer.texture)
+
+ 3: # specular
+ if not specular_name:
+ specular_name = layer.texture
+ else:
+ push_error("Multiple specular layers in material: ", specular_name, " and ", layer.texture)
+
+ 4: # currently unused
+ pass
+
+ 5: # normal
+ if not normal_name:
+ normal_name = layer.texture
+ else:
+ push_error("Multiple normal layers in material: ", normal_name, " and ", layer.texture)
+
+ _:
+ push_error("Unknown layer type: ", layer.mapType)
+ pass
+
+ # Unit colour mask
+ if diffuse_name and specular_name:
+ if normal_name:
+ push_error("Normal texture present in unit colours material: ", normal_name)
+
+ var textures_index_spec : int = added_textures_spec.find(specular_name)
+ if textures_index_spec < 0:
+ var unit_colours_mask_texture : ImageTexture = AssetManager.get_texture(TEXTURES_PATH % specular_name)
+ if unit_colours_mask_texture:
+ added_textures_spec.push_back(specular_name)
+
+ # Should we still attempt to add the texture to the shader?
+ if len(added_textures_spec) >= MAX_UNIT_TEXTURES:
+ push_error("Colour masks have exceeded max number of textures supported by unit shader!")
+
+ var colour_masks : Array = shader.get_shader_parameter(&"texture_nation_colors_mask")
+ colour_masks.push_back(unit_colours_mask_texture)
+ textures_index_spec = len(colour_masks) - 1
+ shader.set_shader_parameter(&"texture_nation_colors_mask", colour_masks)
+ else:
+ push_error("Failed to load specular texture: ", specular_name)
+
+ var textures_index_diffuse : int = added_textures_diffuse.find(diffuse_name)
+ if textures_index_diffuse < 0:
+ var diffuse_texture : ImageTexture = AssetManager.get_texture(TEXTURES_PATH % diffuse_name)
+ if diffuse_texture:
+ added_textures_diffuse.push_back(diffuse_name)
+
+ # Should we still attempt to add the texture to the shader?
+ if len(added_textures_diffuse) >= MAX_UNIT_TEXTURES:
+ push_error("Albedos have exceeded max number of textures supported by unit shader!")
+
+ var albedoes : Array = shader.get_shader_parameter(&"texture_albedo")
+ albedoes.push_back(diffuse_texture)
+ textures_index_diffuse = len(albedoes) - 1
+ shader.set_shader_parameter(&"texture_albedo", albedoes)
+ else:
+ push_error("Failed to load diffuse texture: ", diffuse_name)
+
+ materials.push_back(MaterialDefinition.new(shader, textures_index_diffuse, textures_index_spec))
+
+ # Flag (diffuse is unionjacksquare which is ignored)
+ elif normal_name and not diffuse_name:
+ if specular_name:
+ push_error("Specular texture present in flag material: ", specular_name)
+
+ var flag_normal_texture : ImageTexture = AssetManager.get_texture(TEXTURES_PATH % normal_name)
+ if flag_normal_texture:
+ flag_shader.set_shader_parameter(&"texture_normal", flag_normal_texture)
+ else:
+ push_error("Failed to load normal texture: ", normal_name)
+
+ materials.push_back(MaterialDefinition.new(flag_shader))
+
+ # Standard material
+ else:
+ if specular_name:
+ push_error("Specular texture present in standard material: ", specular_name)
+
+ var mat : StandardMaterial3D = StandardMaterial3D.new()
+ mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_DEPTH_PRE_PASS
+
+ if diffuse_name:
+ var diffuse_texture : ImageTexture = AssetManager.get_texture(TEXTURES_PATH % diffuse_name)
+ if diffuse_texture:
+ mat.set_texture(BaseMaterial3D.TEXTURE_ALBEDO, diffuse_texture)
+ else:
+ push_error("Failed to load diffuse texture: ", diffuse_name)
+
+ if normal_name:
+ var normal_texture : ImageTexture = AssetManager.get_texture(TEXTURES_PATH % normal_name)
+ if normal_texture:
+ mat.normal_enabled = true
+ mat.set_texture(BaseMaterial3D.TEXTURE_NORMAL, normal_texture)
+ else:
+ push_error("Failed to load normal texture: ", normal_name)
+
+ #TODO: Verify that this is correct thing to do to make sure
+ #that places where models are double sided are correct
+ mat.cull_mode = BaseMaterial3D.CULL_DISABLED
+
+ materials.push_back(MaterialDefinition.new(mat))
+
+ return materials
+
+static func build_armature(hierarchy_chunk : NodeHierarchyChunk) -> Skeleton3D:
+ var skeleton : Skeleton3D = Skeleton3D.new()
+ skeleton.name = "skeleton"
+ var cur_id : int = 0
+ for node : NodeData in hierarchy_chunk.nodes:
+ # godot doesn't like the ':' in bone names unlike paradox
+ skeleton.add_bone(FileAccessUtils.replace_chars(node.name))
+ skeleton.set_bone_parent(cur_id, node.parentNodeId)
+
+ #For now assume rest and current position are the same
+ skeleton.set_bone_rest(cur_id, Transform3D(Basis(node.rotation).scaled(node.scale), node.position))
+
+ skeleton.set_bone_pose_position(cur_id, node.position)
+ skeleton.set_bone_pose_rotation(cur_id, node.rotation)
+ skeleton.set_bone_pose_scale(cur_id, node.scale)
+
+ cur_id += 1
+ # conveniently both godot and xac use a parent node id of -1 to represent no parent
+ # TODO: What is the point of xac having both a transform and separate vec3s for rotation, scale, pos, etc.?
+ # for now, will assume the separate components are the truth
+ # it might be that one is a current position and the other is a rest position?
+ return skeleton
+
+static func build_armature_chunk0(nodeChunks : Array[NodeChunk]) -> Skeleton3D:
+ var skeleton : Skeleton3D = Skeleton3D.new()
+ skeleton.name = "skeleton"
+ var cur_id : int = 0
+ for node : NodeChunk in nodeChunks:
+ # godot doesn't like the ':' in bone names unlike paradox
+ skeleton.add_bone(FileAccessUtils.replace_chars(node.name))
+ skeleton.set_bone_parent(cur_id, node.parentBone)
+
+ # For now assume rest and current position are the same
+ skeleton.set_bone_rest(cur_id, Transform3D(Basis(node.rotation).scaled(node.scale), node.position))
+
+ skeleton.set_bone_pose_position(cur_id, node.position)
+ skeleton.set_bone_pose_rotation(cur_id, node.rotation)
+ skeleton.set_bone_pose_scale(cur_id, node.scale)
+
+ cur_id += 1
+ return skeleton
+
+static func readHeader(file : FileAccess) -> void:
+ var magic_bytes : PackedByteArray = [file.get_8(), file.get_8(), file.get_8(), file.get_8()]
+ var magic : String = magic_bytes.get_string_from_ascii()
+ var version : String = "%d.%d" % [file.get_8(), file.get_8()]
+ var bBigEndian : bool = file.get_8()
+ var multiplyOrder : int = file.get_8()
+ #print(magic, ", version: ", version, ", bigEndian: ", bBigEndian, " multiplyOrder: ", multiplyOrder)
+
+static func readMetaDataChunk(file : FileAccess) -> MetadataChunk:
+ return MetadataChunk.new(
+ file.get_32(), FileAccessUtils.read_int32(file), file.get_8(), file.get_8(),
+ Vector2i(file.get_8(), file.get_8()), file.get_float(),
+ FileAccessUtils.read_xac_str(file), FileAccessUtils.read_xac_str(file),
+ FileAccessUtils.read_xac_str(file), FileAccessUtils.read_xac_str(file)
+ )
+
+class MetadataChunk:
+ var repositionMask : int # uint32
+ var repositioningNode : int # int32
+ var exporterMajorVersion : int # byte
+ var exporterMinorVersion : int # byte
+ var unused : Vector2i # 2x byte
+ var retargetRootOffset : float
+ var sourceApp : String
+ var origFileName : String
+ var exportDate : String
+ var actorName : String
+
+ func _init(
+ repMask : int,
+ repNode : int,
+ exMajV : int,
+ exMinV : int,
+ un : Vector2i,
+ retRootOff : float,
+ sourceApp : String,
+ origFile : String,
+ date : String,
+ actorName : String
+ ) -> void:
+ self.repositionMask = repMask
+ self.repositioningNode = repNode
+ self.exporterMajorVersion = exMajV
+ self.exporterMinorVersion = exMinV
+ self.unused = un
+ self.retargetRootOffset = retRootOff
+ self.sourceApp = sourceApp
+ self.origFileName = origFile
+ self.exportDate = date
+ self.actorName = actorName
+
+ func debugPrint() -> void:
+ print("Ver: %d.%d, exported: %s, fileName: %s, sourceApp: %s" %
+ [exporterMajorVersion, exporterMinorVersion, exportDate, origFileName, sourceApp])
+ print("Actor: %s, retargetRootOffset: %d, repositionMask: %d, repositionNode: %d" %
+ [actorName, retargetRootOffset, repositionMask, repositioningNode])
+
+# HIERARCHY
+
+static func readNodeData(file : FileAccess) -> NodeData:
+ return NodeData.new(
+ FileAccessUtils.read_quat(file), FileAccessUtils.read_quat(file),
+ FileAccessUtils.read_pos(file), FileAccessUtils.read_vec3(file),
+ FileAccessUtils.read_vec3(file), # 3x unused floats
+ FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file),
+ FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file),
+ FileAccessUtils.read_mat4x4(file), file.get_float(), FileAccessUtils.read_xac_str(file)
+ )
+
+class NodeData:
+ var rotation : Quaternion
+ var scaleRotation : Quaternion
+ var position : Vector3
+ var scale : Vector3
+ var unused : Vector3 # 3x unused floats
+ var unknown : int # int32
+ var unknown2 : int # int32
+ var parentNodeId : int # int32
+ var numChildNodes : int # int32
+ var bIncludeInBoundsCalc : bool # int32
+ var transform : FileAccessUtils.xac_mat4x4
+ var fImportanceFactor : float
+ var name : String
+
+ func _init(
+ rot : Quaternion,
+ scaleRot : Quaternion,
+ pos : Vector3,
+ scale : Vector3,
+ unused : Vector3,
+ unknown : int,
+ unknown2 : int,
+ parentNodeId : int,
+ numChildNodes : int,
+ incInBoundsCalc : bool,
+ transform : FileAccessUtils.xac_mat4x4,
+ fImportanceFactor : float,
+ name : String
+ ) -> void:
+ self.rotation = rot
+ self.scaleRotation = scaleRot
+ self.position = pos
+ self.scale = scale
+ self.unused = unused
+ self.unknown = unknown2
+ self.unknown2 = unknown2
+ self.parentNodeId = parentNodeId
+ self.numChildNodes = numChildNodes
+ self.bIncludeInBoundsCalc = incInBoundsCalc
+ self.transform = transform
+ self.fImportanceFactor = fImportanceFactor
+ self.name = name
+
+ func debugPrint() -> void:
+ print("\tparentNodeId: %d,\t numChildNodes: %d,\t Node Name: %s" % [parentNodeId, numChildNodes, name])
+ print("\tunused %s,%s,%s, -1: %s, -1: %s" % [unused[0], unused[1], unused[2], unknown, unknown2])
+
+static func readNodeHierarchyChunk(file : FileAccess) -> NodeHierarchyChunk:
+ var numNodes : int = FileAccessUtils.read_int32(file)
+ var numRootNodes : int = FileAccessUtils.read_int32(file)
+ var nodes : Array[NodeData] = []
+ for i : int in numNodes:
+ nodes.push_back(readNodeData(file))
+ return NodeHierarchyChunk.new(numNodes, numRootNodes, nodes)
+
+class NodeHierarchyChunk:
+ var numNodes : int # int32
+ var numRootNodes : int # int32
+ var nodes : Array[NodeData]
+
+ func _init(numNodes : int, numRootNodes : int, nodes : Array[NodeData]) -> void:
+ self.numNodes = numNodes
+ self.numRootNodes = numRootNodes
+ self.nodes = nodes
+
+ func debugPrint() -> void:
+ print("numNodes: %d, numRootNodes: %d" % [numNodes, numRootNodes])
+ for node : NodeData in nodes:
+ node.debugPrint()
+
+# MATERIAL TOTALS
+
+static func readMaterialTotalsChunk(file : FileAccess) -> MaterialTotalsChunk:
+ return MaterialTotalsChunk.new(FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file))
+
+class MaterialTotalsChunk:
+ var numTotalMaterials : int # int32
+ var numStandMaterials : int # int32
+ var numFxMaterials : int # int32
+
+ func _init(numTotalMaterials : int, numStandMaterials : int, numFxMaterials : int) -> void:
+ self.numTotalMaterials = numTotalMaterials
+ self.numStandMaterials = numStandMaterials
+ self.numFxMaterials = numFxMaterials
+
+ func debugPrint() -> void:
+ print("totalMaterials: %d, standardMaterials: %d, fxMaterials: %d" %
+ [numTotalMaterials, numStandMaterials, numFxMaterials])
+
+# MATERIAL DEFINITION
+
+static func readLayer(file : FileAccess, isV1 : bool) -> Layer:
+ var unknown : Vector3i
+ if isV1:
+ unknown = Vector3i(
+ FileAccessUtils.read_int32(file),
+ FileAccessUtils.read_int32(file),
+ FileAccessUtils.read_int32(file)
+ )
+ var layer : Layer = Layer.new(
+ file.get_float(), file.get_float(), file.get_float(), file.get_float(),
+ file.get_float(), file.get_float(), FileAccessUtils.read_int16(file),
+ file.get_8(), file.get_8(), FileAccessUtils.read_xac_str(file), unknown
+ )
+ return layer
+
+class Layer:
+ var amount : float
+ var uOffset : float
+ var vOffset : float
+ var uTiling : float
+ var vTiling : float
+ var rotInRad : float
+ var matId : int # int16
+ var mapType : int # byte
+ var unused : int # byte
+ var texture : String
+ var unknown : Vector3i # Unknown 3 integers present in v1 of the chunk
+
+ func _init(
+ amount : float,
+ uOffset : float,
+ vOffset : float,
+ uTiling : float,
+ vTiling : float,
+ rotInRad : float,
+ matId : int,
+ mapType : int,
+ unused : int,
+ texture : String,
+ unknown : Vector3i
+ ) -> void:
+ self.amount = amount
+ self.uOffset = uOffset
+ self.vOffset = vOffset
+ self.uTiling = uTiling
+ self.vTiling = vTiling
+ self.rotInRad = rotInRad
+ self.matId = matId
+ self.mapType = mapType
+ self.unused = unused
+ self.texture = texture
+ self.unknown = unknown
+
+ func debugPrint() -> void:
+ print("\tLayer MatId:%d,\t UVOffset:%d,%d,\t UVTiling %d,%d mapType: %d,\t Texture Name: %s" %
+ [matId, uOffset, vOffset, uTiling, vTiling, mapType, texture])
+ print("\t amount:%s,\t rot:%s,\t unused:%d" % [amount, rotInRad, unused])
+
+ func is_specular() -> bool:
+ return mapType == 3
+
+# TODO: Might want to change this from vec4d to colours where appropriate
+static func readMaterialDefinitionChunk(file : FileAccess, isV1 : bool) -> MaterialDefinitionChunk:
+ var chunk : MaterialDefinitionChunk = MaterialDefinitionChunk.new(
+ FileAccessUtils.read_vec4(file), FileAccessUtils.read_vec4(file),
+ FileAccessUtils.read_vec4(file), FileAccessUtils.read_vec4(file),
+ file.get_float(), file.get_float(), file.get_float(), file.get_float(),
+ file.get_8(), file.get_8(), file.get_8(), file.get_8(),
+ FileAccessUtils.read_xac_str(file)
+ )
+ var layers : Array[Layer] = []
+ for i : int in chunk.numLayers:
+ layers.push_back(readLayer(file, isV1))
+ chunk.setLayers(layers)
+ return chunk
+
+class MaterialDefinitionChunk:
+ var ambientColor : Vector4
+ var diffuseColor : Vector4
+ var specularColor : Vector4
+ var emissiveColor : Vector4
+ var shine : float
+ var shineStrength : float
+ var opacity : float
+ var ior : float
+ var bDoubleSided : bool # byte
+ var bWireframe : bool # byte
+ var unused : int # 1 byte
+ var numLayers : int # byte
+ var name : String
+ var Layers : Array[Layer]
+
+ func _init(
+ ambientColor : Vector4,
+ diffuseColor : Vector4,
+ specularColor : Vector4,
+ emissiveColor : Vector4,
+ shine : float,
+ shineStrength : float,
+ opacity : float,
+ ior : float,
+ bDoubleSided : bool,
+ bWireframe : bool,
+ unused : int,
+ numLayers : int,
+ name : String
+ ) -> void:
+ self.ambientColor = ambientColor
+ self.diffuseColor = diffuseColor
+ self.specularColor = specularColor
+ self.emissiveColor = emissiveColor
+ self.shine = shine
+ self.shineStrength = shineStrength
+ self.opacity = opacity
+ self.ior = ior
+ self.bDoubleSided = bDoubleSided
+ self.bWireframe = bWireframe
+ self.unused = unused
+ self.numLayers = numLayers
+ self.name = name
+
+ func setLayers(layers : Array[Layer]) -> void:
+ self.Layers = layers
+
+ # Specular textures are used for country-specific unit colours,
+ # which need a UnitModel to be set through
+ func has_specular() -> bool:
+ for layer : Layer in Layers:
+ if layer.is_specular():
+ return true
+ return false
+
+ func debugPrint() -> void:
+ print("Material Name: %s, num layers: %d, doubleSided %s" % [name, numLayers, bDoubleSided])
+ print("\tshine:%s\tshineStrength:%s\t,opacity:%s,\tior:%s,\tunused:%s" % [shine, shineStrength, opacity, ior, unused])
+ for layer : Layer in Layers:
+ layer.debugPrint()
+
+# MESH
+
+static func readVerticesAttribute(file : FileAccess, numVerts : int) -> VerticesAttribute:
+ var vertAttrib : VerticesAttribute = VerticesAttribute.new(
+ FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file),
+ file.get_8(), file.get_8(), Vector2i(file.get_8(), file.get_8()))
+ var data : Variant
+ match vertAttrib.type:
+ 0: # position
+ data = PackedVector3Array()
+ for i : int in numVerts:
+ data.push_back(FileAccessUtils.read_pos(file))
+ 1: # normals
+ data = PackedVector3Array()
+ for i : int in numVerts:
+ data.push_back(FileAccessUtils.read_pos(file))
+ 2: # tangents
+ data = [] as Array[Vector4]
+ for i : int in numVerts:
+ var tangent : Vector4 = FileAccessUtils.read_vec4(file)
+ #tangent.w *= -1
+ data.push_back(tangent)
+ 3: # uvs
+ data = PackedVector2Array()
+ for i : int in numVerts:
+ data.push_back(FileAccessUtils.read_vec2(file))
+ 4: # 32bit colors
+ data = PackedColorArray()
+ for i : int in numVerts:
+ data.push_back(FileAccessUtils.read_Color32(file))
+ 5: # influence range indices
+ data = PackedInt64Array()
+ for i : int in numVerts:
+ data.push_back(file.get_32())
+ 6: # 128bit colors
+ data = PackedColorArray()
+ for i : int in numVerts:
+ data.push_back(FileAccessUtils.read_Color128(file))
+ _:
+ push_error("INVALID XAC VERTATTRIBUTE TYPE %d" % vertAttrib.type)
+ vertAttrib.setData(data)
+ return vertAttrib
+
+# mesh has one of these for each vertex property (position, normals, etc)
+class VerticesAttribute:
+ var type : int # int32
+ var attribSize : int # int32
+ var bKeepOriginals : bool # byte
+ var bIsScaleFactor : bool # byte
+ var pad : Vector2i # 2x byte
+ var data : Variant # numVerts * attribSize
+
+ func _init(
+ type : int,
+ attribSize : int,
+ bKeepOriginals : bool,
+ bIsScaleFactor : bool,
+ pad : Vector2i
+ ) -> void:
+ self.type = type
+ self.attribSize = attribSize
+ self.bKeepOriginals = bKeepOriginals
+ self.bIsScaleFactor = bIsScaleFactor
+ self.pad = pad
+
+ func setData(data : Variant) -> void:
+ self.data = data
+
+ func debugPrint() -> void:
+ var typeStr : String
+ match type:
+ 0:
+ typeStr = "Positions"
+ 1:
+ typeStr = "Normals"
+ 2:
+ typeStr = "Tangents"
+ 3:
+ typeStr = "UV Coords"
+ 4:
+ typeStr = "32bit Colors"
+ 5:
+ typeStr = "Influence Range Indices (u32)"
+ 6:
+ typeStr = "128bit Colors"
+ _:
+ typeStr = "invalid type %d" % type
+ print("\tattribSize:%d (bytes),\t keepOriginals:%s,\t isScaleFactor:%s,\t VertAttrib: type:%s" %
+ [attribSize, bKeepOriginals, bIsScaleFactor, typeStr])
+ print("\tpad: %s" % pad)
+
+static func readSubMesh(file : FileAccess) -> SubMesh:
+ var subMesh : SubMesh = SubMesh.new(
+ FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file),
+ FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file)
+ )
+ var relIndices : PackedInt32Array = PackedInt32Array()
+ var boneIds : PackedInt32Array = PackedInt32Array()
+ for i : int in subMesh.numIndices:
+ relIndices.push_back(FileAccessUtils.read_int32(file))
+ for i : int in subMesh.numBones:
+ boneIds.push_back(FileAccessUtils.read_int32(file))
+ subMesh.setBoneIds(boneIds)
+ subMesh.setRelIndices(relIndices)
+ return subMesh
+
+class SubMesh:
+ var numIndices : int # int32
+ var numVertices : int # int32
+ var materialId : int # int32
+ var numBones : int # int32
+ var relativeIndices : PackedInt32Array # int32 [numIndices]
+ var boneIds : PackedInt32Array # int32 [numBones], unused
+
+ func _init(numIndices : int, numVertices : int, materialId : int, numBones : int) -> void:
+ self.numIndices = numIndices
+ self.numVertices = numVertices
+ self.materialId = materialId
+ self.numBones = numBones
+
+ func setRelIndices(relativeIndices : PackedInt32Array) -> void:
+ self.relativeIndices = relativeIndices
+
+ func setBoneIds(boneIds : PackedInt32Array) -> void:
+ self.boneIds = boneIds
+
+ func debugPrint() -> void:
+ print("\tSubMesh:\t numIndices:%d,\t numVerts:%d,\t matId:%d,\t numBones:%d" %
+ [numIndices, numVertices, materialId, numBones])
+
+static func readMeshChunk(file : FileAccess) -> MeshChunk:
+ var mesh : MeshChunk = MeshChunk.new(
+ FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file),
+ FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file),
+ FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file), file.get_8(),
+ Vector3i(file.get_8(), file.get_8(), file.get_8())
+ )
+ var vertAttribs : Array[VerticesAttribute] = []
+ var submeshes : Array[SubMesh] = []
+ for i : int in mesh.numAttribLayers:
+ vertAttribs.push_back(readVerticesAttribute(file,mesh.numVertices))
+ for i : int in mesh.numSubMeshes:
+ submeshes.push_back(readSubMesh(file))
+ mesh.setVerticesAttributes(vertAttribs)
+ mesh.setSubMeshes(submeshes)
+ return mesh
+
+class MeshChunk:
+ var nodeId : int # int32
+ var numInfluenceRanges : int # int32
+ var numVertices : int # int32
+ var numIndices : int # int32
+ var numSubMeshes : int # int32
+ var numAttribLayers : int # int32
+ var bIsCollisionMesh : bool # byte
+ var pad : Vector3i # 3x byte
+ var VerticesAttributes : Array[VerticesAttribute]
+ var SubMeshes : Array[SubMesh]
+
+ func _init(
+ nodeId : int,
+ numInfluenceRanges : int,
+ numVertices : int,
+ numIndices : int,
+ numSubMeshes : int,
+ numAttribLayers : int,
+ bIsCollisionMesh : bool,
+ pad : Vector3i
+ ) -> void:
+ self.nodeId = nodeId
+ self.numInfluenceRanges = numInfluenceRanges
+ self.numVertices = numVertices
+ self.numIndices = numIndices
+ self.numSubMeshes = numSubMeshes
+ self.numAttribLayers = numAttribLayers
+ self.bIsCollisionMesh = bIsCollisionMesh
+ self.pad = pad
+
+ func setVerticesAttributes(VerticesAttributes : Array[VerticesAttribute]) -> void:
+ self.VerticesAttributes = VerticesAttributes
+
+ func setSubMeshes(SubMeshes : Array[SubMesh]) -> void:
+ self.SubMeshes = SubMeshes
+
+ func debugPrint() -> void:
+ print("Mesh: nodeId:%d, numVerts:%d, numSubMeshes:%d, numVertAttribs:%d, isCollisionMesh:%s" %
+ [nodeId, numVertices, numSubMeshes, numAttribLayers, bIsCollisionMesh])
+ for vertAttrib in VerticesAttributes:
+ vertAttrib.debugPrint()
+ for subMesh : SubMesh in SubMeshes:
+ subMesh.debugPrint()
+
+# SKINNING
+static func readInfluenceData(file : FileAccess) -> InfluenceData:
+ return InfluenceData.new(file.get_float(), FileAccessUtils.read_int16(file), Vector2i(file.get_8(), file.get_8()))
+
+class InfluenceData:
+ var fWeight : float # (0..1)
+ var boneId : int # int16
+ var pad : Vector2i # 2x byte
+
+ func _init(fWeight : float, boneId : int, pad : Vector2i) -> void:
+ self.fWeight = fWeight
+ self.boneId = boneId
+ self.pad = pad
+
+ func debugPrint() -> void:
+ print("\tInfluenceData:\t boneId: %d,\t Weight: %s" % [boneId, fWeight])
+
+# For some reason influenceRange isn't being loaded, needs investigation
+# Weird data on the flag
+
+static func readInfluenceRange(file : FileAccess) -> InfluenceRange:
+ return InfluenceRange.new(FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file))
+
+class InfluenceRange:
+ var firstInfluenceIndex : int # int32
+ var numInfluences : int # int32
+
+ func _init(firstInfluenceIndex : int, numInfluences : int) -> void:
+ self.firstInfluenceIndex = firstInfluenceIndex
+ self.numInfluences = numInfluences
+
+ func debugPrint() -> void:
+ print("\tInfluenceRange:\t firstIndex: %d,\t numInfluences: %d" % [firstInfluenceIndex, numInfluences])
+
+static func readSkinningChunk(file : FileAccess, meshChunks : Array[MeshChunk], isV2 : bool) -> SkinningChunk:
+ var skinning : SkinningChunk = null
+ skinning = SkinningChunk.new(
+ FileAccessUtils.read_int32(file), -1 if isV2 else FileAccessUtils.read_int32(file),
+ FileAccessUtils.read_int32(file), file.get_8(), Vector3i(file.get_8(), file.get_8(), file.get_8())
+ )
+ var influenceData : Array[InfluenceData] = []
+ var influenceRange : Array[InfluenceRange] = []
+ for i : int in skinning.numInfluences:
+ influenceData.push_back(readInfluenceData(file))
+ # search the list of mesh chunks for the one which matches IsCollisionMesh and NodeId?
+ # documentation is a little unclear about this (lists the mesh accessing by node, which isn't possible)
+ for chunk : MeshChunk in meshChunks:
+ if chunk.nodeId == skinning.nodeId and chunk.bIsCollisionMesh == skinning.bIsForCollisionMesh:
+ for i : int in chunk.numInfluenceRanges:
+ influenceRange.push_back(readInfluenceRange(file))
+ break
+ skinning.setInfluenceData(influenceData)
+ skinning.setInfluenceRange(influenceRange)
+ return skinning
+
+class SkinningChunk:
+ var nodeId : int # int32
+ var numLocalBones : int # int32, of bones in the influence data
+ var numInfluences : int # int32
+ var bIsForCollisionMesh : bool # byte boolean
+ var pad : Vector3i # 3x pad
+ var influenceData : Array[InfluenceData]
+ var influenceRange : Array[InfluenceRange]
+
+ func _init(
+ nodeId : int,
+ numLocalBones : int,
+ numInfluences : int,
+ bIsForCollisionMesh : bool,
+ pad : Vector3i
+ ) -> void:
+ self.nodeId = nodeId
+ self.numLocalBones = numLocalBones
+ self.numInfluences = numInfluences
+ self.bIsForCollisionMesh = bIsForCollisionMesh
+ self.pad = pad
+
+ func setInfluenceData(influenceData : Array[InfluenceData]) -> void:
+ self.influenceData = influenceData
+
+ func setInfluenceRange(influenceRange : Array[InfluenceRange]) -> void:
+ self.influenceRange = influenceRange
+
+ func debugPrint() -> void:
+ print("Skinning: nodeId:%d, numInfluencedBones: %d, numInfluences: %d, CollisionMesh?: %d" %
+ [nodeId, numLocalBones, numInfluences, bIsForCollisionMesh])
+ for infDat : InfluenceData in influenceData:
+ infDat.debugPrint()
+ for infRange : InfluenceRange in influenceRange:
+ infRange.debugPrint()
+
+# TODO: What is chunk type6, figure out what the plausible datatypes are
+# Currently, since XACs tend to use int32s and float (32bit) + strings, and no strings
+# are evident in chunk type 6, to load these as arrays of int32
+
+static func readChunkType6(file : FileAccess) -> ChunkType6:
+ var intArr : PackedInt32Array = []
+ var floatArr : PackedFloat32Array = []
+ for i : int in 12:
+ intArr.push_back(FileAccessUtils.read_int32(file))
+ for i : int in 9:
+ floatArr.push_back(file.get_float())
+ return ChunkType6.new(intArr, floatArr, FileAccessUtils.read_int32(file))
+
+class ChunkType6:
+ var unknown : PackedInt32Array
+ var unknown_floats : PackedFloat32Array
+ var maybe_node_id : int
+
+ func _init(unknown : PackedInt32Array, unknown_floats : PackedFloat32Array, maybe_node_id : int) -> void:
+ self.unknown = unknown
+ self.unknown_floats = unknown_floats
+ self.maybe_node_id = maybe_node_id
+
+ func debugPrint() -> void:
+ print("\tUnknown: %s\n\tFloats: %s\n\tPerhaps NodeId? %s" % [self.unknown, self.unknown_floats, self.maybe_node_id])
+
+# Chunk type 0x0
+static func readNodeChunk(file : FileAccess) -> NodeChunk:
+ return NodeChunk.new(
+ FileAccessUtils.read_quat(file), FileAccessUtils.read_quat(file),
+ FileAccessUtils.read_pos(file), FileAccessUtils.read_vec3(file),
+ FileAccessUtils.read_vec3(file), # unused 3 floats
+ FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file), #-1-1
+ # 17 x 32bit unknowns, likely matrix and some other info
+ # last 32bit unknown is likely fImportanceFactor, (float 1f). Rest is likely matrix
+ (func() -> PackedInt32Array:
+ var array : PackedInt32Array = PackedInt32Array()
+ for i : int in 17:
+ array.push_back(FileAccessUtils.read_int32(file))
+ return array).call(),
+ FileAccessUtils.read_xac_str(file)
+ )
+
+class NodeChunk:
+ var rotation : Quaternion
+ var scaleRotation : Quaternion
+ var position : Vector3
+ var scale : Vector3
+ var unused : Vector3 # 3x unused floats
+ var unknown : int # int32
+ var parentBone : int # int32
+ var unknown2 : PackedInt32Array # 17x int32 sized numbers
+ var name : String
+
+ func _init(
+ rot : Quaternion,
+ scaleRot : Quaternion,
+ pos : Vector3,
+ scale : Vector3,
+ unused : Vector3,
+ unknown : int,
+ parentBone : int,
+ unknown2 : PackedInt32Array,
+ name : String
+ ) -> void:
+ self.rotation = rot
+ self.scaleRotation = scaleRot
+ self.position = pos
+ self.scale = scale
+ self.unused = unused
+ self.unknown = unknown
+ self.parentBone = parentBone
+ self.unknown2 = unknown2
+ self.name = name
+
+ func debugPrint() -> void:
+ print("\tNode Name: %s, parentBone %s" % [name, parentBone])
+ print("\tunused (%s) unknown: %s" % [unused, unknown])
+ print("\tunknown after -1-1: %s" % unknown2)
+
+static func readChunkTypeUnknown(file : FileAccess, length : int) -> ChunkTypeUnknown:
+ return ChunkTypeUnknown.new(file.get_buffer(length))
+
+class ChunkTypeUnknown:
+ var unknown : PackedByteArray
+
+ func _init(unknown : PackedByteArray) -> void:
+ self.unknown = unknown
+
+ func debugPrint() -> void:
+ print("\tUnknown: %s" % self.unknown)
+
+# TODO: MorphTarget, Deformation, Transformation, MorphTargetsChunk
diff --git a/game/src/Game/Model/XSMLoader.gd b/game/src/Game/Model/XSMLoader.gd
new file mode 100644
index 0000000..332bbec
--- /dev/null
+++ b/game/src/Game/Model/XSMLoader.gd
@@ -0,0 +1,363 @@
+class_name XSMLoader
+
+# Keys: source_file (String)
+# Values: loaded animation (Animation) or LOAD_FAILED_MARKER (StringName)
+static var xsm_cache : Dictionary
+
+const LOAD_FAILED_MARKER : StringName = &"XSM LOAD FAILED"
+
+static func get_xsm_animation(source_file : String) -> Animation:
+ var cached : Variant = xsm_cache.get(source_file)
+ if not cached:
+ cached = _load_xsm_animation(source_file)
+ if cached:
+ xsm_cache[source_file] = cached
+ else:
+ xsm_cache[source_file] = LOAD_FAILED_MARKER
+ push_error("Failed to get XSM model \"", source_file, "\" (current load failed)")
+ return null
+
+ if not cached is Animation:
+ push_error("Failed to get XSM model \"", source_file, "\" (previous load failed)")
+ return null
+
+ return cached
+
+const SKELETON_PATH : String = "./skeleton:%s"
+
+static func _load_xsm_animation(source_file : String) -> Animation:
+ var source_path : String = GameSingleton.lookup_file_path(source_file)
+ var file : FileAccess = FileAccess.open(source_path, FileAccess.READ)
+ if file == null:
+ push_error("Failed to load XSM ", source_file, " from looked up path ", source_path)
+ return null
+
+ readHeader(file)
+
+ var metadataChunk : MetadataChunk = null
+ var boneAnimationChunks : Array[BoneAnimationChunk] = []
+
+ while file.get_position() < file.get_length():
+ var type : int = FileAccessUtils.read_int32(file)
+ var length : int = FileAccessUtils.read_int32(file)
+ var version : int = FileAccessUtils.read_int32(file)
+
+ match type:
+ 0xC9: #Metadata
+ metadataChunk = readMetadataChunk(file)
+ 0xCA: #Bone Animation
+ boneAnimationChunks.push_back(readBoneAnimationChunk(file, metadataChunk.use_quat_16()))
+ _:
+ push_error(">> INVALID XSM CHUNK TYPE %X" % type)
+ break
+
+ var animLength : float = 0.0
+ var anim : Animation = Animation.new()
+ for anim_Chunk : BoneAnimationChunk in boneAnimationChunks:
+ for submotion : SkeletalSubMotion in anim_Chunk.SkeletalSubMotions:
+ # NOTE: godot uses ':' to specify properties, so we replace such characters with '_'
+ var skeleton_path : String = SKELETON_PATH % FileAccessUtils.replace_chars(submotion.nodeName)
+
+ if submotion.numPosKeys > 0:
+ var id : int = anim.add_track(Animation.TYPE_POSITION_3D)
+ anim.track_set_path(id, skeleton_path)
+ for key : PosKey in submotion.PosKeys:
+ anim.position_track_insert_key(id, key.fTime, key.pos)
+ if key.fTime > animLength:
+ animLength = key.fTime
+ else: # EXPERIMENTAL: see if setting posePos fixes idle3
+ var id : int = anim.add_track(Animation.TYPE_POSITION_3D)
+ anim.track_set_path(id, skeleton_path)
+ anim.position_track_insert_key(id, 0, submotion.posePos)
+
+ if submotion.numRotKeys > 0:
+ var id : int = anim.add_track(Animation.TYPE_ROTATION_3D)
+ anim.track_set_path(id, skeleton_path)
+ for key : RotKey in submotion.RotKeys:
+ anim.rotation_track_insert_key(id, key.fTime, key.rot)
+ if key.fTime > animLength:
+ animLength = key.fTime
+ else: # EXPERIMENTAL: see if setting posePos fixes idle3
+ var id : int = anim.add_track(Animation.TYPE_ROTATION_3D)
+ anim.track_set_path(id, skeleton_path)
+ anim.rotation_track_insert_key(id, 0, submotion.poseRot)
+
+ if submotion.numScaleKeys > 0:
+ var id : int = anim.add_track(Animation.TYPE_SCALE_3D)
+ anim.track_set_path(id, skeleton_path)
+ for key : ScaleKey in submotion.ScaleKeys:
+ anim.scale_track_insert_key(id, key.fTime, key.scale)
+ if key.fTime > animLength:
+ animLength = key.fTime
+
+ # TODO: submotion.numScaleRotKeys
+
+ anim.length = animLength
+ anim.loop_mode = Animation.LOOP_LINEAR
+
+ xsm_cache[source_file] = anim
+ return anim
+
+static func readHeader(file : FileAccess) -> void:
+ var magic_bytes : PackedByteArray = [file.get_8(), file.get_8(), file.get_8(), file.get_8()]
+ var magic : String = magic_bytes.get_string_from_ascii()
+ var version : String = "%d.%d" % [file.get_8(), file.get_8()]
+ var bBigEndian : bool = file.get_8()
+ var pad : int = file.get_8()
+ #print(magic, ", version: ", version, ", bigEndian: ", bBigEndian, " pad: ", pad)
+
+# NOTE: the "pad" variable is actually very important!
+# It seems to have something to do with whether paradox uses int16 or int32
+# for quaternions (it's "pad" or version number, can't tell)
+
+static func readMetadataChunk(file : FileAccess) -> MetadataChunk:
+ return MetadataChunk.new(
+ file.get_float(), file.get_float(), FileAccessUtils.read_int32(file),
+ file.get_8(), file.get_8(), file.get_16(),
+ FileAccessUtils.read_xac_str(file), FileAccessUtils.read_xac_str(file), FileAccessUtils.read_xac_str(file), FileAccessUtils.read_xac_str(file)
+ )
+
+class MetadataChunk:
+ var unused : float
+ var fMaxAcceptableError : float
+ var fps : int # int32
+ var exporterMajorVersion : int # byte
+ var exporterMinorVersion : int # byte
+ var pad : int # 2x byte
+ var sourceApp : String
+ var origFileName : String
+ var exportDate : String
+ var motionName : String
+
+ func _init(
+ unused : float,
+ fMaxAcceptableError : float,
+ fps : int,
+ exporterMajorVersion : int,
+ exporterMinorVersion : int,
+ pad : int,
+ sourceApp : String,
+ origFileName : String,
+ exportDate : String,
+ motionName : String
+ ) -> void:
+ self.unused = unused
+ self.fMaxAcceptableError = fMaxAcceptableError
+ self.fps = fps
+ self.exporterMajorVersion = exporterMajorVersion
+ self.exporterMinorVersion = exporterMinorVersion
+ self.pad = pad
+ self.sourceApp = sourceApp
+ self.origFileName = origFileName
+ self.exportDate = exportDate
+ self.motionName = motionName
+
+ func debugPrint() -> void:
+ print("FileName: %s, sourceApp: %s, exportDate: %s, ExporterV:%d.%d" %
+ [origFileName, sourceApp, exportDate, exporterMajorVersion, exporterMinorVersion])
+ print("MotionName: %s, fps: %d, MaxError: %s, Use 16-bit int Quaternions?:%s" %
+ [motionName, fps, fMaxAcceptableError, use_quat_16()])
+
+ func use_quat_16() -> bool:
+ return pad == 0x0
+
+static func readPosKey(file : FileAccess) -> PosKey:
+ return PosKey.new(FileAccessUtils.read_pos(file), file.get_float())
+
+class PosKey:
+ var pos : Vector3
+ var fTime : float
+
+ func _init(pos : Vector3, fTime : float) -> void:
+ self.pos = pos
+ self.fTime = fTime
+
+ func debugPrint() -> void:
+ print("\t\tPos:%s, time:%s" % [pos, fTime])
+
+static func readRotKey(file : FileAccess, use_quat16 : bool) -> RotKey:
+ return RotKey.new(FileAccessUtils.read_quat(file, use_quat16), file.get_float())
+
+class RotKey:
+ var rot : Quaternion
+ var fTime : float
+
+ func _init(rot : Quaternion, fTime : float) -> void:
+ self.rot = rot
+ self.fTime = fTime
+
+ func debugPrint() -> void:
+ print("\t\tRot:%s, time:%s" % [rot, fTime])
+
+static func readScaleKey(file : FileAccess) -> ScaleKey:
+ return ScaleKey.new(FileAccessUtils.read_vec3(file), file.get_float())
+
+class ScaleKey:
+ var scale : Vector3
+ var fTime : float
+
+ func _init(scale : Vector3, fTime : float) -> void:
+ self.scale = scale
+ self.fTime = fTime
+
+ func debugPrint() -> void:
+ print("\t\tScale:%s, time:%s" % [scale, fTime])
+
+static func readScaleRotKey(file : FileAccess, use_quat16 : bool) -> ScaleRotKey:
+ return ScaleRotKey.new(FileAccessUtils.read_quat(file, use_quat16), file.get_float())
+
+class ScaleRotKey:
+ var rot : Quaternion
+ var fTime : float
+
+ func _init(rot : Quaternion, fTime : float) -> void:
+ self.rot = rot
+ self.fTime = fTime
+
+ func debugPrint() -> void:
+ print("\t\tScaleRot:%s, time:%s" % [rot, fTime])
+
+static func readSkeletalSubMotion(file : FileAccess, use_quat16 : bool) -> SkeletalSubMotion:
+ var a : Quaternion = FileAccessUtils.read_quat(file, use_quat16)
+ var b : Quaternion = FileAccessUtils.read_quat(file, use_quat16)
+ var c : Quaternion = FileAccessUtils.read_quat(file, use_quat16)
+ var d : Quaternion = FileAccessUtils.read_quat(file, use_quat16)
+
+ var e : Vector3 = FileAccessUtils.read_pos(file)
+ var f : Vector3 = FileAccessUtils.read_vec3(file)
+ var g : Vector3 = FileAccessUtils.read_pos(file)
+ var h : Vector3 = FileAccessUtils.read_vec3(file)
+
+ var p : int = FileAccessUtils.read_int32(file)
+ var j : int = FileAccessUtils.read_int32(file)
+ var k : int = FileAccessUtils.read_int32(file)
+ var l : int = FileAccessUtils.read_int32(file)
+
+ var m : float = file.get_float()
+ var n : String = FileAccessUtils.read_xac_str(file)
+
+ var submotion : SkeletalSubMotion = SkeletalSubMotion.new(
+ a, b, c, d, # quats
+ e, f, g, h, # vec3
+ p, j, k, l, # ints
+ m, n
+ )
+ var poskeys : Array[PosKey] = []
+ var rotkeys : Array[RotKey] = []
+ var scalekeys : Array[ScaleKey] = []
+ var scalerotkeys : Array[ScaleRotKey] = []
+ #FIXME: Did paradox store the number of pos keys as a float instead of int?
+
+ for i : int in submotion.numPosKeys:
+ poskeys.push_back(readPosKey(file))
+ for i : int in submotion.numRotKeys:
+ rotkeys.push_back(readRotKey(file, use_quat16))
+ for i : int in submotion.numScaleKeys:
+ scalekeys.push_back(readScaleKey(file))
+ for i : int in submotion.numScaleRotKeys:
+ scalerotkeys.push_back(readScaleRotKey(file, use_quat16))
+ submotion.setPosKeys(poskeys)
+ submotion.setRotKeys(rotkeys)
+ submotion.setScaleKeys(scalekeys)
+ submotion.setScaleRotKeys(scalerotkeys)
+ return submotion
+
+class SkeletalSubMotion:
+ var poseRot : Quaternion
+ var bindPoseRot : Quaternion
+ var poseScaleRot : Quaternion
+ var bindPoseScaleRot : Quaternion
+ var posePos : Vector3
+ var poseScale : Vector3
+ var bindPosePos : Vector3
+ var bindPoseScale : Vector3
+ var numPosKeys : int # int32
+ var numRotKeys : int # int32
+ var numScaleKeys : int # int32
+ var numScaleRotKeys : int # int32
+ var fMaxError : float
+ var nodeName : String
+
+ var PosKeys : Array[PosKey]
+ var RotKeys : Array[RotKey]
+ var ScaleKeys : Array[ScaleKey]
+ var ScaleRotKeys : Array[ScaleRotKey]
+
+ func _init(
+ poseRot : Quaternion,
+ bindPoseRot : Quaternion,
+ poseScaleRot : Quaternion,
+ bindPoseScaleRot : Quaternion,
+ posePos : Vector3,
+ poseScale : Vector3,
+ bindPosePos : Vector3,
+ bindPoseScale : Vector3,
+ numPosKeys : int,
+ numRotKeys : int,
+ numScaleKeys : int,
+ numScaleRotKeys : int,
+ fMaxError : float,
+ nodeName : String
+ ) -> void:
+ self.poseRot = poseRot
+ self.bindPoseRot = bindPoseRot
+ self.poseScaleRot = poseScaleRot
+ self.bindPoseScaleRot = bindPoseScaleRot
+ self.posePos = posePos
+ self.poseScale = poseScale
+ self.bindPosePos = bindPosePos
+ self.bindPoseScale = bindPoseScale
+ self.numPosKeys = numPosKeys
+ self.numRotKeys = numRotKeys
+ self.numScaleKeys = numScaleKeys
+ self.numScaleRotKeys = numScaleRotKeys
+ self.fMaxError = fMaxError
+ self.nodeName = nodeName
+
+ func setPosKeys(PosKeys : Array[PosKey]) -> void:
+ self.PosKeys = PosKeys
+
+ func setRotKeys(RotKeys : Array[RotKey]) -> void:
+ self.RotKeys = RotKeys
+
+ func setScaleKeys(ScaleKeys : Array[ScaleKey]) -> void:
+ self.ScaleKeys = ScaleKeys
+
+ func setScaleRotKeys(ScaleRotKeys : Array[ScaleRotKey]) -> void:
+ self.ScaleRotKeys = ScaleRotKeys
+
+ func debugPrint() -> void:
+ print("Node: %s, #PosKeys %d, #RotKeys %d, #ScaleKeys %d, #ScaleRotKeys %d" % [nodeName, numPosKeys, numRotKeys, numScaleKeys, numScaleRotKeys])
+ print("\tposeScaleRot %s,\tbindPoseScaleRot %s,\tposeScale %s,\tbindPoseScale %s" % [poseScaleRot, bindPoseScaleRot, poseScale, bindPoseScale])
+ for key : PosKey in PosKeys:
+ key.debugPrint()
+ for key : RotKey in RotKeys:
+ key.debugPrint()
+ for key : ScaleKey in ScaleKeys:
+ key.debugPrint()
+ for key : ScaleRotKey in ScaleRotKeys:
+ key.debugPrint()
+
+static func readBoneAnimationChunk(file : FileAccess, use_quat16 : bool) -> BoneAnimationChunk:
+ var numSubMotions : int = FileAccessUtils.read_int32(file)
+ var animChunk : BoneAnimationChunk = BoneAnimationChunk.new(numSubMotions)
+ var submotions : Array[SkeletalSubMotion] = []
+ for i : int in animChunk.numSubMotions:
+ submotions.push_back(readSkeletalSubMotion(file, use_quat16))
+ animChunk.setSkeletalSubMotions(submotions)
+ return animChunk
+
+class BoneAnimationChunk:
+ var numSubMotions : int # int32
+ var SkeletalSubMotions : Array[SkeletalSubMotion]
+
+ func _init(numSubMotions : int) -> void:
+ self.numSubMotions = numSubMotions
+
+ func setSkeletalSubMotions(SkeletalSubMotions : Array[SkeletalSubMotion]) -> void:
+ self.SkeletalSubMotions = SkeletalSubMotions
+
+ func debugPrint() -> void:
+ print("Number of Submotions: %d" % numSubMotions)
+ for submotion : SkeletalSubMotion in SkeletalSubMotions:
+ submotion.debugPrint()
diff --git a/game/src/Game/Model/flag.gdshader b/game/src/Game/Model/flag.gdshader
new file mode 100644
index 0000000..d338e36
--- /dev/null
+++ b/game/src/Game/Model/flag.gdshader
@@ -0,0 +1,26 @@
+shader_type spatial;
+
+render_mode cull_disabled;
+
+// Both vanilla flags use the same normal texture
+uniform uvec2 flag_dims;
+uniform sampler2D texture_flag_sheet_diffuse : source_color;
+uniform sampler2D texture_normal : hint_normal;
+
+instance uniform uint flag_index;
+
+uniform vec2 scroll_speed = vec2(-0.25,0);
+
+// Scroll the Normal map, but leave the albedo alone
+void fragment() {
+ uvec2 flag_sheet_dims = uvec2(textureSize(texture_flag_sheet_diffuse, 0));
+ uint scaled_index = flag_index * flag_dims.x;
+
+ uvec2 flag_pos = uvec2(scaled_index % flag_sheet_dims.x, scaled_index / flag_sheet_dims.x * flag_dims.y);
+
+ vec2 flag_uv = (vec2(flag_pos) + UV * vec2(flag_dims)) / vec2(flag_sheet_dims);
+
+ ALBEDO = texture(texture_flag_sheet_diffuse, flag_uv).rgb;
+ //ALBEDO = vec3(1, 0, 0);
+ NORMAL_MAP = texture(texture_normal, UV + TIME*scroll_speed).rgb;
+}
diff --git a/game/src/Game/Model/flag_mat.tres b/game/src/Game/Model/flag_mat.tres
new file mode 100644
index 0000000..beefd15
--- /dev/null
+++ b/game/src/Game/Model/flag_mat.tres
@@ -0,0 +1,7 @@
+[gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://5utra6tpdqag"]
+
+[ext_resource type="Shader" path="res://src/Game/Model/flag.gdshader" id="1_oqkkj"]
+
+[resource]
+render_priority = 0
+shader = ExtResource("1_oqkkj")
diff --git a/game/src/Game/Model/unit_colours.gdshader b/game/src/Game/Model/unit_colours.gdshader
new file mode 100644
index 0000000..dd0f5e2
--- /dev/null
+++ b/game/src/Game/Model/unit_colours.gdshader
@@ -0,0 +1,31 @@
+
+shader_type spatial;
+
+render_mode blend_mix, depth_draw_opaque, cull_disabled, diffuse_burley, specular_schlick_ggx;
+
+//hold all the textures for the units that need this shader to mix in their
+//nation colours (mostly generic infantry units)
+uniform sampler2D texture_albedo[32] : source_color, filter_linear_mipmap, repeat_enable;
+uniform sampler2D texture_nation_colors_mask[32] : source_color, filter_linear_mipmap, repeat_enable;
+
+instance uniform vec3 colour_primary : source_color;
+instance uniform vec3 colour_secondary : source_color;
+instance uniform vec3 colour_tertiary : source_color;
+
+//used to access the right textures since different units (with different textures)
+//will use this same shader
+instance uniform uint tex_index_specular;
+instance uniform uint tex_index_diffuse;
+
+void fragment() {
+ vec2 base_uv = UV;
+ vec4 albedo_tex = texture(texture_albedo[tex_index_diffuse], base_uv);
+ vec4 nation_colours_tex = texture(texture_nation_colors_mask[tex_index_specular], base_uv);
+
+ //set colours to either be white (1,1,1) or the nation colour based on the mask
+ vec3 primary_col = mix(vec3(1.0, 1.0, 1.0), colour_primary, nation_colours_tex.g);
+ vec3 secondary_col = mix(vec3(1.0, 1.0, 1.0), colour_secondary, nation_colours_tex.b);
+ vec3 tertiary_col = mix(vec3(1.0, 1.0, 1.0), colour_tertiary, nation_colours_tex.r);
+
+ ALBEDO = albedo_tex.rgb * primary_col * secondary_col * tertiary_col;
+}
diff --git a/game/src/Game/Model/unit_colours_mat.tres b/game/src/Game/Model/unit_colours_mat.tres
new file mode 100644
index 0000000..43ca523
--- /dev/null
+++ b/game/src/Game/Model/unit_colours_mat.tres
@@ -0,0 +1,9 @@
+[gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://bkham060cwpr3"]
+
+[ext_resource type="Shader" path="res://src/Game/Model/unit_colours.gdshader" id="1_axmiw"]
+
+[resource]
+render_priority = 0
+shader = ExtResource("1_axmiw")
+shader_parameter/texture_albedo = []
+shader_parameter/texture_nation_colors_mask = []