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 +37 -0
- sdforge/api.py +554 -0
- sdforge/glsl/scene/camera.glsl +25 -0
- sdforge/glsl/scene/lighting.glsl +24 -0
- sdforge/glsl/scene/raymarching.glsl +25 -0
- sdforge/glsl/sdf/operations.glsl +40 -0
- sdforge/glsl/sdf/primitives.glsl +79 -0
- sdforge/glsl/sdf/transforms.glsl +46 -0
- sdforge/mesh.py +95 -0
- sdforge/render.py +415 -0
- sdforge-0.1.0.dist-info/METADATA +235 -0
- sdforge-0.1.0.dist-info/RECORD +15 -0
- sdforge-0.1.0.dist-info/WHEEL +5 -0
- sdforge-0.1.0.dist-info/licenses/LICENSE +21 -0
- sdforge-0.1.0.dist-info/top_level.txt +1 -0
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
|
+
}
|