Skip to content

Commit 0781b3e

Browse files
committed
Add render graph example script (#73)
CPU-only demo showing custom passes, capability gating, fallback wiring, and the full compile-then-execute workflow.
1 parent d424272 commit 0781b3e

1 file changed

Lines changed: 196 additions & 0 deletions

File tree

examples/render_graph_demo.py

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
"""Render graph demo — build a multi-pass pipeline with capability gating.
2+
3+
Shows how to define custom render passes, wire them into a graph, and let the
4+
graph handle execution order and fallback wiring when optional passes are
5+
unavailable.
6+
7+
This example runs on CPU (numpy) and doesn't need a GPU.
8+
"""
9+
10+
import numpy as np
11+
12+
from rtxpy import BufferDesc, RenderGraph, RenderPass
13+
14+
# Buffer descriptors for the pipeline
15+
RGB = BufferDesc(dtype="float32", channels=3, per_pixel=True)
16+
SCALAR = BufferDesc(dtype="float32", channels=1, per_pixel=True)
17+
18+
19+
# --- Pass definitions --------------------------------------------------------
20+
21+
22+
class GBufferPass(RenderPass):
23+
"""Simulate a GBuffer pass that produces albedo, normals, and depth."""
24+
25+
def __init__(self):
26+
super().__init__(
27+
"gbuffer",
28+
outputs={"albedo": RGB, "normal": RGB, "depth": SCALAR},
29+
)
30+
31+
def execute(self, buffers):
32+
h, w, _ = buffers["albedo"].shape
33+
# Checkerboard albedo
34+
yy, xx = np.mgrid[:h, :w]
35+
checker = ((xx // 8 + yy // 8) % 2).astype(np.float32)
36+
buffers["albedo"][:, :, 0] = 0.2 + 0.6 * checker
37+
buffers["albedo"][:, :, 1] = 0.3 + 0.3 * checker
38+
buffers["albedo"][:, :, 2] = 0.1 + 0.2 * (1 - checker)
39+
40+
# Upward-facing normals
41+
buffers["normal"][:] = [0.0, 0.0, 1.0]
42+
43+
# Linear depth gradient
44+
buffers["depth"][:, :] = np.linspace(0.0, 1.0, w, dtype=np.float32)
45+
46+
47+
class ShadowPass(RenderPass):
48+
"""Compute a simple shadow mask from depth."""
49+
50+
def __init__(self):
51+
super().__init__(
52+
"shadow",
53+
inputs={"depth": SCALAR},
54+
outputs={"shadow_mask": SCALAR},
55+
)
56+
57+
def execute(self, buffers):
58+
# Fake shadow: darker where depth > 0.5
59+
buffers["shadow_mask"][:] = np.where(
60+
buffers["depth"] > 0.5, 0.4, 1.0
61+
).astype(np.float32)
62+
63+
64+
class AOPass(RenderPass):
65+
"""Fake ambient occlusion from depth edges."""
66+
67+
def __init__(self):
68+
super().__init__(
69+
"ao",
70+
inputs={"depth": SCALAR},
71+
outputs={"ao_map": SCALAR},
72+
)
73+
74+
def execute(self, buffers):
75+
depth = buffers["depth"]
76+
# Approximate AO by depth variance in a 3x3 window
77+
padded = np.pad(depth, ((1, 1), (1, 1)), mode="edge")
78+
ao = np.ones_like(depth)
79+
for dy in (-1, 0, 1):
80+
for dx in (-1, 0, 1):
81+
ao -= 0.02 * np.abs(
82+
padded[1 + dy : depth.shape[0] + 1 + dy,
83+
1 + dx : depth.shape[1] + 1 + dx]
84+
- depth
85+
)
86+
buffers["ao_map"][:] = np.clip(ao, 0.3, 1.0)
87+
88+
89+
class ShadePass(RenderPass):
90+
"""Combine albedo, shadow, and AO into a lit color buffer."""
91+
92+
def __init__(self):
93+
super().__init__(
94+
"shade",
95+
inputs={"albedo": RGB, "shadow_mask": SCALAR, "ao_map": SCALAR},
96+
outputs={"color": RGB},
97+
)
98+
99+
def execute(self, buffers):
100+
albedo = buffers["albedo"]
101+
shadow = buffers["shadow_mask"][:, :, np.newaxis]
102+
ao = buffers["ao_map"][:, :, np.newaxis]
103+
buffers["color"][:] = albedo * shadow * ao
104+
105+
106+
class DenoisePass(RenderPass):
107+
"""Placeholder denoiser — requires 'optix_denoiser' capability."""
108+
109+
def __init__(self):
110+
super().__init__(
111+
"denoise",
112+
inputs={"color": RGB, "albedo": RGB, "normal": RGB},
113+
outputs={"denoised_color": RGB},
114+
requires=["optix_denoiser"],
115+
)
116+
117+
def execute(self, buffers):
118+
# Real implementation would call OptiX denoiser
119+
buffers["denoised_color"][:] = buffers["color"]
120+
121+
122+
class TonemapPass(RenderPass):
123+
"""Simple Reinhard tone mapping."""
124+
125+
def __init__(self):
126+
super().__init__(
127+
"tonemap",
128+
inputs={"denoised_color": RGB},
129+
outputs={"ldr_color": RGB},
130+
)
131+
132+
def execute(self, buffers):
133+
hdr = buffers["denoised_color"]
134+
buffers["ldr_color"][:] = hdr / (1.0 + hdr)
135+
136+
137+
# --- Build and run the graph ------------------------------------------------
138+
139+
140+
def main():
141+
width, height = 128, 96
142+
143+
graph = RenderGraph(width=width, height=height)
144+
graph.add_pass(GBufferPass())
145+
graph.add_pass(ShadowPass())
146+
graph.add_pass(AOPass())
147+
graph.add_pass(ShadePass())
148+
graph.add_pass(DenoisePass())
149+
graph.add_pass(TonemapPass())
150+
151+
# If denoiser is unavailable, tonemap reads 'color' directly
152+
graph.set_fallback("denoised_color", "color")
153+
154+
# --- Run without denoiser ---
155+
print("Compiling graph WITHOUT denoiser capability...")
156+
compiled = graph.compile(capabilities={})
157+
print(f" Active passes: {[p.name for p in compiled.ordered_passes]}")
158+
print(f" Buffer pool slots: {compiled.allocation_plan.num_slots}")
159+
160+
result = compiled.execute(
161+
allocator=lambda shape, dtype: np.zeros(shape, dtype=dtype)
162+
)
163+
ldr = result["ldr_color"]
164+
print(f" Output shape: {ldr.shape}, range: [{ldr.min():.3f}, {ldr.max():.3f}]")
165+
166+
# --- Run with denoiser ---
167+
print("\nCompiling graph WITH denoiser capability...")
168+
compiled2 = graph.compile(capabilities={"optix_denoiser": True})
169+
print(f" Active passes: {[p.name for p in compiled2.ordered_passes]}")
170+
171+
result2 = compiled2.execute(
172+
allocator=lambda shape, dtype: np.zeros(shape, dtype=dtype)
173+
)
174+
ldr2 = result2["ldr_color"]
175+
print(f" Output shape: {ldr2.shape}, range: [{ldr2.min():.3f}, {ldr2.max():.3f}]")
176+
177+
# Save to PNG if matplotlib available
178+
try:
179+
import matplotlib.pyplot as plt
180+
181+
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
182+
axes[0].imshow(np.clip(result["ldr_color"], 0, 1))
183+
axes[0].set_title("Without denoiser")
184+
axes[0].axis("off")
185+
axes[1].imshow(np.clip(result2["ldr_color"], 0, 1))
186+
axes[1].set_title("With denoiser")
187+
axes[1].axis("off")
188+
plt.tight_layout()
189+
plt.savefig("render_graph_demo.png", dpi=150)
190+
print("\nSaved render_graph_demo.png")
191+
except ImportError:
192+
print("\nmatplotlib not available, skipping image save")
193+
194+
195+
if __name__ == "__main__":
196+
main()

0 commit comments

Comments
 (0)