diff --git a/README.md b/README.md index e042802..b16f77c 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,14 @@ Blender add-on for importing various Call of Duty assets via the game files. - XModel - Compiled models - Call of Duty 5 World at War - XModel - Compiled models +- Call of Duty Black Ops + - XModel - Compiled models ## Installation & setup First of all, extract all the necessary game specific contents. Make sure to have the exact same folder structure as they have originally. ### Call of Duty & Call of Duty United Offensive -Files can be found inside the .pk3 files. +Files can be found inside the `.pk3` files. ``` . ├── maps/ @@ -34,7 +36,7 @@ Files can be found inside the .pk3 files. ``` ### Call of Duty 2 -Files can be found inside the .iwd files. +Files can be found inside the `.iwd` files. ``` . ├── images/ @@ -47,7 +49,7 @@ Files can be found inside the .iwd files. └── xmodelsurfs/ ``` ### Call of Duty 4 Modern Warfare -Images can be found inside the .iwd files. The rest of the assets can be acquired by installing modtools. +Images can be found inside the `.iwd` files. The rest of the assets can be acquired by installing modtools. ``` . ├── images/ @@ -60,7 +62,20 @@ Images can be found inside the .iwd files. The rest of the assets can be acquire ``` ### Call of Duty 5 World at War -Images can be found inside the .iwd files. The rest of the assets can be acquired by installing modtools. +Images can be found inside the `.iwd` files. The rest of the assets can be acquired by installing modtools. +``` + . + ├── images/ + ├── materials/ + ├── xanim/ + ├── xmodel/ + ├── xmodelalias/ + ├── xmodelparts/ + └── xmodelsurfs/ +``` + +### Call of Duty Black Ops +Images can be found inside the `.iwd` files. The rest of the assets can be acquired by installing modtools. There are still some missing assets though (mostly materials and images). ``` . ├── images/ diff --git a/python/cod_asset_importer/__init__.py b/python/cod_asset_importer/__init__.py index 7260187..5599229 100644 --- a/python/cod_asset_importer/__init__.py +++ b/python/cod_asset_importer/__init__.py @@ -2,7 +2,7 @@ "name": "Call of Duty Asset Importer", "description": "Import Call of Duty assets", "author": "Soma Rádóczi", - "version": (3, 0, 0), + "version": (3, 1, 0), "blender": (3, 0, 0), "location": "File > Import -> CoD Asset Importer", "category": "Import-Export", diff --git a/python/cod_asset_importer/cod_asset_importer.pyi b/python/cod_asset_importer/cod_asset_importer.pyi index cef83b0..be40fcd 100644 --- a/python/cod_asset_importer/cod_asset_importer.pyi +++ b/python/cod_asset_importer/cod_asset_importer.pyi @@ -1,16 +1,26 @@ from typing import List, Dict import importer -class XMODEL_VERSIONS: +class XMODEL_VERSION: V14: int V20: int V25: int + V62: int -class GAME_VERSIONS: - CoD1: int +class GAME_VERSION: + CoD: int CoD2: int CoD4: int CoD5: int + CoDBO1: int + +class TEXTURE_TYPE: + Unused: int + Color: int + Normal: int + Specular: int + Roughness: int + Detail: int class Loader: def __init__(self, importer: importer.Importer) -> None: ... @@ -19,7 +29,7 @@ class Loader: self, asset_path: str, file_path: str, - selected_version: GAME_VERSIONS, + selected_version: GAME_VERSION, angles: List[float], origin: List[float], scale: List[float], @@ -31,22 +41,22 @@ class LoadedIbsp: class LoadedModel: def name(self) -> str: ... - def version(self) -> XMODEL_VERSIONS: ... + def version(self) -> XMODEL_VERSION: ... def angles(self) -> List[float]: ... def origin(self) -> List[float]: ... def scale(self) -> List[float]: ... - def materials(self) -> List[LoadedMaterial]: ... + def materials(self) -> Dict[str, LoadedMaterial]: ... def surfaces(self) -> List[LoadedSurface]: ... def bones(self) -> List[LoadedBone]: ... class LoadedMaterial: def name(self) -> str: ... - def version(self) -> XMODEL_VERSIONS: ... + def version(self) -> XMODEL_VERSION: ... def textures(self) -> List[LoadedTexture]: ... class LoadedTexture: def name(self) -> str: ... - def texture_type(self) -> str: ... + def texture_type(self) -> TEXTURE_TYPE: ... def width(self) -> int: ... def height(self) -> int: ... def data(self) -> List[float]: ... diff --git a/python/cod_asset_importer/importer.py b/python/cod_asset_importer/importer.py index 358589a..0832309 100644 --- a/python/cod_asset_importer/importer.py +++ b/python/cod_asset_importer/importer.py @@ -5,8 +5,9 @@ import math import traceback from .cod_asset_importer import ( - XMODEL_VERSIONS, - GAME_VERSIONS, + XMODEL_VERSION, + GAME_VERSION, + TEXTURE_TYPE, LoadedModel, LoadedIbsp, LoadedMaterial, @@ -16,9 +17,6 @@ from .blender_shadernodes import ( BLENDER_SHADERNODES, ) -from . import ( - base_enum, -) class Importer: @@ -36,9 +34,9 @@ def xmodel(self, loaded_model: LoadedModel) -> None: mesh_objects = [] materials = loaded_model.materials() - for material in materials: + for _, material in materials.items(): append_asset_path = "" - if model_version == XMODEL_VERSIONS.V14: + if model_version == XMODEL_VERSION.V14: append_asset_path = "skins" self.material(loaded_material=material, append_asset_path=append_asset_path) @@ -71,9 +69,8 @@ def xmodel(self, loaded_model: LoadedModel) -> None: vertex_color_layer.data.foreach_set("color", surface.colors()) obj = bpy.data.objects.new(model_name, mesh) - - active_material_name = materials[i].name() - if model_version == XMODEL_VERSIONS.V14: + active_material_name = surface.material() + if model_version == XMODEL_VERSION.V14: active_material_name = os.path.splitext(active_material_name)[0] obj.active_material = bpy.data.materials.get(active_material_name) @@ -236,15 +233,19 @@ def material( has_ext: bool = True, append_asset_path: str = "", ) -> None: - if loaded_material.version() == XMODEL_VERSIONS.V14: + version = loaded_material.version() + if version == XMODEL_VERSION.V14: self._import_material_v14( loaded_material=loaded_material, has_ext=has_ext, append_asset_path=append_asset_path, ) + elif version == XMODEL_VERSION.V62: + self._import_material_v62(loaded_material=loaded_material) else: self._import_material_v20_v25(loaded_material=loaded_material) + # TODO def _import_material_v14( self, loaded_material: LoadedMaterial, has_ext: bool, append_asset_path: str ) -> None: @@ -318,7 +319,7 @@ def _import_material_v14( ) texture_node = nodes.new(BLENDER_SHADERNODES.SHADERNODE_TEXIMAGE) - texture_node.label = "colorMap" + texture_node.label = str(TEXTURE_TYPE.Color) texture_node.location = (-700, 0) texture_node.image = texture_image links.new( @@ -353,6 +354,7 @@ def _import_material_v14( except: return + # TODO def _import_material_v20_v25(self, loaded_material: LoadedMaterial) -> None: material_name = loaded_material.name() @@ -419,11 +421,224 @@ def _import_material_v20_v25(self, loaded_material: LoadedMaterial) -> None: loaded_texture_type = loaded_texture.texture_type() texture_node = nodes.new(BLENDER_SHADERNODES.SHADERNODE_TEXIMAGE) - texture_node.label = loaded_texture_type + texture_node.label = str(loaded_texture_type) + texture_node.location = (-700, -255 * i) + texture_node.image = texture_image + + if loaded_texture_type == TEXTURE_TYPE.Color: + links.new( + texture_node.outputs[BLENDER_SHADERNODES.OUTPUT_TEXIMAGE_COLOR], + principled_bsdf_node.inputs[ + BLENDER_SHADERNODES.INPUT_BSDFPRINCIPLED_BASECOLOR + ], + ) + links.new( + texture_node.outputs[BLENDER_SHADERNODES.OUTPUT_TEXIMAGE_ALPHA], + mix_shader_node.inputs[BLENDER_SHADERNODES.INPUT_MIXSHADER_FAC], + ) + elif loaded_texture_type == TEXTURE_TYPE.Specular: + links.new( + texture_node.outputs[BLENDER_SHADERNODES.OUTPUT_TEXIMAGE_COLOR], + principled_bsdf_node.inputs[ + BLENDER_SHADERNODES.INPUT_BSDFPRINCIPLED_SPECULAR + ], + ) + texture_node.image.colorspace_settings.name = ( + BLENDER_SHADERNODES.TEXIMAGE_COLORSPACE_LINEAR + ) + texture_node.location = (-700, -255) + elif loaded_texture_type == TEXTURE_TYPE.Normal: + texture_node.image.colorspace_settings.name = ( + BLENDER_SHADERNODES.TEXIMAGE_COLORSPACE_LINEAR + ) + texture_node.location = (-1900, -655) + + normal_map_node = nodes.new(BLENDER_SHADERNODES.SHADERNODE_NORMALMAP) + normal_map_node.location = (-450, -650) + normal_map_node.space = BLENDER_SHADERNODES.NORMALMAP_SPACE_TANGENT + normal_map_node.inputs[ + BLENDER_SHADERNODES.INPUT_NORMALMAP_STRENGTH + ].default_value = 0.3 + links.new( + normal_map_node.outputs[ + BLENDER_SHADERNODES.OUTPUT_NORMALMAP_NORMAL + ], + principled_bsdf_node.inputs[ + BLENDER_SHADERNODES.INPUT_BSDFPRINCIPLED_NORMAL + ], + ) + + combine_rgb_node = nodes.new(BLENDER_SHADERNODES.SHADERNODE_COMBINERGB) + combine_rgb_node.location = (-650, -750) + links.new( + combine_rgb_node.outputs[ + BLENDER_SHADERNODES.OUTPUT_COMBINERGB_IMAGE + ], + normal_map_node.inputs[BLENDER_SHADERNODES.INPUT_NORMALMAP_COLOR], + ) + + math_sqrt_node = nodes.new(BLENDER_SHADERNODES.SHADERNODE_MATH) + math_sqrt_node.location = (-850, -850) + math_sqrt_node.operation = BLENDER_SHADERNODES.OPERATION_MATH_SQRT + links.new( + math_sqrt_node.outputs[BLENDER_SHADERNODES.OUTPUT_MATH_VALUE], + combine_rgb_node.inputs[BLENDER_SHADERNODES.INPUT_COMBINERGB_B], + ) + + math_subtract_node = nodes.new(BLENDER_SHADERNODES.SHADERNODE_MATH) + math_subtract_node.location = (-1050, -850) + math_subtract_node.operation = ( + BLENDER_SHADERNODES.OPERATION_MATH_SUBTRACT + ) + links.new( + math_subtract_node.outputs[BLENDER_SHADERNODES.OUTPUT_MATH_VALUE], + math_sqrt_node.inputs[BLENDER_SHADERNODES.INPUT_MATH_SQRT_VALUE], + ) + + math_subtract_node2 = nodes.new(BLENDER_SHADERNODES.SHADERNODE_MATH) + math_subtract_node2.location = (-1250, -950) + math_subtract_node2.operation = ( + BLENDER_SHADERNODES.OPERATION_MATH_SUBTRACT + ) + math_subtract_node2.inputs[ + BLENDER_SHADERNODES.INPUT_MATH_SUBTRACT_VALUE1 + ].default_value = 1.0 + links.new( + math_subtract_node2.outputs[BLENDER_SHADERNODES.OUTPUT_MATH_VALUE], + math_subtract_node.inputs[ + BLENDER_SHADERNODES.INPUT_MATH_SUBTRACT_VALUE1 + ], + ) + + math_power_node = nodes.new(BLENDER_SHADERNODES.SHADERNODE_MATH) + math_power_node.location = (-1250, -750) + math_power_node.operation = BLENDER_SHADERNODES.OPERATION_MATH_POWER + math_power_node.inputs[ + BLENDER_SHADERNODES.INPUT_MATH_POWER_EXPONENT + ].default_value = 2.0 + links.new( + math_power_node.outputs[BLENDER_SHADERNODES.OUTPUT_MATH_VALUE], + math_subtract_node.inputs[ + BLENDER_SHADERNODES.INPUT_MATH_SUBTRACT_VALUE2 + ], + ) + + math_power_node2 = nodes.new(BLENDER_SHADERNODES.SHADERNODE_MATH) + math_power_node2.location = (-1500, -950) + math_power_node2.operation = BLENDER_SHADERNODES.OPERATION_MATH_POWER + math_power_node2.inputs[ + BLENDER_SHADERNODES.INPUT_MATH_POWER_EXPONENT + ].default_value = 2.0 + links.new( + math_power_node2.outputs[BLENDER_SHADERNODES.OUTPUT_MATH_VALUE], + math_subtract_node2.inputs[ + BLENDER_SHADERNODES.INPUT_MATH_SUBTRACT_VALUE2 + ], + ) + links.new( + texture_node.outputs[BLENDER_SHADERNODES.OUTPUT_TEXIMAGE_ALPHA], + math_power_node2.inputs[BLENDER_SHADERNODES.INPUT_MATH_POWER_BASE], + ) + + separate_rgb_node = nodes.new( + BLENDER_SHADERNODES.SHADERNODE_SEPARATERGB + ) + separate_rgb_node.location = (-1500, -450) + links.new( + separate_rgb_node.outputs[BLENDER_SHADERNODES.OUTPUT_SEPARATERGB_G], + combine_rgb_node.inputs[BLENDER_SHADERNODES.INPUT_COMBINERGB_G], + ) + links.new( + separate_rgb_node.outputs[BLENDER_SHADERNODES.OUTPUT_SEPARATERGB_G], + math_power_node.inputs[BLENDER_SHADERNODES.INPUT_MATH_POWER_BASE], + ) + links.new( + texture_node.outputs[BLENDER_SHADERNODES.OUTPUT_TEXIMAGE_COLOR], + separate_rgb_node.inputs[ + BLENDER_SHADERNODES.INPUT_SEPARATERGB_IMAGE + ], + ) + links.new( + texture_node.outputs[BLENDER_SHADERNODES.OUTPUT_TEXIMAGE_ALPHA], + math_power_node2.inputs[BLENDER_SHADERNODES.INPUT_MATH_POWER_BASE], + ) + links.new( + texture_node.outputs[BLENDER_SHADERNODES.OUTPUT_TEXIMAGE_ALPHA], + combine_rgb_node.inputs[BLENDER_SHADERNODES.INPUT_COMBINERGB_R], + ) + + # TODO + def _import_material_v62(self, loaded_material: LoadedMaterial) -> None: + material_name = loaded_material.name() + + if bpy.data.materials.get(material_name): + return + + material = bpy.data.materials.new(material_name) + material.use_nodes = True + material.blend_method = "HASHED" + material.shadow_method = "HASHED" + + nodes = material.node_tree.nodes + links = material.node_tree.links + + output_node = None + + for node in nodes: + if node.type != "OUTPUT_MATERIAL": + nodes.remove(node) + continue + + if node.type == "OUTPUT_MATERIAL" and output_node == None: + output_node = node + + if output_node == None: + output_node = nodes.new(BLENDER_SHADERNODES.SHADERNODE_OUTPUTMATERIAL) + + output_node.location = (300, 0) + + mix_shader_node = nodes.new(BLENDER_SHADERNODES.SHADERNODE_MIXSHADER) + mix_shader_node.location = (100, 0) + links.new( + mix_shader_node.outputs[BLENDER_SHADERNODES.OUTPUT_MIXSHADER_SHADER], + output_node.inputs[BLENDER_SHADERNODES.INPUT_OUTPUTMATERIAL_SURFACE], + ) + + transparent_bsdf_node = nodes.new( + BLENDER_SHADERNODES.SHADERNODE_BSDFTRANSPARENT + ) + transparent_bsdf_node.location = (-200, 100) + links.new( + transparent_bsdf_node.outputs[ + BLENDER_SHADERNODES.OUTPUT_BSDFTRANSPARENT_BSDF + ], + mix_shader_node.inputs[BLENDER_SHADERNODES.INPUT_MIXSHADER_SHADER1], + ) + + principled_bsdf_node = nodes.new(BLENDER_SHADERNODES.SHADERNODE_BSDFPRINCIPLED) + principled_bsdf_node.location = (-200, 0) + principled_bsdf_node.width = 200 + links.new( + principled_bsdf_node.outputs[ + BLENDER_SHADERNODES.OUTPUT_BSDFTRANSPARENT_BSDF + ], + mix_shader_node.inputs[BLENDER_SHADERNODES.INPUT_MIXSHADER_SHADER2], + ) + + loaded_textures = loaded_material.textures() + for i, loaded_texture in enumerate(loaded_textures): + texture_image = self._import_texture(loaded_texture=loaded_texture) + if texture_image == None: + continue + + loaded_texture_type = loaded_texture.texture_type() + + texture_node = nodes.new(BLENDER_SHADERNODES.SHADERNODE_TEXIMAGE) + texture_node.label = str(loaded_texture_type) texture_node.location = (-700, -255 * i) texture_node.image = texture_image - if loaded_texture_type == TEXTURE_TYPES.COLORMAP: + if loaded_texture_type == TEXTURE_TYPE.Color: links.new( texture_node.outputs[BLENDER_SHADERNODES.OUTPUT_TEXIMAGE_COLOR], principled_bsdf_node.inputs[ @@ -434,7 +649,7 @@ def _import_material_v20_v25(self, loaded_material: LoadedMaterial) -> None: texture_node.outputs[BLENDER_SHADERNODES.OUTPUT_TEXIMAGE_ALPHA], mix_shader_node.inputs[BLENDER_SHADERNODES.INPUT_MIXSHADER_FAC], ) - elif loaded_texture_type == TEXTURE_TYPES.SPECULARMAP: + elif loaded_texture_type == TEXTURE_TYPE.Specular: links.new( texture_node.outputs[BLENDER_SHADERNODES.OUTPUT_TEXIMAGE_COLOR], principled_bsdf_node.inputs[ @@ -445,7 +660,7 @@ def _import_material_v20_v25(self, loaded_material: LoadedMaterial) -> None: BLENDER_SHADERNODES.TEXIMAGE_COLORSPACE_LINEAR ) texture_node.location = (-700, -255) - elif loaded_texture_type == TEXTURE_TYPES.NORMALMAP: + elif loaded_texture_type == TEXTURE_TYPE.Normal: texture_node.image.colorspace_settings.name = ( BLENDER_SHADERNODES.TEXIMAGE_COLORSPACE_LINEAR ) @@ -581,18 +796,11 @@ def _import_texture( ) texture_image.pixels = loaded_texture.data() texture_image.file_format = "TARGA" - texture_image.pack() + texture_image.alpha_mode = "CHANNEL_PACKED" return texture_image -class TEXTURE_TYPES(metaclass=base_enum.BaseEnum): - COLORMAP = "colorMap" - DETAILMAP = "detailMap" - NORMALMAP = "normalMap" - SPECULARMAP = "specularMap" - - def import_ibsp(asset_path: str, file_path: str) -> None: importer = Importer(asset_path=asset_path) loader = Loader(importer=importer) @@ -603,7 +811,7 @@ def import_ibsp(asset_path: str, file_path: str) -> None: def import_xmodel( - asset_path: str, file_path: str, selected_version: GAME_VERSIONS + asset_path: str, file_path: str, selected_version: GAME_VERSION ) -> bpy.types.Object | bool: importer = Importer(asset_path=asset_path) loader = Loader(importer=importer) diff --git a/python/cod_asset_importer/operators.py b/python/cod_asset_importer/operators.py index 6b6c80c..ebe61db 100644 --- a/python/cod_asset_importer/operators.py +++ b/python/cod_asset_importer/operators.py @@ -3,7 +3,7 @@ import os from . import importer from .cod_asset_importer import ( - GAME_VERSIONS, + GAME_VERSION, ) @@ -43,18 +43,20 @@ class ModelImporter(bpy.types.Operator): name="Version", description="Version of the model", items=[ - ("cod1", "CoD1 (v14)", "Call of Duty & Call of Duty UO"), + ("cod", "CoD (v14)", "Call of Duty & Call of Duty UO"), ("cod2", "CoD2 (v20)", "Call of Duty 2"), ("cod4", "CoD4 (v25)", "Call of Duty: Modern Warfare"), ("cod5", "CoD5 (v25)", "Call of Duty: World at War"), + ("codbo1", "CoDBO1 (v62)", "Call of Duty: Black Ops"), ], ) version_options = { - "cod1": GAME_VERSIONS.CoD1, - "cod2": GAME_VERSIONS.CoD2, - "cod4": GAME_VERSIONS.CoD4, - "cod5": GAME_VERSIONS.CoD5, + "cod": GAME_VERSION.CoD, + "cod2": GAME_VERSION.CoD2, + "cod4": GAME_VERSION.CoD4, + "cod5": GAME_VERSION.CoD5, + "codbo1": GAME_VERSION.CoDBO1, } def execute(self, context: bpy.types.Context) -> Set[int] | Set[str]: diff --git a/rust/cod_asset_importer/src/assets/iwi.rs b/rust/cod_asset_importer/src/assets/iwi.rs index 31d215e..4f5f903 100644 --- a/rust/cod_asset_importer/src/assets/iwi.rs +++ b/rust/cod_asset_importer/src/assets/iwi.rs @@ -19,7 +19,7 @@ pub struct IWi { struct IWiHeader { magic: [u8; 3], - version: u8, + version: IWiVersion, } #[derive(Clone, Copy)] @@ -37,11 +37,14 @@ struct IWiMipMap { size: u32, } -#[derive(ValidEnum)] +#[derive(ValidEnum, PartialEq)] #[valid_enum(u8)] pub enum IWiVersion { - V5 = 0x05, - V6 = 0x06, + V5 = 0x05, // CoD2 + V6 = 0x06, // CoD4, CoD5 + V8 = 0x08, // CoDMW2, CoDMW3 + V13 = 0x0D, // CoDBO1 + V27 = 0x1B, // CoDBO2 } #[derive(ValidEnum)] @@ -59,10 +62,28 @@ pub enum IWiFormat { impl IWi { pub fn load(file_path: PathBuf) -> Result { let mut file = File::open(file_path)?; - Self::read_header(&mut file)?; + let header = Self::read_header(&mut file)?; + + if header.version == IWiVersion::V8 { + file.seek(SeekFrom::Start(0x08))?; + } + let info = Self::read_info(&mut file)?; - let offsets = binary::read_vec::(&mut file, 4)?; + let mut offset_amount = 4; + match header.version { + IWiVersion::V13 => { + offset_amount = 8; + file.seek(SeekFrom::Start(0x10))?; + } + IWiVersion::V27 => { + offset_amount = 8; + file.seek(SeekFrom::Start(0x20))?; + } + _ => (), + } + + let offsets = binary::read_vec::(&mut file, offset_amount)?; let current_offset = binary::current_offset(&mut file)?; let file_size = file.seek(SeekFrom::End(0))?; let mipmap = Self::calculate_highest_mipmap(offsets, current_offset, file_size); @@ -90,11 +111,11 @@ impl IWi { ))); } - let version = binary::read::(file)?; - match IWiVersion::valid(version) { - Some(_) => (), - None => return Err(Error::new(format!("invalid IWi version {}", version))), - } + let v = binary::read::(file)?; + let version = match IWiVersion::valid(v) { + Some(version) => version, + None => return Err(Error::new(format!("invalid IWi version {}", v))), + }; Ok(IWiHeader { magic: magic.try_into().unwrap(), diff --git a/rust/cod_asset_importer/src/assets/material.rs b/rust/cod_asset_importer/src/assets/material.rs index 2381a2d..b90909d 100644 --- a/rust/cod_asset_importer/src/assets/material.rs +++ b/rust/cod_asset_importer/src/assets/material.rs @@ -1,12 +1,12 @@ +use super::xmodel::XModelVersion; use crate::utils::{binary, Result}; +use pyo3::prelude::*; use std::{ fs::File, io::{Seek, SeekFrom}, path::PathBuf, }; -use super::xmodel::XModelVersion; - pub const ASSETPATH: &str = "materials"; pub struct Material { @@ -17,11 +17,22 @@ pub struct Material { #[derive(Debug)] pub struct MaterialTexture { - pub texture_type: String, + pub texture_type: TextureType, flags: u32, pub name: String, } +#[pyclass(module = "cod_asset_importer", name = "TEXTURE_TYPE")] +#[derive(Debug, Copy, Clone)] +pub enum TextureType { + Unused, + Color, + Normal, + Specular, + Roughness, + Detail, +} + impl Material { pub fn load(file_path: PathBuf, version: XModelVersion) -> Result { let mut file = File::open(&file_path)?; @@ -66,7 +77,7 @@ impl Material { let texture_name = binary::read_string(&mut file)?; textures.push(MaterialTexture { - texture_type, + texture_type: texture_type.into(), flags: texture_flags, name: texture_name, }); @@ -81,3 +92,16 @@ impl Material { }) } } + +impl From for TextureType { + fn from(texture_type: String) -> Self { + match texture_type.as_str() { + "colorMap" | "Diffuse_MapSampler" => TextureType::Color, + "normalMap" | "Normal_Map" => TextureType::Normal, + "detailMap" | "Detail_Map" => TextureType::Detail, + "specularMap" | "Specular_Map" => TextureType::Specular, + "Roughness_Map" => TextureType::Roughness, + _ => TextureType::Unused, + } + } +} diff --git a/rust/cod_asset_importer/src/assets/mod.rs b/rust/cod_asset_importer/src/assets/mod.rs index 231adfb..752c384 100644 --- a/rust/cod_asset_importer/src/assets/mod.rs +++ b/rust/cod_asset_importer/src/assets/mod.rs @@ -8,12 +8,13 @@ pub mod xmodel; pub mod xmodelpart; pub mod xmodelsurf; -#[pyclass(module = "cod_asset_importer", name = "GAME_VERSIONS")] +#[pyclass(module = "cod_asset_importer", name = "GAME_VERSION")] #[derive(ValidEnum, Debug, Clone, Copy)] #[valid_enum(u16)] pub enum GameVersion { - CoD1 = 1, // CoD1 & CoDUO - CoD2 = 2, // CoD2 - CoD4 = 4, // CoD4 - CoD5 = 5, // CoD5 + CoD, // CoD1 & CoDUO + CoD2, // CoD2 + CoD4, // CoD4 + CoD5, // CoD5 + CoDBO1, // CoDBO1 } diff --git a/rust/cod_asset_importer/src/assets/xmodel.rs b/rust/cod_asset_importer/src/assets/xmodel.rs index 4a1028c..74f688f 100644 --- a/rust/cod_asset_importer/src/assets/xmodel.rs +++ b/rust/cod_asset_importer/src/assets/xmodel.rs @@ -30,13 +30,14 @@ pub enum XModelType { Viewhands = 52, } -#[pyclass(module = "cod_asset_importer", name = "XMODEL_VERSIONS")] +#[pyclass(module = "cod_asset_importer", name = "XMODEL_VERSION")] #[derive(ValidEnum, Clone, Copy, PartialEq)] #[valid_enum(u16)] pub enum XModelVersion { V14 = 0x0E, // CoD1 & CoDUO V20 = 0x14, // CoD2 V25 = 0x19, // CoD4 & CoD5 + V62 = 0x3E, // CoDBO1 } type XModelLoadFunction = fn(&mut XModel, &mut File) -> Result<()>; @@ -60,10 +61,15 @@ impl XModel { let (expected_version, load_function): (XModelVersion, XModelLoadFunction) = match selected_version { - GameVersion::CoD1 => (XModelVersion::V14, XModel::load_v14), + GameVersion::CoD => (XModelVersion::V14, XModel::load_v14), GameVersion::CoD2 => (XModelVersion::V20, XModel::load_v20), - GameVersion::CoD4 => (XModelVersion::V25, XModel::load_v25), - GameVersion::CoD5 => (XModelVersion::V25, XModel::load_v25_2), + GameVersion::CoD4 => (XModelVersion::V25, |xmodel, file| { + XModel::load_v25(xmodel, file, 26) + }), + GameVersion::CoD5 => (XModelVersion::V25, |xmodel, file| { + XModel::load_v25(xmodel, file, 27) + }), + GameVersion::CoDBO1 => (XModelVersion::V62, XModel::load_v62), }; if xmodel_version != expected_version { @@ -147,8 +153,8 @@ impl XModel { Ok(()) } - fn load_v25(&mut self, file: &mut File) -> Result<()> { - binary::skip(file, 26)?; + fn load_v25(&mut self, file: &mut File, skip: i64) -> Result<()> { + binary::skip(file, skip)?; for _ in 0..4 { let distance = binary::read::(file)?; @@ -182,8 +188,11 @@ impl XModel { Ok(()) } - fn load_v25_2(&mut self, file: &mut File) -> Result<()> { - binary::skip(file, 27)?; + fn load_v62(&mut self, file: &mut File) -> Result<()> { + binary::skip(file, 28)?; + binary::read_string(file)?; + binary::read_string(file)?; + binary::skip(file, 5)?; for _ in 0..4 { let distance = binary::read::(file)?; diff --git a/rust/cod_asset_importer/src/assets/xmodelpart.rs b/rust/cod_asset_importer/src/assets/xmodelpart.rs index 8aa42f9..60fef6a 100644 --- a/rust/cod_asset_importer/src/assets/xmodelpart.rs +++ b/rust/cod_asset_importer/src/assets/xmodelpart.rs @@ -185,6 +185,10 @@ impl XModelPart { xmodel_part.load_v25(&mut file)?; Ok(xmodel_part) } + Some(XModelVersion::V62) => { + xmodel_part.load_v62(&mut file)?; + Ok(xmodel_part) + } None => Err(Error::new(format!( "invalid xmodelpart version {}", version @@ -380,6 +384,64 @@ impl XModelPart { self.bones[i as usize] = current_bone; } + Ok(()) + } + fn load_v62(&mut self, file: &mut File) -> Result<()> { + let bone_header = binary::read_vec::(file, 2)?; + let bone_count = bone_header[0]; + let root_bone_count = bone_header[1]; + + for _ in 0..root_bone_count { + self.bones.push(XModelPartBone { + name: String::from(""), + parent: -1, + local_transform: XModelPartBoneTransform { + position: [0.0, 0.0, 0.0], + rotation: [1.0, 0.0, 0.0, 0.0], + }, + world_transform: XModelPartBoneTransform { + position: [0.0, 0.0, 0.0], + rotation: [1.0, 0.0, 0.0, 0.0], + }, + }); + } + + for _ in 0..bone_count { + let parent = binary::read::(file)?; + let position = binary::read_vec::(file, 3)?; + let rotation = binary::read_vec::(file, 3)?; + + let qx = (rotation[0] as f32) / ROTATION_DIVISOR; + let qy = (rotation[1] as f32) / ROTATION_DIVISOR; + let qz = (rotation[2] as f32) / ROTATION_DIVISOR; + let qw = f32::sqrt((1.0 - (qx * qx) - (qy * qy) - (qz * qz)).max(0.0)); + + let bone_transform = XModelPartBoneTransform { + position: vec3_from_vec(position).unwrap(), + rotation: [qw, qx, qy, qz], + }; + + self.bones.push(XModelPartBone { + name: String::from(""), + parent, + local_transform: bone_transform, + world_transform: bone_transform, + }) + } + + for i in 0..root_bone_count + bone_count { + let mut current_bone = self.bones[i as usize].to_owned(); + let bone_name = binary::read_string(file)?; + current_bone.name = bone_name.clone(); + + if current_bone.parent > -1 { + let parent_bone = self.bones[current_bone.parent as usize].to_owned(); + current_bone.generate_world_transform_by_parent(parent_bone); + } + + self.bones[i as usize] = current_bone; + } + Ok(()) } } diff --git a/rust/cod_asset_importer/src/assets/xmodelsurf.rs b/rust/cod_asset_importer/src/assets/xmodelsurf.rs index eb4ed21..89b6655 100644 --- a/rust/cod_asset_importer/src/assets/xmodelsurf.rs +++ b/rust/cod_asset_importer/src/assets/xmodelsurf.rs @@ -61,6 +61,10 @@ impl XModelSurf { xmodel_surf.load_v25(&mut file)?; Ok(xmodel_surf) } + Some(XModelVersion::V62) => { + xmodel_surf.load_v62(&mut file)?; + Ok(xmodel_surf) + } None => Err(Error::new(format!( "invalid xmodelsurf version {}", version @@ -374,4 +378,88 @@ impl XModelSurf { Ok(()) } + + fn load_v62(&mut self, file: &mut File) -> Result<()> { + let surface_count = binary::read::(file)?; + + for _ in 0..surface_count { + binary::skip(file, 3)?; + let vertex_count = binary::read::(file)?; + let triangle_count = binary::read::(file)?; + let vertex_count2 = binary::read::(file)?; + + if vertex_count != vertex_count2 { + binary::skip(file, 2)?; + if vertex_count2 != 0 { + loop { + let p = binary::read::(file)?; + if p == 0 { + break; + } + } + binary::skip(file, 2)?; + } + } else { + binary::skip(file, 4)?; + } + + let mut vertices: Vec = Vec::new(); + for _ in 0..vertex_count { + let normal = binary::read_vec::(file, 3)?; + let color = binary::read_vec::(file, 4)?; + let uv = binary::read_vec::(file, 2)?; + + binary::skip(file, 28)?; + + let mut weight_count = 0; + let mut vertex_bone_idx = 0; + if vertex_count != vertex_count2 { + weight_count = binary::read::(file)?; + vertex_bone_idx = binary::read::(file)?; + } + + let position = binary::read_vec::(file, 3)?; + + let mut vertex_weights: Vec = vec![XModelSurfWeight { + bone: vertex_bone_idx, + influence: 1.0, + }]; + + if weight_count > 0 { + for _ in 0..weight_count { + let weight_bone_idx = binary::read::(file)?; + let weight_influence = binary::read::(file)?; + let weight_influence = weight_influence as f32 / RIGGED as f32; + vertex_weights[0].influence -= weight_influence; + vertex_weights.push(XModelSurfWeight { + bone: weight_bone_idx, + influence: weight_influence, + }); + } + } + + vertices.push(XModelSurfVertex { + normal: vec3_from_vec(normal).unwrap(), + color: color_from_vec(color).unwrap(), + uv: uv_from_vec(uv, false).unwrap(), + bone: vertex_bone_idx, + position: vec3_from_vec(position).unwrap(), + weights: vertex_weights, + }) + } + + let mut triangles: Vec = Vec::new(); + for _ in 0..triangle_count { + let t = binary::read_vec::(file, 3)?; + triangles.extend_from_slice(&[t[0], t[2], t[1]]); + } + + self.surfaces.push(XModelSurfSurface { + vertices, + triangles, + }); + } + + Ok(()) + } } diff --git a/rust/cod_asset_importer/src/lib.rs b/rust/cod_asset_importer/src/lib.rs index de14347..7807baf 100644 --- a/rust/cod_asset_importer/src/lib.rs +++ b/rust/cod_asset_importer/src/lib.rs @@ -3,7 +3,7 @@ mod loaded_assets; mod loader; mod utils; -use assets::{xmodel::XModelVersion, GameVersion}; +use assets::{xmodel::XModelVersion, GameVersion, material::TextureType}; use loaded_assets::{ LoadedBone, LoadedIbsp, LoadedIbspEntity, LoadedMaterial, LoadedModel, LoadedSurface, LoadedTexture, @@ -23,6 +23,7 @@ fn cod_asset_importer(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/rust/cod_asset_importer/src/loaded_assets.rs b/rust/cod_asset_importer/src/loaded_assets.rs index be695a1..cc613dc 100644 --- a/rust/cod_asset_importer/src/loaded_assets.rs +++ b/rust/cod_asset_importer/src/loaded_assets.rs @@ -2,8 +2,10 @@ use crate::{ assets::{ ibsp::{Ibsp, IbspEntity, IbspSurface}, iwi::IWi, + material::TextureType, + xmodel::XModelVersion, xmodelpart::XModelPartBone, - xmodelsurf::XModelSurfSurface, xmodel::XModelVersion, + xmodelsurf::XModelSurfSurface, }, utils::math::Vec3, }; @@ -36,7 +38,7 @@ pub struct LoadedModel { angles: Vec3, origin: Vec3, scale: Vec3, - materials: Vec, + materials: HashMap, surfaces: Vec, bones: Vec, } @@ -53,7 +55,7 @@ pub struct LoadedMaterial { #[derive(Clone)] pub struct LoadedTexture { name: String, - texture_type: String, + texture_type: TextureType, width: u16, height: u16, data: Vec, @@ -106,7 +108,7 @@ impl LoadedModel { self.scale } - fn materials(&mut self) -> Vec { + fn materials(&mut self) -> HashMap { mem::take(&mut self.materials) } @@ -140,8 +142,8 @@ impl LoadedTexture { &self.name } - fn texture_type(&self) -> &str { - &self.texture_type + fn texture_type(&self) -> TextureType { + self.texture_type } fn width(&self) -> u16 { @@ -261,7 +263,7 @@ impl LoadedModel { angles: Vec3, origin: Vec3, scale: Vec3, - materials: Vec, + materials: HashMap, surfaces: Vec, bones: Vec, ) -> Self { @@ -301,7 +303,7 @@ impl LoadedMaterial { } impl LoadedTexture { - pub fn set_texture_type(&mut self, texture_type: String) { + pub fn set_texture_type(&mut self, texture_type: TextureType) { self.texture_type = texture_type; } pub fn set_name(&mut self, name: String) { @@ -309,11 +311,17 @@ impl LoadedTexture { } } +impl LoadedSurface { + pub fn set_material(&mut self, material: String) { + self.material = material; + } +} + impl From for LoadedTexture { fn from(iwi: IWi) -> Self { Self { name: "".to_string(), - texture_type: "".to_string(), + texture_type: "".to_string().into(), width: iwi.width, height: iwi.height, data: iwi.data, diff --git a/rust/cod_asset_importer/src/loader.rs b/rust/cod_asset_importer/src/loader.rs index e6a93e6..9681ca5 100644 --- a/rust/cod_asset_importer/src/loader.rs +++ b/rust/cod_asset_importer/src/loader.rs @@ -9,14 +9,16 @@ use crate::{ GameVersion, }, error_log, info_log, - loaded_assets::{LoadedBone, LoadedIbsp, LoadedMaterial, LoadedModel, LoadedTexture}, - utils::{error::Error, Result}, + loaded_assets::{ + LoadedBone, LoadedIbsp, LoadedMaterial, LoadedModel, LoadedSurface, LoadedTexture, + }, + utils::{error::Error, path::file_name, Result}, }; use crossbeam_utils::sync::WaitGroup; use pyo3::{exceptions::PyBaseException, prelude::*}; use rayon::ThreadPoolBuilder; use std::{ - collections::HashMap, + collections::{hash_map::Entry::Vacant, HashMap}, path::PathBuf, sync::{mpsc::channel, Arc, Mutex}, thread, @@ -51,7 +53,7 @@ impl Loader { let loaded_ibsp = match Self::load_ibsp(PathBuf::from(file_path)) { Ok(loaded_ibsp) => loaded_ibsp, Err(error) => { - error_log!("{}", error); + error_log!("[MAP] {} - {}", file_name(PathBuf::from(file_path)), error); return Err(PyBaseException::new_err(error.to_string())); } }; @@ -61,7 +63,7 @@ impl Loader { let entities = loaded_ibsp.entities.clone(); let mut version = XModelVersion::V14; - let mut game_version = GameVersion::CoD1; + let mut game_version = GameVersion::CoD; if loaded_ibsp.version == IbspVersion::V4 as i32 { version = XModelVersion::V20; game_version = GameVersion::CoD2 @@ -87,12 +89,12 @@ impl Loader { ); } Err(error) => { - error_log!("{}", error) + error_log!("[MATERIAL] {} - {}", material_name, error) } } } Err(error) => { - error_log!("{}", error); + error_log!("[MATERIAL] {} - {}", material_name, error); } } } @@ -100,7 +102,7 @@ impl Loader { match importer_ref.call_method1("ibsp", (loaded_ibsp,)) { Ok(_) => (), Err(error) => { - error_log!("{}", error) + error_log!("[MAP] {} - {}", ibsp_name, error) } } @@ -119,20 +121,19 @@ impl Loader { let entity_path = PathBuf::from(asset_path) .join(xmodel::ASSETPATH) .join(entity.name); - let game_version = game_version.clone(); pool.spawn(move || { let load_start = Instant::now(); let mut loaded_model = match Self::load_xmodel_cached( entity_asset_path.clone(), entity_path, - entity_name, + entity_name.clone(), game_version, &mut cache, ) { Ok(loaded_model) => loaded_model, Err(error) => { - error_log!("{}", error); + error_log!("[MODEL] {} - {}", entity_name, error); return; } }; @@ -159,7 +160,7 @@ impl Loader { info_log!("[MODEL] {} [{:?}]", model_name, model_duration); } Err(error) => { - error_log!("{}", error); + error_log!("[MODEL] {} - {}", model_name, error); } } } @@ -191,7 +192,11 @@ impl Loader { ) { Ok(loaded_model) => loaded_model, Err(error) => { - error_log!("{}", error); + error_log!( + "[MODEL] {} - {}", + file_name(PathBuf::from(file_path)), + error + ); return Err(PyBaseException::new_err(error.to_string())); } }; @@ -207,7 +212,7 @@ impl Loader { Ok(()) } Err(error) => { - error_log!("{}", error); + error_log!("[MODEL] {} - {}", model_name, error); Err(error) } } @@ -235,7 +240,7 @@ impl Loader { let xmodelpart = match XModelPart::load(xmodelpart_file_path) { Ok(xmodelpart) => Some(xmodelpart), Err(error) => { - error_log!("{}", error); + error_log!("[XMODELPART] {} - {}", lod0.name.clone(), error); None } }; @@ -243,23 +248,28 @@ impl Loader { let xmodelsurf_file_path = asset_path.join(xmodelsurf::ASSETPATH).join(lod0.name); let xmodelsurf = XModelSurf::load(xmodelsurf_file_path, xmodelpart.clone())?; - let mut loaded_materials: Vec = Vec::new(); - for mat in lod0.materials { - match xmodel.version { - XModelVersion::V14 => { - loaded_materials.push(LoadedMaterial::new(mat, Vec::new(), xmodel.version)) - } - _ => { - let loaded_material = - match Self::load_material(asset_path.clone(), mat, xmodel.version) { + let mut loaded_materials: HashMap = HashMap::new(); + for mat in lod0.materials.clone() { + if let Vacant(entry) = loaded_materials.entry(mat.clone()) { + match xmodel.version { + XModelVersion::V14 => { + entry.insert(LoadedMaterial::new(mat, Vec::new(), xmodel.version)); + } + _ => { + let loaded_material = match Self::load_material( + asset_path.clone(), + mat.clone(), + xmodel.version, + ) { Ok(material) => material, Err(error) => { - error_log!("{}", error); + error_log!("[MATERIAL] {} - {}", mat, error); continue; } }; - loaded_materials.push(loaded_material); + entry.insert(loaded_material); + } } } } @@ -271,7 +281,17 @@ impl Loader { [0f32; 3], [1f32; 3], loaded_materials, - xmodelsurf.surfaces.into_iter().map(|s| s.into()).collect(), + xmodelsurf + .surfaces + .into_iter() + .enumerate() + .map(|(i, s)| { + let mut loaded_surface: LoadedSurface = s.into(); + loaded_surface.set_material(lod0.materials[i].clone()); + + loaded_surface + }) + .collect(), match xmodelpart { Some(xmodelpart) => xmodelpart.bones.into_iter().map(|b| b.into()).collect(), None => Vec::::new(), @@ -308,7 +328,7 @@ impl Loader { { Ok(loaded_model) => loaded_model, Err(error) => { - error_log!("{}", error); + error_log!("[MODEL] {} - {}", model_name, error); return Err(Error::new(error.to_string())); } }; @@ -335,7 +355,7 @@ impl Loader { let mut loaded_texture: LoadedTexture = match IWi::load(texture_file_path) { Ok(iwi) => iwi.into(), Err(error) => { - error_log!("{}", error); + error_log!("[IWI] {} - {}", texture.name, error); continue; } }; diff --git a/rust/cod_asset_importer/src/utils/path.rs b/rust/cod_asset_importer/src/utils/path.rs index 8f7c8ca..ee738f1 100644 --- a/rust/cod_asset_importer/src/utils/path.rs +++ b/rust/cod_asset_importer/src/utils/path.rs @@ -1,11 +1,19 @@ use std::path::PathBuf; pub fn file_name_without_ext(file_path: PathBuf) -> String { - let name = file_path + file_path .file_stem() .unwrap_or_default() .to_str() .unwrap_or_default() - .to_string(); - name + .to_string() +} + +pub fn file_name(file_path: PathBuf) -> String { + file_path + .file_name() + .unwrap_or_default() + .to_str() + .unwrap_or_default() + .to_string() } diff --git a/templates/material_template_v62.bt b/templates/material_template_v62.bt new file mode 100644 index 0000000..ae181b6 --- /dev/null +++ b/templates/material_template_v62.bt @@ -0,0 +1,73 @@ +//-------------------------------------- +//--- 010 Editor v6.0.1 Binary Template +// +// File: +// Author: +// Revision: +// Purpose: +//-------------------------------------- + +LittleEndian(); + +typedef float f32; +typedef double f64; +typedef unsigned char u8; +typedef unsigned short u16; +typedef unsigned int u32; +typedef char i8; +typedef short i16; +typedef int i32; +typedef int64 i64; + +typedef struct { + u32 type_offset; + u32 flags; + u32 texture_name_offset; + + SetBackColor( cLtBlue ); + local i64 current_offset = FTell(); + FSeek( type_offset ); + string type; + + SetBackColor( cLtGreen ); + FSeek( texture_name_offset ); + string texture_name; + + SetBackColor( cNone ); + FSeek( current_offset ); +} TEXTURE; + +typedef struct { + SetBackColor( cLtRed ); + u32 name_offset; + + SetBackColor( cAqua ); + local i64 current_offset = FTell(); + FSeek( name_offset ); + string name; + FSeek( current_offset ); + + FSkip( 44 ); // padding + + SetBackColor( cLtYellow ); + u16 texture_count; + + FSkip( 2 ); // padding + + SetBackColor( cLtGray ); + u32 techset_offset; + + SetBackColor( cLtGreen ); + u32 textures_offset; + + SetBackColor( cLtRed ); + FSeek( techset_offset ); + string techset; + + SetBackColor( cNone ); + FSeek( textures_offset ); + TEXTURE textures[texture_count] ; + +} MATERIAL; + +MATERIAL m; \ No newline at end of file diff --git a/templates/xmodelpart_template_v62.bt b/templates/xmodelpart_template_v62.bt new file mode 100644 index 0000000..1616a07 --- /dev/null +++ b/templates/xmodelpart_template_v62.bt @@ -0,0 +1,56 @@ +//-------------------------------------- +//--- 010 Editor v6.0.1 Binary Template +// +// File: +// Author: +// Revision: +// Purpose: +//-------------------------------------- + +LittleEndian(); + +typedef float f32; +typedef double f64; +typedef unsigned char u8; +typedef unsigned short u16; +typedef unsigned int u32; +typedef char i8; +typedef short i16; +typedef int i32; + +typedef struct { + SetBackColor( cLtRed ); + f32 x; + f32 y; + f32 z; +} JOINTPOS; + +typedef struct { + SetBackColor( cLtGreen ); + i16 x; + i16 y; + i16 z; +} JOINTROT; + +typedef struct { + SetBackColor( cLtBlue ); + u8 parent_joint_id; + JOINTPOS joint_pos; + JOINTROT joint_rot; +} JOINT; + +typedef struct { + SetBackColor( cLtYellow ); + string bone_name; +} BONENAME; + + +typedef struct { + u16 version; + u16 num_joints; + u16 num_joints_relative; + JOINT joints[num_joints] ; + BONENAME bone_names[num_joints + num_joints_relative] ; +} XMODELPART; + +XMODELPART x; \ No newline at end of file