gamnit: don't load the same .mat material more than once
[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 # ~~~
51 class ObjFileParser
52 super StringProcessor
53
54 private var geometry = new ObjDef is lazy
55
56 private var current_material_lib: nullable String = null
57
58 private var current_material_name: nullable String = null
59
60 # Execute parsing of `src` to extract an `ObjDef`
61 fun parse: nullable ObjDef
62 do
63 while not eof do
64 var token = read_token
65 if token.is_empty or token == "#" then
66 # Ignore empty lines and comments
67 else if token == "v" then # Vertex points
68 var vec = read_vec4
69 geometry.vertex_points.add vec
70 else if token == "vt" then # Texture coords
71 var vec = read_vec3
72 geometry.texture_coords.add vec
73 else if token == "vn" then # Normals
74 var vec = read_vec3 # This one should not accept `w` values
75 geometry.normals.add vec
76 else if token == "vp" then # Parameter space vertices
77 var vec = read_vec3
78 geometry.params.add vec
79 else if token == "f" then # Faces
80 var face = read_face
81 geometry.faces.add face
82 else if token == "mtllib" then
83 current_material_lib = read_until_eol_or_comment
84 else if token == "usemtl" then
85 current_material_name = read_until_eol_or_comment
86
87 # TODO other line type headers
88 else if token == "s" then
89 else if token == "o" then
90 else if token == "g" then
91 end
92 skip_eol
93 end
94 return geometry
95 end
96
97 private fun read_face: ObjFace
98 do
99 var face = new ObjFace(current_material_lib, current_material_name)
100
101 loop
102 var r = read_face_index_set(face)
103 if not r then break
104 end
105
106 return face
107 end
108
109 private fun read_face_index_set(face: ObjFace): Bool
110 do
111 var token = read_token
112
113 var parts = token.split('/')
114 if parts.is_empty or parts.first.is_empty then return false
115
116 var v = new ObjVertex
117 for i in parts.length.times, part in parts do
118 part = part.trim
119
120 var n = null
121 if not part.is_empty and part.is_numeric then n = part.to_i
122
123 if i == 0 then
124 n = n or else 0 # Error if n == null
125 if n < 0 then n = geometry.vertex_points.length + n
126 v.vertex_point_index = n
127 else if i == 1 then
128 if n != null and n < 0 then n = geometry.texture_coords.length + n
129 v.texture_coord_index = n
130 else if i == 2 then
131 if n != null and n < 0 then n = geometry.normals.length + n
132 v.normal_index = n
133 else abort
134 end
135 face.vertices.add v
136
137 return true
138 end
139 end
140
141 # Geometry from a .obj file
142 class ObjDef
143 # Vertex coordinates
144 var vertex_points = new Array[Vec4]
145
146 # Texture coordinates
147 var texture_coords = new Array[Vec3]
148
149 # Normals
150 var normals = new Array[Vec3]
151
152 # Surface parameters
153 var params = new Array[Vec3]
154
155 # Faces
156 var faces = new Array[ObjFace]
157
158 # Relative paths to referenced material libraries
159 fun material_libs: Set[String] do
160 var libs = new Set[String]
161 for face in faces do
162 var lib = face.material_lib
163 if lib != null then libs.add lib
164 end
165 return libs
166 end
167
168 # Check the coherence of the model
169 #
170 # Returns `false` on error and prints details to stderr.
171 #
172 # This service can be useful for debugging, however it should not
173 # be executed at each execution of a game.
174 fun is_coherent: Bool
175 do
176 for f in faces do
177 if f.vertices.length < 3 then return error("ObjFace with less than 3 vertices")
178
179 for v in f.vertices do
180 var i = v.vertex_point_index
181 if i < 1 then return error("Vertex point index < 1")
182 if i > vertex_points.length then return error("Vertex point index > than length")
183
184 var j = v.texture_coord_index
185 if j != null then
186 if j < 1 then return error("Texture coord index < 1")
187 if j > texture_coords.length then return error("Texture coord index > than length")
188 end
189
190 j = v.normal_index
191 if j != null then
192 if j < 1 then return error("Normal index < 1")
193 if j > normals.length then return error("Normal index > than length")
194 end
195 end
196 end
197 return true
198 end
199
200 # Service to print errors for `is_coherent`
201 private fun error(msg: Text): Bool
202 do
203 print_error "ObjDef Error: {msg}"
204 return false
205 end
206 end
207
208 # Flat surface of an `ObjDef`
209 class ObjFace
210 # Vertex composing this surface, thene should be 3 or more
211 var vertices = new Array[ObjVertex]
212
213 # Relative path to the .mtl material lib
214 var material_lib: nullable String
215
216 # Name of the material in `material_lib`
217 var material_name: nullable String
218 end
219
220 # Vertex composing a `ObjFace`
221 class ObjVertex
222 # Vertex coordinates index in `ObjDef::vertex_points`, starting at 1
223 var vertex_point_index = 0
224
225 # Texture coordinates index in `ObjDef::texture_coords`, starting at 1
226 var texture_coord_index: nullable Int = null
227
228 # Normal index in `ObjDef::normals`, starting at 1
229 var normal_index: nullable Int = null
230 end