obj_parser 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in obj_parser.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Laurent Cobos
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # ObjParser
2
+
3
+ Parse a 3D obj file to a ruby data structure.
4
+ Can compute tangent per vertex.
5
+ Can merge vertice, normals, and textures indexes into a single index for OpenGL use case.
6
+
7
+ - Support triangle primitives
8
+ - Support only one model per obj file
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ gem 'obj_parser'
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install obj_parser
23
+
24
+ ## Usage
25
+
26
+ Sample to parse an obj file, generate tangents and transform to a single indexed 3D model.
27
+
28
+ @parser = ObjLoader::ObjParser.new
29
+ obj = @parser.load(File.open("/Users/lcobos/development/ios/OpenGL-4/models/cube.obj"))
30
+ obj.compute_tangents
31
+ obj = ObjLoader::SingleIndexedObj.build_with_obj(obj)
32
+
33
+ puts obj.vertice.map(&:data).join(",")
34
+ puts obj.normals.map(&:data).join(",")
35
+ puts obj.textures.map(&:data).join(",")
36
+ puts obj.tangents.map(&:data).join(",")
37
+ puts obj.indexes.join(",")
38
+
39
+
40
+ ## Contributing
41
+
42
+ 1. Fork it ( http://github.com/<my-github-username>/obj_parser/fork )
43
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
44
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
45
+ 4. Push to the branch (`git push origin my-new-feature`)
46
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/lib/obj_parser.rb ADDED
@@ -0,0 +1,8 @@
1
+ require "obj_parser/version"
2
+ require "obj_parser/obj_parser"
3
+ require "obj_parser/single_indexed_obj"
4
+ require "obj_parser/point"
5
+
6
+ module ObjLoader
7
+ # Your code goes here...
8
+ end
@@ -0,0 +1,8 @@
1
+ module ObjLoader
2
+ class Face
3
+ attr_accessor :normals
4
+ attr_accessor :vertice
5
+ attr_accessor :textures
6
+ attr_accessor :tangents
7
+ end
8
+ end
@@ -0,0 +1,107 @@
1
+ require 'matrix'
2
+
3
+ module ObjLoader
4
+
5
+ module MathUtils
6
+
7
+ def self.tangent_for_vertices_and_texures(vertices, textures)
8
+ u1, u2, u3 = textures.map {|texture| texture[0]}
9
+ v1, v2, v3 = textures.map {|texture| texture[1]}
10
+ p1, p2, p3 = vertices.map {|vertex| normalized_vector(vertex)}
11
+ #Tangent formula: http://jerome.jouvie.free.fr/opengl-tutorials/Lesson8.php
12
+ nominator = substract_vectors( mul_vector_by_scalar(substract_vectors(p2, p1), (v3 - v1)), mul_vector_by_scalar(substract_vectors(p3, p1), (v2 - v1)))
13
+ denominator = (u2 - u1) * (v3 - v1) - (v2 - v1) * (u3 - u1)
14
+ denominator = denominator == 0 ? 1 : denominator
15
+ divide_vector_by_scalar(nominator, denominator)
16
+ end
17
+
18
+ def self.tangent2_for_vertices_and_texures(vertices, textures)
19
+ u1, u2, u3 = textures.map {|texture| texture[0]}
20
+ v1, v2, v3 = textures.map {|texture| texture[1]}
21
+ p1, p2, p3 = vertices.map {|vertex| normalized_vector(vertex)}
22
+ q1 = substract_vectors(p2, p1)
23
+ q2 = substract_vectors(p3, p2)
24
+ t1 = u2 - u1
25
+ t2 = u3 - u1
26
+ substract_vectors(mul_vector_by_scalar(q1, t2), mul_vector_by_scalar(q2, t1))
27
+ end
28
+
29
+ def self.binormal2_for_vertices_and_texures(vertices, textures)
30
+ u1, u2, u3 = textures.map {|texture| texture[0]}
31
+ v1, v2, v3 = textures.map {|texture| texture[1]}
32
+ p1, p2, p3 = vertices.map {|vertex| normalized_vector(vertex)}
33
+ q1 = substract_vectors(p2, p1)
34
+ q2 = substract_vectors(p3, p2)
35
+ s1 = v2 - v1
36
+ s2 = v3 - v1
37
+ binormal = sum_vectors(mul_vector_by_scalar(q1, -s2), mul_vector_by_scalar(q2, s1))
38
+ end
39
+
40
+ def self.cross_product(v1, v2)
41
+ [ ( (v1[1] * v2[2]) - (v1[2] * v2[1]) ),
42
+ - ( (v1[0] * v2[2]) - (v1[2] * v2[0]) ),
43
+ ( (v1[0] * v2[1]) - (v1[1] * v2[0]) ) ]
44
+ end
45
+
46
+ def self.substract_vectors a, b
47
+ [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
48
+ end
49
+
50
+ def self.sum_vectors a, b
51
+ [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
52
+ end
53
+
54
+ def self.mul_vector_by_scalar(vector, scalar)
55
+ vector.map {|component| component * scalar}
56
+ end
57
+
58
+ def self.divide_vector_by_scalar(vector, scalar)
59
+ vector.map {|component| component / scalar}
60
+ end
61
+
62
+ def self.vector_length(vector)
63
+ Math.sqrt(vector.map { |item| (item * item) }.reduce(:+))
64
+ end
65
+
66
+ def self.normalized_vector(vector)
67
+ current_vector_length = vector_length(vector)
68
+ current_vector_length = current_vector_length == 0 ? 1 : current_vector_length
69
+ vector.map do |component|
70
+ component / current_vector_length
71
+ end
72
+ end
73
+
74
+ def self.dot(vector1, vector2)
75
+ #http://en.wikipedia.org/wiki/Dot_product
76
+ vector1.each_with_index.map do |component, index|
77
+ component * vector2[index]
78
+ end.inject(&:+)
79
+ end
80
+
81
+ def self.orthogonalized_vector_with_vector(vector1, vector2)
82
+ #Gram-Schmidt orthogonalization -> http://jerome.jouvie.free.fr/opengl-tutorials/Lesson8.php
83
+ substract_vectors(vector1, mul_vector_by_scalar(vector2, dot(vector2, vector1)))
84
+ end
85
+
86
+ def self.mul_mat_per_vector(mat, vector)
87
+ m = Matrix[mat[0], mat[1], mat[2]]
88
+ v = Matrix[ [vector[0]], [vector[1]], [vector[2]]]
89
+ (m * v).to_a
90
+ end
91
+
92
+ def self.mat_inverse(mat)
93
+ m = Matrix[mat[0],mat[1], mat[2]]
94
+ puts "non regular" unless m.regular?
95
+ m.regular? ? m.inverse.to_a : m.to_a
96
+ end
97
+
98
+ def self.normal_for_face_with_vertices(face_vertices)
99
+ p1 = face_vertices[0]
100
+ p2 = face_vertices[1]
101
+ p3 = face_vertices[2]
102
+ u = substract_vectors(p2, p1)
103
+ v = substract_vectors(p3, p1)
104
+ cross_product(u, v)
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,99 @@
1
+ require_relative 'face'
2
+ require_relative 'math_utils'
3
+
4
+ module ObjLoader
5
+ class Obj
6
+ VERTEX_BY_FACE = 3
7
+ attr_accessor :normals, :normals_indexes
8
+ attr_accessor :vertice, :vertice_indexes
9
+ attr_accessor :textures, :textures_indexes
10
+ attr_accessor :tangents, :tangents_indexes
11
+ attr_accessor :faces
12
+
13
+ def initialize
14
+ self.vertice = []
15
+ self.normals = []
16
+ self.textures = []
17
+ self.tangents = []
18
+ self.vertice_indexes = []
19
+ self.normals_indexes = []
20
+ self.textures_indexes = []
21
+ self.tangents_indexes = []
22
+ end
23
+
24
+ def resolve_faces
25
+ self.faces = (self.vertice_indexes.count / VERTEX_BY_FACE).times.map { Face.new }
26
+ self.faces.each_with_index do |face, face_index|
27
+ [:vertice, :normals, :textures, :tangents].each do |element|
28
+ point_indexes = (self.send("#{element}_indexes")[face_index * VERTEX_BY_FACE..-1] || []).take(VERTEX_BY_FACE)
29
+ points = point_indexes.map do |point_index|
30
+ self.send(element)[point_index]
31
+ end
32
+ face.send("#{element}=", points)
33
+ end
34
+ end
35
+ end
36
+
37
+ def compute_tangents
38
+ self.resolve_faces
39
+ self.tangents = []
40
+ self.tangents_indexes = []
41
+ pindex = 0
42
+ self.faces.each do |face|
43
+ pindex += 1
44
+ tangent_for_face = ObjLoader::MathUtils::tangent_for_vertices_and_texures(face.vertice.map(&:data), face.textures.map(&:data))
45
+ tangent_for_face = ObjLoader::MathUtils::normalized_vector(tangent_for_face)
46
+ #set the same tangent for the 3 vertex of current face
47
+ #re-compute tangents for duplicates vertices to get tangent per face
48
+ face.vertice.each_with_index do |vertex, index|
49
+ vertex.tangent.data = ObjLoader::MathUtils::sum_vectors(vertex.tangent.data, tangent_for_face)
50
+ end
51
+ end
52
+
53
+ #orthonormalize
54
+ self.faces.each_with_index do |face,pindex|
55
+ face.vertice.each_with_index do |vertex, index|
56
+ vertex.tangent.data = ObjLoader::MathUtils::orthogonalized_vector_with_vector(vertex.tangent.data, self.normals[self.normals_indexes[pindex * 3 + index]].data)
57
+ vertex.tangent.data = ObjLoader::MathUtils::normalized_vector(vertex.tangent.data)
58
+ end
59
+ end
60
+
61
+ #binormal should be computed with per vertex tangent and summed for each vertex
62
+ self.faces.each_with_index do |face,pindex|
63
+ face.vertice.each_with_index do |vertex, index|
64
+ binormal = ObjLoader::MathUtils::cross_product(self.normals[self.normals_indexes[pindex * 3 + index]].data, vertex.tangent.data)
65
+ vertex.binormal.data = ObjLoader::MathUtils::sum_vectors(vertex.binormal.data, binormal)
66
+ end
67
+ end
68
+
69
+ self.faces.each_with_index do |face,pindex|
70
+ face.vertice.each_with_index do |vertex, index|
71
+ vertex.binormal.data = ObjLoader::MathUtils::normalized_vector(vertex.binormal.data)
72
+ if(ObjLoader::MathUtils::dot(ObjLoader::MathUtils::cross_product(self.normals[self.normals_indexes[pindex * 3 + index]].data, vertex.tangent.data), vertex.binormal.data) < 0.0)
73
+ vertex.tangent.data[3] = -1.0
74
+ else
75
+ vertex.tangent.data[3] = 1.0
76
+ end
77
+ end
78
+ end
79
+
80
+ self.faces.each_with_index do |face, index|
81
+ self.tangents += face.vertice.map(&:tangent)
82
+ point_index = index * 3
83
+ self.tangents_indexes += [point_index, point_index + 1, point_index + 2]
84
+ end
85
+ end
86
+
87
+ def tangents_self_check
88
+ self.resolve_faces
89
+ result = self.faces.each_with_index.map do |face, index|
90
+ face.vertice.map do |vertex|
91
+ ("%.2f" % ObjLoader::MathUtils::dot(vertex.tangent.data[0..2], vertex.normal.data)).to_f
92
+ end.reduce(&:+)
93
+ end.reduce(&:+)
94
+ puts "RESULT: tangents and normals are orthogonal -> [#{result == 0 ? "VALID" : "NOT VALID"}]"
95
+ result == 0
96
+ end
97
+
98
+ end
99
+ end
@@ -0,0 +1,50 @@
1
+ require_relative 'obj'
2
+ require_relative 'point'
3
+
4
+ module ObjLoader
5
+ class ObjParser
6
+
7
+ VERTEX_LINE_ID = 'v'
8
+ NORMAL_LINE_ID = 'vn'
9
+ TEXTURE_LINE_ID = 'vt'
10
+ INDEXES_ID = 'f'
11
+
12
+ attr_accessor :index_array_starting_at_index
13
+
14
+ def initialize(index_array_starting_at = 0)
15
+ self.index_array_starting_at_index = 1
16
+ end
17
+
18
+ def load(input_io)
19
+ obj = Obj.new
20
+ input_io.rewind
21
+ while(line = input_io.gets)
22
+ parser_for_line(line).call(line, obj)
23
+ end
24
+ obj
25
+ end
26
+
27
+ private
28
+
29
+ def parser_for_line(input_line)
30
+ {
31
+ NORMAL_LINE_ID => Proc.new {|line, obj| obj.normals << parse_point_on_line(line, :item_size => 3)},
32
+ TEXTURE_LINE_ID => Proc.new {|line, obj| obj.textures << parse_point_on_line(line, :item_size => 2)},
33
+ VERTEX_LINE_ID => Proc.new {|line, obj| obj.vertice << parse_point_on_line(line, :item_size => 3)},
34
+ INDEXES_ID => Proc.new { |line, obj|
35
+ v_regex = line.include?("/") ? /\s(\d+).*?\s(\d+).*?\s(\d+)/ : /\s*(\d+)\s*(\d+)\s*(\d+)/
36
+ obj.vertice_indexes += (line.scan(v_regex).last || []).map{|index| index.to_i - self.index_array_starting_at_index}
37
+ n_regex = line.include?("/") ? /\s\d*\/\d*\/(\d+)\s\d*\/\d*\/(\d+)\s\d*\/\d*\/(\d+)/ : /\s*(\d+)\s*(\d+)\s*(\d+)/
38
+ obj.normals_indexes += (line.scan(n_regex).last || []).map{|index| index.to_i - self.index_array_starting_at_index}
39
+ t_regex = line.include?("/") ? /\s\d*\/(\d+)\/\d*\s\d*\/(\d+)\/\d*\s\d*\/(\d+)\/\d*/ : /nothing/
40
+ obj.textures_indexes += (line.scan(t_regex).last || []).map{|index| index.to_i - self.index_array_starting_at_index} }
41
+ }[input_line[0..1].rstrip] || Proc.new{|line, obj| [] }
42
+ end
43
+
44
+ def parse_point_on_line(line, options = {})
45
+ item_size = {:item_size => 3}.merge(options)[:item_size]
46
+ vertice_regex = "\s(.[^\s]*)" * item_size
47
+ Point.new((line.scan(/#{vertice_regex}/).last || []))
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,37 @@
1
+ module ObjLoader
2
+ class Point
3
+ attr_accessor :data
4
+ attr_accessor :current_tangent
5
+ attr_accessor :current_binormal
6
+ attr_accessor :current_normal
7
+
8
+ attr_accessor :normals
9
+ attr_accessor :textures
10
+ attr_accessor :flag
11
+
12
+ def initialize(point_data = [0, 0, 0])
13
+ self.data = point_data.map(&:to_f)
14
+ self.textures = []
15
+ self.normals = []
16
+ end
17
+
18
+ def tangent
19
+ self.current_tangent ||= Point.new
20
+ end
21
+
22
+ def binormal
23
+ self.current_binormal ||= Point.new
24
+ end
25
+
26
+ def normal
27
+ self.current_normal ||= Point.new
28
+ end
29
+
30
+ def x; self.data[0]; end
31
+ def y; self.data[1]; end
32
+ def z; self.data[2]; end
33
+
34
+ def u; self.data[0]; end
35
+ def v; self.data[1]; end
36
+ end
37
+ end
@@ -0,0 +1,83 @@
1
+ require 'delegate.rb'
2
+
3
+ module ObjLoader
4
+ class SingleIndexedObj < SimpleDelegator
5
+
6
+ attr_accessor :detailed_vertice
7
+ attr_accessor :indexes
8
+
9
+ def target; __getobj__ ;end
10
+ def class; target.class ;end
11
+
12
+ class << self
13
+ def build_with_obj(obj)
14
+ instance = self.new(obj)
15
+ instance.setup
16
+ instance
17
+ end
18
+ end
19
+
20
+ def setup
21
+ compute_detailed_vertice
22
+ temp_vertice = []
23
+ temp_normals = []
24
+ temp_textures = []
25
+ temp_tangents = []
26
+ vec_indexes = []
27
+ self.vertice_indexes.each_with_index do |vertex_index, index|
28
+ vertex = self.detailed_vertice[vertex_index]
29
+ normal = vertex.normals.select{|n| n == self.normals[self.normals_indexes[index]] }.first
30
+ texture = vertex.textures.select{|n| n == self.textures[self.textures_indexes[index]] }.first
31
+ if(point_used_for_vertex_at_index?(normal, vertex_index) && point_used_for_vertex_at_index?(texture, vertex_index))
32
+ candids = texture.flag[vertex_index] & normal.flag[vertex_index]
33
+ vec_indexes << candids.first
34
+ else
35
+ temp_vertice << vertex
36
+ temp_tangents << vertex.tangent if vertex.tangent
37
+ vec_indexes << temp_vertice.count - 1
38
+ if(normal)
39
+ temp_normals << normal
40
+ normal.flag ||= {}
41
+ normal.flag[vertex_index] ||= []
42
+ normal.flag[vertex_index] << temp_vertice.count - 1
43
+ end
44
+ if(texture)
45
+ temp_textures << texture
46
+ texture.flag ||= {}
47
+ texture.flag[vertex_index] ||= []
48
+ texture.flag[vertex_index] << temp_vertice.count - 1
49
+ end
50
+ end
51
+ end
52
+ self.vertice = temp_vertice.map {|detailed_vertice| Point.new(detailed_vertice.data)}
53
+ self.normals = temp_normals.map {|detailed_vertice| Point.new(detailed_vertice.data)}
54
+ self.textures = temp_textures.map {|detailed_vertice| Point.new(detailed_vertice.data)}
55
+ self.tangents = temp_tangents.map {|detailed_vertice| Point.new(detailed_vertice.data)}
56
+ self.vertice_indexes = vec_indexes
57
+ self.normals_indexes = vec_indexes
58
+ self.textures_indexes = vec_indexes
59
+ self.tangents_indexes = vec_indexes
60
+ self.indexes = vec_indexes
61
+ end
62
+
63
+ private
64
+
65
+ def point_used_for_vertex_at_index?(point, vertex_index)
66
+ point && point.flag && point.flag[vertex_index]
67
+ end
68
+
69
+ def compute_detailed_vertice
70
+ self.detailed_vertice = self.vertice.map {|vertex| Point.new(vertex.data)}
71
+ self.vertice_indexes.each_with_index do |vertex_index, index|
72
+ vertex = self.detailed_vertice[vertex_index]
73
+ [:normals, :textures, :tangents].each do |element|
74
+ if(self.send(element).any? && self.send("#{element}_indexes")[index])
75
+ candid = self.send(element)[self.send("#{element}_indexes")[index]]
76
+ vertex.send(element) << candid unless vertex.send(element).include?(candid)
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,3 @@
1
+ module ObjLoader
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'obj_parser/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "obj_parser"
8
+ spec.version = ObjLoader::VERSION
9
+ spec.authors = ["Laurent Cobos"]
10
+ spec.email = ["laurent@11factory.fr"]
11
+ spec.summary = %q{3D obj file parser.}
12
+ spec.description = %q{Parse a 3D obj file to a ruby data structure. Can compute tangent per vertex. Can merge vertice, normals, and textures indexes into a single index for OpenGL use case.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.5"
22
+ spec.add_development_dependency "rake"
23
+ end
@@ -0,0 +1,60 @@
1
+ require 'test/unit'
2
+ require 'obj_parser'
3
+
4
+ class ObjLoader::MathUtilsTest < Test::Unit::TestCase
5
+
6
+ def test_tangent_computing_given_vertices_and_normal
7
+ vertices = [[0,0,0], [1,0,0], [1,1,0]]
8
+ textures = [[0,1], [1,1], [1,0]]
9
+ assert_equal [1,0,0], ObjLoader::MathUtils.tangent_for_vertices_and_texures(vertices, textures)
10
+ end
11
+
12
+ def test_tangent2_and_binormal_computing_given_vertices_and_normal
13
+ vertices = [[0,0,0], [1,0,0], [1,1,0]]
14
+ textures = [[0,1], [1,1], [1,0]]
15
+ normal = [0,0,-1]
16
+ assert_array_in_delta [1.29289321881345, -0.707106781186547, 0.0], ObjLoader::MathUtils.tangent2_for_vertices_and_texures(vertices, textures), 0.00001
17
+ end
18
+
19
+ def test_vector_length
20
+ assert_equal 0, ObjLoader::MathUtils::vector_length([0,0,0])
21
+ assert_equal 1, ObjLoader::MathUtils::vector_length([1,0,0])
22
+ assert_equal Math.sqrt(29), ObjLoader::MathUtils.vector_length([2,3,4])
23
+ end
24
+
25
+ def test_vector_normalization
26
+ assert_equal [0,0,0], ObjLoader::MathUtils.normalized_vector([0,0,0])
27
+ assert_equal [1,0,0], ObjLoader::MathUtils.normalized_vector([1,0,0])
28
+ assert_array_in_delta [0.371390676354104, 0.557086014531156, 0.742781352708207], ObjLoader::MathUtils.normalized_vector([2,3,4]), 0.00001
29
+ end
30
+
31
+ def test_dot_product
32
+ v1 = [1,0,0]
33
+ v2 = [0,1,0]
34
+ assert_equal 0, ObjLoader::MathUtils.dot(v1, v2)
35
+ v2 = [1,1,0]
36
+ assert_equal 1, ObjLoader::MathUtils.dot(v1, v2)
37
+ end
38
+
39
+ def test_orthogonalize
40
+ v1 = [0.2,1,0]
41
+ v2 = [1,0,0]
42
+ assert_equal [0,1,0], ObjLoader::MathUtils.orthogonalized_vector_with_vector(v1, v2)
43
+ end
44
+
45
+ def test_normal_computing_given_face_vertices
46
+ vertices = [[0,0,0], [1,0,0], [1,1,0]]
47
+ assert_equal [0,0,1].map(&:to_s), ObjLoader::MathUtils.normal_for_face_with_vertices(vertices).map(&:to_s)
48
+ vertices = [[0,0,0], [0,0,1], [0,1,0]]
49
+ assert_equal [-1,0,0].map(&:to_s), ObjLoader::MathUtils.normal_for_face_with_vertices(vertices).map(&:to_s)
50
+ end
51
+
52
+ private
53
+
54
+ def assert_array_in_delta(expected_array, actual_array, delta)
55
+ assert_equal(expected_array.count, actual_array.count)
56
+ expected_array.each_with_index do |expected_element, index|
57
+ assert_in_delta(expected_element, actual_array[index], delta)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,58 @@
1
+ require 'test_helper'
2
+
3
+ describe ObjLoader::ObjParser do
4
+
5
+ it "should get obj vertice, normals, textures and indexes" do
6
+ @parser = ObjLoader::ObjParser.new
7
+ obj = @parser.load(StringIO.new(sample_cube_obj))
8
+ obj.vertice.take(3).map(&:data).must_equal([p([0.0,0.0,0.0]), p([0.0,0.0,1.0]), p([0.0,1.0,0.0])].map(&:data))
9
+ obj.normals.count.must_equal(6)
10
+ obj.textures.count.must_equal(4)
11
+ obj.vertice_indexes.count.must_equal(36)
12
+ obj.normals_indexes.count.must_equal(36)
13
+ obj.textures_indexes.count.must_equal(36)
14
+ end
15
+
16
+ private
17
+
18
+ def sample_cube_obj
19
+ <<CUBE_OBJ
20
+ # Blender v2.65 (sub 0) OBJ File: ''
21
+ # www.blender.org
22
+ o Cube
23
+ v 0.0 0.0 0.0
24
+ v 0.0 0.0 1.0
25
+ v 0.0 1.0 0.0
26
+ v 0.0 1.0 1.0
27
+ v 1.0 0.0 0.0
28
+ v 1.0 0.0 1.0
29
+ v 1.0 1.0 0.0
30
+ v 1.0 1.0 1.0
31
+
32
+ vn 0.0 0.0 1.0
33
+ vn 0.0 0.0 -1.0
34
+ vn 0.0 1.0 0.0
35
+ vn 0.0 -1.0 0.0
36
+ vn 1.0 0.0 0.0
37
+ vn -1.0 0.0 0.0
38
+
39
+ vt 0.0 0.0
40
+ vt 1.0 0.0
41
+ vt 1.0 1.0
42
+ vt 0.0 1.0
43
+
44
+ f 1/1/2 7/3/2 5/2/2
45
+ f 1/1/2 3/4/2 7/3/2
46
+ f 1/1/6 4/3/6 3/4/6
47
+ f 1/1/6 2/2/6 4/3/6
48
+ f 3/1/3 8/3/3 7/4/3
49
+ f 3/1/3 4/2/3 8/3/3
50
+ f 5/2/5 7/3/5 8/4/5
51
+ f 5/2/5 8/4/5 6/1/5
52
+ f 1/1/4 5/2/4 6/3/4
53
+ f 1/1/4 6/3/4 2/4/4
54
+ f 2/1/1 6/2/1 8/3/1
55
+ f 2/1/1 8/3/1 4/4/1
56
+ CUBE_OBJ
57
+ end
58
+ end
data/test/obj_spec.rb ADDED
@@ -0,0 +1,33 @@
1
+ require 'test_helper'
2
+
3
+ describe ObjLoader::Obj do
4
+
5
+ before do
6
+ @obj = ObjLoader::Obj.new
7
+ @obj.vertice = [p([0.0,0.0,0.0]), p([0.0,0.0,1.0]), p([0.0,1.0,0.0]), p([0.0,1.0,1.0])]
8
+ @obj.vertice_indexes = [0, 1, 2, 1, 2, 3]
9
+ @obj.normals = [p([1.0,0.0,0.0]), p([0.0,1.0,0.0])]
10
+ @obj.normals_indexes = [0, 0, 0, 1, 1, 1]
11
+ @obj.textures = [p([1.0,0.0]), p([0.0,1.0])]
12
+ @obj.textures_indexes = [1, 1, 0, 0, 1, 1]
13
+ end
14
+
15
+ it 'resolve faces based on indexes' do
16
+ @obj.resolve_faces
17
+ @obj.faces.count.must_equal(2)
18
+ @obj.faces.first.vertice.map(&:data).must_equal([p([0.0,0.0,0.0]), p([0.0,0.0,1.0]), p([0.0,1.0,0.0])].map(&:data))
19
+ @obj.faces.first.normals.map(&:data).must_equal([p([1.0,0.0,0.0]), p([1.0,0.0,0.0]), p([1.0,0.0,0.0])].map(&:data))
20
+ @obj.faces.first.textures.map(&:data).must_equal([p([0.0,1.0]), p([0.0,1.0]), p([1.0,0.0])].map(&:data))
21
+ end
22
+
23
+ it 'compute tangents' do
24
+ @obj.compute_tangents
25
+ result = @obj.faces.each_with_index.map do |face, index|
26
+ face.vertice.map do |vertex|
27
+ ("%.2f" % ObjLoader::MathUtils::dot(vertex.tangent.data[0..2], vertex.normal.data)).to_f
28
+ end.reduce(&:+)
29
+ end.reduce(&:+)
30
+ result.must_equal(0)
31
+ end
32
+
33
+ end
@@ -0,0 +1,41 @@
1
+ require 'test_helper'
2
+
3
+ describe ObjLoader::SingleIndexedObj do
4
+
5
+ before do
6
+ @obj = ObjLoader::Obj.new
7
+ @obj.vertice = [p([0.0,0.0,0.0]), p([0.0,0.0,1.0]), p([0.0,1.0,0.0]), p([0.0,1.0,1.0])]
8
+ @obj.vertice_indexes = [0, 1, 2, 0, 2, 3]
9
+ @obj.normals = [p([1.0,0.0,0.0]), p([0.0,1.0,0.0])]
10
+ @obj.normals_indexes = [1, 0, 1, 1, 0, 0]
11
+ @single_indexed_obj = ObjLoader::SingleIndexedObj.build_with_obj(@obj)
12
+ end
13
+
14
+ describe 'compute detailed vertice' do
15
+
16
+ it 'can build detailed vertice with associated uniq normals, textures,..' do
17
+ @single_indexed_obj.detailed_vertice[2].normals.map(&:data).must_equal([p([0.0,1.0,0.0]), p([1.0,0.0,0.0])].map(&:data))
18
+ end
19
+
20
+ it 'should skip duplicate normals, textures by vertex' do
21
+ @single_indexed_obj.detailed_vertice[0].normals.count.must_equal(1)
22
+ end
23
+
24
+ it 'should not modify original vertice' do
25
+ @obj.vertice.first.normals.must_be_empty
26
+ @single_indexed_obj.vertice.first.normals.must_be_empty
27
+ end
28
+
29
+ end
30
+
31
+ describe 'use a single index' do
32
+
33
+ it 'reorganize normals, textures, ... based on vertices indexes' do
34
+ @single_indexed_obj.normals.count.must_equal(@obj.vertice_indexes.uniq.count)
35
+ @single_indexed_obj.normals.map(&:data).must_equal(
36
+ [p([0.0,1.0,0.0]), p([1.0,0.0,0.0]), p([0.0,1.0,0.0]), p([0.0,1.0,0.0]), p([1.0,0.0,0.0]), p([1.0,0.0,0.0])].map(&:data))
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,7 @@
1
+ require 'minitest'
2
+ require 'minitest/autorun'
3
+ require 'obj_parser'
4
+
5
+ def p(data)
6
+ ObjLoader::Point.new(data)
7
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: obj_parser
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Laurent Cobos
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-01-24 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.5'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.5'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: Parse a 3D obj file to a ruby data structure. Can compute tangent per
47
+ vertex. Can merge vertice, normals, and textures indexes into a single index for
48
+ OpenGL use case.
49
+ email:
50
+ - laurent@11factory.fr
51
+ executables: []
52
+ extensions: []
53
+ extra_rdoc_files: []
54
+ files:
55
+ - .gitignore
56
+ - Gemfile
57
+ - LICENSE.txt
58
+ - README.md
59
+ - Rakefile
60
+ - lib/obj_parser.rb
61
+ - lib/obj_parser/face.rb
62
+ - lib/obj_parser/math_utils.rb
63
+ - lib/obj_parser/obj.rb
64
+ - lib/obj_parser/obj_parser.rb
65
+ - lib/obj_parser/point.rb
66
+ - lib/obj_parser/single_indexed_obj.rb
67
+ - lib/obj_parser/version.rb
68
+ - obj_parser.gemspec
69
+ - test/math_utils_test.rb
70
+ - test/obj_parser_spec.rb
71
+ - test/obj_spec.rb
72
+ - test/single_indexed_obj_spec.rb
73
+ - test/test_helper.rb
74
+ homepage: ''
75
+ licenses:
76
+ - MIT
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ! '>='
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubyforge_project:
95
+ rubygems_version: 1.8.23
96
+ signing_key:
97
+ specification_version: 3
98
+ summary: 3D obj file parser.
99
+ test_files:
100
+ - test/math_utils_test.rb
101
+ - test/obj_parser_spec.rb
102
+ - test/obj_spec.rb
103
+ - test/single_indexed_obj_spec.rb
104
+ - test/test_helper.rb