bvh 1.0.0

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.
@@ -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
@@ -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
@@ -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
@@ -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