rbgl 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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +7 -0
  3. data/LICENSE +21 -0
  4. data/README.md +123 -0
  5. data/Rakefile +12 -0
  6. data/examples/array_test.rb +99 -0
  7. data/examples/chemical_heartbeat.rb +166 -0
  8. data/examples/color_test.rb +61 -0
  9. data/examples/cube_spinning.rb +87 -0
  10. data/examples/dark_transit.rb +166 -0
  11. data/examples/fractured_orb.rb +428 -0
  12. data/examples/fractured_orb_rb.rb +598 -0
  13. data/examples/gradient.rb +84 -0
  14. data/examples/hexagonal_flow.rb +333 -0
  15. data/examples/multi_return_test.rb +98 -0
  16. data/examples/plasma.rb +78 -0
  17. data/examples/sphere_raymarch.rb +126 -0
  18. data/examples/teapot.rb +362 -0
  19. data/examples/teapot_mcu.rb +344 -0
  20. data/examples/triangle_basic.rb +36 -0
  21. data/examples/triangle_window.rb +62 -0
  22. data/examples/window_test.rb +36 -0
  23. data/lib/rbgl/engine/buffer.rb +160 -0
  24. data/lib/rbgl/engine/context.rb +157 -0
  25. data/lib/rbgl/engine/framebuffer.rb +115 -0
  26. data/lib/rbgl/engine/pipeline.rb +35 -0
  27. data/lib/rbgl/engine/rasterizer.rb +213 -0
  28. data/lib/rbgl/engine/shader.rb +324 -0
  29. data/lib/rbgl/engine/texture.rb +125 -0
  30. data/lib/rbgl/engine.rb +15 -0
  31. data/lib/rbgl/gui/backend.rb +76 -0
  32. data/lib/rbgl/gui/cocoa/backend.rb +121 -0
  33. data/lib/rbgl/gui/event.rb +34 -0
  34. data/lib/rbgl/gui/file_backend.rb +91 -0
  35. data/lib/rbgl/gui/wayland/backend.rb +126 -0
  36. data/lib/rbgl/gui/wayland/connection.rb +331 -0
  37. data/lib/rbgl/gui/window.rb +148 -0
  38. data/lib/rbgl/gui/x11/backend.rb +156 -0
  39. data/lib/rbgl/gui/x11/connection.rb +344 -0
  40. data/lib/rbgl/gui.rb +16 -0
  41. data/lib/rbgl/version.rb +5 -0
  42. data/lib/rbgl.rb +6 -0
  43. metadata +114 -0
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Dark Transit
4
+ # Original: https://www.shadertoy.com/view/WcdczB
5
+ # 28 steps raymarching tunnel
6
+ # License: Creative Commons Attribution-NonCommercial-ShareAlike 3.0
7
+ # https://creativecommons.org/licenses/by-nc-sa/3.0/deed.en
8
+
9
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
10
+
11
+ require "rbgl"
12
+ require "rlsl"
13
+
14
+ WIDTH = 640
15
+ HEIGHT = 480
16
+
17
+ shader = RLSL.define(:dark_transit) do
18
+ uniforms do
19
+ float :time
20
+ end
21
+
22
+ # Register helper function signatures for Ruby mode
23
+ functions do
24
+ define :get_t, returns: :float, params: { time: :float }
25
+ define :path_point, returns: :vec3, params: { z: :float }
26
+ define :noise_a, returns: :float, params: { f: :float, h: :float, k: :float, p: :vec3 }
27
+ define :cross_prod, returns: :vec3, params: { a: :vec3, b: :vec3 }
28
+ define :mat3_mul, returns: :vec3, params: { c0: :vec3, c1: :vec3, c2: :vec3, v: :vec3 }
29
+ end
30
+
31
+ helpers do
32
+ def get_t(time)
33
+ time * 4.0 + 5.0 + 5.0 * sin(time * 0.3)
34
+ end
35
+
36
+ def path_point(z)
37
+ vec3(12.0 * cos(z * 0.1), 12.0 * cos(z * 0.12), z)
38
+ end
39
+
40
+ def noise_a(f, h, k, p)
41
+ abs(h * (sin(f * p.x * k) + sin(f * p.y * k) + sin(f * p.z * k))) / k
42
+ end
43
+
44
+ def cross_prod(a, b)
45
+ vec3(
46
+ a.y * b.z - a.z * b.y,
47
+ a.z * b.x - a.x * b.z,
48
+ a.x * b.y - a.y * b.x
49
+ )
50
+ end
51
+
52
+ def mat3_mul(c0, c1, c2, v)
53
+ vec3(
54
+ c0.x * v.x + c1.x * v.y + c2.x * v.z,
55
+ c0.y * v.x + c1.y * v.y + c2.y * v.z,
56
+ c0.z * v.x + c1.z * v.y + c2.z * v.z
57
+ )
58
+ end
59
+ end
60
+
61
+ fragment do |frag_coord, resolution, u|
62
+ i_time = u.time
63
+ t_val = get_t(i_time)
64
+
65
+ # Scaled coords
66
+ screen_uv = vec2(
67
+ (frag_coord.x - resolution.x * 0.5) / resolution.y,
68
+ (frag_coord.y - resolution.y * 0.5) / resolution.y
69
+ )
70
+
71
+ # Cinema bars
72
+ if abs(screen_uv.y) > 0.375
73
+ vec3(0.0, 0.0, 0.0)
74
+ else
75
+ # Setup variables
76
+ t = 0.0
77
+ s = 0.0
78
+ i = 0.0
79
+ d = 0.0
80
+ e = 0.0
81
+ c = vec3(0.0, 0.0, 0.0)
82
+
83
+ # Camera path
84
+ p = path_point(t_val)
85
+ z_dir = normalize(path_point(t_val + 4.0) - p)
86
+ x_dir = normalize(vec3(z_dir.z, 0.0, 0.0 - z_dir.x))
87
+
88
+ # View matrix
89
+ neg_x = vec3(0.0 - x_dir.x, 0.0 - x_dir.y, 0.0 - x_dir.z)
90
+ cross_xz = cross_prod(x_dir, z_dir)
91
+ ray_d = mat3_mul(neg_x, cross_xz, z_dir, vec3(screen_uv.x, screen_uv.y, 1.0))
92
+
93
+ # Raymarching loop
94
+ while i < 28.0 && d < 30.0
95
+ i = i + 1.0
96
+ p = p + ray_d * s
97
+ x_dir = path_point(p.z)
98
+ t = sin(i_time)
99
+
100
+ # Orb position
101
+ orb = vec3(x_dir.x + t, x_dir.y + t * 2.0, 6.0 + t_val + t * 2.0)
102
+ e = length(p - orb) - 0.01
103
+
104
+ # Tunnel distance
105
+ px_offset = x_dir.x + 6.0
106
+ d1 = length(vec2(p.x - px_offset, p.y - px_offset))
107
+ d2 = length(vec2(p.x - x_dir.x, p.y - x_dir.y))
108
+
109
+ s = cos(p.z * 0.6) * 2.0 + 4.0 - min(d1, d2) + noise_a(4.0, 0.25, 0.1, p) + noise_a(8.0, 0.22, 2.0, p)
110
+ s = min(e, 0.01 + 0.25 * abs(s))
111
+ d = d + s
112
+
113
+ # Accumulate color
114
+ inv_s = 1.0 / s
115
+ inv_e = 10.0 / max(e, 0.6)
116
+ c = vec3(c.x + inv_s + inv_e, c.y + inv_s + inv_e * 2.0, c.z + inv_s + inv_e * 5.0)
117
+ end
118
+
119
+ # Output color
120
+ vec3(c.x * c.x / 1000000.0, c.y * c.y / 1000000.0, c.z * c.z / 1000000.0)
121
+ end
122
+ end
123
+ end
124
+
125
+ # Initialize display
126
+ window = RBGL::GUI::Window.new(width: WIDTH, height: HEIGHT, title: "Dark Transit")
127
+
128
+ puts "Dark Transit"
129
+ puts "Original: https://www.shadertoy.com/view/WcdczB"
130
+ puts "License: Creative Commons Attribution-NonCommercial-ShareAlike 3.0"
131
+ puts "https://creativecommons.org/licenses/by-nc-sa/3.0/deed.en"
132
+ puts "Press 'q' or Escape to quit"
133
+
134
+ start_time = Time.now
135
+ frame_count = 0
136
+ last_fps_time = start_time
137
+ running = true
138
+
139
+ buffer = "\x00" * (WIDTH * HEIGHT * 4)
140
+
141
+ while running && !window.should_close?
142
+ time = Time.now - start_time
143
+
144
+ shader.render(buffer, WIDTH, HEIGHT, { time: time })
145
+
146
+ window.set_pixels(buffer)
147
+
148
+ events = window.poll_events_raw
149
+ events.each do |e|
150
+ case e[:type]
151
+ when :key_press
152
+ running = false if e[:key] == 12 || e[:key] == "q"
153
+ end
154
+ end
155
+
156
+ frame_count += 1
157
+ now = Time.now
158
+ if now - last_fps_time >= 1.0
159
+ fps = frame_count / (now - last_fps_time)
160
+ puts "FPS: #{fps.round(1)}"
161
+ frame_count = 0
162
+ last_fps_time = now
163
+ end
164
+ end
165
+
166
+ window.close
@@ -0,0 +1,428 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Fractured Orb (Metal Compute Shader)
4
+ # Original: https://www.shadertoy.com/view/ttycWW
5
+ # A mashup of 'Crystal Tetrahedron' and 'Buckyball Fracture'
6
+ # License: Creative Commons Attribution-NonCommercial-ShareAlike 3.0
7
+ # https://creativecommons.org/licenses/by-nc-sa/3.0/deed.en
8
+
9
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
10
+
11
+ require "rbgl"
12
+ require "rlsl"
13
+
14
+ WIDTH = 640
15
+ HEIGHT = 480
16
+
17
+ shader = RLSL.define_metal(:fractured_orb_metal) do
18
+ uniforms do
19
+ float :time
20
+ float :rand_seed
21
+ end
22
+
23
+ helpers(:c) do
24
+ <<~MSL
25
+ #define PHI 1.618033988749895f
26
+ #define PI 3.14159265359f
27
+ #define TAU 6.28318530718f
28
+ #define MAX_DISPERSE 5
29
+ #define MAX_BOUNCE 10
30
+
31
+ constant float OUTER = 0.35f;
32
+ constant float INNER = 0.24f;
33
+
34
+ // pR - 2D rotation
35
+ float2 pR(float2 p, float a) {
36
+ float c = cos(a), s = sin(a);
37
+ return float2(c * p.x + s * p.y, -s * p.x + c * p.y);
38
+ }
39
+
40
+ // smax - smooth maximum
41
+ float smax(float a, float b, float r) {
42
+ float ua = max(r + a, 0.0f);
43
+ float ub = max(r + b, 0.0f);
44
+ return min(-r, max(a, b)) + sqrt(ua * ua + ub * ub);
45
+ }
46
+
47
+ // vmax
48
+ float vmax2(float2 v) { return max(v.x, v.y); }
49
+ float vmax3(float3 v) { return max(max(v.x, v.y), v.z); }
50
+
51
+ // fBox - box SDF
52
+ float fBox2(float2 p, float2 b) {
53
+ float2 d = abs(p) - b;
54
+ return length(max(d, float2(0))) + vmax2(min(d, float2(0)));
55
+ }
56
+
57
+ float fBox3(float3 p, float3 b) {
58
+ float3 d = abs(p) - b;
59
+ return length(max(d, float3(0))) + vmax3(min(d, float3(0)));
60
+ }
61
+
62
+ // erot - rotate on axis
63
+ float3 erot(float3 p, float3 ax, float ro) {
64
+ float d = dot(ax, p);
65
+ float3 dax = ax * d;
66
+ float c = cos(ro), s = sin(ro);
67
+ return mix(dax, p, c) + s * cross(ax, p);
68
+ }
69
+
70
+ // Spectrum palette (IQ)
71
+ float3 spectrum(float n) {
72
+ float3 a = float3(0.5f);
73
+ float3 b = float3(0.5f);
74
+ float3 c = float3(1.0f);
75
+ float3 d = float3(0.0f, 0.33f, 0.67f);
76
+ return a + b * cos(TAU * (c * n + d));
77
+ }
78
+
79
+ // expImpulse
80
+ float expImpulse(float x, float k) {
81
+ float h = k * x;
82
+ return h * exp(1.0f - h);
83
+ }
84
+
85
+ // boolSign
86
+ float boolSign(float v) {
87
+ return v > 0.0f ? 1.0f : -1.0f;
88
+ }
89
+
90
+ float3 boolSign3(float3 v) {
91
+ return float3(boolSign(v.x), boolSign(v.y), boolSign(v.z));
92
+ }
93
+
94
+ // Icosahedron vertex (optimized by iq)
95
+ float3 icosahedronVertex(float3 p) {
96
+ float3 ap = abs(p);
97
+ float3 v = float3(PHI, 1.0f, 0.0f);
98
+ if (ap.x + ap.z * PHI > dot(ap, v)) v = float3(1.0f, 0.0f, PHI);
99
+ if (ap.z + ap.y * PHI > dot(ap, v)) v = float3(0.0f, PHI, 1.0f);
100
+ return v * 0.52573111f * boolSign3(p);
101
+ }
102
+
103
+ // Dodecahedron vertex (optimized by iq)
104
+ float3 dodecahedronVertex(float3 p) {
105
+ float3 ap = abs(p);
106
+ float3 v = float3(PHI);
107
+ float3 v2 = float3(0.0f, 1.0f, PHI + 1.0f);
108
+ float3 v3 = v2.yzx;
109
+ float3 v4 = v2.zxy;
110
+ if (dot(ap, v2) > dot(ap, v)) v = v2;
111
+ if (dot(ap, v3) > dot(ap, v)) v = v3;
112
+ if (dot(ap, v4) > dot(ap, v)) v = v4;
113
+ return v * 0.35682209f * boolSign3(p);
114
+ }
115
+
116
+ // Object SDF
117
+ float object_sdf(float3 p) {
118
+ float d = length(p) - OUTER;
119
+ d = max(d, -d - (OUTER - INNER));
120
+ return d;
121
+ }
122
+
123
+ // Map function
124
+ float2 map(float3 p, float anim_time) {
125
+ float scale = 2.5f;
126
+ p /= scale;
127
+
128
+ float outerBound = length(p) - OUTER;
129
+
130
+ float spin = anim_time * (PI / 2.0f) - 0.15f;
131
+ p.xz = pR(p.xz, spin);
132
+
133
+ // Buckyball faces
134
+ float3 va = icosahedronVertex(p);
135
+ float3 vb = dodecahedronVertex(p);
136
+
137
+ float side = boolSign(dot(p, cross(va, vb)));
138
+ float r = TAU / 5.0f * side;
139
+ float3 vc = erot(vb, va, r);
140
+ float3 vd = erot(vb, va, -r);
141
+
142
+ float d = 1e12f;
143
+ float3 pp = p;
144
+
145
+ for (int i = 0; i < 4; i++) {
146
+ // Animation
147
+ float t = fmod(anim_time * 2.0f / 3.0f + 0.25f - dot(va.xy, float2(1.0f, -1.0f)) / 30.0f, 1.0f);
148
+ if (t < 0.0f) t += 1.0f;
149
+ float t2 = clamp(t * 5.0f - 1.7f, 0.0f, 1.0f);
150
+ float explode = 1.0f - pow(1.0f - t2, 10.0f);
151
+ explode *= 1.0f - pow(t2, 5.0f);
152
+ explode += (smoothstep(0.32f, 0.34f, t) - smoothstep(0.34f, 0.5f, t)) * 0.05f;
153
+ explode *= 1.4f;
154
+ t2 = max(t - 0.53f, 0.0f) * 1.2f;
155
+ float wobble = sin(expImpulse(t2, 20.0f) * 2.2f + pow(3.0f * t2, 1.5f) * 2.0f * TAU - PI) * smoothstep(0.4f, 0.0f, t2) * 0.2f;
156
+ float anim = wobble + explode;
157
+ p -= va * anim / 2.8f;
158
+
159
+ // Build boundary edge of face
160
+ float edgeA = dot(p, normalize(vb - va));
161
+ float edgeB = dot(p, normalize(vc - va));
162
+ float edgeC = dot(p, normalize(vd - va));
163
+ float edge = max(max(edgeA, edgeB), edgeC) - 0.005f;
164
+
165
+ d = min(d, smax(object_sdf(p), edge, 0.002f));
166
+
167
+ p = pp;
168
+
169
+ // Cycle faces
170
+ float3 va2 = va;
171
+ va = vb; vb = vc; vc = vd; vd = va2;
172
+ }
173
+
174
+ float bound = outerBound - 0.002f;
175
+ if (bound * scale > 0.002f) {
176
+ d = min(d, bound);
177
+ }
178
+
179
+ return float2(d * scale, 1.0f);
180
+ }
181
+
182
+ // Normal calculation
183
+ float3 calc_normal(float3 pos, float anim_time) {
184
+ float3 n = float3(0.0f);
185
+ for (int i = 0; i < 4; i++) {
186
+ float3 e = 0.5773f * (2.0f * float3((((i + 3) >> 1) & 1), ((i >> 1) & 1), (i & 1)) - 1.0f);
187
+ n += e * map(pos + 0.001f * e, anim_time).x;
188
+ }
189
+ return normalize(n);
190
+ }
191
+
192
+ // Spherical matrix multiply
193
+ float3 sphericalMatrix_mul(float2 tp, float3 v) {
194
+ float theta = tp.x, phi = tp.y;
195
+ float cx = cos(theta), cy = cos(phi);
196
+ float sx = sin(theta), sy = sin(phi);
197
+ return float3(
198
+ cy * v.x + (-sy * -sx) * v.y + (-sy * cx) * v.z,
199
+ cx * v.y + sx * v.z,
200
+ sy * v.x + (cy * -sx) * v.y + (cy * cx) * v.z
201
+ );
202
+ }
203
+
204
+ // Light function
205
+ float3 light_func(float3 origin, float3 rayDir, float2 envOrientation) {
206
+ origin = -origin;
207
+ rayDir = -rayDir;
208
+ origin = sphericalMatrix_mul(envOrientation, origin);
209
+ rayDir = sphericalMatrix_mul(envOrientation, rayDir);
210
+
211
+ float3 pos = float3(-6.0f);
212
+ float3 normal = normalize(pos);
213
+ float3 up = normalize(float3(-1.0f, 1.0f, 0.0f));
214
+
215
+ float denom = dot(rayDir, normal);
216
+ if (abs(denom) < 0.0001f) return float3(0.0f);
217
+
218
+ float t = dot(pos - origin, normal) / denom;
219
+ if (t < 0.0f) return float3(0.0f);
220
+
221
+ float3 point = origin + t * rayDir - pos;
222
+ float3 tangent = cross(normal, up);
223
+ float3 bitangent = cross(normal, tangent);
224
+ float2 uv_l = float2(dot(tangent, point), dot(bitangent, point));
225
+
226
+ float l = smoothstep(0.75f, 0.0f, fBox2(uv_l, float2(0.5f, 2.0f)) - 1.0f);
227
+ l *= smoothstep(6.0f, 0.0f, length(uv_l));
228
+ return float3(l);
229
+ }
230
+
231
+ // Environment function
232
+ float3 env_func(float3 origin, float3 rayDir, float2 envOrientation) {
233
+ origin = -origin;
234
+ rayDir = -rayDir;
235
+ origin = sphericalMatrix_mul(envOrientation, origin);
236
+ rayDir = sphericalMatrix_mul(envOrientation, rayDir);
237
+
238
+ float l = smoothstep(0.0f, 1.7f, dot(rayDir, float3(0.5f, -0.3f, 1.0f))) * 0.4f;
239
+ return float3(0.9f, 0.83f, 1.0f) * l;
240
+ }
241
+
242
+ // Simple hash
243
+ float simple_hash(float seed) {
244
+ return fract(sin(seed * 12.9898f) * 43758.5453f);
245
+ }
246
+ MSL
247
+ end
248
+
249
+ fragment do
250
+ <<~MSL
251
+ float duration = 10.0f / 3.0f;
252
+ float anim_time = fmod(u.time / duration, 1.0f);
253
+
254
+ float2 envOrientation = (float2(81.5f / 187.0f, 119.0f / 187.0f) * 2.0f - 1.0f) * 2.0f;
255
+
256
+ // Shadertoy-style centered UV: (fragCoord - resolution/2) / resolution.y
257
+ float2 screen_uv = uv - float2(resolution.x / resolution.y * 0.5f, 0.5f);
258
+
259
+ float3 col = float3(0.0f);
260
+ float3 BGCOL = float3(0.9f, 0.83f, 1.0f);
261
+ float3 bgCol = BGCOL * 0.22f;
262
+
263
+ float maxDist = 30.0f;
264
+ float3 camOrigin = float3(0.0f, 0.0f, 25.0f);
265
+ float3 camDir = normalize(float3(screen_uv * 0.168f, -1.0f));
266
+
267
+ // First march for depth
268
+ float firstLen = 0.0f;
269
+ float firstResY = 0.0f;
270
+ float3 firstP = camOrigin;
271
+ {
272
+ float len = 0.0f;
273
+ float dist = 0.0f;
274
+ for (int i = 0; i < 300; i++) {
275
+ len += dist * 0.8f;
276
+ float3 p = camOrigin + len * camDir;
277
+ float2 res = map(p, anim_time);
278
+ dist = res.x;
279
+ if (dist < 0.001f) { firstResY = res.y; break; }
280
+ if (len >= maxDist) { len = maxDist; firstResY = 0.0f; break; }
281
+ }
282
+ firstLen = len;
283
+ firstP = camOrigin + len * camDir;
284
+ }
285
+
286
+ // Dispersion loop
287
+ for (int disperse = 0; disperse < MAX_DISPERSE; disperse++) {
288
+ float invert = 1.0f;
289
+ float3 sam = float3(0.0f);
290
+ float3 origin = camOrigin;
291
+ float3 rayDir = camDir;
292
+
293
+ float extinctionDist = 0.0f;
294
+ float wavelength = float(disperse) / float(MAX_DISPERSE);
295
+
296
+ float rand = simple_hash(u.rand_seed + float(disperse) * 0.1f + uv.x * 100.0f + uv.y * 1000.0f);
297
+ wavelength += (rand * 2.0f - 1.0f) * 0.1f;
298
+
299
+ int bounceCount = 0;
300
+
301
+ for (int bounce = 0; bounce < MAX_BOUNCE; bounce++) {
302
+ float hitLen, hitResY;
303
+ float3 p;
304
+
305
+ if (bounce == 0) {
306
+ hitLen = firstLen;
307
+ hitResY = firstResY;
308
+ p = firstP;
309
+ } else {
310
+ // March
311
+ float len = 0.0f;
312
+ float dist = 0.0f;
313
+ hitResY = 0.0f;
314
+ for (int mi = 0; mi < 300; mi++) {
315
+ len += dist;
316
+ p = origin + len * rayDir;
317
+ float2 res = map(p, anim_time);
318
+ dist = res.x * invert;
319
+ if (dist < 0.001f) { hitResY = res.y; break; }
320
+ if (len >= maxDist / 2.0f) { len = maxDist / 2.0f; hitResY = 0.0f; break; }
321
+ }
322
+ hitLen = len;
323
+ }
324
+
325
+ if (invert < 0.0f) {
326
+ extinctionDist += hitLen;
327
+ }
328
+
329
+ if (hitResY == 0.0f) break;
330
+
331
+ float3 nor = calc_normal(p, anim_time) * invert;
332
+ float3 ref = reflect(rayDir, nor);
333
+
334
+ // Shade
335
+ sam += light_func(p, ref, envOrientation) * 0.5f;
336
+ float fresnel = 1.0f - abs(dot(rayDir, nor));
337
+ sam += pow(fresnel, 5.0f) * 0.1f;
338
+ sam *= float3(0.85f, 0.85f, 0.98f);
339
+
340
+ // Refract
341
+ float ior = mix(1.2f, 1.8f, wavelength);
342
+ ior = invert < 0.0f ? ior : 1.0f / ior;
343
+
344
+ float3 raf = refract(rayDir, nor, ior);
345
+ bool tif = (length(raf) < 0.001f);
346
+ rayDir = tif ? ref : raf;
347
+
348
+ float offset = 0.01f / (abs(dot(rayDir, nor)) + 0.0001f);
349
+ origin = p + offset * rayDir;
350
+ invert *= -1.0f;
351
+
352
+ bounceCount = bounce;
353
+ }
354
+
355
+ sam += (bounceCount == 0) ? bgCol : env_func(firstP, rayDir, envOrientation);
356
+
357
+ if (bounceCount == 0) {
358
+ col += sam * float(MAX_DISPERSE) / 2.0f;
359
+ break;
360
+ } else {
361
+ float3 spec = spectrum(-wavelength + 0.25f);
362
+ col += sam * spec;
363
+ }
364
+ }
365
+
366
+ col /= float(MAX_DISPERSE);
367
+
368
+ // Tonemap
369
+ col = pow(col, float3(1.25f)) * 2.5f;
370
+ col = col / 2.0f * 16.0f;
371
+ col = max(float3(0.0f), col - 0.004f);
372
+ col = (col * (6.2f * col + 0.5f)) / (col * (6.2f * col + 1.7f) + 0.06f);
373
+
374
+ return col;
375
+ MSL
376
+ end
377
+ end
378
+
379
+ # Initialize display
380
+ window = RBGL::GUI::Window.new(width: WIDTH, height: HEIGHT, title: "Fractured Orb (Metal)")
381
+
382
+ unless window.metal_available?
383
+ puts "Metal is not available!"
384
+ exit 1
385
+ end
386
+
387
+ puts "Fractured Orb"
388
+ puts "Original: https://www.shadertoy.com/view/ttycWW"
389
+ puts "License: Creative Commons Attribution-NonCommercial-ShareAlike 3.0"
390
+ puts "https://creativecommons.org/licenses/by-nc-sa/3.0/deed.en"
391
+ puts "Press 'q' or Escape to quit"
392
+
393
+ start_time = Time.now
394
+ frame_count = 0
395
+ last_fps_time = start_time
396
+ running = true
397
+
398
+ while running && !window.should_close?
399
+ time = Time.now - start_time
400
+ rand_seed = rand * 1000.0
401
+
402
+ begin
403
+ shader.render_metal(window.native_handle, WIDTH, HEIGHT, { time: time, rand_seed: rand_seed })
404
+ rescue => e
405
+ puts "Error: #{e.message}"
406
+ puts e.backtrace.first(10).join("\n")
407
+ running = false
408
+ break
409
+ end
410
+
411
+ events = window.poll_events_raw
412
+ events.each do |e|
413
+ if e[:type] == :key_press && (e[:key] == 12 || e[:key] == "q")
414
+ running = false
415
+ end
416
+ end
417
+
418
+ frame_count += 1
419
+ now = Time.now
420
+ if now - last_fps_time >= 1.0
421
+ fps = frame_count / (now - last_fps_time)
422
+ puts "FPS: #{fps.round(1)}"
423
+ frame_count = 0
424
+ last_fps_time = now
425
+ end
426
+ end
427
+
428
+ window.close