Skip to main content

unity_formats/
material.rs

1//! Unity materials as stored: the shader's name and render state (decoded into [`crate::shader`]'s types), and the saved
2//! properties (textures with their tiling and offset, floats, integers, colours), keywords, tags and render queue. What
3//! they mean depends on the shader.
4
5use crate::player::{ObjRef, Player};
6use crate::shader::{
7    PassKind, PassState, ShaderPass, ShaderStencilOp, ShaderTargetBlend, ShaderValue, SubShader,
8};
9use crate::value::{Array, Value};
10use crate::{Result, invalid};
11
12/// A texture property: the texture (`None`: no texture set) and its tiling (`_ST`) with Unity's bottom-left origin.
13#[derive(Clone, Debug, PartialEq)]
14pub struct TexEnv {
15    /// The texture object.
16    pub texture: Option<ObjRef>,
17    /// The tiling (`m_Scale`).
18    pub scale: [f32; 2],
19    /// The offset (`m_Offset`).
20    pub offset: [f32; 2],
21}
22
23/// A `Material` as stored, its properties in stored order.
24#[derive(Clone, Debug, PartialEq)]
25pub struct UnityMaterialData {
26    /// `m_Name`.
27    pub name: String,
28    /// The material's shader; `None` for a null pointer.
29    pub shader: Option<ObjRef>,
30    /// Texture properties by name (`m_TexEnvs`).
31    pub textures: Vec<(String, TexEnv)>,
32    /// Float properties by name (`m_Floats`).
33    pub floats: Vec<(String, f32)>,
34    /// Integer properties by name (`m_Ints`).
35    pub ints: Vec<(String, i64)>,
36    /// RGBA as stored (gamma-encoded for colour properties).
37    pub colors: Vec<(String, [f32; 4])>,
38    /// The enabled shader keywords.
39    pub keywords: Vec<String>,
40    /// Override tags (`stringTagMap`).
41    pub tags: Vec<(String, String)>,
42    /// `m_CustomRenderQueue` (-1: the shader's).
43    pub render_queue: i64,
44}
45
46fn list<'a>(v: &'a Value, k: &str) -> Result<&'a [Value]> {
47    match v.get(k) {
48        Some(Value::Array(Array::Values(items))) => Ok(items),
49        Some(Value::Array(a)) if a.is_empty() => Ok(&[]),
50        None => Ok(&[]),
51        _ => Err(invalid(format!("material: {k} is not a list"))),
52    }
53}
54
55/// A list of `pair(string, T)` as (name, T).
56fn pairs<'a, T>(v: &'a Value, k: &str, f: impl Fn(&'a Value) -> Option<T>) -> Result<Vec<(String, T)>> {
57    list(v, k)?
58        .iter()
59        .map(|p| match p {
60            Value::Pair(kv) => {
61                let name =
62                    kv.0.as_str()
63                        .ok_or_else(|| invalid(format!("material: {k} key")))?;
64                let value = f(&kv.1).ok_or_else(|| invalid(format!("material: {k} {name}")))?;
65                Ok((name.to_owned(), value))
66            }
67            _ => Err(invalid(format!("material: {k} entry is not a pair"))),
68        })
69        .collect()
70}
71
72fn f2(v: &Value) -> Option<[f32; 2]> {
73    Some([v.get("x")?.as_f32()?, v.get("y")?.as_f32()?])
74}
75
76fn strings(v: &Value, k: &str) -> Result<Vec<String>> {
77    list(v, k)?
78        .iter()
79        .map(|s| {
80            s.as_str()
81                .map(str::to_owned)
82                .ok_or_else(|| invalid(format!("material: {k} entry")))
83        })
84        .collect()
85}
86
87/// A material's enabled keywords: `m_ValidKeywords` (Unity 2021.2 on; `m_InvalidKeywords`, those its shader does not
88/// declare, are not enabled), else `m_ShaderKeywords`, one string separated by spaces (earlier versions).
89fn keywords(m: &Value) -> Result<Vec<String>> {
90    if m.get("m_ValidKeywords").is_some() {
91        return strings(m, "m_ValidKeywords");
92    }
93    match m.get("m_ShaderKeywords") {
94        None => Ok(Vec::new()),
95        Some(v) => Ok(v
96            .as_str()
97            .ok_or_else(|| invalid("material: m_ShaderKeywords is not a string"))?
98            .split_whitespace()
99            .map(str::to_owned)
100            .collect()),
101    }
102}
103
104impl UnityMaterialData {
105    /// Read the Material `r` of `player`.
106    pub fn read(player: &Player, r: &ObjRef) -> Result<Self> {
107        let m = player.decode(r)?;
108        let props = m
109            .get("m_SavedProperties")
110            .ok_or_else(|| invalid("material: no m_SavedProperties"))?;
111        let shader = match m.get("m_Shader") {
112            Some(p) => player.resolve(&r.file, p)?,
113            None => None,
114        };
115        let textures = list(props, "m_TexEnvs")?
116            .iter()
117            .map(|p| {
118                let Value::Pair(kv) = p else {
119                    return Err(invalid("material: m_TexEnvs entry is not a pair"));
120                };
121                let name = kv.0.as_str().ok_or_else(|| invalid("material: texture name"))?;
122                let env = &kv.1;
123                let texture = match env.get("m_Texture") {
124                    Some(t) => player.resolve(&r.file, t)?,
125                    None => None,
126                };
127                let ts = |k: &str| {
128                    env.get(k)
129                        .and_then(f2)
130                        .ok_or_else(|| invalid(format!("material: {name} {k}")))
131                };
132                Ok((
133                    name.to_owned(),
134                    TexEnv {
135                        texture,
136                        scale: ts("m_Scale")?,
137                        offset: ts("m_Offset")?,
138                    },
139                ))
140            })
141            .collect::<Result<_>>()?;
142        Ok(UnityMaterialData {
143            name: m.get("m_Name").and_then(Value::as_str).unwrap_or("").to_owned(),
144            shader,
145            textures,
146            floats: pairs(props, "m_Floats", Value::as_f32)?,
147            ints: pairs(props, "m_Ints", Value::as_i64)?,
148            colors: pairs(props, "m_Colors", |c| {
149                Some([
150                    c.get("r")?.as_f32()?,
151                    c.get("g")?.as_f32()?,
152                    c.get("b")?.as_f32()?,
153                    c.get("a")?.as_f32()?,
154                ])
155            })?,
156            keywords: keywords(&m)?,
157            tags: pairs(&m, "stringTagMap", |s| s.as_str().map(str::to_owned))?,
158            render_queue: m.get("m_CustomRenderQueue").and_then(Value::as_i64).unwrap_or(-1),
159        })
160    }
161}
162
163/// A Shader's name and render state (`m_ParsedForm`); its compiled programs are not read.
164#[derive(Clone, Debug, PartialEq)]
165pub struct ShaderData {
166    /// The shader's name (`Universal Render Pipeline/Lit`).
167    pub name: String,
168    /// The subshaders, in order.
169    pub subshaders: Vec<SubShader>,
170}
171
172fn tag_list(v: Option<&Value>) -> Result<Vec<(String, String)>> {
173    let Some(v) = v else { return Ok(Vec::new()) };
174    let inner = v.get("tags").or_else(|| v.get("m_Tags")).unwrap_or(v);
175    pairs_of(inner, "tag")
176}
177
178fn pairs_of(v: &Value, what: &str) -> Result<Vec<(String, String)>> {
179    let items: &[Value] = match v {
180        Value::Array(Array::Values(items)) => items,
181        Value::Array(a) if a.is_empty() => &[],
182        _ => return Err(invalid(format!("shader: {what}s are not a list"))),
183    };
184    items
185        .iter()
186        .map(|p| match p {
187            Value::Pair(kv) => Ok((
188                kv.0.as_str()
189                    .ok_or_else(|| invalid(format!("shader: {what} key")))?
190                    .to_owned(),
191                kv.1.as_str()
192                    .ok_or_else(|| invalid(format!("shader: {what} value")))?
193                    .to_owned(),
194            )),
195            _ => Err(invalid(format!("shader: {what} entry is not a pair"))),
196        })
197        .collect()
198}
199
200fn shader_value(v: Option<&Value>, what: &str) -> Result<ShaderValue> {
201    let v = v.ok_or_else(|| invalid(format!("shader pass: no {what}")))?;
202    let value = v
203        .get("val")
204        .and_then(Value::as_f32)
205        .ok_or_else(|| invalid(format!("shader pass: {what} without a value")))?;
206    let property = match v.get("name").and_then(Value::as_str) {
207        None | Some("") | Some("<noninit>") => None,
208        Some(n) => Some(n.to_owned()),
209    };
210    Ok(ShaderValue { value, property })
211}
212
213/// A render target's blend (`rtBlend<n>`).
214fn target_blend(rt: &Value) -> Result<ShaderTargetBlend> {
215    Ok(ShaderTargetBlend {
216        src: shader_value(rt.get("srcBlend"), "srcBlend")?,
217        dst: shader_value(rt.get("destBlend"), "destBlend")?,
218        src_alpha: shader_value(rt.get("srcBlendAlpha"), "srcBlendAlpha")?,
219        dst_alpha: shader_value(rt.get("destBlendAlpha"), "destBlendAlpha")?,
220        op: shader_value(rt.get("blendOp"), "blendOp")?,
221        op_alpha: shader_value(rt.get("blendOpAlpha"), "blendOpAlpha")?,
222        mask: shader_value(rt.get("colMask"), "colMask")?,
223    })
224}
225
226/// A stencil face's operations (`stencilOp`, `stencilOpFront`, `stencilOpBack`).
227fn stencil_op(st: &Value, what: &str) -> Result<ShaderStencilOp> {
228    let op = st
229        .get(what)
230        .ok_or_else(|| invalid(format!("shader pass: no {what}")))?;
231    Ok(ShaderStencilOp {
232        pass: shader_value(op.get("pass"), "stencil pass")?,
233        fail: shader_value(op.get("fail"), "stencil fail")?,
234        z_fail: shader_value(op.get("zFail"), "stencil zFail")?,
235        comp: shader_value(op.get("comp"), "stencil comp")?,
236    })
237}
238
239/// A pass's render state beyond the first target's blend, depth write and culling (`m_State`).
240fn pass_state(st: &Value, name: &str) -> Result<PassState> {
241    let mut targets = Vec::with_capacity(8);
242    for n in 0..8 {
243        let rt = st
244            .get(&format!("rtBlend{n}"))
245            .ok_or_else(|| invalid(format!("shader {name}: a pass without rtBlend{n}")))?;
246        targets.push(target_blend(rt)?);
247    }
248    Ok(PassState {
249        targets,
250        separate_blend: st
251            .get("rtSeparateBlend")
252            .and_then(Value::as_bool)
253            .ok_or_else(|| invalid(format!("shader {name}: a pass without rtSeparateBlend")))?,
254        z_test: shader_value(st.get("zTest"), "zTest")?,
255        offset_factor: shader_value(st.get("offsetFactor"), "offsetFactor")?,
256        offset_units: shader_value(st.get("offsetUnits"), "offsetUnits")?,
257        stencil_ref: shader_value(st.get("stencilRef"), "stencilRef")?,
258        stencil_read_mask: shader_value(st.get("stencilReadMask"), "stencilReadMask")?,
259        stencil_write_mask: shader_value(st.get("stencilWriteMask"), "stencilWriteMask")?,
260        stencil_op: stencil_op(st, "stencilOp")?,
261        stencil_op_front: stencil_op(st, "stencilOpFront")?,
262        stencil_op_back: stencil_op(st, "stencilOpBack")?,
263    })
264}
265
266impl ShaderData {
267    /// Read the Shader `r` of `player`.
268    pub fn read(player: &Player, r: &ObjRef) -> Result<Self> {
269        let s = player.decode(r)?;
270        let name =
271            shader_name_of(&s).ok_or_else(|| invalid(format!("shader {}/{}: no name", r.file, r.path_id)))?;
272        let mut subshaders = Vec::new();
273        if let Some(form) = s.get("m_ParsedForm") {
274            for sub in list(form, "m_SubShaders")? {
275                let mut passes = Vec::new();
276                for p in list(sub, "m_Passes")? {
277                    let st = p
278                        .get("m_State")
279                        .ok_or_else(|| invalid(format!("shader {name}: a pass without m_State")))?;
280                    let rt = st
281                        .get("rtBlend0")
282                        .ok_or_else(|| invalid(format!("shader {name}: a pass without rtBlend0")))?;
283                    let kind = match p.get("m_Type").and_then(Value::as_i64) {
284                        Some(0) => PassKind::Normal,
285                        Some(1) => PassKind::Use(
286                            p.get("m_UseName")
287                                .and_then(Value::as_str)
288                                .ok_or_else(|| {
289                                    invalid(format!("shader {name}: a UsePass without m_UseName"))
290                                })?
291                                .to_owned(),
292                        ),
293                        Some(2) => PassKind::Grab,
294                        t => return Err(invalid(format!("shader {name}: pass type {t:?}"))),
295                    };
296                    passes.push(ShaderPass {
297                        kind,
298                        light_mode: tag_list(st.get("m_Tags"))?
299                            .into_iter()
300                            .find(|(k, _)| k.eq_ignore_ascii_case("LightMode"))
301                            .map(|(_, v)| v),
302                        src_blend: shader_value(rt.get("srcBlend"), "srcBlend")?,
303                        dst_blend: shader_value(rt.get("destBlend"), "destBlend")?,
304                        zwrite: shader_value(st.get("zWrite"), "zWrite")?,
305                        cull: shader_value(st.get("culling"), "culling")?,
306                        state: pass_state(st, &name)?,
307                    });
308                }
309                subshaders.push(SubShader {
310                    tags: tag_list(sub.get("m_Tags"))?,
311                    passes,
312                });
313            }
314        }
315        Ok(ShaderData { name, subshaders })
316    }
317}
318
319fn shader_name_of(s: &Value) -> Option<String> {
320    s.at(&["m_ParsedForm", "m_Name"])
321        .or_else(|| s.get("m_Name"))
322        .and_then(Value::as_str)
323        .map(str::to_owned)
324}
325
326/// The name of the Shader `r` (`m_ParsedForm.m_Name`).
327pub fn shader_name(player: &Player, r: &ObjRef) -> Result<String> {
328    shader_name_of(&player.decode(r)?)
329        .ok_or_else(|| invalid(format!("shader {}/{}: no name", r.file, r.path_id)))
330}