gamnit: collect errors when loading models and its textures
[nit.git] / lib / gamnit / depth / more_models.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 load models from the assets folder
16 module more_models
17
18 intrude import depth_core
19
20 import gamnit::obj
21 import gamnit::mtl
22
23 import more_materials
24 import more_meshes
25
26 redef class Model
27 # Prepare to load a model from the assets folder
28 new(path: Text) do return new ModelAsset(path.to_s)
29 end
30
31 # Model loaded from a file in the asset folder
32 #
33 # In case of error, `error` is set accordingly.
34 # If the error is on the mesh, `mesh` is set to a default `new Mesh.cube`.
35 # If the material is missing or it failed to load, `material` is set to a `new SimpleMaterial.default`.
36 class ModelAsset
37 super Model
38 super Asset
39
40 init do models.add self
41
42 private var loaded = false
43
44 redef fun load
45 do
46 var ext = path.file_extension
47 if ext == "obj" then
48 load_obj_file
49 else
50 errors.add new Error("Model at '{path}' failed to load: Extension '{ext or else "null"}' unrecognized")
51 end
52
53 if leaves_cache.is_empty then
54 # Nothing was loaded, use a cube with the default material
55 var leaf = placeholder_model
56 leaves_cache.add leaf
57 end
58
59 loaded = true
60 end
61
62 private fun load_obj_file
63 do
64 # Read .obj description from assets
65 var text_asset = new TextAsset(path)
66 var content = text_asset.to_s
67 if content.is_empty then
68 errors.add new Error("Model failed to load: Asset empty at '{self.path}'")
69 leaves_cache.add new LeafModel(new Cube, new Material)
70 return
71 end
72
73 # Parse .obj description
74 var parser = new ObjFileParser(content)
75 var obj_def = parser.parse
76 if obj_def == null then
77 errors.add new Error("Model failed to load: .obj format error on '{self.path}'")
78 leaves_cache.add new LeafModel(new Cube, new Material)
79 return
80 end
81
82 # Check for errors
83 if debug_gamnit then assert obj_def.is_coherent
84
85 # Build models
86 var converter = new ModelFromObj(path, obj_def)
87 converter.models leaves_cache
88 errors.add_all converter.errors
89 end
90
91 redef fun leaves
92 do
93 if not loaded then
94 # Lazy load
95 load
96
97 # Print errors when lazy loading only
98 if errors.length == 1 then
99 print_error errors.first
100 else if errors.length > 1 then
101 print_error "Loading model at '{path}' raised {errors.length} errors:\n* "
102 print_error errors.join("\n* ")
103 end
104 end
105
106 return leaves_cache
107 end
108
109 private var leaves_cache = new Array[LeafModel]
110 end
111
112 # Short-lived service to convert an `ObjDef` to `models`
113 #
114 # Limitations: This service only support faces with 3 or 4 vertices.
115 # Faces with more vertices should be triangulated by the modeling tool.
116 private class ModelFromObj
117
118 # Path to the .obj file in the assets folder, used to find .mtl files
119 var path: String
120
121 # Parsed .obj definition
122 var obj_def: ObjDef
123
124 # Errors raised by calls to `models`
125 var errors = new Array[Error]
126
127 # Fill `leaves` with models described in `obj_def`
128 fun models(leaves: Array[LeafModel])
129 do
130 # Sort faces by material
131 var mtl_to_faces = new MultiHashMap[String, ObjFace]
132 for face in obj_def.faces do
133 var mtl_lib_name = face.material_lib
134 var mtl_name = face.material_name
135
136 var full_name = ""
137 if mtl_lib_name != null and mtl_name != null then full_name = mtl_lib_name / mtl_name
138
139 mtl_to_faces[full_name].add face
140 end
141
142 # Load material libs
143 var mtl_libs = sys.mtl_libs
144 var lib_names = obj_def.material_libs
145 for name in lib_names do
146 var asset_path = self.path.dirname / name
147 var lib_asset = new TextAsset(asset_path)
148 lib_asset.load
149
150 var error = lib_asset.error
151 if error != null then
152 errors.add error
153 continue
154 end
155
156 var mtl_parser = new MtlFileParser(lib_asset.to_s)
157 var mtl_lib = mtl_parser.parse
158 mtl_libs[asset_path] = mtl_lib
159 end
160
161 # Create 1 mesh per material, and prepare materials
162 var mesh_to_mtl = new Map[Mesh, nullable MtlDef]
163 var texture_names = new Set[String]
164 for full_name, faces in mtl_to_faces do
165
166 # Create mesh
167 var mesh = new Mesh
168 mesh.vertices = vertices(faces)
169 mesh.normals = normals(faces)
170 mesh.texture_coords = texture_coords(faces)
171
172 # Material
173 var mtl_def = null
174
175 var mtl_lib_name = faces.first.material_lib
176 var mtl_name = faces.first.material_name
177 if mtl_lib_name != null and mtl_name != null then
178 var asset_path = self.path.dirname / mtl_lib_name
179 var mtl_lib = mtl_libs[asset_path]
180 var mtl = mtl_lib.get_or_null(mtl_name)
181 if mtl != null then
182 mtl_def = mtl
183
184 for e in mtl.maps do
185 texture_names.add self.path.dirname / e
186 end
187 else
188 errors.add new Error("Error loading model at '{path}': mtl '{mtl_name}' not found in '{asset_path}'")
189 end
190 end
191
192 mesh_to_mtl[mesh] = mtl_def
193 end
194
195 # Load textures need for these materials
196 for name in texture_names do
197 if not asset_textures_by_name.keys.has(name) then
198 var tex = new TextureAsset(name)
199 asset_textures_by_name[name] = tex
200
201 tex.load
202 var error = tex.error
203 if error != null then errors.add error
204 end
205 end
206
207 # Create final `Materials` from defs and textures
208 var materials = new Map[MtlDef, Material]
209 for mtl in mesh_to_mtl.values do
210 if mtl == null then continue
211
212 var ambient = mtl.ambient.to_a
213 ambient.add 1.0
214
215 var diffuse = mtl.diffuse.to_a
216 diffuse.add 1.0
217
218 var specular = mtl.specular.to_a
219 specular.add 1.0
220
221 var material = new TexturedMaterial(ambient, diffuse, specular)
222 materials[mtl] = material
223
224 var tex_name = mtl.map_ambient
225 if tex_name != null then
226 tex_name = self.path.dirname / tex_name
227 material.ambient_texture = asset_textures_by_name[tex_name]
228 end
229
230 tex_name = mtl.map_diffuse
231 if tex_name != null then
232 tex_name = self.path.dirname / tex_name
233 material.diffuse_texture = asset_textures_by_name[tex_name]
234 end
235
236 tex_name = mtl.map_specular
237 if tex_name != null then
238 tex_name = self.path.dirname / tex_name
239 material.specular_texture = asset_textures_by_name[tex_name]
240 end
241 end
242
243 # Create models and store them
244 for mesh, mtl_def in mesh_to_mtl do
245 var material = materials.get_or_null(mtl_def)
246 if material == null then material = new Material
247
248 var model = new LeafModel(mesh, material)
249 leaves.add model
250 end
251 end
252
253 # Compute the vertices coordinates of `faces` in a flat `Array[Float]`
254 fun vertices(faces: Array[ObjFace]): Array[Float] do
255 var obj_def = obj_def
256
257 var vertices = new Array[Float]
258 for face in faces do
259
260 # 1st triangle
261 var count = 0
262 for e in face.vertices do
263 var i = e.vertex_point_index - 1
264 var v = obj_def.vertex_points[i]
265
266 vertices.add v.x
267 vertices.add v.y
268 vertices.add v.z
269
270 if count == 2 then break
271 count += 1
272 end
273
274 # If square, 2nd triangle
275 #
276 # This may not support all vertices ordering.
277 if face.vertices.length > 3 then
278 for e in [face.vertices[0], face.vertices[2], face.vertices[3]] do
279 var i = e.vertex_point_index - 1
280 var v = obj_def.vertex_points[i]
281
282 vertices.add v.x
283 vertices.add v.y
284 vertices.add v.z
285 end
286 end
287
288 # TODO use polygon triangulation to support larger polygons
289 end
290 return vertices
291 end
292
293 # Compute the normals of `faces` in a flat `Array[Float]`
294 fun normals(faces: Array[ObjFace]): Array[Float] do
295 var obj_def = obj_def
296
297 var normals = new Array[Float]
298 for face in faces do
299 # 1st triangle
300 var count = 0
301 for e in face.vertices do
302 var i = e.normal_index
303 if i == null then
304 compute_and_append_normal(normals, face)
305 else
306 var v = obj_def.normals[i-1]
307 normals.add v.x
308 normals.add v.y
309 normals.add v.z
310 end
311
312 if count == 2 then break
313 count += 1
314 end
315
316 # If square, 2nd triangle
317 #
318 # This may not support all vertices ordering.
319 if face.vertices.length > 3 then
320 for e in [face.vertices[0], face.vertices[2], face.vertices[3]] do
321 var i = e.normal_index
322 if i == null then
323 compute_and_append_normal(normals, face)
324 else
325 var v = obj_def.normals[i-1]
326 normals.add v.x
327 normals.add v.y
328 normals.add v.z
329 end
330 end
331 end
332 end
333 return normals
334 end
335
336 # Compute the normal of `face` and append it as 3 floats to `seq`
337 #
338 # Resulting normals are not normalized.
339 fun compute_and_append_normal(seq: Sequence[Float], face: ObjFace)
340 do
341 var i1 = face.vertices[0].vertex_point_index
342 var i2 = face.vertices[1].vertex_point_index
343 var i3 = face.vertices[2].vertex_point_index
344
345 var v1 = obj_def.vertex_points[i1-1]
346 var v2 = obj_def.vertex_points[i2-1]
347 var v3 = obj_def.vertex_points[i3-1]
348
349 var vx = v2.x - v1.x
350 var vy = v2.y - v1.y
351 var vz = v2.z - v1.z
352 var wx = v3.x - v1.x
353 var wy = v3.y - v1.y
354 var wz = v3.z - v1.z
355
356 var nx = (vy*wz) - (vz*wy)
357 var ny = (vz*wx) - (vx*wz)
358 var nz = (vx*wy) - (vy*wx)
359
360 # Append to `seq`
361 seq.add nx
362 seq.add ny
363 seq.add nz
364 end
365
366 # Compute the texture coordinates of `faces` in a flat `Array[Float]`
367 fun texture_coords(faces: Array[ObjFace]): Array[Float] do
368 var obj_def = obj_def
369
370 var coords = new Array[Float]
371 for face in faces do
372
373 # 1st triangle
374 var count = 0
375 for e in face.vertices do
376 var i = e.texture_coord_index
377 if i == null then
378 coords.add 0.0
379 coords.add 0.0
380 else
381 var tc = obj_def.texture_coords[i-1]
382 coords.add tc.u
383 coords.add tc.v
384 end
385
386 if count == 2 then break
387 count += 1
388 end
389
390 # If square, 2nd triangle
391 #
392 # This may not support all vertices ordering.
393 if face.vertices.length > 3 then
394 for e in [face.vertices[0], face.vertices[2], face.vertices[3]] do
395 var i = e.texture_coord_index
396 if i == null then
397 coords.add 0.0
398 coords.add 0.0
399 else
400 var tc = obj_def.texture_coords[i-1]
401 coords.add tc.u
402 coords.add tc.v
403 end
404 end
405 end
406 end
407 return coords
408 end
409 end
410
411 redef class Sys
412 # Textures loaded from .mtl files for models
413 var asset_textures_by_name = new Map[String, TextureAsset]
414
415 # Loaded .mtl material definitions, sorted by path in assets and material name
416 private var mtl_libs = new Map[String, Map[String, MtlDef]]
417
418 # All instantiated asset models
419 var models = new Set[ModelAsset]
420
421 # Blue cube of 1 unit on each side, acting as placeholder for models failing to load
422 #
423 # This model can be freely used by any `Actor` as placeholder or for debugging.
424 var placeholder_model = new LeafModel(new Cube, new Material) is lazy
425 end