lib/gamnit: intro parsers for .obj and .mtl files
authorAlexis Laferrière <alexis.laf@xymus.net>
Tue, 12 Jan 2016 03:52:31 +0000 (22:52 -0500)
committerAlexis Laferrière <alexis.laf@xymus.net>
Thu, 14 Jan 2016 15:57:11 +0000 (10:57 -0500)
Signed-off-by: Alexis Laferrière <alexis.laf@xymus.net>

lib/gamnit/model_parsers/mtl.nit [new file with mode: 0644]
lib/gamnit/model_parsers/obj.nit [new file with mode: 0644]

diff --git a/lib/gamnit/model_parsers/mtl.nit b/lib/gamnit/model_parsers/mtl.nit
new file mode 100644 (file)
index 0000000..b962fe1
--- /dev/null
@@ -0,0 +1,131 @@
+# 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
diff --git a/lib/gamnit/model_parsers/obj.nit b/lib/gamnit/model_parsers/obj.nit
new file mode 100644 (file)
index 0000000..abd573d
--- /dev/null
@@ -0,0 +1,230 @@
+# 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