obj_parser 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +46 -0
- data/Rakefile +1 -0
- data/lib/obj_parser.rb +8 -0
- data/lib/obj_parser/face.rb +8 -0
- data/lib/obj_parser/math_utils.rb +107 -0
- data/lib/obj_parser/obj.rb +99 -0
- data/lib/obj_parser/obj_parser.rb +50 -0
- data/lib/obj_parser/point.rb +37 -0
- data/lib/obj_parser/single_indexed_obj.rb +83 -0
- data/lib/obj_parser/version.rb +3 -0
- data/obj_parser.gemspec +23 -0
- data/test/math_utils_test.rb +60 -0
- data/test/obj_parser_spec.rb +58 -0
- data/test/obj_spec.rb +33 -0
- data/test/single_indexed_obj_spec.rb +41 -0
- data/test/test_helper.rb +7 -0
- metadata +104 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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,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
|
data/obj_parser.gemspec
ADDED
@@ -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
|
data/test/test_helper.rb
ADDED
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
|