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