obj_parser 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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