phanerozoic commited on
Commit
2468c9d
·
verified ·
1 Parent(s): 269ccd5
Files changed (3) hide show
  1. CARD.md +9 -0
  2. README.md +10 -1
  3. media/make_hero.py +167 -0
CARD.md CHANGED
@@ -18,6 +18,15 @@ albedo texels, the per-material emission, and the environment texels.
18
  Inverse rendering runs as a bare torch loop around the kernel, with no
19
  rendering framework in the loop: render, compare, `backward()`, step.
20
 
 
 
 
 
 
 
 
 
 
21
  ## Usage
22
 
23
  ```python
 
18
  Inverse rendering runs as a bare torch loop around the kernel, with no
19
  rendering framework in the loop: render, compare, `backward()`, step.
20
 
21
+ ![inverse rendering time-lapse: a gray scene converging to a plasma-textured floor with a gold monolith and a glass box](https://huggingface.co/kernels/phanerozoic/pathtracer-diff/resolve/main/media/inverse.gif)
22
+
23
+ *An inverse-rendering time-lapse produced entirely by this kernel
24
+ (`media/make_hero.py`): from a gray start, Adam through `render()`
25
+ recovers a 64x64 floor texture (12,288 unknowns), the gold conductor's
26
+ tint, and the environment map against the fixed target on the right,
27
+ with a glass box refracting the lot. Final parameter errors: texture
28
+ 0.047 mean, tint 0.007 max, environment 0.081 mean.*
29
+
30
  ## Usage
31
 
32
  ```python
README.md CHANGED
@@ -18,6 +18,15 @@ albedo texels, the per-material emission, and the environment texels.
18
  Inverse rendering runs as a bare torch loop around the kernel, with no
19
  rendering framework in the loop: render, compare, `backward()`, step.
20
 
 
 
 
 
 
 
 
 
 
21
  ## Usage
22
 
23
  ```python
@@ -172,4 +181,4 @@ Duff et al., "Building an Orthonormal Basis, Revisited" (JCGT 2017); Wald,
172
 
173
  ## License
174
 
175
- Apache-2.0.
 
18
  Inverse rendering runs as a bare torch loop around the kernel, with no
19
  rendering framework in the loop: render, compare, `backward()`, step.
20
 
21
+ ![inverse rendering time-lapse: a gray scene converging to a plasma-textured floor with a gold monolith and a glass box](https://huggingface.co/kernels/phanerozoic/pathtracer-diff/resolve/main/media/inverse.gif)
22
+
23
+ *An inverse-rendering time-lapse produced entirely by this kernel
24
+ (`media/make_hero.py`): from a gray start, Adam through `render()`
25
+ recovers a 64x64 floor texture (12,288 unknowns), the gold conductor's
26
+ tint, and the environment map against the fixed target on the right,
27
+ with a glass box refracting the lot. Final parameter errors: texture
28
+ 0.047 mean, tint 0.007 max, environment 0.081 mean.*
29
+
30
  ## Usage
31
 
32
  ```python
 
181
 
182
  ## License
183
 
184
+ Apache-2.0.
media/make_hero.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hero time-lapse for the card: inverse rendering, live.
2
+
3
+ From a gray start, Adam through render() recovers a 64x64 floor texture
4
+ (12,288 unknowns), the gold conductor's tint, and the environment map
5
+ against a fixed target (right panel), with a glass box refracting the lot.
6
+ Rendered and differentiated entirely by the kernel. Output:
7
+ media/inverse.gif (animated, log-spaced snapshots).
8
+ """
9
+ import math
10
+ import os
11
+ import sys
12
+
13
+ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
14
+ sys.path.insert(0, ROOT)
15
+
16
+ import torch
17
+ from PIL import Image
18
+
19
+ import load_local
20
+
21
+ ptd = load_local.load()
22
+
23
+ DEV = "cuda"
24
+ H = W = 256
25
+
26
+
27
+ def quad(a, b, c, d):
28
+ return [[a, b, c], [a, c, d]]
29
+
30
+
31
+ def add_box(verts, faces, mat, uv, lo, hi, mid, rot_deg=0.0):
32
+ x0, y0, z0 = lo
33
+ x1, y1, z1 = hi
34
+ cx, cz = (x0 + x1) / 2, (z0 + z1) / 2
35
+ ca, sa = math.cos(math.radians(rot_deg)), math.sin(math.radians(rot_deg))
36
+ b = len(verts)
37
+ for (x, y, z) in [(x0, y0, z0), (x1, y0, z0), (x1, y0, z1), (x0, y0, z1),
38
+ (x0, y1, z0), (x1, y1, z0), (x1, y1, z1), (x0, y1, z1)]:
39
+ dx, dz = x - cx, z - cz
40
+ verts.append((cx + ca * dx - sa * dz, y, cz + sa * dx + ca * dz))
41
+ for q in [quad(b, b + 1, b + 2, b + 3), quad(b + 4, b + 7, b + 6, b + 5),
42
+ quad(b, b + 4, b + 5, b + 1), quad(b + 3, b + 2, b + 6, b + 7),
43
+ quad(b, b + 3, b + 7, b + 4), quad(b + 1, b + 5, b + 6, b + 2)]:
44
+ faces.extend(q)
45
+ mat.extend([mid, mid])
46
+ uv.extend([[(0, 0)] * 3] * 2)
47
+
48
+
49
+ def build_scene(floor_tex, gold, env):
50
+ verts, faces, mat, uv = [], [], [], []
51
+ b = len(verts)
52
+ verts.extend([(-5, 0, -5), (5, 0, -5), (5, 0, 5), (-5, 0, 5)])
53
+ faces.extend(quad(b, b + 1, b + 2, b + 3))
54
+ mat.extend([0, 0])
55
+ uv.extend([[(0, 0), (1, 0), (1, 1)], [(0, 0), (1, 1), (0, 1)]])
56
+ add_box(verts, faces, mat, uv, (1.1, 0, -0.6), (2.5, 2.6, 0.8), 1, 25.0)
57
+ add_box(verts, faces, mat, uv, (-2.5, 0, 0.2), (-0.9, 2.2, 1.8), 2, 12.0)
58
+ b = len(verts)
59
+ verts.extend([(-1, 6.5, -1), (1, 6.5, -1), (1, 6.5, 1), (-1, 6.5, 1)])
60
+ faces.extend(quad(b, b + 1, b + 2, b + 3))
61
+ mat.extend([3, 3])
62
+ uv.extend([[(0, 0)] * 3] * 2)
63
+ return ptd.Scene(verts, faces, mat,
64
+ albedo=[floor_tex, gold,
65
+ torch.tensor([1.0, 1.0, 1.0], device=DEV),
66
+ torch.tensor([0.8, 0.8, 0.8], device=DEV)],
67
+ emission=[[0, 0, 0], [0, 0, 0], [0, 0, 0],
68
+ [7.0, 6.5, 5.8]],
69
+ material_types=[ptd.DIFFUSE, ptd.CONDUCTOR,
70
+ ptd.DIELECTRIC, ptd.DIFFUSE],
71
+ roughness=[0.3, 0.10, 0.0, 0.3],
72
+ ior=[1.5, 1.5, 1.5, 1.5], uvs=uv, env=env)
73
+
74
+
75
+ def plasma(n=64):
76
+ y, x = torch.meshgrid(torch.linspace(0, 1, n), torch.linspace(0, 1, n),
77
+ indexing="ij")
78
+ r = 0.5 + 0.45 * torch.sin(6.0 * x + 9.0 * y * y + 1.2)
79
+ g = 0.5 + 0.45 * torch.sin(8.0 * (x - 0.5) ** 2 + 5.5 * y + 3.9)
80
+ bl = 0.5 + 0.45 * torch.sin(7.0 * torch.sqrt((x - 0.5) ** 2 +
81
+ (y - 0.5) ** 2) * 4.0 + 0.7)
82
+ t = torch.stack([r, g, bl], dim=-1)
83
+ return (0.08 + 0.84 * t).to(DEV)
84
+
85
+
86
+ def sky(eh=16, ew=32):
87
+ v = torch.linspace(0, 1, eh).unsqueeze(1).unsqueeze(2)
88
+ top = torch.tensor([0.32, 0.52, 1.10]) * 1.25
89
+ hor = torch.tensor([1.15, 0.72, 0.42])
90
+ e = top * (1 - v) + hor * v
91
+ return e.expand(eh, ew, 3).contiguous().to(DEV)
92
+
93
+
94
+ cam = ptd.Camera(position=(7.0, 4.2, 9.0), look_at=(0.0, 1.1, 0.4),
95
+ vfov_deg=42.0)
96
+
97
+ tex_t = plasma()
98
+ gold_t = torch.tensor([1.0, 0.72, 0.30], device=DEV)
99
+ env_t = sky()
100
+ target_scene = build_scene(tex_t, gold_t, env_t)
101
+ target_hi = ptd.render(target_scene, cam, H, W, spp=200, max_bounces=6,
102
+ seed=3).detach()
103
+ target_lo = ptd.render(target_scene, cam, H, W, spp=8, max_bounces=6,
104
+ seed=3).detach()
105
+
106
+ tex = torch.full((64, 64, 3), 0.45, device=DEV, requires_grad=True)
107
+ gold = torch.full((3,), 0.55, device=DEV, requires_grad=True)
108
+ env = torch.full((16, 32, 3), 0.35, device=DEV, requires_grad=True)
109
+ scene = build_scene(tex, gold, env)
110
+ opt = torch.optim.Adam([tex, gold, env], lr=0.06)
111
+
112
+ ITERS = 240
113
+ snaps = sorted({0, 1, 2, 3, 4, 5, 7, 9, 12, 15, 19, 24, 30, 38, 48, 60, 75,
114
+ 95, 120, 150, 190, 239})
115
+
116
+
117
+ def tonemap(img):
118
+ x = img.clamp(0, 1) ** (1 / 2.2)
119
+ return (x * 255).byte().cpu().numpy()
120
+
121
+
122
+ def panel(left_img):
123
+ gap = 12
124
+ fr = Image.new("RGB", (W * 2 * 2 + gap, H * 2), (16, 16, 16))
125
+ li = Image.fromarray(tonemap(left_img)).resize((W * 2, H * 2),
126
+ Image.LANCZOS)
127
+ ri = Image.fromarray(tonemap(target_hi)).resize((W * 2, H * 2),
128
+ Image.LANCZOS)
129
+ fr.paste(li, (0, 0))
130
+ fr.paste(ri, (W * 2 + gap, 0))
131
+ return fr
132
+
133
+
134
+ frames = []
135
+ losses = []
136
+ for it in range(ITERS):
137
+ opt.zero_grad()
138
+ img = ptd.render(scene, cam, H, W, spp=8, max_bounces=6, seed=3)
139
+ loss = (img - target_lo).square().mean()
140
+ loss.backward()
141
+ opt.step()
142
+ with torch.no_grad():
143
+ tex.clamp_(0.02, 0.98)
144
+ gold.clamp_(0.02, 1.0)
145
+ env.clamp_(0.0, 3.0)
146
+ losses.append(loss.item())
147
+ if it in snaps:
148
+ frames.append(panel(img.detach()))
149
+
150
+ final_hi = ptd.render(scene, cam, H, W, spp=200, max_bounces=6,
151
+ seed=9).detach()
152
+ for _ in range(10):
153
+ frames.append(panel(final_hi))
154
+
155
+ durations = [240] * 6 + [140] * (len(frames) - 16) + [140] * 9 + [2600]
156
+ gif = os.path.join(ROOT, "media", "inverse.gif")
157
+ frames[0].save(gif, save_all=True, append_images=frames[1:],
158
+ duration=durations, loop=0, optimize=True)
159
+ chk = Image.open(gif)
160
+ size = os.path.getsize(gif) / 1e6
161
+ tex_err = (tex.detach() - tex_t).abs().mean().item()
162
+ gold_err = (gold.detach() - gold_t).abs().max().item()
163
+ env_err = (env.detach() - env_t).abs().mean().item()
164
+ print(f"loss {losses[0]:.2e} -> {losses[-1]:.2e} in {ITERS} steps")
165
+ print(f"floor texture mean abs err {tex_err:.3f}; gold tint max err "
166
+ f"{gold_err:.3f}; env mean abs err {env_err:.3f}")
167
+ print(f"wrote {gif} ({size:.1f} MB, {chk.n_frames} frames)")