bvh 1.0.0

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