22// Use of this source code is governed by a BSD-style
33// license that can be found in the LICENSE file.
44
5- // Package obj
5+ // Package obj is used to parse the Wavefront OBJ file format (*.obj), including
6+ // associated materials (*.mtl). Not all features of the OBJ format are
7+ // supported. Basic format info: https://en.wikipedia.org/wiki/Wavefront_.obj_file
68package obj
79
810import (
@@ -71,6 +73,14 @@ type Material struct {
7173 MapKd string // Texture file linked to diffuse color
7274}
7375
76+ // Light gray default material used as when other materials cannot be loaded.
77+ var defaultMat = & Material {
78+ Diffuse : math32.Color {R : 0.7 , G : 0.7 , B : 0.7 },
79+ Ambient : math32.Color {R : 0.7 , G : 0.7 , B : 0.7 },
80+ Specular : math32.Color {R : 0.5 , G : 0.5 , B : 0.5 },
81+ Shininess : 30.0 ,
82+ }
83+
7484// Local constants
7585const (
7686 blanks = "\r \n \t "
@@ -80,7 +90,9 @@ const (
8090)
8191
8292// Decode decodes the specified obj and mtl files returning a decoder
83- // object and an error.
93+ // object and an error. Passing an empty string (or otherwise invalid path)
94+ // to mtlpath will cause the decoder to check the 'mtllib' file in the OBJ if
95+ // present, and fall back to a default material as a last resort.
8496func Decode (objpath string , mtlpath string ) (* Decoder , error ) {
8597
8698 // Opens obj file
@@ -90,31 +102,32 @@ func Decode(objpath string, mtlpath string) (*Decoder, error) {
90102 }
91103 defer fobj .Close ()
92104
93- // If path of material file not supplied,
94- // try to use the base name of the obj file
95- if len (mtlpath ) == 0 {
96- dir , objfile := filepath .Split (objpath )
97- ext := filepath .Ext (objfile )
98- mtlpath = dir + objfile [:len (objfile )- len (ext )] + ".mtl"
99- }
100-
101105 // Opens mtl file
106+ // if mtlpath=="", then os.Open() will produce an error,
107+ // causing fmtl to be nil
102108 fmtl , err := os .Open (mtlpath )
103- if err != nil {
104- return nil , err
105- }
106- defer fmtl .Close ()
109+ defer fmtl .Close () // will produce (ignored) err if fmtl==nil
107110
111+ // if fmtl==nil, the io.Reader in DecodeReader() will be (T=*os.File, V=nil)
112+ // which is NOT equal to plain nil or (io.Reader, nil) but will produce
113+ // the desired result of passing nil to DecodeReader() per it's func comment.
108114 dec , err := DecodeReader (fobj , fmtl )
109115 if err != nil {
110116 return nil , err
111117 }
118+
112119 dec .mtlDir = filepath .Dir (objpath )
113120 return dec , nil
114121}
115122
116123// DecodeReader decodes the specified obj and mtl readers returning a decoder
117- // object and an error.
124+ // object and an error if a problem was encoutered while parsing the OBJ.
125+ //
126+ // Pass a valid io.Reader to override the materials defined in the OBJ file,
127+ // or `nil` to use the materials listed in the OBJ's "mtllib" line (if present),
128+ // a ".mtl" file with the same name as the OBJ file if presemt, or a default
129+ // material as a last resort. No error will be returned for problems
130+ // with materials--a gray default material will be used if nothing else works.
118131func DecodeReader (objreader , mtlreader io.Reader ) (* Decoder , error ) {
119132
120133 dec := new (Decoder )
@@ -133,12 +146,71 @@ func DecodeReader(objreader, mtlreader io.Reader) (*Decoder, error) {
133146 }
134147
135148 // Parses mtl lines
149+ // 1) try passed in mtlreader,
150+ // 2) try file in mtllib line
151+ // 3) try <obj_filename>.mtl
152+ // 4) use default material as last resort
136153 dec .matCurrent = nil
137154 dec .line = 1
155+ // first try: use the material file passed in as an io.Reader
138156 err = dec .parse (mtlreader , dec .parseMtlLine )
139157 if err != nil {
140- return nil , err
158+
159+ // 2) if mtlreader produces an error (eg. it's nil), try the file listed
160+ // in the OBJ's matlib line, if it exists.
161+ if dec .Matlib != "" {
162+ // ... first need to get the path of the OBJ, since mtllib is relative
163+ var mtllibPath string
164+ if objf , ok := objreader .(* os.File ); ok {
165+ // NOTE (quillaja): this is a hack because we need the directory of
166+ // the OBJ, but can't get it any other way (dec.mtlDir isn't set
167+ // until AFTER this function is finished).
168+ objdir := filepath .Dir (objf .Name ())
169+ mtllibPath = filepath .Join (objdir , dec .Matlib )
170+ dec .mtlDir = objdir // NOTE (quillaja): should this be set?
171+ }
172+ mtlf , errMTL := os .Open (mtllibPath )
173+ defer mtlf .Close ()
174+ if errMTL == nil {
175+ err = dec .parse (mtlf , dec .parseMtlLine ) // will set err to nil if successful
176+ }
177+ }
178+
179+ // 3) if the mtllib line fails try <obj_filename>.mtl in the same directory.
180+ // process is basically identical to the above code block.
181+ if err != nil {
182+ var mtlpath string
183+ if objf , ok := objreader .(* os.File ); ok {
184+ objdir := strings .TrimSuffix (objf .Name (), ".obj" )
185+ mtlpath = objdir + ".mtl"
186+ dec .mtlDir = objdir // NOTE (quillaja): should this be set?
187+ }
188+ mtlf , errMTL := os .Open (mtlpath )
189+ defer mtlf .Close ()
190+ if errMTL == nil {
191+ err = dec .parse (mtlf , dec .parseMtlLine ) // will set err to nil if successful
192+ if err == nil {
193+ // log a warning
194+ msg := fmt .Sprintf ("using material file %s" , mtlpath )
195+ dec .appendWarn (mtlType , msg )
196+ }
197+ }
198+ }
199+
200+ // 4) handle error(s) instead of simply passing it up the call stack.
201+ // range over the materials named in the OBJ file and substitute a default
202+ // But log that an error occured.
203+ if err != nil {
204+ for key := range dec .Materials {
205+ dec .Materials [key ] = defaultMat
206+ }
207+ // NOTE (quillaja): could be an error of some custom type. But people
208+ // tend to ignore errors and pass them up the call stack instead
209+ // of handling them... so all this work would probably be wasted.
210+ dec .appendWarn (mtlType , "unable to parse a material file for obj. using default material instead." )
211+ }
141212 }
213+
142214 return dec , nil
143215}
144216
@@ -169,9 +241,22 @@ func (dec *Decoder) NewMesh(obj *Object) (*graphic.Mesh, error) {
169241
170242 // Single material
171243 if geom .GroupCount () == 1 {
172- matName := obj .materials [0 ]
173- matDesc := dec .Materials [matName ]
174- // Creates material
244+ // get Material info from mtl file and ensure it's valid.
245+ // substitute default material if it is not.
246+ var matDesc * Material
247+ var matName string
248+ if len (obj .materials ) > 0 {
249+ matName = obj .materials [0 ]
250+ }
251+ matDesc = dec .Materials [matName ]
252+ if matDesc == nil {
253+ matDesc = defaultMat
254+ // log warning
255+ msg := fmt .Sprintf ("could not find material for %s. using default material." , obj .Name )
256+ dec .appendWarn (objType , msg )
257+ }
258+
259+ // Creates material for mesh
175260 mat := material .NewPhong (& matDesc .Diffuse )
176261 ambientColor := mat .AmbientColor ()
177262 mat .SetAmbientColor (ambientColor .Multiply (& matDesc .Ambient ))
@@ -182,16 +267,31 @@ func (dec *Decoder) NewMesh(obj *Object) (*graphic.Mesh, error) {
182267 if err != nil {
183268 return nil , err
184269 }
270+
185271 return graphic .NewMesh (geom , mat ), nil
186272 }
187273
188274 // Multi material
189275 mesh := graphic .NewMesh (geom , nil )
190276 for idx := 0 ; idx < geom .GroupCount (); idx ++ {
191277 group := geom .GroupAt (idx )
192- matName := obj .materials [group .Matindex ]
193- matDesc := dec .Materials [matName ]
194- // Creates material
278+
279+ // get Material info from mtl file and ensure it's valid.
280+ // substitute default material if it is not.
281+ var matDesc * Material
282+ var matName string
283+ if len (obj .materials ) > group .Matindex {
284+ matName = obj .materials [group .Matindex ]
285+ }
286+ matDesc = dec .Materials [matName ]
287+ if matDesc == nil {
288+ matDesc = defaultMat
289+ // log warning
290+ msg := fmt .Sprintf ("could not find material for %s. using default material." , obj .Name )
291+ dec .appendWarn (objType , msg )
292+ }
293+
294+ // Creates material for mesh
195295 matGroup := material .NewPhong (& matDesc .Diffuse )
196296 ambientColor := matGroup .AmbientColor ()
197297 matGroup .SetAmbientColor (ambientColor .Multiply (& matDesc .Ambient ))
@@ -202,6 +302,7 @@ func (dec *Decoder) NewMesh(obj *Object) (*graphic.Mesh, error) {
202302 if err != nil {
203303 return nil , err
204304 }
305+
205306 mesh .AddGroupMaterial (matGroup , idx )
206307 }
207308 return mesh , nil
@@ -375,7 +476,7 @@ func (dec *Decoder) parseObjLine(line string) error {
375476func (dec * Decoder ) parseMatlib (fields []string ) error {
376477
377478 if len (fields ) < 1 {
378- return errors .New ("Object line (o ) with less than 2 fields" )
479+ return errors .New ("Material library (mtllib ) with no fields" )
379480 }
380481 dec .Matlib = fields [0 ]
381482 return nil
@@ -386,7 +487,7 @@ func (dec *Decoder) parseMatlib(fields []string) error {
386487func (dec * Decoder ) parseObject (fields []string ) error {
387488
388489 if len (fields ) < 1 {
389- return errors .New ("Object line (o) with less than 2 fields" )
490+ return errors .New ("Object line (o) with no fields" )
390491 }
391492 var ob Object
392493 ob .Name = fields [0 ]
@@ -460,14 +561,18 @@ func (dec *Decoder) parseFace(fields []string) error {
460561 if len (fields ) < 3 {
461562 return dec .formatError ("Face line with less 3 fields" )
462563 }
463- if dec .matCurrent == nil {
464- return dec .formatError ("No material defined" )
465- }
466564 var face Face
467565 face .Vertices = make ([]int , len (fields ))
468566 face .Uvs = make ([]int , len (fields ))
469567 face .Normals = make ([]int , len (fields ))
470- face .Material = dec .matCurrent .Name
568+ if dec .matCurrent != nil {
569+ face .Material = dec .matCurrent .Name
570+ } else {
571+ // TODO (quillaja): do something better than spamming warnings for each line
572+ // dec.appendWarn(objType, "No material defined")
573+ face .Material = "internal default" // causes error on in NewGeom() if ""
574+ // dec.matCurrent = defaultMat
575+ }
471576 face .Smooth = dec .smoothCurrent
472577
473578 for pos , f := range fields {
0 commit comments