oxideav-obj
Pure-Rust Wavefront OBJ + MTL 3D mesh codec. Implements the
oxideav_mesh3d::Mesh3DDecoder and Mesh3DEncoder traits, plugging into
the wider OxideAV codec ecosystem.
OBJ is the universal mesh-interchange format published by Wavefront Technologies in the early 1990s as Appendix B of the Advanced Visualizer manual. This crate implements the polygonal subset (the part that modern loaders actually load):
v/vt/vnvertex data — with the optionalw4th component on positions (rational weight per spec §"v x y z w" — preserved verbatim throughPrimitive::extras["obj:vertex_weight"]) and the optionalv/wextra components on UVs. The widely-deployed MeshLab / libigl / Meshroom / OpenCVv x y z r g bper-vertex-colour extension is accepted at parse time, surfaced throughPrimitive::colors[0](alpha pinned to 1.0 since the extension only spells out three channels), and re-emitted at the original token width —xyz,xyzw,xyzrgb, orxyzwrgb— using thePrimitive::extras["obj:vertex_color_present"]bitmap so partial- colouring inputs preserve their per-vertex partition on round-trip rather than fabricating synthetic white. 5-floatvlines are rejected as ambiguous.ffaces in all four index syntaxes (v,v/vt,v//vn,v/vt/vn), with 1-based indexing and the negative-index relative-from-end shorthand. Polygons (n-gons) are fan-triangulated on read; the original per-face arity is stashed inMesh::extras["obj:original_face_arities"]so the encoder can re-emit n-gons rather than triangles.lline elements →Topology::LineStripfor a singlelelement with three or more distinct vertices,Topology::LineLoopwhen the polyline closes (last vertex equals the first; redundant closing index dropped), orTopology::Linesfor multi-lprimitives and 2-vertex segments. The encoder picks the matching emit shape:LineStripwrites the natural index list,LineLoopre-appends the first index to spell out the closing edge, andLinesrejoins contiguous segment pairs into one polyline rather than emitting onel v1 v2per pair.ppoint elements →Topology::Points. Multi-vertexp v1 v2 v3 …lines pack onto one element list; mixing point and face/line elements under oneusemtlsplits into one primitive per topology.mg <group_number> [res]merging-group state-setting → preserved verbatim inPrimitive::extras["obj:merging_group"]; a change mid-stream splits the primitive (mirrorssbehaviour).bevel on/off,c_interp on/off,d_interp on/off, andlod <level>display attributes → captured per-primitive inPrimitive::extras["obj:bevel"]/["obj:c_interp"]/["obj:d_interp"]/["obj:lod"]. Mid-stream changes split the primitive so each one carries one consistent assignment per attribute.o <name>→ oneMeshper object directive (or a single mesh if the file has noo).g name1 name2 …→ multiple group names per line, captured inPrimitive::extras["obj:groups"]and re-emitted on a singlegline.s 1/s off/s 0smoothing groups → preserved verbatim inPrimitive::extras["obj:smoothing_group"]; a smoothing-group change mid-object splits the primitive so each one carries a single consistent assignment.mtllib <file.mtl> [<file2.mtl> …]andusemtl <name>— eachusemtlswitch starts a freshPrimitiveso a multi-material OBJ becomes aMeshwith N primitives, each with its ownMaterialId.- Free-form geometry (
vpparameter-space vertices,cstype,deg,curv,curv2,surf,parm,trim,hole,scrv,sp,end, plus supersededbzp/bsppatches) — captured verbatim intoScene3D::extras["obj:vp"](1-based parallel vertex pool) andScene3D::extras["obj:freeform_directives"](sequence of[keyword, arg1, arg2, …]arrays). The encoder replays both after the polygonal section so a decode → encode round-trip preserves the directive order and arguments. No tessellation — consumers that need to evaluate the curves walk the directive sequence themselves.
The companion MTL parser/serialiser handles:
- Phong colours (
Ka/Kd/Ks/Ke) → glTFbase_color(fromKd) +emissive_factor(fromKe);Ka/Ksand theNsexponent are stashed inMaterial::extrasfor round-trip. - Transparency (
ddissolve /Tr = 1 - d) →AlphaMode::Blendbase_color.a. Thed -halo factororientation-dependent variant is detected and re-emitted viaMaterial::extras["mtl:d_halo_factor"].
- Index of refraction (
Ni) and illumination model (illum) → extras. - Transmission filter — three mutually-exclusive forms per spec:
Tf r g b(withg/bdefaulting tor),Tf spectral file.rfl factor→Material::extras["mtl:Tf:spectral"], andTf xyz x y z→Material::extras["mtl:Tf:xyz"]. - Reflection sharpness (
sharpness) and displacement / decal / reflection maps (disp↔map_disp,decal↔map_decal,refl↔map_refl) round-trip via extras. - Typed reflection maps —
refl -type sphere fileand the six-facerefl -type cube_top|cube_bottom|cube_front|cube_back|cube_left|cube_rightcubemap. Sphere lands asMaterial::extras["mtl:refl:sphere"]; the six face lines bundle intoMaterial::extras["mtl:refl:cube"](one entry per face) so they don't collapse onto each other under last-write-wins; the encoder re-emits onerefl -type <kind> ... fileline per face / sphere in deterministic order. - Texture references (
map_Kd→base_color_texture,map_Bump→normal_texture,map_detc.) emitted asImageData::External { uri, mime: None }— the caller resolves paths against the OBJ file's directory. Leading-flag valueoption chunks (-blendu,-bm,-mm,-clamp,-imfchan,-o,-s,-t,-texres) are parsed out of the filename and surfaced viaMaterial::extras["mtl:<map_name>:options"]; the encoder splices them back inline. - Wavefront-PBR extension (
Prroughness,Pmmetallic,Pcclearcoat,Pssheen,map_Pr/map_Pm) →Material::roughness/Material::metallic/metallic_roughness_texture.
Both decoders are registered against Mesh3DRegistry under the
default-on registry cargo feature; drop the feature for a free-standing
build that only exposes ObjDecoder / ObjEncoder and the
oxideav_mesh3d standalone trait surface.
For path-based loading, obj::parse_obj_from_path resolves
mtllib foo.mtl references against the OBJ file's parent directory
(handles multiple MTL files per line). For round-trip mirroring of
inputs that used negative-from-end indices, the encoder accepts
ObjEncoder::new().with_negative_indices(true) (or the same flag on
obj::SerializeOptions).
Sourcing
The Wavefront spec is mirrored in the OxideAV docs repository:
docs/3d/obj/wavefront-obj-spec.txt— Martin Reddy plain-text Appendix B1 mirror.docs/3d/obj/wavefront-mtl-spec.html— Paul Bourke MTL mirror (carries the originalCopyright 1995 Alias|Wavefront, Inc.notice).docs/3d/obj/paulbourke-obj-reference.html— Paul Bourke OBJ cross-check mirror.
This crate was implemented strictly from those references — no existing OBJ loader (tinyobj, assimp, blender io_scene_obj, three.js OBJLoader) was consulted.
Status
Round 1: polygonal subset + MTL Phong + Wavefront-PBR extension.
Round 2: multi-name g lines, smoothing-group state-setting (split-on-
change), Tf / sharpness / displacement-map round-trip, path-based
loader with mtllib resolution, and an opt-in negative-index encoder.
Round 3: p point elements, mg merging groups, bevel / c_interp
/ d_interp / lod display attributes, MTL map_* option flags
(-blendu, -clamp, -bm, …) preserved through round-trip, MTL
d -halo factor, encoder polyline rejoin.
Round 4: free-form geometry (vp parameter-space vertex pool plus
the verbatim cstype / deg / curv / curv2 / surf / parm /
trim / hole / scrv / sp / end / bzp / bsp directive
sequence) — round-trips through Scene3D::extras without
tessellation.
Round 5: MTL Tf spectral / Tf xyz alternative transmission-filter
forms, refl -type sphere / refl -type cube_* typed reflection-map
sets bundled into mtl:refl:sphere / mtl:refl:cube extras, and
single-l polylines promoted to Topology::LineStrip /
Topology::LineLoop (with closure detection at decode time).
Round 6: per-vertex colour extension (v x y z r g b, MeshLab /
libigl / Meshroom de-facto) accepted on parse, populated on
Primitive::colors[0], and re-emitted at the source's original
3-/4-/6-/7-token width via the obj:vertex_color_present bitmap.
The v 4th w weight component is now preserved through
Primitive::extras["obj:vertex_weight"] rather than silently dropped.
The .mod binary form remains out of scope; tessellation evaluators
that turn captured cstype + deg + curv + parm blocks into actual
mesh primitives are the obvious next round of work.
License
MIT. See LICENSE.