Skip to content

Commit

Permalink
Implement OBJ parser
Browse files Browse the repository at this point in the history
  • Loading branch information
taiki-e committed Jan 22, 2024
1 parent 33c3ead commit 5382c7e
Show file tree
Hide file tree
Showing 19 changed files with 2,001 additions and 148 deletions.
12 changes: 12 additions & 0 deletions .github/.cspell/project-dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ binormal
bitangent
brep
bytecount
clearcoat
collada
ctypes
disp
Eisel
elems
emin
Expand All @@ -14,14 +16,17 @@ endsolid
gltf
idents
IDREF
illum
instancenodes
kwxport
Lemire
linestrips
memchr
memrchr
mmap
mtllib
nalgebra
newmtl
NMTOKEN
polylist
powerset
Expand All @@ -32,13 +37,20 @@ rustflags
rustup
SIDREF
significand
specularity
splitn
testline
testmixed
testpoints
texbinormal
texcoord
texcoords
textangent
trifans
tristrips
usemtl
vcolors
vcount
vertexcolors
wasi
xmlspecialchars
11 changes: 5 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,25 @@ edition = "2021"
rust-version = "1.60"
license = "Apache-2.0"
repository = "https://github.com/openrr/mesh-loader"
keywords = ["asset", "mesh", "stl", "collada"] # TODO: "obj"
keywords = ["asset", "mesh", "stl", "collada", "obj"]
categories = ["parser-implementations", "graphics"]
exclude = ["/.*", "/assets"]
description = """
Fast parser for 3D-model-formats.
"""

[features]
default = ["stl", "collada"]
default = ["stl", "collada", "obj"]

# STL (.stl)
# https://en.wikipedia.org/wiki/STL_(file_format)
stl = []
# COLLADA (.dae)
# https://en.wikipedia.org/wiki/COLLADA
collada = ["roxmltree"]
# TODO
# # Wavefront OBJ (.obj)
# # https://en.wikipedia.org/wiki/Wavefront_.obj_file
# obj = []
# Wavefront OBJ (.obj)
# https://en.wikipedia.org/wiki/Wavefront_.obj_file
obj = []

[dependencies]
# Used in COLLADA parsing.
Expand Down
72 changes: 38 additions & 34 deletions examples/kiss3d/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,38 +77,42 @@ fn main() -> Result<()> {
}

fn add_mesh(window: &mut Window, path: &Path, scale: na::Vector3<f32>) -> Result<SceneNode> {
let loader = mesh_loader::Loader::default().merge_meshes(true);
let mut scene = loader.load(path)?;
assert_eq!(scene.meshes.len(), 1); // merge_meshes guarantees this.
let mesh = scene.meshes.pop().unwrap();
eprintln!("mesh={mesh:?}");
let coords = mesh.vertices.into_iter().map(Into::into).collect();
let faces = mesh
.faces
.into_iter()
.map(|f| na::Point3::new(f[0], f[1], f[2]))
.collect();
let normals = if mesh.normals.is_empty() {
None
} else {
Some(mesh.normals.into_iter().map(Into::into).collect())
};
let uvs = if mesh.texcoords[0].is_empty() {
None
} else {
Some(mesh.texcoords[0].iter().copied().map(Into::into).collect())
};
let kiss3d_mesh = Rc::new(RefCell::new(kiss3d::resource::Mesh::new(
coords, faces, normals, uvs, false,
)));
let kiss3d_scene = window.add_mesh(kiss3d_mesh, scale);
// TODO(material)
// if let Some(color) = material.diffuse_color() {
// kiss3d_scene.set_color(color[0], color[1], color[2]);
// }
// if let Some(path) = materials.get(0) {
// kiss3d_scene.set_texture_from_file(path, path.to_str().unwrap());
// }

Ok(kiss3d_scene)
let mut base = window.add_group();
let loader = mesh_loader::Loader::default();
let scene = loader.load(path)?;
assert_eq!(scene.meshes.len(), scene.materials.len());
for (mesh, material) in scene.meshes.into_iter().zip(scene.materials) {
eprintln!("mesh={mesh:?}");
eprintln!("material={material:?}");
let coords = mesh.vertices.into_iter().map(Into::into).collect();
let faces = mesh
.faces
.into_iter()
.map(|f| na::Point3::new(f[0], f[1], f[2]))
.collect();
let normals = if mesh.normals.is_empty() {
None
} else {
Some(mesh.normals.into_iter().map(Into::into).collect())
};
let uvs = if mesh.texcoords[0].is_empty() {
None
} else {
Some(mesh.texcoords[0].iter().copied().map(Into::into).collect())
};
let kiss3d_mesh = Rc::new(RefCell::new(kiss3d::resource::Mesh::new(
coords, faces, normals, uvs, false,
)));
let mut kiss3d_scene = base.add_mesh(kiss3d_mesh, scale);
if let Some(color) = material.color.diffuse {
kiss3d_scene.set_color(color[0], color[1], color[2]);
}
if let Some(path) = &material.texture.diffuse {
kiss3d_scene.set_texture_from_file(path, path.to_str().unwrap());
}
if let Some(path) = &material.texture.ambient {
kiss3d_scene.set_texture_from_file(path, path.to_str().unwrap());
}
}
Ok(base)
}
12 changes: 12 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ mesh-loader = { path = ".." }
libfuzzer-sys = { version = "0.4", optional = true }
afl = { version = "0.15", optional = true }

[[bin]]
name = "mtl"
path = "mtl.rs"
test = false
doc = false

[[bin]]
name = "obj"
path = "obj.rs"
test = false
doc = false

[[bin]]
name = "stl"
path = "stl.rs"
Expand Down
43 changes: 43 additions & 0 deletions fuzz/mtl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
Run with libFuzzer:
```sh
cargo fuzz run --release --features libfuzzer mtl
```
Run with AFL++:
```sh
cd fuzz
cargo afl build --release --features afl
cargo afl fuzz -i seeds/mtl -o out target/release/mtl
```
*/

#![cfg_attr(feature = "libfuzzer", no_main)]

use std::collections::HashMap;

use mesh_loader::obj::read_mtl;

#[cfg(any(
not(any(feature = "libfuzzer", feature = "afl")),
all(feature = "libfuzzer", feature = "afl"),
))]
compile_error!("exactly one of 'libfuzzer' or 'afl' feature must be enabled");

#[cfg(feature = "libfuzzer")]
libfuzzer_sys::fuzz_target!(|bytes: &[u8]| {
run(bytes);
});

#[cfg(feature = "afl")]
fn main() {
afl::fuzz!(|bytes: &[u8]| {
run(bytes);
});
}

fn run(bytes: &[u8]) {
let _result = read_mtl(bytes, None, &mut vec![], &mut HashMap::new());
}
41 changes: 41 additions & 0 deletions fuzz/obj.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
Run with libFuzzer:
```sh
cargo fuzz run --release --features libfuzzer obj
```
Run with AFL++:
```sh
cd fuzz
cargo afl build --release --features afl
cargo afl fuzz -i seeds/obj -o out target/release/obj
```
*/

#![cfg_attr(feature = "libfuzzer", no_main)]

use mesh_loader::obj::from_slice;

#[cfg(any(
not(any(feature = "libfuzzer", feature = "afl")),
all(feature = "libfuzzer", feature = "afl"),
))]
compile_error!("exactly one of 'libfuzzer' or 'afl' feature must be enabled");

#[cfg(feature = "libfuzzer")]
libfuzzer_sys::fuzz_target!(|bytes: &[u8]| {
run(bytes);
});

#[cfg(feature = "afl")]
fn main() {
afl::fuzz!(|bytes: &[u8]| {
run(bytes);
});
}

fn run(bytes: &[u8]) {
let _result = from_slice::<Vec<u8>, _>(bytes, None, |_| panic!());
}
9 changes: 9 additions & 0 deletions fuzz/seeds/mtl/seed.mtl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#
newmtl name
Ka 0.1 0.2 1.0
Kd 0.9 0.8 0.0
Ks 0.3 0.5 0.7
d 1.0
Ns 0.0
illum 2
map_Kd .\texture.jpg
13 changes: 13 additions & 0 deletions fuzz/seeds/obj/seed.obj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#
mtllib usemtl.mtl

g group

v 0.0 -0.0 1.0

vt -0.1 0.5 -1.0

vn 1.0 0.0 -0.6

usemtl mtl
f 1/1/1 1/1/1 1/1/1
4 changes: 2 additions & 2 deletions src/collada/iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::{
slice,
};

use crate::{collada as ast, Vec3};
use crate::{collada as ast, Vec2, Vec3};

impl ast::Document {
pub(super) fn meshes(&self) -> Meshes<'_> {
Expand Down Expand Up @@ -334,7 +334,7 @@ struct TexcoordsInner<'a> {
}

impl Iterator for Texcoords<'_> {
type Item = [f32; 2];
type Item = Vec2;

fn next(&mut self) -> Option<Self::Item> {
let inner = self.0.as_mut()?;
Expand Down
16 changes: 11 additions & 5 deletions src/collada/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,30 @@ use std::{

use self::geometry::*;
use crate::{
utils::xml::{self, XmlNodeExt},
utils::{
utf16::decode_string,
xml::{self, XmlNodeExt},
},
Scene,
};

/// Parses meshes from bytes of COLLADA text.
#[inline]
pub fn from_slice(bytes: &[u8]) -> io::Result<Scene> {
from_str(str::from_utf8(bytes).map_err(crate::error::invalid_data)?)
let bytes = &decode_string(bytes)?;
from_str(bytes)
}

/// Parses meshes from a string of COLLADA text.
#[inline]
pub fn from_str(s: &str) -> io::Result<Scene> {
let xml = xml::Document::parse(s).map_err(crate::error::invalid_data)?;
let collada = Document::parse(&xml)?;
Ok(Scene {
meshes: instance::build_meshes(&collada),
})
let meshes = instance::build_meshes(&collada);
let materials = (0..meshes.len())
.map(|_| crate::Material::default())
.collect(); // TODO
Ok(Scene { materials, meshes })
}

// Inspired by gltf-json's `Get` trait.
Expand Down
Loading

0 comments on commit 5382c7e

Please sign in to comment.