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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE +21 -0
- data/README.md +123 -0
- data/Rakefile +12 -0
- data/examples/array_test.rb +99 -0
- data/examples/chemical_heartbeat.rb +166 -0
- data/examples/color_test.rb +61 -0
- data/examples/cube_spinning.rb +87 -0
- data/examples/dark_transit.rb +166 -0
- data/examples/fractured_orb.rb +428 -0
- data/examples/fractured_orb_rb.rb +598 -0
- data/examples/gradient.rb +84 -0
- data/examples/hexagonal_flow.rb +333 -0
- data/examples/multi_return_test.rb +98 -0
- data/examples/plasma.rb +78 -0
- data/examples/sphere_raymarch.rb +126 -0
- data/examples/teapot.rb +362 -0
- data/examples/teapot_mcu.rb +344 -0
- data/examples/triangle_basic.rb +36 -0
- data/examples/triangle_window.rb +62 -0
- data/examples/window_test.rb +36 -0
- data/lib/rbgl/engine/buffer.rb +160 -0
- data/lib/rbgl/engine/context.rb +157 -0
- data/lib/rbgl/engine/framebuffer.rb +115 -0
- data/lib/rbgl/engine/pipeline.rb +35 -0
- data/lib/rbgl/engine/rasterizer.rb +213 -0
- data/lib/rbgl/engine/shader.rb +324 -0
- data/lib/rbgl/engine/texture.rb +125 -0
- data/lib/rbgl/engine.rb +15 -0
- data/lib/rbgl/gui/backend.rb +76 -0
- data/lib/rbgl/gui/cocoa/backend.rb +121 -0
- data/lib/rbgl/gui/event.rb +34 -0
- data/lib/rbgl/gui/file_backend.rb +91 -0
- data/lib/rbgl/gui/wayland/backend.rb +126 -0
- data/lib/rbgl/gui/wayland/connection.rb +331 -0
- data/lib/rbgl/gui/window.rb +148 -0
- data/lib/rbgl/gui/x11/backend.rb +156 -0
- data/lib/rbgl/gui/x11/connection.rb +344 -0
- data/lib/rbgl/gui.rb +16 -0
- data/lib/rbgl/version.rb +5 -0
- data/lib/rbgl.rb +6 -0
- metadata +114 -0
data/examples/teapot.rb
ADDED
|
@@ -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
|