phanerozoic commited on
Commit
f65a7f1
·
verified ·
1 Parent(s): aa9c68a

Hero: three acts (appearance, geometry, fog), 384px

Browse files
Files changed (3) hide show
  1. CARD.md +12 -6
  2. README.md +12 -6
  3. media/make_hero.py +241 -76
CARD.md CHANGED
@@ -22,14 +22,20 @@ and camera silhouettes. Inverse rendering runs as a bare torch loop around
22
  the kernel, with no rendering framework in the loop: render, compare,
23
  `backward()`, step.
24
 
25
- ![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)
26
 
27
  *An inverse-rendering time-lapse produced entirely by this kernel
28
- (`media/make_hero.py`): from a gray start, Adam through `render()`
29
- recovers a 64x64 floor texture (12,288 unknowns), the gold conductor's
30
- tint, and the environment map against the fixed target on the right,
31
- with a glass box refracting the lot. Final parameter errors: texture
32
- 0.047 mean, tint 0.007 max, environment 0.081 mean.*
 
 
 
 
 
 
33
 
34
  ## Usage
35
 
 
22
  the kernel, with no rendering framework in the loop: render, compare,
23
  `backward()`, step.
24
 
25
+ ![inverse rendering time-lapse in three acts: a dim gray scene converging to a plasma-textured floor with a gold monolith, red plastic block, glass boxes, and a glowing pi sign; a floating slab sliding home by its shadow; a Cornell box recovering its fog](https://huggingface.co/kernels/phanerozoic/pathtracer-diff/resolve/main/media/inverse.gif)
26
 
27
  *An inverse-rendering time-lapse produced entirely by this kernel
28
+ (`media/make_hero.py`), live optimization frames against the fixed
29
+ target on the right. Act 1: from a dim gray start, Adam through
30
+ `render()` recovers a 128x128 per-texel floor albedo (49,152 unknowns),
31
+ the gold conductor's tint (max err 0.005), a rough-plastic block's
32
+ albedo through its coat (0.002), a 16x32 per-texel emissive pi sign
33
+ (mean err 0.007), and the environment map, with a smooth glass box and
34
+ a frosted rough-dielectric block refracting the scene; image loss falls
35
+ 8.3e-1 to 7.8e-6. Act 2: `geometry_grad()` alone slides the floating
36
+ slab home from its shadow and silhouette, offset 2.00 to 0.001 in 130
37
+ steps. Act 3: a Cornell box recovers its participating-medium
38
+ coefficients from a hazy target; extinction max err 0.002.*
39
 
40
  ## Usage
41
 
README.md CHANGED
@@ -22,14 +22,20 @@ and camera silhouettes. Inverse rendering runs as a bare torch loop around
22
  the kernel, with no rendering framework in the loop: render, compare,
23
  `backward()`, step.
24
 
25
- ![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)
26
 
27
  *An inverse-rendering time-lapse produced entirely by this kernel
28
- (`media/make_hero.py`): from a gray start, Adam through `render()`
29
- recovers a 64x64 floor texture (12,288 unknowns), the gold conductor's
30
- tint, and the environment map against the fixed target on the right,
31
- with a glass box refracting the lot. Final parameter errors: texture
32
- 0.047 mean, tint 0.007 max, environment 0.081 mean.*
 
 
 
 
 
 
33
 
34
  ## Usage
35
 
 
22
  the kernel, with no rendering framework in the loop: render, compare,
23
  `backward()`, step.
24
 
25
+ ![inverse rendering time-lapse in three acts: a dim gray scene converging to a plasma-textured floor with a gold monolith, red plastic block, glass boxes, and a glowing pi sign; a floating slab sliding home by its shadow; a Cornell box recovering its fog](https://huggingface.co/kernels/phanerozoic/pathtracer-diff/resolve/main/media/inverse.gif)
26
 
27
  *An inverse-rendering time-lapse produced entirely by this kernel
28
+ (`media/make_hero.py`), live optimization frames against the fixed
29
+ target on the right. Act 1: from a dim gray start, Adam through
30
+ `render()` recovers a 128x128 per-texel floor albedo (49,152 unknowns),
31
+ the gold conductor's tint (max err 0.005), a rough-plastic block's
32
+ albedo through its coat (0.002), a 16x32 per-texel emissive pi sign
33
+ (mean err 0.007), and the environment map, with a smooth glass box and
34
+ a frosted rough-dielectric block refracting the scene; image loss falls
35
+ 8.3e-1 to 7.8e-6. Act 2: `geometry_grad()` alone slides the floating
36
+ slab home from its shadow and silhouette, offset 2.00 to 0.001 in 130
37
+ steps. Act 3: a Cornell box recovers its participating-medium
38
+ coefficients from a hazy target; extinction max err 0.002.*
39
 
40
  ## Usage
41
 
media/make_hero.py CHANGED
@@ -1,10 +1,20 @@
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
@@ -21,7 +31,8 @@ import load_local
21
  ptd = load_local.load()
22
 
23
  DEV = "cuda"
24
- H = W = 256
 
25
 
26
 
27
  def quad(a, b, c, d):
@@ -46,94 +57,136 @@ def add_box(verts, faces, mat, uv, lo, hi, mid, rot_deg=0.0):
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()
@@ -142,26 +195,138 @@ for it in range(ITERS):
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)")
 
1
+ """Hero time-lapse for the card: inverse rendering, live, every feature.
2
 
3
+ Act 1 (appearance): from a dim gray start, Adam through render() recovers
4
+ simultaneously a 128x128 per-texel floor albedo (49,152 unknowns), the
5
+ gold GGX conductor's tint, a rough-plastic block's albedo through its
6
+ specular coat, a 16x32 per-texel emissive sign (a glowing pi glyph), and
7
+ the environment map, with a smooth glass box and a frosted
8
+ rough-dielectric block refracting the scene throughout.
9
+
10
+ Act 2 (geometry): with appearance fixed, geometry_grad() alone recovers
11
+ the position of a floating occluder slab from its shadow and its image
12
+ silhouette (dual-number interior + edge-sampled boundary terms).
13
+
14
+ Act 3 (fog): a Cornell box recovers its homogeneous medium coefficients
15
+ (sigma_a, sigma_s) from a hazy target under detached distance sampling.
16
+
17
+ Output: media/inverse.gif, live panel | target panel, 384x384 native.
18
  """
19
  import math
20
  import os
 
31
  ptd = load_local.load()
32
 
33
  DEV = "cuda"
34
+ H = W = 384
35
+ GAP = 12
36
 
37
 
38
  def quad(a, b, c, d):
 
57
  uv.extend([[(0, 0)] * 3] * 2)
58
 
59
 
60
+ OCC_CENTER = (0.5, 0.3) # target x/z offset of the occluder box
61
+
62
+
63
+ def build_scene(floor_tex, gold, plast, sign_emi, env, occ_t):
64
  verts, faces, mat, uv = [], [], [], []
65
  b = len(verts)
66
  verts.extend([(-5, 0, -5), (5, 0, -5), (5, 0, 5), (-5, 0, 5)])
67
  faces.extend(quad(b, b + 1, b + 2, b + 3))
68
  mat.extend([0, 0])
69
  uv.extend([[(0, 0), (1, 0), (1, 1)], [(0, 0), (1, 1), (0, 1)]])
70
+ add_box(verts, faces, mat, uv, (1.2, 0, -0.9), (2.4, 3.0, 0.3), 1, 22.0)
71
+ add_box(verts, faces, mat, uv, (-2.6, 0, 0.6), (-1.0, 2.0, 2.2), 2, 10.0)
72
+ add_box(verts, faces, mat, uv, (-1.0, 0, -2.8), (0.4, 1.9, -1.5), 3, -15.0)
73
+ add_box(verts, faces, mat, uv, (2.9, 0, 2.2), (4.2, 1.3, 3.5), 4, 35.0)
74
  b = len(verts)
75
+ verts.extend([(-1.5, 6.8, -1.5), (1.5, 6.8, -1.5), (1.5, 6.8, 1.5),
76
+ (-1.5, 6.8, 1.5)])
77
  faces.extend(quad(b, b + 1, b + 2, b + 3))
78
+ mat.extend([5, 5])
79
  uv.extend([[(0, 0)] * 3] * 2)
80
+ b = len(verts)
81
+ verts.extend([(-4.3, 3.7, -4.8), (-1.1, 3.7, -4.8),
82
+ (-1.1, 5.3, -4.8), (-4.3, 5.3, -4.8)])
83
+ faces.extend(quad(b, b + 1, b + 2, b + 3))
84
+ mat.extend([6, 6])
85
+ uv.extend([[(0, 0), (1, 0), (1, 1)], [(0, 0), (1, 1), (0, 1)]])
86
+ occ0 = len(verts)
87
+ tx, tz = OCC_CENTER[0] + float(occ_t[0]), OCC_CENTER[1] + float(occ_t[1])
88
+ add_box(verts, faces, mat, uv, (-1.0 + tx, 3.1, -1.0 + tz),
89
+ (1.0 + tx, 3.35, 1.0 + tz), 7, 0.0)
90
+ scene = ptd.Scene(
91
+ verts, faces, mat,
92
+ albedo=[floor_tex, gold,
93
+ torch.tensor([1.0, 1.0, 1.0], device=DEV),
94
+ plast,
95
+ torch.tensor([1.0, 1.0, 1.0], device=DEV),
96
+ torch.tensor([0.8, 0.8, 0.8], device=DEV),
97
+ torch.tensor([0.06, 0.06, 0.10], device=DEV),
98
+ torch.tensor([0.16, 0.15, 0.17], device=DEV)],
99
+ emission=[torch.zeros(3), torch.zeros(3), torch.zeros(3),
100
+ torch.zeros(3), torch.zeros(3),
101
+ torch.tensor([26.0, 23.0, 19.0]), sign_emi, torch.zeros(3)],
102
+ material_types=[ptd.DIFFUSE, ptd.CONDUCTOR, ptd.DIELECTRIC,
103
+ ptd.PLASTIC, ptd.ROUGH_DIELECTRIC, ptd.DIFFUSE,
104
+ ptd.DIFFUSE, ptd.DIFFUSE],
105
+ roughness=[0.3, 0.10, 0.0, 0.20, 0.12, 0.3, 0.3, 0.3],
106
+ ior=[1.5] * 8, uvs=uv, env=env)
107
+ return scene, occ0
108
+
109
+
110
+ def plasma(n=128):
111
  y, x = torch.meshgrid(torch.linspace(0, 1, n), torch.linspace(0, 1, n),
112
  indexing="ij")
113
+ r = 0.5 + 0.5 * torch.sin(9.0 * x + 11.0 * y * y + 1.2)
114
+ g = 0.5 + 0.5 * torch.sin(10.0 * (x - 0.5) ** 2 + 7.5 * y + 3.9)
115
+ bl = 0.5 + 0.5 * torch.sin(9.0 * torch.sqrt((x - 0.5) ** 2 +
116
+ (y - 0.5) ** 2) * 4.0 + 0.7)
117
  t = torch.stack([r, g, bl], dim=-1)
118
+ return (0.04 + 0.92 * t).clamp(0.02, 0.98).to(DEV)
119
+
120
+
121
+ def pi_sign(h=16, w=32):
122
+ # texture row 0 sits at the BOTTOM of the sign quad (v runs upward)
123
+ e = torch.full((h, w, 3), 0.10)
124
+ e[..., 2] = 0.35 # dim blue field
125
+ glyph = torch.zeros(h, w, dtype=torch.bool)
126
+ glyph[11:13, 7:25] = True # bar (near the top)
127
+ glyph[3:11, 10:13] = True # left leg
128
+ glyph[4:11, 19:22] = True # right leg
129
+ glyph[3:5, 19:24] = True # right foot flick
130
+ bright = torch.tensor([11.0, 17.0, 18.0])
131
+ e[glyph] = bright
132
+ return e.to(DEV)
133
 
134
 
135
  def sky(eh=16, ew=32):
136
  v = torch.linspace(0, 1, eh).unsqueeze(1).unsqueeze(2)
137
+ top = torch.tensor([0.12, 0.18, 0.42])
138
+ hor = torch.tensor([0.85, 0.42, 0.22])
139
  e = top * (1 - v) + hor * v
140
  return e.expand(eh, ew, 3).contiguous().to(DEV)
141
 
142
 
143
+ cam = ptd.Camera(position=(7.4, 5.0, 9.4), look_at=(0.0, 1.3, -0.4),
144
+ vfov_deg=43.0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
 
146
 
147
  def tonemap(img):
148
+ x = (img * 1.25).clamp(0, 1) ** (1 / 2.2)
149
  return (x * 255).byte().cpu().numpy()
150
 
151
 
152
+ def panel(left_img, right_np):
153
+ fr = Image.new("RGB", (W * 2 + GAP, H), (14, 14, 14))
154
+ fr.paste(Image.fromarray(tonemap(left_img)), (0, 0))
155
+ fr.paste(Image.fromarray(right_np), (W + GAP, 0))
 
 
 
 
 
156
  return fr
157
 
158
 
159
+ # ---- act 1: targets
160
+ tex_t = plasma()
161
+ gold_t = torch.tensor([1.0, 0.72, 0.30], device=DEV)
162
+ plast_t = torch.tensor([0.85, 0.12, 0.10], device=DEV)
163
+ sign_t = pi_sign()
164
+ env_t = sky()
165
+ t_target = torch.zeros(2)
166
+ target_scene, _ = build_scene(tex_t, gold_t, plast_t, sign_t, env_t, t_target)
167
+ target_hi = ptd.render(target_scene, cam, H, W, spp=768, max_bounces=6,
168
+ seed=9).detach()
169
+ target_lo = ptd.render(target_scene, cam, H, W, spp=8, max_bounces=6,
170
+ seed=3).detach()
171
+ target_np = tonemap(target_hi)
172
+
173
+ # ---- act 1: appearance parameters (dim gray start; occluder at target)
174
+ tex = torch.full((128, 128, 3), 0.35, device=DEV, requires_grad=True)
175
+ gold = torch.full((3,), 0.40, device=DEV, requires_grad=True)
176
+ plast = torch.full((3,), 0.40, device=DEV, requires_grad=True)
177
+ sign = torch.full((16, 32, 3), 1.5, device=DEV, requires_grad=True)
178
+ env = torch.full((16, 32, 3), 0.12, device=DEV, requires_grad=True)
179
+ opt = torch.optim.Adam([
180
+ {"params": [tex, gold, plast, env], "lr": 0.06},
181
+ {"params": [sign], "lr": 0.12},
182
+ ])
183
+
184
+ ITERS = 480
185
+ STRIDE = 4
186
+ frames, durations = [], []
187
  losses = []
188
  for it in range(ITERS):
189
+ scene, _ = build_scene(tex, gold, plast, sign, env, t_target)
190
  opt.zero_grad()
191
  img = ptd.render(scene, cam, H, W, spp=8, max_bounces=6, seed=3)
192
  loss = (img - target_lo).square().mean()
 
195
  with torch.no_grad():
196
  tex.clamp_(0.02, 0.98)
197
  gold.clamp_(0.02, 1.0)
198
+ plast.clamp_(0.02, 0.98)
199
+ sign.clamp_(0.0, 30.0)
200
  env.clamp_(0.0, 3.0)
201
  losses.append(loss.item())
202
+ if it % STRIDE == 0:
203
+ frames.append(panel(img.detach(), target_np))
204
+ durations.append(120 if it < 24 else 55)
205
 
206
+ scene, _ = build_scene(tex, gold, plast, sign, env, t_target)
207
+ final_hi = ptd.render(scene, cam, H, W, spp=768, max_bounces=6,
208
  seed=9).detach()
209
+ frames.append(panel(final_hi, target_np))
210
+ durations.append(2600)
211
+
212
+ # ---- act 2: geometry only, appearance fixed at the recovered values
213
+ occ_t = torch.tensor([-1.6, 1.2], requires_grad=True) # displaced start
214
+ opt_g = torch.optim.Adam([occ_t], lr=0.04)
215
+ texf = tex.detach()
216
+ goldf = gold.detach()
217
+ plastf = plast.detach()
218
+ signf = sign.detach()
219
+ envf = env.detach()
220
+ GEO_ITERS = 130
221
+ for it in range(GEO_ITERS):
222
+ scene, occ0 = build_scene(texf, goldf, plastf, signf, envf,
223
+ occ_t.detach())
224
+ img = ptd.render(scene, cam, H, W, spp=8, max_bounces=6,
225
+ seed=3).detach()
226
+ dldi = (2.0 / img.numel()) * (img - target_lo)
227
+ gv = ptd.geometry_grad(scene, cam, dldi, spp=16, edge_samples=1 << 17,
228
+ seed=17 + it)
229
+ opt_g.zero_grad()
230
+ g = gv[occ0:occ0 + 8][:, [0, 2]].sum(dim=0).cpu()
231
+ gn = g.norm()
232
+ if gn > 0.5: # near-contact shadow geometry can spike; clip
233
+ g = g * (0.5 / gn)
234
+ occ_t.grad = g
235
+ opt_g.step()
236
+ if it % 2 == 0:
237
+ frames.append(panel(img, target_np))
238
+ durations.append(55)
239
+
240
+ scene, _ = build_scene(texf, goldf, plastf, signf, envf, occ_t.detach())
241
+ geo_hi = ptd.render(scene, cam, H, W, spp=768, max_bounces=6,
242
+ seed=9).detach()
243
+ frames.append(panel(geo_hi, target_np))
244
+ durations.append(2600)
245
+
246
+ # ---- act 3: fog recovery in a Cornell box
247
+ def cornell_scene(sa, ss):
248
+ verts, faces, mat = [], [], []
249
+
250
+ def wall(a, b, c, d, m):
251
+ b0 = len(verts)
252
+ verts.extend([a, b, c, d])
253
+ faces.extend(quad(b0, b0 + 1, b0 + 2, b0 + 3))
254
+ mat.extend([m, m])
255
+
256
+ X, Y, Z = 5.56, 5.488, 5.592
257
+ wall((0, 0, 0), (X, 0, 0), (X, 0, Z), (0, 0, Z), 0)
258
+ wall((0, Y, 0), (0, Y, Z), (X, Y, Z), (X, Y, 0), 0)
259
+ wall((0, 0, Z), (X, 0, Z), (X, Y, Z), (0, Y, Z), 0)
260
+ wall((0, 0, 0), (0, 0, Z), (0, Y, Z), (0, Y, 0), 1)
261
+ wall((X, 0, 0), (X, Y, 0), (X, Y, Z), (X, 0, Z), 2)
262
+ wall((2.13, Y - 0.001, 2.27), (3.43, Y - 0.001, 2.27),
263
+ (3.43, Y - 0.001, 3.32), (2.13, Y - 0.001, 3.32), 3)
264
+ sc = ptd.Scene(verts, faces, mat,
265
+ albedo=[[0.73] * 3, [0.65, 0.05, 0.05],
266
+ [0.12, 0.45, 0.15], [0.78] * 3],
267
+ emission=[[0, 0, 0], [0, 0, 0], [0, 0, 0],
268
+ [18.4, 15.6, 8.0]],
269
+ medium=(sa, ss))
270
+ sc.med_sbar = 0.10 # fixed detached rate across the optimization
271
+ return sc
272
+
273
+
274
+ fcam = ptd.Camera(position=(2.78, 2.73, -8.0), look_at=(2.78, 2.73, 2.8),
275
+ vfov_deg=39.0)
276
+ sa_t = torch.tensor([0.030, 0.055, 0.085], device=DEV)
277
+ ss_t = torch.tensor([0.110, 0.095, 0.075], device=DEV)
278
+ fog_target_hi = ptd.render(cornell_scene(sa_t, ss_t), fcam, H, W, spp=512,
279
+ max_bounces=6, seed=9).detach()
280
+ fog_target_lo = ptd.render(cornell_scene(sa_t, ss_t), fcam, H, W, spp=16,
281
+ max_bounces=6, seed=5).detach()
282
+ fog_target_np = tonemap(fog_target_hi)
283
+
284
+ sa = torch.full((3,), 0.004, device=DEV, requires_grad=True)
285
+ ss = torch.full((3,), 0.004, device=DEV, requires_grad=True)
286
+ fog_scene = cornell_scene(sa.detach().clone(), ss.detach().clone())
287
+ opt_f = torch.optim.Adam([sa, ss], lr=0.006)
288
+ FOG_ITERS = 240
289
+ fog_loss0 = None
290
+ for it in range(FOG_ITERS):
291
+ opt_f.zero_grad()
292
+ fog_scene.med_sa = sa
293
+ fog_scene.med_ss = ss
294
+ img = ptd.render(fog_scene, fcam, H, W, spp=16, max_bounces=6, seed=5)
295
+ loss = (img - fog_target_lo).square().mean()
296
+ loss.backward()
297
+ opt_f.step()
298
+ with torch.no_grad():
299
+ sa.clamp_(1e-4, 1.0)
300
+ ss.clamp_(1e-4, 1.0)
301
+ if fog_loss0 is None:
302
+ fog_loss0 = loss.item()
303
+ if it % 6 == 0:
304
+ frames.append(panel(img.detach(), fog_target_np))
305
+ durations.append(55)
306
+
307
+ fog_scene.med_sa = sa.detach()
308
+ fog_scene.med_ss = ss.detach()
309
+ fog_hi = ptd.render(fog_scene, fcam, H, W, spp=512, max_bounces=6,
310
+ seed=9).detach()
311
+ frames.append(panel(fog_hi, fog_target_np))
312
+ durations.append(2600)
313
 
 
314
  gif = os.path.join(ROOT, "media", "inverse.gif")
315
  frames[0].save(gif, save_all=True, append_images=frames[1:],
316
  duration=durations, loop=0, optimize=True)
317
  chk = Image.open(gif)
318
  size = os.path.getsize(gif) / 1e6
319
+ print(f"act 1 loss {losses[0]:.2e} -> {losses[-1]:.2e} in {ITERS} steps")
320
+ print(f"floor tex mean err {(tex.detach() - tex_t).abs().mean():.3f}; "
321
+ f"gold max err {(gold.detach() - gold_t).abs().max():.3f}; "
322
+ f"plastic max err {(plast.detach() - plast_t).abs().max():.3f}")
323
+ print(f"sign mean err {(sign.detach() - sign_t).abs().mean():.3f}; "
324
+ f"env mean err {(env.detach() - env_t).abs().mean():.3f}")
325
+ print(f"act 2 occluder offset residual {occ_t.detach().norm():.3f} "
326
+ f"(start 2.00) in {GEO_ITERS} geometry-only steps")
327
+ st_err = ((sa + ss).detach() - (sa_t + ss_t)).abs().max()
328
+ print(f"fog: loss {fog_loss0:.2e} -> {loss.item():.2e}; "
329
+ f"sigma_a err {(sa.detach() - sa_t).abs().max():.4f}, "
330
+ f"sigma_s err {(ss.detach() - ss_t).abs().max():.4f}, "
331
+ f"sigma_t err {st_err:.4f}")
332
  print(f"wrote {gif} ({size:.1f} MB, {chk.n_frames} frames)")