-
Notifications
You must be signed in to change notification settings - Fork 16
/
Copy pathobj-parser.go
205 lines (178 loc) · 6.46 KB
/
obj-parser.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
package main
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"runtime/debug"
"strings"
"time"
"github.com/jonnenauha/obj-simplify/objectfile"
)
var (
ObjectsParsed int
GroupsParsed int
)
func ParseFile(path string) (*objectfile.OBJ, int, error) {
f, err := os.Open(path)
if err != nil {
return nil, -1, err
}
defer f.Close()
return parse(f)
}
func ParseBytes(b []byte) (*objectfile.OBJ, int, error) {
return parse(bytes.NewBuffer(b))
}
func parse(src io.Reader) (*objectfile.OBJ, int, error) {
dest := objectfile.NewOBJ()
geom := dest.Geometry
scanner := bufio.NewScanner(src)
linenum := 0
var (
currentObject *objectfile.Object
currentObjectName string
currentObjectChildIndex int
currentMaterial string
currentSmoothGroup string
)
fakeObject := func(material string) *objectfile.Object {
ot := objectfile.ChildObject
if currentObject != nil {
ot = currentObject.Type
}
currentObjectChildIndex++
name := fmt.Sprintf("%s_%d", currentObjectName, currentObjectChildIndex)
return dest.CreateObject(ot, name, material)
}
for scanner.Scan() {
linenum++
line := strings.TrimSpace(scanner.Text())
if len(line) == 0 {
continue
}
t, value := parseLineType(line)
// Force GC and release mem to OS for >1 million
// line source files, every million lines.
//
// @todo We should also do data structure optimizations to handle
// multiple gig source files without swapping on low mem machines.
// A 4.5gb 82 million line test source file starts swapping on my 8gb
// mem machine (though this app used ~5gb) at about the 40 million line mark.
//
// Above should be done when actualy users have a real use case for such
// large files :)
if linenum%1000000 == 0 {
rt := time.Now()
debug.FreeOSMemory()
logInfo("%s lines parsed - Forced GC took %s", formatInt(linenum), formatDurationSince(rt))
}
switch t {
// comments
case objectfile.Comment:
if currentObject == nil && len(dest.MaterialLibraries) == 0 {
dest.Comments = append(dest.Comments, value)
} else if currentObject != nil {
// skip comments that might refecence vertex, normal, uv, polygon etc.
// counts as they wont be most likely true after this tool is done.
if len(value) > 0 && !strContainsAny(value, []string{"vertices", "normals", "uvs", "texture coords", "polygons", "triangles"}, caseInsensitive) {
currentObject.Comments = append(currentObject.Comments, value)
}
}
// mtl file ref
case objectfile.MtlLib:
dest.MaterialLibraries = append(dest.MaterialLibraries, value)
// geometry
case objectfile.Vertex, objectfile.Normal, objectfile.UV, objectfile.Param:
if _, err := geom.ReadValue(t, value, StartParams.Strict); err != nil {
return nil, linenum, wrapErrorLine(err, linenum)
}
// object, group
case objectfile.ChildObject, objectfile.ChildGroup:
currentObjectName = value
currentObjectChildIndex = 0
// inherit currently declared material
currentObject = dest.CreateObject(t, currentObjectName, currentMaterial)
if t == objectfile.ChildObject {
ObjectsParsed++
} else if t == objectfile.ChildGroup {
GroupsParsed++
}
// object: material
case objectfile.MtlUse:
// obj files can define multiple materials inside a single object/group.
// usually these are small face groups that kill performance on 3D engines
// as they have to render hundreds or thousands of meshes with the same material,
// each mesh containing a few faces.
//
// this app will convert all these "multi material" objects into
// separate object, later merging all meshes with the same material into
// a single draw call geometry.
//
// this might be undesirable for certain users, renderers and authoring software,
// in this case don't use this simplified on your obj files. simple as that.
// only fake if an object has been declared
if currentObject != nil {
// only fake if the current object has declared vertex data (faces etc.)
// and the material name actually changed (ecountering the same usemtl
// multiple times in a row would be rare, but check for completeness)
if len(currentObject.VertexData) > 0 && currentObject.Material != value {
currentObject = fakeObject(value)
}
}
// store material value for inheriting
currentMaterial = value
// set material to current object
if currentObject != nil {
currentObject.Material = currentMaterial
}
// object: faces
case objectfile.Face, objectfile.Line, objectfile.Point:
// most tools support the file not defining a o/g prior to face declarations.
// I'm not sure if the spec allows not declaring any o/g.
// Our data structures and parsing however requires objects to put the faces into,
// create a default object that is named after the input file (without suffix).
if currentObject == nil {
currentObject = dest.CreateObject(objectfile.ChildObject, fileBasename(StartParams.Input), currentMaterial)
}
vd, vdErr := currentObject.ReadVertexData(t, value, StartParams.Strict)
if vdErr != nil {
return nil, linenum, wrapErrorLine(vdErr, linenum)
}
// attach current smooth group and reset it
if len(currentSmoothGroup) > 0 {
vd.SetMeta(objectfile.SmoothingGroup, currentSmoothGroup)
currentSmoothGroup = ""
}
case objectfile.SmoothingGroup:
// smooth group can change mid vertex data declaration
// so it is attched to the vertex data instead of current object directly
currentSmoothGroup = value
// unknown
case objectfile.Unkown:
return nil, linenum, wrapErrorLine(fmt.Errorf("Unsupported line %q\n\nPlease submit a bug report. If you can, provide this file as an attachement.\n> %s\n", line, ApplicationURL+"/issues"), linenum)
default:
return nil, linenum, wrapErrorLine(fmt.Errorf("Unsupported line %q\n\nPlease submit a bug report. If you can, provide this file as an attachement.\n> %s\n", line, ApplicationURL+"/issues"), linenum)
}
}
if err := scanner.Err(); err != nil {
return nil, linenum, err
}
return dest, linenum, nil
}
func wrapErrorLine(err error, linenum int) error {
return fmt.Errorf("line:%d %s", linenum, err.Error())
}
func parseLineType(str string) (objectfile.Type, string) {
value := ""
// comments, unlike other tokens, might not have a space after #
if str[0] == '#' {
return objectfile.Comment, strings.TrimSpace(str[1:])
}
if i := strings.Index(str, " "); i != -1 {
value = strings.TrimSpace(str[i+1:])
str = str[0:i]
}
return objectfile.TypeFromString(str), value
}