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,362 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Teapot - Raymarched Bezier Curves (Ruby Mode)
4
+ # Original: https://www.shadertoy.com/view/MdKcDw
5
+ # Created by Sebastien Durand - 2014
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(:teapot_ruby) do
18
+ uniforms do
19
+ float :time
20
+ vec2 :mouse
21
+ end
22
+
23
+ functions do
24
+ # Helper functions
25
+ define :cross2d, returns: :float, params: { a: :vec2, b: :vec2 }
26
+ define :smin, returns: :float, params: { a: :float, b: :float, k: :float }
27
+ define :bezier_dist, returns: :vec2, params: { m: :vec2, n: :vec2, o: :vec2, p: :vec3 }
28
+ define :scene_dist, returns: :float, params: { p: :vec3 }
29
+ define :basis, returns: [:vec3, :vec3], params: { n: :vec3 }
30
+ define :calc_normal, returns: :vec3, params: { p: :vec3, ray: :vec3, t: :float, res_x: :float }
31
+ define :compute_brdf, returns: :vec3, params: { n: :vec3, l: :vec3, h: :vec3, r: :vec3, tang: :vec3, binorm: :vec3 }
32
+ define :hsv2rgb_smooth, returns: :vec3, params: { h: :float, s: :float, v: :float }
33
+ end
34
+
35
+ helpers do
36
+ # Control points - Body profile (15 points)
37
+ A = [
38
+ vec2(0.0, 0.0), vec2(0.64, 0.0), vec2(0.64, 0.03),
39
+ vec2(0.8, 0.12), vec2(0.8, 0.3), vec2(0.8, 0.48),
40
+ vec2(0.64, 0.9), vec2(0.6, 0.93), vec2(0.56, 0.9),
41
+ vec2(0.56, 0.96), vec2(0.12, 1.02), vec2(0.0, 1.05),
42
+ vec2(0.16, 1.14), vec2(0.2, 1.2), vec2(0.0, 1.2)
43
+ ]
44
+
45
+ # Control points - Spout (5 points)
46
+ T1 = [
47
+ vec2(1.16, 0.96), vec2(1.04, 0.9), vec2(1.0, 0.72),
48
+ vec2(0.92, 0.48), vec2(0.72, 0.42)
49
+ ]
50
+
51
+ # Control points - Handle (5 points)
52
+ T2 = [
53
+ vec2(-0.6, 0.78), vec2(-1.16, 0.84), vec2(-1.16, 0.63),
54
+ vec2(-1.2, 0.42), vec2(-0.72, 0.24)
55
+ ]
56
+
57
+ # Material properties
58
+ LO = vec2(0.450, 0.048)
59
+ ALPHA_M = vec2(0.045, 0.068)
60
+ SCALE = vec3(1.0, 20.0, 10.0)
61
+ SURFACE_COLOR = vec3(0.45, 0.54, 1.0)
62
+
63
+ # Light direction (pre-normalized: normalize(vec3(1.0, 0.72, 1.0)))
64
+ L = vec3(0.6286, 0.4526, 0.6286)
65
+
66
+ # Up vector
67
+ Y = vec3(0.0, 1.0, 0.0)
68
+
69
+ # Constants
70
+ ONE_OVER_PI = 0.31830988618
71
+
72
+ # Cross product 2D
73
+ def cross2d(a, b)
74
+ a.x * b.y - b.x * a.y
75
+ end
76
+
77
+ # Smooth minimum for blending
78
+ def smin(a, b, k)
79
+ h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0)
80
+ mix(b, a, h) - k * h * (1.0 - h)
81
+ end
82
+
83
+ # Distance to quadratic Bezier curve
84
+ def bezier_dist(m, n, o, p)
85
+ q = vec2(p.x, p.y)
86
+ m = vec2(m.x - q.x, m.y - q.y)
87
+ n = vec2(n.x - q.x, n.y - q.y)
88
+ o = vec2(o.x - q.x, o.y - q.y)
89
+
90
+ x = cross2d(m, o)
91
+ y = 2.0 * cross2d(n, m)
92
+ z = 2.0 * cross2d(o, n)
93
+
94
+ i = vec2(o.x - m.x, o.y - m.y)
95
+ j = vec2(o.x - n.x, o.y - n.y)
96
+ k = vec2(n.x - m.x, n.y - m.y)
97
+
98
+ s = vec2(
99
+ 2.0 * (x * i.x + y * j.x + z * k.x),
100
+ 2.0 * (x * i.y + y * j.y + z * k.y)
101
+ )
102
+
103
+ dot_s = s.x * s.x + s.y * s.y
104
+ if dot_s < 0.0000000001
105
+ dot_s = 0.0000000001
106
+ end
107
+
108
+ r = vec2(
109
+ m.x + (y * z - x * x) * s.y / dot_s,
110
+ m.y - (y * z - x * x) * s.x / dot_s
111
+ )
112
+
113
+ denom = x + x + y + z
114
+ if abs(denom) < 0.0000000001
115
+ denom = 0.0000000001
116
+ end
117
+
118
+ t = clamp((cross2d(r, i) + 2.0 * cross2d(k, r)) / denom, 0.0, 1.0)
119
+
120
+ jk = vec2(j.x - k.x, j.y - k.y)
121
+ r = vec2(
122
+ m.x + t * (k.x + k.x + t * jk.x),
123
+ m.y + t * (k.y + k.y + t * jk.y)
124
+ )
125
+
126
+ vec2(sqrt(r.x * r.x + r.y * r.y + p.z * p.z), t)
127
+ end
128
+
129
+ # Scene distance function
130
+ def scene_dist(p)
131
+ # Spout curve
132
+ h = bezier_dist(T1[2], T1[3], T1[4], p)
133
+
134
+ # Handle distance
135
+ handle_d = min(
136
+ bezier_dist(T2[0], T2[1], T2[2], p).x,
137
+ bezier_dist(T2[2], T2[3], T2[4], p).x
138
+ ) - 0.06
139
+
140
+ # Spout distance
141
+ spout_hole = abs(bezier_dist(T1[0], T1[1], T1[2], p).x - 0.07) - 0.01
142
+ spout_body = h.x * (1.0 - 0.75 * h.y) - 0.08
143
+ spout_d = max(p.y - 0.9, min(spout_hole, spout_body))
144
+
145
+ b = min(handle_d, spout_d)
146
+
147
+ # Body distance (rotation symmetry)
148
+ r_xz = sqrt(p.x * p.x + p.z * p.z)
149
+ qq = vec3(r_xz, p.y, 0.0)
150
+
151
+ # Body curves (step by 2)
152
+ a0 = bezier_dist(A[0], A[1], A[2], qq).x - 0.015
153
+ a1 = bezier_dist(A[2], A[3], A[4], qq).x - 0.015
154
+ a2 = bezier_dist(A[4], A[5], A[6], qq).x - 0.015
155
+ a3 = bezier_dist(A[6], A[7], A[8], qq).x - 0.015
156
+ a4 = bezier_dist(A[8], A[9], A[10], qq).x - 0.015
157
+ a5 = bezier_dist(A[10], A[11], A[12], qq).x - 0.015
158
+ a6 = bezier_dist(A[12], A[13], A[14], qq).x - 0.015
159
+
160
+ a = min(a0, min(a1, min(a2, min(a3, min(a4, min(a5, a6)))))) * 0.7
161
+
162
+ smin(a, b, 0.02)
163
+ end
164
+
165
+ # Build orthonormal basis from normal
166
+ def basis(n)
167
+ a = n.y / (1.0 + n.z + 0.0000000001)
168
+ b = n.y * a
169
+ c = 0.0 - n.x * a
170
+ xp = vec3(n.z + b, c, 0.0 - n.x)
171
+ yp = vec3(c, 1.0 - b, 0.0 - n.y)
172
+ [xp, yp]
173
+ end
174
+
175
+ # Normal calculation
176
+ def calc_normal(p, ray, t, res_x)
177
+ eps = 0.4 * t / res_x
178
+
179
+ d0 = scene_dist(p)
180
+ dx = scene_dist(vec3(p.x + eps, p.y, p.z)) - d0
181
+ dy = scene_dist(vec3(p.x, p.y + eps, p.z)) - d0
182
+ dz = scene_dist(vec3(p.x, p.y, p.z + eps)) - d0
183
+
184
+ grad = vec3(dx, dy, dz)
185
+
186
+ # Prevent normals pointing away from camera
187
+ d = grad.x * ray.x + grad.y * ray.y + grad.z * ray.z
188
+ if d > 0.0
189
+ grad = vec3(grad.x - ray.x * d, grad.y - ray.y * d, grad.z - ray.z * d)
190
+ end
191
+
192
+ normalize(grad)
193
+ end
194
+
195
+ # BRDF computation
196
+ def compute_brdf(n, l, h, r, tang, binorm)
197
+ e1 = (h.x * tang.x + h.y * tang.y + h.z * tang.z) / ALPHA_M.x
198
+ e2 = (h.x * binorm.x + h.y * binorm.y + h.z * binorm.z) / ALPHA_M.y
199
+ hn = h.x * n.x + h.y * n.y + h.z * n.z
200
+ big_e = 0.0 - 2.0 * ((e1 * e1 + e2 * e2) / (1.0 + hn))
201
+
202
+ cos_i = n.x * l.x + n.y * l.y + n.z * l.z
203
+ cos_r = n.x * r.x + n.y * r.y + n.z * r.z
204
+ denom = sqrt(abs(cos_i * cos_r) + 0.000001)
205
+
206
+ brdf = LO.x * ONE_OVER_PI + LO.y * (1.0 / denom) * (1.0 / (4.0 * PI * ALPHA_M.x * ALPHA_M.y)) * exp(big_e)
207
+
208
+ intensity = SCALE.x * LO.x * ONE_OVER_PI + SCALE.y * LO.y * cos_i * brdf + SCALE.z * hn * LO.y
209
+
210
+ vec3(SURFACE_COLOR.x * intensity, SURFACE_COLOR.y * intensity, SURFACE_COLOR.z * intensity)
211
+ end
212
+
213
+ # HSV to RGB with cubic smoothing
214
+ def hsv2rgb_smooth(h, s, v)
215
+ rx = abs(mod(h * 6.0 + 0.0, 6.0) - 3.0) - 1.0
216
+ gx = abs(mod(h * 6.0 + 4.0, 6.0) - 3.0) - 1.0
217
+ bx = abs(mod(h * 6.0 + 2.0, 6.0) - 3.0) - 1.0
218
+
219
+ rx = clamp(rx, 0.0, 1.0)
220
+ gx = clamp(gx, 0.0, 1.0)
221
+ bx = clamp(bx, 0.0, 1.0)
222
+
223
+ # Cubic smoothing
224
+ rx = rx * rx * (3.0 - 2.0 * rx)
225
+ gx = gx * gx * (3.0 - 2.0 * gx)
226
+ bx = bx * bx * (3.0 - 2.0 * bx)
227
+
228
+ vec3(
229
+ v * (1.0 + s * (rx - 1.0)),
230
+ v * (1.0 + s * (gx - 1.0)),
231
+ v * (1.0 + s * (bx - 1.0))
232
+ )
233
+ end
234
+ end
235
+
236
+ fragment do |frag_coord, resolution, u|
237
+ # UV coordinates (aspect-ratio preserving, same as Metal)
238
+ uv = vec2(frag_coord.x / resolution.y, frag_coord.y / resolution.y)
239
+
240
+ # Normalized screen coordinates
241
+ q = vec2(uv.x * resolution.y / resolution.x, uv.y)
242
+
243
+ # Centered coordinates for camera ray
244
+ p = vec2(q.x * 2.0 - 1.0, q.y * 2.0 - 1.0)
245
+ p = vec2(p.x * resolution.x / resolution.y, p.y)
246
+
247
+ # Camera with mouse control
248
+ mouse_x = u.mouse.x / resolution.x
249
+ mouse_y = u.mouse.y / resolution.y
250
+ cam_angle = 5.0 + 0.2 * u.time + 4.0 * mouse_x
251
+
252
+ origin = vec3(cos(cam_angle) * 3.5, (0.7 - mouse_y) * 3.5, sin(cam_angle) * 3.5)
253
+ target = vec3(Y.x * 0.4, Y.y * 0.4, Y.z * 0.4)
254
+ w = normalize(vec3(target.x - origin.x, target.y - origin.y, target.z - origin.z))
255
+
256
+ # Camera basis
257
+ cam_u = normalize(vec3(
258
+ w.y * Y.z - w.z * Y.y,
259
+ w.z * Y.x - w.x * Y.z,
260
+ w.x * Y.y - w.y * Y.x
261
+ ))
262
+ cam_v = vec3(
263
+ cam_u.y * w.z - cam_u.z * w.y,
264
+ cam_u.z * w.x - cam_u.x * w.z,
265
+ cam_u.x * w.y - cam_u.y * w.x
266
+ )
267
+
268
+ ray = normalize(vec3(
269
+ cam_u.x * p.x + cam_v.x * p.y + w.x * 2.0,
270
+ cam_u.y * p.x + cam_v.y * p.y + w.y * 2.0,
271
+ cam_u.z * p.x + cam_v.z * p.y + w.z * 2.0
272
+ ))
273
+
274
+ # Raymarching
275
+ t = 0.0
276
+ h = 0.1
277
+ i = 0.0
278
+ while i < 48.0 && h > 0.0001 && t < 4.7
279
+ pos = vec3(origin.x + ray.x * t, origin.y + ray.y * t, origin.z + ray.z * t)
280
+ h = scene_dist(pos)
281
+ t = t + h
282
+ i = i + 1.0
283
+ end
284
+
285
+ # Background gradient
286
+ color = mix(
287
+ hsv2rgb_smooth(0.5 + u.time * 0.02, 0.35, 0.4),
288
+ hsv2rgb_smooth(0.0 - 0.5 + u.time * 0.02, 0.35, 0.7),
289
+ q.y
290
+ )
291
+
292
+ if h < 0.001
293
+ hit = vec3(origin.x + ray.x * t, origin.y + ray.y * t, origin.z + ray.z * t)
294
+ n = calc_normal(hit, ray, t, resolution.x)
295
+
296
+ big_v = normalize(vec3(origin.x - hit.x, origin.y - hit.y, origin.z - hit.z))
297
+ big_h = normalize(vec3(L.x + big_v.x, L.y + big_v.y, L.z + big_v.z))
298
+ dot_nl = n.x * L.x + n.y * L.y + n.z * L.z
299
+ big_r = normalize(vec3(
300
+ n.x * 2.0 * dot_nl - L.x,
301
+ n.y * 2.0 * dot_nl - L.y,
302
+ n.z * 2.0 * dot_nl - L.z
303
+ ))
304
+
305
+ tang, binorm = basis(n)
306
+ color = compute_brdf(n, L, big_h, big_r, tang, binorm)
307
+ end
308
+
309
+ # Vignette
310
+ vignette = pow(16.0 * q.x * q.y * (1.0 - q.x) * (1.0 - q.y), 0.16)
311
+ vec3(color.x * vignette, color.y * vignette, color.z * vignette)
312
+ end
313
+ end
314
+
315
+ # Initialize display
316
+ window = RBGL::GUI::Window.new(width: WIDTH, height: HEIGHT, title: "Teapot Ruby (Raymarched Bezier)")
317
+
318
+ puts "Teapot - Raymarched Bezier Curves (Ruby Mode)"
319
+ puts "Original: https://www.shadertoy.com/view/MdKcDw"
320
+ puts "License: Creative Commons Attribution-NonCommercial-ShareAlike 3.0"
321
+ puts "https://creativecommons.org/licenses/by-nc-sa/3.0/deed.en"
322
+ puts "Move mouse to rotate camera"
323
+ puts "Press 'q' or Escape to quit"
324
+
325
+ start_time = Time.now
326
+ frame_count = 0
327
+ last_fps_time = start_time
328
+ running = true
329
+ mouse_x = 0.0
330
+ mouse_y = 0.0
331
+
332
+ buffer = "\x00" * (WIDTH * HEIGHT * 4)
333
+
334
+ while running && !window.should_close?
335
+ time = Time.now - start_time
336
+
337
+ shader.render(buffer, WIDTH, HEIGHT, { time: time, mouse: [mouse_x, mouse_y] })
338
+
339
+ window.set_pixels(buffer)
340
+
341
+ events = window.poll_events_raw
342
+ events.each do |e|
343
+ case e[:type]
344
+ when :key_press
345
+ running = false if e[:key] == 12 || e[:key] == "q"
346
+ when :mouse_move
347
+ mouse_x = e[:x].to_f
348
+ mouse_y = e[:y].to_f
349
+ end
350
+ end
351
+
352
+ frame_count += 1
353
+ now = Time.now
354
+ if now - last_fps_time >= 1.0
355
+ fps = frame_count / (now - last_fps_time)
356
+ puts "FPS: #{fps.round(1)}"
357
+ frame_count = 0
358
+ last_fps_time = now
359
+ end
360
+ end
361
+
362
+ window.close
@@ -0,0 +1,344 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Teapot - Raymarched Bezier Curves (Metal Compute Shader)
4
+ # Original: https://www.shadertoy.com/view/MdKcDw
5
+ # GPU-accelerated version
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
+ # Generate control points as Metal constants
18
+ def generate_teapot_constants
19
+ body = [
20
+ [0.0, 0.0], [0.64, 0.0], [0.64, 0.03],
21
+ [0.8, 0.12], [0.8, 0.3], [0.8, 0.48],
22
+ [0.64, 0.9], [0.6, 0.93], [0.56, 0.9],
23
+ [0.56, 0.96], [0.12, 1.02], [0.0, 1.05],
24
+ [0.16, 1.14], [0.2, 1.2], [0.0, 1.2]
25
+ ]
26
+ spout = [
27
+ [1.16, 0.96], [1.04, 0.9], [1.0, 0.72],
28
+ [0.92, 0.48], [0.72, 0.42]
29
+ ]
30
+ handle = [
31
+ [-0.6, 0.78], [-1.16, 0.84], [-1.16, 0.63],
32
+ [-1.2, 0.42], [-0.72, 0.24]
33
+ ]
34
+
35
+ body_init = body.map.with_index { |(x, y), i| "constant float2 A#{i} = float2(#{x}f, #{y}f);" }.join("\n")
36
+ spout_init = spout.map.with_index { |(x, y), i| "constant float2 T1_#{i} = float2(#{x}f, #{y}f);" }.join("\n")
37
+ handle_init = handle.map.with_index { |(x, y), i| "constant float2 T2_#{i} = float2(#{x}f, #{y}f);" }.join("\n")
38
+
39
+ <<~MSL
40
+ // Control points as constants
41
+ #{body_init}
42
+ #{spout_init}
43
+ #{handle_init}
44
+
45
+ // Pre-computed: normalize(float3(1.0f, 0.72f, 1.0f))
46
+ constant float3 L = float3(0.6302f, 0.4537f, 0.6302f);
47
+ constant float3 Y_VEC = float3(0.0f, 1.0f, 0.0f);
48
+
49
+ // Material properties
50
+ constant float2 lo = float2(0.450f, 0.048f);
51
+ constant float2 alpha_m = float2(0.045f, 0.068f);
52
+ constant float3 Scale = float3(1.0f, 20.0f, 10.0f);
53
+ constant float3 surfaceColor = float3(0.45f, 0.54f, 1.0f);
54
+ constant float ONE_OVER_PI = 0.31830988618f;
55
+ MSL
56
+ end
57
+
58
+ shader = RLSL.define_metal(:teapot_metal) do
59
+ uniforms do
60
+ float :time
61
+ vec2 :mouse
62
+ end
63
+
64
+ helpers(:c) do
65
+ teapot_constants = generate_teapot_constants
66
+
67
+ <<~MSL
68
+ #{teapot_constants}
69
+
70
+ // Cross product 2D
71
+ #define U(a,b) ((a).x*(b).y-(b).x*(a).y)
72
+
73
+ // Distance to quadratic Bezier curve
74
+ float2 bezier_dist(float2 m, float2 n, float2 o, float3 p) {
75
+ float2 q = float2(p.x, p.y);
76
+ m = m - q;
77
+ n = n - q;
78
+ o = o - q;
79
+
80
+ float x = U(m, o);
81
+ float y = 2.0f * U(n, m);
82
+ float z = 2.0f * U(o, n);
83
+
84
+ float2 i = o - m;
85
+ float2 j = o - n;
86
+ float2 k = n - m;
87
+
88
+ float2 s = float2(
89
+ 2.0f * (x * i.x + y * j.x + z * k.x),
90
+ 2.0f * (x * i.y + y * j.y + z * k.y)
91
+ );
92
+
93
+ float dot_s = dot(s, s);
94
+ if (dot_s < 1e-10f) dot_s = 1e-10f;
95
+
96
+ float2 r = float2(
97
+ m.x + (y * z - x * x) * s.y / dot_s,
98
+ m.y - (y * z - x * x) * s.x / dot_s
99
+ );
100
+
101
+ float denom = x + x + y + z;
102
+ if (abs(denom) < 1e-10f) denom = 1e-10f;
103
+
104
+ float t = clamp((U(r, i) + 2.0f * U(k, r)) / denom, 0.0f, 1.0f);
105
+
106
+ float2 jk = j - k;
107
+ r.x = m.x + t * (k.x + k.x + t * jk.x);
108
+ r.y = m.y + t * (k.y + k.y + t * jk.y);
109
+
110
+ return float2(sqrt(dot(r, r) + p.z * p.z), t);
111
+ }
112
+
113
+ // Smooth minimum for blending
114
+ float smin(float a, float b, float k) {
115
+ float h = clamp(0.5f + 0.5f * (b - a) / k, 0.0f, 1.0f);
116
+ return mix(b, a, h) - k * h * (1.0f - h);
117
+ }
118
+
119
+ // Scene distance function
120
+ float scene_dist(float3 p) {
121
+ // Spout
122
+ float2 h = bezier_dist(T1_2, T1_3, T1_4, p);
123
+
124
+ // Handle
125
+ float handle_d = min(
126
+ bezier_dist(T2_0, T2_1, T2_2, p).x,
127
+ bezier_dist(T2_2, T2_3, T2_4, p).x
128
+ ) - 0.06f;
129
+
130
+ // Spout
131
+ float spout_hole = abs(bezier_dist(T1_0, T1_1, T1_2, p).x - 0.07f) - 0.01f;
132
+ float spout_body = h.x * (1.0f - 0.75f * h.y) - 0.08f;
133
+ float spout_d = max(p.y - 0.9f, min(spout_hole, spout_body));
134
+
135
+ float b = min(handle_d, spout_d);
136
+
137
+ // Body (rotation symmetry)
138
+ float r_xz = sqrt(p.x * p.x + p.z * p.z);
139
+ float3 qq = float3(r_xz, p.y, 0.0f);
140
+
141
+ float a = 99.0f;
142
+ a = min(a, (bezier_dist(A0, A1, A2, qq).x - 0.015f) * 0.7f);
143
+ a = min(a, (bezier_dist(A2, A3, A4, qq).x - 0.015f) * 0.7f);
144
+ a = min(a, (bezier_dist(A4, A5, A6, qq).x - 0.015f) * 0.7f);
145
+ a = min(a, (bezier_dist(A6, A7, A8, qq).x - 0.015f) * 0.7f);
146
+ a = min(a, (bezier_dist(A8, A9, A10, qq).x - 0.015f) * 0.7f);
147
+ a = min(a, (bezier_dist(A10, A11, A12, qq).x - 0.015f) * 0.7f);
148
+ a = min(a, (bezier_dist(A12, A13, A14, qq).x - 0.015f) * 0.7f);
149
+
150
+ return smin(a, b, 0.02f);
151
+ }
152
+
153
+ // Normal calculation
154
+ float3 calc_normal(float3 p, float3 ray, float t, float res_x) {
155
+ float eps = 0.4f * t / res_x;
156
+
157
+ float d0 = scene_dist(p);
158
+ float dx = scene_dist(p + float3(eps, 0.0f, 0.0f)) - d0;
159
+ float dy = scene_dist(p + float3(0.0f, eps, 0.0f)) - d0;
160
+ float dz = scene_dist(p + float3(0.0f, 0.0f, eps)) - d0;
161
+
162
+ float3 grad = float3(dx, dy, dz);
163
+
164
+ float d = dot(grad, ray);
165
+ if (d > 0.0f) {
166
+ grad = grad - ray * d;
167
+ }
168
+
169
+ return normalize(grad);
170
+ }
171
+
172
+ // Build orthonormal basis
173
+ void basis(float3 n, thread float3* xp, thread float3* yp) {
174
+ float a = n.y / (1.0f + n.z + 1e-10f);
175
+ float b = n.y * a;
176
+ float c = -n.x * a;
177
+ *xp = float3(n.z + b, c, -n.x);
178
+ *yp = float3(c, 1.0f - b, -n.y);
179
+ }
180
+
181
+ // BRDF
182
+ float3 compute_brdf(float3 n, float3 l, float3 h, float3 r, float3 tang, float3 binorm) {
183
+ float e1 = dot(h, tang) / alpha_m.x;
184
+ float e2 = dot(h, binorm) / alpha_m.y;
185
+ float hn = dot(h, n);
186
+ float E = -2.0f * ((e1 * e1 + e2 * e2) / (1.0f + hn));
187
+
188
+ float cos_i = dot(n, l);
189
+ float cos_r = dot(n, r);
190
+ float denom = sqrt(abs(cos_i * cos_r) + 1e-6f);
191
+
192
+ float brdf = lo.x * ONE_OVER_PI +
193
+ lo.y * (1.0f / denom) * (1.0f / (4.0f * 3.14159265f * alpha_m.x * alpha_m.y)) * exp(E);
194
+
195
+ float intensity = Scale.x * lo.x * ONE_OVER_PI +
196
+ Scale.y * lo.y * cos_i * brdf +
197
+ Scale.z * hn * lo.y;
198
+
199
+ return surfaceColor * intensity;
200
+ }
201
+
202
+ // HSV to RGB
203
+ float3 hsv2rgb_smooth(float h, float s, float v) {
204
+ float3 rgb = float3(
205
+ abs(fmod(h * 6.0f + 0.0f, 6.0f) - 3.0f) - 1.0f,
206
+ abs(fmod(h * 6.0f + 4.0f, 6.0f) - 3.0f) - 1.0f,
207
+ abs(fmod(h * 6.0f + 2.0f, 6.0f) - 3.0f) - 1.0f
208
+ );
209
+ rgb = clamp(rgb, 0.0f, 1.0f);
210
+ rgb = rgb * rgb * (3.0f - 2.0f * rgb);
211
+
212
+ return float3(
213
+ v * (1.0f + s * (rgb.x - 1.0f)),
214
+ v * (1.0f + s * (rgb.y - 1.0f)),
215
+ v * (1.0f + s * (rgb.z - 1.0f))
216
+ );
217
+ }
218
+ MSL
219
+ end
220
+
221
+ fragment do
222
+ <<~MSL
223
+ float2 q = float2(uv.x * resolution.y / resolution.x, uv.y);
224
+ float2 p = q * 2.0f - float2(1.0f, 1.0f);
225
+ p.x *= resolution.x / resolution.y;
226
+
227
+ // Camera with mouse control
228
+ float mouse_x = u.mouse.x / resolution.x;
229
+ float mouse_y = u.mouse.y / resolution.y;
230
+ float cam_angle = 5.0f + 0.2f * u.time + 4.0f * mouse_x;
231
+
232
+ float3 origin = float3(cos(cam_angle), 0.7f - mouse_y, sin(cam_angle)) * 3.5f;
233
+ float3 w = normalize(Y_VEC * 0.4f - origin);
234
+ float3 cam_u = normalize(cross(w, Y_VEC));
235
+ float3 cam_v = cross(cam_u, w);
236
+
237
+ float3 ray = normalize(cam_u * p.x + cam_v * p.y + w + w);
238
+
239
+ // Raymarching
240
+ float t = 0.0f;
241
+ float h = 0.1f;
242
+ for (int i = 0; i < 48; i++) {
243
+ if (h < 0.0001f || t > 4.7f) break;
244
+ h = scene_dist(origin + ray * t);
245
+ t += h;
246
+ }
247
+
248
+ // Background
249
+ float3 color = mix(
250
+ hsv2rgb_smooth(0.5f + u.time * 0.02f, 0.35f, 0.4f),
251
+ hsv2rgb_smooth(-0.5f + u.time * 0.02f, 0.35f, 0.7f),
252
+ q.y
253
+ );
254
+
255
+ if (h < 0.001f) {
256
+ float3 hit = origin + ray * t;
257
+ float3 n = calc_normal(hit, ray, t, resolution.x);
258
+
259
+ float3 V = normalize(origin - hit);
260
+ float3 H = normalize(L + V);
261
+ float3 R = normalize(n * 2.0f * dot(n, L) - L);
262
+
263
+ float3 tang, binorm;
264
+ basis(n, &tang, &binorm);
265
+
266
+ color = compute_brdf(n, L, H, R, tang, binorm);
267
+
268
+ // Shadows
269
+ float shadow = 1.0f;
270
+ float j = 0.0f;
271
+ for (int i = 0; i < 20; i++) {
272
+ j += 0.02f;
273
+ shadow = min(shadow, scene_dist(hit + L * j) / j);
274
+ }
275
+ }
276
+
277
+ // Vignette
278
+ float vignette = pow(16.0f * q.x * q.y * (1.0f - q.x) * (1.0f - q.y), 0.16f);
279
+ color = color * vignette;
280
+
281
+ return color;
282
+ MSL
283
+ end
284
+ end
285
+
286
+ # Initialize display
287
+ window = RBGL::GUI::Window.new(width: WIDTH, height: HEIGHT, title: "Teapot Metal (GPU)")
288
+
289
+ puts "Teapot Metal - GPU Compute Shader"
290
+ puts "Original: https://www.shadertoy.com/view/MdKcDw"
291
+ puts "License: Creative Commons Attribution-NonCommercial-ShareAlike 3.0"
292
+ puts "https://creativecommons.org/licenses/by-nc-sa/3.0/deed.en"
293
+ puts "Move mouse to rotate camera"
294
+ puts "Press 'q' or Escape to quit"
295
+
296
+ # Check Metal availability
297
+ unless window.metal_available?
298
+ puts "Metal compute is NOT available"
299
+ exit 1
300
+ end
301
+
302
+ puts "Metal compute is available!"
303
+
304
+ start_time = Time.now
305
+ frame_count = 0
306
+ last_fps_time = start_time
307
+ running = true
308
+ mouse_x = 0.0
309
+ mouse_y = 0.0
310
+
311
+ while running && !window.should_close?
312
+ time = Time.now - start_time
313
+
314
+ begin
315
+ shader.render_metal(window.native_handle, WIDTH, HEIGHT, { time: time, mouse: [mouse_x, mouse_y] })
316
+ rescue => e
317
+ puts "Error: #{e.message}"
318
+ puts e.backtrace.first(10).join("\n")
319
+ running = false
320
+ break
321
+ end
322
+
323
+ events = window.poll_events_raw
324
+ events.each do |e|
325
+ case e[:type]
326
+ when :key_press
327
+ running = false if e[:key] == 12 || e[:key] == "q"
328
+ when :mouse_move
329
+ mouse_x = e[:x].to_f
330
+ mouse_y = e[:y].to_f
331
+ end
332
+ end
333
+
334
+ frame_count += 1
335
+ now = Time.now
336
+ if now - last_fps_time >= 1.0
337
+ fps = frame_count / (now - last_fps_time)
338
+ puts "FPS: #{fps.round(1)}"
339
+ frame_count = 0
340
+ last_fps_time = now
341
+ end
342
+ end
343
+
344
+ window.close