lib/cyberarm_engine/transform.rb in cyberarm_engine-0.12.1 vs lib/cyberarm_engine/transform.rb in cyberarm_engine-0.13.0

- old
+ new

@@ -1,84 +1,273 @@ -module CyberarmEngine - class Transform - attr_reader :elements - def initialize(matrix) - @elements = matrix - - raise "Transform is wrong size! Got #{@elements.size}, expected 16" if 16 != @elements.size - end - - def self.rotate(angle, rotate_around = nil) - double c = Math.cos(angle).degrees_to_radians - double s = Math.sin(angle).degrees_to_radians - matrix = [ - +c, +s, 0, 0, - -s, +c, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1, - ] - - rotate_matrix = Transform.new(matrix, rows: 4, columns: 4) - - if rotate_around && (rotate_around.x != 0.0 || rotate_around.y != 0.0) - negative_rotate_around = Vector.new(-rotate_around.x, -rotate_around.y, -rotate_around.z) - - rotate_matrix = concat( - concat(translate(negative_rotate_around), rotate_matrix), - translate(rotate_around) - ) - end - - return rotate_matrix - end - - def self.translate(vector) - x, y, z = vector.to_a[0..2] - matrix = [ - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - x, y, z, 1, - ] - - Transform.new(matrix) - end - - def self.scale(vector, center_around = nil) - scale_x, scale_y, scale_z = vector.to_a[0..2] - matrix = [ - scale_x, 0, 0, 0, - 0, scale_y, 0, 0, - 0, 0, scale_z, 0, - 0, 0, 0, 1, - ] - - scale_matrix = Transform.new(matrix) - - if center_around && (center_around.x != 0.0 || center_around.y != 0.0) - negative_center_around = Vector.new(-center_around.x, -center_around.y, -center_around.z) - - scale_matrix = concat( - concat(translate(negative_center_around), scale_matrix), - translate(center_around) - ) - end - - return scale_matrix - end - - def self.concat(left, right) - matrix = Array.new(left.elements.size) - rows = 4 - - matrix.size.times do |i| - matrix[i] = 0 - - rows.times do |j| - matrix[i] += left.elements[i / rows * rows + j] * right.elements[i % rows + j * rows] - end - end - - Transform.new(matrix) - end - end +module CyberarmEngine + # Basic 4x4 matrix operations + class Transform + attr_reader :elements + def initialize(matrix) + @elements = matrix + + raise "Transform is wrong size! Got #{@elements.size}, expected 16" if 16 != @elements.size + raise "Invalid value for matrix, must all be numeric!" if @elements.any? { |e| e.nil? || !e.is_a?(Numeric)} + end + + def self.identity + Transform.new( + [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ] + ) + end + + ### 2D Operations meant for interacting with Gosu ### + + # 2d rotate operation, replicates Gosu's Gosu.rotate function + def self.rotate(angle, rotate_around = nil) + double c = Math.cos(angle).degrees_to_radians + double s = Math.sin(angle).degrees_to_radians + matrix = [ + +c, +s, 0, 0, + -s, +c, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1, + ] + + rotate_matrix = Transform.new(matrix, rows: 4, columns: 4) + + if rotate_around && (rotate_around.x != 0.0 || rotate_around.y != 0.0) + negative_rotate_around = Vector.new(-rotate_around.x, -rotate_around.y, -rotate_around.z) + + rotate_matrix = concat( + concat(translate(negative_rotate_around), rotate_matrix), + translate(rotate_around) + ) + end + + return rotate_matrix + end + + # 2d translate operation, replicates Gosu's Gosu.translate function + def self.translate(vector) + x, y, z = vector.to_a[0..2] + matrix = [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + x, y, z, 1, + ] + + Transform.new(matrix) + end + + # 2d scale operation, replicates Gosu's Gosu.rotate function + def self.scale(vector, center_around = nil) + scale_x, scale_y, scale_z = vector.to_a[0..2] + matrix = [ + scale_x, 0, 0, 0, + 0, scale_y, 0, 0, + 0, 0, scale_z, 0, + 0, 0, 0, 1, + ] + + scale_matrix = Transform.new(matrix) + + if center_around && (center_around.x != 0.0 || center_around.y != 0.0) + negative_center_around = Vector.new(-center_around.x, -center_around.y, -center_around.z) + + scale_matrix = concat( + concat(translate(negative_center_around), scale_matrix), + translate(center_around) + ) + end + + return scale_matrix + end + + def self.concat(left, right) + matrix = Array.new(left.elements.size) + rows = 4 + + matrix.size.times do |i| + matrix[i] = 0 + + rows.times do |j| + matrix[i] += left.elements[i / rows * rows + j] * right.elements[i % rows + j * rows] + end + end + + Transform.new(matrix) + end + + #### 3D Operations meant for OpenGL ### + + def self.translate_3d(vector) + x, y, z = vector.to_a[0..2] + matrix = [ + 1, 0, 0, x, + 0, 1, 0, y, + 0, 0, 1, z, + 0, 0, 0, 1, + ] + + Transform.new(matrix) + end + + def self.rotate_3d(vector, order = "zyx") + x, y, z = vector.to_a[0..2].map { |axis| axis * Math::PI / 180.0 } + + rotation_x = Transform.new( + [ + 1, 0, 0, 0, + 0, Math.cos(x), -Math.sin(x), 0, + 0, Math.sin(x), Math.cos(x), 0, + 0, 0, 0, 1 + ] + ) + + rotation_y = Transform.new( + [ + Math.cos(y), 0, Math.sin(y), 0, + 0, 1, 0, 0, + -Math.sin(y), 0, Math.cos(y), 0, + 0, 0, 0, 1 + ] + ) + + rotation_z = Transform.new( + [ + Math.cos(z), -Math.sin(z), 0, 0, + Math.sin(z), Math.cos(z), 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ] + ) + + rotation_z * rotation_y * rotation_x + end + + # Implements glRotatef + # https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/glRotate.xml + def self.rotate_gl(angle, axis) + radians = angle * Math::PI / 180.0 + s = Math.sin(radians) + c = Math.cos(radians) + + axis = axis.normalized + x, y, z = axis.to_a[0..2] + + n = (1.0 - c) + + Transform.new( + [ + x * x * n + c, x * y * n - z * s, x * z * n + y * s, 0, + y * x * n + z * s, y * y * n + c, y * z * n - x * s, 0, + x * z * n - y * s, y * z * n + x * s, z * z * n + c, 0, + 0, 0, 0, 1.0 + ] + ) + end + + def self.scale_3d(vector) + x, y, z = vector.to_a[0..2] + + Transform.new( + [ + x, 0, 0, 0, + 0, y, 0, 0, + 0, 0, z, 0, + 0, 0, 0, 1 + ] + ) + end + + def self.perspective(fov_y, aspect_ratio, near, far) + f = 1.0 / Math.tan(fov_y.degrees_to_radians / 2.0) # cotangent + zn = (far + near.to_f) / (near - far.to_f) + zf = (2.0 * far * near.to_f) / (near - far.to_f) + + Transform.new( + [ + f / aspect_ratio, 0.0, 0.0, 0.0, + 0.0, f, 0.0, 0.0, + 0.0, 0.0, zn, zf, + 0.0, 0.0, -1.0, 0.0 + ] + ) + end + + def self.view(eye, orientation) + # https://www.3dgep.com/understanding-the-view-matrix/#The_View_Matrix + cosPitch = Math.cos(orientation.z * Math::PI / 180.0) + sinPitch = Math.sin(orientation.z * Math::PI / 180.0) + cosYaw = Math.cos(orientation.y * Math::PI / 180.0) + sinYaw = Math.sin(orientation.y * Math::PI / 180.0) + + x_axis = Vector.new(cosYaw, 0, -sinYaw) + y_axis = Vector.new(sinYaw * sinPitch, cosPitch, cosYaw * sinPitch) + z_axis = Vector.new(sinYaw * cosPitch, -sinPitch, cosPitch * cosYaw) + + Transform.new( + [ + x_axis.x, y_axis.y, z_axis.z, 0, + x_axis.x, y_axis.y, z_axis.z, 0, + x_axis.x, y_axis.y, z_axis.z, 0, + -x_axis.dot(eye), -y_axis.dot(eye), -z_axis.dot(eye), 1 + ] + ) + end + + def *(other) + + case other + when CyberarmEngine::Vector + matrix = @elements.clone + list = other.to_a + + @elements.each_with_index do |e, i| + matrix[i] = e + list[i % 4] + end + + Transform.new(matrix) + + when CyberarmEngine::Transform + return multiply_matrices(other) + else + p other.class + raise TypeError, "Expected CyberarmEngine::Vector or CyberarmEngine::Transform got #{other.class}" + end + end + + def get(x, y) + width = 4 + + # puts "Transform|#{self.object_id} -> #{@elements[width * y + x].inspect} (index: #{width * y + x})" + @elements[width * y + x] + end + + def multiply_matrices(other) + matrix = Array.new(16, 0) + + 4.times do |x| + 4.times do |y| + 4.times do |k| + matrix[4 * y + x] += get(x, k) * other.get(k, y) + end + end + end + + return Transform.new(matrix) + end + + # arranges Matrix in column major form + def to_gl + e = @elements + [ + e[0], e[4], e[8], e[12], + e[1], e[5], e[9], e[13], + e[2], e[6], e[10], e[14], + e[3], e[7], e[11], e[15] + ] + end + end end \ No newline at end of file