3rb 0.1.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.
Files changed (100) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +2 -0
  3. data/3rb.gemspec +29 -0
  4. data/CHANGELOG.md +12 -0
  5. data/LICENSE +21 -0
  6. data/README.md +321 -0
  7. data/Rakefile +13 -0
  8. data/examples/01_hello_cube.rb +29 -0
  9. data/examples/02_basic_geometries.rb +56 -0
  10. data/examples/03_materials.rb +61 -0
  11. data/examples/04_lighting.rb +63 -0
  12. data/examples/05_animation.rb +79 -0
  13. data/examples/06_custom_shader.rb +92 -0
  14. data/examples/07_scene_graph.rb +74 -0
  15. data/examples/08_orbit_controls.rb +50 -0
  16. data/examples/09_3d_chart.rb +71 -0
  17. data/examples/10_procedural_terrain.rb +140 -0
  18. data/examples/11_particle_system.rb +68 -0
  19. data/examples/12_model_loader.rb +73 -0
  20. data/examples/13_game_prototype.rb +145 -0
  21. data/examples/14_utah_teapot.rb +291 -0
  22. data/examples/15_stanford_bunny.rb +200 -0
  23. data/examples/16_cornell_box.rb +373 -0
  24. data/examples/17_weird_fractal4.rb +130 -0
  25. data/examples/18_platonic_solids.rb +268 -0
  26. data/lib/3rb/animation/animation_clip.rb +287 -0
  27. data/lib/3rb/animation/animation_mixer.rb +366 -0
  28. data/lib/3rb/cameras/camera.rb +50 -0
  29. data/lib/3rb/cameras/orthographic_camera.rb +92 -0
  30. data/lib/3rb/cameras/perspective_camera.rb +103 -0
  31. data/lib/3rb/controls/orbit_controls.rb +341 -0
  32. data/lib/3rb/core/buffer_attribute.rb +172 -0
  33. data/lib/3rb/core/group.rb +9 -0
  34. data/lib/3rb/core/object3d.rb +298 -0
  35. data/lib/3rb/core/scene.rb +78 -0
  36. data/lib/3rb/dsl/helpers.rb +57 -0
  37. data/lib/3rb/dsl/scene_builder.rb +288 -0
  38. data/lib/3rb/ffi/glfw.rb +61 -0
  39. data/lib/3rb/ffi/opengl.rb +137 -0
  40. data/lib/3rb/ffi/platform.rb +65 -0
  41. data/lib/3rb/geometries/box_geometry.rb +101 -0
  42. data/lib/3rb/geometries/buffer_geometry.rb +345 -0
  43. data/lib/3rb/geometries/cone_geometry.rb +29 -0
  44. data/lib/3rb/geometries/cylinder_geometry.rb +149 -0
  45. data/lib/3rb/geometries/plane_geometry.rb +75 -0
  46. data/lib/3rb/geometries/sphere_geometry.rb +93 -0
  47. data/lib/3rb/geometries/torus_geometry.rb +77 -0
  48. data/lib/3rb/lights/ambient_light.rb +9 -0
  49. data/lib/3rb/lights/directional_light.rb +57 -0
  50. data/lib/3rb/lights/hemisphere_light.rb +26 -0
  51. data/lib/3rb/lights/light.rb +27 -0
  52. data/lib/3rb/lights/point_light.rb +68 -0
  53. data/lib/3rb/lights/rect_area_light.rb +35 -0
  54. data/lib/3rb/lights/spot_light.rb +88 -0
  55. data/lib/3rb/loaders/gltf_loader.rb +304 -0
  56. data/lib/3rb/loaders/loader.rb +94 -0
  57. data/lib/3rb/loaders/obj_loader.rb +186 -0
  58. data/lib/3rb/loaders/texture_loader.rb +55 -0
  59. data/lib/3rb/materials/basic_material.rb +70 -0
  60. data/lib/3rb/materials/lambert_material.rb +102 -0
  61. data/lib/3rb/materials/material.rb +114 -0
  62. data/lib/3rb/materials/phong_material.rb +106 -0
  63. data/lib/3rb/materials/shader_material.rb +104 -0
  64. data/lib/3rb/materials/standard_material.rb +106 -0
  65. data/lib/3rb/math/color.rb +246 -0
  66. data/lib/3rb/math/euler.rb +156 -0
  67. data/lib/3rb/math/math_utils.rb +132 -0
  68. data/lib/3rb/math/matrix3.rb +269 -0
  69. data/lib/3rb/math/matrix4.rb +501 -0
  70. data/lib/3rb/math/quaternion.rb +337 -0
  71. data/lib/3rb/math/vector2.rb +216 -0
  72. data/lib/3rb/math/vector3.rb +366 -0
  73. data/lib/3rb/math/vector4.rb +233 -0
  74. data/lib/3rb/native/gl.rb +382 -0
  75. data/lib/3rb/native/native.rb +55 -0
  76. data/lib/3rb/native/window.rb +111 -0
  77. data/lib/3rb/native.rb +9 -0
  78. data/lib/3rb/objects/line.rb +116 -0
  79. data/lib/3rb/objects/mesh.rb +40 -0
  80. data/lib/3rb/objects/points.rb +71 -0
  81. data/lib/3rb/renderers/opengl_renderer.rb +567 -0
  82. data/lib/3rb/renderers/renderer.rb +60 -0
  83. data/lib/3rb/renderers/shader_lib.rb +100 -0
  84. data/lib/3rb/textures/cube_texture.rb +26 -0
  85. data/lib/3rb/textures/data_texture.rb +35 -0
  86. data/lib/3rb/textures/render_target.rb +125 -0
  87. data/lib/3rb/textures/texture.rb +190 -0
  88. data/lib/3rb/version.rb +5 -0
  89. data/lib/3rb.rb +86 -0
  90. data/shaders/basic.frag +19 -0
  91. data/shaders/basic.vert +15 -0
  92. data/shaders/common/lights.glsl +53 -0
  93. data/shaders/common/uniforms.glsl +9 -0
  94. data/shaders/lambert.frag +37 -0
  95. data/shaders/lambert.vert +22 -0
  96. data/shaders/phong.frag +51 -0
  97. data/shaders/phong.vert +28 -0
  98. data/shaders/standard.frag +92 -0
  99. data/shaders/standard.vert +28 -0
  100. metadata +155 -0
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Platonic Solids - SDF Raymarching
5
+ # Based on https://www.shadertoy.com/view/tclXzr
6
+
7
+ require_relative "../lib/3rb"
8
+
9
+ VERTEX_SHADER = <<~GLSL
10
+ #version 330 core
11
+
12
+ layout(location = 0) in vec3 position;
13
+ layout(location = 2) in vec2 uv;
14
+
15
+ out vec2 vUv;
16
+
17
+ void main() {
18
+ vUv = uv;
19
+ gl_Position = vec4(position.xy * 2.0, 0.0, 1.0);
20
+ }
21
+ GLSL
22
+
23
+ FRAGMENT_SHADER = <<~GLSL
24
+ #version 330 core
25
+
26
+ in vec2 vUv;
27
+
28
+ uniform float iTime;
29
+ uniform vec2 iResolution;
30
+
31
+ out vec4 fragColor;
32
+
33
+ #define TETRAHEDRON 0
34
+ #define CUBE 1
35
+ #define OCTAHEDRON 2
36
+ #define DODECAHEDRON 3
37
+ #define ICOSAHEDRON 4
38
+
39
+ #define PI 3.14159265359
40
+ #define PHI 1.618033988749895
41
+
42
+ #define MAX_STEPS 100
43
+ #define MAX_DIST 100.0
44
+ #define SURF_DIST 0.001
45
+ #define ROTATION_SPEED 0.5
46
+
47
+ vec3 rotateX(vec3 p, float a) {
48
+ float c = cos(a), s = sin(a);
49
+ return vec3(p.x, p.y * c - p.z * s, p.y * s + p.z * c);
50
+ }
51
+
52
+ vec3 rotateY(vec3 p, float a) {
53
+ float c = cos(a), s = sin(a);
54
+ return vec3(p.x * c - p.z * s, p.y, p.x * s + p.z * c);
55
+ }
56
+
57
+ vec3 rotateZ(vec3 p, float a) {
58
+ float c = cos(a), s = sin(a);
59
+ return vec3(p.x * c - p.y * s, p.x * s + p.y * c, p.z);
60
+ }
61
+
62
+ float sdTetrahedron(vec3 p, float r) {
63
+ float k = sqrt(2.0);
64
+ p.xy = abs(p.xy);
65
+ p.xy = (p.y > p.x) ? p.yx : p.xy;
66
+ p.xz = (p.z > p.x) ? p.zx : p.xz;
67
+ p.xy = (p.y > p.x) ? p.yx : p.xy;
68
+ float d = p.z - r;
69
+ return d * 0.57735027;
70
+ }
71
+
72
+ float sdBox(vec3 p, vec3 b) {
73
+ vec3 d = abs(p) - b;
74
+ return length(max(d, 0.0)) + min(max(d.x, max(d.y, d.z)), 0.0);
75
+ }
76
+
77
+ float sdOctahedron(vec3 p, float r) {
78
+ return (abs(p.x) + abs(p.y) + abs(p.z) - r) * 0.57735027;
79
+ }
80
+
81
+ float sdDodecahedron(vec3 p, float r) {
82
+ const vec3 n = normalize(vec3(PHI, 1.0, 0.0));
83
+
84
+ float d = 0.0;
85
+ d = max(d, abs(dot(p, vec3(n.x, n.y, n.z))));
86
+ d = max(d, abs(dot(p, vec3(n.x, n.y, -n.z))));
87
+ d = max(d, abs(dot(p, vec3(n.x, -n.y, n.z))));
88
+ d = max(d, abs(dot(p, vec3(n.x, -n.y, -n.z))));
89
+ d = max(d, abs(dot(p, vec3(n.z, n.x, n.y))));
90
+ d = max(d, abs(dot(p, vec3(n.z, n.x, -n.y))));
91
+ d = max(d, abs(dot(p, vec3(n.z, -n.x, n.y))));
92
+ d = max(d, abs(dot(p, vec3(n.z, -n.x, -n.y))));
93
+ d = max(d, abs(dot(p, vec3(n.y, n.z, n.x))));
94
+ d = max(d, abs(dot(p, vec3(n.y, n.z, -n.x))));
95
+ d = max(d, abs(dot(p, vec3(n.y, -n.z, n.x))));
96
+ d = max(d, abs(dot(p, vec3(n.y, -n.z, -n.x))));
97
+
98
+ return (d - r) * 0.5;
99
+ }
100
+
101
+ float sdIcosahedron(vec3 p, float r) {
102
+ const float q = sqrt(5.0);
103
+ const vec3 n1 = normalize(vec3(q, 2.0, 0));
104
+ const vec3 n2 = normalize(vec3(q, 0, 2.0));
105
+ const vec3 n3 = normalize(vec3(0, 2.0, q));
106
+ const vec3 n4 = normalize(vec3(0, q, 2.0));
107
+ const vec3 n5 = normalize(vec3(2.0, 0, q));
108
+ const vec3 n6 = normalize(vec3(2.0, q, 0));
109
+
110
+ float d;
111
+ d = max(abs(dot(p, n1.xyz)), max(abs(dot(p, n1.zxy)), abs(dot(p, n1.yzx))));
112
+ d = max(d, max(abs(dot(p, n2.xyz)), max(abs(dot(p, n2.zxy)), abs(dot(p, n2.yzx)))));
113
+ d = max(d, max(abs(dot(p, n3.xyz)), max(abs(dot(p, n3.zxy)), abs(dot(p, n3.yzx)))));
114
+ d = max(d, max(abs(dot(p, n4.xyz)), max(abs(dot(p, n4.zxy)), abs(dot(p, n4.yzx)))));
115
+ d = max(d, max(abs(dot(p, n5.xyz)), max(abs(dot(p, n5.zxy)), abs(dot(p, n5.yzx)))));
116
+ d = max(d, max(abs(dot(p, n6.xyz)), max(abs(dot(p, n6.zxy)), abs(dot(p, n6.yzx)))));
117
+
118
+ return (d - r) * 0.4;
119
+ }
120
+
121
+ float getPlatonicSolid(vec3 p, int type, float size) {
122
+ if (type == TETRAHEDRON) return sdTetrahedron(p, size);
123
+ if (type == CUBE) return sdBox(p, vec3(size));
124
+ if (type == OCTAHEDRON) return sdOctahedron(p, size);
125
+ if (type == DODECAHEDRON) return sdDodecahedron(p, size);
126
+ if (type == ICOSAHEDRON) return sdIcosahedron(p, size);
127
+ return MAX_DIST;
128
+ }
129
+
130
+ float getSDF(vec3 p) {
131
+ float t = iTime * ROTATION_SPEED;
132
+
133
+ p = rotateY(p, t * 0.3);
134
+ p = rotateX(p, t * 0.2);
135
+
136
+ int solidType = int(mod(floor(t * 0.2), 5.0));
137
+
138
+ float solidDist = getPlatonicSolid(p, solidType, 1.0);
139
+
140
+ float ripple = sin(20.0 * p.x + t) * sin(20.0 * p.y + t) * sin(20.0 * p.z + t) * 0.05;
141
+ solidDist += ripple;
142
+
143
+ return solidDist;
144
+ }
145
+
146
+ float rayMarch(vec3 ro, vec3 rd) {
147
+ float dO = 0.0;
148
+
149
+ for (int i = 0; i < MAX_STEPS; i++) {
150
+ vec3 p = ro + rd * dO;
151
+ float dS = getSDF(p);
152
+ dO += dS;
153
+ if (dS < SURF_DIST || dO > MAX_DIST) break;
154
+ }
155
+
156
+ return dO;
157
+ }
158
+
159
+ vec3 getNormal(vec3 p) {
160
+ float d = getSDF(p);
161
+ vec2 e = vec2(0.001, 0);
162
+
163
+ vec3 n = d - vec3(
164
+ getSDF(p - e.xyy),
165
+ getSDF(p - e.yxy),
166
+ getSDF(p - e.yyx)
167
+ );
168
+
169
+ return normalize(n);
170
+ }
171
+
172
+ vec3 getColor(vec3 p, vec3 n, float t) {
173
+ vec3 baseColor = 0.5 + 0.5 * sin(vec3(0.0, 0.33, 0.67) * PI * 2.0 + t * 0.5);
174
+
175
+ vec3 color = baseColor + 0.5 * sin(t + p.zxy) * 0.5;
176
+
177
+ color += 0.5 * n;
178
+
179
+ float pattern = sin(p.x * 20.0) * sin(p.y * 20.0) * sin(p.z * 20.0);
180
+ color += pattern * 0.1;
181
+
182
+ return clamp(color, 0.0, 1.0);
183
+ }
184
+
185
+ void main() {
186
+ vec2 fragCoord = vUv * iResolution;
187
+ vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
188
+
189
+ vec3 ro = vec3(0, 0, -4);
190
+ vec3 rd = normalize(vec3(uv, 1.0));
191
+
192
+ float camTime = iTime * 0.3;
193
+ ro = rotateY(ro, camTime * 0.5);
194
+ rd = rotateY(rd, camTime * 0.5);
195
+
196
+ float d = rayMarch(ro, rd);
197
+
198
+ vec3 col = vec3(0.1, 0.1, 0.2) - max(rd.y, 0.0) * 0.2;
199
+
200
+ if (d < MAX_DIST) {
201
+ vec3 p = ro + rd * d;
202
+ vec3 n = getNormal(p);
203
+
204
+ col = getColor(p, n, iTime);
205
+
206
+ vec3 lightDir = normalize(vec3(1, 2, -3));
207
+ float diff = max(dot(n, lightDir), 0.1);
208
+ col *= diff;
209
+
210
+ vec3 h = normalize(lightDir - rd);
211
+ float spec = pow(max(dot(n, h), 0.0), 32.0);
212
+ col += spec * 0.5;
213
+
214
+ float glow = 1.0 - clamp(d / 10.0, 0.0, 1.0);
215
+ col += vec3(0.1, 0.3, 0.6) * glow * glow;
216
+
217
+ col = mix(col, vec3(0.1, 0.1, 0.2), 1.0 - exp(-d * 0.05));
218
+ }
219
+
220
+ vec2 vigUV = fragCoord / iResolution.xy;
221
+ vigUV = (vigUV - 0.5) * 2.0;
222
+ float vignette = 1.0 - dot(vigUV, vigUV) * 0.3;
223
+ col *= vignette;
224
+
225
+ float grain = fract(sin(dot(fragCoord, vec2(12.9898, 78.233)) + iTime) * 43758.5453);
226
+ col += (grain - 0.5) * 0.05;
227
+
228
+ col = pow(col, vec3(0.4545));
229
+
230
+ fragColor = vec4(col, 1.0);
231
+ }
232
+ GLSL
233
+
234
+ scene = Sunrb::Scene.new
235
+ scene.background = Sunrb::Color.new(0x000000)
236
+
237
+ camera = Sunrb::OrthographicCamera.new(
238
+ left: -1, right: 1, top: 1, bottom: -1, near: 0.1, far: 10
239
+ )
240
+ camera.position.z = 1
241
+
242
+ geometry = Sunrb::PlaneGeometry.new(width: 2, height: 2)
243
+
244
+ shader_material = Sunrb::ShaderMaterial.new(
245
+ vertex_shader: VERTEX_SHADER,
246
+ fragment_shader: FRAGMENT_SHADER,
247
+ uniforms: {
248
+ iTime: 0.0,
249
+ iResolution: Sunrb::Vector2.new(800, 600)
250
+ }
251
+ )
252
+
253
+ quad = Sunrb::Mesh.new(geometry, shader_material)
254
+ scene.add(quad)
255
+
256
+ puts "=== Platonic Solids - SDF Raymarching ==="
257
+ puts "Tetrahedron -> Cube -> Octahedron -> Dodecahedron -> Icosahedron"
258
+ puts "Press ESC to exit"
259
+
260
+ renderer = Sunrb::OpenGLRenderer.new(width: 800, height: 600, title: "Platonic Solids - SDF Raymarching")
261
+
262
+ time = 0.0
263
+
264
+ renderer.run do |delta|
265
+ time += delta
266
+ shader_material.uniforms[:iTime] = time
267
+ renderer.render(scene, camera)
268
+ end
@@ -0,0 +1,287 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sunrb
4
+ class AnimationClip
5
+ attr_accessor :uuid, :name, :tracks, :duration, :blend_mode
6
+
7
+ BLEND_MODE = {
8
+ normal: 2500,
9
+ additive: 2501
10
+ }.freeze
11
+
12
+ def initialize(name: "", duration: -1, tracks: [], blend_mode: :normal)
13
+ @uuid = SecureRandom.uuid
14
+ @name = name
15
+ @tracks = tracks
16
+ @duration = duration
17
+ @blend_mode = blend_mode
18
+
19
+ reset_duration if @duration < 0
20
+ end
21
+
22
+ def reset_duration
23
+ @duration = 0
24
+
25
+ @tracks.each do |track|
26
+ @duration = [@duration, track.times.last || 0].max
27
+ end
28
+
29
+ self
30
+ end
31
+
32
+ def trim
33
+ @tracks.each(&:trim)
34
+ self
35
+ end
36
+
37
+ def validate
38
+ valid = true
39
+
40
+ @tracks.each do |track|
41
+ valid = false unless track.validate
42
+ end
43
+
44
+ valid
45
+ end
46
+
47
+ def optimize
48
+ @tracks.each(&:optimize)
49
+ self
50
+ end
51
+
52
+ def clone
53
+ tracks = @tracks.map(&:clone)
54
+ AnimationClip.new(
55
+ name: @name,
56
+ duration: @duration,
57
+ tracks: tracks,
58
+ blend_mode: @blend_mode
59
+ )
60
+ end
61
+
62
+ def to_h
63
+ {
64
+ uuid: @uuid,
65
+ name: @name,
66
+ duration: @duration,
67
+ blend_mode: @blend_mode,
68
+ tracks: @tracks.map(&:to_h)
69
+ }
70
+ end
71
+
72
+ def self.from_h(data)
73
+ tracks = (data[:tracks] || []).map { |t| KeyframeTrack.from_h(t) }
74
+ new(
75
+ name: data[:name],
76
+ duration: data[:duration],
77
+ tracks: tracks,
78
+ blend_mode: data[:blend_mode] || :normal
79
+ )
80
+ end
81
+ end
82
+
83
+ class KeyframeTrack
84
+ attr_accessor :name, :times, :values, :interpolation
85
+
86
+ INTERPOLATION = {
87
+ discrete: 2300,
88
+ linear: 2301,
89
+ smooth: 2302
90
+ }.freeze
91
+
92
+ def initialize(name:, times:, values:, interpolation: :linear)
93
+ @name = name
94
+ @times = times.map(&:to_f)
95
+ @values = values.map(&:to_f)
96
+ @interpolation = interpolation
97
+ end
98
+
99
+ def value_size
100
+ @values.length / @times.length
101
+ end
102
+
103
+ def get_value_at_time(time)
104
+ times = @times
105
+
106
+ return get_value_at_index(0) if time <= times.first
107
+ return get_value_at_index(times.length - 1) if time >= times.last
108
+
109
+ idx = binary_search(times, time)
110
+
111
+ if idx >= 0
112
+ get_value_at_index(idx)
113
+ else
114
+ insert_idx = -idx - 1
115
+ interpolate(insert_idx - 1, insert_idx, time)
116
+ end
117
+ end
118
+
119
+ def get_value_at_index(index)
120
+ size = value_size
121
+ offset = index * size
122
+ @values[offset, size]
123
+ end
124
+
125
+ def interpolate(idx1, idx2, time)
126
+ t1 = @times[idx1]
127
+ t2 = @times[idx2]
128
+ alpha = (time - t1) / (t2 - t1)
129
+
130
+ size = value_size
131
+ v1 = get_value_at_index(idx1)
132
+ v2 = get_value_at_index(idx2)
133
+
134
+ case @interpolation
135
+ when :discrete
136
+ v1
137
+ when :linear
138
+ v1.zip(v2).map { |a, b| a + (b - a) * alpha }
139
+ when :smooth
140
+ t = alpha * alpha * (3 - 2 * alpha)
141
+ v1.zip(v2).map { |a, b| a + (b - a) * t }
142
+ else
143
+ v1.zip(v2).map { |a, b| a + (b - a) * alpha }
144
+ end
145
+ end
146
+
147
+ def trim
148
+ return self if @times.empty?
149
+
150
+ min_time = @times.first
151
+ return self if min_time == 0
152
+
153
+ @times = @times.map { |t| t - min_time }
154
+ self
155
+ end
156
+
157
+ def validate
158
+ return false if @times.empty? || @values.empty?
159
+ return false if @values.length % @times.length != 0
160
+
161
+ @times.each_cons(2).all? { |a, b| a <= b }
162
+ end
163
+
164
+ def optimize
165
+ # Remove keyframes with duplicate values
166
+ return self if @times.length < 2
167
+
168
+ size = value_size
169
+ new_times = [@times.first]
170
+ new_values = @values[0, size]
171
+
172
+ (1...@times.length).each do |i|
173
+ current = @values[i * size, size]
174
+ previous = @values[(i - 1) * size, size]
175
+
176
+ unless values_equal?(current, previous)
177
+ new_times << @times[i]
178
+ new_values.concat(current)
179
+ end
180
+ end
181
+
182
+ @times = new_times
183
+ @values = new_values
184
+ self
185
+ end
186
+
187
+ def clone
188
+ KeyframeTrack.new(
189
+ name: @name,
190
+ times: @times.dup,
191
+ values: @values.dup,
192
+ interpolation: @interpolation
193
+ )
194
+ end
195
+
196
+ def to_h
197
+ {
198
+ name: @name,
199
+ times: @times,
200
+ values: @values,
201
+ interpolation: @interpolation
202
+ }
203
+ end
204
+
205
+ def self.from_h(data)
206
+ new(
207
+ name: data[:name],
208
+ times: data[:times],
209
+ values: data[:values],
210
+ interpolation: data[:interpolation] || :linear
211
+ )
212
+ end
213
+
214
+ private
215
+
216
+ def binary_search(array, value)
217
+ low = 0
218
+ high = array.length - 1
219
+
220
+ while low <= high
221
+ mid = (low + high) / 2
222
+
223
+ if array[mid] == value
224
+ return mid
225
+ elsif array[mid] < value
226
+ low = mid + 1
227
+ else
228
+ high = mid - 1
229
+ end
230
+ end
231
+
232
+ -(low + 1)
233
+ end
234
+
235
+ def values_equal?(a, b)
236
+ return false unless a.length == b.length
237
+
238
+ a.zip(b).all? { |x, y| (x - y).abs < 0.000001 }
239
+ end
240
+ end
241
+
242
+ class VectorKeyframeTrack < KeyframeTrack
243
+ end
244
+
245
+ class QuaternionKeyframeTrack < KeyframeTrack
246
+ def interpolate(idx1, idx2, time)
247
+ t1 = @times[idx1]
248
+ t2 = @times[idx2]
249
+ alpha = (time - t1) / (t2 - t1)
250
+
251
+ q1 = Quaternion.new(*get_value_at_index(idx1))
252
+ q2 = Quaternion.new(*get_value_at_index(idx2))
253
+
254
+ q1.slerp(q2, alpha).to_a
255
+ end
256
+ end
257
+
258
+ class NumberKeyframeTrack < KeyframeTrack
259
+ end
260
+
261
+ class ColorKeyframeTrack < KeyframeTrack
262
+ end
263
+
264
+ class BooleanKeyframeTrack < KeyframeTrack
265
+ def initialize(name:, times:, values:, interpolation: :discrete)
266
+ super(name: name, times: times, values: values.map { |v| v ? 1.0 : 0.0 }, interpolation: interpolation)
267
+ end
268
+ end
269
+
270
+ class StringKeyframeTrack < KeyframeTrack
271
+ def initialize(name:, times:, values:, interpolation: :discrete)
272
+ @name = name
273
+ @times = times.map(&:to_f)
274
+ @string_values = values
275
+ @values = (0...values.length).to_a.map(&:to_f)
276
+ @interpolation = :discrete
277
+ end
278
+
279
+ def get_value_at_index(index)
280
+ @string_values[index]
281
+ end
282
+
283
+ def interpolate(idx1, _idx2, _time)
284
+ @string_values[idx1]
285
+ end
286
+ end
287
+ end