LightMappedGeneric

This commit is contained in:
Robin Appelman 2023-12-18 19:17:34 +01:00
commit ab10e2d05c
14 changed files with 378 additions and 243 deletions

1
.gitignore vendored
View file

@ -3,3 +3,4 @@ target
*.obj
result
.direnv
*.snap.new

15
Cargo.lock generated
View file

@ -426,6 +426,17 @@ dependencies = [
"syn",
]
[[package]]
name = "serde_repr"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "similar"
version = "2.3.0"
@ -608,9 +619,9 @@ name = "vmt-parser"
version = "0.1.0"
dependencies = [
"insta",
"logos",
"miette",
"parse-display",
"serde",
"serde_repr",
"test-case",
"thiserror",
"vdf-reader",

View file

@ -4,11 +4,11 @@ version = "0.1.0"
edition = "2021"
[dependencies]
logos = "0.13.0"
thiserror = "1.0.50"
miette = "5.10.0"
parse-display = "0.8.2"
vdf-reader = { version = "0.1", path = "../vdf-reader" }
serde = { version = "1.0.193", features = ["derive"] }
serde_repr = "0.1.17"
[dev-dependencies]
test-case = "3.3.1"

View file

@ -1,16 +1,14 @@
use miette::{Context, IntoDiagnostic, Result};
use std::env::args;
use std::fs::read_to_string;
use vdf_reader::Reader;
use vmt_parser::material::Material;
use vmt_parser::from_str;
fn main() -> Result<()> {
let path = args().nth(1).expect("no path provided");
let raw = read_to_string(path)
.into_diagnostic()
.wrap_err("failed to read input")?;
let mut reader = Reader::from(raw.as_str());
let material = Material::parse(&mut reader).wrap_err("failed to parse material")?;
let material = from_str(&raw).wrap_err("failed to parse material")?;
dbg!(material);
Ok(())
}

View file

@ -72,6 +72,7 @@
cargo-audit
cargo-msrv
cargo-semver-checks
cargo-insta
(writeShellApplication {
name = "cargo-fuzz";
runtimeInputs = [cargo-fuzz toolchain];

80
src/data/mod.rs Normal file
View file

@ -0,0 +1,80 @@
pub mod texture_transform;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
pub use texture_transform::TextureTransform;
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
#[serde(from = "Vec2OrSingle<f32>")]
pub struct Vec2([f32; 2]);
impl From<Vec2OrSingle<f32>> for Vec2 {
fn from(value: Vec2OrSingle<f32>) -> Self {
match value {
Vec2OrSingle::Vec2(vec) => Vec2(vec),
Vec2OrSingle::Single(val) => Vec2([val; 2]),
}
}
}
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
#[serde(from = "Vec3OrSingle<f32>")]
pub struct Vec3([f32; 3]);
impl From<Vec3OrSingle<f32>> for Vec3 {
fn from(value: Vec3OrSingle<f32>) -> Self {
match value {
Vec3OrSingle::Vec3(vec) => Vec3(vec),
Vec3OrSingle::Single(val) => Vec3([val; 3]),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
enum Vec3OrSingle<T> {
Vec3([T; 3]),
Single(T),
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
enum Vec2OrSingle<T> {
Vec2([T; 2]),
Single(T),
}
pub(crate) fn default_scale3() -> Vec3 {
Vec3([1.0; 3])
}
#[allow(dead_code)]
pub(crate) fn default_scale2() -> Vec2 {
Vec2([1.0; 2])
}
pub(crate) fn default_scale() -> f32 {
1.0
}
pub(crate) fn default_detail_scale() -> Vec2 {
Vec2([4.0; 2])
}
#[derive(Serialize_repr, Deserialize_repr, PartialEq, Debug, Copy, Clone, Default)]
#[repr(u8)]
pub enum BlendMode {
DecalModulate = 0,
#[default]
Additive = 1,
TranslucentOverlay = 2,
BlendFactorOverlay = 3,
TranslucentBase = 4,
UnlitAdditive = 5,
UnlitAdditiveThreshold = 6,
TwoPatternModulate = 7,
Multiply = 8,
BaseMaskAlpha = 9,
SelfShadowedBumpMap = 10,
SelfShadowedBumpAlbedo = 11,
}

View file

@ -0,0 +1,123 @@
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::borrow::Cow;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
#[derive(Debug, PartialEq, Clone)]
pub struct TextureTransform {
pub center: [f32; 2],
pub scale: [f32; 2],
pub rotate: f32,
pub translate: [f32; 2],
}
impl Default for TextureTransform {
fn default() -> Self {
TextureTransform {
center: [0.5; 2],
scale: [1.0; 2],
rotate: 0.0,
translate: [0.0; 2],
}
}
}
impl Display for TextureTransform {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"center {} {} scale {} {} rotate {} translate {} {}",
self.center[0],
self.center[1],
self.scale[0],
self.scale[1],
self.rotate,
self.translate[0],
self.translate[1],
)
}
}
#[test]
fn test_parse_base_texture_transform() {
assert_eq!(
TextureTransform {
center: [0.5; 2],
scale: [1.0; 2],
rotate: 0.0,
translate: [0.0; 2],
},
TextureTransform::from_str("center .5 .5 scale 1 1 rotate 0 translate 0 0").unwrap()
);
assert_eq!(
TextureTransform {
center: [0.2, 0.3],
scale: [1.1, 1.2],
rotate: 1.0,
translate: [0.4, 0.5],
},
TextureTransform::from_str("center .2 .3 scale 1.1 1.2 rotate 1 translate 0.4 0.5")
.unwrap()
);
}
impl FromStr for TextureTransform {
type Err = &'static str;
fn from_str(str: &str) -> Result<Self, Self::Err> {
let mut parts = str.split(' ').filter(|p| !p.is_empty());
match (
parts.next(),
parts.next().and_then(|val| f32::from_str(val).ok()),
parts.next().and_then(|val| f32::from_str(val).ok()),
parts.next(),
parts.next().and_then(|val| f32::from_str(val).ok()),
parts.next().and_then(|val| f32::from_str(val).ok()),
parts.next(),
parts.next().and_then(|val| f32::from_str(val).ok()),
parts.next(),
parts.next().and_then(|val| f32::from_str(val).ok()),
parts.next().and_then(|val| f32::from_str(val).ok()),
) {
(
Some("center"),
Some(center_x),
Some(center_y),
Some("scale"),
Some(scale_x),
Some(scale_y),
Some("rotate"),
Some(rotate),
Some("translate"),
Some(translate_x),
Some(translate_y),
) => Ok(TextureTransform {
center: [center_x, center_y],
scale: [scale_x, scale_y],
rotate,
translate: [translate_x, translate_y],
}),
_ => Err("invalid $basetexturetransform format"),
}
}
}
impl<'de> Deserialize<'de> for TextureTransform {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let str = Cow::<str>::deserialize(deserializer)?;
Self::from_str(str.as_ref()).map_err(D::Error::custom)
}
}
impl Serialize for TextureTransform {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}

View file

@ -1,7 +1,5 @@
use miette::{Diagnostic, SourceSpan};
use miette::Diagnostic;
use thiserror::Error;
use vdf_reader::entry::Entry;
use vdf_reader::error::ParseEntryError;
use vdf_reader::VdfError;
#[derive(Debug, Error, Diagnostic)]
@ -9,60 +7,4 @@ pub enum VmtError {
#[error(transparent)]
#[diagnostic(transparent)]
Vdf(#[from] VdfError),
#[error(transparent)]
#[diagnostic(transparent)]
Eof(#[from] EofError),
#[error(transparent)]
#[diagnostic(transparent)]
ParseValue(#[from] ParseValueError),
}
#[derive(Debug, Error, Diagnostic)]
#[error("Unexpected end of input while looking for {expected}")]
#[diagnostic(code(vmt_parser::eof))]
pub struct EofError {
expected: &'static str,
#[label("Expected {}", self.expected)]
err_span: SourceSpan,
#[source_code]
src: String,
}
impl EofError {
pub fn new(src: String, expected: &'static str) -> Self {
let span = src.len()..src.len();
EofError {
src,
err_span: span.into(),
expected,
}
}
}
#[derive(Debug, Error, Diagnostic)]
#[error("Can't parse value {value:?} as {ty} to read {name}")]
#[diagnostic(code(vmt_parser::eof))]
pub struct ParseValueError {
name: &'static str,
ty: &'static str,
pub value: Entry,
#[label("Expected a {}", self.ty)]
err_span: SourceSpan,
#[source_code]
src: String,
}
impl ParseValueError {
pub fn new(
src: String,
name: &'static str,
err_span: SourceSpan,
parse_err: ParseEntryError,
) -> Self {
ParseValueError {
src,
err_span,
name,
ty: parse_err.ty,
value: parse_err.value,
}
}
}

View file

@ -1,6 +1,14 @@
mod data;
mod error;
pub mod material;
use crate::material::Material;
pub use data::*;
pub use error::VmtError;
pub type Result<T, E = VmtError> = std::result::Result<T, E>;
pub fn from_str(input: &str) -> Result<Material> {
let input = input.to_ascii_lowercase();
vdf_reader::from_str(&input).map_err(VmtError::from)
}

View file

@ -1,176 +0,0 @@
use crate::error::{EofError, ParseValueError};
use crate::Result;
use std::collections::HashMap;
use vdf_reader::entry::{Entry, FromEntry, Table};
use vdf_reader::{Event, GroupStartEvent, Reader, VdfError};
#[derive(Debug, Clone)]
pub enum Material {
Other(OtherMaterial),
LightMappedGeneric(LightMappedGenericMaterial),
}
impl Material {
pub fn parse(reader: &mut Reader) -> Result<Self> {
let start: GroupStartEvent = expect_event(reader, "material name")?;
let name = start.name.to_ascii_lowercase();
Ok(match name.as_str() {
"lightmappedgeneric" => {
Material::LightMappedGeneric(LightMappedGenericMaterial::parse(reader)?)
}
_ => Material::Other(OtherMaterial::parse(name, reader)?),
})
}
}
#[derive(Debug, Clone)]
pub struct LightMappedGenericMaterial {
pub keywords: String,
pub detail: Option<String>,
pub detail_blend_factory: f32,
pub detail_scale: f32,
pub detail_blend_mode: u32,
pub base_texture: String,
pub ss_bump: bool,
pub bump_map: Option<String>,
pub rest: HashMap<String, Entry>,
}
impl LightMappedGenericMaterial {
fn parse(reader: &mut Reader) -> Result<Self> {
let src = reader.source;
let mut keywords = Default::default();
let mut detail = Default::default();
let mut detail_blend_factory = Default::default();
let mut detail_scale = Default::default();
let mut detail_blend_mode = Default::default();
let mut base_texture = Default::default();
let mut ss_bump = Default::default();
let mut bump_map = Default::default();
let mut rest: HashMap<String, Entry> = Default::default();
loop {
let (span, key, value) = match event(reader, "item or group end")? {
Event::GroupEnd(_) => break,
Event::GroupStart(start) => (
start.span,
start.name.to_ascii_lowercase(),
Entry::Table(Table::load(reader)?),
),
Event::Entry(entry) => (
entry.span,
entry.key.into_content().to_ascii_lowercase(),
entry.value.into(),
),
};
match key.as_str() {
"%keywords" => {
keywords = FromEntry::from_entry(value).map_err(|err| {
ParseValueError::new(src.into(), "keywords", span.into(), err)
})?;
}
"$detail" => {
detail = FromEntry::from_entry(value).map_err(|err| {
ParseValueError::new(src.into(), "detail", span.into(), err)
})?;
}
"$detailblendfactor" => {
detail_blend_factory = FromEntry::from_entry(value).map_err(|err| {
ParseValueError::new(src.into(), "detail_blend_factory", span.into(), err)
})?;
}
"$detailscale" => {
detail_scale = FromEntry::from_entry(value).map_err(|err| {
ParseValueError::new(src.into(), "detail_scale", span.into(), err)
})?;
}
"$detailblendmode" => {
detail_blend_mode = FromEntry::from_entry(value).map_err(|err| {
ParseValueError::new(src.into(), "detail_blend_mode", span.into(), err)
})?;
}
"$basetexture" => {
base_texture = FromEntry::from_entry(value).map_err(|err| {
ParseValueError::new(src.into(), "base_texture", span.into(), err)
})?;
}
"$ssbump" => {
ss_bump = FromEntry::from_entry(value).map_err(|err| {
ParseValueError::new(src.into(), "ss_bump", span.into(), err)
})?;
}
"$bumpmap" => {
bump_map = FromEntry::from_entry(value).map_err(|err| {
ParseValueError::new(src.into(), "bump_map", span.into(), err)
})?;
}
_ => {
rest.insert(key, value);
}
}
}
Ok(LightMappedGenericMaterial {
keywords,
detail,
detail_blend_factory,
detail_scale,
detail_blend_mode,
base_texture,
ss_bump,
bump_map,
rest,
})
}
}
#[derive(Debug, Clone)]
pub struct OtherMaterial {
pub name: String,
pub values: HashMap<String, Entry>,
}
impl OtherMaterial {
fn parse(name: String, reader: &mut Reader) -> Result<Self> {
let mut values: HashMap<String, _> = HashMap::new();
loop {
let (key, value) = match event(reader, "item or group end")? {
Event::GroupEnd(_) => break,
Event::GroupStart(start) => (
start.name.to_ascii_lowercase(),
Entry::Table(Table::load(reader)?),
),
Event::Entry(entry) => (
entry.key.into_content().to_ascii_lowercase(),
entry.value.into(),
),
};
values.insert(key, value);
}
Ok(OtherMaterial { name, values })
}
}
fn event<'a>(reader: &mut Reader<'a>, expected: &'static str) -> Result<Event<'a>> {
Ok(reader
.next()
.ok_or_else(|| EofError::new(reader.source.into(), expected))??)
}
fn expect_event<'a, E: 'a>(reader: &mut Reader<'a>, expected: &'static str) -> Result<E>
where
E: TryFrom<Event<'a>, Error = VdfError>,
{
let event = event(reader, expected)?;
E::try_from(event).map_err(|e| {
match e {
VdfError::WrongEntryType(e) => e.with_source(reader.source.into()).into(),
e => e,
}
.into()
})
}

View file

@ -0,0 +1,88 @@
use crate::{
default_detail_scale, default_scale, default_scale3, BlendMode, TextureTransform, Vec2, Vec3,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LightMappedGenericMaterial {
/// Defines an diffuse texture.
#[serde(rename = "$basetexture")]
pub base_texture: String,
/// Use this material as a decal.
#[serde(rename = "$decal", default)]
pub decal: bool,
/// Detail texturing.
#[serde(rename = "$detail")]
pub detail: Option<String>,
/// Links the surface to a set of physical properties.
#[serde(rename = "$surfaceprop")]
pub surface_prop: Option<String>,
/// Transforms the texture before use in the material. This does not affect lightmaps on the surface.
#[serde(rename = "$basetexturetransform", default)]
pub base_texture_transform: TextureTransform,
/// Independently scales the red, green and blue channels of an albedo.
#[serde(rename = "$color", default = "default_scale3")]
pub color: Vec3,
/// the number of units that each texel covers
#[serde(rename = "$decalscale", default = "default_scale")]
pub decal_scale: f32,
#[serde(rename = "$decalscale", default = "default_detail_scale")]
/// Fits the detail texture onto the material the given number of times
pub detail_scale: Vec2,
/// Controls the amount that the detail texture affects the base texture. The precise use of this depends on the blend factor; in most cases it acts similarly to $alpha. A value of 0 usually makes the detail texture have no effect, whilst a value of 1 applies the full effect.
#[serde(rename = "$detailblendfactor", default = "default_scale")]
pub detail_blend_factor: f32,
/// How to combine the detail material with the albedo.
#[serde(rename = "$detailblendmode", default)]
pub detail_blend_mode: BlendMode,
/// A separate VertexLitGeneric material to that will replace this one if the decal hits a model.
#[serde(rename = "$modelmaterial", default)]
pub model_material: Option<String>,
/// Disables texture filtering.
#[serde(rename = "$pointsamplemagfilter", default)]
pub point_sample_mag_filter: bool,
/// Mitigation for displacement texture stretching.
#[serde(rename = "$seamless_scale", default = "default_scale")]
pub seamless_scale: f32,
/// Scales the opacity of an entire material.
#[serde(rename = "$alpha", default = "default_scale")]
pub alpha: f32,
/// Specifies a mask to use to determine binary opacity.
#[serde(rename = "$alphatest", default)]
pub alpha_test: bool,
/// Vector-like edge filtering.
#[serde(rename = "$distancealpha", default)]
pub distance_alpha: bool,
/// Disables backface culling.
#[serde(rename = "$nocull", default)]
pub no_cull: bool,
/// Specifies that the material should be partially see-through.
#[serde(rename = "$translucent", default)]
pub translucent: bool,
/// Specifies a texture that will provide three-dimensional lighting information for a material.
#[serde(rename = "$bumpmap")]
pub bump_map: Option<String>,
/// Per-texel color modification via a warp texture.
#[serde(rename = "$lightwarptexture")]
pub light_wrap_texture: Option<String>,
/// Determines whether the surface is self-illuminated independent of environment lighting.
#[serde(rename = "$selfillum", default)]
pub self_illum: bool,
/// Flags the $bumpmap as being a self-shadowing bumpmap.
#[serde(rename = "$ssbump", default)]
pub ss_bump: bool,
/// Specular reflections.
#[serde(rename = "$envmap")]
pub env_map: Option<String>,
/// Diffuse reflections.
#[serde(rename = "$phong", default)]
pub phong: bool,
/// Prevents fog from overdrawing a material.
#[serde(rename = "$nofog", default)]
pub no_fog: bool,
}

10
src/material/mod.rs Normal file
View file

@ -0,0 +1,10 @@
mod lightmappedgeneric;
use lightmappedgeneric::LightMappedGenericMaterial;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Material {
#[serde(rename = "lightmappedgeneric")]
LightMappedGeneric(LightMappedGenericMaterial),
}

18
tests/parse.rs Normal file
View file

@ -0,0 +1,18 @@
use miette::{GraphicalReportHandler, GraphicalTheme};
use std::fs::read_to_string;
use test_case::test_case;
use vmt_parser::from_str;
#[test_case("tests/data/concretefloor003.vmt")]
fn test_serde(path: &str) {
let raw = read_to_string(path).unwrap();
match from_str(&raw) {
Ok(result) => insta::assert_ron_snapshot!(path, result),
Err(e) => {
let handler = GraphicalReportHandler::new_themed(GraphicalTheme::unicode_nocolor());
let mut out = String::new();
handler.render_report(&mut out, &e).unwrap();
insta::assert_snapshot!(path, out)
}
}
}

View file

@ -0,0 +1,31 @@
---
source: tests/parse.rs
expression: result
---
lightmappedgeneric(LightMappedGenericMaterial(
r#$basetexture: "cp_mountainlab/concrete/concretefloor003",
r#$decal: false,
r#$detail: Some("overlays/detail001"),
r#$surfaceprop: None,
r#$basetexturetransform: "center 0.5 0.5 scale 1 1 rotate 0 translate 0 0",
r#$color: Vec3((1.0, 1.0, 1.0)),
r#$decalscale: 1.0,
r#$decalscale: Vec2((4.0, 4.0)),
r#$detailblendfactor: 1.0,
r#$detailblendmode: 0,
r#$modelmaterial: None,
r#$pointsamplemagfilter: false,
r#$seamless_scale: 1.0,
r#$alpha: 1.0,
r#$alphatest: false,
r#$distancealpha: false,
r#$nocull: false,
r#$translucent: false,
r#$bumpmap: Some("concrete/concretefloor007b_height-ssbump"),
r#$lightwarptexture: None,
r#$selfillum: false,
r#$ssbump: true,
r#$envmap: None,
r#$phong: false,
r#$nofog: false,
))