Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Library structure #13

Closed
alteous opened this issue Mar 24, 2017 · 55 comments
Closed

Library structure #13

alteous opened this issue Mar 24, 2017 · 55 comments

Comments

@alteous
Copy link
Member

alteous commented Mar 24, 2017

This thread exists as a place to discuss the modules, functions, data structures, and their names included in the library. In version 1.0.0 names must be agreed upon and remain immutable for backward compatibility. Until then, we should experiment as much as possible and find out what works best.

Current structure

gltf

  • The main crate, intended for navigating glTF JSON.
  • Intentionally limited in scope.

gltf-json

  • Child crate for gltf.
  • Exists primarily to reduce compile times for gltf.
  • semver matches that of gltf for convenience but is otherwise unstable.

gltf-importer

  • Reference importer with a simple API.
  • May be used by run times to read from the file system.
  • Handles base 64 decoding and distinguishing binary glTF on behalf of the user.
  • Optionally loads buffers and images (to do).

gltf-utils

  • Utility functions to complement the gltf crate.
  • Addresses cases where access to pre-existing buffer / image data is required.
  • May remain unstable post gltf 1.0.
@bwasty
Copy link
Member

bwasty commented May 19, 2017

I think the main import function could use a variant that takes a std::io::Read for input and uses serde_json::from_reader() for deserializing. This would

  • make reading from a file more efficient (no full copy in memory needed)
  • enable loading models directly from the network (the Response structs of both hyper and reqwest implement Read)

@alteous
Copy link
Member Author

alteous commented May 20, 2017

This is not a bad idea, but there is one caveat, namely the dependence on the directory path for buffer and image data sources. If there is a reasonable workaround for this then I'm 100% with you.

Note that:

@alteous
Copy link
Member Author

alteous commented May 20, 2017

I suppose we could provide a import_binary() function that only imports binary or embedded glTF formats. That would be able to take generic Read.

@bwasty
Copy link
Member

bwasty commented May 20, 2017

I understood the section about streaming to be more about starting rendering before everything is loaded rather then whether files are loaded directly from disk.
Especially since (non-embedded) buffers and images are referenced via relative URIs, which doesn't restrict the scheme to file as far as I see. Although:

Clients are free to support other schemes (such as http://) depending on expected usage.

suggests that file is implicitly the default.

Maybe we could abstract over URIs and have (user defined) implementations for different schemes that handle the actual loading.
My end goal would be that it's possible to compile to WebAssembly and still load any kind of glTF. This might even mean a callback to JavaScript to use the Fetch API asynchronously.

Another benefit of an API that supports this would be that even local buffer/image references could be loaded in parallel for maximum performance.

@alteous
Copy link
Member Author

alteous commented May 21, 2017

I understood the section about streaming to be more about starting rendering before everything is loaded ...

That would make a lot more sense, I misunderstood the meaning of streaming,

Maybe we could abstract over URIs ...

The scope of the crate is growing larger than I originally planned. When you say
abstraction over URIs, do you have anything particular in mind?

My first thoughts were a simple callback with the URI passed as a parameter. The
user would then return a Vec<u8>. This approach seems a bit too simplistic to be future-proof.

My second thoughts were some kind of trait, for example:

trait Reader {
    type Error;
    fn read_buffer(&mut self, buffer: &Buffer) -> Result<Vec<u8>, Self::Error>;
    fn read_image(&mut self, image: &Image) -> Result<Vec<u8>, Self::Error>;
}

The user would then write something like this:

struct Reader;

impl Reader for Reader {
    type Error = ...;
    fn read_buffer(&mut self, buffer: &Buffer) -> Result<Vec<u8>, Self::Error> {
        if buffer.uri().starts_with("file://") {
            // Read from file system
        } else if buffer.uri().starts_with("http://") {
            // Download from internet
        } else {
            // Unsupported scheme
            Err(...)
        }
    }

    fn read_image(&mut self, image: &Image) -> Result<Vec<u8>, Self::Error> {
        // Similar to above
    }
}

fn main() {
    let gltf = gltf::v2::import("Foo.gltf", Reader);
}

This seems like a lot of work on the user's behalf. Perhaps we could provide a
basic synchronous implementation for convenience:

fn main() {
    let gltf = gltf::v2::import("Foo.gltf", gltf::v2::reader::Simple("./dir"));
}

@alteous
Copy link
Member Author

alteous commented May 21, 2017

Third thoughts: The above is no good since the original discussion was about import() taking generic Read. So perhaps the user should write something like this instead:

/// A glTF source. Calling `Read::*()` means reading the .gltf file.
/// `read_buffer()` and `read_image()` are extra required methods needed to read
/// the data referenced by the .gltf file.
trait Source: Read {
    type Error;
    fn read_buffer(&mut self, buffer: &Buffer) -> Result<Vec<u8>, Self::Error>;
    fn read_image(&mut self, image: &Image) -> Result<Vec<u8>, Self::Error>;
}

enum Reader {
    /// glTF hosted on the file system.
    File(PathBuf),

    /// glTF hosted on the world wide web.
    Http(Url),
}

impl Read for Reader {
    ...
}

impl Source for Reader {
    type Error = ...;
    fn read_buffer(&mut self, buffer: &Buffer) -> Result<Vec<u8>, Self::Error> {
        if buffer.uri().starts_with("file://") {
            // Read from FS
        } else if buffer.uri().starts_with("http://") {
            // Download from WWW
        } else {
            // Unsupported scheme
            Err(...)
        }
    }

    fn read_image(&mut self, image: &image) -> Result<Vec<u8>, Self::Error> {
        // Similar to above
    }
}

fn main() {
    let gltf_from_fs = gltf::v2::import(Reader::File("./dir/Foo.gltf"));
    let gltf_from_www = gltf::v2::import(Reader::Http("example.com/Foo.gltf"));
}

The import() function would now look something like this:

pub fn import(source: &mut Source) -> ImportResult {
    let root: Root = {
        let mut data = Vec::new();
        source.read_to_end(&mut data)?;
        serde_json::from_slice(&data)?
    };
    root.source = Some(source);
    Ok(root)
}

Note that we would want dynamic dispatch here or else compile times would be insane.

@bwasty
Copy link
Member

bwasty commented May 21, 2017

That's pretty much what I had in mind, though I hadn't thought it out this far. To avoid the if-else cascade in read_buffer (... there's also the "data" URI case), I thought of several structs, FileReader and HttpReader that would each implement Source.

I wonder though, who would actually call read_buffer/read_image? The import function or the user afterwards? If the latter, even asynchronous/parallel loading could be easily done by the user with a different approach, e.g. traversing all buffers/images collecting URIs, and then using something like the new Async Hyper Client.

Also, where would the (down)loaded data be saved? Extra fields on Buffer/Image or up to the user?

About the compile times: I was already surprised how slow cargo test is after making small changes to the lib - takes about 55seconds. Is that related?

@alteous
Copy link
Member Author

alteous commented May 30, 2017

who would actually call read_buffer/read_image

I've been planning a 'rustic' wrapper for the crate for a while. You can browse the ideas in this new wrapper branch. One of the responsiblities of the wrapper would be to load buffers (example) and images on behalf of the user.

The import function or the user afterwards?

I'd say the import function as the name suggests. The Reader trait would aliow the user to customise data acquisition with the async hyper client or otherwise.

where would the (down)loaded data be saved

This has been a pain point in the wrapper idea because the Rust ownership rules start to get in the way. The current way the wrapper handles this is to preload everything upon creation. This seems like a bad idea though because of blocking I/O, denial of service, et cetera. External data should ideally be lazily loaded but as far as I'm aware this means the user would have to make a place to store the data and pass &mut around, thus removing any opportunities for parallelism.

About the compile times: I was already surprised how slow cargo test is after making small changes to the lib - takes about 55seconds. Is that related?

Maybe. I'd suggest using cargo check if you're just making small changes. I find it difficult to reason about the performance of rustc at the moment.

@bwasty
Copy link
Member

bwasty commented Jun 7, 2017

I'd say the import function as the name suggests. The Reader trait would aliow the user to customise data acquisition with the async hyper client or otherwise.

Sounds like a good default, but I think the Reader trait won't properly work for async loading since it synchronizes the reading operations. The client may load asynchronously internally, but for parallel loading you'd need a different interface I think (I looked a bit into using futures directly, or rayon).

... the Rust ownership rules start to get in the way ... External data should ideally be lazily loaded but as far as I'm aware this means the user would have to make a place to store the data and pass &mut around, thus removing any opportunities for parallelism.

Might RefCell or Arc help? I don't have any experience with the advanced pointer types, but this reminds me a bit of what I read here (paragraph in the middle).

On the other hand, perhaps it would be better to leave some of the details to the user and just provide basic helper functions and structs. For example, lazy loading could mean different things:

  • load data for individual nodes when they should be rendered (or first accessed)
  • load everything related to the current scene
  • do frustum culling and load everything visible (I know, sounds like streaming, but the spec also says the binary data in glTF is inherently streamable and the min/max properties on accessors should make this easy)

I'd suggest using cargo check if you're just making small changes.

Even better is the RLS (though it was broken in nightly unfortunately when I was working on gltf...)

@alteous
Copy link
Member Author

alteous commented Jun 8, 2017

Reader trait won't properly work for async loading since it synchronizes the reading operations

This is a very good point. If an import_async function was provided I'm not sure how the data would be lazily-loaded, even with futures - I'll have a think about it. For the 1.0.0 release it would be worth just concentrating on the synchronous case.

Might RefCell or Arc help?

Let's not go there if possible. It would solve some problems but also create new problems, for example making sure there is only one writer at runtime.

Even better is the RLS ...

Thanks, I'll check that out.

@alteous
Copy link
Member Author

alteous commented Jun 8, 2017

Some more points:

load data for individual nodes when they should be rendered (or first accessed)

There are likely to be many nodes, and if they're all reading little segments from say the file system then I doubt that would perform well, but that's only speculation.

load everything related to the current scene

There is a further point to be raised here: Since glTF can contain multiple scenes then perhaps reading the .gltf / .glb file and loading the referenced data should be two separate operations. For example:

// Calls `serde_json::from_whatever` and performs some validation
fn import(path: Path) -> Gltf { ... }

impl Scene {
    // Loads the scene data
    //
    // N.B. taking 'out' parameters is not recommended by the Rust API guidelines
    // but `std::io::Read` does it for reasons similar to one's were facing
    fn load(dest: &mut SceneData) -> LoadResult<()> { ... }
}

On the other hand, perhaps it would be better to leave some of the details to the user and just provide basic helper functions and structs.

I used to have this mindset but then I began to think if the user has this burden then they are unlikely (at least in the short term) to adopt glTF into their project. The wrapper exists alongside the 'raw' data structures to allow any user to do their own thing if they really want to.

do frustum culling and load everything visible (I know, sounds like streaming, but the spec also says the binary data in glTF is inherently streamable and the min/max properties on accessors should make this easy)

That sounds like a optimisation that's out of scope for the 1.0.0 release however it may be an interesting long-term goal.

@bwasty
Copy link
Member

bwasty commented Jun 10, 2017

There are likely to be many nodes, and if they're all reading little segments from say the file system then I doubt that would perform well, but that's only speculation.

I'd expect that if there's only one .bin file as in all the examples I've seen so far, only the first node accessed would read from the file system, later ones would get the data from memory.

Though I also like the separate scene loading interface.

@alteous
Copy link
Member Author

alteous commented Jun 16, 2017

Here's a draft of the new import strategy. I'm still not sure how to handle async yet.

@bwasty
Copy link
Member

bwasty commented Jun 18, 2017

Looks/works good so far. Two comments:

  • is there a meshes function missing on Gltf? I tried let mesh = &gltf.meshes().nth(0);, since that worked with buffers.
  • The .glb format is not an extension anymore, so the error type should not be ExtensionUnsupported

@alteous
Copy link
Member Author

alteous commented Jun 18, 2017

Both the mesh iterator on Gltf and the .glb format is unintentionally missing. I'll fix this very soon.

I'm pleased to hear the wrapper is working well.

@bwasty
Copy link
Member

bwasty commented Jun 25, 2017

Finally got to a first successful render of Box.gltf in my viewer (took a while since I started this in the mean time to get toward a PBR renderer). So here are some more comments:

  • The position/normal/tex_coord iterators on gltf::mesh::Primitive are really convenient, much nicer/shorter than my previous attempt
    • nitpick: I was a bit confused for a moment that they're singular. It makes sense when it's called "position attribute", but without "attribute" in the name I'd find it much more intuitive in the plural.
  • I've run into some missing Debug implementations on some things I tried:
println!("pos: {:?}", primitive.position());
assert_eq!(primitive.mode(), Mode::Triangles);

I'll try writing a network-based Source as soon as I have some basic geometry/texture handling done.

@bwasty
Copy link
Member

bwasty commented Jun 25, 2017

Another thing: loading minimal.gltf fails with
Error: Io(Error { repr: Os { code: 2, message: "No such file or directory" } })
I guess it's because data URIs are not supported yet for buffers (removing it causes JSON validation errors), but it would be nice if the error contained the file name.

Perhaps we could track somewhere what already works/what is missing?

@alteous
Copy link
Member Author

alteous commented Jun 25, 2017

Finally got to a first successful render of Box.gltf in my viewer

That's wonderful news! It's great to see the crate in action.

The position/normal/tex_coord iterators on gltf::mesh::Primitive are really convenient, much nicer/shorter than my previous attempt

They are pretty nifty if I may say so myself, and especially so if used with multizip.

nitpick: I was a bit confused for a moment that they're singular. It makes sense when it's called "position attribute", but without "attribute" in the name I'd find it much more intuitive in the plural.

The only reason why it's named this way is because then it matches the attribute name in the specification. I completely agree that they're confusing, so let's change them to plurals.

I guess it's because data URIs are not supported yet for buffers

It's because the URI contains embedded base64 data, which is at the top of my 'to do' list to implement.

Perhaps we could track somewhere what already works/what is missing?

That's a good idea. I think it would be best to do this using GitHub issues with custom tags. Many Rust projects seem to do this successfully.

I'll try writing a network-based Source as soon as I have some basic geometry/texture handling done.

If you have the time, feedback would be appreciated.

@bwasty
Copy link
Member

bwasty commented Jun 25, 2017

... especially so if used with multizip.

Interesting alternative, I wanted to use itertools::izip!.

@alteous
Copy link
Member Author

alteous commented Jun 25, 2017

It's almost the same.

@alteous
Copy link
Member Author

alteous commented Jun 25, 2017

By the way there's a new version of the wrapper interface. GLB is supported now - the implementation was much more difficult to write than I'd expected.

@bwasty
Copy link
Member

bwasty commented Jun 25, 2017

Perhaps we could track somewhere what already works/what is missing?

That's a good idea. I think it would be best to do this using GitHub issues with custom tags. Many Rust projects seem to do this successfully.

My first Idea was a single issue with a few checkboxes.

GLB is supported now

Nice!

@alteous
Copy link
Member Author

alteous commented Jun 25, 2017

My first Idea was a single issue with a few checkboxes.

How about an issue with a list of all the things that are in a working state? The rest can have individual issues so that we can discuss the details easily.

@bwasty
Copy link
Member

bwasty commented Jun 26, 2017

Found another surprising thing: I wanted to return a Gltf from a loading function, but couldn't because the liftetime was bound to the Importer instance (which was local to the function). Looking at the code I understand why, but I'd naively expect that Gltf owns all the data and can be moved.

@bwasty bwasty mentioned this issue Jun 26, 2017
2 tasks
@alteous
Copy link
Member Author

alteous commented Jun 26, 2017

This is a good point. Until now I've believed that simply returning Gltf<'static> would suffice but this is clearly not the case. If Gltf owned its buffer and image data instead of borrowing it from an Importer then my original supposition should hold, i.e. you should be able to return Gltf<'static> without the borrow checker grumbling. I'll make an update on this tomorrow.

@alteous
Copy link
Member Author

alteous commented Jun 27, 2017

Bad news

I was wrong about returning Gltf<'static> from Importer. The reason is illustrated below:

impl Owner {
    fn borrow<'a>(&'a self) -> Borrowed<'a> {
        // self is borrowed even if 'a is the 'static lifetime
    }
}

Good news

This problem was easily solved by instead having two flavours of the importer - I called them Static and ZeroCopy. The changes are in commit alteous@ef828be.

You should be able to return a Gltf<'static> now from a function using a StaticImporter.

@alteous
Copy link
Member Author

alteous commented Jun 27, 2017

Also, Gltf now owns its buffer and image data - see alteous@2362574 for details.

@bwasty
Copy link
Member

bwasty commented Jun 30, 2017

With the increasing number of possible options, it might be worth considering the builder pattern. Something along the lines of:

let importer = ImporterBuilder::new()
   .source(NetworkSource::new("http://example.com/foo.gltf")
   .load_binary_data(true), // with `false`, only load the json, leave the rest to the user
   .mode(Mode::Async) // or ZeroCopy
   .lazy_loading(Lazy::Off) // or Scene/All
   .validation(Validation::Complete) // or Minimal
   .generate_missing(Semantic::Normals) // can be repeated with Semantic::Tangents
   .build();

This might also make it easier to add new options later without breaking the API.

@bwasty
Copy link
Member

bwasty commented Jul 29, 2017

Issues #21 and #22 would be easier with a math library. Could we (optionally?) add cgmath for those?
Some simple functions such as the cross product (for normals) or a matrix determinant (for matrix validation) could also be implemented here, but things like decomposing into/re-composing from T/R/S components seems a bit much.

Another idea: the Node wrapper could directly return cgmath types. Not sure it's a good idea actually, but having to parse them for my own Node struct was a bit annoying since it wasn't entirely trivial.

@alteous
Copy link
Member Author

alteous commented Jul 29, 2017

Could mint be useful for returning math types?

@alteous
Copy link
Member Author

alteous commented Jul 29, 2017

Did you try node.matrix().into()? (etc.)

@alteous
Copy link
Member Author

alteous commented Jul 29, 2017

We could make functions more generic, maybe?

impl Node {
    fn matrix<T>(&self) -> T
        where T: From<[f32; 4]>
    {
        self.json.matrix.into()
    }
}

@bwasty
Copy link
Member

bwasty commented Jul 29, 2017

Could mint be useful for returning math types?

It could be if it was actually integrated with the libraries - kvark/mint#11 suggests that it's not very successfull so far. I also have the feeling that most people now use cgmath for CG.

Did you try node.matrix().into()?

Yes, same message as for Matrix4::from: the trait `std::convert::From<[f32; 16]>` is not implemented for `cgmath::Matrix4<f32>. According to the docs it should work as far as I understand, but it doesn't somehow...

@alteous
Copy link
Member Author

alteous commented Jul 29, 2017

It seems there is an impl<'a, S> From<&'a [S; 16]> for &'a Matrix4<S> but not an impl<S> From<[S; 16]> for Matrix4<S> which is bizarre.

@alteous
Copy link
Member Author

alteous commented Aug 4, 2017

The crate received some valuable criticism recently on the #rust-gamedev IRC channel. Below is a summary of the comments received.

  1. @tomaka criticised the use of futures due to compatibility and dependency issues when versions mismatch. This is a known issue but not one the crate has ever fully acknowledged.
  2. @tomaka and @Ralith criticised the fact that the crate performs I/O as well as parsing and providing abstractions over data. I don't see this as a huge problem, as long as it's optional and there is a reasonable alternative available, but the way the crate is organised makes users believe such an alternative does not exist.
  3. @Ralith heavily criticised the fact that gltf performs image decoding, arguing that it should not the job of gltf to perform this task at all. I am inclined to agree since it has been a source (heh) of many problems with minimal benefit.
  4. @Ralith also briefly mentioned the problem of 'how to deal with extensions'. The current approach is to support official extensions as and when they are required, relying on pull requests to implement the relevant functionality. This a simple but perhaps inflexible approach.
  5. My impression from the discussion was that potential users are, first and foremost, interested in a glTF parser, and are irritated by the way the crate is trying to do 'too much' on their behalf. Although it is possible to use gltf simply as a JSON parser this is currently far from obvious.

To address these criticisms, I propose the following.

  • Split the crate into smaller sub-crates that perform specialised tasks, for example, JSON parsing, glTF tree iteration, and buffer / image data importing. This would incur:
    • Moving the json module into its own gltf-json crate.
    • Moving the import module into its own gltf-importer crate.
  • Change the strategy Gltf (the wrapper type) receives its buffer data.
  • Remove image_data from Gltf entirely.
  • Outside of gltf-importer, leave I/O completely up to the user.

I encourage further discussion.

@vitvakatu
Copy link

@alteous
I'm outsider for this project, but I interested in it, as gltf is going to be a de facto standard format for modern 3d applications.
I like your proposal about splitting the crate into several sub-projects, I hope it would be hard.

Btw, mint has been integrated into cgmath successfully. I can take care of creating a PR for gltf if you don't mind.

@alteous
Copy link
Member Author

alteous commented Aug 4, 2017

@vitvakatu Thanks for your interest!

Btw, mint has been integrated into cgmath successfully. I can take care of creating a PR for gltf if you don't mind.

Of course. In case I've misunderstood, would the intention be to replace vector types with mint types, i.e. [f32; 3] -> Vector3<f32> etc?

@vitvakatu
Copy link

@alteous only public APIs will be changed, yes, [f32; 3] would be replaced with mint::Vector3<f32> or mint::Point3<f32>.
I made an issue for discussing and tracking progress #67

@Ralith
Copy link
Contributor

Ralith commented Aug 5, 2017

I also have the feeling that most people now use cgmath for CG.

I can't speak for everyone, but I am very happy with nalgebra.

Change the strategy Gltf (the wrapper type) receives its buffer data.

I'm not deeply familiar with the current API (or glTF itself for that matter) but (as alluded to in IRC) it's not at all obvious to me that the root Gltf type needs to have access to buffer data at all, any moreso than it needs access to image data. Anywhere the JSON parser currently yields a struct containing a &Gltf and an index referencing to some binary data contained within it, the parser could instead yield the index alone and let the user worry about constructing and filling in a table of buffers/views/accessors/images/meshes/etc in whatever manner they like, if indeed they want to at all. So long as the parser provides access to the information necessary to load indirectly referenced data, this should be straightforward to use. You could even provide a helper that just loads everything up front into a simple struct for simple usecases and demos.

If you want to continue to provide friendly accessors like gltf::mesh::Mesh, you'd just replace fn new(gltf: &'a Gltf, index: usize, json: &'a Mesh) -> Self with something like fn new(accessor: &'a Accessor<'a>, json: &'a Mesh) -> Self where Accessor is constructed from a &'a [u8] (taking the place of a buffer view) and a gltf::json::accessor::Accessor.

@bwasty
Copy link
Member

bwasty commented Aug 5, 2017

About splitting into sub-crates: gltf-json and gltf-import sound reasonable. But we should try not to create too many sub-crates and make sure each is useful on its own. For example, gltf-derive doesn't seem useful on it's own and ideally shouldn't show up on crates.io.

Apart from that, I agree with the general direction. I personally like the convenience the crate provides beyond just parsing, but I have no problem moving some of that to my viewer/engine.

@alteous
Copy link
Member Author

alteous commented Aug 5, 2017

I personally like the convenience the crate provides beyond just parsing

As do I. I'd like to redesign the way the wrapper library receives data so that we still have these conveniences. One idea I had this morning was to introduce a Load trait that operates on a Source. The user could then choose how much data is prepared. The

trait Source {
    /// Customisable error type - reference implementation provided by `gltf-importer`.
    type Error;

    /// Returns `Ok(())` if the data is prepared. The data should be loaded by the `Source`
    /// implementation if unavailable, returning `Err(...)` if the data cannot be sourced.
    ///
    /// The user is expected to persist the buffer for the lifetime of the loaded data.
    /// Hopefully the borrow checker can guarantee this for us.
    fn prepare(&mut self, buffer: usize) -> Result<(), Self::Error>;

    /// For the wrapper library to retrieve buffer data without taking `&mut`.
    fn retrieve<'a>(&'a self, buffer: usize) -> &'a [u8];
}

/// Internal trait for converting `Unloaded<T>` to `Loaded<T>`.
trait Load {
    /// The loaded result type.
    type Loaded;
    /// Recurses down the hierarchy, calling `prepare()` on every item that will need to be
    /// ready for the wrapper.
    fn load<S: Source>(self, source: &mut S) -> Result<Self::Loaded, S::Error>;
}

/// Wrapper type for items that may not have their data ready.
struct Unloaded<T: Load> {
    item: T,
}

impl<T: Clone + Load> Load for Unloaded<T> {
    type Loaded = T::Loaded;
    fn load<S: Source>(self, source: &mut S) -> Result<Self::Loaded, S::Error> {
        /// Recurse down the hierarchy, loading everything that's needed from hereon.
        self.item.clone().load(source)
    }
}

@alteous
Copy link
Member Author

alteous commented Aug 5, 2017

The advantage of this idea is that you can call load() on, say, a Node, and it will prepare all the data necessary for that Node and all of its children to be rendered - the user need only call load() once. But, if the user wants more fine-grained control over what's loaded, they can call load() on smaller items like Mesh or Skin. The Unloaded version of a struct doesn't have convenience features like attribute iteration.

@alteous
Copy link
Member Author

alteous commented Aug 5, 2017

gltf-derive doesn't seem useful on it's own and ideally shouldn't show up on crates.io

Unfortunately it has to be in its own crate because it's a proc_macro. I tried packaging it up with gltf but crates.io did not like that at all. It's not a huge issue and lots of other crates do this - scan for 'internal' crates.

@alteous
Copy link
Member Author

alteous commented Aug 5, 2017

Actually, I think this idea is overly complex. A much simpler approach might be to allow the user to 'query' what data is needed on individual objects, let the user source the data themselves, and provide the data back to the object. This pseudo-code should briefly explain the idea:

/// A source of buffer data.
trait Source {
    /// N.B. this *must not* fail - the user is expected to
    /// ensure that all the data is available.
    fn source_buffer<'a>(&'a self, buffer: Buffer<'a>) -> &[u8];
}

/// New: Wrapper type for a glTF object with loaded data.
struct Loaded<'a, T> {
    /// The wrapper item - `Mesh` in this example.
    item: T,

    /// User data that (hopefully) contains all the data needed by the item
    /// and all of its children.
    source: &'a Source,
};

/// Same as before.
struct Mesh<'a> {
    gltf: &'a Gltf,
    json: &'a json::Mesh,
    index: usize,
}

impl Mesh {
    /// Returns an `Iterator` over the mesh primtives, none
    /// of which have any data.
    fn primitives(&self) -> Primitives<'a> {
        ...
    }

    /// Returns a `Vec` of all the buffers required by the
    /// user to be loaded.
    fn query(&self) ->Vec<Buffer> {
        vec![Buffer(3), Buffer(1), Buffer(4)]
    }
}

impl Loaded<Mesh> {
    /// Returns an `Iterator` over the mesh primitives, *all* of
    /// which have their data, provided the user initialised the
    /// buffer source correctly.
    fn primitives(&self) -> LoadedPrimitives<'a> {
        LoadedPrimitives {
            iter: self.json.iter(),
            // All the child objects of `Loaded<Mesh>` get their data too!
            // Imagine how flexible this could be with say, individual nodes.
            source: self.source,
        }
    }
}

struct UserSourceImplementation {
    /// For example.
    buffers: Vec<Vec<u8>>,
}

impl Source for UserSourceImplementation {
    fn source_buffer<'a>(&'a self, buffer: Buffer<'a>) -> &[u8] {
        &self.buffers[buffer.index()]
    }
}

enum Gltf {
    Standard(Gltf),
    Binary(Gltf, Vec<(u32, Vec<u8>)>),
}

fn main() {
    let mut source = UserSourceImplementation;
    let gltf = match gltf::parse(include_bytes!("example.glb")) {
        Gltf::Standard(gltf) => gltf,
        Gltf::Binary(gltf, embedded_buffers) => {
            source.store(embedded_buffers);
            gltf
        },
    };
    let unloaded_mesh = gltf.meshes().nth(0).unwrap();
    source.load_buffers(unloaded_mesh.query());
    let mesh = unloaded_mesh.load(&source);
    for loaded_primitive in mesh.primitives() {
        let indices = loaded_primitive.indices_u32().unwrap().collect();
        // you get the picture...
    }
}

@alteous
Copy link
Member Author

alteous commented Aug 5, 2017

Also, we can make query a trait method and use the same trick as Validate to recurse through the heirarchy.

@Ralith
Copy link
Contributor

Ralith commented Aug 5, 2017

That looks similar to what I had in mind. However, I'd still much rather do away with the Source trait entirely and have the parser just yield indexes for everything, and let the user construct Loaded<T> from a specific buffer (not an entire library of buffers) at the last possible moment--no implicit assumptions about entire subtrees being fully available. For example, I wouldn't want to be required to load buffers used only for extensions I don't implement, or data which is irrelevant to a processing task I'm implementing

@alteous
Copy link
Member Author

alteous commented Aug 6, 2017

let unloaded_mesh = gltf.meshes().nth(0).unwrap();
source.load_buffers(unloaded_mesh.query());
let mesh = unloaded_mesh.load(&source);

Good news: this can be done in one line!

Contents of example:

struct Loader {
    data: Vec<u8>,
}

#[derive(Debug)]
struct LoadError;

trait Source {
    fn source(&self, buffer: usize) -> &[u8];
}

trait Query {
    fn query(&self) -> Vec<usize>;
}

struct Item {
    required_buffer: usize,
}

impl Query for Item {
    fn query(&self) -> Vec<usize> {
        vec![self.required_buffer]
    }
}

struct Loaded<'a, T> {
    item: T,
    source: &'a Source,
}

impl<'a> Loaded<'a, Item> {
    fn print(&self) {
        println!("{:?}", self.source.source(self.item.required_buffer));
    }
}

impl Loader {
    fn load<T: Query>(&mut self, item: T) -> Result<Loaded<T>, LoadError> {
        for buffer in item.query() {
            self.data.push(buffer as u8);
        }
        Ok(Loaded {
            item,
            source: self,
        })
    }
}

impl Source for Loader {
    fn source(&self, _buffer: usize) -> &[u8] {
        self.data.as_slice()
    }
}

fn main() {
    let mut loader = Loader { data: vec![] };
    let unloaded_item = Item { required_buffer: 123 };
    let item = loader.load(unloaded_item).expect("loading error");
    item.print();
}

@alteous
Copy link
Member Author

alteous commented Aug 6, 2017

The above decouples buffer loading from tree traversal. The Loader type is nothing special - a similar implementation could be written the user if specialisation is required. The crate could provide a reference implementation that covers the most common cases (reading from fs) enabled via a cargo feature.

@alteous
Copy link
Member Author

alteous commented Aug 6, 2017

the user construct Loaded from a specific buffer

glTF data can come from many buffers. For example, a Primitive may read from a buffer containing geometry and a separate buffer containing joints and weights.

I wouldn't want to be required to load buffers used only for extensions I don't implement

Extensions have always been opt-in via cargo features, so one pays for only what is used. There are no extensions implemented at the moment.

processing task I'm implementing

Forgive me for being nosy, but what is it you're trying to achieve? glTF is intended to be the 'jpeg of 3D', with the further intention of replacing proprietary asset formats with its own extensible, open format. Hence, supporting conversion from glTF to proprietary formats is not something the crate was ever intended to achieve.

@Ralith
Copy link
Contributor

Ralith commented Aug 6, 2017

glTF data can come from many buffers. For example, a Primitive may read from a buffer containing geometry and a separate buffer containing joints and weights.

Ah, that clarifies the reasoning behind the direction you're taking things. I especially like the Query trait, though I'd consider returning vec::IntoIter instead of Vec in preparation for eventually replacing it with impl Iterator and removing the need to allocate at all. You could also do something like fn query<F: FnMut(usize)>(&self, f: F) but it's probably not worth the decreased ergonomics.

The Source trait looks exactly equivalent to std::ops::Index<usize> with type Output=[u8]. Is there really a need for something distinct?

Extensions have always been opt-in via cargo features

How are non-standard extensions (which nearly every standard extension starts out as) going to be supported, then? I'm not a big fan of forcing people to fork the library.

Forgive me for being nosy, but what is it you're trying to achieve?

It was intended as a vague example. A more concrete example might be a tool to extract individual nodes, or a visualizer (or even game engine!) that only loads buffers associated with data that actually needs to be displayed, which, given the "asset library" usecase glTF explicitly supports, should frequently not be "all of them." Even if I am using all data in a glTF file, I'd prefer to be able to stream it in continuously (ordered by visibility, say) rather than as a single monolithic blob.

Hence, supporting conversion from glTF to proprietary formats is not something the crate was ever intended to achieve.

I do, however, fully intend to convert from glTF to a format of my own. Not because I have some weird political requirement to use proprietary formats, but because my internal format has a fundamentally different architecture than glTF does--it's a fine-grained content-addressed store, not to mention that it encodes a variety of application-specific data in addition to geometry and textures.

I'm a fan of glTF's objectives and hope to support it as fully as possible, but it should be no surprise that it's not the perfect terminal format for literally every application.

@alteous
Copy link
Member Author

alteous commented Aug 9, 2017

I've abandoned the idea of the Query trait for now because one can achieve the same goal without it. Currently, the reference loader just loads everything, guaranteeing all the required data to be available.

@alteous
Copy link
Member Author

alteous commented Aug 23, 2017

Updated the OP.

@alteous
Copy link
Member Author

alteous commented Sep 11, 2017

The names of some animation data structures are quite verbose. I suggest we rename InterpolationAlgortihm as Interpolation, TrsProperty as Property and rename animation::Target::path as animation::Target::property to match.

@alteous
Copy link
Member Author

alteous commented Sep 13, 2017

KhronosGroup/glTF#1100 suggests we shouldn't be returning #bin from Buffer::uri.

@alteous alteous mentioned this issue Sep 22, 2017
@alteous
Copy link
Member Author

alteous commented Oct 22, 2017

Moving discussion over to #12

@alteous alteous closed this as completed Oct 22, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants