├── LICENSE ├── README.md ├── build.sh ├── example ├── main.go ├── red_cube.mtl └── red_cube.obj ├── go.mod ├── obj.go ├── obj_test.go └── parser.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 udhos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![license](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/udhos/gwob/blob/master/LICENSE) 2 | [![GoDoc](https://godoc.org/github.com/udhos/gwob?status.svg)](http://godoc.org/github.com/udhos/gwob) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/udhos/gwob)](https://goreportcard.com/report/github.com/udhos/gwob) 4 | 5 | # gwob 6 | gwob - Pure Go Golang parser for Wavefront .OBJ 3D geometry file format 7 | 8 | # Install 9 | 10 | ## Install with Go Modules (Go 1.11 or higher) 11 | 12 | git clone https://github.com/udhos/gwob 13 | cd gwob 14 | go install 15 | 16 | ## Install without Go Modules (Go before 1.11) 17 | 18 | go get github.com/udhos/gwob 19 | cd ~/go/src/github.com/udhos/gwob 20 | go install github.com/udhos/gwob 21 | 22 | # Usage 23 | 24 | Import the package in your Go program: 25 | 26 | import "github.com/udhos/gwob" 27 | 28 | Example: 29 | 30 | // Error handling omitted for simplicity. 31 | 32 | import "github.com/udhos/gwob" 33 | 34 | options := &gwob.ObjParserOptions{} // parser options 35 | 36 | o, errObj := gwob.NewObjFromFile("gopher.obj", options) // parse/load OBJ 37 | 38 | // Scan OBJ groups 39 | for _, g := range o.Groups { 40 | // ... 41 | } 42 | 43 | # Example 44 | 45 | Run the example: 46 | 47 | cd example 48 | go run main.go 49 | 50 | You can supply a custom input OBJ by setting the env var INPUT: 51 | 52 | INPUT=gopher.obj go run main.go 53 | 54 | If you specify any command line argument, the OBJ will be dumped to stdout: 55 | 56 | go run main.go d 57 | 58 | See directory [example](example). 59 | 60 | # Documentation 61 | 62 | See the [GoDoc](http://godoc.org/github.com/udhos/gwob) documentation. 63 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | go install golang.org/x/vuln/cmd/govulncheck@latest 4 | 5 | gofmt -s -w . 6 | 7 | revive ./... 8 | 9 | #gocyclo -over 15 . 10 | 11 | go mod tidy 12 | 13 | govulncheck ./... 14 | 15 | go env -w CGO_ENABLED=1 16 | 17 | go test -race ./... 18 | 19 | go test -race -bench=. ./... 20 | 21 | go env -w CGO_ENABLED=0 22 | 23 | go install ./... 24 | 25 | go env -u CGO_ENABLED 26 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package main shows how to use the 'gwob' package to parse geometry data from OBJ files. 3 | 4 | See also: https://github.com/udhos/gwob 5 | */ 6 | package main 7 | 8 | import ( 9 | "fmt" 10 | "log" 11 | "os" 12 | 13 | "github.com/udhos/gwob" 14 | ) 15 | 16 | func main() { 17 | 18 | fileObj := os.Getenv("INPUT") 19 | if fileObj == "" { 20 | fileObj = "red_cube.obj" 21 | } 22 | log.Printf("env var INPUT=[%s] using input=%s", os.Getenv("INPUT"), fileObj) 23 | 24 | // Set options 25 | options := &gwob.ObjParserOptions{ 26 | LogStats: true, 27 | Logger: func(msg string) { fmt.Fprintln(os.Stderr, msg) }, 28 | } 29 | 30 | // Load OBJ 31 | o, errObj := gwob.NewObjFromFile(fileObj, options) 32 | if errObj != nil { 33 | log.Printf("obj: parse error input=%s: %v", fileObj, errObj) 34 | return 35 | } 36 | 37 | fileMtl := o.Mtllib 38 | 39 | // Load material lib 40 | lib, errMtl := gwob.ReadMaterialLibFromFile(fileMtl, options) 41 | if errMtl != nil { 42 | log.Printf("mtl: parse error input=%s: %v", fileMtl, errMtl) 43 | } else { 44 | 45 | // Scan OBJ groups 46 | for _, g := range o.Groups { 47 | 48 | mtl, found := lib.Lib[g.Usemtl] 49 | if found { 50 | log.Printf("obj=%s lib=%s group=%s material=%s MapKd=%s Kd=%v", fileObj, fileMtl, g.Name, g.Usemtl, mtl.MapKd, mtl.Kd) 51 | continue 52 | } 53 | 54 | log.Printf("obj=%s lib=%s group=%s material=%s NOT FOUND", fileObj, fileMtl, g.Name, g.Usemtl) 55 | } 56 | 57 | } 58 | 59 | if len(os.Args) < 2 { 60 | log.Printf("no cmd line args - dump to stdout suppressed") 61 | return 62 | } 63 | 64 | log.Printf("cmd line arg found - dumping to stdout") 65 | 66 | // Dump to stdout 67 | o.ToWriter(os.Stdout) 68 | } 69 | -------------------------------------------------------------------------------- /example/red_cube.mtl: -------------------------------------------------------------------------------- 1 | # 3DTin generated Material library 2 | # http://www.3dtin.com 3 | # Material count 1 4 | 5 | newmtl color0_1 6 | Ns 100 7 | Ka 0.000000 0.000000 0.000000 8 | Kd 1 0.33725490196078434 0.33725490196078434 9 | Ks 0.500000 0.500000 0.500000 10 | Ni 1.000000 11 | d 1.000000 12 | illum 2 13 | 14 | -------------------------------------------------------------------------------- /example/red_cube.obj: -------------------------------------------------------------------------------- 1 | # 3DTin generated OBJ 2 | # http://www.3dtin.com 3 | mtllib red_cube.mtl 4 | o Object0 5 | 6 | # Vertices 7 | v 0.000000 4.500000 -1.500000 1.000000 8 | v 0.000000 0.000000 3.000000 1.000000 9 | v 0.000000 4.500000 3.000000 1.000000 10 | v 0.000000 0.000000 -1.500000 1.000000 11 | v 4.500000 4.500000 3.000000 1.000000 12 | v 4.500000 0.000000 3.000000 1.000000 13 | v 4.500000 4.500000 -1.500000 1.000000 14 | v 4.500000 0.000000 -1.500000 1.000000 15 | v 4.500000 0.000000 -1.500000 1.000000 16 | v 4.500000 0.000000 3.000000 1.000000 17 | v 0.000000 0.000000 -1.500000 1.000000 18 | v 0.000000 0.000000 3.000000 1.000000 19 | v 0.000000 4.500000 -1.500000 1.000000 20 | v 4.500000 4.500000 3.000000 1.000000 21 | v 4.500000 4.500000 -1.500000 1.000000 22 | v 0.000000 4.500000 3.000000 1.000000 23 | v 0.000000 4.500000 -1.500000 1.000000 24 | v 4.500000 4.500000 -1.500000 1.000000 25 | v 0.000000 0.000000 -1.500000 1.000000 26 | v 4.500000 0.000000 -1.500000 1.000000 27 | v 0.000000 0.000000 3.000000 1.000000 28 | v 4.500000 4.500000 3.000000 1.000000 29 | v 0.000000 4.500000 3.000000 1.000000 30 | v 4.500000 0.000000 3.000000 1.000000 31 | 32 | 33 | # Faces 34 | usemtl color0_1 35 | s off 36 | f 1 2 3 37 | f 4 2 1 38 | f 5 6 7 39 | f 7 6 8 40 | f 9 10 11 41 | f 11 10 12 42 | f 13 14 15 43 | f 16 14 13 44 | f 17 18 19 45 | f 19 18 20 46 | f 21 22 23 47 | f 24 22 21 48 | 49 | 50 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/udhos/gwob 2 | 3 | go 1.22.2 4 | -------------------------------------------------------------------------------- /obj.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package gwob is a pure Go parser for Wavefront .OBJ 3D geometry file format. 3 | 4 | Example: 5 | 6 | // Error handling omitted for simplicity. 7 | 8 | import "github.com/udhos/gwob" 9 | 10 | options := &gwob.ObjParserOptions{} // parser options 11 | 12 | o, errObj := gwob.NewObjFromFile("gopher.obj", options) // parse 13 | 14 | // Scan OBJ groups 15 | for _, g := range o.Groups { 16 | // snip 17 | } 18 | 19 | See also: https://github.com/udhos/gwob 20 | */ 21 | package gwob 22 | 23 | import ( 24 | "bufio" 25 | "bytes" 26 | "fmt" 27 | "io" 28 | "math" 29 | "os" 30 | "strconv" 31 | "strings" 32 | ) 33 | 34 | // Internal parsing error 35 | const ( 36 | ErrFatal = true // ErrFatal means fatal stream error 37 | ErrNonFatal = false // ErrNonFatal means non-fatal parsing error 38 | ) 39 | 40 | // Material holds information for a material. 41 | // Kd - diffuse color. 42 | // Ka - ambient color. 43 | // Ks - specular color. 44 | // Ns - specular exponent. 45 | // Ni - optical density aka. index of refraction. 46 | // Illum - illumination model enum id. 47 | // D / Tr - trasparency (Tr = 1 - D) 48 | // MapKa - ambient map 49 | // MapKd - diffuse map 50 | // MapKs - specular map 51 | // MapD - scalar procedural texture map 52 | // Bump/map_Bump - bump texture map - modify surface normal 53 | // Ke/MapKe - emissive map - clara.io extension 54 | type Material struct { 55 | Name string 56 | MapKd string 57 | MapKa string 58 | MapKs string 59 | MapD string 60 | Bump string 61 | MapKe string 62 | Kd [3]float32 63 | Ka [3]float32 64 | Ks [3]float32 65 | Ns float32 66 | Ni float32 67 | Illum int 68 | D float32 69 | Tr float32 70 | } 71 | 72 | // MaterialLib stores materials. 73 | type MaterialLib struct { 74 | Lib map[string]*Material 75 | } 76 | 77 | // StringReader is input for the parser. 78 | type StringReader interface { 79 | ReadString(delim byte) (string, error) // Example: bufio.Reader 80 | } 81 | 82 | // ReadMaterialLibFromBuf parses material lib from a buffer. 83 | func ReadMaterialLibFromBuf(buf []byte, options *ObjParserOptions) (MaterialLib, error) { 84 | return readLib(bytes.NewBuffer(buf), options) 85 | } 86 | 87 | // ReadMaterialLibFromReader parses material lib from a reader. 88 | func ReadMaterialLibFromReader(rd io.Reader, options *ObjParserOptions) (MaterialLib, error) { 89 | return readLib(bufio.NewReader(rd), options) 90 | } 91 | 92 | // ReadMaterialLibFromStringReader parses material lib from StringReader. 93 | func ReadMaterialLibFromStringReader(rd StringReader, options *ObjParserOptions) (MaterialLib, error) { 94 | return readLib(rd, options) 95 | } 96 | 97 | // ReadMaterialLibFromFile parses material lib from a file. 98 | func ReadMaterialLibFromFile(filename string, options *ObjParserOptions) (MaterialLib, error) { 99 | 100 | input, errOpen := os.Open(filename) 101 | if errOpen != nil { 102 | return NewMaterialLib(), errOpen 103 | } 104 | 105 | defer input.Close() 106 | 107 | return ReadMaterialLibFromReader(input, options) 108 | } 109 | 110 | // NewMaterialLib creates a new material lib. 111 | func NewMaterialLib() MaterialLib { 112 | return MaterialLib{Lib: map[string]*Material{}} 113 | } 114 | 115 | // libParser holds auxiliary internal state for the parsing. 116 | type libParser struct { 117 | currMaterial *Material 118 | } 119 | 120 | func readLib(reader StringReader, options *ObjParserOptions) (MaterialLib, error) { 121 | 122 | lineCount := 0 123 | 124 | parser := &libParser{} 125 | lib := NewMaterialLib() 126 | 127 | for { 128 | lineCount++ 129 | line, err := reader.ReadString('\n') 130 | if err == io.EOF { 131 | // parse last line 132 | if _, e := parseLibLine(parser, lib, line, lineCount); e != nil { 133 | options.log(fmt.Sprintf("readLib: %v", e)) 134 | return lib, e 135 | } 136 | break // EOF 137 | } 138 | 139 | if err != nil { 140 | // unexpected IO error 141 | return lib, fmt.Errorf("readLib: error: %v", err) 142 | } 143 | 144 | if fatal, e := parseLibLine(parser, lib, line, lineCount); e != nil { 145 | options.log(fmt.Sprintf("readLib: %v", e)) 146 | if fatal { 147 | return lib, e 148 | } 149 | } 150 | } 151 | 152 | return lib, nil 153 | } 154 | 155 | func parseLibLine(p *libParser, lib MaterialLib, rawLine string, lineCount int) (bool, error) { 156 | line := strings.TrimSpace(rawLine) 157 | 158 | switch { 159 | case line == "" || line[0] == '#': 160 | case strings.HasPrefix(line, "newmtl "): 161 | 162 | newmtl := line[7:] 163 | var mat *Material 164 | var ok bool 165 | if mat, ok = lib.Lib[newmtl]; !ok { 166 | // create new material 167 | mat = &Material{Name: newmtl} 168 | lib.Lib[newmtl] = mat 169 | } 170 | p.currMaterial = mat 171 | 172 | case strings.HasPrefix(line, "Kd "): 173 | Kd := line[3:] 174 | 175 | if p.currMaterial == nil { 176 | return ErrNonFatal, fmt.Errorf("parseLibLine: %d undefined material for Kd=%s [%s]", lineCount, Kd, line) 177 | } 178 | 179 | color, err := parseFloatVector3Space(Kd) 180 | if err != nil { 181 | return ErrNonFatal, fmt.Errorf("parseLibLine: %d parsing error for Kd=%s [%s]: %v", lineCount, Kd, line, err) 182 | } 183 | 184 | p.currMaterial.Kd[0] = float32(color[0]) 185 | p.currMaterial.Kd[1] = float32(color[1]) 186 | p.currMaterial.Kd[2] = float32(color[2]) 187 | 188 | case strings.HasPrefix(line, "map_Kd "): 189 | mapKd := line[7:] 190 | 191 | if p.currMaterial == nil { 192 | return ErrNonFatal, fmt.Errorf("parseLibLine: %d undefined material for map_Kd=%s [%s]", lineCount, mapKd, line) 193 | } 194 | 195 | p.currMaterial.MapKd = mapKd 196 | 197 | case strings.HasPrefix(line, "map_Ka "): 198 | mapKa := line[7:] 199 | 200 | if p.currMaterial == nil { 201 | return ErrNonFatal, fmt.Errorf("parseLibLine: %d undefined material for map_Ka=%s [%s]", lineCount, mapKa, line) 202 | } 203 | 204 | p.currMaterial.MapKa = mapKa 205 | 206 | case strings.HasPrefix(line, "map_Ks "): 207 | mapKs := line[7:] 208 | 209 | if p.currMaterial == nil { 210 | return ErrNonFatal, fmt.Errorf("parseLibLine: %d undefined material for map_Ks=%s [%s]", lineCount, mapKs, line) 211 | } 212 | 213 | p.currMaterial.MapKs = mapKs 214 | 215 | case strings.HasPrefix(line, "map_d "): 216 | mapD := line[6:] 217 | 218 | if p.currMaterial == nil { 219 | return ErrNonFatal, fmt.Errorf("parseLibLine: %d undefined material for map_D=%s [%s]", lineCount, mapD, line) 220 | } 221 | 222 | p.currMaterial.MapD = mapD 223 | 224 | case strings.HasPrefix(line, "map_Bump "): 225 | bump := line[9:] 226 | 227 | if p.currMaterial == nil { 228 | return ErrNonFatal, fmt.Errorf("parseLibLine: %d undefined material for bump=%s [%s]", lineCount, bump, line) 229 | } 230 | 231 | p.currMaterial.Bump = bump 232 | 233 | case strings.HasPrefix(line, "bump "): 234 | bump := line[5:] 235 | 236 | if p.currMaterial == nil { 237 | return ErrNonFatal, fmt.Errorf("parseLibLine: %d undefined material for bump=%s [%s]", lineCount, bump, line) 238 | } 239 | 240 | p.currMaterial.Bump = bump 241 | 242 | case strings.HasPrefix(line, "Ns "): 243 | Ns := line[3:] 244 | 245 | if p.currMaterial == nil { 246 | return ErrNonFatal, fmt.Errorf("parseLibLine: %d undefined material for Ns=%s [%s]", lineCount, Ns, line) 247 | } 248 | 249 | value, err := parseFloatVectorSpace(Ns, 1) 250 | if err != nil { 251 | return ErrNonFatal, fmt.Errorf("parseLibLine: %d parsing error for Ns=%s [%s]: %v", lineCount, Ns, line, err) 252 | } 253 | 254 | p.currMaterial.Ns = float32(value[0]) 255 | 256 | case strings.HasPrefix(line, "Ka "): 257 | Ka := line[3:] 258 | 259 | if p.currMaterial == nil { 260 | return ErrNonFatal, fmt.Errorf("parseLibLine: %d undefined material for Ka=%s [%s]", lineCount, Ka, line) 261 | } 262 | 263 | color, err := parseFloatVector3Space(Ka) 264 | if err != nil { 265 | return ErrNonFatal, fmt.Errorf("parseLibLine: %d parsing error for Ka=%s [%s]: %v", lineCount, Ka, line, err) 266 | } 267 | 268 | p.currMaterial.Ka[0] = float32(color[0]) 269 | p.currMaterial.Ka[1] = float32(color[1]) 270 | p.currMaterial.Ka[2] = float32(color[2]) 271 | 272 | case strings.HasPrefix(line, "Ke "): 273 | MapKe := line[3:] 274 | 275 | if p.currMaterial == nil { 276 | return ErrNonFatal, fmt.Errorf("parseLibLine: %d undefined material for MapKe=%s [%s]", lineCount, MapKe, line) 277 | } 278 | 279 | p.currMaterial.MapKe = MapKe 280 | 281 | case strings.HasPrefix(line, "Ks "): 282 | Ks := line[3:] 283 | 284 | if p.currMaterial == nil { 285 | return ErrNonFatal, fmt.Errorf("parseLibLine: %d undefined material for Ks=%s [%s]", lineCount, Ks, line) 286 | } 287 | 288 | color, err := parseFloatVector3Space(Ks) 289 | if err != nil { 290 | return ErrNonFatal, fmt.Errorf("parseLibLine: %d parsing error for Ks=%s [%s]: %v", lineCount, Ks, line, err) 291 | } 292 | 293 | p.currMaterial.Ks[0] = float32(color[0]) 294 | p.currMaterial.Ks[1] = float32(color[1]) 295 | p.currMaterial.Ks[2] = float32(color[2]) 296 | 297 | case strings.HasPrefix(line, "Ni "): 298 | Ni := line[3:] 299 | 300 | if p.currMaterial == nil { 301 | return ErrNonFatal, fmt.Errorf("parseLibLine: %d undefined material for Ni=%s [%s]", lineCount, Ni, line) 302 | } 303 | 304 | value, err := parseFloatVectorSpace(Ni, 1) 305 | if err != nil { 306 | return ErrNonFatal, fmt.Errorf("parseLibLine: %d parsing error for Ni=%s [%s]: %v", lineCount, Ni, line, err) 307 | } 308 | 309 | p.currMaterial.Ni = float32(value[0]) 310 | 311 | case strings.HasPrefix(line, "d "): 312 | D := line[2:] 313 | 314 | if p.currMaterial == nil { 315 | return ErrNonFatal, fmt.Errorf("parseLibLine: %d undefined material for D=%s [%s]", lineCount, D, line) 316 | } 317 | 318 | value, err := parseFloatVectorSpace(D, 1) 319 | if err != nil { 320 | return ErrNonFatal, fmt.Errorf("parseLibLine: %d parsing error for D=%s [%s]: %v", lineCount, D, line, err) 321 | } 322 | 323 | p.currMaterial.D = float32(value[0]) 324 | 325 | case strings.HasPrefix(line, "illum "): 326 | Illum := line[6:] 327 | 328 | if p.currMaterial == nil { 329 | return ErrNonFatal, fmt.Errorf("parseLibLine: %d undefined material for Illum=%s [%s]", lineCount, Illum, line) 330 | } 331 | 332 | value, err := parseFloatVectorSpace(Illum, 1) 333 | if err != nil { 334 | return ErrNonFatal, fmt.Errorf("parseLibLine: %d parsing error for Illum=%s [%s]: %v", lineCount, Illum, line, err) 335 | } 336 | 337 | p.currMaterial.Illum = int(value[0]) 338 | 339 | case strings.HasPrefix(line, "Tf "): 340 | case strings.HasPrefix(line, "Tr "): 341 | default: 342 | return ErrNonFatal, fmt.Errorf("parseLibLine %v: [%v]: unexpected", lineCount, line) 343 | } 344 | 345 | return ErrNonFatal, nil 346 | } 347 | 348 | // Group holds parser result for a group. 349 | type Group struct { 350 | Name string 351 | Smooth int 352 | Usemtl string 353 | IndexBegin int 354 | IndexCount int 355 | } 356 | 357 | // Obj holds parser result for .obj file. 358 | type Obj struct { 359 | Indices []int 360 | Coord []float32 // vertex data pos=(x,y,z) tex=(tx,ty) norm=(nx,ny,nz) 361 | Mtllib string 362 | Groups []*Group 363 | 364 | BigIndexFound bool // index larger than 65535 365 | TextCoordFound bool // texture coord 366 | NormCoordFound bool // normal coord 367 | 368 | StrideSize int // (px,py,pz),(tu,tv),(nx,ny,nz) = 8 x 4-byte floats = 32 bytes max 369 | StrideOffsetPosition int // 0 370 | StrideOffsetTexture int // 3 x 4-byte floats 371 | StrideOffsetNormal int // 5 x 4-byte floats 372 | } 373 | 374 | // objParser holds auxiliary internal parser state. 375 | type objParser struct { 376 | lineBuf []string 377 | lineCount int 378 | vertCoord []float32 379 | textCoord []float32 380 | normCoord []float32 381 | currGroup *Group 382 | indexTable map[string]int 383 | indexCount int 384 | vertLines int 385 | textLines int 386 | normLines int 387 | faceLines int // stat-only 388 | triangles int // stat-only 389 | } 390 | 391 | // ObjParserOptions sets options for the parser. 392 | type ObjParserOptions struct { 393 | LogStats bool 394 | Logger func(string) 395 | IgnoreNormals bool 396 | } 397 | 398 | func (opt *ObjParserOptions) log(msg string) { 399 | if opt.Logger == nil { 400 | return 401 | } 402 | opt.Logger(msg) 403 | } 404 | 405 | func (o *Obj) newGroup(name, usemtl string, begin int, smooth int) *Group { 406 | gr := &Group{Name: name, Usemtl: usemtl, IndexBegin: begin, Smooth: smooth} 407 | o.Groups = append(o.Groups, gr) 408 | return gr 409 | } 410 | 411 | // Coord64 gets vertex data as float64. 412 | func (o *Obj) Coord64(i int) float64 { 413 | return float64(o.Coord[i]) 414 | } 415 | 416 | // NumberOfElements gets the number of strides. 417 | func (o *Obj) NumberOfElements() int { 418 | return 4 * len(o.Coord) / o.StrideSize 419 | } 420 | 421 | // VertexCoordinates gets vertex coordinates for a stride index. 422 | func (o *Obj) VertexCoordinates(stride int) (float32, float32, float32) { 423 | offset := o.StrideOffsetPosition / 4 424 | floatsPerStride := o.StrideSize / 4 425 | f := offset + stride*floatsPerStride 426 | return o.Coord[f], o.Coord[f+1], o.Coord[f+2] 427 | } 428 | 429 | // ToFile saves OBJ to file. 430 | func (o *Obj) ToFile(filename string) error { 431 | f, err := os.Create(filename) 432 | if err != nil { 433 | return err 434 | } 435 | defer f.Close() 436 | return o.ToWriter(f) 437 | } 438 | 439 | // ToWriter writes OBJ to writer stream. 440 | func (o *Obj) ToWriter(w io.Writer) error { 441 | 442 | fmt.Fprintf(w, "# OBJ exported by gwob - https://github.com/udhos/gwob\n") 443 | fmt.Fprintf(w, "\n") 444 | 445 | if o.Mtllib != "" { 446 | fmt.Fprintf(w, "mtllib %s\n", o.Mtllib) 447 | } 448 | 449 | // write vertex data 450 | strides := o.NumberOfElements() 451 | for s := 0; s < strides; s++ { 452 | stride := s * o.StrideSize / 4 453 | v := stride + o.StrideOffsetPosition/4 454 | fmt.Fprintf(w, "v %f %f %f\n", o.Coord[v], o.Coord[v+1], o.Coord[v+2]) 455 | 456 | if o.TextCoordFound { 457 | t := stride + o.StrideOffsetTexture/4 458 | fmt.Fprintf(w, "vt %f %f\n", o.Coord[t], o.Coord[t+1]) 459 | } 460 | 461 | if o.NormCoordFound { 462 | n := stride + o.StrideOffsetNormal/4 463 | fmt.Fprintf(w, "vn %f %f %f\n", o.Coord[n], o.Coord[n+1], o.Coord[n+2]) 464 | } 465 | } 466 | 467 | // write group faces 468 | for _, g := range o.Groups { 469 | if g.Name != "" { 470 | fmt.Fprintf(w, "g %s\n", g.Name) 471 | } 472 | if g.Usemtl != "" { 473 | fmt.Fprintf(w, "usemtl %s\n", g.Usemtl) 474 | } 475 | fmt.Fprintf(w, "s %d\n", g.Smooth) 476 | if g.IndexCount%3 != 0 { 477 | return fmt.Errorf("group=%s count=%d must be a multiple of 3", g.Name, g.IndexCount) 478 | } 479 | pastEnd := g.IndexBegin + g.IndexCount 480 | for s := g.IndexBegin; s < pastEnd; s += 3 { 481 | fmt.Fprintf(w, "f") 482 | for f := s; f < s+3; f++ { 483 | ff := o.Indices[f] + 1 484 | str := strconv.Itoa(ff) 485 | if o.TextCoordFound { 486 | if o.NormCoordFound { 487 | fmt.Fprintf(w, " %s/%s/%s", str, str, str) 488 | } else { 489 | fmt.Fprintf(w, " %s/%s", str, str) 490 | } 491 | } else { 492 | if o.NormCoordFound { 493 | fmt.Fprintf(w, " %s//%s", str, str) 494 | } else { 495 | fmt.Fprintf(w, " %s", str) 496 | } 497 | } 498 | } 499 | fmt.Fprintf(w, "\n") 500 | } 501 | } 502 | 503 | return nil 504 | } 505 | 506 | // NewObjFromVertex creates Obj from vertex data. 507 | func NewObjFromVertex(coord []float32, indices []int) (*Obj, error) { 508 | o := &Obj{} 509 | 510 | group := o.newGroup("", "", 0, 0) 511 | 512 | o.Coord = append(o.Coord, coord...) 513 | for _, ind := range indices { 514 | pushIndex(group, o, ind) 515 | } 516 | 517 | setupStride(o) 518 | 519 | return o, nil 520 | } 521 | 522 | // NewObjFromBuf parses Obj from a buffer. 523 | func NewObjFromBuf(objName string, buf []byte, options *ObjParserOptions) (*Obj, error) { 524 | return readObj(objName, bytes.NewBuffer(buf), options) 525 | } 526 | 527 | // NewObjFromReader parses Obj from a reader. 528 | func NewObjFromReader(objName string, rd io.Reader, options *ObjParserOptions) (*Obj, error) { 529 | return readObj(objName, bufio.NewReader(rd), options) 530 | } 531 | 532 | // NewObjFromStringReader parses Obj from a StringReader. 533 | func NewObjFromStringReader(objName string, rd StringReader, options *ObjParserOptions) (*Obj, error) { 534 | return readObj(objName, rd, options) 535 | } 536 | 537 | // NewObjFromFile parses Obj from a file. 538 | func NewObjFromFile(filename string, options *ObjParserOptions) (*Obj, error) { 539 | 540 | input, errOpen := os.Open(filename) 541 | if errOpen != nil { 542 | return nil, errOpen 543 | } 544 | 545 | defer input.Close() 546 | 547 | return NewObjFromReader(filename, input, options) 548 | } 549 | 550 | func setupStride(o *Obj) { 551 | o.StrideSize = 3 * 4 // (px,py,pz) = 3 x 4-byte floats 552 | o.StrideOffsetPosition = 0 553 | o.StrideOffsetTexture = 0 554 | o.StrideOffsetNormal = 0 555 | 556 | if o.TextCoordFound { 557 | o.StrideOffsetTexture = o.StrideSize 558 | o.StrideSize += 2 * 4 // add (tu,tv) = 2 x 4-byte floats 559 | } 560 | 561 | if o.NormCoordFound { 562 | o.StrideOffsetNormal = o.StrideSize 563 | o.StrideSize += 3 * 4 // add (nx,ny,nz) = 3 x 4-byte floats 564 | } 565 | } 566 | 567 | func readObj(objName string, reader StringReader, options *ObjParserOptions) (*Obj, error) { 568 | 569 | if options == nil { 570 | options = &ObjParserOptions{LogStats: true, Logger: func(msg string) { fmt.Print(msg) }} 571 | } 572 | 573 | p := &objParser{indexTable: make(map[string]int)} 574 | o := &Obj{} 575 | 576 | // 1. vertex-only parsing 577 | if fatal, err := readLines(p, reader, options); err != nil { 578 | if fatal { 579 | return o, err 580 | } 581 | } 582 | 583 | p.faceLines = 0 584 | p.vertLines = 0 585 | p.textLines = 0 586 | p.normLines = 0 587 | 588 | // 2. full parsing 589 | if fatal, err := scanLines(p, o, options); err != nil { 590 | if fatal { 591 | return o, err 592 | } 593 | } 594 | 595 | // 3. output 596 | 597 | // drop empty groups 598 | tmp := []*Group{} 599 | for _, g := range o.Groups { 600 | switch { 601 | case g.IndexCount < 0: 602 | continue // discard empty bogus group created internally by parser 603 | case g.IndexCount < 3: 604 | options.log(fmt.Sprintf("readObj: obj=%s BAD GROUP SIZE group=%s size=%d < 3", objName, g.Name, g.IndexCount)) 605 | } 606 | tmp = append(tmp, g) 607 | } 608 | o.Groups = tmp 609 | 610 | setupStride(o) // setup stride size 611 | 612 | if options.LogStats { 613 | options.log(fmt.Sprintf("readObj: INPUT lines=%v vertLines=%v textLines=%v normLines=%v faceLines=%v triangles=%v", 614 | p.lineCount, p.vertLines, p.textLines, p.normLines, p.faceLines, p.triangles)) 615 | 616 | options.log(fmt.Sprintf("readObj: STATS numberOfElements=%v indicesArraySize=%v", p.indexCount, len(o.Indices))) 617 | options.log(fmt.Sprintf("readObj: STATS bigIndexFound=%v groups=%v", o.BigIndexFound, len(o.Groups))) 618 | options.log(fmt.Sprintf("readObj: STATS textureCoordFound=%v normalCoordFound=%v", o.TextCoordFound, o.NormCoordFound)) 619 | options.log(fmt.Sprintf("readObj: STATS stride=%v textureOffset=%v normalOffset=%v", o.StrideSize, o.StrideOffsetTexture, o.StrideOffsetNormal)) 620 | for _, g := range o.Groups { 621 | options.log(fmt.Sprintf("readObj: GROUP name=%s first=%d count=%d", g.Name, g.IndexBegin, g.IndexCount)) 622 | } 623 | } 624 | 625 | return o, nil 626 | } 627 | 628 | func readLines(p *objParser, reader StringReader, options *ObjParserOptions) (bool, error) { 629 | p.lineCount = 0 630 | 631 | for { 632 | p.lineCount++ 633 | line, err := reader.ReadString('\n') 634 | if err == io.EOF { 635 | // parse last line 636 | if fatal, e := parseLineVertex(p, line, options); e != nil { 637 | options.log(fmt.Sprintf("readLines: %v", e)) 638 | return fatal, e 639 | } 640 | break // EOF 641 | } 642 | 643 | if err != nil { 644 | // unexpected IO error 645 | return ErrFatal, fmt.Errorf("readLines: error: %v", err) 646 | } 647 | 648 | if fatal, e := parseLineVertex(p, line, options); e != nil { 649 | options.log(fmt.Sprintf("readLines: %v", e)) 650 | if fatal { 651 | return fatal, e 652 | } 653 | } 654 | } 655 | 656 | return ErrNonFatal, nil 657 | } 658 | 659 | // parseLineVertex: parse only vertex lines 660 | func parseLineVertex(p *objParser, rawLine string, options *ObjParserOptions) (bool, error) { 661 | line := strings.TrimSpace(rawLine) 662 | 663 | p.lineBuf = append(p.lineBuf, line) // save line for 2nd pass 664 | 665 | switch { 666 | case line == "" || line[0] == '#': 667 | case strings.HasPrefix(line, "s "): 668 | case strings.HasPrefix(line, "o "): 669 | case strings.HasPrefix(line, "g "): 670 | case strings.HasPrefix(line, "usemtl "): 671 | case strings.HasPrefix(line, "mtllib "): 672 | case strings.HasPrefix(line, "f "): 673 | case strings.HasPrefix(line, "vt "): 674 | 675 | tex := line[3:] 676 | t, err := parseFloatSliceSpace(tex) 677 | if err != nil { 678 | return ErrNonFatal, fmt.Errorf("parseLine: line=%d bad vertex texture=[%s]: %v", p.lineCount, tex, err) 679 | } 680 | size := len(t) 681 | if size < 2 || size > 3 { 682 | return ErrNonFatal, fmt.Errorf("parseLine: line=%d bad vertex texture=[%s] size=%d", p.lineCount, tex, size) 683 | } 684 | if size > 2 { 685 | if w := t[2]; !closeToZero(w) { 686 | options.log(fmt.Sprintf("parseLine: line=%d non-zero third texture coordinate w=%f: [%v]", p.lineCount, w, line)) 687 | } 688 | } 689 | p.textCoord = append(p.textCoord, float32(t[0]), float32(t[1])) 690 | 691 | case strings.HasPrefix(line, "vn "): 692 | 693 | norm := line[3:] 694 | n, err := parseFloatVector3Space(norm) 695 | if err != nil { 696 | return ErrNonFatal, fmt.Errorf("parseLine: line=%d bad vertex normal=[%s]: %v", p.lineCount, norm, err) 697 | } 698 | p.normCoord = append(p.normCoord, float32(n[0]), float32(n[1]), float32(n[2])) 699 | 700 | case strings.HasPrefix(line, "v "): 701 | 702 | result, err := parseFloatSliceSpace(line[2:]) 703 | if err != nil { 704 | return ErrNonFatal, fmt.Errorf("parseLine %v: [%v]: error: %v", p.lineCount, line, err) 705 | } 706 | coordLen := len(result) 707 | switch coordLen { 708 | case 3: 709 | p.vertCoord = append(p.vertCoord, float32(result[0]), float32(result[1]), float32(result[2])) 710 | case 4: 711 | w := result[3] 712 | p.vertCoord = append(p.vertCoord, float32(result[0]/w), float32(result[1]/w), float32(result[2]/w)) 713 | default: 714 | return ErrNonFatal, fmt.Errorf("parseLine %v: [%v]: bad number of coords: %v", p.lineCount, line, coordLen) 715 | } 716 | 717 | default: 718 | return ErrNonFatal, fmt.Errorf("parseLine %v: [%v]: unexpected", p.lineCount, line) 719 | } 720 | 721 | return ErrNonFatal, nil 722 | } 723 | 724 | func scanLines(p *objParser, o *Obj, options *ObjParserOptions) (bool, error) { 725 | 726 | p.currGroup = o.newGroup("", "", 0, 0) 727 | 728 | p.lineCount = 0 729 | 730 | for _, line := range p.lineBuf { 731 | p.lineCount++ 732 | 733 | if fatal, e := parseLine(p, o, line, options); e != nil { 734 | options.log(fmt.Sprintf("scanLines: %v", e)) 735 | if fatal { 736 | return fatal, e 737 | } 738 | } 739 | } 740 | 741 | return ErrNonFatal, nil 742 | } 743 | 744 | func solveRelativeIndex(index, size int) int { 745 | if index > 0 { 746 | return index - 1 747 | } 748 | return size + index 749 | } 750 | 751 | func splitSlash(s string) []string { 752 | isSlash := func(c rune) bool { 753 | return c == '/' 754 | } 755 | 756 | return strings.FieldsFunc(s, isSlash) 757 | 758 | } 759 | 760 | func pushIndex(currGroup *Group, o *Obj, i int) { 761 | if i > 65535 { 762 | o.BigIndexFound = true 763 | } 764 | o.Indices = append(o.Indices, i) 765 | currGroup.IndexCount++ 766 | } 767 | 768 | func addVertex(p *objParser, o *Obj, index string, options *ObjParserOptions) error { 769 | ind := splitSlash(strings.Replace(index, "//", "/0/", 1)) 770 | size := len(ind) 771 | if size < 1 || size > 3 { 772 | return fmt.Errorf("addVertex: line=%d bad index=[%s] size=%d", p.lineCount, index, size) 773 | } 774 | 775 | v, err := strconv.ParseInt(ind[0], 10, 32) 776 | if err != nil { 777 | return fmt.Errorf("addVertex: line=%d bad integer 1st index=[%s]: %v", p.lineCount, ind[0], err) 778 | } 779 | vi := solveRelativeIndex(int(v), p.vertLines) 780 | 781 | var ti int 782 | var tIndex string 783 | hasTextureCoord := strings.Index(index, "//") == -1 && size > 1 784 | if hasTextureCoord { 785 | t, e := strconv.ParseInt(ind[1], 10, 32) 786 | if e != nil { 787 | return fmt.Errorf("addVertex: line=%d bad integer 2nd index=[%s]: %v", p.lineCount, ind[1], e) 788 | } 789 | ti = solveRelativeIndex(int(t), p.textLines) 790 | tIndex = strconv.Itoa(ti) 791 | } 792 | 793 | var ni int 794 | var nIndex string 795 | if size > 2 { 796 | n, e := strconv.ParseInt(ind[2], 10, 32) 797 | if e != nil { 798 | return fmt.Errorf("addVertex: line=%d bad integer 3rd index=[%s]: %v", p.lineCount, ind[2], e) 799 | } 800 | ni = solveRelativeIndex(int(n), p.normLines) 801 | nIndex = strconv.Itoa(ni) 802 | } 803 | 804 | absIndex := fmt.Sprintf("%d/%s/%s", vi, tIndex, nIndex) 805 | 806 | // known unified index? 807 | if i, ok := p.indexTable[absIndex]; ok { 808 | pushIndex(p.currGroup, o, i) 809 | return nil 810 | } 811 | 812 | vOffset := vi * 3 813 | if vOffset+2 >= len(p.vertCoord) { 814 | return fmt.Errorf("err: line=%d invalid vertex index=[%s]", p.lineCount, ind[0]) 815 | } 816 | 817 | o.Coord = append(o.Coord, p.vertCoord[vOffset+0]) // x 818 | o.Coord = append(o.Coord, p.vertCoord[vOffset+1]) // y 819 | o.Coord = append(o.Coord, p.vertCoord[vOffset+2]) // z 820 | 821 | if tIndex != "" && hasTextureCoord { 822 | tOffset := ti * 2 823 | 824 | if tOffset+1 >= len(p.textCoord) { 825 | return fmt.Errorf("err: line=%d invalid texture index=[%s]", p.lineCount, ind[1]) 826 | } 827 | 828 | o.Coord = append(o.Coord, p.textCoord[tOffset+0]) // u 829 | o.Coord = append(o.Coord, p.textCoord[tOffset+1]) // v 830 | o.TextCoordFound = true 831 | } 832 | 833 | if !options.IgnoreNormals && nIndex != "" { 834 | nOffset := ni * 3 835 | 836 | o.Coord = append(o.Coord, p.normCoord[nOffset+0]) // x 837 | o.Coord = append(o.Coord, p.normCoord[nOffset+1]) // y 838 | o.Coord = append(o.Coord, p.normCoord[nOffset+2]) // z 839 | 840 | o.NormCoordFound = true 841 | } 842 | 843 | // add unified index 844 | pushIndex(p.currGroup, o, p.indexCount) 845 | p.indexTable[absIndex] = p.indexCount 846 | p.indexCount++ 847 | 848 | return nil 849 | } 850 | 851 | func smoothGroup(s string) (int, error) { 852 | s = strings.ToLower(strings.TrimSpace(s)) 853 | 854 | if s == "off" { 855 | return 0, nil 856 | } 857 | 858 | i, err := strconv.ParseInt(s, 0, 32) 859 | 860 | return int(i), err 861 | } 862 | 863 | func parseLine(p *objParser, o *Obj, line string, options *ObjParserOptions) (bool, error) { 864 | 865 | switch { 866 | case line == "" || line[0] == '#': 867 | case strings.HasPrefix(line, "s "): 868 | smooth := line[2:] 869 | if s, err := smoothGroup(smooth); err == nil { 870 | if p.currGroup.Smooth != s { 871 | if p.currGroup.IndexCount == 0 { 872 | // mark previous empty group as bogus 873 | p.currGroup.IndexCount = -1 874 | } 875 | // create new group 876 | p.currGroup = o.newGroup(p.currGroup.Name, p.currGroup.Usemtl, len(o.Indices), s) 877 | } 878 | } else { 879 | return ErrNonFatal, fmt.Errorf("parseLine: line=%d bad boolean smooth=[%s]: %v: line=[%v]", p.lineCount, smooth, err, line) 880 | } 881 | case strings.HasPrefix(line, "o ") || strings.HasPrefix(line, "g "): 882 | name := line[2:] 883 | if p.currGroup.Name == "" { 884 | // only set missing name for group 885 | p.currGroup.Name = name 886 | } else if p.currGroup.Name != name { 887 | // create new group 888 | p.currGroup = o.newGroup(name, p.currGroup.Usemtl, len(o.Indices), p.currGroup.Smooth) 889 | } 890 | case strings.HasPrefix(line, "usemtl "): 891 | usemtl := line[7:] 892 | if p.currGroup.Usemtl == "" { 893 | // only set the missing material name for group 894 | p.currGroup.Usemtl = usemtl 895 | } else if p.currGroup.Usemtl != usemtl { 896 | if p.currGroup.IndexCount == 0 { 897 | // mark previous empty group as bogus 898 | p.currGroup.IndexCount = -1 899 | } 900 | // create new group for material 901 | p.currGroup = o.newGroup(p.currGroup.Name, usemtl, len(o.Indices), p.currGroup.Smooth) 902 | } 903 | case strings.HasPrefix(line, "mtllib "): 904 | mtllib := line[7:] 905 | if o.Mtllib != "" { 906 | options.log(fmt.Sprintf("parseLine: line=%d mtllib redefinition old=%s new=%s", p.lineCount, o.Mtllib, mtllib)) 907 | } 908 | o.Mtllib = mtllib 909 | case strings.HasPrefix(line, "f "): 910 | p.faceLines++ 911 | 912 | face := line[2:] 913 | f := strings.Fields(face) 914 | size := len(f) 915 | if size < 3 || size > 4 { 916 | return ErrNonFatal, fmt.Errorf("parseLine: line=%d bad face=[%s] size=%d", p.lineCount, face, size) 917 | } 918 | // triangle face: v0 v1 v2 919 | // quad face: 920 | // v0 v1 v2 v3 => 921 | // v0 v1 v2 922 | // v2 v3 v0 923 | p.triangles++ 924 | if err := addVertex(p, o, f[0], options); err != nil { 925 | return ErrNonFatal, fmt.Errorf("parseLine: line=%d bad face=[%s] index_v0=[%s]: %v", p.lineCount, face, f[0], err) 926 | } 927 | if err := addVertex(p, o, f[1], options); err != nil { 928 | return ErrNonFatal, fmt.Errorf("parseLine: line=%d bad face=[%s] index_v1=[%s]: %v", p.lineCount, face, f[1], err) 929 | } 930 | if err := addVertex(p, o, f[2], options); err != nil { 931 | return ErrNonFatal, fmt.Errorf("parseLine: line=%d bad face=[%s] index_v2=[%s]: %v", p.lineCount, face, f[2], err) 932 | } 933 | if size > 3 { 934 | // quad face 935 | p.triangles++ 936 | if err := addVertex(p, o, f[2], options); err != nil { 937 | return ErrNonFatal, fmt.Errorf("parseLine: line=%d bad face=[%s] index_v2=[%s]: %v", p.lineCount, face, f[2], err) 938 | } 939 | if err := addVertex(p, o, f[3], options); err != nil { 940 | return ErrNonFatal, fmt.Errorf("parseLine: line=%d bad face=[%s] index_v3=[%s]: %v", p.lineCount, face, f[3], err) 941 | } 942 | if err := addVertex(p, o, f[0], options); err != nil { 943 | return ErrNonFatal, fmt.Errorf("parseLine: line=%d bad face=[%s] index_v0=[%s]: %v", p.lineCount, face, f[0], err) 944 | } 945 | } 946 | case strings.HasPrefix(line, "v "): 947 | p.vertLines++ 948 | case strings.HasPrefix(line, "vt "): 949 | p.textLines++ 950 | case strings.HasPrefix(line, "vn "): 951 | p.normLines++ 952 | default: 953 | return ErrNonFatal, fmt.Errorf("parseLine %v: [%v]: unexpected", p.lineCount, line) 954 | } 955 | 956 | return ErrNonFatal, nil 957 | } 958 | 959 | func closeToZero(f float64) bool { 960 | return math.Abs(f-0) < 0.000001 961 | } 962 | -------------------------------------------------------------------------------- /obj_test.go: -------------------------------------------------------------------------------- 1 | package gwob 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func BenchmarkCube1(b *testing.B) { 10 | buf := []byte(cubeObj) 11 | options := &ObjParserOptions{} 12 | for i := 0; i < b.N; i++ { 13 | NewObjFromBuf("cubeObj", buf, options) 14 | } 15 | } 16 | 17 | func BenchmarkRelativeIndex1(b *testing.B) { 18 | buf := []byte(relativeObj) 19 | options := &ObjParserOptions{} 20 | for i := 0; i < b.N; i++ { 21 | NewObjFromBuf("relativeObj", buf, options) 22 | } 23 | } 24 | 25 | func BenchmarkForwardVertex1(b *testing.B) { 26 | buf := []byte(forwardObj) 27 | options := &ObjParserOptions{} 28 | for i := 0; i < b.N; i++ { 29 | NewObjFromBuf("forwardObj", buf, options) 30 | } 31 | } 32 | 33 | const LogStats = false 34 | 35 | func expectInt(t *testing.T, label string, want, got int) { 36 | if want != got { 37 | t.Errorf("%s: want=%d got=%d", label, want, got) 38 | } 39 | } 40 | 41 | func sliceEqualInt(a, b []int) bool { 42 | if len(a) != len(b) { 43 | return false 44 | } 45 | 46 | for i, v := range a { 47 | if v != b[i] { 48 | return false 49 | } 50 | } 51 | 52 | return true 53 | } 54 | 55 | func sliceEqualFloat(a, b []float32) bool { 56 | if len(a) != len(b) { 57 | return false 58 | } 59 | 60 | for i, v := range a { 61 | if v != b[i] { 62 | return false 63 | } 64 | } 65 | 66 | return true 67 | } 68 | 69 | func TestCube(t *testing.T) { 70 | 71 | options := ObjParserOptions{LogStats: LogStats, Logger: func(msg string) { fmt.Printf("TestCube NewObjFromBuf: log: %s\n", msg) }} 72 | 73 | o, err := NewObjFromBuf("cubeObj", []byte(cubeObj), &options) 74 | if err != nil { 75 | t.Errorf("TestCube: NewObjFromBuf: %v", err) 76 | return 77 | } 78 | 79 | if !sliceEqualInt(cubeIndices, o.Indices) { 80 | t.Errorf("TestCube: indices: want=%v got=%v", cubeIndices, o.Indices) 81 | } 82 | 83 | if !sliceEqualFloat(cubeCoord, o.Coord) { 84 | t.Errorf("TestCube: coord: want=%d%v got=%d%v", len(cubeCoord), cubeCoord, len(o.Coord), o.Coord) 85 | } 86 | 87 | if o.StrideSize != cubeStrideSize { 88 | t.Errorf("TestCube: stride size: want=%d got=%d", cubeStrideSize, o.StrideSize) 89 | } 90 | 91 | if o.StrideOffsetPosition != cubeStrideOffsetPosition { 92 | t.Errorf("TestCube: stride offset position: want=%d got=%d", cubeStrideOffsetPosition, o.StrideOffsetPosition) 93 | } 94 | 95 | if o.StrideOffsetTexture != cubeStrideOffsetTexture { 96 | t.Errorf("TestCube: stride offset texture: want=%d got=%d", cubeStrideOffsetTexture, o.StrideOffsetTexture) 97 | } 98 | 99 | if o.StrideOffsetNormal != cubeStrideOffsetNormal { 100 | t.Errorf("TestCube: stride offset normal: want=%d got=%d", cubeStrideOffsetNormal, o.StrideOffsetNormal) 101 | } 102 | } 103 | 104 | func TestWriteEmpty(t *testing.T) { 105 | 106 | // load 107 | options := ObjParserOptions{LogStats: LogStats, Logger: func(msg string) { fmt.Printf("TestWriteEmpty NewObjFromBuf: log: %s\n", msg) }} 108 | orig, err := NewObjFromBuf("empty", []byte{}, &options) 109 | if err != nil { 110 | t.Errorf("TestWriteEmpty: NewObjFromBuf: %v", err) 111 | return 112 | } 113 | 114 | // export 115 | buf := bytes.Buffer{} 116 | errWrite := orig.ToWriter(&buf) 117 | if errWrite != nil { 118 | t.Errorf("TestWriteEmpty: ToWriter: %v", errWrite) 119 | return 120 | } 121 | 122 | // reload 123 | _, errParse := NewObjFromReader("empty-reload", &buf, &options) 124 | if errParse != nil { 125 | t.Errorf("TestWriteEmpty: NewObjFromReader: %v", errParse) 126 | return 127 | } 128 | } 129 | 130 | func TestWriteBad(t *testing.T) { 131 | 132 | // load 133 | orig, err := NewObjFromVertex([]float32{}, []int{0}) 134 | if err != nil { 135 | t.Errorf("TestWriteBad: NewObjFromVertex: %v", err) 136 | return 137 | } 138 | 139 | // export 140 | buf := bytes.Buffer{} 141 | errWrite := orig.ToWriter(&buf) 142 | if errWrite == nil { 143 | t.Errorf("TestWriteBad: unexpected writer success for bad group index count (non multiple of 3)") 144 | return 145 | } 146 | 147 | } 148 | 149 | func TestCubeWrite(t *testing.T) { 150 | 151 | // load cube 152 | options := ObjParserOptions{LogStats: LogStats, Logger: func(msg string) { fmt.Printf("TestCube NewObjFromBuf: log: %s\n", msg) }} 153 | orig, err := NewObjFromBuf("cube-orig", []byte(cubeObj), &options) 154 | if err != nil { 155 | t.Errorf("TestCubeWrite: NewObjFromBuf: %v", err) 156 | return 157 | } 158 | 159 | // export cube 160 | buf := bytes.Buffer{} 161 | errWrite := orig.ToWriter(&buf) 162 | if errWrite != nil { 163 | t.Errorf("TestCubeWrite: ToWriter: %v", errWrite) 164 | return 165 | } 166 | 167 | // reload cube 168 | o, errParse := NewObjFromReader("cube-reload", &buf, &options) 169 | if errParse != nil { 170 | t.Errorf("TestCubeWrite: NewObjFromReader: %v", errParse) 171 | return 172 | } 173 | 174 | if !sliceEqualInt(cubeIndices, o.Indices) { 175 | t.Errorf("TestCubeWrite: indices: want=%v got=%v", cubeIndices, o.Indices) 176 | } 177 | 178 | if !sliceEqualFloat(cubeCoord, o.Coord) { 179 | t.Errorf("TestCubeWrite: coord: want=%d%v got=%d%v", len(cubeCoord), cubeCoord, len(o.Coord), o.Coord) 180 | } 181 | 182 | if o.StrideSize != cubeStrideSize { 183 | t.Errorf("TestCubeWrite: stride size: want=%d got=%d", cubeStrideSize, o.StrideSize) 184 | } 185 | 186 | if o.StrideOffsetPosition != cubeStrideOffsetPosition { 187 | t.Errorf("TestCubeWrite: stride offset position: want=%d got=%d", cubeStrideOffsetPosition, o.StrideOffsetPosition) 188 | } 189 | 190 | if o.StrideOffsetTexture != cubeStrideOffsetTexture { 191 | t.Errorf("TestCubeWrite: stride offset texture: want=%d got=%d", cubeStrideOffsetTexture, o.StrideOffsetTexture) 192 | } 193 | 194 | if o.StrideOffsetNormal != cubeStrideOffsetNormal { 195 | t.Errorf("TestCubeWrite: stride offset normal: want=%d got=%d", cubeStrideOffsetNormal, o.StrideOffsetNormal) 196 | } 197 | } 198 | 199 | func TestRelativeIndex(t *testing.T) { 200 | 201 | options := ObjParserOptions{LogStats: LogStats, Logger: func(msg string) { fmt.Printf("TestRelativeIndex NewObjFromBuf: log: %s\n", msg) }} 202 | 203 | o, err := NewObjFromBuf("relativeObj", []byte(relativeObj), &options) 204 | if err != nil { 205 | t.Errorf("TestRelativeIndex: NewObjFromBuf: %v", err) 206 | return 207 | } 208 | 209 | //indices := o.Indices[:len(o.Indices):len(o.Indices)] 210 | if !sliceEqualInt(relativeIndices, o.Indices) { 211 | t.Errorf("TestRelativeIndex: indices: want=%v got=%v", relativeIndices, o.Indices) 212 | } 213 | 214 | //coord := o.Coord[:len(o.Coord):len(o.Coord)] 215 | if !sliceEqualFloat(relativeCoord, o.Coord) { 216 | t.Errorf("TestRelativeIndex: coord: want=%v got=%v", relativeCoord, o.Coord) 217 | } 218 | } 219 | 220 | func TestForwardVertex(t *testing.T) { 221 | 222 | options := ObjParserOptions{LogStats: LogStats, Logger: func(msg string) { fmt.Printf("TestForwardVertex NewObjFromBuf: log: %s\n", msg) }} 223 | 224 | o, err := NewObjFromBuf("forwardObj", []byte(forwardObj), &options) 225 | if err != nil { 226 | t.Errorf("TestForwardVertex: NewObjFromBuf: %v", err) 227 | return 228 | } 229 | 230 | if !sliceEqualInt(forwardIndices, o.Indices) { 231 | t.Errorf("TestForwardVertex: indices: want=%v got=%v", forwardIndices, o.Indices) 232 | } 233 | 234 | if !sliceEqualFloat(forwardCoord, o.Coord) { 235 | t.Errorf("TestForwardVertex: coord: want=%v got=%v", forwardCoord, o.Coord) 236 | } 237 | } 238 | 239 | func TestMisc(t *testing.T) { 240 | str := ` 241 | mtllib lib1 242 | 243 | usemtl mtl1 244 | usemtl mtl2 245 | 246 | s off 247 | s 1 248 | ` 249 | 250 | options := ObjParserOptions{LogStats: LogStats, Logger: func(msg string) { fmt.Printf("TestMisc NewObjFromBuf: log: %s\n", msg) }} 251 | 252 | _, err := NewObjFromBuf("TestMisc local str obj", []byte(str), &options) 253 | if err != nil { 254 | t.Errorf("NewObjFromBuf: %v", err) 255 | } 256 | } 257 | 258 | func TestSkippedUV1(t *testing.T) { 259 | 260 | options := ObjParserOptions{LogStats: LogStats, Logger: func(msg string) { fmt.Printf("TestSkippedUV NewObjFromBuf: log: %s\n", msg) }} 261 | 262 | o, err := NewObjFromBuf("skippedUV", []byte(skippedUVObj), &options) 263 | if err != nil { 264 | t.Errorf("TestSkippedUV: NewObjFromBuf: %v", err) 265 | return 266 | } 267 | 268 | if !sliceEqualInt(skippedUVIndices, o.Indices) { 269 | t.Errorf("TestSkippedUV: indices: want=%v got=%v", skippedUVIndices, o.Indices) 270 | } 271 | 272 | if !sliceEqualFloat(skippedUVCoord, o.Coord) { 273 | t.Errorf("TestSkippedUV: coord: want=%v got=%v", skippedUVCoord, o.Coord) 274 | } 275 | } 276 | 277 | func TestSkippedUV2(t *testing.T) { 278 | 279 | options := ObjParserOptions{LogStats: LogStats, Logger: func(msg string) { fmt.Printf("TestSkippedUV2 NewObjFromBuf: log: %s\n", msg) }} 280 | 281 | o, err := NewObjFromBuf("skippedUV2", []byte(skippedUV2Obj), &options) 282 | if err != nil { 283 | t.Errorf("TestSkippedUV2: NewObjFromBuf: %v", err) 284 | return 285 | } 286 | 287 | if !sliceEqualInt(skippedUV2Indices, o.Indices) { 288 | t.Errorf("TestSkippedUV2: indices: want=%v got=%v", skippedUV2Indices, o.Indices) 289 | } 290 | 291 | if !sliceEqualFloat(skippedUV2Coord, o.Coord) { 292 | t.Errorf("TestSkippedUV2: coord: want=%v got=%v", skippedUV2Coord, o.Coord) 293 | } 294 | } 295 | 296 | func TestSmoothGroup1(t *testing.T) { 297 | 298 | options := ObjParserOptions{LogStats: LogStats, Logger: func(msg string) { fmt.Printf("TestSmoothGroup1 NewObjFromBuf: log: %s\n", msg) }} 299 | 300 | o, err := NewObjFromBuf("smoothGroupObj1", []byte(smoothGroupObj1), &options) 301 | if err != nil { 302 | t.Errorf("TestSmoothGroup1: NewObjFromBuf: %v", err) 303 | return 304 | } 305 | 306 | if len(o.Groups) != 1 { 307 | t.Errorf("smoothGroupObj1: groups: want=%d got=%d", 1, len(o.Groups)) 308 | } 309 | } 310 | 311 | func TestSmoothGroup2(t *testing.T) { 312 | 313 | options := ObjParserOptions{LogStats: LogStats, Logger: func(msg string) { fmt.Printf("TestSmoothGroup1 NewObjFromBuf: log: %s\n", msg) }} 314 | 315 | o, err := NewObjFromBuf("smoothGroupObj1", []byte(smoothGroupObj2), &options) 316 | if err != nil { 317 | t.Errorf("TestSmoothGroup2: NewObjFromBuf: %v", err) 318 | return 319 | } 320 | 321 | if len(o.Groups) != 1 { 322 | t.Errorf("smoothGroupObj2: groups: want=%d got=%d", 1, len(o.Groups)) 323 | } 324 | } 325 | 326 | var cubeStrideSize = 32 327 | var cubeStrideOffsetPosition = 0 328 | var cubeStrideOffsetTexture = 12 329 | var cubeStrideOffsetNormal = 20 330 | var cubeIndices = []int{0, 1, 2, 2, 3, 0, 4, 5, 6, 6, 7, 4, 8, 9, 10, 10, 11, 8, 12, 13, 14, 14, 15, 12, 16, 17, 18, 18, 19, 16, 20, 21, 22, 22, 23, 20} 331 | var cubeCoord = []float32{1, -1, 1, 0.5, 0, 0, -1, 0, -1, -1, 1, 0.5, 0, 0, -1, 0, -1, -1, -1, 0.5, 0, 0, -1, 0, 1, -1, -1, 0.5, 0, 0, -1, 0, 1, 1, -1, 0.5, 0, 0, 1, 0, -1, 1, -1, 0.5, 0, 0, 1, 0, -1, 1, 1, 0.5, 0, 0, 1, 0, 1, 1, 1, 0.5, 0, 0, 1, 0, 1, -1, -1, 0, 0, 1, 0, 0, 1, 1, -1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, -1, 1, 0, 0, 1, 0, 0, -1, -1, 1, 0, 0, -1, 0, 0, -1, 1, 1, 0, 0, -1, 0, 0, -1, 1, -1, 0, 0, -1, 0, 0, -1, -1, -1, 0, 0, -1, 0, 0, 1, -1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, -1, 1, 1, 1, 0, 0, 0, 1, -1, -1, 1, 1, 0, 0, 0, 1, -1, -1, -1, 1, 0, 0, 0, -1, -1, 1, -1, 1, 0, 0, 0, -1, 1, 1, -1, 1, 0, 0, 0, -1, 1, -1, -1, 1, 0, 0, 0, -1} 332 | 333 | var relativeIndices = []int{0, 1, 2, 0, 1, 2, 3, 4, 5, 3, 4, 5, 0, 1, 2, 0, 1, 2} 334 | var relativeCoord = []float32{1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 4.0, 4.0, 5.0, 5.0, 5.0, 6.0, 6.0, 6.0} 335 | 336 | var forwardIndices = []int{0, 1, 2} 337 | var forwardCoord = []float32{1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 3.0, 3.0, 3.0} 338 | 339 | var cubeObj = ` 340 | # texture_cube.obj 341 | 342 | mtllib texture_cube.mtl 343 | 344 | o cube 345 | 346 | # square bottom 347 | v -1 -1 -1 348 | v -1 -1 1 349 | v 1 -1 1 350 | v 1 -1 -1 351 | 352 | # square top 353 | v -1 1 -1 354 | v -1 1 1 355 | v 1 1 1 356 | v 1 1 -1 357 | 358 | # uv coord 359 | 360 | # red -3 361 | vt 0 0 362 | 363 | # green -2 364 | vt .5 0 365 | 366 | # blue -1 367 | vt 1 0 368 | 369 | # normal coord 370 | 371 | # down -6 372 | vn 0 -1 0 373 | 374 | # up -5 375 | vn 0 1 0 376 | 377 | # right -4 378 | vn 1 0 0 379 | 380 | # left -3 381 | vn -1 0 0 382 | 383 | # front -2 384 | vn 0 0 1 385 | 386 | # back -1 387 | vn 0 0 -1 388 | 389 | usemtl 3-pixel-rgb 390 | 391 | # face down (green -2) 392 | f -6/-2/-6 -7/-2/-6 -8/-2/-6 393 | f -8/-2/-6 -5/-2/-6 -6/-2/-6 394 | 395 | # face up (green -2) 396 | f -1/-2/-5 -4/-2/-5 -3/-2/-5 397 | f -3/-2/-5 -2/-2/-5 -1/-2/-5 398 | 399 | # face right (red -3) 400 | f -5/-3/-4 -1/-3/-4 -2/-3/-4 401 | f -2/-3/-4 -6/-3/-4 -5/-3/-4 402 | 403 | # face left (red -3) 404 | f -7/-3/-3 -3/-3/-3 -4/-3/-3 405 | f -4/-3/-3 -8/-3/-3 -7/-3/-3 406 | 407 | # face front (blue -1) 408 | f -6/-1/-2 -2/-1/-2 -3/-1/-2 409 | f -3/-1/-2 -7/-1/-2 -6/-1/-2 410 | 411 | # face back (blue -1) 412 | f -8/-1/-1 -4/-1/-1 -1/-1/-1 413 | f -1/-1/-1 -5/-1/-1 -8/-1/-1 414 | ` 415 | 416 | var relativeObj = ` 417 | o relative_test 418 | v 1 1 1 419 | v 2 2 2 420 | v 3 3 3 421 | f 1 2 3 422 | # this line should affect indices, but not vertex array 423 | f -3 -2 -1 424 | v 4 4 4 425 | v 5 5 5 426 | v 6 6 6 427 | f 4 5 6 428 | # this line should affect indices, but not vertex array 429 | f -3 -2 -1 430 | # these lines should affect indices, but not vertex array 431 | f 1 2 3 432 | f -6 -5 -4 433 | ` 434 | 435 | var forwardObj = ` 436 | o forward_vertices_test 437 | # face pointing to forward vertex definitions 438 | # support for this isn't usual in OBJ parsers 439 | # since it requires multiple passes 440 | # but currently we do support this layout 441 | f 1 2 3 442 | v 1 1 1 443 | v 2 2 2 444 | v 3 3 3 445 | ` 446 | 447 | var skippedUVObj = ` 448 | 449 | o skipped_uv 450 | 451 | v 1 1 1 452 | v 2 2 2 453 | v 3 3 3 454 | 455 | vn 1 0 0 456 | vn 0 1 0 457 | vn 0 0 1 458 | 459 | f 1//1 2//2 3//3 460 | ` 461 | 462 | var skippedUVIndices = []int{0, 1, 2} 463 | var skippedUVCoord = []float32{1, 1, 1, 1, 0, 0, 2, 2, 2, 0, 1, 0, 3, 3, 3, 0, 0, 1} 464 | 465 | var skippedUV2Obj = ` 466 | 467 | o skipped_uv 468 | 469 | v 1 1 1 470 | v 2 2 2 471 | v 3 3 3 472 | 473 | vt 0 0 474 | vt .5 .5 475 | vt 1 1 476 | 477 | vn 1 0 0 478 | vn 0 1 0 479 | vn 0 0 1 480 | 481 | f 1//1 2//2 3//3 482 | ` 483 | 484 | var skippedUV2Indices = []int{0, 1, 2} 485 | var skippedUV2Coord = []float32{1, 1, 1, 1, 0, 0, 2, 2, 2, 0, 1, 0, 3, 3, 3, 0, 0, 1} 486 | 487 | const smoothGroupObj1 = ` 488 | o Cube.001 489 | v 1 -1 1 490 | v -1 -1 1 491 | v 1 1 1 492 | v -1 1 1 493 | v 1 -1 -1 494 | v 1 1 -1 495 | v -1 1 -1 496 | v -1 -1 -1 497 | vt 0 0 498 | vt 1 0 499 | vt 0 1 500 | vt 1 1 501 | vt 1 0 502 | vt 1 1 503 | vt 0 1 504 | vt 0 0 505 | vn 0 -1 0 506 | vn 0 1 0 507 | vn 1 0 0 508 | vn -1 0 0 509 | vn 0 0 -1 510 | vn 0 0 1 511 | f 1/1/6 3/3/6 4/4/6 512 | f 1/1/6 4/4/6 2/2/6 513 | f 1/1/1 2/2/1 8/8/1 514 | f 1/1/1 8/8/1 5/5/1 515 | f 1/1/3 5/5/3 6/6/3 516 | f 1/1/3 6/6/3 3/3/3 517 | ` 518 | 519 | const smoothGroupObj2 = ` 520 | o Cube.001 521 | v 1 -1 1 522 | v -1 -1 1 523 | v 1 1 1 524 | v -1 1 1 525 | v 1 -1 -1 526 | v 1 1 -1 527 | v -1 1 -1 528 | v -1 -1 -1 529 | vt 0 0 530 | vt 1 0 531 | vt 0 1 532 | vt 1 1 533 | vt 1 0 534 | vt 1 1 535 | vt 0 1 536 | vt 0 0 537 | vn 0 -1 0 538 | vn 0 1 0 539 | vn 1 0 0 540 | vn -1 0 0 541 | vn 0 0 -1 542 | vn 0 0 1 543 | s 1 544 | f 1/1/6 3/3/6 4/4/6 545 | f 1/1/6 4/4/6 2/2/6 546 | f 1/1/1 2/2/1 8/8/1 547 | f 1/1/1 8/8/1 5/5/1 548 | f 1/1/3 5/5/3 6/6/3 549 | f 1/1/3 6/6/3 3/3/3 550 | ` 551 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package gwob 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "unicode" 8 | ) 9 | 10 | func parseFloatSlice(list []string) ([]float64, error) { 11 | result := make([]float64, len(list)) 12 | 13 | for i, j := range list { 14 | j = strings.TrimSpace(j) 15 | var err error 16 | if result[i], err = strconv.ParseFloat(j, 64); err != nil { 17 | return nil, fmt.Errorf("parseFloatSlice: list=[%v] elem[%v]=[%s] failure: %v", list, i, j, err) 18 | } 19 | } 20 | 21 | return result, nil 22 | } 23 | 24 | func parseFloatSliceFunc(text string, f func(rune) bool) ([]float64, error) { 25 | return parseFloatSlice(strings.FieldsFunc(text, f)) 26 | } 27 | 28 | func parseFloatSliceSpace(text string) ([]float64, error) { 29 | return parseFloatSliceFunc(text, unicode.IsSpace) 30 | } 31 | 32 | func parseFloatVectorFunc(text string, size int, f func(rune) bool) ([]float64, error) { 33 | list := strings.FieldsFunc(text, f) 34 | if s := len(list); s != size { 35 | return nil, fmt.Errorf("parseFloatVectorFunc: text=[%v] size=%v must be %v", text, s, size) 36 | } 37 | 38 | return parseFloatSlice(list) 39 | } 40 | 41 | func parseFloatVectorSpace(text string, size int) ([]float64, error) { 42 | return parseFloatVectorFunc(text, size, unicode.IsSpace) 43 | } 44 | 45 | func parseFloatVectorComma(text string, size int) ([]float64, error) { 46 | isComma := func(c rune) bool { 47 | return c == ',' 48 | } 49 | 50 | return parseFloatVectorFunc(text, size, isComma) 51 | } 52 | 53 | func parseFloatVector3Space(text string) ([]float64, error) { 54 | return parseFloatVectorSpace(text, 3) 55 | } 56 | 57 | func parseFloatVector3Comma(text string) ([]float64, error) { 58 | return parseFloatVectorComma(text, 3) 59 | } 60 | --------------------------------------------------------------------------------