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.
- checksums.yaml +7 -0
- data/.rspec +2 -0
- data/3rb.gemspec +29 -0
- data/CHANGELOG.md +12 -0
- data/LICENSE +21 -0
- data/README.md +321 -0
- data/Rakefile +13 -0
- data/examples/01_hello_cube.rb +29 -0
- data/examples/02_basic_geometries.rb +56 -0
- data/examples/03_materials.rb +61 -0
- data/examples/04_lighting.rb +63 -0
- data/examples/05_animation.rb +79 -0
- data/examples/06_custom_shader.rb +92 -0
- data/examples/07_scene_graph.rb +74 -0
- data/examples/08_orbit_controls.rb +50 -0
- data/examples/09_3d_chart.rb +71 -0
- data/examples/10_procedural_terrain.rb +140 -0
- data/examples/11_particle_system.rb +68 -0
- data/examples/12_model_loader.rb +73 -0
- data/examples/13_game_prototype.rb +145 -0
- data/examples/14_utah_teapot.rb +291 -0
- data/examples/15_stanford_bunny.rb +200 -0
- data/examples/16_cornell_box.rb +373 -0
- data/examples/17_weird_fractal4.rb +130 -0
- data/examples/18_platonic_solids.rb +268 -0
- data/lib/3rb/animation/animation_clip.rb +287 -0
- data/lib/3rb/animation/animation_mixer.rb +366 -0
- data/lib/3rb/cameras/camera.rb +50 -0
- data/lib/3rb/cameras/orthographic_camera.rb +92 -0
- data/lib/3rb/cameras/perspective_camera.rb +103 -0
- data/lib/3rb/controls/orbit_controls.rb +341 -0
- data/lib/3rb/core/buffer_attribute.rb +172 -0
- data/lib/3rb/core/group.rb +9 -0
- data/lib/3rb/core/object3d.rb +298 -0
- data/lib/3rb/core/scene.rb +78 -0
- data/lib/3rb/dsl/helpers.rb +57 -0
- data/lib/3rb/dsl/scene_builder.rb +288 -0
- data/lib/3rb/ffi/glfw.rb +61 -0
- data/lib/3rb/ffi/opengl.rb +137 -0
- data/lib/3rb/ffi/platform.rb +65 -0
- data/lib/3rb/geometries/box_geometry.rb +101 -0
- data/lib/3rb/geometries/buffer_geometry.rb +345 -0
- data/lib/3rb/geometries/cone_geometry.rb +29 -0
- data/lib/3rb/geometries/cylinder_geometry.rb +149 -0
- data/lib/3rb/geometries/plane_geometry.rb +75 -0
- data/lib/3rb/geometries/sphere_geometry.rb +93 -0
- data/lib/3rb/geometries/torus_geometry.rb +77 -0
- data/lib/3rb/lights/ambient_light.rb +9 -0
- data/lib/3rb/lights/directional_light.rb +57 -0
- data/lib/3rb/lights/hemisphere_light.rb +26 -0
- data/lib/3rb/lights/light.rb +27 -0
- data/lib/3rb/lights/point_light.rb +68 -0
- data/lib/3rb/lights/rect_area_light.rb +35 -0
- data/lib/3rb/lights/spot_light.rb +88 -0
- data/lib/3rb/loaders/gltf_loader.rb +304 -0
- data/lib/3rb/loaders/loader.rb +94 -0
- data/lib/3rb/loaders/obj_loader.rb +186 -0
- data/lib/3rb/loaders/texture_loader.rb +55 -0
- data/lib/3rb/materials/basic_material.rb +70 -0
- data/lib/3rb/materials/lambert_material.rb +102 -0
- data/lib/3rb/materials/material.rb +114 -0
- data/lib/3rb/materials/phong_material.rb +106 -0
- data/lib/3rb/materials/shader_material.rb +104 -0
- data/lib/3rb/materials/standard_material.rb +106 -0
- data/lib/3rb/math/color.rb +246 -0
- data/lib/3rb/math/euler.rb +156 -0
- data/lib/3rb/math/math_utils.rb +132 -0
- data/lib/3rb/math/matrix3.rb +269 -0
- data/lib/3rb/math/matrix4.rb +501 -0
- data/lib/3rb/math/quaternion.rb +337 -0
- data/lib/3rb/math/vector2.rb +216 -0
- data/lib/3rb/math/vector3.rb +366 -0
- data/lib/3rb/math/vector4.rb +233 -0
- data/lib/3rb/native/gl.rb +382 -0
- data/lib/3rb/native/native.rb +55 -0
- data/lib/3rb/native/window.rb +111 -0
- data/lib/3rb/native.rb +9 -0
- data/lib/3rb/objects/line.rb +116 -0
- data/lib/3rb/objects/mesh.rb +40 -0
- data/lib/3rb/objects/points.rb +71 -0
- data/lib/3rb/renderers/opengl_renderer.rb +567 -0
- data/lib/3rb/renderers/renderer.rb +60 -0
- data/lib/3rb/renderers/shader_lib.rb +100 -0
- data/lib/3rb/textures/cube_texture.rb +26 -0
- data/lib/3rb/textures/data_texture.rb +35 -0
- data/lib/3rb/textures/render_target.rb +125 -0
- data/lib/3rb/textures/texture.rb +190 -0
- data/lib/3rb/version.rb +5 -0
- data/lib/3rb.rb +86 -0
- data/shaders/basic.frag +19 -0
- data/shaders/basic.vert +15 -0
- data/shaders/common/lights.glsl +53 -0
- data/shaders/common/uniforms.glsl +9 -0
- data/shaders/lambert.frag +37 -0
- data/shaders/lambert.vert +22 -0
- data/shaders/phong.frag +51 -0
- data/shaders/phong.vert +28 -0
- data/shaders/standard.frag +92 -0
- data/shaders/standard.vert +28 -0
- 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
|