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
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// --- Standard Boolean Operations ---
|
|
2
|
+
// These operate on vec4(distance, material_id, 0, 0)
|
|
3
|
+
|
|
4
|
+
vec4 opU(vec4 a, vec4 b) {
|
|
5
|
+
return (a.x < b.x) ? a : b;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
vec4 opI(vec4 a, vec4 b) {
|
|
9
|
+
return (a.x > b.x) ? a : b;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
vec4 opS(vec4 a, vec4 b) {
|
|
13
|
+
return opI(a, vec4(-b.x, b.y, b.z, b.w));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
// --- Smooth Boolean Operations ---
|
|
18
|
+
|
|
19
|
+
vec4 sUnion(vec4 a, vec4 b, float k )
|
|
20
|
+
{
|
|
21
|
+
float h = clamp( 0.5 + 0.5*(b.x-a.x)/k, 0.0, 1.0 );
|
|
22
|
+
float dist = mix( b.x, a.x, h ) - k*h*(1.0-h);
|
|
23
|
+
// Return the material of the object that is closer at the blend point
|
|
24
|
+
return (a.x < b.x) ? vec4(dist, a.y, a.z, a.w) : vec4(dist, b.y, b.z, b.w);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
vec4 sIntersect(vec4 a, vec4 b, float k )
|
|
28
|
+
{
|
|
29
|
+
float h = clamp( 0.5 - 0.5*(b.x-a.x)/k, 0.0, 1.0 );
|
|
30
|
+
float dist = mix( b.x, a.x, h ) + k*h*(1.0-h);
|
|
31
|
+
return (a.x > b.x) ? vec4(dist, a.y, a.z, a.w) : vec4(dist, b.y, b.z, b.w);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
vec4 sDifference(vec4 a, vec4 b, float k )
|
|
35
|
+
{
|
|
36
|
+
float h = clamp( 0.5 - 0.5*(b.x+a.x)/k, 0.0, 1.0 );
|
|
37
|
+
float dist = mix( b.x, -a.x, h ) + k*h*(1.0-h);
|
|
38
|
+
// Difference is tricky, it usually inherits the material of the first object
|
|
39
|
+
return vec4(dist, a.y, a.z, a.w);
|
|
40
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// sdforge/glsl/sdf/primitives.glsl
|
|
2
|
+
|
|
3
|
+
// --- 3D Primitives ---
|
|
4
|
+
|
|
5
|
+
float sdSphere(in vec3 p, in float r) {
|
|
6
|
+
return length(p) - r;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
float sdBox(in vec3 p, in vec3 b) {
|
|
10
|
+
vec3 q = abs(p) - b;
|
|
11
|
+
return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
float sdRoundedBox(in vec3 p, in vec3 b, in float r) {
|
|
15
|
+
vec3 q = abs(p) - b;
|
|
16
|
+
return length(max(q, 0.0)) - r;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
float sdTorus(in vec3 p, in vec2 t) { // t.x=major radius, t.y=minor radius
|
|
20
|
+
vec2 q = vec2(length(p.xz) - t.x, p.y);
|
|
21
|
+
return length(q) - t.y;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
float sdCapsule(in vec3 p, in vec3 a, in vec3 b, in float r) {
|
|
25
|
+
vec3 pa = p - a, ba = b - a;
|
|
26
|
+
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
|
|
27
|
+
return length(pa - ba * h) - r;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
float sdCone(in vec3 p, in vec2 c) { // c.x=height, c.y=radius
|
|
31
|
+
vec2 q = vec2(length(p.xz), p.y);
|
|
32
|
+
vec2 a = c - q;
|
|
33
|
+
vec2 b = q - c * vec2(1, -1);
|
|
34
|
+
float k = sign(c.y);
|
|
35
|
+
float d = min(dot(a, a), dot(b, b));
|
|
36
|
+
float s = max(k * (q.x * c.y - q.y * c.x), k * (q.y - c.y));
|
|
37
|
+
return sqrt(d) * sign(s);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
float sdPlane(in vec3 p, in vec4 n) { // n.xyz is normal, n.w is offset
|
|
41
|
+
return dot(p, n.xyz) + n.w;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
float sdHexPrism(in vec3 p, in vec2 h) { // h.x=radius, h.y=height
|
|
45
|
+
const vec3 k = vec3(-0.8660254, 0.5, 0.57735026);
|
|
46
|
+
p = abs(p);
|
|
47
|
+
p.xy -= 2.0 * min(dot(k.xy, p.xy), 0.0) * k.xy;
|
|
48
|
+
vec2 d = vec2(
|
|
49
|
+
length(p.xy - vec2(clamp(p.x, -k.z * h.x, k.z * h.x), h.x)) * sign(p.y - h.x),
|
|
50
|
+
p.z - h.y);
|
|
51
|
+
return min(max(d.x, d.y), 0.0) + length(max(d, 0.0));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
float sdOctahedron(in vec3 p, in float s) {
|
|
55
|
+
p = abs(p);
|
|
56
|
+
return (p.x + p.y + p.z - s) * 0.57735027;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
float sdEllipsoid(in vec3 p, in vec3 r) { // r is radii on each axis
|
|
60
|
+
float k0 = length(p / r);
|
|
61
|
+
float k1 = length(p / (r * r));
|
|
62
|
+
return k0 * (k0 - 1.0) / k1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
float sdCylinder(vec3 p, vec2 h) { // h.x=radius, h.y=half-height
|
|
66
|
+
vec2 d = abs(vec2(length(p.xz), p.y)) - h;
|
|
67
|
+
return min(max(d.x, d.y), 0.0) + length(max(d, 0.0));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// --- 2D Primitives (for extrusion or direct use if p.z=0) ---
|
|
71
|
+
|
|
72
|
+
float sdCircle(in vec2 p, in float r) {
|
|
73
|
+
return length(p) - r;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
float sdRectangle(in vec2 p, in vec2 b) {
|
|
77
|
+
vec2 d = abs(p) - b;
|
|
78
|
+
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
|
|
79
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// --- Transformations ---
|
|
2
|
+
|
|
3
|
+
vec4 opTwist( in vec4 sdf_result, inout vec3 p, float k )
|
|
4
|
+
{
|
|
5
|
+
float c = cos(k*p.y);
|
|
6
|
+
float s = sin(k*p.y);
|
|
7
|
+
mat2 m = mat2(c,-s,s,c);
|
|
8
|
+
p.xz = m*p.xz;
|
|
9
|
+
|
|
10
|
+
return sdf_result;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
vec3 opRotateX(vec3 p, float theta) {
|
|
14
|
+
float c = cos(theta);
|
|
15
|
+
float s = sin(theta);
|
|
16
|
+
return vec3(p.x, p.y * c - p.z * s, p.y * s + p.z * c);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
vec3 opRotateY(vec3 p, float theta) {
|
|
20
|
+
float c = cos(theta);
|
|
21
|
+
float s = sin(theta);
|
|
22
|
+
return vec3(p.x * c + p.z * s, p.y, -p.x * s + p.z * c);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
vec3 opRotateZ(vec3 p, float theta) {
|
|
26
|
+
float c = cos(theta);
|
|
27
|
+
float s = sin(theta);
|
|
28
|
+
return vec3(p.x * c - p.y * s, p.x * s + p.y * c, p.z);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
// --- Domain Repetition ---
|
|
33
|
+
|
|
34
|
+
vec3 opRepeat(vec3 p, vec3 c)
|
|
35
|
+
{
|
|
36
|
+
return mod(p + 0.5 * c, c) - 0.5 * c;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// --- Mirroring ---
|
|
40
|
+
|
|
41
|
+
vec3 opMirror(vec3 p, vec3 a) {
|
|
42
|
+
if (a.x > 0.5) p.x = abs(p.x);
|
|
43
|
+
if (a.y > 0.5) p.y = abs(p.y);
|
|
44
|
+
if (a.z > 0.5) p.z = abs(p.z);
|
|
45
|
+
return p;
|
|
46
|
+
}
|
sdforge/mesh.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import time
|
|
3
|
+
import struct
|
|
4
|
+
from skimage import measure
|
|
5
|
+
|
|
6
|
+
def _cartesian_product(*arrays):
|
|
7
|
+
la = len(arrays)
|
|
8
|
+
dtype = np.result_type(*arrays)
|
|
9
|
+
arr = np.empty([len(a) for a in arrays] + [la], dtype=dtype)
|
|
10
|
+
for i, a in enumerate(np.ix_(*arrays)):
|
|
11
|
+
arr[...,i] = a
|
|
12
|
+
return arr.reshape(-1, la)
|
|
13
|
+
|
|
14
|
+
def _write_binary_stl(path, points):
|
|
15
|
+
n = len(points) // 3
|
|
16
|
+
points = np.array(points, dtype='float32').reshape((-1, 3, 3))
|
|
17
|
+
normals = np.cross(points[:,1] - points[:,0], points[:,2] - points[:,0])
|
|
18
|
+
norm = np.linalg.norm(normals, axis=1).reshape((-1, 1))
|
|
19
|
+
normals /= np.where(norm == 0, 1, norm)
|
|
20
|
+
|
|
21
|
+
dtype = np.dtype([
|
|
22
|
+
('normal', ('<f', 3)),
|
|
23
|
+
('points', ('<f', (3, 3))),
|
|
24
|
+
('attr', '<H'),
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
a = np.zeros(n, dtype=dtype)
|
|
28
|
+
a['points'] = points
|
|
29
|
+
a['normal'] = normals
|
|
30
|
+
|
|
31
|
+
with open(path, 'wb') as fp:
|
|
32
|
+
fp.write(b'\x00' * 80)
|
|
33
|
+
fp.write(struct.pack('<I', n))
|
|
34
|
+
fp.write(a.tobytes())
|
|
35
|
+
|
|
36
|
+
def save(sdf_obj, path, bounds=((-1.5, -1.5, -1.5), (1.5, 1.5, 1.5)), samples=2**22, verbose=True):
|
|
37
|
+
"""
|
|
38
|
+
Generates a mesh from an SDF object using the Marching Cubes algorithm and saves it to a file.
|
|
39
|
+
"""
|
|
40
|
+
start_time = time.time()
|
|
41
|
+
if verbose:
|
|
42
|
+
print(f"INFO: Generating mesh for '{path}'...")
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
sdf_callable = sdf_obj.to_callable()
|
|
46
|
+
except (NotImplementedError, ImportError) as e:
|
|
47
|
+
print(f"ERROR: Could not generate mesh. {e}")
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
volume = (bounds[1][0] - bounds[0][0]) * (bounds[1][1] - bounds[0][1]) * (bounds[1][2] - bounds[0][2])
|
|
51
|
+
step = (volume / samples) ** (1 / 3)
|
|
52
|
+
|
|
53
|
+
if verbose:
|
|
54
|
+
print(f" - Bounds: {bounds}")
|
|
55
|
+
print(f" - Target samples: {samples}")
|
|
56
|
+
print(f" - Voxel step size: {step:.4f}")
|
|
57
|
+
|
|
58
|
+
X = np.arange(bounds[0][0], bounds[1][0], step)
|
|
59
|
+
Y = np.arange(bounds[0][1], bounds[1][1], step)
|
|
60
|
+
Z = np.arange(bounds[0][2], bounds[1][2], step)
|
|
61
|
+
|
|
62
|
+
if verbose:
|
|
63
|
+
count = len(X)*len(Y)*len(Z)
|
|
64
|
+
print(f" - Grid dimensions: {len(X)} x {len(Y)} x {len(Z)} = {count} points")
|
|
65
|
+
|
|
66
|
+
# Create all grid points (float32 for downstream libs)
|
|
67
|
+
points_grid = _cartesian_product(X, Y, Z).astype('f4')
|
|
68
|
+
|
|
69
|
+
if verbose:
|
|
70
|
+
print(" - Evaluating SDF on grid...")
|
|
71
|
+
|
|
72
|
+
distances = sdf_callable(points_grid)
|
|
73
|
+
distances = np.array(distances, dtype='f4').reshape(len(X), len(Y), len(Z))
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
verts, faces, _, _ = measure.marching_cubes(distances, level=0, spacing=(step, step, step))
|
|
77
|
+
except ValueError:
|
|
78
|
+
print("ERROR: Marching cubes failed. The surface may not intersect the specified bounds or the SDF evaluation returned invalid values.")
|
|
79
|
+
print(" Suggestions:")
|
|
80
|
+
print(" - Increase 'samples' to produce a finer voxel grid (e.g. samples=2**23 or 2**24).")
|
|
81
|
+
print(" - Expand 'bounds' so the object is definitely inside the volume.")
|
|
82
|
+
print(" - Verify your SDF callable returns finite numeric distances for all points.")
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
verts += np.array(bounds[0]) # Offset vertices to correct world position
|
|
86
|
+
|
|
87
|
+
if path.lower().endswith('.stl'):
|
|
88
|
+
_write_binary_stl(path, verts[faces])
|
|
89
|
+
else:
|
|
90
|
+
print(f"ERROR: Unsupported file format '{path}'. Only .stl is currently supported.")
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
elapsed = time.time() - start_time
|
|
94
|
+
if verbose:
|
|
95
|
+
print(f"SUCCESS: Mesh with {len(faces)} triangles saved to '{path}' in {elapsed:.2f}s.")
|
sdforge/render.py
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import importlib.util
|
|
6
|
+
import numpy as np
|
|
7
|
+
from watchdog.observers import Observer
|
|
8
|
+
from watchdog.events import FileSystemEventHandler
|
|
9
|
+
|
|
10
|
+
from .api import Camera, Light
|
|
11
|
+
|
|
12
|
+
# The maximum number of unique materials supported in a scene.
|
|
13
|
+
MAX_MATERIALS = 64
|
|
14
|
+
|
|
15
|
+
def _glsl_format(val):
|
|
16
|
+
"""Formats a Python value for injection into a GLSL string."""
|
|
17
|
+
if isinstance(val, str):
|
|
18
|
+
return val
|
|
19
|
+
return f"{float(val)}"
|
|
20
|
+
|
|
21
|
+
def is_in_colab():
|
|
22
|
+
"""Checks if the code is running in a Google Colab environment."""
|
|
23
|
+
return 'google.colab' in sys.modules
|
|
24
|
+
|
|
25
|
+
def get_glsl_content(filename: str) -> str:
|
|
26
|
+
"""Reads the content of a GLSL file from the package."""
|
|
27
|
+
glsl_dir = Path(__file__).parent / 'glsl'
|
|
28
|
+
try:
|
|
29
|
+
with open(glsl_dir / filename, 'r') as f:
|
|
30
|
+
return f.read()
|
|
31
|
+
except FileNotFoundError:
|
|
32
|
+
print(f"ERROR: Could not find GLSL file: {filename}")
|
|
33
|
+
return ""
|
|
34
|
+
|
|
35
|
+
def assemble_shader_code(sdf_obj) -> str:
|
|
36
|
+
"""Assembles the final GLSL scene function from an SDF object."""
|
|
37
|
+
# The Scene function now returns a vec4: (distance, material_id, 0, 0)
|
|
38
|
+
scene_glsl = sdf_obj.to_glsl()
|
|
39
|
+
inline_definitions = []
|
|
40
|
+
# Use a set to prevent duplicates, which cause redefinition errors
|
|
41
|
+
unique_defs = set(sdf_obj.get_glsl_definitions())
|
|
42
|
+
for d in unique_defs:
|
|
43
|
+
inline_definitions.append(d)
|
|
44
|
+
joined_defs = '\n'.join(inline_definitions)
|
|
45
|
+
return f"""
|
|
46
|
+
{joined_defs}
|
|
47
|
+
vec4 Scene(in vec3 p) {{ return {scene_glsl}; }}
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
class NativeRenderer:
|
|
51
|
+
"""Handles the creation of a native window and renders the SDF."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, sdf_obj, camera=None, lighting=None, watch=False, width=1280, height=720, record=None, bg_color=(0.1, 0.12, 0.15)):
|
|
54
|
+
self.sdf_obj = sdf_obj
|
|
55
|
+
self.camera = camera
|
|
56
|
+
self.lighting = lighting
|
|
57
|
+
self.watching = watch
|
|
58
|
+
self.width = width
|
|
59
|
+
self.height = height
|
|
60
|
+
self.record_path = record
|
|
61
|
+
self.bg_color = bg_color
|
|
62
|
+
self.window = None
|
|
63
|
+
self.ctx = None
|
|
64
|
+
self.program = None
|
|
65
|
+
self.vao = None
|
|
66
|
+
self.vbo = None
|
|
67
|
+
self.script_path = os.path.abspath(sys.argv[0])
|
|
68
|
+
self.reload_pending = False
|
|
69
|
+
|
|
70
|
+
def _init_window(self):
|
|
71
|
+
import glfw
|
|
72
|
+
if not glfw.init(): raise RuntimeError("Could not initialize GLFW")
|
|
73
|
+
glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 3)
|
|
74
|
+
glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 3)
|
|
75
|
+
glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
|
|
76
|
+
glfw.window_hint(glfw.OPENGL_FORWARD_COMPAT, True)
|
|
77
|
+
self.window = glfw.create_window(self.width, self.height, "SDF Forge Viewer", None, None)
|
|
78
|
+
if not self.window:
|
|
79
|
+
glfw.terminate()
|
|
80
|
+
raise RuntimeError("Could not create GLFW window")
|
|
81
|
+
glfw.make_context_current(self.window)
|
|
82
|
+
|
|
83
|
+
def _compile_shader(self):
|
|
84
|
+
import moderngl
|
|
85
|
+
|
|
86
|
+
materials = []
|
|
87
|
+
self.sdf_obj._collect_materials(materials)
|
|
88
|
+
if len(materials) > MAX_MATERIALS:
|
|
89
|
+
print(f"WARNING: Exceeded maximum of {MAX_MATERIALS} materials. Truncating.")
|
|
90
|
+
materials = materials[:MAX_MATERIALS]
|
|
91
|
+
|
|
92
|
+
material_struct_glsl = "struct MaterialInfo { vec3 color; };\n"
|
|
93
|
+
material_uniform_glsl = f"uniform MaterialInfo u_materials[{max(1, len(materials))}];\n"
|
|
94
|
+
|
|
95
|
+
# --- Light ---
|
|
96
|
+
light_pos_str = "ro"
|
|
97
|
+
ambient_strength_str = "0.1"
|
|
98
|
+
shadow_softness_str = "8.0"
|
|
99
|
+
ao_strength_str = "3.0"
|
|
100
|
+
|
|
101
|
+
if self.lighting:
|
|
102
|
+
if self.lighting.position:
|
|
103
|
+
pos = self.lighting.position
|
|
104
|
+
light_pos_str = f"vec3({_glsl_format(pos[0])}, {_glsl_format(pos[1])}, {_glsl_format(pos[2])})"
|
|
105
|
+
ambient_strength_str = _glsl_format(self.lighting.ambient_strength)
|
|
106
|
+
shadow_softness_str = _glsl_format(self.lighting.shadow_softness)
|
|
107
|
+
ao_strength_str = _glsl_format(self.lighting.ao_strength)
|
|
108
|
+
|
|
109
|
+
# --- Camera ---
|
|
110
|
+
camera_logic_glsl = ""
|
|
111
|
+
if self.camera:
|
|
112
|
+
pos = self.camera.position
|
|
113
|
+
pos_str = f"vec3({_glsl_format(pos[0])}, {_glsl_format(pos[1])}, {_glsl_format(pos[2])})"
|
|
114
|
+
|
|
115
|
+
target = self.camera.target
|
|
116
|
+
target_str = f"vec3({_glsl_format(target[0])}, {_glsl_format(target[1])}, {_glsl_format(target[2])})"
|
|
117
|
+
|
|
118
|
+
zoom_str = _glsl_format(self.camera.zoom)
|
|
119
|
+
|
|
120
|
+
camera_logic_glsl = f"cameraStatic(st, {pos_str}, {target_str}, {zoom_str}, ro, rd);"
|
|
121
|
+
else:
|
|
122
|
+
camera_logic_glsl = "cameraOrbit(st, u_mouse.xy, u_resolution, 1.0, ro, rd);"
|
|
123
|
+
|
|
124
|
+
scene_code = assemble_shader_code(self.sdf_obj)
|
|
125
|
+
|
|
126
|
+
full_fragment_shader = f"""
|
|
127
|
+
#version 330 core
|
|
128
|
+
|
|
129
|
+
uniform vec2 u_resolution;
|
|
130
|
+
uniform float u_time;
|
|
131
|
+
uniform vec4 u_mouse;
|
|
132
|
+
uniform vec3 u_bg_color;
|
|
133
|
+
|
|
134
|
+
{material_struct_glsl}
|
|
135
|
+
{material_uniform_glsl}
|
|
136
|
+
|
|
137
|
+
out vec4 f_color;
|
|
138
|
+
|
|
139
|
+
{get_glsl_content('sdf/primitives.glsl')}
|
|
140
|
+
{get_glsl_content('scene/camera.glsl')}
|
|
141
|
+
{get_glsl_content('scene/raymarching.glsl')}
|
|
142
|
+
{get_glsl_content('scene/lighting.glsl')}
|
|
143
|
+
|
|
144
|
+
{scene_code}
|
|
145
|
+
|
|
146
|
+
void main() {{
|
|
147
|
+
vec2 st = (2.0 * gl_FragCoord.xy - u_resolution.xy) / u_resolution.y;
|
|
148
|
+
vec3 ro, rd;
|
|
149
|
+
{camera_logic_glsl}
|
|
150
|
+
|
|
151
|
+
vec3 color = u_bg_color;
|
|
152
|
+
vec4 hit = raymarch(ro, rd);
|
|
153
|
+
float t = hit.x;
|
|
154
|
+
int material_id = int(hit.y);
|
|
155
|
+
|
|
156
|
+
if (t > 0.0) {{
|
|
157
|
+
vec3 p = ro + t * rd;
|
|
158
|
+
vec3 normal = estimateNormal(p);
|
|
159
|
+
vec3 lightPos = {light_pos_str};
|
|
160
|
+
vec3 lightDir = normalize(lightPos - p);
|
|
161
|
+
|
|
162
|
+
float diffuse = max(dot(normal, lightDir), {ambient_strength_str});
|
|
163
|
+
float shadow = softShadow(p + normal * 0.01, lightDir, {shadow_softness_str});
|
|
164
|
+
diffuse *= shadow;
|
|
165
|
+
float ao = ambientOcclusion(p, normal, {ao_strength_str});
|
|
166
|
+
|
|
167
|
+
vec3 material_color = vec3(0.8); // Default color if no material
|
|
168
|
+
if (material_id >= 0 && material_id < {len(materials)}) {{
|
|
169
|
+
material_color = u_materials[material_id].color;
|
|
170
|
+
}}
|
|
171
|
+
|
|
172
|
+
color = material_color * diffuse * ao;
|
|
173
|
+
}}
|
|
174
|
+
f_color = vec4(color, 1.0);
|
|
175
|
+
}}
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
vertex_shader = """
|
|
179
|
+
#version 330 core
|
|
180
|
+
in vec2 in_vert;
|
|
181
|
+
void main() { gl_Position = vec4(in_vert, 0.0, 1.0); }
|
|
182
|
+
"""
|
|
183
|
+
try:
|
|
184
|
+
program = self.ctx.program(vertex_shader=vertex_shader, fragment_shader=full_fragment_shader)
|
|
185
|
+
print("INFO: Shader compiled successfully.")
|
|
186
|
+
|
|
187
|
+
# Upload material data
|
|
188
|
+
for i, mat in enumerate(materials):
|
|
189
|
+
program[f'u_materials[{i}].color'].value = mat.color
|
|
190
|
+
|
|
191
|
+
program['u_bg_color'].value = self.bg_color
|
|
192
|
+
|
|
193
|
+
return program
|
|
194
|
+
except Exception as e:
|
|
195
|
+
print(f"ERROR: Shader compilation failed. Keeping previous shader. Details:\n{e}")
|
|
196
|
+
return self.program
|
|
197
|
+
|
|
198
|
+
def _setup_gl(self):
|
|
199
|
+
import moderngl
|
|
200
|
+
self.ctx = moderngl.create_context()
|
|
201
|
+
self.program = self._compile_shader()
|
|
202
|
+
if self.program is None:
|
|
203
|
+
raise RuntimeError("Failed to compile initial shader. Cannot continue.")
|
|
204
|
+
vertices = np.array([-1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0], dtype='f4')
|
|
205
|
+
self.vbo = self.ctx.buffer(vertices)
|
|
206
|
+
self.vao = self.ctx.simple_vertex_array(self.program, self.vbo, 'in_vert')
|
|
207
|
+
|
|
208
|
+
def _reload_script(self):
|
|
209
|
+
print(f"INFO: Change detected in '{Path(self.script_path).name}'. Reloading...")
|
|
210
|
+
try:
|
|
211
|
+
spec = importlib.util.spec_from_file_location("user_script", self.script_path)
|
|
212
|
+
module = importlib.util.module_from_spec(spec)
|
|
213
|
+
spec.loader.exec_module(module)
|
|
214
|
+
if hasattr(module, 'main') and callable(module.main):
|
|
215
|
+
result = module.main()
|
|
216
|
+
new_sdf_obj, new_cam_obj, new_lighting_obj = None, None, None
|
|
217
|
+
|
|
218
|
+
if isinstance(result, tuple):
|
|
219
|
+
new_sdf_obj = result[0]
|
|
220
|
+
for item in result[1:]:
|
|
221
|
+
if isinstance(item, Camera):
|
|
222
|
+
new_cam_obj = item
|
|
223
|
+
elif isinstance(item, Light):
|
|
224
|
+
new_lighting_obj = item
|
|
225
|
+
else:
|
|
226
|
+
new_sdf_obj = result
|
|
227
|
+
|
|
228
|
+
if new_sdf_obj:
|
|
229
|
+
self.sdf_obj = new_sdf_obj
|
|
230
|
+
self.camera = new_cam_obj
|
|
231
|
+
self.lighting = new_lighting_obj
|
|
232
|
+
self.program = self._compile_shader()
|
|
233
|
+
self.vao = self.ctx.simple_vertex_array(self.program, self.vbo, 'in_vert')
|
|
234
|
+
else:
|
|
235
|
+
print("WARNING: No valid `main` function found. Cannot reload.")
|
|
236
|
+
except Exception as e:
|
|
237
|
+
print(f"ERROR: Failed to reload script: {e}")
|
|
238
|
+
|
|
239
|
+
def _start_watcher(self):
|
|
240
|
+
class ChangeHandler(FileSystemEventHandler):
|
|
241
|
+
def __init__(self, renderer_instance): self.renderer = renderer_instance
|
|
242
|
+
def on_modified(self, event):
|
|
243
|
+
if event.src_path == self.renderer.script_path:
|
|
244
|
+
self.renderer.reload_pending = True
|
|
245
|
+
|
|
246
|
+
observer = Observer()
|
|
247
|
+
observer.schedule(ChangeHandler(self), str(Path(self.script_path).parent), recursive=False)
|
|
248
|
+
observer.daemon = True
|
|
249
|
+
observer.start()
|
|
250
|
+
print(f"INFO: Watching '{Path(self.script_path).name}' for changes...")
|
|
251
|
+
|
|
252
|
+
def run(self):
|
|
253
|
+
import glfw
|
|
254
|
+
import moderngl
|
|
255
|
+
|
|
256
|
+
writer = None
|
|
257
|
+
if self.record_path:
|
|
258
|
+
try:
|
|
259
|
+
import imageio
|
|
260
|
+
fps = 30
|
|
261
|
+
writer = imageio.get_writer(self.record_path, fps=fps)
|
|
262
|
+
print(f"INFO: Recording video to '{self.record_path}' at {fps} FPS.")
|
|
263
|
+
except ImportError:
|
|
264
|
+
print("ERROR: 'imageio' is required for video recording.\nPlease install it via: pip install sdforge[record]")
|
|
265
|
+
self.record_path = None
|
|
266
|
+
|
|
267
|
+
self._init_window()
|
|
268
|
+
self._setup_gl()
|
|
269
|
+
|
|
270
|
+
if self.watching: self._start_watcher()
|
|
271
|
+
|
|
272
|
+
while not glfw.window_should_close(self.window):
|
|
273
|
+
if self.reload_pending:
|
|
274
|
+
self._reload_script()
|
|
275
|
+
self.reload_pending = False
|
|
276
|
+
|
|
277
|
+
width, height = glfw.get_framebuffer_size(self.window)
|
|
278
|
+
self.ctx.viewport = (0, 0, width, height)
|
|
279
|
+
|
|
280
|
+
try: self.program['u_resolution'].value = (width, height)
|
|
281
|
+
except KeyError: pass
|
|
282
|
+
try: self.program['u_time'].value = glfw.get_time()
|
|
283
|
+
except KeyError: pass
|
|
284
|
+
try:
|
|
285
|
+
mx, my = glfw.get_cursor_pos(self.window)
|
|
286
|
+
self.program['u_mouse'].value = (mx, height - my, 0, 0)
|
|
287
|
+
except KeyError: pass
|
|
288
|
+
|
|
289
|
+
self.vao.render(mode=moderngl.TRIANGLE_STRIP)
|
|
290
|
+
|
|
291
|
+
if writer:
|
|
292
|
+
frame_bytes = self.ctx.fbo.read(components=3, alignment=1)
|
|
293
|
+
frame_np = np.frombuffer(frame_bytes, dtype=np.uint8).reshape((height, width, 3))
|
|
294
|
+
writer.append_data(np.flipud(frame_np))
|
|
295
|
+
|
|
296
|
+
glfw.swap_buffers(self.window)
|
|
297
|
+
glfw.poll_events()
|
|
298
|
+
|
|
299
|
+
if writer: writer.close()
|
|
300
|
+
print("INFO: Viewer window closed.")
|
|
301
|
+
glfw.terminate()
|
|
302
|
+
|
|
303
|
+
def render(sdf_obj, camera=None, lighting=None, watch=True, record=None, bg_color=(0.1, 0.12, 0.15), **kwargs):
|
|
304
|
+
if is_in_colab():
|
|
305
|
+
from IPython.display import display, HTML
|
|
306
|
+
|
|
307
|
+
materials = []
|
|
308
|
+
sdf_obj._collect_materials(materials)
|
|
309
|
+
if len(materials) > MAX_MATERIALS:
|
|
310
|
+
materials = materials[:MAX_MATERIALS]
|
|
311
|
+
|
|
312
|
+
material_struct_glsl = "struct MaterialInfo { vec3 color; };\n"
|
|
313
|
+
|
|
314
|
+
colors_glsl = ", ".join([f"vec3({c.color[0]}, {c.color[1]}, {c.color[2]})" for c in materials])
|
|
315
|
+
material_array_glsl = f"const MaterialInfo u_materials[{max(1, len(materials))}] = MaterialInfo[]({colors_glsl});\n"
|
|
316
|
+
|
|
317
|
+
# --- Light ---
|
|
318
|
+
light_pos_str = "ro"
|
|
319
|
+
ambient_strength_str = "0.1"
|
|
320
|
+
shadow_softness_str = "8.0"
|
|
321
|
+
ao_strength_str = "3.0"
|
|
322
|
+
|
|
323
|
+
if lighting:
|
|
324
|
+
if lighting.position:
|
|
325
|
+
pos = lighting.position
|
|
326
|
+
light_pos_str = f"vec3({_glsl_format(pos[0])}, {_glsl_format(pos[1])}, {_glsl_format(pos[2])})"
|
|
327
|
+
ambient_strength_str = _glsl_format(lighting.ambient_strength)
|
|
328
|
+
shadow_softness_str = _glsl_format(lighting.shadow_softness)
|
|
329
|
+
ao_strength_str = _glsl_format(lighting.ao_strength)
|
|
330
|
+
|
|
331
|
+
# --- Camera ---
|
|
332
|
+
camera_logic_glsl = ""
|
|
333
|
+
if camera:
|
|
334
|
+
pos = camera.position
|
|
335
|
+
pos_str = f"vec3({_glsl_format(pos[0])}, {_glsl_format(pos[1])}, {_glsl_format(pos[2])})"
|
|
336
|
+
target = camera.target
|
|
337
|
+
target_str = f"vec3({_glsl_format(target[0])}, {_glsl_format(target[1])}, {_glsl_format(target[2])})"
|
|
338
|
+
zoom_str = _glsl_format(camera.zoom)
|
|
339
|
+
camera_logic_glsl = f"cameraStatic(st, {pos_str}, {target_str}, {zoom_str}, ro, rd);"
|
|
340
|
+
else:
|
|
341
|
+
camera_logic_glsl = "cameraOrbit(st, u_mouse.xy, u_resolution, 1.0, ro, rd);"
|
|
342
|
+
|
|
343
|
+
shader_code = assemble_shader_code(sdf_obj)
|
|
344
|
+
html_template = f"""
|
|
345
|
+
<!DOCTYPE html><html><head><title>SDF Forge Viewer</title>
|
|
346
|
+
<style>body{{margin:0;overflow:hidden}}canvas{{display:block}}</style></head>
|
|
347
|
+
<body><script type="importmap">{{"imports":{{"three":"https://unpkg.com/three@0.157.0/build/three.module.js"}}}}</script>
|
|
348
|
+
<script type="module">
|
|
349
|
+
import * as THREE from 'three';
|
|
350
|
+
const fragmentShader = `
|
|
351
|
+
varying vec2 vUv;
|
|
352
|
+
uniform float u_time;
|
|
353
|
+
uniform vec4 u_mouse;
|
|
354
|
+
#define u_resolution vec2(800.0, 600.0)
|
|
355
|
+
#define u_bg_color vec3({bg_color[0]}, {bg_color[1]}, {bg_color[2]})
|
|
356
|
+
|
|
357
|
+
{material_struct_glsl}
|
|
358
|
+
{material_array_glsl}
|
|
359
|
+
|
|
360
|
+
{get_glsl_content('sdf/primitives.glsl')}
|
|
361
|
+
{get_glsl_content('scene/camera.glsl')}
|
|
362
|
+
{get_glsl_content('scene/raymarching.glsl')}
|
|
363
|
+
{get_glsl_content('scene/lighting.glsl')}
|
|
364
|
+
{shader_code}
|
|
365
|
+
void main() {{
|
|
366
|
+
vec2 st = (2.0*vUv - 1.0) * vec2(u_resolution.x/u_resolution.y, 1.0);
|
|
367
|
+
vec3 ro, rd;
|
|
368
|
+
{camera_logic_glsl}
|
|
369
|
+
vec3 color = u_bg_color;
|
|
370
|
+
vec4 hit = raymarch(ro, rd);
|
|
371
|
+
float t = hit.x;
|
|
372
|
+
int material_id = int(hit.y);
|
|
373
|
+
if (t > 0.0) {{
|
|
374
|
+
vec3 p = ro + t * rd;
|
|
375
|
+
vec3 normal = estimateNormal(p);
|
|
376
|
+
vec3 lightPos = {light_pos_str};
|
|
377
|
+
vec3 lightDir = normalize(lightPos - p);
|
|
378
|
+
float diffuse = max(dot(normal, lightDir), {ambient_strength_str});
|
|
379
|
+
float shadow = softShadow(p+normal*0.01, lightDir, {shadow_softness_str});
|
|
380
|
+
diffuse *= shadow;
|
|
381
|
+
float ao = ambientOcclusion(p, normal, {ao_strength_str});
|
|
382
|
+
vec3 material_color = vec3(0.8);
|
|
383
|
+
if (material_id >= 0 && material_id < {len(materials)}) {{
|
|
384
|
+
material_color = u_materials[material_id].color;
|
|
385
|
+
}}
|
|
386
|
+
color = material_color * diffuse * ao;
|
|
387
|
+
}}
|
|
388
|
+
gl_FragColor = vec4(color, 1.0);
|
|
389
|
+
}}
|
|
390
|
+
`;
|
|
391
|
+
const scene = new THREE.Scene();
|
|
392
|
+
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
393
|
+
const renderer = new THREE.WebGLRenderer({{antialias: true}});
|
|
394
|
+
document.body.appendChild(renderer.domElement);
|
|
395
|
+
const uniforms = {{u_time:{{value:0}}, u_mouse:{{value:new THREE.Vector4()}}}};
|
|
396
|
+
const material = new THREE.ShaderMaterial({{vertexShader:`varying vec2 vUv; void main(){{vUv=uv;gl_Position=vec4(position,1.0);}}`, fragmentShader, uniforms}});
|
|
397
|
+
scene.add(new THREE.Mesh(new THREE.PlaneGeometry(2,2), material));
|
|
398
|
+
renderer.setSize(800,600);
|
|
399
|
+
document.addEventListener('mousemove', e=>{{uniforms.u_mouse.value.x=e.clientX;uniforms.u_mouse.value.y=600-e.clientY;}});
|
|
400
|
+
function animate(t){{requestAnimationFrame(animate);uniforms.u_time.value=t*0.001;renderer.render(scene,camera);}}
|
|
401
|
+
animate();
|
|
402
|
+
</script></body></html>
|
|
403
|
+
"""
|
|
404
|
+
escaped_html = html_template.replace('"', """)
|
|
405
|
+
display(HTML(f'<iframe srcdoc="{escaped_html}" width="800" height="600" style="border:1px solid #ccc"></iframe>'))
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
import moderngl, glfw
|
|
410
|
+
except ImportError:
|
|
411
|
+
print("ERROR: Live rendering requires 'moderngl' and 'glfw'.\nPlease install them via: pip install sdforge")
|
|
412
|
+
return
|
|
413
|
+
|
|
414
|
+
renderer = NativeRenderer(sdf_obj, camera=camera, lighting=lighting, watch=watch, record=record, bg_color=bg_color, **kwargs)
|
|
415
|
+
renderer.run()
|