Skip to content

Commit 1b502de

Browse files
authored
Merge pull request #115 from quillaja/issue-29-obj-default-mat
Issue 29: default material for OBJ decode
2 parents 9643172 + 759b6c7 commit 1b502de

File tree

1 file changed

+133
-28
lines changed

1 file changed

+133
-28
lines changed

loader/obj/obj.go

Lines changed: 133 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
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
68
package obj
79

810
import (
@@ -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
7585
const (
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.
8496
func 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.
118131
func 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 {
375476
func (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 {
386487
func (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

Comments
 (0)