--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Services to parse .mtl material files
+module mtl
+
+import model_parser_base
+
+# Parser of `.mtl` files
+#
+# ~~~
+# var mtl_src = """
+# # Green material with low reflections
+# newmtl GreenMaterial
+# Ns 96.078431
+# Ka 0.000000 0.000000 0.000000
+# Kd 0.027186 0.180434 0.012088
+# Ks 0.500000 0.500000 0.500000
+# Ni 1.000000
+# d 1.000000
+# illum 2
+# """
+#
+# var parser = new MtlFileParser(mtl_src)
+# var name_to_mtls = parser.parse
+# assert name_to_mtls.keys.join == "GreenMaterial"
+# ~~~
+class MtlFileParser
+ super StringProcessor
+
+ # Read and parse source and return all materials organized by their names
+ fun parse: Map[String, MtlDef]
+ do
+ var mat_lib = new Map[String, MtlDef]
+ var material: nullable MtlDef = null
+
+ while not eof do
+ var token = read_token
+ if token == "newmtl" then
+ var name = read_until_eol_or_comment
+ material = new MtlDef(name)
+ mat_lib[name] = material
+ else if material != null then
+ if token == "Ka" then
+ material.ambient = read_vec3
+ else if token == "Kd" then
+ material.diffuse = read_vec3
+ else if token == "Ks" then
+ material.specular = read_vec3
+ else if token == "d" then
+ material.dissolved = read_number
+ else if token == "Tr" then
+ material.dissolved = 1.0 - read_number
+ else if token == "illum" then
+ material.illumination_model = read_number.to_i
+ else if token == "map_Kd" then
+ material.map_diffuse = read_until_eol_or_comment
+ else if token == "map_Bump" then
+ material.map_bump = read_until_eol_or_comment
+ else if token == "map_Ks" then
+ material.map_specular = read_until_eol_or_comment
+ else if token == "map_Ns" then
+ material.map_exponent = read_until_eol_or_comment
+
+ # TODO other line type headers
+ else if token == "Ns" then
+ else if token == "Ni" then
+ else if token == "sharpness" then
+ else if token == "bump" then
+ end
+ end
+ skip_eol
+ end
+
+ return mat_lib
+ end
+end
+
+# Material defined in a `.mtl` file
+class MtlDef
+
+ # Name of this material
+ var name: String
+
+ # Ambient color
+ var ambient = new Vec3 is lazy
+
+ # Diffuse color
+ var diffuse = new Vec3 is lazy
+
+ # Specular color
+ var specular = new Vec3 is lazy
+
+ # Dissolved level, where 1.0 is fully visible
+ var dissolved = 1.0
+
+ # Transparency level, where 1.0 is fully invisible
+ fun transparency: Float do return 1.0 - dissolved
+
+ # Illumination model
+ var illumination_model = 0
+
+ # Diffuse map
+ var map_diffuse: nullable String = null
+
+ # Bump map or normals texture
+ var map_bump: nullable String = null
+
+ # Specular reflectivity map
+ var map_specular: nullable String = null
+
+ # Specular exponent map
+ var map_exponent: nullable String = null
+
+ # Collect non-null maps from `map_diffuse, map_bump, map_specular, map_exponent`
+ fun maps: Array[String]
+ do
+ return [for m in [map_diffuse, map_bump, map_specular, map_exponent] do if m != null then m]
+ end
+end
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Services to parse .obj geometry files
+module obj
+
+import model_parser_base
+
+# Parser of .obj files in ASCII format
+#
+# Instantiate from a `String` and use `parse` to extract the `ObjDef`.
+#
+# ~~~
+# var obj_src = """
+# # Model of a cube
+# mtllib material_file.mtl
+# o Cube
+# v 1.000000 0.000000 0.000000
+# v 1.000000 0.000000 1.000000
+# v 0.000000 0.000000 1.000000
+# v 0.000000 0.000000 0.000000
+# v 1.000000 1.000000 0.999999
+# v 0.999999 1.000000 1.000001
+# v 0.000000 1.000000 1.000000
+# v 0.000000 1.000000 0.000000
+# usemtl GreenMaterial
+# s off
+# f 1 2 3 4
+# f 5 6 7 8
+# f 1 5 8 2
+# f 2 8 7 3
+# f 3 7 6 4
+# f 5 1 4 6
+# """
+#
+# var parser = new ObjFileParser(obj_src)
+# var parsed_obj = parser.parse
+# assert parsed_obj.is_coherent
+# ~~~
+class ObjFileParser
+ super StringProcessor
+
+ private var geometry = new ObjDef is lazy
+
+ private var current_material_lib: nullable String = null
+
+ private var current_material_name: nullable String = null
+
+ # Execute parsing of `src` to extract an `ObjDef`
+ fun parse: nullable ObjDef
+ do
+ while not eof do
+ var token = read_token
+ if token.is_empty or token == "#" then
+ # Ignore empty lines and comments
+ else if token == "v" then # Vertex points
+ var vec = read_vec4
+ geometry.vertex_points.add vec
+ else if token == "vt" then # Texture coords
+ var vec = read_vec3
+ geometry.texture_coords.add vec
+ else if token == "vn" then # Normals
+ var vec = read_vec3 # This one should not accept `w` values
+ geometry.normals.add vec
+ else if token == "vp" then # Parameter space vertices
+ var vec = read_vec3
+ geometry.params.add vec
+ else if token == "f" then # Faces
+ var face = read_face
+ geometry.faces.add face
+ else if token == "mtllib" then
+ current_material_lib = read_until_eol_or_comment
+ else if token == "usemtl" then
+ current_material_name = read_until_eol_or_comment
+
+ # TODO other line type headers
+ else if token == "s" then
+ else if token == "o" then
+ else if token == "g" then
+ end
+ skip_eol
+ end
+ return geometry
+ end
+
+ private fun read_face: ObjFace
+ do
+ var face = new ObjFace(current_material_lib, current_material_name)
+
+ loop
+ var r = read_face_index_set(face)
+ if not r then break
+ end
+
+ return face
+ end
+
+ private fun read_face_index_set(face: ObjFace): Bool
+ do
+ var token = read_token
+
+ var parts = token.split('/')
+ if parts.is_empty or parts.first.is_empty then return false
+
+ var v = new ObjVertex
+ for i in parts.length.times, part in parts do
+ part = part.trim
+
+ var n = null
+ if not part.is_empty and part.is_numeric then n = part.to_i
+
+ if i == 0 then
+ n = n or else 0 # Error if n == null
+ if n < 0 then n = geometry.vertex_points.length + n
+ v.vertex_point_index = n
+ else if i == 1 then
+ if n != null and n < 0 then n = geometry.texture_coords.length + n
+ v.texture_coord_index = n
+ else if i == 2 then
+ if n != null and n < 0 then n = geometry.normals.length + n
+ v.normal_index = n
+ else abort
+ end
+ face.vertices.add v
+
+ return true
+ end
+end
+
+# Geometry from a .obj file
+class ObjDef
+ # Vertex coordinates
+ var vertex_points = new Array[Vec4]
+
+ # Texture coordinates
+ var texture_coords = new Array[Vec3]
+
+ # Normals
+ var normals = new Array[Vec3]
+
+ # Surface parameters
+ var params = new Array[Vec3]
+
+ # Faces
+ var faces = new Array[ObjFace]
+
+ # Referenced material libraries
+ fun material_libs: Set[String] do
+ var libs = new Set[String]
+ for face in faces do
+ var lib = face.material_lib
+ if lib != null then libs.add lib
+ end
+ return libs
+ end
+
+ # Check the coherence of the model
+ #
+ # Returns `false` on error and prints details to stderr.
+ #
+ # This service can be useful for debugging, however it should not
+ # be executed at each execution of a game.
+ fun is_coherent: Bool
+ do
+ for f in faces do
+ if f.vertices.length < 3 then return error("ObjFace with less than 3 vertices")
+
+ for v in f.vertices do
+ var i = v.vertex_point_index
+ if i < 1 then return error("Vertex point index < 1")
+ if i > vertex_points.length then return error("Vertex point index > than length")
+
+ var j = v.texture_coord_index
+ if j != null then
+ if j < 1 then return error("Texture coord index < 1")
+ if j > texture_coords.length then return error("Texture coord index > than length")
+ end
+
+ j = v.normal_index
+ if j != null then
+ if j < 1 then return error("Normal index < 1")
+ if j > normals.length then return error("Normal index > than length")
+ end
+ end
+ end
+ return true
+ end
+
+ # Service to print errors for `is_coherent`
+ private fun error(msg: Text): Bool
+ do
+ print_error "ObjDef Error: {msg}"
+ return false
+ end
+end
+
+# Flat surface of an `ObjDef`
+class ObjFace
+ # Vertex composing this surface, thene should be 3 or more
+ var vertices = new Array[ObjVertex]
+
+ # Relative path to the .mtl material lib
+ var material_lib: nullable String
+
+ # Name of the material in `material_lib`
+ var material_name: nullable String
+end
+
+# Vertex composing a `ObjFace`
+class ObjVertex
+ # Vertex coordinates index in `ObjDef::vertex_points`, starting at 1
+ var vertex_point_index = 0
+
+ # Texture coordinates index in `ObjDef::texture_coords`, starting at 1
+ var texture_coord_index: nullable Int = null
+
+ # Normal index in `ObjDef::normals`, starting at 1
+ var normal_index: nullable Int = null
+end