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.
- 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
data/ext/bvh/bvh.h
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
#include "bvh.h"
|
2
|
+
|
3
|
+
static VALUE rb_fParseChannelData(VALUE self, VALUE channels, VALUE bone);
|
4
|
+
|
5
|
+
static VALUE rb_cBvhParser = Qnil;
|
6
|
+
static VALUE rb_cMotion = Qnil;
|
7
|
+
static VALUE rb_cChannelData = Qnil;
|
8
|
+
|
9
|
+
void bvh_init_parser()
|
10
|
+
{
|
11
|
+
rb_cBvhParser = rb_define_class_under(rb_cBvh, "Parser", rb_cObject);
|
12
|
+
rb_cMotion = rb_define_class_under(rb_cBvh, "Motion", rb_cObject);
|
13
|
+
rb_cChannelData = rb_define_class_under(rb_cMotion, "ChannelData", rb_cHash);
|
14
|
+
|
15
|
+
rb_define_private_method(rb_cBvhParser, "channel_data", rb_fParseChannelData, 2);
|
16
|
+
}
|
17
|
+
|
18
|
+
static void debug(const char *text)
|
19
|
+
{
|
20
|
+
rb_funcall(rb_cObject, rb_intern("puts"), rb_str_new2(text));
|
21
|
+
}
|
22
|
+
|
23
|
+
static VALUE rb_fParseChannelData(VALUE self, VALUE channels, VALUE bone)
|
24
|
+
{
|
25
|
+
char buf[256];
|
26
|
+
|
27
|
+
VALUE data;
|
28
|
+
struct RArray *rchannels, *joints;
|
29
|
+
long i;
|
30
|
+
VALUE channel, joint;
|
31
|
+
VALUE r = rb_ary_new();
|
32
|
+
|
33
|
+
// return [] unless bone.respond_to? :channels
|
34
|
+
if (!rb_respond_to(bone, rb_intern("channels")))
|
35
|
+
return r;
|
36
|
+
|
37
|
+
// data = Bvh::Motion::ChannelData.new(bone)
|
38
|
+
data = rb_funcall(rb_cChannelData, rb_intern("new"), 1, bone);
|
39
|
+
|
40
|
+
// bone.channels.each { |channel| data[channel] = channels.shift }
|
41
|
+
rchannels = RARRAY(rb_funcall(bone, rb_intern("channels"), 0));
|
42
|
+
for (i = 0; i < rchannels->len; i++)
|
43
|
+
{
|
44
|
+
channel = *(rchannels->ptr+i);
|
45
|
+
//... data[channel] = channels.shift ...
|
46
|
+
rb_hash_aset(data, channel, rb_funcall(channels, rb_intern("shift"), 0));
|
47
|
+
}
|
48
|
+
|
49
|
+
// r = [data]
|
50
|
+
rb_ary_push(r, data);
|
51
|
+
|
52
|
+
// bone.joints.each { |joint| r.concat channel_data(channels, joint) }
|
53
|
+
joints = RARRAY(rb_funcall(bone, rb_intern("joints"), 0));
|
54
|
+
for (i = 0; i < joints->len; i++)
|
55
|
+
{
|
56
|
+
joint = *(joints->ptr+i);
|
57
|
+
// ... r.concat(channel_data(channels, joint)) ...
|
58
|
+
rb_funcall(r, rb_intern("concat"), 1, rb_fParseChannelData(self, channels, joint));
|
59
|
+
}
|
60
|
+
|
61
|
+
// r
|
62
|
+
return r;
|
63
|
+
}
|
data/ext/bvh/extconf.rb
ADDED
data/lib/bvh.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'bvh.so'
|
2
|
+
require 'bvh/matrix'
|
3
|
+
require 'bvh/motion'
|
4
|
+
require 'bvh/motion/channel_data'
|
5
|
+
require 'bvh/motion/frame'
|
6
|
+
require 'bvh/skeleton'
|
7
|
+
require 'bvh/parser'
|
8
|
+
require 'bvh/exporter'
|
9
|
+
|
10
|
+
|
11
|
+
class Bvh
|
12
|
+
# The array of skeletons associated with this BVH.
|
13
|
+
attr_reader :skeletons
|
14
|
+
|
15
|
+
# The motion capture data associated with this BVH.
|
16
|
+
attr_reader :motion
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@motion = Bvh::Motion.new
|
20
|
+
@skeletons = []
|
21
|
+
end
|
22
|
+
|
23
|
+
# Assigns the root bone in #skeleton
|
24
|
+
def root=(a); skeleton.root = a; end
|
25
|
+
|
26
|
+
# Returns the root bone in #skeleton
|
27
|
+
def root; skeleton.root; end
|
28
|
+
|
29
|
+
# Returns the frame data associated with this BVH file.
|
30
|
+
def frames; motion.frames; end
|
31
|
+
|
32
|
+
# Returns the last frame associated with this animation.
|
33
|
+
def last_frame; frames.last; end
|
34
|
+
|
35
|
+
# Returns the amount of time, in seconds, each frame occupies. Most BVH files have this set to
|
36
|
+
# 0.333333, which is equal to 30 frames per second.
|
37
|
+
def frame_time; motion.frame_time; end
|
38
|
+
|
39
|
+
# Sets the amount of time, in seconds, each frame occupies. Most BVH files have this set to
|
40
|
+
# 0.333333, which is equal to 30 frames per second.
|
41
|
+
def frame_time=(a); motion.frame_time = a; end
|
42
|
+
|
43
|
+
# Returns the frames per second calculated by (1 / frame_time)
|
44
|
+
def frames_per_second; motion.frames_per_second; end
|
45
|
+
|
46
|
+
# Assigns the frame_time by calculating it from (1 / frames_per_second)
|
47
|
+
def frames_per_second=(a); motion.frames_per_second = a; end
|
48
|
+
|
49
|
+
# Returns the number of frames in the motion capture data.
|
50
|
+
def frame_count; motion.frame_count; end
|
51
|
+
|
52
|
+
# Returns the first (and usually the only) skeleton in the #skeletons array.
|
53
|
+
def skeleton; @skeletons.first; end
|
54
|
+
|
55
|
+
# Creates a new skeleton, adds it to the #skeletons array, and returns it.
|
56
|
+
def create_skeleton!; (@skeletons << Bvh::Skeleton.new).last; end
|
57
|
+
|
58
|
+
class << self
|
59
|
+
# Instantiates a new BVH and imports its data from the specified file
|
60
|
+
def import(file)
|
61
|
+
bvh = self.new
|
62
|
+
parser = Bvh::Parser.new(file)
|
63
|
+
parser.parse(bvh)
|
64
|
+
bvh
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Exports this BVH into the specified file.
|
69
|
+
def export(file)
|
70
|
+
exporter = Bvh::Exporter.new(file)
|
71
|
+
exporter.export(self)
|
72
|
+
self
|
73
|
+
end
|
74
|
+
end
|
data/lib/bvh/exporter.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
class Bvh
|
2
|
+
class Exporter
|
3
|
+
attr_reader :filename
|
4
|
+
attr_reader :source
|
5
|
+
|
6
|
+
def initialize(file)
|
7
|
+
@filename = file
|
8
|
+
end
|
9
|
+
|
10
|
+
def export(bvh)
|
11
|
+
#raise "File exists" if File.exist?(filename)
|
12
|
+
unless bvh.frame_count == 0
|
13
|
+
raise ArgumentError, "Frame time is 0; this would result in infinite FPS!" if bvh.frame_time == 0
|
14
|
+
raise ArgumentError, "Frame time is #{bvh.frame_time}! Should be a positive number." if bvh.frame_time < 0
|
15
|
+
end
|
16
|
+
@bvh = bvh
|
17
|
+
File.open(filename, "w+") do |file|
|
18
|
+
@file = file
|
19
|
+
file.puts "HIERARCHY"
|
20
|
+
bvh.skeletons.each { |skel| export_skeleton(skel) }
|
21
|
+
export_motion(bvh.motion)
|
22
|
+
file.puts # end with a blank line
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
attr_reader :bvh, :file
|
28
|
+
|
29
|
+
# export HIERARCHY data
|
30
|
+
def export_skeleton(skele)
|
31
|
+
export_root skele.root
|
32
|
+
end
|
33
|
+
|
34
|
+
# export ROOT node and its joints
|
35
|
+
def export_root(root)
|
36
|
+
file.puts "ROOT #{root.name}"
|
37
|
+
file.puts "{"
|
38
|
+
export_bone_data(root)
|
39
|
+
file.puts "}"
|
40
|
+
end
|
41
|
+
|
42
|
+
# export JOINT node, or End Site if joint has no joints
|
43
|
+
# level defines number of preceding tab stops
|
44
|
+
def export_joint(bone, level = 1)
|
45
|
+
tabs = "\t"*level
|
46
|
+
header = if bone.end_site? then "End Site"
|
47
|
+
else "JOINT #{bone.name}"
|
48
|
+
end
|
49
|
+
file.puts "#{tabs}#{header}"
|
50
|
+
file.puts "#{tabs}{"
|
51
|
+
export_bone_data(bone, level)
|
52
|
+
file.puts "#{tabs}}"
|
53
|
+
end
|
54
|
+
|
55
|
+
# exports bone data, regardless of type, including its joints
|
56
|
+
# level defines number of preceding tab stops
|
57
|
+
def export_bone_data(bone, level = 0)
|
58
|
+
level += 1 # we're within a node, so it's one more tab than expected
|
59
|
+
tabs = "\t"*level
|
60
|
+
file.puts "#{tabs}OFFSET\t #{bone.offset.collect { |i| "%.6f" % i }.join("\t ")}"
|
61
|
+
if bone.channels.length > 0
|
62
|
+
chans = bone.channels.join(" ")
|
63
|
+
file.puts("#{tabs}CHANNELS #{bone.channels.length} #{chans}")
|
64
|
+
end
|
65
|
+
bone.joints.each { |joint| export_joint(joint, level) }
|
66
|
+
end
|
67
|
+
|
68
|
+
# export MOTION data
|
69
|
+
def export_motion(motion)
|
70
|
+
file.puts "MOTION"
|
71
|
+
file.puts "Frames: #{motion.frame_count}"
|
72
|
+
file.puts "Frame Time: #{motion.frame_time}"
|
73
|
+
motion.frames.each do |frame|
|
74
|
+
line = ""
|
75
|
+
# chan.bone.channels vs. chan.channels to maintain order
|
76
|
+
frame.channel_data.each do |channel_data|
|
77
|
+
channel_data.bone.channels.each do |i|
|
78
|
+
chan = "%.6f" % channel_data[i]
|
79
|
+
line = "#{line}#{chan}\t"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
file.puts line.strip
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/bvh/matrix.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'matrix'
|
2
|
+
|
3
|
+
# This adds a few methods to the default Ruby Matrix implementation.
|
4
|
+
class Matrix
|
5
|
+
# Sets row "i", column "j", to the value "x".
|
6
|
+
def []=(i,j,x)
|
7
|
+
@rows[i][j] = x
|
8
|
+
end
|
9
|
+
|
10
|
+
# Turns this matrix into an identity matrix, erasing all previous values.
|
11
|
+
def load_identity!
|
12
|
+
row_size.times do |r|
|
13
|
+
column_size.times do |c|
|
14
|
+
self[r, c] = (r == c ? 1 : 0)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
self
|
18
|
+
end
|
19
|
+
end
|
data/lib/bvh/motion.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
class Bvh
|
2
|
+
class Motion
|
3
|
+
# The amount of time that passes for each frame. Most BVH files have this set to 0.0333, or 30 frames per second.
|
4
|
+
# The following calculations might also be useful to you:
|
5
|
+
# frame_time = 1 / frames_per_second # => 0.0333 = 1 / 30
|
6
|
+
# frames_per_second = 1 / frame_time # => 30 = 1 / 0.0333
|
7
|
+
attr_accessor :frame_time
|
8
|
+
|
9
|
+
# The array of frames in this animation. You can modify this directly if you wish.
|
10
|
+
attr_reader :frames
|
11
|
+
|
12
|
+
# Returns the frames per second calculated by (1 / frame_time)
|
13
|
+
def frames_per_second; 1 / frame_time; end
|
14
|
+
|
15
|
+
# Assigns the frame_time by calculating it from (1 / frames_per_second)
|
16
|
+
def frames_per_second=(a); self.frame_time = 1 / a; end
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@frames = []
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns the number of frames in this animation.
|
23
|
+
def frame_count
|
24
|
+
frames.length
|
25
|
+
end
|
26
|
+
|
27
|
+
# Adds the specified frame or frames to the end of this animation.
|
28
|
+
def add_frame(*frames)
|
29
|
+
self.frames.concat frames.flatten
|
30
|
+
end
|
31
|
+
|
32
|
+
# Creates a single frame that is an exact copy of the last frame in this animation.
|
33
|
+
def create_frame
|
34
|
+
frames.last.copy
|
35
|
+
end
|
36
|
+
|
37
|
+
# Creates N frames via #create_frame.
|
38
|
+
def create_frames(n)
|
39
|
+
r = []
|
40
|
+
n.times { r << create_frame }
|
41
|
+
r
|
42
|
+
end
|
43
|
+
|
44
|
+
# Creates enough frames to fill the specified number of seconds, adds them to the animation, and returns self.
|
45
|
+
#
|
46
|
+
# target_frame is the state at which the animation should be in upon reaching the final frame of the alotted time.
|
47
|
+
# If it contains data that is different from the last frame in this animation, then the result will be an
|
48
|
+
# interpolation between the two over the course of the alotted time.
|
49
|
+
#
|
50
|
+
# You can use this method to specify key points in the animation, and produce a fluid result.
|
51
|
+
def add_time(seconds, target_frame = frames.last)
|
52
|
+
raise "Can't calculate frames from seconds: frame_time is zero!" if frame_time == 0
|
53
|
+
num_frames = (seconds / frame_time).to_i
|
54
|
+
return self if num_frames == 0
|
55
|
+
|
56
|
+
# buf holds the amount to change each frame
|
57
|
+
buf = (frames.last - target_frame) / num_frames.to_f
|
58
|
+
|
59
|
+
num_frames.times do |i|
|
60
|
+
if i == (num_frames-1) then add_frame(target_frame.copy) # last iteration, last frame... we can cheat.
|
61
|
+
else add_frame(create_frame + buf) # create_frame clones the last frame, and buf moves it closer to target
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
# Chops the specified amount of time off of the end of this animation and then returns self.
|
69
|
+
def truncate_time(seconds)
|
70
|
+
raise "Can't calculate frames from seconds: frame_time is zero!" if frame_time == 0
|
71
|
+
num_frames = (seconds / frame_time).to_i
|
72
|
+
return self if num_frames == 0
|
73
|
+
frames[-1..-num_frames].each { |i| frames.delete i }
|
74
|
+
self
|
75
|
+
end
|
76
|
+
|
77
|
+
alias add_frames add_frame
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
class Bvh
|
2
|
+
class Motion
|
3
|
+
class ChannelData < Hash
|
4
|
+
# The bone that is related to this channel data.
|
5
|
+
attr_reader :bone
|
6
|
+
|
7
|
+
def initialize(bone, *args, &block)
|
8
|
+
@bone = bone
|
9
|
+
super(*args, &block)
|
10
|
+
end
|
11
|
+
|
12
|
+
# call-seq:
|
13
|
+
# channel_data + channel_data => new_channel_data
|
14
|
+
# channel_data - channel_data => new_channel_data
|
15
|
+
# channel_data / channel_data => new_channel_data
|
16
|
+
# channel_data * channel_data => new_channel_data
|
17
|
+
# channel_data + number => new_channel_data
|
18
|
+
# channel_data - number => new_channel_data
|
19
|
+
# channel_data / number => new_channel_data
|
20
|
+
# channel_data * number => new_channel_data
|
21
|
+
#
|
22
|
+
# Performs arithmetic on this frame with the target. The second operand may be either a number or another
|
23
|
+
# ChannelData. If the target is a number, then that number is added to, subtracted from, multiplied with, or
|
24
|
+
# divided against each channel of this ChannelData.
|
25
|
+
#
|
26
|
+
# If the target is another ChannelData, the arithmetic looks something like this:
|
27
|
+
# return_value['Xposition'] = channel_data_one['Xposition'] * channel_data_two['Xposition']
|
28
|
+
# return_value['Yposition'] = channel_data_one['Yposition'] * channel_data_two['Yposition']
|
29
|
+
# return_value['Zposition'] = channel_data_one['Zposition'] * channel_data_two['Zposition']
|
30
|
+
# . . .
|
31
|
+
#
|
32
|
+
# Both objects must contain the same number of channels, and must also reference the same bone.
|
33
|
+
#
|
34
|
+
# Returns a new instance of ChannelData containing the result.
|
35
|
+
#
|
36
|
+
def arithmetic_proc(target)
|
37
|
+
# Fooled you again! I metaprogrammed it to save some typing!
|
38
|
+
end
|
39
|
+
undef arithmetic_proc
|
40
|
+
|
41
|
+
[:+, :-, :/, :*].each do |operator|
|
42
|
+
define_method operator do |target|
|
43
|
+
ret = ChannelData.new(bone)
|
44
|
+
if target.kind_of?(ChannelData)
|
45
|
+
target.each { |key, value| ret[key] = 0.send(operator, value) }
|
46
|
+
self.each do |key, value|
|
47
|
+
ret[key] = value.send(operator, 0)
|
48
|
+
ret[key] = value.send(operator, target[key]) if target.key?(key)
|
49
|
+
end
|
50
|
+
else
|
51
|
+
self.each { |key, value| ret[key] = value.send(operator, target) }
|
52
|
+
end
|
53
|
+
ret
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Sets the specified channel to a value of theta, and then returns self.
|
58
|
+
def set_channel(channel, theta)
|
59
|
+
channel = channel.to_s if channel.kind_of? Symbol
|
60
|
+
raise "Channel not found: #{channel} (expected one of #{self.keys.inspect})" unless self.keys.include? channel
|
61
|
+
self[channel] = theta
|
62
|
+
end
|
63
|
+
|
64
|
+
# Retrieves and returns the specified channel datum, raising an error if its key is not found.
|
65
|
+
# If the channel key is a symbol, it is converted to a string before the lookup is performed.
|
66
|
+
def get_channel(channel)
|
67
|
+
channel = channel.to_s if channel.kind_of?(Symbol)
|
68
|
+
raise "Channel not found: #{channel} (expected one of #{self.keys.inspect})" unless self.keys.include?(channel)
|
69
|
+
self[channel]
|
70
|
+
end
|
71
|
+
|
72
|
+
# Modifies the specified channel datum, resulting in a rotation around the specified channel.
|
73
|
+
def rotate!(channel, theta)
|
74
|
+
theta = get_channel(channel)+theta
|
75
|
+
# this doesn't really have an effect, but may help keep the file from getting junked.
|
76
|
+
if theta >= 360 then theta %= 360
|
77
|
+
elsif theta <= -360 then theta %= -360
|
78
|
+
end
|
79
|
+
set_channel(channel, theta)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Adds x, y and z to the X, Y and Z position channels, resulting in a "movement" or translation.
|
83
|
+
def translate!(x, y, z)
|
84
|
+
set_channel('Xposition', get_channel('Xposition')+x)
|
85
|
+
set_channel('Yposition', get_channel('Yposition')+y)
|
86
|
+
set_channel('Zposition', get_channel('Zposition')+z)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns the transform matrix for this bone's channel data. See also
|
90
|
+
# Frame#relative_transform_matrix and Frame#absolute_transform_matrix.
|
91
|
+
#
|
92
|
+
# The resultant matrix needs to be multiplied against the parent bone's transform matrix in order
|
93
|
+
# to be accurate to worldspace. Otherwise it's only accurate in local space. If you're not sure what
|
94
|
+
# that means, then use Frame#absolute_transform_matrix instead.
|
95
|
+
def relative_transform_matrix()
|
96
|
+
# theta is retrieved from the set of numbers loaded from the BVH file, or is supplied directly
|
97
|
+
#
|
98
|
+
# R is the matrix calculated for a rotation of theta degrees around the vector V
|
99
|
+
# This is performed on right, view and up vectors, multiplying the three matrices together
|
100
|
+
# to construct the rotation matrix for this bone. Since the calculations are done in world
|
101
|
+
# space, the right, up and view vectors are [1,0,0]; [0,1,0]; [0,0,1], respectively. No reason
|
102
|
+
# we couldn't attach a Camera object to this bone and figure it out that way, but it'd actually
|
103
|
+
# be more work to create and maintain the camera than to just calculate the matrix. Obviously,
|
104
|
+
# the resultant matrix needs to be multiplied against the parent bone's rotation matrix in order
|
105
|
+
# to be accurate to worldspace. Otherwise it's only accurate in local space.
|
106
|
+
#
|
107
|
+
# R = [tx^2 + c, txy - sz, txz + sy, 0]
|
108
|
+
# [txy + sz, ty^2 + c, tyz - sx, 0]
|
109
|
+
# [txz - sy, tyz + sx, tz^2 + c, 0]
|
110
|
+
# [ 0, 0, 0, 1]
|
111
|
+
#
|
112
|
+
# where c = cos theta,
|
113
|
+
# s = sin theta,
|
114
|
+
# t = 1 - c
|
115
|
+
# [x,y,z] = a unit vector on the axis of rotation
|
116
|
+
|
117
|
+
# bone.channels.each so that we don't lose order of operation
|
118
|
+
r = Matrix.identity(4)
|
119
|
+
bone.channels.each do |chan|
|
120
|
+
v = nil
|
121
|
+
case chan
|
122
|
+
when 'Xrotation' then v = [1,0,0] # right vector
|
123
|
+
when 'Yrotation' then v = [0,1,0] # up vector
|
124
|
+
when 'Zrotation' then v = [0,0,1] # view vector
|
125
|
+
else next # ignore nonrotational values. To my knowledge, this includes only position values.
|
126
|
+
end
|
127
|
+
theta = self[chan]
|
128
|
+
x, y, z = v
|
129
|
+
c, s, t = Math.cos(theta), Math.sin(theta), 1 - Math.cos(theta)
|
130
|
+
mat = Matrix.identity(4)
|
131
|
+
mat[0,0], mat[0,1], mat[0,2] = t*(x**2) + c, t*x*y - s*z, t*x*z + s*y
|
132
|
+
mat[1,0], mat[1,1], mat[1,2] = t*x*y + s*z, t*(y**2) + c, t*y*z - s*x
|
133
|
+
mat[2,0], mat[2,1], mat[2,2] = t*x*z - s*y, t*y*z + s*x, t*(z**2) + c
|
134
|
+
r *= mat
|
135
|
+
end
|
136
|
+
|
137
|
+
# Finally, the last row is simply set to the translation and/or bone offset.
|
138
|
+
r[0,3] = bone.offset[0] + (self.key?("Xposition") ? self['Xposition'] : 0)
|
139
|
+
r[1,3] = bone.offset[1] + (self.key?("Yposition") ? self['Yposition'] : 0)
|
140
|
+
r[2,3] = bone.offset[2] + (self.key?("Zposition") ? self['Zposition'] : 0)
|
141
|
+
r
|
142
|
+
end
|
143
|
+
|
144
|
+
alias local_transform_matrix relative_transform_matrix
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|