bvh 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +27 -0
- data/EXAMPLES.rdoc +1 -0
- data/LICENSE +20 -0
- data/README.rdoc +97 -0
- data/Rakefile +58 -0
- data/VERSION +1 -0
- data/bvh.gemspec +69 -0
- data/ext/bvh/Makefile +149 -0
- data/ext/bvh/bvh.c +10 -0
- data/ext/bvh/bvh.h +10 -0
- data/ext/bvh/bvh_parser.c +63 -0
- data/ext/bvh/extconf.rb +4 -0
- data/lib/bvh.rb +74 -0
- data/lib/bvh/exporter.rb +86 -0
- data/lib/bvh/matrix.rb +19 -0
- data/lib/bvh/motion.rb +79 -0
- data/lib/bvh/motion/channel_data.rb +147 -0
- data/lib/bvh/motion/frame.rb +124 -0
- data/lib/bvh/parser.rb +177 -0
- data/lib/bvh/skeleton.rb +84 -0
- data/test/bvh_test.rb +62 -0
- data/test/data/in/karate.bvh +767 -0
- data/test/helper.rb +13 -0
- metadata +89 -0
@@ -0,0 +1,124 @@
|
|
1
|
+
class Bvh
|
2
|
+
class Motion
|
3
|
+
|
4
|
+
class Frame
|
5
|
+
# The array of ChannelData objects for this frame: one ChannelData instance for each bone.
|
6
|
+
attr_reader :channel_data
|
7
|
+
|
8
|
+
def initialize()
|
9
|
+
@channel_data = []
|
10
|
+
end
|
11
|
+
|
12
|
+
# Creates a copy of this frame, including a dup of its channel data.
|
13
|
+
def copy
|
14
|
+
ret = self.class.new
|
15
|
+
channel_data.each do |datum|
|
16
|
+
ret.channel_data << datum.dup
|
17
|
+
end
|
18
|
+
ret
|
19
|
+
end
|
20
|
+
|
21
|
+
# call-seq:
|
22
|
+
# frame + frame => new_frame
|
23
|
+
# frame - frame => new_frame
|
24
|
+
# frame / frame => new_frame
|
25
|
+
# frame * frame => new_frame
|
26
|
+
# frame + number => new_frame
|
27
|
+
# frame - number => new_frame
|
28
|
+
# frame / number => new_frame
|
29
|
+
# frame * number => new_frame
|
30
|
+
#
|
31
|
+
# Performs arithmetic on this frame with the target. The second operand may be either a number or another Frame.
|
32
|
+
# If the target is a number, then that number is added to, subtracted from, multiplied with, or divided against
|
33
|
+
# each channel of each ChannelData object in this frame.
|
34
|
+
#
|
35
|
+
# If the target is another Frame, the arithmetic looks something like this (simplified):
|
36
|
+
# return_value.channel_data[0] = frame1.channel_data[0] * frame2.channel_data[0]
|
37
|
+
# return_value.channel_data[0] = frame1.channel_data[1] * frame2.channel_data[1]
|
38
|
+
# return_value.channel_data[0] = frame1.channel_data[2] * frame2.channel_data[2]
|
39
|
+
# . . .
|
40
|
+
#
|
41
|
+
# Both frames must contain the same number of ChannelData instances, and each instance must
|
42
|
+
# have the same number of channels, and each instance must also reference the same bone.
|
43
|
+
#
|
44
|
+
# Returns a new frame containing the result.
|
45
|
+
#
|
46
|
+
def arithmetic_proc(target)
|
47
|
+
# Fooled you! I metaprogrammed it to save some typing!
|
48
|
+
end
|
49
|
+
undef arithmetic_proc
|
50
|
+
|
51
|
+
[:+, :-, :/, :*].each do |operator|
|
52
|
+
define_method operator do |target|
|
53
|
+
if target.kind_of?(Frame)
|
54
|
+
unless channel_data.length == target.channel_data.length
|
55
|
+
raise "Expected both frames to have the same amount of data"
|
56
|
+
end
|
57
|
+
ret = Frame.new
|
58
|
+
channel_data.each_with_index do |datum, index|
|
59
|
+
ret.channel_data << datum.send(operator, target.channel_data[index])
|
60
|
+
end
|
61
|
+
ret
|
62
|
+
else
|
63
|
+
ret = Frame.new
|
64
|
+
channel_data.each { |datum| ret.channel_data << datum.send(operator, target) }
|
65
|
+
end
|
66
|
+
ret
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns the channel data for the specified bone.
|
71
|
+
def channel_data_for(bone)
|
72
|
+
@channel_data.each { |c| return c if c.bone == bone }
|
73
|
+
raise "Channel data for bone not found: #{bone.name}"
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns the relative, or local, transform matrix for the specified bone in this frame.
|
77
|
+
def relative_transform_matrix(bone)
|
78
|
+
return channel_data_for(bone).relative_transform_matrix
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns the transform matrix of the root node multiplied with its children recursively
|
82
|
+
# down to the specified bone, the result being the total transformation in worldspace for this frame.
|
83
|
+
def absolute_transform_matrix(bone)
|
84
|
+
relative = relative_transform_matrix(bone)
|
85
|
+
if bone.parent then absolute_transform_matrix(bone.parent) * relative
|
86
|
+
else relative
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Modifies a single attribute of the channel data for this bone in this frame. Returns self.
|
91
|
+
#
|
92
|
+
# Ex:
|
93
|
+
# bvh.last_frame.set_channel bone, 'Xrotation', 180 # => sets the bone to a 180 deg rotation around the X-axis
|
94
|
+
def set_channel(bone, channel, theta)
|
95
|
+
channel_data_for(bone).set_channel(channel, theta)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Retrives the value for a specific channel for a specific bone within this frame.
|
99
|
+
def get_channel(bone, channel)
|
100
|
+
channel_data_for(bone).get_channel(channel)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Modifies the channel data for this bone in this frame, resulting in a rotation around the specified
|
104
|
+
# channel.
|
105
|
+
#
|
106
|
+
# Ex:
|
107
|
+
# bvh.last_frame.set_channel bone, 'Xrotation', 180 # => rotates the bone by 180 deg rotation around the X-axis
|
108
|
+
def rotate!(bone, channel, theta)
|
109
|
+
set_channel(bone, channel, channel_data_for(bone).get_channel(channel) + theta)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Adds x, y and z to the X, Y and Z position channels for this bone in this frame, resulting in a "movement" or
|
113
|
+
# translation.
|
114
|
+
def translate!(bone, x, y, z)
|
115
|
+
set_channel(bone, 'Xposition', channel_data_for(bone).get_channel('Xposition')+x)
|
116
|
+
set_channel(bone, 'Yposition', channel_data_for(bone).get_channel('Yposition')+y)
|
117
|
+
set_channel(bone, 'Zposition', channel_data_for(bone).get_channel('Zposition')+z)
|
118
|
+
end
|
119
|
+
|
120
|
+
alias transform_matrix absolute_transform_matrix
|
121
|
+
alias local_transform_matrix relative_transform_matrix
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
data/lib/bvh/parser.rb
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
class Bvh
|
2
|
+
class Parser
|
3
|
+
attr_reader :filename
|
4
|
+
attr_reader :source
|
5
|
+
|
6
|
+
def initialize(file)
|
7
|
+
@filename = file
|
8
|
+
@source = File.read(file)
|
9
|
+
end
|
10
|
+
|
11
|
+
def parse(bvh)
|
12
|
+
@bvh = bvh
|
13
|
+
|
14
|
+
# a little touch-up...
|
15
|
+
s = source
|
16
|
+
# it's tempting to just downcase the whole string, but that'd change the case of object names, which might be
|
17
|
+
# bad for the end user. So we'll swap out specific keywords instead.
|
18
|
+
s.gsub!(/^((\s*)(root|offset|channels|joint|end site|hierarchy)(([^\n\{]*)(\n|\{)))/mi) do
|
19
|
+
match = $~
|
20
|
+
"#{match[2]}#{match[3].downcase.gsub(/\s/, '_')} \"#{match[5].strip}\"#{match[6]}"
|
21
|
+
end
|
22
|
+
# make { . . . } into proper Ruby blocks
|
23
|
+
s.gsub!(/[\n\s]*\{/m, ' do').gsub!(/[\n\s]*\}/m, "\nend")
|
24
|
+
|
25
|
+
# Finally, handle the MOTION segment, which can be treated as a single method call.
|
26
|
+
s.gsub!(/^((\s*)(motion)(.*))/mi) do
|
27
|
+
"#{$~[2]}#{$~[3].downcase} <<-EOF\n#{$~[4].strip}\nEOF\n"
|
28
|
+
end
|
29
|
+
|
30
|
+
eval(s, binding, @filename, 1)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
attr_reader :bvh, :mode, :current_node
|
35
|
+
|
36
|
+
def bone(type, name, &block)
|
37
|
+
raise ArgumentError, "#{type} #{name} is unexpected at this time" unless mode == :hierarchy
|
38
|
+
bone = type.new(name)
|
39
|
+
|
40
|
+
if current_node.nil?
|
41
|
+
bvh.create_skeleton!.root = bone
|
42
|
+
else
|
43
|
+
current_node.add_joint!(bone)
|
44
|
+
end
|
45
|
+
|
46
|
+
@current_node, bone = bone, @current_node
|
47
|
+
instance_eval(&block)
|
48
|
+
@current_node = bone
|
49
|
+
end
|
50
|
+
|
51
|
+
# Ex:
|
52
|
+
# root "joint-pelvis" do
|
53
|
+
# offset "0.00 0.00 0.00"
|
54
|
+
# channels "6 Xposition Ypoisition Zposition Zrotation Xrotation Yrotation"
|
55
|
+
# joint "joint-spine3" do
|
56
|
+
# . . .
|
57
|
+
# end
|
58
|
+
# end
|
59
|
+
def root(name, &block)
|
60
|
+
@current_node = nil
|
61
|
+
bone Bvh::Skeleton::Bone, name, &block
|
62
|
+
end
|
63
|
+
|
64
|
+
# Ex:
|
65
|
+
# root "joint-pelvis" do
|
66
|
+
# . . .
|
67
|
+
# joint "joint-spine3" do
|
68
|
+
# offset "0.00 -10.65 0.00"
|
69
|
+
# channels "3 Zrotation Xrotation Yrotation"
|
70
|
+
# end_site do
|
71
|
+
# . . .
|
72
|
+
# end
|
73
|
+
# end
|
74
|
+
# end
|
75
|
+
def joint(name, &block)
|
76
|
+
bone Bvh::Skeleton::Bone, name, &block
|
77
|
+
end
|
78
|
+
|
79
|
+
# Ex:
|
80
|
+
# root "joint-pelvis" do
|
81
|
+
# . . .
|
82
|
+
# joint "joint-spine3" do
|
83
|
+
# . . .
|
84
|
+
# end_site do
|
85
|
+
# offset "0.00 -7.00 0.00"
|
86
|
+
# end
|
87
|
+
# end
|
88
|
+
# end
|
89
|
+
def end_site(*unused, &block)
|
90
|
+
bone Bvh::Skeleton::Bone, nil, &block
|
91
|
+
end
|
92
|
+
|
93
|
+
def hierarchy(*unused)
|
94
|
+
@mode = :hierarchy
|
95
|
+
end
|
96
|
+
|
97
|
+
def offset(val)
|
98
|
+
raise ArgumentError, "OFFSET is unexpected at this time" unless current_node
|
99
|
+
raise ArgumentError, "Already have OFFSET data for this node" unless current_node.offset.length == 0
|
100
|
+
current_node.offset.concat val.split.collect { |i| i.to_f }
|
101
|
+
end
|
102
|
+
|
103
|
+
def channels(val)
|
104
|
+
raise ArgumentError, "CHANNELS is unexpected at this time" unless current_node
|
105
|
+
raise ArgumentError, "Already have CHANNELS data for this node" unless current_node.channels.length == 0
|
106
|
+
vals = val.split
|
107
|
+
count = vals.shift.to_i # how many?
|
108
|
+
raise ArgumentError, "Expected #{count} channels, found #{vals.length}" if vals.length != count
|
109
|
+
current_node.channels.concat vals
|
110
|
+
end
|
111
|
+
|
112
|
+
def motion(motion_data)
|
113
|
+
raise ArgumentError, "Motion data is unexpected at this time" unless mode == :hierarchy
|
114
|
+
@mode = :motion
|
115
|
+
frame_count = nil
|
116
|
+
motion_data.each_line do |line|
|
117
|
+
line = line.downcase.strip
|
118
|
+
words = line.split
|
119
|
+
case words.first
|
120
|
+
when 'frames:'
|
121
|
+
if words[1] =~ /[^0-9]/
|
122
|
+
raise ArgumentError, "Only one positive numeric integer value expected at this time (not #{words[1]})"
|
123
|
+
end
|
124
|
+
frame_count = words[1].to_i
|
125
|
+
when 'frame'
|
126
|
+
case words[1]
|
127
|
+
when 'time:'
|
128
|
+
if words[2] =~ /[^0-9\.]/
|
129
|
+
raise ArgumentError, "Only one positive numeric decimal value expected at this time (not #{words[2]})"
|
130
|
+
end
|
131
|
+
bvh.frame_time = words[2].to_f
|
132
|
+
else
|
133
|
+
raise ArgumentError, "Motion data not understood: #{line}"
|
134
|
+
end
|
135
|
+
when nil, '' # blank line, do nothing
|
136
|
+
else
|
137
|
+
channels = words.collect do |w|
|
138
|
+
raise ArgumentError, "Only numeric values are expected at this time (not #{w})" if w =~ /[^0-9\.\-]/
|
139
|
+
w.to_f
|
140
|
+
end
|
141
|
+
add_frame(channels)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
unless frame_count.nil? or frame_count == bvh.frame_count
|
145
|
+
raise ArgumentError, "Expected %s frames, found %s" % [frame_count, bvh.frame_count]
|
146
|
+
end
|
147
|
+
unless frame_count == 0
|
148
|
+
raise ArgumentError, "Frame time is 0; this would result in infinite FPS!" if bvh.frame_time == 0
|
149
|
+
raise ArgumentError, "Frame time is #{bvh.frame_time}! Should be a positive number." if bvh.frame_time < 0
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def add_frame(channels)
|
154
|
+
frame = Bvh::Motion::Frame.new
|
155
|
+
frame.channel_data.concat channel_data(channels, bvh.root)
|
156
|
+
bvh.frames << frame
|
157
|
+
raise ArgumentError, "Not enough channels: Need #{-channels.length} more" if channels.length < 0
|
158
|
+
raise ArgumentError, "Too many channels: #{channels.length} unaccounted for" if channels.length > 0
|
159
|
+
end
|
160
|
+
|
161
|
+
# this is now a binding.
|
162
|
+
# def channel_data(channels, bone)
|
163
|
+
# return [] unless bone.respond_to? :channels
|
164
|
+
# data = Bvh::Motion::ChannelData.new(bone)
|
165
|
+
# bone.channels.each do |channel|
|
166
|
+
# data[channel] = channels.shift
|
167
|
+
# end
|
168
|
+
# r = [ data ]
|
169
|
+
# if bone.respond_to? :joints
|
170
|
+
# bone.joints.each do |child|
|
171
|
+
# r.concat channel_data(channels, child)
|
172
|
+
# end
|
173
|
+
# end
|
174
|
+
# r
|
175
|
+
# end
|
176
|
+
end
|
177
|
+
end
|
data/lib/bvh/skeleton.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
class Bvh
|
2
|
+
class Skeleton
|
3
|
+
attr_accessor :root
|
4
|
+
|
5
|
+
def find_by_name(name); root.find_by_name(name); end
|
6
|
+
|
7
|
+
alias / find_by_name
|
8
|
+
|
9
|
+
class Bone
|
10
|
+
# the name of this bone
|
11
|
+
attr_accessor :name
|
12
|
+
|
13
|
+
# this bone's parent bone, if any (if nil, this bone is a root)
|
14
|
+
attr_reader :parent
|
15
|
+
|
16
|
+
# the channels, or degrees of freedom, that this bone allows -- and the order that they appear in
|
17
|
+
attr_reader :channels
|
18
|
+
|
19
|
+
# the offset of this bone in relation to its parent
|
20
|
+
attr_reader :offset
|
21
|
+
|
22
|
+
# an array of joints, or child bones. Each should have its parent set to this bone. See also #add_joint!
|
23
|
+
attr_reader :joints
|
24
|
+
|
25
|
+
def initialize(name = nil, parent = nil)
|
26
|
+
@name, @parent = name, parent
|
27
|
+
@channels, @offset, @joints = [], [], []
|
28
|
+
@frames = []
|
29
|
+
end
|
30
|
+
|
31
|
+
# Adds the specified bone to this bone's list of joints, setting its parent equal to this bone.
|
32
|
+
# This bone is returned.
|
33
|
+
def add_joint!(joint)
|
34
|
+
joint.instance_variable_set("@parent", self)
|
35
|
+
raise "Cannot add joint: name '#{joint.name}' already exists" if self / joint.name
|
36
|
+
joints << joint unless joints.include? joint
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
# returns true if this is a "leaf" node; that is, there are no joints
|
41
|
+
def end_site?
|
42
|
+
joints.length == 0
|
43
|
+
end
|
44
|
+
|
45
|
+
# returns the descendant joint (or self) with the specified name.
|
46
|
+
def find_by_name(name)
|
47
|
+
return self if self.name == name
|
48
|
+
joints.each { |j| r = j.find_by_name(name); return r unless r.nil? }
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns the length of this bone, inferred from the offset of its first joint.
|
53
|
+
def length
|
54
|
+
if end_site? then 0
|
55
|
+
else
|
56
|
+
ofs = joints.first.offset
|
57
|
+
# ofs is a vector, so we need its magnitude
|
58
|
+
if ofs.nil? then 0
|
59
|
+
else Math.sqrt(ofs[0]**2) + Math.sqrt(ofs[1]**2) + Math.sqrt(ofs[2]**2)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns a unit vector representing the orientation of this bone, inferred from the offset of its first joint.
|
65
|
+
def orientation
|
66
|
+
if end_site? then [0,0,0]
|
67
|
+
else
|
68
|
+
ofs = joints.first.offset
|
69
|
+
if ofs.nil? then [0,0,0]
|
70
|
+
else
|
71
|
+
# ofs is a vector, so we need to normalize it
|
72
|
+
max = proc { |a,b| a.to_f.abs > b.to_f.abs ? a.to_f.abs : b.to_f.abs }
|
73
|
+
max = max.call(ofs[0], max.call(ofs[1], ofs[2]))
|
74
|
+
if max > 0 then [ofs[0].to_f / max, ofs[1].to_f / max, ofs[2].to_f / max]
|
75
|
+
else [0,0,0]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
alias / find_by_name
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/test/bvh_test.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
### Since I don't really have much data to test against, this file mostly just tests that no
|
4
|
+
### errors are encountered.
|
5
|
+
###
|
6
|
+
### FIXME: Add assertion data.
|
7
|
+
###
|
8
|
+
class BvhTest < Test::Unit::TestCase
|
9
|
+
context "The Bvh class" do
|
10
|
+
should "import BVH files" do
|
11
|
+
Bvh.import(File.join(test_path, "data/in/karate.bvh"))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context "A Bvh instance" do
|
16
|
+
setup do
|
17
|
+
@karate = Bvh.import(File.join(test_path, "data/in/karate.bvh"))
|
18
|
+
end
|
19
|
+
|
20
|
+
should "export to file" do
|
21
|
+
@karate.export(File.join(test_path, "data/out/karate.bvh"))
|
22
|
+
end
|
23
|
+
|
24
|
+
should "be able to import its own exports" do
|
25
|
+
@karate.export(File.join(test_path, "data/out/karate.bvh"))
|
26
|
+
Bvh.import(File.join(test_path, "data/out/karate.bvh"))
|
27
|
+
end
|
28
|
+
|
29
|
+
should "be able to calculate the transform matrices" do
|
30
|
+
frames = @karate.motion.frames
|
31
|
+
bone = @karate.skeleton/"LeftWrist"
|
32
|
+
|
33
|
+
# FIXME: i don't know what values to assert against
|
34
|
+
frames.last.local_transform_matrix(bone)
|
35
|
+
frames.last.transform_matrix(bone)
|
36
|
+
end
|
37
|
+
|
38
|
+
should "be able to add time to frames" do
|
39
|
+
# add 3 seconds, during which each bone's offset should move and rotate by 3 units
|
40
|
+
# no idea what this would look like if rendered, it's just a test
|
41
|
+
frame_count = @karate.frame_count
|
42
|
+
@karate.motion.add_time(3, @karate.motion.frames.last + 3)
|
43
|
+
assert_equal frame_count+90, @karate.frame_count # 30 fps for 3 sec
|
44
|
+
end
|
45
|
+
|
46
|
+
context "A Bone" do
|
47
|
+
setup do
|
48
|
+
@bone = @karate.skeleton/"LeftWrist"
|
49
|
+
end
|
50
|
+
|
51
|
+
should "have a length" do
|
52
|
+
len = @bone.length
|
53
|
+
assert_equal 7.0, len
|
54
|
+
end
|
55
|
+
|
56
|
+
should "have an orientation" do
|
57
|
+
orientation = @bone.orientation
|
58
|
+
assert_equal [0,-1,0], orientation
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|