sdforge 0.1.0__py3-none-any.whl

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.
sdforge/__init__.py ADDED
@@ -0,0 +1,37 @@
1
+ from .api import (
2
+ # Base class (for type hinting or extension)
3
+ SDFObject,
4
+
5
+ # Primitives
6
+ sphere,
7
+ box,
8
+ rounded_box,
9
+ cylinder,
10
+ torus,
11
+ capsule,
12
+ cone,
13
+ plane,
14
+ hex_prism,
15
+ octahedron,
16
+ ellipsoid,
17
+
18
+ # Custom GLSL
19
+ Forge,
20
+
21
+ # Camera
22
+ Camera,
23
+
24
+ # Light
25
+ Light,
26
+
27
+ # Constants
28
+ X, Y, Z,
29
+ )
30
+
31
+ # By attaching render and save to the base class, any created object
32
+ # can call them directly, e.g., sphere(1).render()
33
+ from .render import render
34
+ from .mesh import save
35
+ from .api import SDFObject
36
+ SDFObject.render = render
37
+ SDFObject.save = save
sdforge/api.py ADDED
@@ -0,0 +1,554 @@
1
+ import numpy as np
2
+ import uuid
3
+ from functools import reduce, lru_cache
4
+ from pathlib import Path
5
+ import atexit
6
+
7
+ # --- Constants ---
8
+ X = np.array([1, 0, 0])
9
+ Y = np.array([0, 1, 0])
10
+ Z = np.array([0, 0, 1])
11
+
12
+ # --- Optional GPU Dependency Check for Forge ---
13
+ _MODERNGL_AVAILABLE = False
14
+ try:
15
+ import moderngl
16
+ import glfw
17
+ _MODERNGL_AVAILABLE = True
18
+ except ImportError:
19
+ pass
20
+
21
+
22
+ # --- GLSL File Loader Utility ---
23
+ @lru_cache(maxsize=None)
24
+ def _get_glsl_content(filename: str) -> str:
25
+ """Cached reader for GLSL library files."""
26
+ glsl_dir = Path(__file__).parent / 'glsl' / 'sdf'
27
+ try:
28
+ with open(glsl_dir / filename, 'r') as f:
29
+ return f.read()
30
+ except FileNotFoundError:
31
+ return ""
32
+
33
+ # --- Helper for formatting GLSL parameters ---
34
+ def _glsl_format(val):
35
+ """Formats a Python value for injection into a GLSL string."""
36
+ if isinstance(val, str):
37
+ return val # Assume it's a raw GLSL expression
38
+ return f"{float(val)}"
39
+
40
+
41
+ # --- Camera ---
42
+
43
+ class Camera:
44
+ """
45
+ Represents a camera in the scene, allowing for static or animated positioning.
46
+ """
47
+ def __init__(self, position=(5, 4, 5), target=(0, 0, 0), zoom=1.0):
48
+ """
49
+ Initializes the camera.
50
+
51
+ Args:
52
+ position (tuple, optional): The position of the camera in 3D space.
53
+ Components can be numbers or GLSL expressions (str).
54
+ Defaults to (5, 4, 5).
55
+ target (tuple, optional): The point the camera is looking at.
56
+ Components can be numbers or GLSL expressions (str).
57
+ Defaults to (0, 0, 0).
58
+ zoom (float or str, optional): The zoom level. Defaults to 1.0.
59
+ """
60
+ self.position = position
61
+ self.target = target
62
+ self.zoom = zoom
63
+
64
+
65
+ # --- Light ---
66
+
67
+ class Light:
68
+ """
69
+ Represents lighting and shadow properties for the scene.
70
+ """
71
+ def __init__(self, position=None, ambient_strength=0.1, shadow_softness=8.0, ao_strength=3.0):
72
+ """
73
+ Initializes the scene lighting.
74
+
75
+ Args:
76
+ position (tuple, optional): The position of the light source.
77
+ Components can be numbers or GLSL expressions (str).
78
+ If None, the light is positioned at the camera (headlight).
79
+ Defaults to None.
80
+ ambient_strength (float or str, optional): The minimum brightness for surfaces. Defaults to 0.1.
81
+ shadow_softness (float or str, optional): How soft the shadows are. Higher is softer. Defaults to 8.0.
82
+ ao_strength (float or str, optional): Strength of ambient occlusion. Defaults to 3.0.
83
+ """
84
+ self.position = position
85
+ self.ambient_strength = ambient_strength
86
+ self.shadow_softness = shadow_softness
87
+ self.ao_strength = ao_strength
88
+
89
+
90
+ # --- Base Class ---
91
+
92
+ class SDFObject:
93
+ """Base class for all SDF objects, defining the core interface."""
94
+ def __init__(self):
95
+ self.uuid = uuid.uuid4()
96
+
97
+ def to_glsl(self) -> str: raise NotImplementedError
98
+ def to_callable(self): raise NotImplementedError
99
+ def get_glsl_definitions(self) -> list: return []
100
+ def _collect_materials(self, materials): pass
101
+ def __or__(self, other): return Union(self, other)
102
+ def __and__(self, other): return Intersection(self, other)
103
+ def __sub__(self, other): return Difference(self, other)
104
+ def translate(self, offset): return Translate(self, np.array(offset))
105
+ def scale(self, factor): return Scale(self, factor)
106
+ def orient(self, axis): return Orient(self, np.array(axis))
107
+ def rotate(self, axis, angle): return Rotate(self, np.array(axis), angle)
108
+ def twist(self, k): return Twist(self, k)
109
+ def repeat(self, spacing): return Repeat(self, np.array(spacing))
110
+ def mirror(self, axes): return Mirror(self, np.array(axes))
111
+ def smooth_union(self, other, k): return SmoothUnion(self, other, k)
112
+ def smooth_intersection(self, other, k): return SmoothIntersection(self, other, k)
113
+ def smooth_difference(self, other, k): return SmoothDifference(self, other, k)
114
+ def color(self, r, g, b): return Material(self, (r, g, b))
115
+
116
+
117
+ # --- Primitives ---
118
+
119
+ class Sphere(SDFObject):
120
+ def __init__(self, r=1.0):
121
+ super().__init__()
122
+ self.r = r
123
+ def to_glsl(self) -> str: return f"vec4(sdSphere(p, {_glsl_format(self.r)}), -1.0, 0.0, 0.0)"
124
+ def to_callable(self):
125
+ if isinstance(self.r, str): raise TypeError("Cannot save mesh of an object with animated (string) parameters.")
126
+ return lambda p: np.linalg.norm(p, axis=-1) - self.r
127
+
128
+ def sphere(r=1.0) -> SDFObject: return Sphere(r)
129
+
130
+ class Box(SDFObject):
131
+ def __init__(self, size=1.0):
132
+ super().__init__()
133
+ if isinstance(size, (int, float, str)): size = (size, size, size)
134
+ self.size = size
135
+ def to_glsl(self) -> str:
136
+ s = []
137
+ for v in self.size:
138
+ if isinstance(v, str): s.append(f"({v})")
139
+ else: s.append(_glsl_format(v / 2.0))
140
+ return f"vec4(sdBox(p, vec3({s[0]}, {s[1]}, {s[2]})), -1.0, 0.0, 0.0)"
141
+ def to_callable(self):
142
+ if any(isinstance(v, str) for v in self.size): raise TypeError("Cannot save mesh of an object with animated (string) parameters.")
143
+ size_arr = np.array(self.size)
144
+ def _callable(p):
145
+ q = np.abs(p) - size_arr / 2.0
146
+ return np.linalg.norm(np.maximum(q, 0), axis=-1) + np.minimum(np.max(q, axis=-1), 0)
147
+ return _callable
148
+
149
+ def box(size=1.0) -> SDFObject: return Box(size)
150
+
151
+ class RoundedBox(SDFObject):
152
+ def __init__(self, size=1.0, radius=0.1):
153
+ super().__init__()
154
+ if isinstance(size, (int, float, str)): size = (size, size, size)
155
+ self.size, self.radius = size, radius
156
+ def to_glsl(self) -> str:
157
+ s = []
158
+ for v in self.size:
159
+ if isinstance(v, str): s.append(f"({v})")
160
+ else: s.append(_glsl_format(v / 2.0))
161
+ r = _glsl_format(self.radius)
162
+ return f"vec4(sdRoundedBox(p, vec3({s[0]}, {s[1]}, {s[2]}), {r}), -1.0, 0.0, 0.0)"
163
+ def to_callable(self):
164
+ if any(isinstance(v, str) for v in self.size) or isinstance(self.radius, str):
165
+ raise TypeError("Cannot save mesh of an object with animated (string) parameters.")
166
+ size_arr = np.array(self.size)
167
+ def _callable(p):
168
+ q = np.abs(p) - size_arr / 2.0
169
+ return np.linalg.norm(np.maximum(q, 0), axis=-1) - self.radius
170
+ return _callable
171
+
172
+ def rounded_box(size=1.0, radius=0.1) -> SDFObject: return RoundedBox(size, radius)
173
+
174
+ class Torus(SDFObject):
175
+ def __init__(self, major=1.0, minor=0.25):
176
+ super().__init__()
177
+ self.major, self.minor = major, minor
178
+ def to_glsl(self) -> str: return f"vec4(sdTorus(p, vec2({_glsl_format(self.major)}, {_glsl_format(self.minor)})), -1.0, 0.0, 0.0)"
179
+ def to_callable(self):
180
+ if isinstance(self.major, str) or isinstance(self.minor, str):
181
+ raise TypeError("Cannot save mesh of an object with animated (string) parameters.")
182
+ def _callable(p):
183
+ q = np.array([np.linalg.norm(p[:, [0, 2]], axis=-1) - self.major, p[:, 1]]).T
184
+ return np.linalg.norm(q, axis=-1) - self.minor
185
+ return _callable
186
+
187
+ def torus(major=1.0, minor=0.25) -> SDFObject: return Torus(major, minor)
188
+
189
+ class Capsule(SDFObject):
190
+ def __init__(self, a, b, radius=0.1):
191
+ super().__init__()
192
+ self.a, self.b, self.radius = np.array(a), np.array(b), radius
193
+ def to_glsl(self) -> str:
194
+ a, b, r = self.a, self.b, _glsl_format(self.radius)
195
+ return f"vec4(sdCapsule(p, vec3({a[0]},{a[1]},{a[2]}), vec3({b[0]},{b[1]},{b[2]}), {r}), -1.0, 0.0, 0.0)"
196
+ def to_callable(self):
197
+ if isinstance(self.radius, str): raise TypeError("Cannot save mesh of an object with animated (string) parameters.")
198
+ def _callable(p):
199
+ pa = p - self.a; ba = self.b - self.a
200
+ h = np.clip(np.dot(pa, ba) / np.dot(ba, ba), 0.0, 1.0)
201
+ return np.linalg.norm(pa - ba * h[:, np.newaxis], axis=-1) - self.radius
202
+ return _callable
203
+
204
+ def capsule(a, b, radius=0.1) -> SDFObject: return Capsule(a, b, radius)
205
+
206
+ class Cylinder(SDFObject):
207
+ def __init__(self, radius=0.5, height=1.0):
208
+ super().__init__()
209
+ self.radius, self.height = radius, height
210
+ def to_glsl(self) -> str:
211
+ r = _glsl_format(self.radius)
212
+ h = _glsl_format(self.height / 2.0) if not isinstance(self.height, str) else f"({self.height})/2.0"
213
+ return f"vec4(sdCylinder(p, vec2({r}, {h})), -1.0, 0.0, 0.0)"
214
+ def to_callable(self):
215
+ if isinstance(self.radius, str) or isinstance(self.height, str):
216
+ raise TypeError("Cannot save mesh of an object with animated (string) parameters.")
217
+ def _callable(p):
218
+ r, h_half = self.radius, self.height / 2.0
219
+ d = np.abs(np.array([np.linalg.norm(p[:, [0, 2]], axis=-1), p[:, 1]]).T) - np.array([r, h_half])
220
+ return np.minimum(np.maximum(d[:, 0], d[:, 1]), 0.0) + np.linalg.norm(np.maximum(d, 0.0), axis=-1)
221
+ return _callable
222
+
223
+ def cylinder(radius=0.5, height=1.0) -> SDFObject: return Cylinder(radius, height)
224
+
225
+ class Cone(SDFObject):
226
+ def __init__(self, height=1.0, radius=0.5):
227
+ super().__init__()
228
+ self.height, self.radius = height, radius
229
+ def to_glsl(self) -> str: return f"vec4(sdCone(p, vec2({_glsl_format(self.height)}, {_glsl_format(self.radius)})), -1.0, 0.0, 0.0)"
230
+ def to_callable(self):
231
+ if isinstance(self.height, str) or isinstance(self.radius, str):
232
+ raise TypeError("Cannot save mesh of an object with animated (string) parameters.")
233
+ def _callable(p):
234
+ c = np.array([self.height, self.radius])
235
+ q = np.array([np.linalg.norm(p[:, [0, 2]], axis=-1), p[:, 1]]).T; a = c - q; b = q - c * np.array([1, -1])
236
+ k = np.sign(c[1]); d = np.minimum(np.sum(a*a, axis=-1), np.sum(b*b, axis=-1))
237
+ s = np.maximum(k * (q[:,0]*c[1] - q[:,1]*c[0]), k * (q[:,1] - c[1]))
238
+ return np.sqrt(d) * np.sign(s)
239
+ return _callable
240
+
241
+ def cone(height=1.0, radius=0.5) -> SDFObject: return Cone(height, radius)
242
+
243
+ class Plane(SDFObject):
244
+ def __init__(self, normal=Y, offset=0):
245
+ super().__init__()
246
+ self.normal, self.offset = np.array(normal), offset
247
+ def to_glsl(self) -> str: n = self.normal; return f"vec4(sdPlane(p, vec4({n[0]}, {n[1]}, {n[2]}, {_glsl_format(self.offset)})), -1.0, 0.0, 0.0)"
248
+ def to_callable(self):
249
+ if isinstance(self.offset, str): raise TypeError("Cannot save mesh of an object with animated (string) parameters.")
250
+ return lambda p: np.dot(p, self.normal) + self.offset
251
+
252
+ def plane(normal=Y, offset=0) -> SDFObject: return Plane(normal, offset)
253
+
254
+ class HexPrism(SDFObject):
255
+ def __init__(self, radius=1.0, height=0.1):
256
+ super().__init__()
257
+ self.radius, self.height = radius, height
258
+ def to_glsl(self) -> str: return f"vec4(sdHexPrism(p, vec2({_glsl_format(self.radius)}, {_glsl_format(self.height)})), -1.0, 0.0, 0.0)"
259
+ def to_callable(self):
260
+ if isinstance(self.radius, str) or isinstance(self.height, str):
261
+ raise TypeError("Cannot save mesh of an object with animated (string) parameters.")
262
+ raise NotImplementedError("HexPrism for mesh generation is not yet implemented.")
263
+
264
+ def hex_prism(radius=1.0, height=0.1) -> SDFObject: return HexPrism(radius, height)
265
+
266
+ class Octahedron(SDFObject):
267
+ def __init__(self, size=1.0):
268
+ super().__init__()
269
+ self.size = size
270
+ def to_glsl(self) -> str: return f"vec4(sdOctahedron(p, {_glsl_format(self.size)}), -1.0, 0.0, 0.0)"
271
+ def to_callable(self):
272
+ if isinstance(self.size, str): raise TypeError("Cannot save mesh of an object with animated (string) parameters.")
273
+ return lambda p: (np.sum(np.abs(p), axis=-1) - self.size) * 0.57735027
274
+
275
+ def octahedron(size=1.0) -> SDFObject: return Octahedron(size)
276
+
277
+ class Ellipsoid(SDFObject):
278
+ def __init__(self, radii):
279
+ super().__init__()
280
+ self.radii = radii
281
+ def to_glsl(self) -> str:
282
+ r = [_glsl_format(v) for v in self.radii]
283
+ return f"vec4(sdEllipsoid(p, vec3({r[0]}, {r[1]}, {r[2]})), -1.0, 0.0, 0.0)"
284
+ def to_callable(self):
285
+ if any(isinstance(v, str) for v in self.radii):
286
+ raise TypeError("Cannot save mesh of an object with animated (string) parameters.")
287
+ radii_arr = np.array(self.radii)
288
+ def _callable(p):
289
+ k0 = np.linalg.norm(p / radii_arr, axis=-1)
290
+ k1 = np.linalg.norm(p / (radii_arr * radii_arr), axis=-1)
291
+ return k0 * (k0 - 1.0) / (k1 + 1e-9)
292
+ return _callable
293
+
294
+ def ellipsoid(radii) -> SDFObject: return Ellipsoid(radii)
295
+
296
+
297
+ # --- Material ---
298
+
299
+ class Material(SDFObject):
300
+ def __init__(self, child, color):
301
+ super().__init__()
302
+ self.child = child
303
+ self.color = color
304
+ self.material_id = -1 # Will be set by the renderer
305
+
306
+ def to_glsl(self) -> str:
307
+ child_glsl = self.child.to_glsl()
308
+ # The child returns a vec4. We need to overwrite the material ID.
309
+ return f"vec4(({child_glsl}).x, {float(self.material_id)}, 0.0, 0.0)"
310
+
311
+ def to_callable(self):
312
+ # Materials are a render-time concept; for mesh generation, we use the child's shape.
313
+ return self.child.to_callable()
314
+
315
+ def _collect_materials(self, materials):
316
+ if self not in materials:
317
+ self.material_id = len(materials)
318
+ materials.append(self)
319
+ self.child._collect_materials(materials)
320
+
321
+ def get_glsl_definitions(self) -> list:
322
+ return self.child.get_glsl_definitions()
323
+
324
+
325
+ # --- Standard Operations ---
326
+
327
+ class Union(SDFObject):
328
+ def __init__(self, *children):
329
+ super().__init__()
330
+ self.children = children
331
+ def to_glsl(self) -> str: return reduce(lambda a, b: f"opU({a}, {b})", [c.to_glsl() for c in self.children])
332
+ def to_callable(self):
333
+ callables = [c.to_callable() for c in self.children]; return lambda p: reduce(np.minimum, [c(p) for c in callables])
334
+ def get_glsl_definitions(self) -> list: return [_get_glsl_content('operations.glsl')] + sum([c.get_glsl_definitions() for c in self.children], [])
335
+ def _collect_materials(self, materials):
336
+ for c in self.children: c._collect_materials(materials)
337
+
338
+ class Intersection(SDFObject):
339
+ def __init__(self, *children):
340
+ super().__init__()
341
+ self.children = children
342
+ def to_glsl(self) -> str: return reduce(lambda a, b: f"opI({a}, {b})", [c.to_glsl() for c in self.children])
343
+ def to_callable(self):
344
+ callables = [c.to_callable() for c in self.children]; return lambda p: reduce(np.maximum, [c(p) for c in callables])
345
+ def get_glsl_definitions(self) -> list: return [_get_glsl_content('operations.glsl')] + sum([c.get_glsl_definitions() for c in self.children], [])
346
+ def _collect_materials(self, materials):
347
+ for c in self.children: c._collect_materials(materials)
348
+
349
+ class Difference(SDFObject):
350
+ def __init__(self, a, b):
351
+ super().__init__()
352
+ self.a, self.b = a, b
353
+ def to_glsl(self) -> str: return f"opS({self.a.to_glsl()}, {self.b.to_glsl()})"
354
+ def to_callable(self):
355
+ a_call, b_call = self.a.to_callable(), self.b.to_callable(); return lambda p: np.maximum(a_call(p), -b_call(p))
356
+ def get_glsl_definitions(self) -> list: return [_get_glsl_content('operations.glsl')] + self.a.get_glsl_definitions() + self.b.get_glsl_definitions()
357
+ def _collect_materials(self, materials):
358
+ self.a._collect_materials(materials)
359
+ self.b._collect_materials(materials)
360
+
361
+
362
+ # --- Smooth Operations ---
363
+
364
+ class SmoothUnion(SDFObject):
365
+ def __init__(self, a, b, k):
366
+ super().__init__()
367
+ self.a, self.b, self.k = a, b, k
368
+ def to_glsl(self) -> str: return f"sUnion({self.a.to_glsl()}, {self.b.to_glsl()}, {_glsl_format(self.k)})"
369
+ def to_callable(self):
370
+ if isinstance(self.k, str): raise TypeError("Cannot save mesh of an object with animated (string) parameters.")
371
+ a_call, b_call, k = self.a.to_callable(), self.b.to_callable(), float(self.k)
372
+ def _callable(p):
373
+ d1, d2 = a_call(p), b_call(p); h = np.clip(0.5 + 0.5 * (d2 - d1) / k, 0.0, 1.0)
374
+ return d2 * (1.0 - h) + d1 * h - k * h * (1.0 - h)
375
+ return _callable
376
+ def get_glsl_definitions(self) -> list: return [_get_glsl_content('operations.glsl')] + self.a.get_glsl_definitions() + self.b.get_glsl_definitions()
377
+ def _collect_materials(self, materials):
378
+ self.a._collect_materials(materials)
379
+ self.b._collect_materials(materials)
380
+
381
+ class SmoothIntersection(SDFObject):
382
+ def __init__(self, a, b, k):
383
+ super().__init__()
384
+ self.a, self.b, self.k = a, b, k
385
+ def to_glsl(self) -> str: return f"sIntersect({self.a.to_glsl()}, {self.b.to_glsl()}, {_glsl_format(self.k)})"
386
+ def to_callable(self):
387
+ if isinstance(self.k, str): raise TypeError("Cannot save mesh of an object with animated (string) parameters.")
388
+ raise NotImplementedError("Smooth Intersection for mesh generation is not yet implemented.")
389
+ def get_glsl_definitions(self) -> list: return [_get_glsl_content('operations.glsl')] + self.a.get_glsl_definitions() + self.b.get_glsl_definitions()
390
+ def _collect_materials(self, materials):
391
+ self.a._collect_materials(materials)
392
+ self.b._collect_materials(materials)
393
+
394
+ class SmoothDifference(SDFObject):
395
+ def __init__(self, a, b, k):
396
+ super().__init__()
397
+ self.a, self.b, self.k = a, b, k
398
+ def to_glsl(self) -> str: return f"sDifference({self.a.to_glsl()}, {self.b.to_glsl()}, {_glsl_format(self.k)})"
399
+ def to_callable(self):
400
+ if isinstance(self.k, str): raise TypeError("Cannot save mesh of an object with animated (string) parameters.")
401
+ raise NotImplementedError("Smooth Difference for mesh generation is not yet implemented.")
402
+ def get_glsl_definitions(self) -> list: return [_get_glsl_content('operations.glsl')] + self.a.get_glsl_definitions() + self.b.get_glsl_definitions()
403
+ def _collect_materials(self, materials):
404
+ self.a._collect_materials(materials)
405
+ self.b._collect_materials(materials)
406
+
407
+
408
+ # --- Basic Transformations ---
409
+
410
+ class Translate(SDFObject):
411
+ def __init__(self, child, offset):
412
+ super().__init__()
413
+ self.child, self.offset = child, offset
414
+ def to_glsl(self) -> str: o = self.offset; return self.child.to_glsl().replace('p', f'(p - vec3({o[0]}, {o[1]}, {o[2]}))')
415
+ def to_callable(self): child_call = self.child.to_callable(); return lambda p: child_call(p - self.offset)
416
+ def get_glsl_definitions(self) -> list: return self.child.get_glsl_definitions()
417
+ def _collect_materials(self, materials): self.child._collect_materials(materials)
418
+
419
+ class Scale(SDFObject):
420
+ def __init__(self, child, factor):
421
+ super().__init__()
422
+ self.child, self.factor = child, factor
423
+ def to_glsl(self) -> str:
424
+ f = _glsl_format(self.factor)
425
+ # Use an IIFE to avoid evaluating the child GLSL twice
426
+ return f"(() {{ vec4 res = {self.child.to_glsl().replace('p', f'(p / ({f}))')}; res.x *= ({f}); return res; }})()"
427
+ def to_callable(self):
428
+ if isinstance(self.factor, str): raise TypeError("Cannot save mesh of an object with animated (string) parameters.")
429
+ child_call = self.child.to_callable(); return lambda p: child_call(p / self.factor) * self.factor
430
+ def get_glsl_definitions(self) -> list: return self.child.get_glsl_definitions()
431
+ def _collect_materials(self, materials): self.child._collect_materials(materials)
432
+
433
+ class Orient(SDFObject):
434
+ def __init__(self, child, axis):
435
+ super().__init__()
436
+ self.child, self.axis = child, axis
437
+ def to_glsl(self) -> str:
438
+ if np.allclose(self.axis, X): return self.child.to_glsl().replace('p', 'p.zyx')
439
+ elif np.allclose(self.axis, Y): return self.child.to_glsl().replace('p', 'p.xzy')
440
+ return self.child.to_glsl()
441
+ def to_callable(self):
442
+ child_call = self.child.to_callable()
443
+ if np.allclose(self.axis, X): return lambda p: child_call(p[:, [2, 1, 0]])
444
+ elif np.allclose(self.axis, Y): return lambda p: child_call(p[:, [0, 2, 1]])
445
+ return child_call
446
+ def get_glsl_definitions(self) -> list: return self.child.get_glsl_definitions()
447
+ def _collect_materials(self, materials): self.child._collect_materials(materials)
448
+
449
+
450
+ # --- Advanced Transformations ---
451
+
452
+ class Rotate(SDFObject):
453
+ def __init__(self, child, axis, angle):
454
+ super().__init__()
455
+ self.child, self.axis, self.angle = child, axis, angle
456
+ def to_glsl(self) -> str:
457
+ if np.allclose(self.axis, X): func = 'opRotateX'
458
+ elif np.allclose(self.axis, Y): func = 'opRotateY'
459
+ else: func = 'opRotateZ'
460
+ return self.child.to_glsl().replace('p', f"{func}(p, {_glsl_format(self.angle)})")
461
+ def to_callable(self):
462
+ if isinstance(self.angle, str): raise TypeError("Cannot save mesh of an object with animated (string) parameters.")
463
+ child_call, angle = self.child.to_callable(), self.angle; c, s = np.cos(angle), np.sin(angle)
464
+ if np.allclose(self.axis, X): R = np.array([[1, 0, 0], [0, c, -s], [0, s, c]])
465
+ elif np.allclose(self.axis, Y): R = np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]])
466
+ else: R = np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])
467
+ return lambda p: child_call(p @ R.T)
468
+ def get_glsl_definitions(self) -> list: return [_get_glsl_content('transforms.glsl')] + self.child.get_glsl_definitions()
469
+ def _collect_materials(self, materials): self.child._collect_materials(materials)
470
+
471
+ class Twist(SDFObject):
472
+ def __init__(self, child, k):
473
+ super().__init__()
474
+ self.child, self.k = child, k
475
+ def to_glsl(self) -> str:
476
+ k_str = _glsl_format(self.k)
477
+ # The opTwist function now modifies p in place and returns the child's result
478
+ return f"opTwist({self.child.to_glsl()}, p, {k_str})"
479
+ def to_callable(self):
480
+ if isinstance(self.k, str): raise TypeError("Cannot save mesh of an object with animated (string) parameters.")
481
+ child_call, k = self.child.to_callable(), float(self.k)
482
+ def _callable(p):
483
+ c, s = np.cos(k * p[:, 1]), np.sin(k * p[:, 1])
484
+ x_new, z_new = p[:, 0] * c - p[:, 2] * s, p[:, 0] * s + p[:, 2] * c
485
+ q = np.stack([x_new, p[:, 1], z_new], axis=-1); return child_call(q)
486
+ return _callable
487
+ def get_glsl_definitions(self) -> list:
488
+ return [_get_glsl_content('transforms.glsl')] + self.child.get_glsl_definitions()
489
+ def _collect_materials(self, materials): self.child._collect_materials(materials)
490
+
491
+ class Repeat(SDFObject):
492
+ def __init__(self, child, spacing):
493
+ super().__init__()
494
+ self.child, self.spacing = child, spacing
495
+ def to_glsl(self) -> str: s = self.spacing; return self.child.to_glsl().replace('p', f"opRepeat(p, vec3({s[0]}, {s[1]}, {s[2]}))")
496
+ def to_callable(self):
497
+ child_call, s = self.child.to_callable(), self.spacing
498
+ active_spacing = np.where(s == 0, np.inf, s)
499
+ def _callable(p): return child_call(np.mod(p + 0.5 * active_spacing, active_spacing) - 0.5 * active_spacing)
500
+ return _callable
501
+ def get_glsl_definitions(self) -> list: return [_get_glsl_content('transforms.glsl')] + self.child.get_glsl_definitions()
502
+ def _collect_materials(self, materials): self.child._collect_materials(materials)
503
+
504
+ class Mirror(SDFObject):
505
+ def __init__(self, child, axes):
506
+ super().__init__()
507
+ self.child, self.axes = child, axes
508
+ def to_glsl(self) -> str: a = self.axes; return self.child.to_glsl().replace('p', f"opMirror(p, vec3({a[0]}, {a[1]}, {a[2]}))")
509
+ def to_callable(self):
510
+ child_call = self.child.to_callable(); a = self.axes
511
+ def _callable(p):
512
+ q = p.copy()
513
+ if a[0] > 0.5: q[:,0] = np.abs(q[:,0])
514
+ if a[1] > 0.5: q[:,1] = np.abs(q[:,1])
515
+ if a[2] > 0.5: q[:,2] = np.abs(q[:,2])
516
+ return child_call(q)
517
+ return _callable
518
+ def get_glsl_definitions(self) -> list: return [_get_glsl_content('transforms.glsl')] + self.child.get_glsl_definitions()
519
+ def _collect_materials(self, materials): self.child._collect_materials(materials)
520
+
521
+
522
+ # --- Custom GLSL ---
523
+
524
+ class Forge(SDFObject):
525
+ def __init__(self, glsl_code_body: str):
526
+ super().__init__()
527
+ self.glsl_code_body = glsl_code_body
528
+ self.unique_id = "forge_func_" + uuid.uuid4().hex[:8]
529
+ def to_glsl(self) -> str: return f"vec4({self.unique_id}(p), -1.0, 0.0, 0.0)"
530
+ def get_glsl_definitions(self) -> list:
531
+ return [f"float {self.unique_id}(vec3 p){{ {self.glsl_code_body} }}"]
532
+ def to_callable(self):
533
+ if not _MODERNGL_AVAILABLE: raise ImportError("To save meshes with Forge objects, 'moderngl' and 'glfw' are required.")
534
+ cls = self.__class__
535
+ if not hasattr(cls, '_mgl_context'):
536
+ if not glfw.init(): raise RuntimeError("glfw.init() failed")
537
+ atexit.register(glfw.terminate)
538
+ glfw.window_hint(glfw.VISIBLE, False); win = glfw.create_window(1, 1, "", None, None)
539
+ glfw.make_context_current(win); cls._mgl_context = moderngl.create_context(require=430)
540
+ ctx = cls._mgl_context
541
+ compute_shader = ctx.compute_shader(f"""
542
+ #version 430
543
+ layout(local_size_x=256, local_size_y=1, local_size_z=1) in;
544
+ layout(std430, binding=0) buffer points {{ vec3 p[]; }};
545
+ layout(std430, binding=1) buffer distances {{ float d[]; }};
546
+ {self.get_glsl_definitions()[0]}
547
+ void main() {{ uint gid = gl_GlobalInvocationID.x; d[gid] = {self.to_glsl().replace('p', 'p[gid]').replace('vec4', '').strip('()').split(',')[0]}; }}""")
548
+ def _gpu_evaluator(points_np):
549
+ points_np = np.array(points_np, dtype='f4'); num_points = len(points_np)
550
+ point_buffer = ctx.buffer(points_np.tobytes()); dist_buffer = ctx.buffer(reserve=num_points * 4)
551
+ point_buffer.bind_to_storage_buffer(0); dist_buffer.bind_to_storage_buffer(1)
552
+ group_size = (num_points + 255) // 256; compute_shader.run(group_x=group_size)
553
+ return np.frombuffer(dist_buffer.read(), dtype='f4')
554
+ return _gpu_evaluator
@@ -0,0 +1,25 @@
1
+ vec3 getRayDir(vec2 st, vec3 ro, vec3 lookAt, float zoom) {
2
+ vec3 f = normalize(lookAt - ro);
3
+ vec3 r = normalize(cross(vec3(0,1,0), f));
4
+ vec3 u = cross(f, r);
5
+ return normalize(st.x * r + st.y * u + 1.5 * f / zoom);
6
+ }
7
+
8
+ void cameraStatic(in vec2 st, in vec3 pos, in vec3 target, in float zoom, out vec3 ro, out vec3 rd) {
9
+ ro = pos;
10
+ rd = getRayDir(st, ro, target, zoom);
11
+ }
12
+
13
+ void cameraOrbit(in vec2 st, in vec2 mouse, in vec2 resolution, in float zoom, out vec3 ro, out vec3 rd) {
14
+ vec2 mouse_norm = mouse / resolution;
15
+ float yaw = (mouse_norm.x - 0.5) * 6.28;
16
+ float pitch = (mouse_norm.y - 0.5) * 3.14;
17
+ pitch = clamp(pitch, -1.5, 1.5);
18
+
19
+ float dist = 5.0;
20
+ ro.x = dist * cos(pitch) * sin(yaw);
21
+ ro.y = dist * sin(pitch);
22
+ ro.z = dist * cos(pitch) * cos(yaw);
23
+
24
+ rd = getRayDir(st, ro, vec3(0.0), zoom);
25
+ }
@@ -0,0 +1,24 @@
1
+ float softShadow(vec3 ro, vec3 rd, float softness) {
2
+ float res = 1.0;
3
+ float t = 0.02;
4
+ for (int i = 0; i < 32; i++) {
5
+ float h = Scene(ro + rd * t).x;
6
+ if (h < 0.001) return 0.0;
7
+ res = min(res, softness * h / t);
8
+ t += h;
9
+ if (t > 10.0) break;
10
+ }
11
+ return clamp(res, 0.0, 1.0);
12
+ }
13
+
14
+ float ambientOcclusion(vec3 p, vec3 n, float strength) {
15
+ float ao = 0.0;
16
+ float sca = 1.0;
17
+ for (int i = 0; i < 5; i++) {
18
+ float h = 0.01 + 0.1 * float(i) / 4.0;
19
+ float d = Scene(p + n * h).x;
20
+ ao += -(d-h)*sca;
21
+ sca *= 0.95;
22
+ }
23
+ return clamp(1.0 - strength * ao, 0.0, 1.0);
24
+ }
@@ -0,0 +1,25 @@
1
+ vec4 Scene(in vec3 p);
2
+
3
+ vec4 raymarch(in vec3 ro, in vec3 rd) {
4
+ float t = 0.0;
5
+ for (int i = 0; i < 100; ++i) {
6
+ vec3 p = ro + rd * t;
7
+ vec4 res = Scene(p);
8
+ float d = res.x;
9
+ if (d < 0.001) return vec4(t, res.y, res.z, res.w);
10
+ t += d;
11
+ if (t > 100.0) break;
12
+ }
13
+ return vec4(-1.0);
14
+ }
15
+
16
+ vec3 estimateNormal(vec3 p) {
17
+ float eps = 0.001;
18
+ vec2 e = vec2(1.0, -1.0) * 0.5773 * eps;
19
+ return normalize(
20
+ e.xyy * Scene(p + e.xyy).x +
21
+ e.yyx * Scene(p + e.yyx).x +
22
+ e.yxy * Scene(p + e.yxy).x +
23
+ e.xxx * Scene(p + e.xxx).x
24
+ );
25
+ }