kvark / obj

Basic Wavefront OBJ loader
Apache License 2.0
29 stars 12 forks source link

Refactor and writer #15

Closed elrnv closed 4 years ago

elrnv commented 4 years ago

This PR is unfortunately very large, and makes a bunch of improvements (IMO) as well as adding a writer for .obj as well as .mtl files.

I am open to making changes or reintroducing some of the code/APIs that have been removed here if need be.

Below is a summary of noteworthy changes.

Edit: Additional changes followed from the discussion:

elrnv commented 4 years ago

Fixes #5

kvark commented 4 years ago

Oh wow, grand work here! I'll try to review later today.

elrnv commented 4 years ago

Thank you :) I had a lingering question about having all the Obj data be generic over polygons. Is this strictly necessary? My impression was that the data format in these import/export formats should just be optimal for reading and writing since there is no way to predict how the user will want their polygons represented anyways (could be in structure of arrays format for instance).

The reason I ask is I think it would be nicer to have Obj have no generic parameter to make it easier to use. For example loading a file, I think should look like:

let obj = Obj::load(&Path::new("file.obj"))?;

but we currently need

let obj = Obj::<SimplePolygon>::load(&Path::new("file.obj"))?;

or with this PR

let obj: Obj = Obj::load(&Path::new("file.obj"))?;

since SimplePolygon was made default. If the idea was to just switch between SimplePolygon and use genmesh, we could still do it without the generic parameter, but just with conditional compilation...

What do you think?

PS: I could also add an impl AsRef<Path> for the load and save arguments to make it even less verbose to use those functions.

kvark commented 4 years ago

If the idea was to just switch between SimplePolygon and use genmesh, we could still do it without the generic parameter, but just with conditional compilation...

I don't think this will work well. Features are supposed to be additive, and if I understand your suggestion, it will change the signature of functions based on the genmesh feature...

elrnv commented 4 years ago

This looks just wonderful, although I haven't tried to carefully inspect all the logic. I assume it works for some cases, and I'm happy to see the tests!

Thanks I wasn't expecting this to be reviewed so soon since there are quite a few changes. I think the most questionable logic is in the WriteToBuf impl for Group, which reads

        // When index is greater than 0, we know that this group is the same as the previous group,
        // so don't bother declaring a new one.
        if self.index == 0 {
            writeln!(out, "g {}", self.name)?;
        }

So I assume here that the increased index will always indicate that this group should be merged with the previous when writing.

The biggest thing I would propose is, since you are basically rewriting the whole thing, that we refresh the formatting. We'd want to do the stable rustfmt, so we'll need to clear the config of all the things that are nightly-only. All of that, of course, needs to be in a separate commit.

Would you want to do this? Or leave for somebody to follow up?

Thanks! I would be happy to do that. Should we add fmt and clippy to CI as well? I can simply run rustfmt for this PR and add another PR for the CI additions.

kvark commented 4 years ago

It would be welcome to add clippy, we can do it here or as a follow up. It's not implying as big of a change as formatting :)

So I assume here that the increased index will always indicate that this group should be merged with the previous when writing.

The knowledge of the OBJ details is completely out of my head right now, so I can't validate this without doing a small research. Do you think we could turn that assumption into an assertion?

elrnv commented 4 years ago

If the idea was to just switch between SimplePolygon and use genmesh, we could still do it without the generic parameter, but just with conditional compilation...

I don't think this will work well. Features are supposed to be additive, and if I understand your suggestion, it will change the signature of functions based on the genmesh feature...

I see that makes sense. What is the benefit of using genmesh Polygons here instead of just SimplePolygon? Could we simply have one or the other?

elrnv commented 4 years ago

FYI, beyond the things we discussed, I sneaked in the impl AsRef<Path> in the load/save fns I was talking about earlier.

kvark commented 4 years ago

What is the benefit of using genmesh Polygons here instead of just SimplePolygon? Could we simply have one or the other?

You could use genmesh-provided utilities, like LruIndexer, without an extra conversion step. I was doing this in https://github.com/three-rs/three/ and quite enjoyed the integration.

elrnv commented 4 years ago

You could use genmesh-provided utilities, like LruIndexer, without an extra conversion step. I was doing this in https://github.com/three-rs/three/ and quite enjoyed the integration.

I see that makes sense, thanks for the example! Looking briefly at that code, if we were to simply provide a converter from SimplePolygon to Polygon<IndexTuple>, for instance with:

#[cfg(feature = "genmesh")]
impl SimplePolygon  {
    fn into_genmesh_poly(self) -> Polygon<IndexTuple> {
        std::convert::TryFrom::try_from(self).unwrap()
    }
}

#[cfg(feature = "genmesh")]
impl std::convert::TryFrom<SimplePolygon> for Polygon<IndexTuple> {
    type Error = ObjError;
    fn try_from(gs: SimplePolygon) -> Result<Polygon<IndexTuple>, ObjError> {
        match gs.0.len() {
            3 => Ok(Polygon::PolyTri(Triangle::new(gs.0[0], gs.0[1], gs.0[2]))),
            4 => Ok(Polygon::PolyQuad(Quad::new(gs.0[0], gs.0[1], gs.0[2], gs.0[3]))),
            n => {
                return Err(ObjError::GenMeshTooManyVertsInPolygon {
                    vert_count: n,
                })
            }
        }
    }
}

then the only real code change this would require in the three-rs example is the addition of

.map(|p| Polygon::try_from(p).unwrap())

when iterating over the polys.

The other alternative is to make SimplePolygon a non-exhaustive enum. This will at least push the concern down to where the user needs to deal with polygons.

I made a prototype based on this PR for the first variation above in https://github.com/elrnv/obj/tree/non-generic-obj. Let me know what you think.

kvark commented 4 years ago

Ok, I think this is convincing enough, thank you! We can proceed with your plan then :)

elrnv commented 4 years ago

Looks like it's good to go, thanks for all the feedback!

Edit: I have the patch for three-rs ready here: https://github.com/elrnv/three/tree/update-obj

kvark commented 4 years ago

Alright, time to land this!