Add SaveAll method for OBJ files - #39
Conversation
|
Hey! Thanks for this. I think we're starting to get at a larger issue here and I want your opinion. PrefaceHonestly, I haven't been happy with the OBJ package API (and most formats) for a while. OBJ was the first format I implemented in polyform years ago, and it's probably the ugliest to work with. Being OBJ was the first format, it also has had the largest influence on the material API in polyform, which is also not amazing to work with. I've kept polyform in Problems with Materials
SolutionDelete the concept of materials right out of the Mesh, and defer implementation to the format in question. So now you'll be dealing with a Problem with Format APICurrently, the API has no cohesion from format to format. A bunch of common terms are used, Below is an overview of the current state of affairs; // PLY ===========================================================================
ply.Save(plyPath string, meshToSave modeling.Mesh, format ply.Format) error
ply.Write(out io.Writer, model modeling.Mesh, format ply.Format) error
ply.Load(filepath string) (*modeling.Mesh, error)
ply.ReadMesh(in io.Reader) (*modeling.Mesh, error)
// GLTF ==========================================================================
gltf.Save(modelPath string, scene gltf.PolyformScene) error
gltf.SaveBinary(gltfPath string, scene gltf.PolyformScene) error
gltf.SaveText(gltfPath string, scene gltf.PolyformScene) error
gltf.WriteBinary(scene gltf.PolyformScene, out io.Writer) error
gltf.WriteText(scene gltf.PolyformScene, out io.Writer) error
// OBJ ===========================================================================
obj.Save(objPath string, meshToSave modeling.Mesh) error
obj.WriteMesh(m modeling.Mesh, materialFile string, out io.Writer) error
obj.WriteMeshes(meshes []obj.ObjMesh, materialFile string, out io.Writer) error
obj.WriteMaterial(mat modeling.Material, out io.Writer) (err error)
obj.WriteMaterials(m modeling.Mesh, out io.Writer) error
obj.Load(objPath string) ([]obj.ObjMesh, error)
obj.ReadMaterials(in io.Reader) ([]modeling.Material, error)
obj.ReadMesh(in io.Reader) ([]obj.ObjMesh, []string, error)
// STL ===========================================================================
stl.Save(fp string, m modeling.Mesh) error
stl.Write(out io.Writer, bin stl.Binary) error
stl.WriteMesh(out io.Writer, m modeling.Mesh) error
stl.Load(fp string) (*modeling.Mesh, error)
stl.ReadMesh(in io.Reader) (*modeling.Mesh, error)SolutionGenerally, formats have extra data associated with them that don't fit into a mesh. STL has an 80 byte header, OBJ has materials, GLTF has a ton of stuff, and PLY has comments and obj_info. This means that no matter what, signatures are bound to be different in some capacity. To allow developers to ignore the format details, we supply an API serving as our "best guess" for translating data into a single polyform mesh. This "best guess" will inevitably fall short for use cases that require context around the mesh that the format in question provides, and in those scenarios, the developer will then need to use the format-specific APIs to resolve those features. The general purpose interface that every format needs to implement is as follows: type Format {
ReadMesh(io.Reader) (modeling.Mesh, error)
WriteMesh(io.Writer, modeling.Mesh) error
LoadMesh(path string) (modeling.Mesh, error)
SaveMesh(path string, modeling.Mesh) error
}We then reserve the function names ConclusionI'm not asking you to implement any of these changes in your PR. I just wanted to let you know what's probably to come and get your thoughts since you are the first person I know who has been digging into the format API. After learning of this, if you're still happy with your PR, I will happily merge it. Just be aware I might mess with the API in the coming months. |
|
Hi, sure. To give a bit of context on what I'm doing and why I'm digging into this. I don't actually know much about 3D modelling, and this is not my area of expertise. Triangle meshes and RGB colours make sense to me intuitively, but the math behind them - not so much. Fixing minor interface issues and using what's already there is about the top of my current abilities. I'm working on a pet project for a CAD system of sorts and I need a visualization layer that is able to output 3D models in a format that I can then render in-browser. I picked OBJ because it ticked my boxes (simple to understand, has libraries to work with, supports coloring), but I don't know enough in this area to make a properly competent choice. And I've not looked at the APIs of other formats at all, since I'm not using those. So take all my inputs with a pinch of salt 🙂 On MaterialsYeah, I already came across that welding doesn't yield good results if you have multiple materials. I hacked around to create a box with different sides in different colours, and those have to stay unwelded, which makes model bigger for no good reason, but if welded - the whole box just goes gray 🤷 I guess it would make sense to make materials more generic, but I really don't know what neither OBJ nor GLTF features are to tell what a common lowest denominator would look like, or if it's even feasible/useful to have one. I can imagine a situation where different material implementations are fitting a common interface, and the interface can return an enum value indicating it's internal format (or just try casting it to the type). Then the format packages would be able to do the conversion and use full features if the underlying material behind the interface is native to that format, or use only basic features provided by the common-denominator interface. In that situation I would work with the format-agnostic generic material until I need specific features supported only by the format of choice. And at that point I as a user of the library would have to decide whether I want to stay generic and support format compatibility in my software, or it's fine for me to specialize to one format only. On pointers specifically - I would make it a struct and have an On Formats APIYeah, I think what you're proposing makes perfect sense. A two-layer API:
Again, this structure will shift the decision to the library consumers - whether to stay with the generic The Since all operations are on the Yes, no problem with version updates, fixing this should be fairly easy on my end. I hope this helps. 🙂 |
|
Hey, brief update, if it helps. I've switched from OBJ to glTF as OBJ stopped working for me. After my model grew in size (with more parts added in my CAD) - web renderer I was using started failing with Part of the I see now that glTF has way more advanced lighting and material rendering control features. For my use case that is not necessary, I need it to look okay and no more (at this point anyway). So if a generic interface would just produce standard more-less unified visual results using only basic rendering - that would be good enough for me. |
This PR adds a
obj.SaveAll(objPath string, meshesToSave map[string]modeling.Mesh)function to OBJ FS operations. This allows saving multiple named meshes in a single OBJ file. All required internal mechanics for this already exist, i.e.WriteMeshes()can already handle this, but this wasn't exposed as an option in FS operations. Now it is.This PR slightly changes the exported interface of the
objpackage - formerWriteMaterials(modeling.Mesh, io.Writer)is renamed toWriteMaterialsFromMesh(modeling.Mesh, io.Writer), and a new function added under old name, but with a different signature -WriteMaterials([]modeling.MeshMaterial, io.Writer). Examples using those methods are updated accordingly.This seems more appropriate to me as there is a
WriteMaterial()func that accepts singular material, and now alsoWriteMaterials()that accepts multiple. Although this isn't perfect by itself since singular ismodeling.Material, but plural is[]modeling.MeshMaterial, slightly different objects.Naming
WriteMaterialsFromMesh()seems uncontroversial to me as this is exactly what the method does. Behind the interfaceWriteMaterialsFromMesh()is now a proxy toWriteMaterials(), with a convenient interface for the most common use case.Since
polyformis atv0at the moment - I assume introducing small changes in the exported interfaces of secondary packages is acceptable. Though also I'm happy to rollback and find a different, backwards compatible naming approach if that is preferrable, let me know.If taking exported interface changes further - it would probably make some sense to combine
obj.Save()andobj.SaveAll()into oneobj.Save(objPath string, meshesToSave ...obj.ObjMesh)interface. But this would introduce more breaking changes, and on the (probably) most widely used function of this package, so I think that's a bridge too far.