Merge: doc: fixed some typos and other misc. corrections
[nit.git] / lib / gamnit / model_parsers / obj.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Services to parse .obj geometry files
16 module obj
17
18 import model_parser_base
19
20 # Parser of .obj files in ASCII format
21 #
22 # Instantiate from a `String` and use `parse` to extract the `ObjDef`.
23 #
24 # ~~~
25 # var obj_src = """
26 # # Model of a cube
27 # mtllib material_file.mtl
28 # o Cube
29 # v 1.000000 0.000000 0.000000
30 # v 1.000000 0.000000 1.000000
31 # v 0.000000 0.000000 1.000000
32 # v 0.000000 0.000000 0.000000
33 # v 1.000000 1.000000 0.999999
34 # v 0.999999 1.000000 1.000001
35 # v 0.000000 1.000000 1.000000
36 # v 0.000000 1.000000 0.000000
37 # usemtl GreenMaterial
38 # s off
39 # f 1 2 3 4
40 # f 5 6 7 8
41 # f 1 5 8 2
42 # f 2 8 7 3
43 # f 3 7 6 4
44 # f 5 1 4 6
45 # """
46 #
47 # var parser = new ObjFileParser(obj_src)
48 # var parsed_obj = parser.parse
49 # assert parsed_obj.is_coherent
50 # assert parsed_obj.objects.first.name == "Cube"
51 # ~~~
52 class ObjFileParser
53 super StringProcessor
54
55 private var geometry = new ObjDef is lazy
56
57 private var current_material_lib: nullable String = null
58
59 private var current_material_name: nullable String = null
60
61 # Execute parsing of `src` to extract an `ObjDef`
62 fun parse: nullable ObjDef
63 do
64 var obj_obj = null
65 while not eof do
66 var token = read_token
67 if token.is_empty or token == "#" then
68 # Ignore empty lines and comments
69 else if token == "v" then # Vertex points
70 var vec = read_vec4
71 geometry.vertex_points.add vec
72 else if token == "vt" then # Texture coords
73 var vec = read_vec3
74 geometry.texture_coords.add vec
75 else if token == "vn" then # Normals
76 var vec = read_vec3 # This one should not accept `w` values
77 geometry.normals.add vec
78 else if token == "vp" then # Parameter space vertices
79 var vec = read_vec3
80 geometry.params.add vec
81 else if token == "f" then # Faces
82 var face = read_face
83 if obj_obj == null then
84 obj_obj = new ObjObj("")
85 geometry.objects.add obj_obj
86 end
87 obj_obj.faces.add face
88 else if token == "mtllib" then
89 current_material_lib = read_until_eol_or_comment
90 else if token == "usemtl" then
91 current_material_name = read_until_eol_or_comment
92
93 # TODO other line type headers
94 else if token == "s" then
95 else if token == "o" then
96 obj_obj = new ObjObj(read_until_eol_or_comment)
97 geometry.objects.add obj_obj
98 else if token == "g" then
99 end
100 skip_eol
101 end
102 return geometry
103 end
104
105 private fun read_face: ObjFace
106 do
107 var face = new ObjFace(current_material_lib, current_material_name)
108
109 loop
110 var r = read_face_index_set(face)
111 if not r then break
112 end
113
114 return face
115 end
116
117 private fun read_face_index_set(face: ObjFace): Bool
118 do
119 var token = read_token
120
121 var parts = token.split('/')
122 if parts.is_empty or parts.first.is_empty then return false
123
124 var v = new ObjVertex
125 for i in parts.length.times, part in parts do
126 part = part.trim
127
128 var n = null
129 if not part.is_empty and part.is_numeric then n = part.to_i
130
131 if i == 0 then
132 n = n or else 0 # Error if n == null
133 if n < 0 then n = geometry.vertex_points.length + n
134 v.vertex_point_index = n
135 else if i == 1 then
136 if n != null and n < 0 then n = geometry.texture_coords.length + n
137 v.texture_coord_index = n
138 else if i == 2 then
139 if n != null and n < 0 then n = geometry.normals.length + n
140 v.normal_index = n
141 else abort
142 end
143 face.vertices.add v
144
145 return true
146 end
147 end
148
149 # Geometry from a .obj file
150 class ObjDef
151 # Vertex coordinates
152 var vertex_points = new Array[Vec4]
153
154 # Texture coordinates
155 var texture_coords = new Array[Vec3]
156
157 # Normals
158 var normals = new Array[Vec3]
159
160 # Surface parameters
161 var params = new Array[Vec3]
162
163 # Sub-objects
164 var objects = new Array[ObjObj]
165
166 # Relative paths to referenced material libraries
167 fun material_libs: Set[String] do
168 var libs = new Set[String]
169 for obj in objects do
170 for face in obj.faces do
171 var lib = face.material_lib
172 if lib != null then libs.add lib
173 end
174 end
175 return libs
176 end
177
178 # Check the coherence of the model
179 #
180 # Returns `false` on error and prints details to stderr.
181 #
182 # This service can be useful for debugging, however it should not
183 # be executed at each execution of a game.
184 fun is_coherent: Bool
185 do
186 for obj in objects do
187 for f in obj.faces do
188 if f.vertices.length < 3 then return error("Face with less than 3 vertices")
189 end
190
191 for f in obj.faces do for v in f.vertices do
192 var i = v.vertex_point_index
193 if i < 1 then return error("Vertex point index < 1")
194 if i > vertex_points.length then return error("Vertex point index > than length")
195
196 var j = v.texture_coord_index
197 if j != null then
198 if j < 1 then return error("Texture coord index < 1")
199 if j > texture_coords.length then return error("Texture coord index > than length")
200 end
201
202 j = v.normal_index
203 if j != null then
204 if j < 1 then return error("Normal index < 1")
205 if j > normals.length then return error("Normal index > than length")
206 end
207 end
208 end
209 return true
210 end
211
212 # Service to print errors for `is_coherent`
213 private fun error(msg: Text): Bool
214 do
215 print_error "ObjDef Error: {msg}"
216 return false
217 end
218 end
219
220 # Sub-object within an `ObjDef`
221 class ObjObj
222
223 # Sub-object name as declared in the source file
224 var name: String
225
226 # Sub-object faces
227 var faces = new Array[ObjFace]
228 end
229
230 # Flat surface of an `ObjDef`
231 class ObjFace
232 # Vertex composing this surface, there should be 3 or more
233 var vertices = new Array[ObjVertex]
234
235 # Relative path to the .mtl material lib
236 var material_lib: nullable String
237
238 # Name of the material in `material_lib`
239 var material_name: nullable String
240 end
241
242 # Vertex composing a `ObjFace`
243 class ObjVertex
244 # Vertex coordinates index in `ObjDef::vertex_points`, starting at 1
245 var vertex_point_index = 0
246
247 # Texture coordinates index in `ObjDef::texture_coords`, starting at 1
248 var texture_coord_index: nullable Int = null
249
250 # Normal index in `ObjDef::normals`, starting at 1
251 var normal_index: nullable Int = null
252 end