Skip to main content

unity_formats/
texture.rs

1//! Unity textures (`Texture2D`): the pixel format, the mip chain from the object or its streamed resource, colour
2//! space, how the texture is used, and its sampler state.
3//!
4//! Unity stores rows bottom first (row 0 is v = 0, its texture origin being bottom-left); the bytes are returned as
5//! stored.
6
7use crate::player::{ObjRef, Player};
8use crate::resource::StreamingInfo;
9use crate::value::Value;
10use crate::{Error, Result, invalid};
11
12/// Unity's TextureUsageMode for a normal map stored as DXT5nm: X in alpha, Y in green.
13pub const USAGE_NORMAL_DXT5NM: i64 = 3;
14/// Unity's TextureUsageMode for a normal map stored as RGB.
15pub const USAGE_NORMAL_PLAIN: i64 = 4;
16
17/// A `Texture2D` as stored.
18#[derive(Clone, Debug, PartialEq)]
19pub struct UnityTexture {
20    /// `m_Name`.
21    pub name: String,
22    /// Width in pixels of the largest level.
23    pub width: u32,
24    /// Height in pixels of the largest level.
25    pub height: u32,
26    /// Unity's TextureFormat id.
27    pub format: i64,
28    /// `m_MipCount`: how many levels `data` holds.
29    pub mips: u32,
30    /// `m_ColorSpace` 1: sampled as sRGB.
31    pub srgb: bool,
32    /// Unity's TextureUsageMode (`m_LightmapFormat`).
33    pub usage: i64,
34    /// TextureWrapMode along u and v (0 repeat, 1 clamp, 2 mirror, 3 mirror once).
35    pub wrap: [i64; 2],
36    /// FilterMode (0 point, 1 bilinear, 2 trilinear).
37    pub filter: i64,
38    /// `m_MipBias`: added to the level of detail the texture is sampled at.
39    pub mip_bias: f32,
40    /// `m_Aniso`: the anisotropic filtering level (above 1: anisotropic).
41    pub aniso: i64,
42    /// All levels, largest first, bottom row first.
43    pub data: Vec<u8>,
44}
45
46/// A non-negative 32-bit integer of the texture (a negative one is no size).
47fn dimension(v: &Value, k: &str) -> Result<u32> {
48    let n = int(v, k)?;
49    u32::try_from(n).map_err(|_| invalid(format!("texture: {k} of {n}")))
50}
51
52fn int(v: &Value, k: &str) -> Result<i64> {
53    v.get(k)
54        .and_then(Value::as_i64)
55        .ok_or_else(|| invalid(format!("texture: no integer {k}")))
56}
57
58fn float(v: &Value, k: &str) -> Result<f32> {
59    v.get(k)
60        .and_then(Value::as_f32)
61        .ok_or_else(|| invalid(format!("texture: no float {k}")))
62}
63
64/// The pixel formats of Unity's `TextureFormat` this reader names. Crunched formats (28, 29, 64, 65) need Unity's
65/// crunch decoder and are refused.
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67pub enum TextureFormat {
68    /// `Alpha8` (1): one 8-bit channel.
69    Alpha8,
70    /// `ARGB4444` (2): one little-endian 16-bit value, A in the top 4 bits, then R, G, B.
71    Argb4444,
72    /// `RGB24` (3): three bytes a pixel, R, G, B.
73    Rgb24,
74    /// `RGBA32` (4): four bytes a pixel, R, G, B, A.
75    Rgba32,
76    /// `DXT1` (10): BC1, 4x4 blocks of 8 bytes.
77    Dxt1,
78    /// `DXT5` (12): BC3, 4x4 blocks of 16 bytes.
79    Dxt5,
80    /// `BC6H` (24): 4x4 blocks of 16 bytes, unsigned half floats.
81    Bc6h,
82    /// `BC7` (25): 4x4 blocks of 16 bytes.
83    Bc7,
84    /// `BC4` (26): 4x4 blocks of 8 bytes.
85    Bc4,
86    /// `BC5` (27): 4x4 blocks of 16 bytes.
87    Bc5,
88    /// `R8` (63): one 8-bit channel.
89    R8,
90}
91
92impl TextureFormat {
93    /// The format Unity's `TextureFormat` id `id` names.
94    pub fn from_id(id: i64) -> Result<TextureFormat> {
95        Ok(match id {
96            1 => TextureFormat::Alpha8,
97            2 => TextureFormat::Argb4444,
98            3 => TextureFormat::Rgb24,
99            4 => TextureFormat::Rgba32,
100            10 => TextureFormat::Dxt1,
101            12 => TextureFormat::Dxt5,
102            24 => TextureFormat::Bc6h,
103            25 => TextureFormat::Bc7,
104            26 => TextureFormat::Bc4,
105            27 => TextureFormat::Bc5,
106            63 => TextureFormat::R8,
107            28 | 29 | 64 | 65 => {
108                return Err(Error::Unsupported(format!("crunched texture format {id}")));
109            }
110            id => return Err(Error::Unsupported(format!("texture format {id}"))),
111        })
112    }
113
114    /// The size in pixels of a block and its size in bytes (one pixel for the uncompressed formats).
115    fn block(self) -> (u32, usize) {
116        match self {
117            TextureFormat::Alpha8 | TextureFormat::R8 => (1, 1),
118            TextureFormat::Argb4444 => (1, 2),
119            TextureFormat::Rgb24 => (1, 3),
120            TextureFormat::Rgba32 => (1, 4),
121            TextureFormat::Dxt1 | TextureFormat::Bc4 => (4, 8),
122            TextureFormat::Dxt5 | TextureFormat::Bc6h | TextureFormat::Bc7 | TextureFormat::Bc5 => (4, 16),
123        }
124    }
125
126    /// The bytes of a `width` x `height` level, whole blocks rounded up; `None` when the size does not fit in a
127    /// `usize` (the dimensions are the file's to state).
128    pub fn level_size(self, width: u32, height: u32) -> Option<usize> {
129        let (side, bytes) = self.block();
130        let blocks_x = usize::try_from(width.div_ceil(side).max(1)).ok()?;
131        let blocks_y = usize::try_from(height.div_ceil(side).max(1)).ok()?;
132        blocks_x.checked_mul(blocks_y)?.checked_mul(bytes)
133    }
134}
135
136impl UnityTexture {
137    /// Read the Texture2D `r` of `player` (its pixels in the object or streamed beside its file).
138    pub fn read(player: &Player, r: &ObjRef) -> Result<Self> {
139        let t = player.decode(r)?;
140        let settings = t
141            .get("m_TextureSettings")
142            .ok_or_else(|| invalid("texture: no m_TextureSettings"))?;
143        let stream = t
144            .get("m_StreamData")
145            .map(StreamingInfo::of)
146            .transpose()?
147            .flatten();
148        let data = match stream {
149            Some(s) => player.read_stream(&r.file, &s)?,
150            None => t
151                .get("image data")
152                .and_then(Value::as_bytes)
153                .ok_or_else(|| invalid("texture: no image data"))?
154                .to_vec(),
155        };
156        Ok(UnityTexture {
157            name: t.get("m_Name").and_then(Value::as_str).unwrap_or("").to_owned(),
158            width: dimension(&t, "m_Width")?,
159            height: dimension(&t, "m_Height")?,
160            format: int(&t, "m_TextureFormat")?,
161            mips: dimension(&t, "m_MipCount")?,
162            srgb: int(&t, "m_ColorSpace")? == 1,
163            usage: int(&t, "m_LightmapFormat")?,
164            wrap: [int(settings, "m_WrapU")?, int(settings, "m_WrapV")?],
165            filter: int(settings, "m_FilterMode")?,
166            mip_bias: float(settings, "m_MipBias")?,
167            aniso: int(settings, "m_Aniso")?,
168            data,
169        })
170    }
171
172    /// The mip chain split into its levels, largest first, each as stored (bottom row first), with the format the
173    /// texture id names. Every size is checked: no more levels than halvings of a 32-bit side, no level whose size
174    /// overflows or runs past the data, no data left over.
175    pub fn levels(&self) -> Result<(TextureFormat, Vec<&[u8]>)> {
176        if self.width == 0 || self.height == 0 || self.mips == 0 {
177            return Err(Error::Unsupported(format!(
178                "{}: an empty texture ({}x{}, {} levels)",
179                self.name, self.width, self.height, self.mips
180            )));
181        }
182        let format = TextureFormat::from_id(self.format)?;
183        // no more levels than halvings of a 32-bit side
184        if self.mips > 32 {
185            return Err(invalid(format!("{}: {} mip levels", self.name, self.mips)));
186        }
187        let mut levels = Vec::with_capacity(self.mips as usize);
188        let mut at = 0usize;
189        for i in 0..self.mips {
190            let (w, h) = ((self.width >> i).max(1), (self.height >> i).max(1));
191            let size = format.level_size(w, h).ok_or_else(|| {
192                invalid(format!(
193                    "{}: level {i} of {w}x{h} does not fit in memory",
194                    self.name
195                ))
196            })?;
197            let bytes = at
198                .checked_add(size)
199                .and_then(|end| self.data.get(at..end))
200                .ok_or_else(|| {
201                    invalid(format!(
202                        "{}: level {i} past the {} bytes of pixels",
203                        self.name,
204                        self.data.len()
205                    ))
206                })?;
207            levels.push(bytes);
208            at = at.saturating_add(size);
209        }
210        if at != self.data.len() {
211            return Err(invalid(format!(
212                "{}: {} levels use {at} of {} bytes",
213                self.name,
214                self.mips,
215                self.data.len()
216            )));
217        }
218        Ok((format, levels))
219    }
220}