Skip to main content

glacier_texture/
pack.rs

1use crate::atlas::AtlasData;
2use crate::mipblock::MipblockData;
3use crate::pack::TexturePackerError::{DirectXTexError, PackingError};
4use crate::texture_map::{
5    TextureData, TextureMap, TextureMapHeaderV1, TextureMapHeaderV2, TextureMapHeaderV3,
6    TextureMapHeaderV4, TextureMapInner,
7};
8use crate::{convert, GlacierGame};
9use directxtex::{
10    Image, ScratchImage, DDS_FLAGS, DXGI_FORMAT, TEX_COMPRESS_FLAGS, TEX_FILTER_FLAGS,
11    TEX_THRESHOLD_DEFAULT, TGA_FLAGS,
12};
13#[cfg(feature = "image")]
14use image::DynamicImage;
15use lz4::block::CompressionMode;
16use std::cmp::max;
17use std::io;
18use std::io::{Cursor, Read};
19use thiserror::Error;
20use crate::enums::{Dimensions, InterpretAs, RenderFormat, TextureType, TextureFlagsInner, TextureFlags, WoaRenderFormat, BondRenderFormat};
21use crate::pack::MipLevels::Limit;
22
23#[derive(Debug, Error)]
24pub enum TexturePackerError {
25    #[error("Error serializing the texture: {0}")]
26    SerializationError(#[from] binrw::Error),
27
28    #[error("Failed to read data: {0}")]
29    IoError(#[from] io::Error),
30
31    #[error("DirectX error: {0}")]
32    DirectXTexError(#[from] directxtex::HResultError),
33
34    #[error("Error building texture: {0}")]
35    PackingError(String),
36}
37
38#[derive(Copy, Clone, Debug)]
39pub enum MipLevels {
40    All,
41    Limit(u8),
42}
43
44#[derive(Copy, Clone, Debug)]
45pub enum MipFilter {
46    Nearest,
47    Linear,
48    Cubic,
49    Box,
50}
51
52#[derive(Copy, Clone, Debug)]
53pub struct TextureMapParameters {
54    texture_type: TextureType,
55    interpret_as: InterpretAs,
56    dimensions: Dimensions,
57    flags: TextureFlagsInner,
58    format: RenderFormat,
59    num_mip_levels: MipLevels,
60    default_mip_level: u8,
61    texd_identifier: u32,
62    mip_filter: MipFilter,
63}
64
65impl TextureMapParameters {
66    pub fn new(format: RenderFormat) -> Self {
67        Self {
68            texture_type: TextureType::Colour,
69            interpret_as: InterpretAs::Normal,
70            dimensions: Dimensions::_2D,
71
72            flags: TextureFlagsInner::default().with_lz4_compression(true),
73            format,
74            num_mip_levels: MipLevels::All,
75            default_mip_level: 0,
76            texd_identifier: 0x4000,
77            mip_filter: MipFilter::Box,
78        }
79    }
80
81    pub fn from_texture_map(texture: &TextureMap) -> Self {
82        Self {
83            texture_type: texture.texture_type(),
84            interpret_as: texture.interpret_as().unwrap_or(InterpretAs::Normal),
85            dimensions: Dimensions::_2D,
86
87            flags: texture.flags().inner,
88            format: texture.format(),
89            num_mip_levels: MipLevels::All,
90            default_mip_level: 0,
91            texd_identifier: 0x4000,
92            mip_filter: MipFilter::Box,
93        }
94    }
95
96    pub fn texture_type(&self) -> TextureType {
97        self.texture_type
98    }
99    pub fn interpret_as(&self) -> InterpretAs {
100        self.interpret_as
101    }
102    pub fn dimensions(&self) -> Dimensions {
103        self.dimensions
104    }
105    pub fn flags(&self) -> TextureFlags {
106        TextureFlags { inner: self.flags }
107    }
108    pub fn format(&self) -> RenderFormat {
109        self.format
110    }
111    pub fn num_mip_levels(&self) -> MipLevels {
112        self.num_mip_levels
113    }
114    pub fn default_mip_level(&self) -> u8 {
115        self.default_mip_level
116    }
117
118    pub fn texd_identifier(&self) -> u32 {
119        self.texd_identifier
120    }
121
122    pub fn mip_filter(&self) -> MipFilter {
123        self.mip_filter
124    }
125
126    pub fn set_texture_type(&mut self, texture_type: TextureType) {
127        self.texture_type = texture_type;
128    }
129
130    pub fn set_interpret_as(&mut self, interpret_as: InterpretAs) {
131        self.interpret_as = interpret_as;
132    }
133
134    #[cfg(feature = "unstable")]
135    pub fn set_dimensions(&mut self, dimensions: Dimensions) {
136        self.dimensions = dimensions;
137    }
138
139    pub fn set_flags(&mut self, flags: TextureFlags) {
140        self.flags = flags.inner;
141    }
142
143    pub fn set_format(&mut self, format: RenderFormat) {
144        self.format = format;
145    }
146
147    pub fn set_num_mip_levels(&mut self, num_mip_levels: MipLevels) {
148        self.num_mip_levels = num_mip_levels;
149    }
150
151    pub fn set_default_mip_level(&mut self, default_mip_level: u8) {
152        self.default_mip_level = default_mip_level;
153    }
154
155    #[cfg(feature = "unstable")]
156    pub fn set_texd_identifier(&mut self, texd_identifier: u32) {
157        self.texd_identifier = texd_identifier;
158    }
159
160    pub fn set_mip_filter(&mut self, mip_filter: MipFilter) {
161        self.mip_filter = mip_filter;
162    }
163}
164
165/// Builder struct for constructing TextureMap instances.
166/// Will enable the [`unknown3`] flag by default.
167pub struct TextureMapBuilder {
168    params: TextureMapParameters,
169    atlas_data: Option<AtlasData>,
170    image: ScratchImage,
171    use_mipblock1: bool,
172}
173
174impl TextureMapBuilder {
175    pub fn from_dds<R: Read>(mut reader: R) -> Result<Self, TexturePackerError> {
176        let mut image_data = vec![];
177        reader
178            .read_to_end(&mut image_data)
179            .map_err(TexturePackerError::IoError)?;
180        let image = ScratchImage::load_dds(
181            image_data.as_slice(),
182            DDS_FLAGS::DDS_FLAGS_FORCE_DX10_EXT,
183            None,
184            None,
185        )
186        .map_err(DirectXTexError)?;
187
188        Self::from_scratch_image(image)
189    }
190
191    pub fn from_tga<R: Read>(mut reader: R) -> Result<Self, TexturePackerError> {
192        let mut image_data = vec![];
193        reader
194            .read_to_end(&mut image_data)
195            .map_err(TexturePackerError::IoError)?;
196        let image = ScratchImage::load_tga(image_data.as_slice(), TGA_FLAGS::TGA_FLAGS_NONE, None)
197            .map_err(DirectXTexError)?;
198        Self::from_scratch_image(image)
199    }
200
201    #[cfg(feature = "image")]
202    pub fn from_dynamic_image(image: DynamicImage) -> Result<Self, TexturePackerError> {
203        let scratch_image = crate::image::helpers::dynamic_image_to_scratch_image(
204            image.as_bytes(),
205            image.width(),
206            image.height(),
207            image.color().into(),
208        )
209        .map_err(|e| PackingError(e.to_string()))?;
210        Self::from_scratch_image(scratch_image)
211    }
212
213    pub(crate) fn from_scratch_image(image: ScratchImage) -> Result<Self, TexturePackerError> {
214        let metadata = image.metadata();
215        let render_format = metadata.format.try_into().or_else(|_err| {
216            let bits_per_pixel = metadata.format.bits_per_pixel();
217            let bits_per_color = metadata.format.bits_per_color();
218            let num_channels = bits_per_pixel.checked_div(bits_per_color).unwrap_or(0);
219
220            match (bits_per_pixel, bits_per_color) {
221                (8, 8) => Ok(RenderFormat::A8),
222                (16, 8) => Ok(RenderFormat::R8G8),
223                (24, 8) => Ok(RenderFormat::R8G8B8A8),
224                (32, 8) => Ok(RenderFormat::R8G8B8A8),
225                (64, 16) => Ok(RenderFormat::R16G16B16A16),
226                _ => Err(PackingError(format!(
227                    "Unsupported render format: bpp={}, bpc={}, channels={:?}, format={:?}",
228                    bits_per_pixel, bits_per_color, num_channels, metadata.format
229                ))),
230            }
231        })?;
232
233        Ok(Self {
234            params: TextureMapParameters::new(render_format),
235            atlas_data: None,
236            image,
237            use_mipblock1: true,
238        })
239    }
240
241    pub fn from_texture_map(texture: &TextureMap) -> Result<Self, TexturePackerError> {
242        let mut builder = convert::create_dds(texture)
243            .map(|dds| {
244                let reader = Cursor::new(dds);
245                Self::from_dds(reader)
246            })
247            .map_err(|e| PackingError(format!("Failed to convert texture: {e}")))??;
248
249        builder.atlas_data = texture.atlas().clone();
250        builder.params.texture_type = texture.texture_type();
251        if let Some(interpret_as) = texture.interpret_as() {
252            builder.params.interpret_as = interpret_as;
253        }
254        Ok(builder)
255    }
256
257    pub fn with_params(mut self, params: TextureMapParameters) -> Self {
258        self.params = params;
259        self
260    }
261
262    // Builder methods for each field
263    pub fn texture_type(mut self, texture_type: TextureType) -> Self {
264        self.params.set_texture_type(texture_type);
265        self
266    }
267
268    pub fn with_texture_type(mut self, texture_type: TextureType) -> Self {
269        self.params.set_texture_type(texture_type);
270        self
271    }
272
273    pub fn with_default_mip_level(mut self, level: u8) -> Self {
274        self.params.set_default_mip_level(level);
275        self
276    }
277
278    pub fn with_num_mip_levels(mut self, levels: MipLevels) -> Self {
279        self.params.set_num_mip_levels(levels);
280        self
281    }
282
283    pub fn with_format(mut self, format: RenderFormat) -> Self {
284        self.params.set_format(format);
285        self
286    }
287
288    pub fn interpret_as(mut self, interpret_as: InterpretAs) -> Self {
289        self.params.set_interpret_as(interpret_as);
290        self
291    }
292
293    pub fn with_mip_filter(mut self, mip_filter: MipFilter) -> Self {
294        self.params.set_mip_filter(mip_filter);
295        self
296    }
297
298    pub fn with_atlas(mut self, atlas_data: AtlasData) -> Self {
299        self.atlas_data = Some(atlas_data);
300        self.params.flags = self.params.flags.with_atlas(true);
301        self
302    }
303
304    pub fn with_mipblock1(mut self, enabled: bool) -> Self {
305        self.use_mipblock1 = enabled;
306        self
307    }
308
309    pub fn with_flags(mut self, flags: TextureFlags) -> Self {
310        self.params.set_flags(flags);
311        self
312    }
313
314    #[cfg(feature = "unstable")]
315    pub fn with_dimensions(mut self, dimensions: Dimensions) -> Self {
316        self.params.set_dimensions(dimensions);
317        self
318    }
319
320    #[cfg(feature = "unstable")]
321    pub fn with_texd_id(mut self, texd_id: u32) -> Self {
322        self.params.set_texd_identifier(texd_id);
323        self
324    }
325
326    ///Convert the image to a different format.
327    /// It is assumed that the input image is not compressed
328    fn convert_to_format(
329        image: ScratchImage,
330        new_format: DXGI_FORMAT,
331    ) -> Result<ScratchImage, TexturePackerError> {
332        let reqs = [
333            new_format.is_typeless(false),
334            new_format.is_planar(),
335            new_format.is_palettized(),
336        ];
337        if reqs.iter().any(|b| *b) {
338            return Err(PackingError(format!("Invalid compression format provided, the provided format is [typeless: {}, planar: {}, palettized: {}]", reqs[0], reqs[1], reqs[2])));
339        }
340
341        Ok(match new_format.is_compressed() {
342            true => image
343                .compress(
344                    new_format,
345                    TEX_COMPRESS_FLAGS::TEX_COMPRESS_BC7_QUICK,
346                    // TEX_COMPRESS_FLAGS::TEX_COMPRESS_BC7_QUICK | TEX_COMPRESS_FLAGS::TEX_COMPRESS_PARALLEL,
347                    TEX_THRESHOLD_DEFAULT,
348                )
349                .map_err(DirectXTexError)?,
350            false => image
351                .convert(
352                    new_format,
353                    TEX_FILTER_FLAGS::TEX_FILTER_DEFAULT,
354                    TEX_THRESHOLD_DEFAULT,
355                )
356                .map_err(DirectXTexError)?,
357        })
358    }
359
360    /// Final build method to create a TextureMap.
361    pub fn build(self, glacier_game: GlacierGame) -> Result<TextureMap, TexturePackerError> {
362        let width = self.image.metadata().width as u16;
363        let height = self.image.metadata().height as u16;
364
365        if !width.is_power_of_two() {
366            return Err(PackingError(format!(
367                "Width ({width}) is not a power of two!"
368            )));
369        }
370
371        if !height.is_power_of_two() {
372            return Err(PackingError(format!(
373                "Height ({height}) is not a power of two!"
374            )));
375        }
376
377        let mut filter = match self.params.mip_filter {
378            MipFilter::Nearest => TEX_FILTER_FLAGS::TEX_FILTER_POINT,
379            MipFilter::Linear => TEX_FILTER_FLAGS::TEX_FILTER_LINEAR,
380            MipFilter::Cubic => TEX_FILTER_FLAGS::TEX_FILTER_CUBIC,
381            MipFilter::Box => TEX_FILTER_FLAGS::TEX_FILTER_BOX,
382        };
383
384        // if cfg!(windows) {
385        filter |= TEX_FILTER_FLAGS::TEX_FILTER_FORCE_NON_WIC;
386        // }
387
388        let mut image = self.image;
389        if image.metadata().format.is_compressed() {
390            image = directxtex::decompress(
391                image.images(),
392                image.metadata(),
393                match image.metadata().format {
394                    DXGI_FORMAT::DXGI_FORMAT_BC1_UNORM
395                    | DXGI_FORMAT::DXGI_FORMAT_BC2_UNORM
396                    | DXGI_FORMAT::DXGI_FORMAT_BC3_UNORM
397                    | DXGI_FORMAT::DXGI_FORMAT_BC7_UNORM => DXGI_FORMAT::DXGI_FORMAT_R8G8B8A8_UNORM,
398                    DXGI_FORMAT::DXGI_FORMAT_BC4_UNORM => DXGI_FORMAT::DXGI_FORMAT_A8_UNORM,
399                    DXGI_FORMAT::DXGI_FORMAT_BC5_UNORM => DXGI_FORMAT::DXGI_FORMAT_R8G8_UNORM,
400                    _ => DXGI_FORMAT::DXGI_FORMAT_UNKNOWN,
401                },
402            )?;
403        }
404
405        let should_generate_mips = self.use_mipblock1 && !matches!(self.params.num_mip_levels, Limit(1));
406        if should_generate_mips {
407            image = image.generate_mip_maps(
408                filter,
409                match self.params.num_mip_levels {
410                    MipLevels::All => 0,
411                    Limit(n) => n as usize,
412                },
413            )?;
414        }
415
416        let target_format = self.params.format.into();
417        if image.metadata().format != target_format {
418            image = Self::convert_to_format(image, target_format)?;
419        }
420
421        let generated_mip_levels = image.metadata().mip_levels.clamp(0, 14) as u8;
422        let num_mip_levels = generated_mip_levels;
423
424        // Handle mip sizes
425        let mut mip_sizes = [0u32; 14];
426        for i in 0..generated_mip_levels as usize {
427            let last: u32 = i
428                .checked_sub(1)
429                .and_then(|index| mip_sizes.get(index))
430                .copied()
431                .unwrap_or(0);
432            mip_sizes[i] =
433                last + image.image(i, 0, 0).map(|img| img.slice_pitch).unwrap_or(0) as u32;
434        }
435
436        let mut data = Self::serialize_mipmaps(&image, generated_mip_levels)?;
437        let mut compressed_mip_sizes = mip_sizes;
438        if glacier_game.supports_lz4_compression() && self.params.flags.lz4_compression() {
439            let mut compressed_image_buffer = vec![];
440            let mut cursor = Cursor::new(&data);
441            for mip in 0..generated_mip_levels as usize {
442                if let Some(mip_image) = image.image(mip, 0, 0) {
443                    let mut mip_data = vec![0u8; mip_image.slice_pitch];
444                    cursor
445                        .read(mip_data.as_mut_slice())
446                        .map_err(TexturePackerError::IoError)?;
447                    let mip_compressed = lz4::block::compress(
448                        &mip_data,
449                        Some(CompressionMode::HIGHCOMPRESSION(12)),
450                        false,
451                    )
452                    .map_err(|_| PackingError(format!("Failed to compress mip level {mip}")))?;
453
454                    let last: u32 = mip
455                        .checked_sub(1)
456                        .and_then(|index| compressed_mip_sizes.get(index))
457                        .copied()
458                        .unwrap_or(0);
459                    compressed_mip_sizes[mip] = last
460                        + image
461                            .image(mip, 0, 0)
462                            .map(|_| mip_compressed.len())
463                            .unwrap_or(0) as u32;
464
465                    compressed_image_buffer.extend(mip_compressed);
466                }
467            }
468            data = compressed_image_buffer;
469        }
470
471        let texture_data = if self.use_mipblock1 {
472            TextureData::Mipblock1(MipblockData {
473                video_memory_requirement: (mip_sizes.first().copied().unwrap_or(0x0)
474                    + mip_sizes.get(1).copied().unwrap_or(0x0))
475                    as usize,
476                header: vec![],
477                data,
478            })
479        } else {
480            TextureData::Tex(data)
481        };
482
483        let texture_map_inner = match glacier_game {
484            GlacierGame::HM2016 => {
485                let header = TextureMapHeaderV1 {
486                    type_: self.params.texture_type,
487                    texd_identifier: self.params.texd_identifier,
488                    #[cfg(feature = "unstable")]
489                    flags: self.params.flags,
490                    #[cfg(not(feature = "unstable"))]
491                    flags: TextureFlagsInner::default(), //detached from builder
492                    width,
493                    height,
494                    format: WoaRenderFormat {
495                        format: self.params.format,
496                    },
497                    num_mip_levels,
498                    default_mip_level: self.params.default_mip_level,
499                    interpret_as: self.params.interpret_as,
500                    dimensions: self.params.dimensions,
501                    mip_sizes,
502                    has_atlas: self.atlas_data.is_some(),
503                };
504                TextureMapInner {
505                    header,
506                    atlas_data: self.atlas_data,
507                    data: texture_data,
508                }
509                .into()
510            }
511            GlacierGame::HM2 => {
512                let header = TextureMapHeaderV2 {
513                    type_: self.params.texture_type,
514                    texd_identifier: self.params.texd_identifier,
515                    #[cfg(feature = "unstable")]
516                    flags: self.params.flags,
517                    #[cfg(not(feature = "unstable"))]
518                    flags: TextureFlagsInner::default(), //detached from builder
519                    width,
520                    height,
521                    format: WoaRenderFormat {
522                        format: self.params.format,
523                    },
524                    num_mip_levels,
525                    default_mip_level: max(self.params.default_mip_level, 1), //H2 crashes with index 0
526                    mip_sizes,
527                    compressed_mip_sizes,
528                    has_atlas: self.atlas_data.is_some(),
529                };
530                TextureMapInner {
531                    header,
532                    atlas_data: self.atlas_data,
533                    data: texture_data,
534                }
535                .into()
536            }
537            GlacierGame::HM3 => {
538                let header = TextureMapHeaderV3 {
539                    type_: self.params.texture_type,
540                    flags: self.params.flags,
541                    width,
542                    height,
543                    format: WoaRenderFormat {
544                        format: self.params.format,
545                    },
546                    num_mip_levels,
547                    default_mip_level: self.params.default_mip_level,
548                    interpret_as: self.params.interpret_as,
549                    dimensions: self.params.dimensions,
550                    mip_sizes,
551                    compressed_mip_sizes,
552                    has_atlas: self.atlas_data.is_some(),
553                };
554                TextureMapInner {
555                    header,
556                    atlas_data: self.atlas_data,
557                    data: texture_data,
558                }
559                .into()
560            }
561            GlacierGame::KNT => {
562                let header = TextureMapHeaderV4 {
563                    type_: self.params.texture_type,
564                    flags: self.params.flags,
565                    width,
566                    height,
567                    format: BondRenderFormat {
568                        format: self.params.format,
569                    },
570                    num_mip_levels,
571                    default_mip_level: self.params.default_mip_level,
572                    interpret_as: self.params.interpret_as,
573                    dimensions: self.params.dimensions,
574                    mip_sizes,
575                    compressed_mip_sizes,
576                    has_atlas: self.atlas_data.is_some(),
577                };
578                TextureMapInner {
579                    header,
580                    atlas_data: self.atlas_data,
581                    data: texture_data,
582                }
583                .into()
584            }
585        };
586
587        Ok(texture_map_inner)
588    }
589
590    fn process_mip_image(mip_image: &Image) -> Option<Vec<u8>> {
591        convert::image_pixels(mip_image)
592    }
593
594    fn serialize_mipmaps(
595        image: &directxtex::ScratchImage,
596        mip_levels: u8,
597    ) -> Result<Vec<u8>, TexturePackerError> {
598        let mut serialized = Vec::new();
599        for mip in 0..mip_levels {
600            if let Some(mip_image) = image.image(mip as usize, 0, 0) {
601                let buffer = Self::process_mip_image(mip_image).unwrap_or(vec![]);
602                serialized.extend_from_slice(buffer.as_slice());
603            } else {
604                return Err(PackingError(format!("Missing mip level {mip}")));
605            }
606        }
607        Ok(serialized)
608    }
609}