Per-texel albedo textures with bilinear-footprint adjoints
Browse files- CARD.md +112 -90
- README.md +113 -91
- dev/report_local.py +199 -29
- local_bind.cpp +90 -42
- pathtracer_diff_cuda/pathtracer.cu +538 -233
- pathtracer_diff_cuda/pathtracer_launch.h +37 -21
- tests/test_pathtracer_diff.py +212 -0
- torch-ext/pathtracer_diff/__init__.py +380 -81
- torch-ext/torch_binding.cpp +118 -57
- torch-ext/torch_binding.h +14 -5
CARD.md
CHANGED
|
@@ -6,18 +6,17 @@ license: apache-2.0
|
|
| 6 |
# pathtracer-diff
|
| 7 |
|
| 8 |
A differentiable Monte Carlo path tracer as one kernel, loadable through
|
| 9 |
-
`kernels`. The forward pass
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
handful of scalars.
|
| 21 |
|
| 22 |
## Usage
|
| 23 |
|
|
@@ -28,67 +27,73 @@ from kernels import get_kernel
|
|
| 28 |
ptd = get_kernel("phanerozoic/pathtracer-diff", version=1, trust_remote_code=True)
|
| 29 |
|
| 30 |
wall_tex = torch.full((256, 256, 3), 0.5, device="cuda", requires_grad=True)
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
cam = ptd.Camera(position=(2.78, 2.73, -8.0), look_at=(2.78, 2.73, 2.8),
|
| 36 |
vfov_deg=39.0)
|
| 37 |
|
| 38 |
img = ptd.render(scene, cam, 512, 512, spp=64, max_bounces=4) # [H, W, 3] linear
|
| 39 |
loss = (img - target).square().mean()
|
| 40 |
-
loss.backward()
|
| 41 |
```
|
| 42 |
|
| 43 |
`version` selects the release branch; `trust_remote_code` is required by
|
| 44 |
`kernels` for publishers without the trusted-publisher mark.
|
| 45 |
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
landscape at any spp; that is the configuration an inverse-rendering loop
|
| 52 |
-
wants.
|
| 53 |
|
| 54 |
## API
|
| 55 |
|
| 56 |
| Symbol | Purpose |
|
| 57 |
|---|---|
|
| 58 |
-
| `Scene(vertices, faces, material_ids, albedo, emission, uvs)` | triangle-mesh scene;
|
| 59 |
| `Camera(position, look_at, up, vfov_deg)` | pinhole camera |
|
| 60 |
-
| `render(scene, camera, height, width, spp, max_bounces,
|
|
|
|
| 61 |
| `ops.pt_forward` / `ops.pt_backward` | raw kernel launches |
|
| 62 |
|
| 63 |
## How it works
|
| 64 |
|
| 65 |
One thread owns one pixel and accumulates its spp samples serially, so the
|
| 66 |
-
image is bitwise deterministic with no atomics in the forward pass.
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
## Correctness
|
| 94 |
|
|
@@ -96,57 +101,74 @@ Measured on RTX 6000 Ada (sm89) from a source build; the published `v1`
|
|
| 96 |
variants additionally pass the full suite through `get_kernel` on a GeForce
|
| 97 |
RTX 3070 Ti (Ampere sm86, Linux, torch 2.13, cu126):
|
| 98 |
|
| 99 |
-
- **Analytic furnace, zero variance.**
|
| 100 |
-
sampling
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
- **
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
- **
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
## Measured
|
| 121 |
|
| 122 |
-
RTX 6000 Ada,
|
| 123 |
-
|
|
|
|
| 124 |
|
| 125 |
-
|
|
| 126 |
-
|---|---|---|
|
| 127 |
-
|
|
| 128 |
-
|
|
|
|
|
| 129 |
|
| 130 |
## Requirements and limits
|
| 131 |
|
| 132 |
- NVIDIA GPU with compute capability 8.0+; float32 throughout.
|
| 133 |
-
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
## References
|
| 144 |
|
| 145 |
Kajiya, "The Rendering Equation" (SIGGRAPH 1986); Vicini, Speierer, Jakob,
|
| 146 |
"Path Replay Backpropagation" (SIGGRAPH 2021); Nimier-David, Speierer,
|
| 147 |
-
Ruiz, Jakob, "Radiative Backpropagation" (SIGGRAPH 2020);
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
## License
|
| 152 |
|
|
|
|
| 6 |
# pathtracer-diff
|
| 7 |
|
| 8 |
A differentiable Monte Carlo path tracer as one kernel, loadable through
|
| 9 |
+
`kernels`. The forward pass is a megakernel unidirectional path tracer:
|
| 10 |
+
Lambertian diffuse, GGX conductor (VNDF-sampled, height-correlated Smith),
|
| 11 |
+
and smooth dielectric materials; area lights and an importance-sampled
|
| 12 |
+
equirectangular environment map; multiple importance sampling (balance
|
| 13 |
+
heuristic) by default; per-texel albedo with bilinear filtering; a
|
| 14 |
+
binned-SAH BVH built in vectorized torch in fractions of a second at a
|
| 15 |
+
million triangles. The backward pass replays the identical paths with the
|
| 16 |
+
identical counter-based random draws and emits analytic gradients to the
|
| 17 |
+
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 |
|
|
|
|
| 27 |
ptd = get_kernel("phanerozoic/pathtracer-diff", version=1, trust_remote_code=True)
|
| 28 |
|
| 29 |
wall_tex = torch.full((256, 256, 3), 0.5, device="cuda", requires_grad=True)
|
| 30 |
+
env = torch.full((64, 128, 3), 0.8, device="cuda", requires_grad=True)
|
| 31 |
+
scene = ptd.Scene(vertices, faces, material_ids,
|
| 32 |
+
albedo=[torch.tensor([0.73] * 3), # constants are 1x1 textures
|
| 33 |
+
wall_tex, # [H, W, 3] per-texel albedo
|
| 34 |
+
torch.tensor([0.9, 0.6, 0.3])],
|
| 35 |
+
emission=emission, uvs=uvs,
|
| 36 |
+
material_types=[ptd.DIFFUSE, ptd.DIFFUSE, ptd.CONDUCTOR],
|
| 37 |
+
roughness=[0.3, 0.3, 0.2], env=env)
|
| 38 |
cam = ptd.Camera(position=(2.78, 2.73, -8.0), look_at=(2.78, 2.73, 2.8),
|
| 39 |
vfov_deg=39.0)
|
| 40 |
|
| 41 |
img = ptd.render(scene, cam, 512, 512, spp=64, max_bounces=4) # [H, W, 3] linear
|
| 42 |
loss = (img - target).square().mean()
|
| 43 |
+
loss.backward() # gradients on wall_tex, emission, and env
|
| 44 |
```
|
| 45 |
|
| 46 |
`version` selects the release branch; `trust_remote_code` is required by
|
| 47 |
`kernels` for publishers without the trusted-publisher mark.
|
| 48 |
|
| 49 |
+
A fixed `seed` renders the same paths every call, so the Monte Carlo
|
| 50 |
+
objective is a deterministic function of the parameters and gradient
|
| 51 |
+
descent sees a smooth landscape at any spp; that is the configuration an
|
| 52 |
+
inverse-rendering loop wants. `estimator` selects `"mis"` (default),
|
| 53 |
+
`"nee"`, or `"brdf"`.
|
|
|
|
|
|
|
| 54 |
|
| 55 |
## API
|
| 56 |
|
| 57 |
| Symbol | Purpose |
|
| 58 |
|---|---|
|
| 59 |
+
| `Scene(vertices, faces, material_ids, albedo, emission, uvs, material_types, roughness, ior, env)` | triangle-mesh scene; BVH, light list, and the detached environment CDF are built at construction; albedo is `[M, 3]` constants or a list of `[Hm, Wm, 3]` textures; `env` is an optional `[Eh, Ew, 3]` equirect map; texture/env shapes are fixed at construction |
|
| 60 |
| `Camera(position, look_at, up, vfov_deg)` | pinhole camera |
|
| 61 |
+
| `render(scene, camera, height, width, spp, max_bounces, estimator, seed)` | differentiable render to `[H, W, 3]` f32 linear radiance; autograd to albedo texels, emission, and env texels |
|
| 62 |
+
| `DIFFUSE`, `CONDUCTOR`, `DIELECTRIC` | material type constants |
|
| 63 |
| `ops.pt_forward` / `ops.pt_backward` | raw kernel launches |
|
| 64 |
|
| 65 |
## How it works
|
| 66 |
|
| 67 |
One thread owns one pixel and accumulates its spp samples serially, so the
|
| 68 |
+
image is bitwise deterministic with no atomics in the forward pass.
|
| 69 |
+
Diffuse vertices sample the cosine hemisphere (the throughput multiplier
|
| 70 |
+
is then exactly the albedo); conductors sample the GGX visible-normal
|
| 71 |
+
distribution (Heitz 2018) with the height-correlated Smith term, so the
|
| 72 |
+
continuation weight is `F * G2/G1`; smooth dielectrics branch on the
|
| 73 |
+
Fresnel term between reflection and refraction. Direct light is estimated
|
| 74 |
+
at every non-delta vertex by area sampling over the emissive faces and,
|
| 75 |
+
when an environment map is present, by sampling a detached mixture of its
|
| 76 |
+
luminance CDF and the uniform sphere; BSDF-sampled hits on emitters and
|
| 77 |
+
environment misses are combined with the balance heuristic, so all three
|
| 78 |
+
estimators agree in expectation at any bounce budget.
|
| 79 |
+
|
| 80 |
+
The backward pass is exact path replay. Sampling distributions depend only
|
| 81 |
+
on geometry, frozen material parameters (roughness, ior), the
|
| 82 |
+
construction-time environment CDF, and the counter-based Philox stream
|
| 83 |
+
keyed by `(seed, pixel, sample)`, never on the differentiable parameters,
|
| 84 |
+
so the backward kernel re-traces identical paths with identical draws and
|
| 85 |
+
no stored path state. Every radiance term is a product of per-bounce
|
| 86 |
+
factors, each affine in its vertex's albedo texels (Schlick Fresnel is
|
| 87 |
+
affine in F0; dielectric factors are constant), times a linear emission or
|
| 88 |
+
environment texel; the replay differentiates the product in closed form
|
| 89 |
+
with prefix/suffix exclusion products (no division, so zero albedos are
|
| 90 |
+
safe) and scatters each factor's gradient into the four texels of its
|
| 91 |
+
bilinear footprint with the sampling weights, the exact adjoint of the
|
| 92 |
+
fetch. Texture-gradient buffers up to 2048 texels accumulate in per-block
|
| 93 |
+
shared memory and flush once; larger textures and environment gradients
|
| 94 |
+
use direct global atomics spread across the buffer. Gradients are the
|
| 95 |
+
exact derivative of the rendered estimate under frozen sampling: interior
|
| 96 |
+
terms only, no visibility/silhouette gradients.
|
| 97 |
|
| 98 |
## Correctness
|
| 99 |
|
|
|
|
| 101 |
variants additionally pass the full suite through `get_kernel` on a GeForce
|
| 102 |
RTX 3070 Ti (Ampere sm86, Linux, torch 2.13, cu126):
|
| 103 |
|
| 104 |
+
- **Analytic furnace, zero variance.** Closed uniform diffuse box under
|
| 105 |
+
BRDF sampling: every path returns exactly `E * sum a^k`; the analytic
|
| 106 |
+
value 0.498046875 is met with worst pixel deviation 3.4e-3. A diffuse
|
| 107 |
+
plane under a constant environment reproduces `albedo * c` the same way.
|
| 108 |
+
- **Fresnel slab.** A smooth glass slab (ior 1.5) at normal incidence
|
| 109 |
+
transmits the internal-bounce series `(1-R)/(1+R)`; measured 0.9222
|
| 110 |
+
against 0.923077 analytic (0.10%).
|
| 111 |
+
- **Estimator agreement.** MIS, NEE-only, and BRDF-only means agree within
|
| 112 |
+
Monte Carlo tolerance on a Cornell box, and MIS vs BRDF-only agree on a
|
| 113 |
+
GGX conductor under an environment map, which jointly exercises the VNDF
|
| 114 |
+
weight, the GGX pdf, and the environment pdf.
|
| 115 |
+
- **Gradients match finite differences along the same paths.** Constants
|
| 116 |
+
4.6e-4 max relative error over nine entries; wall-texture texels 5.2e-3;
|
| 117 |
+
conductor F0 through GGX sampling and MIS 8.4e-5; environment texels
|
| 118 |
+
2.1e-4 (detached CDF). Texels no path can see get exactly zero from both
|
| 119 |
+
the replay and the differences.
|
| 120 |
+
- **Constants are 1x1 textures.** Both albedo forms render equal images to
|
| 121 |
+
float rounding and receive matching gradients.
|
| 122 |
+
- **Inverse rendering.** A wall color (3 unknowns) recovers to max error
|
| 123 |
+
0.004 in 80 Adam steps; an 8x8 wall texture (192 unknowns) drives the
|
| 124 |
+
image loss from 1.8e-4 to 6.5e-14 with mean texel error below 5e-4 in
|
| 125 |
+
200 steps, both through the kernel alone.
|
| 126 |
+
- **Deterministic.** Repeated forward renders are bitwise identical;
|
| 127 |
+
gradient buffers accumulate through float atomics and are deterministic
|
| 128 |
+
in expectation but not bitwise.
|
| 129 |
|
| 130 |
## Measured
|
| 131 |
|
| 132 |
+
RTX 6000 Ada, 512x512, MIS, 4 bounces. The terrain scenes are displaced
|
| 133 |
+
grids with a 256x256 checker albedo texture, a GGX conductor block, and a
|
| 134 |
+
constant environment map; the SAH build is the vectorized torch builder.
|
| 135 |
|
| 136 |
+
| scene | build | forward | backward (replay) |
|
| 137 |
+
|---|---|---|---|
|
| 138 |
+
| Cornell box, 24 tris, 64 spp | - | 12.8 ms (1.31 Gpaths/s) | 59.0 ms (4.6x) |
|
| 139 |
+
| terrain, 522,254 tris, 16 spp | 0.2 s | 18.2 ms (230 Mpaths/s) | 35.3 ms (1.9x) |
|
| 140 |
+
| terrain, 1,045,470 tris, 16 spp | 0.3 s | 21.6 ms (194 Mpaths/s) | 37.9 ms (1.8x) |
|
| 141 |
|
| 142 |
## Requirements and limits
|
| 143 |
|
| 144 |
- NVIDIA GPU with compute capability 8.0+; float32 throughout.
|
| 145 |
+
- Materials: Lambertian diffuse, GGX conductor (roughness clamped to
|
| 146 |
+
>= 0.01), smooth dielectric; per-material emission; pinhole camera; at
|
| 147 |
+
most 64 materials and 16 bounces; two-sided surfaces; no Russian
|
| 148 |
+
roulette (fixed bounce budget keeps the replay exact).
|
| 149 |
+
- Differentiable parameters: albedo texels/constants (all material types;
|
| 150 |
+
conductor F0 tint), per-material emission, environment texels. Geometry,
|
| 151 |
+
camera, UVs, roughness, and ior are frozen, and there are no
|
| 152 |
+
visibility/silhouette gradients.
|
| 153 |
+
- The environment sampling CDF is detached at Scene construction (a 0.5
|
| 154 |
+
uniform-sphere mixture keeps every direction reachable, so optimizing an
|
| 155 |
+
env map from an arbitrary initialization stays unbiased); rebuild the
|
| 156 |
+
Scene to re-importance-sample a changed map.
|
| 157 |
+
- Texture and env shapes are fixed at Scene construction; UVs wrap.
|
| 158 |
+
- Published variants are Linux x86_64; on Windows `load_local.py`
|
| 159 |
+
JIT-builds the same source and exposes the identical API.
|
| 160 |
|
| 161 |
## References
|
| 162 |
|
| 163 |
Kajiya, "The Rendering Equation" (SIGGRAPH 1986); Vicini, Speierer, Jakob,
|
| 164 |
"Path Replay Backpropagation" (SIGGRAPH 2021); Nimier-David, Speierer,
|
| 165 |
+
Ruiz, Jakob, "Radiative Backpropagation" (SIGGRAPH 2020); Heitz, "Sampling
|
| 166 |
+
the GGX Distribution of Visible Normals" (JCGT 2018); Heitz,
|
| 167 |
+
"Understanding the Masking-Shadowing Function" (JCGT 2014); Veach and
|
| 168 |
+
Guibas, "Optimally Combining Sampling Techniques" (SIGGRAPH 1995); Möller
|
| 169 |
+
and Trumbore, "Fast, Minimum Storage Ray-Triangle Intersection" (1997);
|
| 170 |
+
Duff et al., "Building an Orthonormal Basis, Revisited" (JCGT 2017); Wald,
|
| 171 |
+
"On fast Construction of SAH-based Bounding Volume Hierarchies" (2007).
|
| 172 |
|
| 173 |
## License
|
| 174 |
|
README.md
CHANGED
|
@@ -6,18 +6,17 @@ license: apache-2.0
|
|
| 6 |
# pathtracer-diff
|
| 7 |
|
| 8 |
A differentiable Monte Carlo path tracer as one kernel, loadable through
|
| 9 |
-
`kernels`. The forward pass
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
handful of scalars.
|
| 21 |
|
| 22 |
## Usage
|
| 23 |
|
|
@@ -28,67 +27,73 @@ from kernels import get_kernel
|
|
| 28 |
ptd = get_kernel("phanerozoic/pathtracer-diff", version=1, trust_remote_code=True)
|
| 29 |
|
| 30 |
wall_tex = torch.full((256, 256, 3), 0.5, device="cuda", requires_grad=True)
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
cam = ptd.Camera(position=(2.78, 2.73, -8.0), look_at=(2.78, 2.73, 2.8),
|
| 36 |
vfov_deg=39.0)
|
| 37 |
|
| 38 |
img = ptd.render(scene, cam, 512, 512, spp=64, max_bounces=4) # [H, W, 3] linear
|
| 39 |
loss = (img - target).square().mean()
|
| 40 |
-
loss.backward()
|
| 41 |
```
|
| 42 |
|
| 43 |
`version` selects the release branch; `trust_remote_code` is required by
|
| 44 |
`kernels` for publishers without the trusted-publisher mark.
|
| 45 |
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
landscape at any spp; that is the configuration an inverse-rendering loop
|
| 52 |
-
wants.
|
| 53 |
|
| 54 |
## API
|
| 55 |
|
| 56 |
| Symbol | Purpose |
|
| 57 |
|---|---|
|
| 58 |
-
| `Scene(vertices, faces, material_ids, albedo, emission, uvs)` | triangle-mesh scene;
|
| 59 |
| `Camera(position, look_at, up, vfov_deg)` | pinhole camera |
|
| 60 |
-
| `render(scene, camera, height, width, spp, max_bounces,
|
|
|
|
| 61 |
| `ops.pt_forward` / `ops.pt_backward` | raw kernel launches |
|
| 62 |
|
| 63 |
## How it works
|
| 64 |
|
| 65 |
One thread owns one pixel and accumulates its spp samples serially, so the
|
| 66 |
-
image is bitwise deterministic with no atomics in the forward pass.
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
## Correctness
|
| 94 |
|
|
@@ -96,58 +101,75 @@ Measured on RTX 6000 Ada (sm89) from a source build; the published `v1`
|
|
| 96 |
variants additionally pass the full suite through `get_kernel` on a GeForce
|
| 97 |
RTX 3070 Ti (Ampere sm86, Linux, torch 2.13, cu126):
|
| 98 |
|
| 99 |
-
- **Analytic furnace, zero variance.**
|
| 100 |
-
sampling
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
- **
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
- **
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
## Measured
|
| 121 |
|
| 122 |
-
RTX 6000 Ada,
|
| 123 |
-
|
|
|
|
| 124 |
|
| 125 |
-
|
|
| 126 |
-
|---|---|---|
|
| 127 |
-
|
|
| 128 |
-
|
|
|
|
|
| 129 |
|
| 130 |
## Requirements and limits
|
| 131 |
|
| 132 |
- NVIDIA GPU with compute capability 8.0+; float32 throughout.
|
| 133 |
-
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
## References
|
| 144 |
|
| 145 |
Kajiya, "The Rendering Equation" (SIGGRAPH 1986); Vicini, Speierer, Jakob,
|
| 146 |
"Path Replay Backpropagation" (SIGGRAPH 2021); Nimier-David, Speierer,
|
| 147 |
-
Ruiz, Jakob, "Radiative Backpropagation" (SIGGRAPH 2020);
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
## License
|
| 152 |
|
| 153 |
-
Apache-2.0.
|
|
|
|
| 6 |
# pathtracer-diff
|
| 7 |
|
| 8 |
A differentiable Monte Carlo path tracer as one kernel, loadable through
|
| 9 |
+
`kernels`. The forward pass is a megakernel unidirectional path tracer:
|
| 10 |
+
Lambertian diffuse, GGX conductor (VNDF-sampled, height-correlated Smith),
|
| 11 |
+
and smooth dielectric materials; area lights and an importance-sampled
|
| 12 |
+
equirectangular environment map; multiple importance sampling (balance
|
| 13 |
+
heuristic) by default; per-texel albedo with bilinear filtering; a
|
| 14 |
+
binned-SAH BVH built in vectorized torch in fractions of a second at a
|
| 15 |
+
million triangles. The backward pass replays the identical paths with the
|
| 16 |
+
identical counter-based random draws and emits analytic gradients to the
|
| 17 |
+
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 |
|
|
|
|
| 27 |
ptd = get_kernel("phanerozoic/pathtracer-diff", version=1, trust_remote_code=True)
|
| 28 |
|
| 29 |
wall_tex = torch.full((256, 256, 3), 0.5, device="cuda", requires_grad=True)
|
| 30 |
+
env = torch.full((64, 128, 3), 0.8, device="cuda", requires_grad=True)
|
| 31 |
+
scene = ptd.Scene(vertices, faces, material_ids,
|
| 32 |
+
albedo=[torch.tensor([0.73] * 3), # constants are 1x1 textures
|
| 33 |
+
wall_tex, # [H, W, 3] per-texel albedo
|
| 34 |
+
torch.tensor([0.9, 0.6, 0.3])],
|
| 35 |
+
emission=emission, uvs=uvs,
|
| 36 |
+
material_types=[ptd.DIFFUSE, ptd.DIFFUSE, ptd.CONDUCTOR],
|
| 37 |
+
roughness=[0.3, 0.3, 0.2], env=env)
|
| 38 |
cam = ptd.Camera(position=(2.78, 2.73, -8.0), look_at=(2.78, 2.73, 2.8),
|
| 39 |
vfov_deg=39.0)
|
| 40 |
|
| 41 |
img = ptd.render(scene, cam, 512, 512, spp=64, max_bounces=4) # [H, W, 3] linear
|
| 42 |
loss = (img - target).square().mean()
|
| 43 |
+
loss.backward() # gradients on wall_tex, emission, and env
|
| 44 |
```
|
| 45 |
|
| 46 |
`version` selects the release branch; `trust_remote_code` is required by
|
| 47 |
`kernels` for publishers without the trusted-publisher mark.
|
| 48 |
|
| 49 |
+
A fixed `seed` renders the same paths every call, so the Monte Carlo
|
| 50 |
+
objective is a deterministic function of the parameters and gradient
|
| 51 |
+
descent sees a smooth landscape at any spp; that is the configuration an
|
| 52 |
+
inverse-rendering loop wants. `estimator` selects `"mis"` (default),
|
| 53 |
+
`"nee"`, or `"brdf"`.
|
|
|
|
|
|
|
| 54 |
|
| 55 |
## API
|
| 56 |
|
| 57 |
| Symbol | Purpose |
|
| 58 |
|---|---|
|
| 59 |
+
| `Scene(vertices, faces, material_ids, albedo, emission, uvs, material_types, roughness, ior, env)` | triangle-mesh scene; BVH, light list, and the detached environment CDF are built at construction; albedo is `[M, 3]` constants or a list of `[Hm, Wm, 3]` textures; `env` is an optional `[Eh, Ew, 3]` equirect map; texture/env shapes are fixed at construction |
|
| 60 |
| `Camera(position, look_at, up, vfov_deg)` | pinhole camera |
|
| 61 |
+
| `render(scene, camera, height, width, spp, max_bounces, estimator, seed)` | differentiable render to `[H, W, 3]` f32 linear radiance; autograd to albedo texels, emission, and env texels |
|
| 62 |
+
| `DIFFUSE`, `CONDUCTOR`, `DIELECTRIC` | material type constants |
|
| 63 |
| `ops.pt_forward` / `ops.pt_backward` | raw kernel launches |
|
| 64 |
|
| 65 |
## How it works
|
| 66 |
|
| 67 |
One thread owns one pixel and accumulates its spp samples serially, so the
|
| 68 |
+
image is bitwise deterministic with no atomics in the forward pass.
|
| 69 |
+
Diffuse vertices sample the cosine hemisphere (the throughput multiplier
|
| 70 |
+
is then exactly the albedo); conductors sample the GGX visible-normal
|
| 71 |
+
distribution (Heitz 2018) with the height-correlated Smith term, so the
|
| 72 |
+
continuation weight is `F * G2/G1`; smooth dielectrics branch on the
|
| 73 |
+
Fresnel term between reflection and refraction. Direct light is estimated
|
| 74 |
+
at every non-delta vertex by area sampling over the emissive faces and,
|
| 75 |
+
when an environment map is present, by sampling a detached mixture of its
|
| 76 |
+
luminance CDF and the uniform sphere; BSDF-sampled hits on emitters and
|
| 77 |
+
environment misses are combined with the balance heuristic, so all three
|
| 78 |
+
estimators agree in expectation at any bounce budget.
|
| 79 |
+
|
| 80 |
+
The backward pass is exact path replay. Sampling distributions depend only
|
| 81 |
+
on geometry, frozen material parameters (roughness, ior), the
|
| 82 |
+
construction-time environment CDF, and the counter-based Philox stream
|
| 83 |
+
keyed by `(seed, pixel, sample)`, never on the differentiable parameters,
|
| 84 |
+
so the backward kernel re-traces identical paths with identical draws and
|
| 85 |
+
no stored path state. Every radiance term is a product of per-bounce
|
| 86 |
+
factors, each affine in its vertex's albedo texels (Schlick Fresnel is
|
| 87 |
+
affine in F0; dielectric factors are constant), times a linear emission or
|
| 88 |
+
environment texel; the replay differentiates the product in closed form
|
| 89 |
+
with prefix/suffix exclusion products (no division, so zero albedos are
|
| 90 |
+
safe) and scatters each factor's gradient into the four texels of its
|
| 91 |
+
bilinear footprint with the sampling weights, the exact adjoint of the
|
| 92 |
+
fetch. Texture-gradient buffers up to 2048 texels accumulate in per-block
|
| 93 |
+
shared memory and flush once; larger textures and environment gradients
|
| 94 |
+
use direct global atomics spread across the buffer. Gradients are the
|
| 95 |
+
exact derivative of the rendered estimate under frozen sampling: interior
|
| 96 |
+
terms only, no visibility/silhouette gradients.
|
| 97 |
|
| 98 |
## Correctness
|
| 99 |
|
|
|
|
| 101 |
variants additionally pass the full suite through `get_kernel` on a GeForce
|
| 102 |
RTX 3070 Ti (Ampere sm86, Linux, torch 2.13, cu126):
|
| 103 |
|
| 104 |
+
- **Analytic furnace, zero variance.** Closed uniform diffuse box under
|
| 105 |
+
BRDF sampling: every path returns exactly `E * sum a^k`; the analytic
|
| 106 |
+
value 0.498046875 is met with worst pixel deviation 3.4e-3. A diffuse
|
| 107 |
+
plane under a constant environment reproduces `albedo * c` the same way.
|
| 108 |
+
- **Fresnel slab.** A smooth glass slab (ior 1.5) at normal incidence
|
| 109 |
+
transmits the internal-bounce series `(1-R)/(1+R)`; measured 0.9222
|
| 110 |
+
against 0.923077 analytic (0.10%).
|
| 111 |
+
- **Estimator agreement.** MIS, NEE-only, and BRDF-only means agree within
|
| 112 |
+
Monte Carlo tolerance on a Cornell box, and MIS vs BRDF-only agree on a
|
| 113 |
+
GGX conductor under an environment map, which jointly exercises the VNDF
|
| 114 |
+
weight, the GGX pdf, and the environment pdf.
|
| 115 |
+
- **Gradients match finite differences along the same paths.** Constants
|
| 116 |
+
4.6e-4 max relative error over nine entries; wall-texture texels 5.2e-3;
|
| 117 |
+
conductor F0 through GGX sampling and MIS 8.4e-5; environment texels
|
| 118 |
+
2.1e-4 (detached CDF). Texels no path can see get exactly zero from both
|
| 119 |
+
the replay and the differences.
|
| 120 |
+
- **Constants are 1x1 textures.** Both albedo forms render equal images to
|
| 121 |
+
float rounding and receive matching gradients.
|
| 122 |
+
- **Inverse rendering.** A wall color (3 unknowns) recovers to max error
|
| 123 |
+
0.004 in 80 Adam steps; an 8x8 wall texture (192 unknowns) drives the
|
| 124 |
+
image loss from 1.8e-4 to 6.5e-14 with mean texel error below 5e-4 in
|
| 125 |
+
200 steps, both through the kernel alone.
|
| 126 |
+
- **Deterministic.** Repeated forward renders are bitwise identical;
|
| 127 |
+
gradient buffers accumulate through float atomics and are deterministic
|
| 128 |
+
in expectation but not bitwise.
|
| 129 |
|
| 130 |
## Measured
|
| 131 |
|
| 132 |
+
RTX 6000 Ada, 512x512, MIS, 4 bounces. The terrain scenes are displaced
|
| 133 |
+
grids with a 256x256 checker albedo texture, a GGX conductor block, and a
|
| 134 |
+
constant environment map; the SAH build is the vectorized torch builder.
|
| 135 |
|
| 136 |
+
| scene | build | forward | backward (replay) |
|
| 137 |
+
|---|---|---|---|
|
| 138 |
+
| Cornell box, 24 tris, 64 spp | - | 12.8 ms (1.31 Gpaths/s) | 59.0 ms (4.6x) |
|
| 139 |
+
| terrain, 522,254 tris, 16 spp | 0.2 s | 18.2 ms (230 Mpaths/s) | 35.3 ms (1.9x) |
|
| 140 |
+
| terrain, 1,045,470 tris, 16 spp | 0.3 s | 21.6 ms (194 Mpaths/s) | 37.9 ms (1.8x) |
|
| 141 |
|
| 142 |
## Requirements and limits
|
| 143 |
|
| 144 |
- NVIDIA GPU with compute capability 8.0+; float32 throughout.
|
| 145 |
+
- Materials: Lambertian diffuse, GGX conductor (roughness clamped to
|
| 146 |
+
>= 0.01), smooth dielectric; per-material emission; pinhole camera; at
|
| 147 |
+
most 64 materials and 16 bounces; two-sided surfaces; no Russian
|
| 148 |
+
roulette (fixed bounce budget keeps the replay exact).
|
| 149 |
+
- Differentiable parameters: albedo texels/constants (all material types;
|
| 150 |
+
conductor F0 tint), per-material emission, environment texels. Geometry,
|
| 151 |
+
camera, UVs, roughness, and ior are frozen, and there are no
|
| 152 |
+
visibility/silhouette gradients.
|
| 153 |
+
- The environment sampling CDF is detached at Scene construction (a 0.5
|
| 154 |
+
uniform-sphere mixture keeps every direction reachable, so optimizing an
|
| 155 |
+
env map from an arbitrary initialization stays unbiased); rebuild the
|
| 156 |
+
Scene to re-importance-sample a changed map.
|
| 157 |
+
- Texture and env shapes are fixed at Scene construction; UVs wrap.
|
| 158 |
+
- Published variants are Linux x86_64; on Windows `load_local.py`
|
| 159 |
+
JIT-builds the same source and exposes the identical API.
|
| 160 |
|
| 161 |
## References
|
| 162 |
|
| 163 |
Kajiya, "The Rendering Equation" (SIGGRAPH 1986); Vicini, Speierer, Jakob,
|
| 164 |
"Path Replay Backpropagation" (SIGGRAPH 2021); Nimier-David, Speierer,
|
| 165 |
+
Ruiz, Jakob, "Radiative Backpropagation" (SIGGRAPH 2020); Heitz, "Sampling
|
| 166 |
+
the GGX Distribution of Visible Normals" (JCGT 2018); Heitz,
|
| 167 |
+
"Understanding the Masking-Shadowing Function" (JCGT 2014); Veach and
|
| 168 |
+
Guibas, "Optimally Combining Sampling Techniques" (SIGGRAPH 1995); Möller
|
| 169 |
+
and Trumbore, "Fast, Minimum Storage Ray-Triangle Intersection" (1997);
|
| 170 |
+
Duff et al., "Building an Orthonormal Basis, Revisited" (JCGT 2017); Wald,
|
| 171 |
+
"On fast Construction of SAH-based Bounding Volume Hierarchies" (2007).
|
| 172 |
|
| 173 |
## License
|
| 174 |
|
| 175 |
+
Apache-2.0.
|
dev/report_local.py
CHANGED
|
@@ -222,36 +222,206 @@ print(f"texture inverse: 8x8x3 (192 unknowns) recovered to mean abs err "
|
|
| 222 |
f"{err.mean().item():.3f} (max {err.max().item():.3f}), "
|
| 223 |
f"loss {first:.2e} -> {loss.item():.2e} in 200 steps")
|
| 224 |
|
| 225 |
-
# ----
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
alb = scene.albedo.clone().requires_grad_(True)
|
| 242 |
scene.albedo = alb
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
t0 = time.perf_counter()
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
f"{err.mean().item():.3f} (max {err.max().item():.3f}), "
|
| 223 |
f"loss {first:.2e} -> {loss.item():.2e} in 200 steps")
|
| 224 |
|
| 225 |
+
# ---- Fresnel slab analytic
|
| 226 |
+
verts, faces, mat = [], [], []
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def quad_w(a, b, c, d, m):
|
| 230 |
+
base = len(verts)
|
| 231 |
+
verts.extend([a, b, c, d])
|
| 232 |
+
faces.extend(quad(base, base + 1, base + 2, base + 3))
|
| 233 |
+
mat.extend([m, m])
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
quad_w((-10, -10, 5), (10, -10, 5), (10, 10, 5), (-10, 10, 5), 0)
|
| 237 |
+
add_box(verts, faces, mat, (-10, -10, 2.0), (10, 10, 2.2), 1)
|
| 238 |
+
slab = ptd.Scene(verts, faces, mat, albedo=[[0, 0, 0], [0, 0, 0]],
|
| 239 |
+
emission=[[1.0] * 3, [0, 0, 0]],
|
| 240 |
+
material_types=[ptd.DIFFUSE, ptd.DIELECTRIC], ior=[1.5, 1.5])
|
| 241 |
+
scam = ptd.Camera(position=(0, 0, 0), look_at=(0, 0, 1), vfov_deg=10.0)
|
| 242 |
+
img = ptd.render(slab, scam, 32, 32, spp=1024, max_bounces=10,
|
| 243 |
+
estimator="brdf", seed=5)
|
| 244 |
+
R = (0.5 / 2.5) ** 2
|
| 245 |
+
exp_T = (1 - R) / (1 + R)
|
| 246 |
+
got = img[12:20, 12:20].mean().item()
|
| 247 |
+
print(f"fresnel slab (ior 1.5): analytic T {exp_T:.6f}, measured {got:.6f}, "
|
| 248 |
+
f"rel err {abs(got-exp_T)/exp_T:.4%}")
|
| 249 |
+
|
| 250 |
+
# ---- conductor F0 FD (through GGX + MIS)
|
| 251 |
+
H = W = 48
|
| 252 |
+
torch.manual_seed(0)
|
| 253 |
+
Wr = torch.rand(H, W, 3, device="cuda")
|
| 254 |
+
base_alb = torch.tensor([[0.73] * 3, [0.9, 0.5, 0.2], [0.12, 0.45, 0.15],
|
| 255 |
+
[0.78] * 3], device="cuda")
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def cond_loss(alb):
|
| 259 |
+
verts, faces, mat = [], [], []
|
| 260 |
+
|
| 261 |
+
def wall(a, b, c, d, m):
|
| 262 |
+
b0 = len(verts)
|
| 263 |
+
verts.extend([a, b, c, d])
|
| 264 |
+
faces.extend(quad(b0, b0 + 1, b0 + 2, b0 + 3))
|
| 265 |
+
mat.extend([m, m])
|
| 266 |
+
|
| 267 |
+
X, Y, Z = 5.56, 5.488, 5.592
|
| 268 |
+
wall((0, 0, 0), (X, 0, 0), (X, 0, Z), (0, 0, Z), 0)
|
| 269 |
+
wall((0, Y, 0), (0, Y, Z), (X, Y, Z), (X, Y, 0), 0)
|
| 270 |
+
wall((0, 0, Z), (X, 0, Z), (X, Y, Z), (0, Y, Z), 0)
|
| 271 |
+
wall((0, 0, 0), (0, 0, Z), (0, Y, Z), (0, Y, 0), 1)
|
| 272 |
+
wall((X, 0, 0), (X, Y, 0), (X, Y, Z), (X, 0, Z), 2)
|
| 273 |
+
wall((2.13, Y - 0.001, 2.27), (3.43, Y - 0.001, 2.27),
|
| 274 |
+
(3.43, Y - 0.001, 3.32), (2.13, Y - 0.001, 3.32), 3)
|
| 275 |
+
sc = ptd.Scene(verts, faces, mat, albedo=alb,
|
| 276 |
+
emission=[[0, 0, 0], [0, 0, 0], [0, 0, 0],
|
| 277 |
+
[18.4, 15.6, 8.0]],
|
| 278 |
+
material_types=[0, 1, 0, 0], roughness=[0.3, 0.2, 0.3, 0.3])
|
| 279 |
+
return (ptd.render(sc, cam, H, W, spp=8, max_bounces=3, seed=13) * Wr).sum()
|
| 280 |
+
|
| 281 |
|
| 282 |
+
alb = base_alb.clone().requires_grad_(True)
|
| 283 |
+
cond_loss(alb).backward()
|
| 284 |
+
worst = 0.0
|
| 285 |
+
for (i, c) in [(1, 0), (1, 2), (0, 1)]:
|
| 286 |
+
h = 2e-3
|
| 287 |
+
ap = base_alb.clone(); ap[i, c] += h
|
| 288 |
+
am = base_alb.clone(); am[i, c] -= h
|
| 289 |
+
fd = (cond_loss(ap) - cond_loss(am)).item() / (2 * h)
|
| 290 |
+
worst = max(worst, abs(alb.grad[i, c].item() - fd) / abs(fd))
|
| 291 |
+
print(f"conductor F0 gradients vs central differences (GGX+MIS): "
|
| 292 |
+
f"max rel err {worst:.2e} over 3 entries")
|
| 293 |
+
|
| 294 |
+
# ---- env texel FD
|
| 295 |
+
torch.manual_seed(3)
|
| 296 |
+
env0 = torch.rand(4, 8, 3, device="cuda") * 1.5 + 0.2
|
| 297 |
+
verts, faces, mat = [], [], []
|
| 298 |
+
b0 = len(verts)
|
| 299 |
+
verts.extend([(-5, 0, -5), (5, 0, -5), (5, 0, 5), (-5, 0, 5)])
|
| 300 |
+
faces.extend(quad(b0, b0 + 1, b0 + 2, b0 + 3))
|
| 301 |
+
mat.extend([0, 0])
|
| 302 |
+
ecam = ptd.Camera(position=(0, 2.0, 0.01), look_at=(0, 0, 0.5), vfov_deg=40.0)
|
| 303 |
+
torch.manual_seed(0)
|
| 304 |
+
Wre = torch.rand(32, 32, 3, device="cuda")
|
| 305 |
+
esc = ptd.Scene(verts, faces, mat, albedo=[[0.6] * 3], emission=[[0, 0, 0]],
|
| 306 |
+
env=env0)
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
def env_loss(e):
|
| 310 |
+
esc.env = e.to("cuda")
|
| 311 |
+
return (ptd.render(esc, ecam, 32, 32, spp=8, max_bounces=3, seed=11)
|
| 312 |
+
* Wre).sum()
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
e = env0.clone().requires_grad_(True)
|
| 316 |
+
env_loss(e).backward()
|
| 317 |
+
worst = 0.0
|
| 318 |
+
for (y, x, c) in [(0, 0, 0), (1, 4, 1), (1, 7, 2)]:
|
| 319 |
+
h = 5e-3
|
| 320 |
+
ep = env0.clone(); ep[y, x, c] += h
|
| 321 |
+
em2 = env0.clone(); em2[y, x, c] -= h
|
| 322 |
+
fd = (env_loss(ep) - env_loss(em2)).item() / (2 * h)
|
| 323 |
+
worst = max(worst, abs(e.grad[y, x, c].item() - fd) / abs(fd))
|
| 324 |
+
print(f"environment texel gradients vs central differences: "
|
| 325 |
+
f"max rel err {worst:.2e} over 3 texels (detached CDF)")
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
# ---- throughput helper
|
| 329 |
+
def bench(scene, bcam, H, W, spp, B, label, grad_leaf=None):
|
| 330 |
+
img = ptd.render(scene, bcam, H, W, spp=spp, max_bounces=B)
|
| 331 |
+
torch.cuda.synchronize()
|
| 332 |
+
ts = []
|
| 333 |
+
for _ in range(3):
|
| 334 |
+
t0 = time.perf_counter()
|
| 335 |
+
img = ptd.render(scene, bcam, H, W, spp=spp, max_bounces=B)
|
| 336 |
+
torch.cuda.synchronize()
|
| 337 |
+
ts.append(time.perf_counter() - t0)
|
| 338 |
+
fwd = sorted(ts)[1]
|
| 339 |
+
paths = H * W * spp
|
| 340 |
+
line = (f"{label}: forward {fwd*1e3:.1f} ms ({paths/fwd/1e6:.0f} Mpaths/s)")
|
| 341 |
+
if grad_leaf is not None:
|
| 342 |
+
g = torch.ones(H, W, 3, device="cuda")
|
| 343 |
+
img = ptd.render(scene, bcam, H, W, spp=spp, max_bounces=B)
|
| 344 |
+
img.backward(g)
|
| 345 |
+
torch.cuda.synchronize()
|
| 346 |
+
ts = []
|
| 347 |
+
for _ in range(3):
|
| 348 |
+
grad_leaf.grad = None
|
| 349 |
+
img = ptd.render(scene, bcam, H, W, spp=spp, max_bounces=B)
|
| 350 |
+
torch.cuda.synchronize()
|
| 351 |
+
t0 = time.perf_counter()
|
| 352 |
+
img.backward(g)
|
| 353 |
+
torch.cuda.synchronize()
|
| 354 |
+
ts.append(time.perf_counter() - t0)
|
| 355 |
+
bwd = sorted(ts)[1]
|
| 356 |
+
line += f"; backward {bwd*1e3:.1f} ms ({bwd/fwd:.2f}x)"
|
| 357 |
+
print(line)
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
# cornell (continuity with v1 numbers)
|
| 361 |
+
scene = cornell(with_box=True)
|
| 362 |
alb = scene.albedo.clone().requires_grad_(True)
|
| 363 |
scene.albedo = alb
|
| 364 |
+
bench(scene, cam, 512, 512, 64, 4, "cornell 24 tris, 512x512 spp 64 b4", alb)
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
# ---- large scenes: displaced terrain + conductor box + checker + env
|
| 368 |
+
def terrain_scene(n):
|
| 369 |
+
ax = torch.linspace(0, 10, n + 1)
|
| 370 |
+
X, Z = torch.meshgrid(ax, ax, indexing="ij")
|
| 371 |
+
Y = (0.35 * torch.sin(X * 1.7) * torch.cos(Z * 1.3) +
|
| 372 |
+
0.2 * torch.sin(X * 4.1 + 1.0) * torch.sin(Z * 3.7) +
|
| 373 |
+
0.08 * torch.sin(X * 9.3) * torch.cos(Z * 8.1))
|
| 374 |
+
verts = torch.stack([X, Y, Z], dim=-1).reshape(-1, 3)
|
| 375 |
+
ii = torch.arange(n)
|
| 376 |
+
jj = torch.arange(n)
|
| 377 |
+
I, J = torch.meshgrid(ii, jj, indexing="ij")
|
| 378 |
+
v00 = (I * (n + 1) + J).reshape(-1)
|
| 379 |
+
v10 = ((I + 1) * (n + 1) + J).reshape(-1)
|
| 380 |
+
v01 = (I * (n + 1) + J + 1).reshape(-1)
|
| 381 |
+
v11 = ((I + 1) * (n + 1) + J + 1).reshape(-1)
|
| 382 |
+
f1 = torch.stack([v00, v10, v11], dim=1)
|
| 383 |
+
f2 = torch.stack([v00, v11, v01], dim=1)
|
| 384 |
+
faces = torch.cat([f1, f2], dim=0)
|
| 385 |
+
mat = torch.zeros(faces.shape[0], dtype=torch.int64)
|
| 386 |
+
uv = torch.stack([X / 10.0, Z / 10.0], dim=-1).reshape(-1, 2)
|
| 387 |
+
|
| 388 |
+
verts_l, faces_l, mat_l = [verts], [faces], [mat]
|
| 389 |
+
base = verts.shape[0]
|
| 390 |
+
bx = torch.tensor([(4.2, 0.4, 4.2), (5.8, 0.4, 4.2), (5.8, 0.4, 5.8),
|
| 391 |
+
(4.2, 0.4, 5.8), (4.2, 2.2, 4.2), (5.8, 2.2, 4.2),
|
| 392 |
+
(5.8, 2.2, 5.8), (4.2, 2.2, 5.8)])
|
| 393 |
+
verts_l.append(bx)
|
| 394 |
+
bq = [[0, 1, 2], [0, 2, 3], [4, 7, 6], [4, 6, 5], [0, 4, 5], [0, 5, 1],
|
| 395 |
+
[3, 2, 6], [3, 6, 7], [0, 3, 7], [0, 7, 4], [1, 5, 6], [1, 6, 2]]
|
| 396 |
+
faces_l.append(torch.tensor(bq, dtype=torch.int64) + base)
|
| 397 |
+
mat_l.append(torch.ones(12, dtype=torch.int64))
|
| 398 |
+
verts = torch.cat(verts_l)
|
| 399 |
+
faces = torch.cat(faces_l)
|
| 400 |
+
mat = torch.cat(mat_l)
|
| 401 |
+
uv = torch.cat([uv, torch.zeros(8, 2)])
|
| 402 |
+
|
| 403 |
+
yy, xx = torch.meshgrid(torch.arange(256), torch.arange(256),
|
| 404 |
+
indexing="ij")
|
| 405 |
+
checker = (((yy // 16 + xx // 16) % 2).float() * 0.45 + 0.3)
|
| 406 |
+
tex = torch.stack([checker, checker * 0.9, checker * 0.7], dim=-1)
|
| 407 |
+
env = torch.full((16, 32, 3), 0.9)
|
| 408 |
t0 = time.perf_counter()
|
| 409 |
+
sc = ptd.Scene(verts, faces, mat,
|
| 410 |
+
albedo=[tex.cuda(), torch.tensor([0.9, 0.65, 0.35])],
|
| 411 |
+
emission=[[0, 0, 0], [0, 0, 0]],
|
| 412 |
+
material_types=[ptd.DIFFUSE, ptd.CONDUCTOR],
|
| 413 |
+
roughness=[0.3, 0.15], uvs=uv, env=env)
|
| 414 |
+
build = time.perf_counter() - t0
|
| 415 |
+
return sc, build
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
tcam = ptd.Camera(position=(11.0, 4.5, 11.0), look_at=(5.0, 0.5, 5.0),
|
| 419 |
+
vfov_deg=45.0)
|
| 420 |
+
for n in (511, 723):
|
| 421 |
+
sc, build = terrain_scene(n)
|
| 422 |
+
ntri = sc.n_faces
|
| 423 |
+
tex_leaf = sc.albedo_textures[0].requires_grad_(True)
|
| 424 |
+
sc.albedo_textures[0] = tex_leaf
|
| 425 |
+
bench(sc, tcam, 512, 512, 16, 4,
|
| 426 |
+
f"terrain {ntri} tris (SAH build {build:.1f} s), 512x512 spp 16 b4",
|
| 427 |
+
tex_leaf)
|
local_bind.cpp
CHANGED
|
@@ -9,35 +9,83 @@
|
|
| 9 |
|
| 10 |
namespace {
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
void pt_forward(torch::Tensor tris, torch::Tensor mat_ids, torch::Tensor uvs,
|
| 13 |
torch::Tensor nodes_f, torch::Tensor nodes_i,
|
| 14 |
torch::Tensor light_faces, torch::Tensor light_cdf,
|
| 15 |
double total_light_area, torch::Tensor tex,
|
| 16 |
torch::Tensor tex_hdr, torch::Tensor emission,
|
| 17 |
-
torch::Tensor
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
| 25 |
const at::cuda::CUDAGuard guard(tris.device());
|
| 26 |
cudaStream_t stream = at::cuda::getCurrentCUDAStream();
|
| 27 |
-
int L = (int)light_faces.numel();
|
| 28 |
torch::Tensor cam_h = cam.to(torch::kFloat32).to(torch::kCPU).contiguous();
|
| 29 |
-
ptd_forward_launch(
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
(int)nodes_f.size(0), L ? light_faces.const_data_ptr<int>() : nullptr,
|
| 34 |
-
L ? light_cdf.const_data_ptr<float>() : nullptr, L,
|
| 35 |
-
(float)total_light_area, tex.const_data_ptr<float>(),
|
| 36 |
-
tex_hdr.const_data_ptr<int>(), (int)tex.size(0),
|
| 37 |
-
emission.const_data_ptr<float>(),
|
| 38 |
-
(int)tex_hdr.size(0), cam_h.const_data_ptr<float>(),
|
| 39 |
-
(int)image.size(0), (int)image.size(1), (int)spp, (int)max_bounces,
|
| 40 |
-
nee ? 1 : 0, (long long)seed, image.data_ptr<float>(), stream);
|
| 41 |
C10_CUDA_KERNEL_LAUNCH_CHECK();
|
| 42 |
}
|
| 43 |
|
|
@@ -46,30 +94,30 @@ void pt_backward(torch::Tensor tris, torch::Tensor mat_ids, torch::Tensor uvs,
|
|
| 46 |
torch::Tensor light_faces, torch::Tensor light_cdf,
|
| 47 |
double total_light_area, torch::Tensor tex,
|
| 48 |
torch::Tensor tex_hdr, torch::Tensor emission,
|
| 49 |
-
torch::Tensor
|
| 50 |
-
|
| 51 |
-
torch::Tensor
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
const at::cuda::CUDAGuard guard(tris.device());
|
| 56 |
cudaStream_t stream = at::cuda::getCurrentCUDAStream();
|
| 57 |
-
int L = (int)light_faces.numel();
|
| 58 |
torch::Tensor cam_h = cam.to(torch::kFloat32).to(torch::kCPU).contiguous();
|
| 59 |
-
ptd_backward_launch(
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
emission.const_data_ptr<float>(),
|
| 68 |
-
(int)tex_hdr.size(0), cam_h.const_data_ptr<float>(),
|
| 69 |
-
(int)grad_image.size(0), (int)grad_image.size(1), (int)spp,
|
| 70 |
-
(int)max_bounces, nee ? 1 : 0, (long long)seed,
|
| 71 |
-
grad_image.const_data_ptr<float>(), grad_tex.data_ptr<float>(),
|
| 72 |
-
grad_emission.data_ptr<float>(), stream);
|
| 73 |
C10_CUDA_KERNEL_LAUNCH_CHECK();
|
| 74 |
}
|
| 75 |
|
|
|
|
| 9 |
|
| 10 |
namespace {
|
| 11 |
|
| 12 |
+
const float* fptr(const torch::Tensor& t) {
|
| 13 |
+
return t.numel() ? t.const_data_ptr<float>() : nullptr;
|
| 14 |
+
}
|
| 15 |
+
const int* iptr(const torch::Tensor& t) {
|
| 16 |
+
return t.numel() ? t.const_data_ptr<int>() : nullptr;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
PtdSceneArgs pack_args(const torch::Tensor& tris, const torch::Tensor& mat_ids,
|
| 20 |
+
const torch::Tensor& uvs, const torch::Tensor& nodes_f,
|
| 21 |
+
const torch::Tensor& nodes_i,
|
| 22 |
+
const torch::Tensor& light_faces,
|
| 23 |
+
const torch::Tensor& light_cdf, double total_light_area,
|
| 24 |
+
const torch::Tensor& tex, const torch::Tensor& tex_hdr,
|
| 25 |
+
const torch::Tensor& emission,
|
| 26 |
+
const torch::Tensor& mat_type,
|
| 27 |
+
const torch::Tensor& mat_rough,
|
| 28 |
+
const torch::Tensor& mat_ior, const torch::Tensor& env,
|
| 29 |
+
const torch::Tensor& env_cdf_m,
|
| 30 |
+
const torch::Tensor& env_cdf_c,
|
| 31 |
+
const torch::Tensor& env_pdf, int64_t env_w,
|
| 32 |
+
int64_t env_h, int64_t max_bounces) {
|
| 33 |
+
TORCH_CHECK(tris.is_cuda() && tris.is_contiguous(), "tris");
|
| 34 |
+
TORCH_CHECK(uvs.numel() == tris.size(0) * 6, "uvs [F, 3, 2]");
|
| 35 |
+
TORCH_CHECK(tex_hdr.size(0) <= 64, "at most 64 materials");
|
| 36 |
+
TORCH_CHECK(max_bounces >= 1 && max_bounces <= 16, "bounces in [1,16]");
|
| 37 |
+
PtdSceneArgs a;
|
| 38 |
+
a.tris = tris.const_data_ptr<float>();
|
| 39 |
+
a.mat_ids = mat_ids.const_data_ptr<int>();
|
| 40 |
+
a.uvs = uvs.const_data_ptr<float>();
|
| 41 |
+
a.n_faces = (int)tris.size(0);
|
| 42 |
+
a.nodes_f = nodes_f.const_data_ptr<float>();
|
| 43 |
+
a.nodes_i = nodes_i.const_data_ptr<int>();
|
| 44 |
+
a.n_nodes = (int)nodes_f.size(0);
|
| 45 |
+
a.light_faces = iptr(light_faces);
|
| 46 |
+
a.light_cdf = fptr(light_cdf);
|
| 47 |
+
a.n_lights = (int)light_faces.numel();
|
| 48 |
+
a.total_light_area = (float)total_light_area;
|
| 49 |
+
a.tex = tex.const_data_ptr<float>();
|
| 50 |
+
a.tex_hdr = tex_hdr.const_data_ptr<int>();
|
| 51 |
+
a.n_texels = (int)tex.size(0);
|
| 52 |
+
a.emission = emission.const_data_ptr<float>();
|
| 53 |
+
a.mat_type = mat_type.const_data_ptr<int>();
|
| 54 |
+
a.mat_rough = mat_rough.const_data_ptr<float>();
|
| 55 |
+
a.mat_ior = mat_ior.const_data_ptr<float>();
|
| 56 |
+
a.n_mats = (int)tex_hdr.size(0);
|
| 57 |
+
a.env = fptr(env);
|
| 58 |
+
a.env_w = (int)env_w;
|
| 59 |
+
a.env_h = (int)env_h;
|
| 60 |
+
a.env_cdf_m = fptr(env_cdf_m);
|
| 61 |
+
a.env_cdf_c = fptr(env_cdf_c);
|
| 62 |
+
a.env_pdf = fptr(env_pdf);
|
| 63 |
+
return a;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
void pt_forward(torch::Tensor tris, torch::Tensor mat_ids, torch::Tensor uvs,
|
| 67 |
torch::Tensor nodes_f, torch::Tensor nodes_i,
|
| 68 |
torch::Tensor light_faces, torch::Tensor light_cdf,
|
| 69 |
double total_light_area, torch::Tensor tex,
|
| 70 |
torch::Tensor tex_hdr, torch::Tensor emission,
|
| 71 |
+
torch::Tensor mat_type, torch::Tensor mat_rough,
|
| 72 |
+
torch::Tensor mat_ior, torch::Tensor env,
|
| 73 |
+
torch::Tensor env_cdf_m, torch::Tensor env_cdf_c,
|
| 74 |
+
torch::Tensor env_pdf, int64_t env_w, int64_t env_h,
|
| 75 |
+
torch::Tensor cam, int64_t spp, int64_t max_bounces,
|
| 76 |
+
int64_t mode, int64_t seed, torch::Tensor image) {
|
| 77 |
+
PtdSceneArgs a = pack_args(tris, mat_ids, uvs, nodes_f, nodes_i, light_faces,
|
| 78 |
+
light_cdf, total_light_area, tex, tex_hdr,
|
| 79 |
+
emission, mat_type, mat_rough, mat_ior, env,
|
| 80 |
+
env_cdf_m, env_cdf_c, env_pdf, env_w, env_h,
|
| 81 |
+
max_bounces);
|
| 82 |
const at::cuda::CUDAGuard guard(tris.device());
|
| 83 |
cudaStream_t stream = at::cuda::getCurrentCUDAStream();
|
|
|
|
| 84 |
torch::Tensor cam_h = cam.to(torch::kFloat32).to(torch::kCPU).contiguous();
|
| 85 |
+
ptd_forward_launch(&a, cam_h.const_data_ptr<float>(), (int)image.size(0),
|
| 86 |
+
(int)image.size(1), (int)spp, (int)max_bounces,
|
| 87 |
+
(int)mode, (long long)seed, image.data_ptr<float>(),
|
| 88 |
+
stream);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
C10_CUDA_KERNEL_LAUNCH_CHECK();
|
| 90 |
}
|
| 91 |
|
|
|
|
| 94 |
torch::Tensor light_faces, torch::Tensor light_cdf,
|
| 95 |
double total_light_area, torch::Tensor tex,
|
| 96 |
torch::Tensor tex_hdr, torch::Tensor emission,
|
| 97 |
+
torch::Tensor mat_type, torch::Tensor mat_rough,
|
| 98 |
+
torch::Tensor mat_ior, torch::Tensor env,
|
| 99 |
+
torch::Tensor env_cdf_m, torch::Tensor env_cdf_c,
|
| 100 |
+
torch::Tensor env_pdf, int64_t env_w, int64_t env_h,
|
| 101 |
+
torch::Tensor cam, int64_t spp, int64_t max_bounces,
|
| 102 |
+
int64_t mode, int64_t seed, torch::Tensor grad_image,
|
| 103 |
+
torch::Tensor grad_tex, torch::Tensor grad_emission,
|
| 104 |
+
torch::Tensor grad_env) {
|
| 105 |
+
PtdSceneArgs a = pack_args(tris, mat_ids, uvs, nodes_f, nodes_i, light_faces,
|
| 106 |
+
light_cdf, total_light_area, tex, tex_hdr,
|
| 107 |
+
emission, mat_type, mat_rough, mat_ior, env,
|
| 108 |
+
env_cdf_m, env_cdf_c, env_pdf, env_w, env_h,
|
| 109 |
+
max_bounces);
|
| 110 |
const at::cuda::CUDAGuard guard(tris.device());
|
| 111 |
cudaStream_t stream = at::cuda::getCurrentCUDAStream();
|
|
|
|
| 112 |
torch::Tensor cam_h = cam.to(torch::kFloat32).to(torch::kCPU).contiguous();
|
| 113 |
+
ptd_backward_launch(&a, cam_h.const_data_ptr<float>(),
|
| 114 |
+
(int)grad_image.size(0), (int)grad_image.size(1),
|
| 115 |
+
(int)spp, (int)max_bounces, (int)mode, (long long)seed,
|
| 116 |
+
grad_image.const_data_ptr<float>(),
|
| 117 |
+
grad_tex.data_ptr<float>(),
|
| 118 |
+
grad_emission.data_ptr<float>(),
|
| 119 |
+
grad_env.numel() ? grad_env.data_ptr<float>() : nullptr,
|
| 120 |
+
stream);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
C10_CUDA_KERNEL_LAUNCH_CHECK();
|
| 122 |
}
|
| 123 |
|
pathtracer_diff_cuda/pathtracer.cu
CHANGED
|
@@ -1,28 +1,29 @@
|
|
| 1 |
// pathtracer-diff: a differentiable Monte Carlo path tracer as one kernel.
|
| 2 |
//
|
| 3 |
-
// Forward: a megakernel unidirectional path tracer
|
| 4 |
-
//
|
| 5 |
-
//
|
| 6 |
-
//
|
| 7 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
//
|
| 9 |
-
// Backward: exact path replay. Sampling
|
| 10 |
-
//
|
| 11 |
-
//
|
| 12 |
-
//
|
| 13 |
-
//
|
| 14 |
-
//
|
| 15 |
-
//
|
| 16 |
-
// texels
|
| 17 |
-
//
|
| 18 |
-
//
|
| 19 |
-
//
|
| 20 |
-
//
|
| 21 |
-
//
|
| 22 |
-
// camera-to-light paths of the same maximum length. With NEE off: emission
|
| 23 |
-
// is gathered at every hit (with cosine sampling the diffuse throughput
|
| 24 |
-
// factor is exactly the albedo, which makes a closed uniform furnace a
|
| 25 |
-
// zero-variance analytic test case).
|
| 26 |
|
| 27 |
#include <cuda_runtime.h>
|
| 28 |
#include <curand_kernel.h>
|
|
@@ -36,14 +37,16 @@ namespace {
|
|
| 36 |
constexpr int kThreads = 128;
|
| 37 |
constexpr int kMaxBounces = 16;
|
| 38 |
constexpr int kMaxMats = 64;
|
| 39 |
-
|
| 40 |
-
// dynamic shared memory per block and flushed once; larger buffers take
|
| 41 |
-
// direct global atomics (contention is then spread across many texels).
|
| 42 |
-
constexpr int kSharedTexels = 2048;
|
| 43 |
constexpr int kStack = 64;
|
|
|
|
| 44 |
constexpr float kInvPi = 0.31830988618379067154f;
|
| 45 |
constexpr float kRayEps = 1e-4f;
|
| 46 |
constexpr float kShadowEps = 1e-3f;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
// ---------------------------------------------------------------- float3 ops
|
| 49 |
__device__ __forceinline__ float3 f3(float x, float y, float z) {
|
|
@@ -67,6 +70,7 @@ __device__ __forceinline__ float3 cross(float3 a, float3 b) {
|
|
| 67 |
__device__ __forceinline__ float3 normalize(float3 a) {
|
| 68 |
return a * rsqrtf(fmaxf(dot(a, a), 1e-30f));
|
| 69 |
}
|
|
|
|
| 70 |
|
| 71 |
// Duff et al. 2017, "Building an Orthonormal Basis, Revisited".
|
| 72 |
__device__ __forceinline__ void onb(float3 n, float3& t, float3& b) {
|
|
@@ -79,24 +83,33 @@ __device__ __forceinline__ void onb(float3 n, float3& t, float3& b) {
|
|
| 79 |
|
| 80 |
// ---------------------------------------------------------------- scene view
|
| 81 |
struct DevScene {
|
| 82 |
-
const float* tris; // [F, 9]
|
| 83 |
const int* mats; // [F]
|
| 84 |
-
const float* uvs; // [F, 3, 2]
|
| 85 |
-
const float* nf; // [N, 6]
|
| 86 |
-
const int* ni; // [N, 3] (
|
| 87 |
int n_nodes;
|
| 88 |
-
const int* lf; // [L] emissive
|
| 89 |
-
const float* lcdf; // [L]
|
| 90 |
int nl;
|
| 91 |
-
float larea;
|
| 92 |
-
const float* tex; // [T, 3]
|
| 93 |
-
const int* thdr; // [M, 3] (
|
| 94 |
-
int nt;
|
| 95 |
const float* emi; // [M, 3]
|
|
|
|
|
|
|
|
|
|
| 96 |
int nm;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
};
|
| 98 |
|
| 99 |
-
struct DevCam {
|
| 100 |
float p[3], f[3], r[3], u[3];
|
| 101 |
};
|
| 102 |
|
|
@@ -155,6 +168,7 @@ __device__ int bvh_closest(const DevScene& sc, float3 ro, float3 rd,
|
|
| 155 |
float tmin, float& tbest, float& bu, float& bv,
|
| 156 |
float3& ngbest) {
|
| 157 |
float roa[3] = {ro.x, ro.y, ro.z};
|
|
|
|
| 158 |
float inv[3];
|
| 159 |
inv_dir(rd, inv);
|
| 160 |
int stack[kStack];
|
|
@@ -165,7 +179,7 @@ __device__ int bvh_closest(const DevScene& sc, float3 ro, float3 rd,
|
|
| 165 |
int nid = stack[--sp];
|
| 166 |
if (!slab(&sc.nf[nid * 6], roa, inv, tbest)) continue;
|
| 167 |
const int* n = &sc.ni[nid * 3];
|
| 168 |
-
if (n[2]) {
|
| 169 |
for (int f = n[0]; f < n[0] + n[1]; ++f) {
|
| 170 |
float t, u, v;
|
| 171 |
float3 ng;
|
|
@@ -178,8 +192,11 @@ __device__ int bvh_closest(const DevScene& sc, float3 ro, float3 rd,
|
|
| 178 |
}
|
| 179 |
}
|
| 180 |
} else if (sp + 2 <= kStack) {
|
| 181 |
-
|
| 182 |
-
|
|
|
|
|
|
|
|
|
|
| 183 |
}
|
| 184 |
}
|
| 185 |
return best;
|
|
@@ -197,7 +214,7 @@ __device__ bool bvh_occluded(const DevScene& sc, float3 ro, float3 rd,
|
|
| 197 |
int nid = stack[--sp];
|
| 198 |
if (!slab(&sc.nf[nid * 6], roa, inv, tmax)) continue;
|
| 199 |
const int* n = &sc.ni[nid * 3];
|
| 200 |
-
if (n[2]) {
|
| 201 |
for (int f = n[0]; f < n[0] + n[1]; ++f) {
|
| 202 |
float t, u, v;
|
| 203 |
float3 ng;
|
|
@@ -212,8 +229,8 @@ __device__ bool bvh_occluded(const DevScene& sc, float3 ro, float3 rd,
|
|
| 212 |
return false;
|
| 213 |
}
|
| 214 |
|
| 215 |
-
__device__ __forceinline__ int
|
| 216 |
-
int lo = 0, hi =
|
| 217 |
while (lo < hi) {
|
| 218 |
int mid = (lo + hi) >> 1;
|
| 219 |
if (cdf[mid] < r) lo = mid + 1; else hi = mid;
|
|
@@ -221,18 +238,10 @@ __device__ __forceinline__ int pick_light(const float* cdf, int nl, float r) {
|
|
| 221 |
return lo;
|
| 222 |
}
|
| 223 |
|
| 224 |
-
// Bilinear
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
float rgb[3], int idx4[4],
|
| 229 |
-
float w4[4]) {
|
| 230 |
-
const float* U = &sc.uvs[face * 6];
|
| 231 |
-
float w0 = 1.0f - bu - bv;
|
| 232 |
-
float u = w0 * U[0] + bu * U[2] + bv * U[4];
|
| 233 |
-
float v = w0 * U[1] + bu * U[3] + bv * U[5];
|
| 234 |
-
const int* h = &sc.thdr[m * 3];
|
| 235 |
-
int off = h[0], W = h[1], H = h[2];
|
| 236 |
float uu = u * (float)W - 0.5f;
|
| 237 |
float vv = v * (float)H - 0.5f;
|
| 238 |
float fx = uu - floorf(uu);
|
|
@@ -249,52 +258,240 @@ __device__ __forceinline__ void sample_albedo(const DevScene& sc, int m,
|
|
| 249 |
rgb[0] = rgb[1] = rgb[2] = 0.0f;
|
| 250 |
#pragma unroll
|
| 251 |
for (int i = 0; i < 4; ++i) {
|
| 252 |
-
const float* t = &
|
| 253 |
rgb[0] += w4[i] * t[0];
|
| 254 |
rgb[1] += w4[i] * t[1];
|
| 255 |
rgb[2] += w4[i] * t[2];
|
| 256 |
}
|
| 257 |
}
|
| 258 |
|
| 259 |
-
|
| 260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
__device__ __forceinline__ void scatter_tex(float* s_gt, float* g_tex,
|
| 262 |
int idx, int c, float v) {
|
| 263 |
-
if (s_gt)
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
}
|
| 268 |
}
|
| 269 |
|
| 270 |
-
// ------------------------------------------------------------- path tracing
|
| 271 |
-
// One camera path: forward accumulates radiance into acc; backward (GRAD)
|
| 272 |
-
// accumulates parameter gradients (texel scatter for albedo, shared buffer
|
| 273 |
-
// for emission). The RNG draw order is identical in both instantiations by
|
| 274 |
-
// construction.
|
| 275 |
template <bool GRAD>
|
| 276 |
__device__ void trace_path(const DevScene& sc, float3 ro, float3 rd,
|
| 277 |
-
curandStatePhilox4_32_10_t& st, int B, int
|
| 278 |
float3& acc, const float gs[3], float* g_tex,
|
| 279 |
-
float* s_gt, float* s_ge) {
|
| 280 |
-
float T[3] = {1.0f, 1.0f, 1.0f};
|
| 281 |
-
|
| 282 |
-
int
|
| 283 |
-
float
|
| 284 |
-
|
| 285 |
-
|
| 286 |
|
| 287 |
for (int k = 0; k < B; ++k) {
|
| 288 |
-
float tbest =
|
| 289 |
float bu = 0.0f, bv = 0.0f;
|
| 290 |
float3 ng;
|
| 291 |
int face = bvh_closest(sc, ro, rd, kRayEps, tbest, bu, bv, ng);
|
| 292 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
|
| 294 |
int m = sc.mats[face];
|
|
|
|
| 295 |
float3 x = ro + rd * tbest;
|
| 296 |
float3 n = normalize(ng);
|
| 297 |
-
|
|
|
|
| 298 |
|
| 299 |
float am[3];
|
| 300 |
int idx4[4];
|
|
@@ -302,118 +499,231 @@ __device__ void trace_path(const DevScene& sc, float3 ro, float3 rd,
|
|
| 302 |
sample_albedo(sc, m, face, bu, bv, am, idx4, w4);
|
| 303 |
const float* em = &sc.emi[m * 3];
|
| 304 |
|
| 305 |
-
// Emission
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
for (int c = 0; c < 3; ++c) {
|
| 323 |
-
float base = gs[c] * em[c] * pref[c] * suf[j + 1][c];
|
| 324 |
-
#pragma unroll
|
| 325 |
-
for (int i = 0; i < 4; ++i)
|
| 326 |
-
scatter_tex(s_gt, g_tex, fp_i[j][i], c, base * fp_w[j][i]);
|
| 327 |
-
pref[c] *= albs[j][c];
|
| 328 |
-
}
|
| 329 |
-
}
|
| 330 |
-
}
|
| 331 |
}
|
| 332 |
}
|
| 333 |
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
#pragma unroll
|
| 339 |
-
|
| 340 |
-
fp_i[k][i] = idx4[i];
|
| 341 |
-
fp_w[k][i] = w4[i];
|
| 342 |
}
|
| 343 |
-
|
|
|
|
|
|
|
| 344 |
}
|
| 345 |
|
| 346 |
-
//
|
| 347 |
-
|
| 348 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
float r1 = curand_uniform(&st);
|
| 350 |
float r2 = curand_uniform(&st);
|
| 351 |
float r3 = curand_uniform(&st);
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
float
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
} else {
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
float base = gs[c] * el[c] * S * pref[c] * suf[j + 1][c];
|
| 392 |
-
#pragma unroll
|
| 393 |
-
for (int i = 0; i < 4; ++i)
|
| 394 |
-
scatter_tex(s_gt, g_tex, fp_i[j][i], c, base * fp_w[j][i]);
|
| 395 |
-
pref[c] *= albs[j][c];
|
| 396 |
-
}
|
| 397 |
}
|
| 398 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
}
|
| 400 |
}
|
| 401 |
}
|
| 402 |
|
| 403 |
-
//
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
ro = x + n * kRayEps;
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
}
|
| 418 |
}
|
| 419 |
|
|
@@ -428,7 +738,7 @@ __device__ __forceinline__ float3 camera_ray(const DevCam& cam, int px, int py,
|
|
| 428 |
}
|
| 429 |
|
| 430 |
__global__ void k_forward(DevScene sc, DevCam cam, int H, int W, int spp,
|
| 431 |
-
int B, int
|
| 432 |
float* img) {
|
| 433 |
int pid = blockIdx.x * blockDim.x + threadIdx.x;
|
| 434 |
if (pid >= H * W) return;
|
|
@@ -441,8 +751,8 @@ __global__ void k_forward(DevScene sc, DevCam cam, int H, int W, int spp,
|
|
| 441 |
float jx = curand_uniform(&st);
|
| 442 |
float jy = curand_uniform(&st);
|
| 443 |
float3 rd = camera_ray(cam, px, py, W, H, jx, jy);
|
| 444 |
-
trace_path<false>(sc, ro, rd, st, B,
|
| 445 |
-
nullptr);
|
| 446 |
}
|
| 447 |
float inv_spp = 1.0f / (float)spp;
|
| 448 |
img[pid * 3 + 0] = acc.x * inv_spp;
|
|
@@ -451,12 +761,13 @@ __global__ void k_forward(DevScene sc, DevCam cam, int H, int W, int spp,
|
|
| 451 |
}
|
| 452 |
|
| 453 |
__global__ void k_backward(DevScene sc, DevCam cam, int H, int W, int spp,
|
| 454 |
-
int B, int
|
| 455 |
-
const float* gimg, float* g_tex, float* ge
|
|
|
|
| 456 |
extern __shared__ float smem[];
|
| 457 |
-
float* s_ge = smem;
|
| 458 |
bool staged = sc.nt <= kSharedTexels;
|
| 459 |
-
float* s_gt = staged ? smem + sc.nm * 3 : nullptr;
|
| 460 |
int nm3 = sc.nm * 3;
|
| 461 |
int nt3 = staged ? sc.nt * 3 : 0;
|
| 462 |
for (int i = threadIdx.x; i < nm3 + nt3; i += blockDim.x) smem[i] = 0.0f;
|
|
@@ -476,7 +787,8 @@ __global__ void k_backward(DevScene sc, DevCam cam, int H, int W, int spp,
|
|
| 476 |
float jx = curand_uniform(&st);
|
| 477 |
float jy = curand_uniform(&st);
|
| 478 |
float3 rd = camera_ray(cam, px, py, W, H, jx, jy);
|
| 479 |
-
trace_path<true>(sc, ro, rd, st, B,
|
|
|
|
| 480 |
}
|
| 481 |
}
|
| 482 |
|
|
@@ -488,28 +800,32 @@ __global__ void k_backward(DevScene sc, DevCam cam, int H, int W, int spp,
|
|
| 488 |
}
|
| 489 |
}
|
| 490 |
|
| 491 |
-
DevScene make_scene(const
|
| 492 |
-
const float* nodes_f, const int* nodes_i, int n_nodes,
|
| 493 |
-
const int* light_faces, const float* light_cdf,
|
| 494 |
-
int n_lights, float total_light_area, const float* tex,
|
| 495 |
-
const int* tex_hdr, int n_texels, const float* emission,
|
| 496 |
-
int n_mats) {
|
| 497 |
DevScene sc;
|
| 498 |
-
sc.tris = tris;
|
| 499 |
-
sc.mats = mat_ids;
|
| 500 |
-
sc.uvs = uvs;
|
| 501 |
-
sc.nf = nodes_f;
|
| 502 |
-
sc.ni = nodes_i;
|
| 503 |
-
sc.n_nodes = n_nodes;
|
| 504 |
-
sc.lf = light_faces;
|
| 505 |
-
sc.lcdf = light_cdf;
|
| 506 |
-
sc.nl = n_lights;
|
| 507 |
-
sc.larea = total_light_area;
|
| 508 |
-
sc.tex = tex;
|
| 509 |
-
sc.thdr = tex_hdr;
|
| 510 |
-
sc.nt = n_texels;
|
| 511 |
-
sc.emi = emission;
|
| 512 |
-
sc.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 513 |
return sc;
|
| 514 |
}
|
| 515 |
|
|
@@ -526,41 +842,30 @@ DevCam make_cam(const float* cam) {
|
|
| 526 |
|
| 527 |
} // namespace
|
| 528 |
|
| 529 |
-
extern "C" void ptd_forward_launch(
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
int n_texels, const float* emission, int n_mats, const float* cam, int H,
|
| 535 |
-
int W, int spp, int max_bounces, int nee, long long seed, float* image,
|
| 536 |
-
cudaStream_t stream) {
|
| 537 |
-
DevScene sc = make_scene(tris, mat_ids, uvs, nodes_f, nodes_i, n_nodes,
|
| 538 |
-
light_faces, light_cdf, n_lights, total_light_area,
|
| 539 |
-
tex, tex_hdr, n_texels, emission, n_mats);
|
| 540 |
DevCam dc = make_cam(cam);
|
| 541 |
int blocks = (H * W + kThreads - 1) / kThreads;
|
| 542 |
k_forward<<<blocks, kThreads, 0, stream>>>(
|
| 543 |
-
sc, dc, H, W, spp, max_bounces,
|
| 544 |
-
}
|
| 545 |
-
|
| 546 |
-
extern "C" void ptd_backward_launch(
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
const float* grad_image, float* grad_tex, float* grad_emission,
|
| 554 |
-
cudaStream_t stream) {
|
| 555 |
-
DevScene sc = make_scene(tris, mat_ids, uvs, nodes_f, nodes_i, n_nodes,
|
| 556 |
-
light_faces, light_cdf, n_lights, total_light_area,
|
| 557 |
-
tex, tex_hdr, n_texels, emission, n_mats);
|
| 558 |
DevCam dc = make_cam(cam);
|
| 559 |
int blocks = (H * W + kThreads - 1) / kThreads;
|
| 560 |
-
bool staged = n_texels <= kSharedTexels;
|
| 561 |
-
size_t smem = (size_t)(n_mats * 3 +
|
| 562 |
-
|
| 563 |
k_backward<<<blocks, kThreads, smem, stream>>>(
|
| 564 |
-
sc, dc, H, W, spp, max_bounces,
|
| 565 |
-
grad_image, grad_tex, grad_emission);
|
| 566 |
}
|
|
|
|
| 1 |
// pathtracer-diff: a differentiable Monte Carlo path tracer as one kernel.
|
| 2 |
//
|
| 3 |
+
// Forward: a megakernel unidirectional path tracer. Materials: Lambertian
|
| 4 |
+
// diffuse, GGX conductor (height-correlated Smith, VNDF sampling), and
|
| 5 |
+
// smooth dielectric (Fresnel-branched reflection/refraction). Lights: any
|
| 6 |
+
// emissive face (area-sampled) and an optional importance-sampled
|
| 7 |
+
// equirectangular environment map. Estimators: multiple importance
|
| 8 |
+
// sampling (balance heuristic) by default, with legacy NEE-only and
|
| 9 |
+
// BRDF-only modes. Per-texel albedo with bilinear filtering; binned-SAH
|
| 10 |
+
// BVH with near-child-first traversal. One thread owns one pixel and
|
| 11 |
+
// accumulates its spp samples serially, so the image is bitwise
|
| 12 |
+
// deterministic.
|
| 13 |
//
|
| 14 |
+
// Backward: exact path replay. Sampling distributions depend only on
|
| 15 |
+
// geometry, frozen material parameters (roughness, ior), the detached
|
| 16 |
+
// environment CDF, and the counter-based Philox stream -- never on the
|
| 17 |
+
// differentiable parameters (albedo texels, per-material emission,
|
| 18 |
+
// environment texels). The backward kernel therefore re-traces identical
|
| 19 |
+
// paths with identical draws and no stored path state. Every radiance term
|
| 20 |
+
// is a product of per-bounce factors, each AFFINE in its own vertex's
|
| 21 |
+
// albedo texels (diffuse: a; conductor: Schlick F(F0) is affine in F0;
|
| 22 |
+
// dielectric: constant), times an emission or environment texel (linear).
|
| 23 |
+
// The replay differentiates the product in closed form with prefix/suffix
|
| 24 |
+
// exclusion products and scatters each factor's gradient into the four
|
| 25 |
+
// texels of its bilinear footprint -- the exact adjoint of the fetch.
|
| 26 |
+
// Interior terms only: no visibility/silhouette gradients.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
#include <cuda_runtime.h>
|
| 29 |
#include <curand_kernel.h>
|
|
|
|
| 37 |
constexpr int kThreads = 128;
|
| 38 |
constexpr int kMaxBounces = 16;
|
| 39 |
constexpr int kMaxMats = 64;
|
| 40 |
+
constexpr int kSharedTexels = 2048; // shared-staged texture-gradient limit
|
|
|
|
|
|
|
|
|
|
| 41 |
constexpr int kStack = 64;
|
| 42 |
+
constexpr float kPi = 3.14159265358979323846f;
|
| 43 |
constexpr float kInvPi = 0.31830988618379067154f;
|
| 44 |
constexpr float kRayEps = 1e-4f;
|
| 45 |
constexpr float kShadowEps = 1e-3f;
|
| 46 |
+
constexpr float kEnvDist = 1e30f;
|
| 47 |
+
|
| 48 |
+
enum Mode { kModeBrdf = 0, kModeNee = 1, kModeMis = 2 };
|
| 49 |
+
enum MatType { kDiffuse = 0, kConductor = 1, kDielectric = 2 };
|
| 50 |
|
| 51 |
// ---------------------------------------------------------------- float3 ops
|
| 52 |
__device__ __forceinline__ float3 f3(float x, float y, float z) {
|
|
|
|
| 70 |
__device__ __forceinline__ float3 normalize(float3 a) {
|
| 71 |
return a * rsqrtf(fmaxf(dot(a, a), 1e-30f));
|
| 72 |
}
|
| 73 |
+
__device__ __forceinline__ float3 neg(float3 a) { return f3(-a.x, -a.y, -a.z); }
|
| 74 |
|
| 75 |
// Duff et al. 2017, "Building an Orthonormal Basis, Revisited".
|
| 76 |
__device__ __forceinline__ void onb(float3 n, float3& t, float3& b) {
|
|
|
|
| 83 |
|
| 84 |
// ---------------------------------------------------------------- scene view
|
| 85 |
struct DevScene {
|
| 86 |
+
const float* tris; // [F, 9]
|
| 87 |
const int* mats; // [F]
|
| 88 |
+
const float* uvs; // [F, 3, 2]
|
| 89 |
+
const float* nf; // [N, 6]
|
| 90 |
+
const int* ni; // [N, 3] internal (l, r, axis<<1) | leaf (start, count, odd)
|
| 91 |
int n_nodes;
|
| 92 |
+
const int* lf; // [L] emissive faces
|
| 93 |
+
const float* lcdf; // [L]
|
| 94 |
int nl;
|
| 95 |
+
float larea;
|
| 96 |
+
const float* tex; // [T, 3] albedo texels
|
| 97 |
+
const int* thdr; // [M, 3] (offset, W, H)
|
| 98 |
+
int nt;
|
| 99 |
const float* emi; // [M, 3]
|
| 100 |
+
const int* mtype; // [M] material type
|
| 101 |
+
const float* mrough; // [M] GGX alpha (conductor)
|
| 102 |
+
const float* mior; // [M] ior (dielectric)
|
| 103 |
int nm;
|
| 104 |
+
// environment map (equirect) + detached sampling tables
|
| 105 |
+
const float* env; // [Eh*Ew, 3] or null
|
| 106 |
+
int ew, eh;
|
| 107 |
+
const float* ecdf_m; // [Eh] marginal cdf over rows
|
| 108 |
+
const float* ecdf_c; // [Eh*Ew] conditional cdf per row
|
| 109 |
+
const float* epdf; // [Eh*Ew] table pdf over the image (integrates to 1)
|
| 110 |
};
|
| 111 |
|
| 112 |
+
struct DevCam {
|
| 113 |
float p[3], f[3], r[3], u[3];
|
| 114 |
};
|
| 115 |
|
|
|
|
| 168 |
float tmin, float& tbest, float& bu, float& bv,
|
| 169 |
float3& ngbest) {
|
| 170 |
float roa[3] = {ro.x, ro.y, ro.z};
|
| 171 |
+
float dira[3] = {rd.x, rd.y, rd.z};
|
| 172 |
float inv[3];
|
| 173 |
inv_dir(rd, inv);
|
| 174 |
int stack[kStack];
|
|
|
|
| 179 |
int nid = stack[--sp];
|
| 180 |
if (!slab(&sc.nf[nid * 6], roa, inv, tbest)) continue;
|
| 181 |
const int* n = &sc.ni[nid * 3];
|
| 182 |
+
if (n[2] & 1) {
|
| 183 |
for (int f = n[0]; f < n[0] + n[1]; ++f) {
|
| 184 |
float t, u, v;
|
| 185 |
float3 ng;
|
|
|
|
| 192 |
}
|
| 193 |
}
|
| 194 |
} else if (sp + 2 <= kStack) {
|
| 195 |
+
int axis = n[2] >> 1;
|
| 196 |
+
int near = (dira[axis] >= 0.0f) ? n[0] : n[1];
|
| 197 |
+
int far = (dira[axis] >= 0.0f) ? n[1] : n[0];
|
| 198 |
+
stack[sp++] = far;
|
| 199 |
+
stack[sp++] = near;
|
| 200 |
}
|
| 201 |
}
|
| 202 |
return best;
|
|
|
|
| 214 |
int nid = stack[--sp];
|
| 215 |
if (!slab(&sc.nf[nid * 6], roa, inv, tmax)) continue;
|
| 216 |
const int* n = &sc.ni[nid * 3];
|
| 217 |
+
if (n[2] & 1) {
|
| 218 |
for (int f = n[0]; f < n[0] + n[1]; ++f) {
|
| 219 |
float t, u, v;
|
| 220 |
float3 ng;
|
|
|
|
| 229 |
return false;
|
| 230 |
}
|
| 231 |
|
| 232 |
+
__device__ __forceinline__ int cdf_pick(const float* cdf, int n, float r) {
|
| 233 |
+
int lo = 0, hi = n - 1;
|
| 234 |
while (lo < hi) {
|
| 235 |
int mid = (lo + hi) >> 1;
|
| 236 |
if (cdf[mid] < r) lo = mid + 1; else hi = mid;
|
|
|
|
| 238 |
return lo;
|
| 239 |
}
|
| 240 |
|
| 241 |
+
// Bilinear fetch with repeat wrap over a flat texel block.
|
| 242 |
+
__device__ __forceinline__ void bilinear(const float* block, int off, int W,
|
| 243 |
+
int H, float u, float v, float rgb[3],
|
| 244 |
+
int idx4[4], float w4[4]) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
float uu = u * (float)W - 0.5f;
|
| 246 |
float vv = v * (float)H - 0.5f;
|
| 247 |
float fx = uu - floorf(uu);
|
|
|
|
| 258 |
rgb[0] = rgb[1] = rgb[2] = 0.0f;
|
| 259 |
#pragma unroll
|
| 260 |
for (int i = 0; i < 4; ++i) {
|
| 261 |
+
const float* t = &block[idx4[i] * 3];
|
| 262 |
rgb[0] += w4[i] * t[0];
|
| 263 |
rgb[1] += w4[i] * t[1];
|
| 264 |
rgb[2] += w4[i] * t[2];
|
| 265 |
}
|
| 266 |
}
|
| 267 |
|
| 268 |
+
__device__ __forceinline__ void sample_albedo(const DevScene& sc, int m,
|
| 269 |
+
int face, float bu, float bv,
|
| 270 |
+
float rgb[3], int idx4[4],
|
| 271 |
+
float w4[4]) {
|
| 272 |
+
const float* U = &sc.uvs[face * 6];
|
| 273 |
+
float w0 = 1.0f - bu - bv;
|
| 274 |
+
float u = w0 * U[0] + bu * U[2] + bv * U[4];
|
| 275 |
+
float v = w0 * U[1] + bu * U[3] + bv * U[5];
|
| 276 |
+
const int* h = &sc.thdr[m * 3];
|
| 277 |
+
bilinear(sc.tex, h[0], h[1], h[2], u, v, rgb, idx4, w4);
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
// ----------------------------------------------------------- environment map
|
| 281 |
+
__device__ __forceinline__ void dir_to_equirect(float3 d, float& u, float& v) {
|
| 282 |
+
float phi = atan2f(d.z, d.x); // [-pi, pi]
|
| 283 |
+
float theta = acosf(fminf(fmaxf(d.y, -1.0f), 1.0f)); // [0, pi], y-up
|
| 284 |
+
u = (phi + kPi) / (2.0f * kPi);
|
| 285 |
+
v = theta / kPi;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
__device__ __forceinline__ float3 equirect_to_dir(float u, float v) {
|
| 289 |
+
float phi = u * 2.0f * kPi - kPi;
|
| 290 |
+
float theta = v * kPi;
|
| 291 |
+
float st = sinf(theta);
|
| 292 |
+
return f3(st * cosf(phi), cosf(theta), st * sinf(phi));
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
// Solid-angle pdf of the detached env sampling mixture (0.5 table + 0.5
|
| 296 |
+
// uniform sphere).
|
| 297 |
+
__device__ __forceinline__ float env_pdf(const DevScene& sc, float3 d) {
|
| 298 |
+
float u, v;
|
| 299 |
+
dir_to_equirect(d, u, v);
|
| 300 |
+
int x = min(sc.ew - 1, (int)(u * sc.ew));
|
| 301 |
+
int y = min(sc.eh - 1, (int)(v * sc.eh));
|
| 302 |
+
float st = fmaxf(sinf((y + 0.5f) * kPi / sc.eh), 1e-4f);
|
| 303 |
+
float p_img = sc.epdf[y * sc.ew + x]; // per-texel image pdf (sums to 1)
|
| 304 |
+
float p_tab = p_img * (float)(sc.ew * sc.eh) / (2.0f * kPi * kPi * st);
|
| 305 |
+
return 0.5f * p_tab + 0.5f * (1.0f / (4.0f * kPi));
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
__device__ __forceinline__ void env_fetch(const DevScene& sc, float3 d,
|
| 309 |
+
float rgb[3], int idx4[4],
|
| 310 |
+
float w4[4]) {
|
| 311 |
+
float u, v;
|
| 312 |
+
dir_to_equirect(d, u, v);
|
| 313 |
+
// clamp v away from the poles to keep the bilinear rows in range
|
| 314 |
+
v = fminf(fmaxf(v, 0.5f / sc.eh), 1.0f - 0.5f / sc.eh);
|
| 315 |
+
bilinear(sc.env, 0, sc.ew, sc.eh, u, v, rgb, idx4, w4);
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
// ------------------------------------------------------------------- GGX
|
| 319 |
+
__device__ __forceinline__ float ggx_lambda(float a2, float cs) {
|
| 320 |
+
float c2 = cs * cs;
|
| 321 |
+
float t2 = fmaxf(0.0f, 1.0f - c2) / fmaxf(c2, 1e-12f);
|
| 322 |
+
return 0.5f * (-1.0f + sqrtf(1.0f + a2 * t2));
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
__device__ __forceinline__ float ggx_d(float a2, float ch) {
|
| 326 |
+
float d = ch * ch * (a2 - 1.0f) + 1.0f;
|
| 327 |
+
return a2 / fmaxf(kPi * d * d, 1e-20f);
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
// Heitz 2018 VNDF sampling; wi in local frame (z up), wi.z > 0.
|
| 331 |
+
__device__ __forceinline__ float3 ggx_sample_vndf(float3 wi, float alpha,
|
| 332 |
+
float u1, float u2) {
|
| 333 |
+
float3 vh = normalize(f3(alpha * wi.x, alpha * wi.y, wi.z));
|
| 334 |
+
float lensq = vh.x * vh.x + vh.y * vh.y;
|
| 335 |
+
float3 T1 = lensq > 1e-12f ? f3(-vh.y, vh.x, 0.0f) * rsqrtf(lensq)
|
| 336 |
+
: f3(1.0f, 0.0f, 0.0f);
|
| 337 |
+
float3 T2 = cross(vh, T1);
|
| 338 |
+
float r = sqrtf(u1);
|
| 339 |
+
float phi = 2.0f * kPi * u2;
|
| 340 |
+
float t1 = r * cosf(phi);
|
| 341 |
+
float t2 = r * sinf(phi);
|
| 342 |
+
float s = 0.5f * (1.0f + vh.z);
|
| 343 |
+
t2 = (1.0f - s) * sqrtf(fmaxf(0.0f, 1.0f - t1 * t1)) + s * t2;
|
| 344 |
+
float3 nh = T1 * t1 + T2 * t2 +
|
| 345 |
+
vh * sqrtf(fmaxf(0.0f, 1.0f - t1 * t1 - t2 * t2));
|
| 346 |
+
return normalize(f3(alpha * nh.x, alpha * nh.y, fmaxf(1e-6f, nh.z)));
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
// pdf of wo under VNDF sampling (solid angle).
|
| 350 |
+
__device__ __forceinline__ float ggx_pdf(float a2, float3 wi, float3 wo) {
|
| 351 |
+
float3 h = normalize(wi + wo);
|
| 352 |
+
float ch = fmaxf(h.z, 1e-6f);
|
| 353 |
+
float wih = fmaxf(dot(wi, h), 1e-6f);
|
| 354 |
+
float g1 = 1.0f / (1.0f + ggx_lambda(a2, wi.z));
|
| 355 |
+
return g1 * ggx_d(a2, ch) * wih / fmaxf(wi.z, 1e-6f) / (4.0f * wih);
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
// Fresnel (dielectric, unpolarized), cos_i >= 0; n1 -> n2.
|
| 359 |
+
__device__ __forceinline__ float fresnel_dielectric(float cos_i, float eta) {
|
| 360 |
+
float s2 = (1.0f - cos_i * cos_i) / (eta * eta);
|
| 361 |
+
if (s2 >= 1.0f) return 1.0f; // TIR
|
| 362 |
+
float cos_t = sqrtf(1.0f - s2);
|
| 363 |
+
float rs = (cos_i - eta * cos_t) / (cos_i + eta * cos_t);
|
| 364 |
+
float rp = (eta * cos_i - cos_t) / (eta * cos_i + cos_t);
|
| 365 |
+
return 0.5f * (rs * rs + rp * rp);
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
// Schlick term split so F(F0) = F0 * (1 - s) + s: affine in F0.
|
| 369 |
+
__device__ __forceinline__ float schlick_s(float cos_h) {
|
| 370 |
+
float m = fminf(fmaxf(1.0f - cos_h, 0.0f), 1.0f);
|
| 371 |
+
float m2 = m * m;
|
| 372 |
+
return m2 * m2 * m;
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
__device__ __forceinline__ float mis_w(float pa, float pb) {
|
| 376 |
+
return pa / fmaxf(pa + pb, 1e-20f);
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
// ------------------------------------------------------------- path tracing
|
| 380 |
__device__ __forceinline__ void scatter_tex(float* s_gt, float* g_tex,
|
| 381 |
int idx, int c, float v) {
|
| 382 |
+
if (s_gt) atomicAdd(&s_gt[idx * 3 + c], v);
|
| 383 |
+
else atomicAdd(&g_tex[idx * 3 + c], v);
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
// Per-bounce factor record for the replay: value and d(value)/d(albedo
|
| 387 |
+
// texel channel), both per channel, plus the texel footprint.
|
| 388 |
+
struct Factor {
|
| 389 |
+
float val[3];
|
| 390 |
+
float dc[3];
|
| 391 |
+
int fi[4];
|
| 392 |
+
float fw[4];
|
| 393 |
+
};
|
| 394 |
+
|
| 395 |
+
// Accumulate one radiance term and (GRAD) its gradients:
|
| 396 |
+
// term = (prod over the nk factors fs, channelwise) * S * E
|
| 397 |
+
// where fs holds the continuation factors and, for NEE-type terms, the
|
| 398 |
+
// vertex's own BSDF factor as the last entry. T is the product of the
|
| 399 |
+
// continuation factors only; tf is the last factor's value (null for
|
| 400 |
+
// emission/env gathers, whose product is T alone). E is emission (index
|
| 401 |
+
// em_mat >= 0) or environment (footprint env_i/env_w).
|
| 402 |
+
template <bool GRAD>
|
| 403 |
+
__device__ void add_term(const Factor* fs, int nk, const float T[3],
|
| 404 |
+
const float* tf, float S, const float E[3],
|
| 405 |
+
int em_mat, const int env_i[4], const float env_w[4],
|
| 406 |
+
float3& acc, const float gs[3], float* g_tex,
|
| 407 |
+
float* s_gt, float* s_ge, float* g_env) {
|
| 408 |
+
float P[3] = {T[0], T[1], T[2]};
|
| 409 |
+
if (tf) {
|
| 410 |
+
P[0] *= tf[0];
|
| 411 |
+
P[1] *= tf[1];
|
| 412 |
+
P[2] *= tf[2];
|
| 413 |
+
}
|
| 414 |
+
if (!GRAD) {
|
| 415 |
+
acc.x += P[0] * S * E[0];
|
| 416 |
+
acc.y += P[1] * S * E[1];
|
| 417 |
+
acc.z += P[2] * S * E[2];
|
| 418 |
+
return;
|
| 419 |
+
}
|
| 420 |
+
// gradient to the emission / environment texels: g * prodA * S
|
| 421 |
+
if (em_mat >= 0) {
|
| 422 |
+
for (int c = 0; c < 3; ++c)
|
| 423 |
+
atomicAdd(&s_ge[em_mat * 3 + c], gs[c] * P[c] * S);
|
| 424 |
+
} else if (env_i) {
|
| 425 |
+
for (int c = 0; c < 3; ++c) {
|
| 426 |
+
float base = gs[c] * P[c] * S;
|
| 427 |
+
#pragma unroll
|
| 428 |
+
for (int i = 0; i < 4; ++i)
|
| 429 |
+
atomicAdd(&g_env[env_i[i] * 3 + c], base * env_w[i]);
|
| 430 |
+
}
|
| 431 |
+
}
|
| 432 |
+
// gradient to each factor's albedo texels via exclusion products
|
| 433 |
+
if (nk <= 0) return;
|
| 434 |
+
float suf[kMaxBounces + 1][3];
|
| 435 |
+
suf[nk][0] = suf[nk][1] = suf[nk][2] = 1.0f;
|
| 436 |
+
for (int i = nk - 1; i >= 0; --i)
|
| 437 |
+
for (int c = 0; c < 3; ++c) suf[i][c] = suf[i + 1][c] * fs[i].val[c];
|
| 438 |
+
float pref[3] = {1.0f, 1.0f, 1.0f};
|
| 439 |
+
for (int j = 0; j < nk; ++j) {
|
| 440 |
+
for (int c = 0; c < 3; ++c) {
|
| 441 |
+
if (fs[j].dc[c] != 0.0f) {
|
| 442 |
+
float base = gs[c] * E[c] * S * pref[c] * suf[j + 1][c] * fs[j].dc[c];
|
| 443 |
+
#pragma unroll
|
| 444 |
+
for (int i = 0; i < 4; ++i)
|
| 445 |
+
scatter_tex(s_gt, g_tex, fs[j].fi[i], c, base * fs[j].fw[i]);
|
| 446 |
+
}
|
| 447 |
+
pref[c] *= fs[j].val[c];
|
| 448 |
+
}
|
| 449 |
}
|
| 450 |
}
|
| 451 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
template <bool GRAD>
|
| 453 |
__device__ void trace_path(const DevScene& sc, float3 ro, float3 rd,
|
| 454 |
+
curandStatePhilox4_32_10_t& st, int B, int mode,
|
| 455 |
float3& acc, const float gs[3], float* g_tex,
|
| 456 |
+
float* s_gt, float* s_ge, float* g_env) {
|
| 457 |
+
float T[3] = {1.0f, 1.0f, 1.0f};
|
| 458 |
+
Factor fs[kMaxBounces];
|
| 459 |
+
int nk = 0; // factors recorded so far (= bounces done)
|
| 460 |
+
float prev_pdf = 0.0f; // solid-angle pdf of the previous BSDF sample
|
| 461 |
+
bool prev_delta = true; // camera counts as delta (w = 1 on first hit)
|
| 462 |
+
bool has_env = sc.env != nullptr;
|
| 463 |
|
| 464 |
for (int k = 0; k < B; ++k) {
|
| 465 |
+
float tbest = kEnvDist;
|
| 466 |
float bu = 0.0f, bv = 0.0f;
|
| 467 |
float3 ng;
|
| 468 |
int face = bvh_closest(sc, ro, rd, kRayEps, tbest, bu, bv, ng);
|
| 469 |
+
|
| 470 |
+
if (face < 0) {
|
| 471 |
+
// Environment gather with MIS against the env-NEE at the previous
|
| 472 |
+
// vertex (weight 1 after the camera or a delta vertex, or outside
|
| 473 |
+
// MIS mode).
|
| 474 |
+
if (has_env) {
|
| 475 |
+
float E[3];
|
| 476 |
+
int ei[4];
|
| 477 |
+
float ew[4];
|
| 478 |
+
env_fetch(sc, rd, E, ei, ew);
|
| 479 |
+
float w = 1.0f;
|
| 480 |
+
if (mode == kModeMis && !prev_delta)
|
| 481 |
+
w = mis_w(prev_pdf, env_pdf(sc, rd));
|
| 482 |
+
if (mode != kModeNee || prev_delta)
|
| 483 |
+
add_term<GRAD>(fs, nk, T, nullptr, w, E, -1, ei, ew, acc, gs,
|
| 484 |
+
g_tex, s_gt, s_ge, g_env);
|
| 485 |
+
}
|
| 486 |
+
break;
|
| 487 |
+
}
|
| 488 |
|
| 489 |
int m = sc.mats[face];
|
| 490 |
+
int mt = sc.mtype[m];
|
| 491 |
float3 x = ro + rd * tbest;
|
| 492 |
float3 n = normalize(ng);
|
| 493 |
+
bool backface = dot(n, rd) > 0.0f;
|
| 494 |
+
if (backface) n = neg(n);
|
| 495 |
|
| 496 |
float am[3];
|
| 497 |
int idx4[4];
|
|
|
|
| 499 |
sample_albedo(sc, m, face, bu, bv, am, idx4, w4);
|
| 500 |
const float* em = &sc.emi[m * 3];
|
| 501 |
|
| 502 |
+
// Emission gather. BRDF mode: always. NEE mode: camera hit only. MIS:
|
| 503 |
+
// always, weighted against the light-NEE at the previous vertex.
|
| 504 |
+
bool emissive = em[0] > 0.0f || em[1] > 0.0f || em[2] > 0.0f;
|
| 505 |
+
if (emissive) {
|
| 506 |
+
float w = 1.0f;
|
| 507 |
+
bool add = true;
|
| 508 |
+
if (mode == kModeNee) {
|
| 509 |
+
add = (k == 0);
|
| 510 |
+
} else if (mode == kModeMis && !prev_delta && sc.nl > 0) {
|
| 511 |
+
float cy = fabsf(dot(normalize(ng), rd));
|
| 512 |
+
float p_l = (tbest * tbest) / fmaxf(cy * sc.larea, 1e-12f);
|
| 513 |
+
w = mis_w(prev_pdf, p_l);
|
| 514 |
+
}
|
| 515 |
+
if (add) {
|
| 516 |
+
float E[3] = {em[0], em[1], em[2]};
|
| 517 |
+
add_term<GRAD>(fs, nk, T, nullptr, w, E, m, nullptr, nullptr, acc, gs,
|
| 518 |
+
g_tex, s_gt, s_ge, g_env);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 519 |
}
|
| 520 |
}
|
| 521 |
|
| 522 |
+
// ---------------- dielectric: delta lobes, no NEE, unit factor
|
| 523 |
+
if (mt == kDielectric) {
|
| 524 |
+
float r1 = curand_uniform(&st);
|
| 525 |
+
float r2 = curand_uniform(&st);
|
| 526 |
+
(void)r2;
|
| 527 |
+
float eta = backface ? sc.mior[m] : 1.0f / sc.mior[m]; // n1/n2
|
| 528 |
+
float ci = -dot(rd, n);
|
| 529 |
+
float F = fresnel_dielectric(ci, 1.0f / eta);
|
| 530 |
+
float3 nd;
|
| 531 |
+
if (r1 < F) {
|
| 532 |
+
nd = rd + n * (2.0f * ci);
|
| 533 |
+
ro = x + n * kRayEps;
|
| 534 |
+
} else {
|
| 535 |
+
float s2 = eta * eta * (1.0f - ci * ci);
|
| 536 |
+
float ct = sqrtf(fmaxf(0.0f, 1.0f - s2));
|
| 537 |
+
nd = rd * eta + n * (eta * ci - ct);
|
| 538 |
+
ro = x - n * kRayEps;
|
| 539 |
+
}
|
| 540 |
+
rd = normalize(nd);
|
| 541 |
+
if (nk < kMaxBounces) {
|
| 542 |
+
Factor& f = fs[nk++];
|
| 543 |
+
for (int c = 0; c < 3; ++c) { f.val[c] = 1.0f; f.dc[c] = 0.0f; }
|
| 544 |
#pragma unroll
|
| 545 |
+
for (int i = 0; i < 4; ++i) { f.fi[i] = idx4[i]; f.fw[i] = w4[i]; }
|
|
|
|
|
|
|
| 546 |
}
|
| 547 |
+
prev_delta = true;
|
| 548 |
+
prev_pdf = 0.0f;
|
| 549 |
+
continue;
|
| 550 |
}
|
| 551 |
|
| 552 |
+
// Local frame; wi = toward camera.
|
| 553 |
+
float3 tang, bit;
|
| 554 |
+
onb(n, tang, bit);
|
| 555 |
+
float3 wi_w = neg(rd);
|
| 556 |
+
float3 wi = f3(dot(wi_w, tang), dot(wi_w, bit), dot(wi_w, n));
|
| 557 |
+
wi.z = fmaxf(wi.z, 1e-6f);
|
| 558 |
+
float alpha = fmaxf(sc.mrough[m], 0.01f);
|
| 559 |
+
float a2 = alpha * alpha;
|
| 560 |
+
|
| 561 |
+
// ---------------- light NEE (area lights)
|
| 562 |
+
bool nee_here = (mode == kModeNee && sc.nl > 0 && k + 1 < B) ||
|
| 563 |
+
(mode == kModeMis && sc.nl > 0);
|
| 564 |
+
if ((mode != kModeBrdf) && sc.nl > 0 &&
|
| 565 |
+
(mode == kModeMis || k + 1 < B)) {
|
| 566 |
float r1 = curand_uniform(&st);
|
| 567 |
float r2 = curand_uniform(&st);
|
| 568 |
float r3 = curand_uniform(&st);
|
| 569 |
+
if (nee_here) {
|
| 570 |
+
int li = cdf_pick(sc.lcdf, sc.nl, r1);
|
| 571 |
+
int lface = sc.lf[li];
|
| 572 |
+
const float* lv = &sc.tris[lface * 9];
|
| 573 |
+
float3 lv0 = f3(lv[0], lv[1], lv[2]);
|
| 574 |
+
float3 le1 = f3(lv[3], lv[4], lv[5]) - lv0;
|
| 575 |
+
float3 le2 = f3(lv[6], lv[7], lv[8]) - lv0;
|
| 576 |
+
float su = sqrtf(r2);
|
| 577 |
+
float3 y = lv0 + le1 * (1.0f - su) + le2 * (r3 * su);
|
| 578 |
+
float3 ln = normalize(cross(le1, le2));
|
| 579 |
+
float3 dvec = y - x;
|
| 580 |
+
float d2 = fmaxf(dot(dvec, dvec), 1e-12f);
|
| 581 |
+
float d = sqrtf(d2);
|
| 582 |
+
float3 wo_w = dvec * (1.0f / d);
|
| 583 |
+
if (dot(ln, dvec) > 0.0f) ln = neg(ln);
|
| 584 |
+
float cx = dot(n, wo_w);
|
| 585 |
+
float cy = -dot(ln, wo_w);
|
| 586 |
+
int lm = sc.mats[lface];
|
| 587 |
+
if (cx > 1e-6f && cy > 1e-6f && d > kShadowEps * 2.0f &&
|
| 588 |
+
!bvh_occluded(sc, x + n * kRayEps, wo_w, d - kShadowEps)) {
|
| 589 |
+
float3 wo = f3(dot(wo_w, tang), dot(wo_w, bit), cx);
|
| 590 |
+
// f * cos / pdf_A, with pdf_A -> S_geo = cx*cy/d2 * larea
|
| 591 |
+
float S_geo = cx * cy / d2 * sc.larea;
|
| 592 |
+
float tf_val[3], tf_dc[3];
|
| 593 |
+
float S;
|
| 594 |
+
if (mt == kDiffuse) {
|
| 595 |
+
S = S_geo * kInvPi;
|
| 596 |
+
for (int c = 0; c < 3; ++c) { tf_val[c] = am[c]; tf_dc[c] = 1.0f; }
|
| 597 |
+
} else { // conductor
|
| 598 |
+
float3 h = normalize(wi + wo);
|
| 599 |
+
float sgl = schlick_s(fmaxf(dot(wi, h), 0.0f));
|
| 600 |
+
float G2 = 1.0f / (1.0f + ggx_lambda(a2, wi.z) +
|
| 601 |
+
ggx_lambda(a2, wo.z));
|
| 602 |
+
float gg = ggx_d(a2, fmaxf(h.z, 1e-6f)) * G2 /
|
| 603 |
+
(4.0f * wi.z * fmaxf(wo.z, 1e-6f));
|
| 604 |
+
S = S_geo * gg;
|
| 605 |
+
for (int c = 0; c < 3; ++c) {
|
| 606 |
+
tf_val[c] = am[c] * (1.0f - sgl) + sgl;
|
| 607 |
+
tf_dc[c] = 1.0f - sgl;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 608 |
}
|
| 609 |
}
|
| 610 |
+
float w = 1.0f;
|
| 611 |
+
if (mode == kModeMis) {
|
| 612 |
+
float p_lw = d2 / fmaxf(cy * sc.larea, 1e-12f);
|
| 613 |
+
float p_bw = (mt == kDiffuse) ? fmaxf(wo.z, 0.0f) * kInvPi
|
| 614 |
+
: ggx_pdf(a2, wi, wo);
|
| 615 |
+
w = mis_w(p_lw, p_bw);
|
| 616 |
+
}
|
| 617 |
+
if (nk < kMaxBounces) {
|
| 618 |
+
Factor& f = fs[nk];
|
| 619 |
+
for (int c = 0; c < 3; ++c) { f.val[c] = tf_val[c]; f.dc[c] = tf_dc[c]; }
|
| 620 |
+
#pragma unroll
|
| 621 |
+
for (int i = 0; i < 4; ++i) { f.fi[i] = idx4[i]; f.fw[i] = w4[i]; }
|
| 622 |
+
float E[3] = {sc.emi[lm * 3], sc.emi[lm * 3 + 1], sc.emi[lm * 3 + 2]};
|
| 623 |
+
add_term<GRAD>(fs, nk + 1, T, f.val, S * w, E, lm, nullptr, nullptr,
|
| 624 |
+
acc, gs, g_tex, s_gt, s_ge, g_env);
|
| 625 |
+
}
|
| 626 |
}
|
| 627 |
}
|
| 628 |
}
|
| 629 |
|
| 630 |
+
// ---------------- environment NEE (MIS mode only)
|
| 631 |
+
if (mode == kModeMis && has_env) {
|
| 632 |
+
float e1 = curand_uniform(&st);
|
| 633 |
+
float e2 = curand_uniform(&st);
|
| 634 |
+
float e3 = curand_uniform(&st);
|
| 635 |
+
float3 wo_w;
|
| 636 |
+
if (e1 < 0.5f) { // table
|
| 637 |
+
int y = cdf_pick(sc.ecdf_m, sc.eh, e2);
|
| 638 |
+
int xcol = cdf_pick(&sc.ecdf_c[y * sc.ew], sc.ew, e3);
|
| 639 |
+
float u = (xcol + 0.5f) / sc.ew;
|
| 640 |
+
float v = (y + 0.5f) / sc.eh;
|
| 641 |
+
wo_w = equirect_to_dir(u, v);
|
| 642 |
+
} else { // uniform sphere
|
| 643 |
+
float z = 1.0f - 2.0f * e2;
|
| 644 |
+
float rxy = sqrtf(fmaxf(0.0f, 1.0f - z * z));
|
| 645 |
+
float ph = 2.0f * kPi * e3;
|
| 646 |
+
wo_w = f3(rxy * cosf(ph), z, rxy * sinf(ph));
|
| 647 |
+
}
|
| 648 |
+
float cx = dot(n, wo_w);
|
| 649 |
+
if (cx > 1e-6f &&
|
| 650 |
+
!bvh_occluded(sc, x + n * kRayEps, wo_w, kEnvDist)) {
|
| 651 |
+
float p = env_pdf(sc, wo_w);
|
| 652 |
+
float3 wo = f3(dot(wo_w, tang), dot(wo_w, bit), cx);
|
| 653 |
+
float tf_val[3], tf_dc[3];
|
| 654 |
+
float S;
|
| 655 |
+
if (mt == kDiffuse) {
|
| 656 |
+
S = cx * kInvPi / p;
|
| 657 |
+
for (int c = 0; c < 3; ++c) { tf_val[c] = am[c]; tf_dc[c] = 1.0f; }
|
| 658 |
+
} else {
|
| 659 |
+
float3 h = normalize(wi + wo);
|
| 660 |
+
float sgl = schlick_s(fmaxf(dot(wi, h), 0.0f));
|
| 661 |
+
float G2 = 1.0f / (1.0f + ggx_lambda(a2, wi.z) +
|
| 662 |
+
ggx_lambda(a2, wo.z));
|
| 663 |
+
float gg = ggx_d(a2, fmaxf(h.z, 1e-6f)) * G2 /
|
| 664 |
+
(4.0f * wi.z * fmaxf(wo.z, 1e-6f));
|
| 665 |
+
S = cx * gg / p;
|
| 666 |
+
for (int c = 0; c < 3; ++c) {
|
| 667 |
+
tf_val[c] = am[c] * (1.0f - sgl) + sgl;
|
| 668 |
+
tf_dc[c] = 1.0f - sgl;
|
| 669 |
+
}
|
| 670 |
+
}
|
| 671 |
+
float p_bw = (mt == kDiffuse) ? fmaxf(wo.z, 0.0f) * kInvPi
|
| 672 |
+
: ggx_pdf(a2, wi, wo);
|
| 673 |
+
float w = mis_w(p, p_bw);
|
| 674 |
+
float E[3];
|
| 675 |
+
int ei[4];
|
| 676 |
+
float ewt[4];
|
| 677 |
+
env_fetch(sc, wo_w, E, ei, ewt);
|
| 678 |
+
if (nk < kMaxBounces) {
|
| 679 |
+
Factor& f = fs[nk];
|
| 680 |
+
for (int c = 0; c < 3; ++c) { f.val[c] = tf_val[c]; f.dc[c] = tf_dc[c]; }
|
| 681 |
+
#pragma unroll
|
| 682 |
+
for (int i = 0; i < 4; ++i) { f.fi[i] = idx4[i]; f.fw[i] = w4[i]; }
|
| 683 |
+
add_term<GRAD>(fs, nk + 1, T, f.val, S * w, E, -1, ei, ewt, acc, gs,
|
| 684 |
+
g_tex, s_gt, s_ge, g_env);
|
| 685 |
+
}
|
| 686 |
+
}
|
| 687 |
+
}
|
| 688 |
+
|
| 689 |
+
// ---------------- BSDF continuation
|
| 690 |
+
float c1 = curand_uniform(&st);
|
| 691 |
+
float c2 = curand_uniform(&st);
|
| 692 |
+
float3 wo;
|
| 693 |
+
float fv[3], fdc[3];
|
| 694 |
+
if (mt == kDiffuse) {
|
| 695 |
+
float rr = sqrtf(c1);
|
| 696 |
+
float phi = 2.0f * kPi * c2;
|
| 697 |
+
wo = f3(rr * cosf(phi), rr * sinf(phi), sqrtf(fmaxf(0.0f, 1.0f - c1)));
|
| 698 |
+
prev_pdf = fmaxf(wo.z, 1e-8f) * kInvPi;
|
| 699 |
+
for (int c = 0; c < 3; ++c) { fv[c] = am[c]; fdc[c] = 1.0f; }
|
| 700 |
+
} else { // conductor: VNDF sample, weight = F * G2/G1
|
| 701 |
+
float3 h = ggx_sample_vndf(wi, alpha, c1, c2);
|
| 702 |
+
float wih = dot(wi, h);
|
| 703 |
+
wo = h * (2.0f * wih) - wi;
|
| 704 |
+
if (wo.z <= 1e-6f) break; // sampled below the horizon: zero weight
|
| 705 |
+
float sgl = schlick_s(fmaxf(wih, 0.0f));
|
| 706 |
+
float li = ggx_lambda(a2, wi.z);
|
| 707 |
+
float lo = ggx_lambda(a2, wo.z);
|
| 708 |
+
float gw = (1.0f + li) / (1.0f + li + lo); // G2/G1
|
| 709 |
+
prev_pdf = ggx_pdf(a2, wi, wo);
|
| 710 |
+
for (int c = 0; c < 3; ++c) {
|
| 711 |
+
fv[c] = (am[c] * (1.0f - sgl) + sgl) * gw;
|
| 712 |
+
fdc[c] = (1.0f - sgl) * gw;
|
| 713 |
+
}
|
| 714 |
+
}
|
| 715 |
+
prev_delta = false;
|
| 716 |
+
rd = normalize(tang * wo.x + bit * wo.y + n * wo.z);
|
| 717 |
ro = x + n * kRayEps;
|
| 718 |
+
if (nk < kMaxBounces) {
|
| 719 |
+
Factor& f = fs[nk++];
|
| 720 |
+
for (int c = 0; c < 3; ++c) { f.val[c] = fv[c]; f.dc[c] = fdc[c]; }
|
| 721 |
+
#pragma unroll
|
| 722 |
+
for (int i = 0; i < 4; ++i) { f.fi[i] = idx4[i]; f.fw[i] = w4[i]; }
|
| 723 |
+
}
|
| 724 |
+
T[0] *= fv[0];
|
| 725 |
+
T[1] *= fv[1];
|
| 726 |
+
T[2] *= fv[2];
|
| 727 |
}
|
| 728 |
}
|
| 729 |
|
|
|
|
| 738 |
}
|
| 739 |
|
| 740 |
__global__ void k_forward(DevScene sc, DevCam cam, int H, int W, int spp,
|
| 741 |
+
int B, int mode, unsigned long long seed,
|
| 742 |
float* img) {
|
| 743 |
int pid = blockIdx.x * blockDim.x + threadIdx.x;
|
| 744 |
if (pid >= H * W) return;
|
|
|
|
| 751 |
float jx = curand_uniform(&st);
|
| 752 |
float jy = curand_uniform(&st);
|
| 753 |
float3 rd = camera_ray(cam, px, py, W, H, jx, jy);
|
| 754 |
+
trace_path<false>(sc, ro, rd, st, B, mode, acc, nullptr, nullptr, nullptr,
|
| 755 |
+
nullptr, nullptr);
|
| 756 |
}
|
| 757 |
float inv_spp = 1.0f / (float)spp;
|
| 758 |
img[pid * 3 + 0] = acc.x * inv_spp;
|
|
|
|
| 761 |
}
|
| 762 |
|
| 763 |
__global__ void k_backward(DevScene sc, DevCam cam, int H, int W, int spp,
|
| 764 |
+
int B, int mode, unsigned long long seed,
|
| 765 |
+
const float* gimg, float* g_tex, float* ge,
|
| 766 |
+
float* g_env) {
|
| 767 |
extern __shared__ float smem[];
|
| 768 |
+
float* s_ge = smem;
|
| 769 |
bool staged = sc.nt <= kSharedTexels;
|
| 770 |
+
float* s_gt = staged ? smem + sc.nm * 3 : nullptr;
|
| 771 |
int nm3 = sc.nm * 3;
|
| 772 |
int nt3 = staged ? sc.nt * 3 : 0;
|
| 773 |
for (int i = threadIdx.x; i < nm3 + nt3; i += blockDim.x) smem[i] = 0.0f;
|
|
|
|
| 787 |
float jx = curand_uniform(&st);
|
| 788 |
float jy = curand_uniform(&st);
|
| 789 |
float3 rd = camera_ray(cam, px, py, W, H, jx, jy);
|
| 790 |
+
trace_path<true>(sc, ro, rd, st, B, mode, acc, gs, g_tex, s_gt, s_ge,
|
| 791 |
+
g_env);
|
| 792 |
}
|
| 793 |
}
|
| 794 |
|
|
|
|
| 800 |
}
|
| 801 |
}
|
| 802 |
|
| 803 |
+
DevScene make_scene(const PtdSceneArgs& a) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 804 |
DevScene sc;
|
| 805 |
+
sc.tris = a.tris;
|
| 806 |
+
sc.mats = a.mat_ids;
|
| 807 |
+
sc.uvs = a.uvs;
|
| 808 |
+
sc.nf = a.nodes_f;
|
| 809 |
+
sc.ni = a.nodes_i;
|
| 810 |
+
sc.n_nodes = a.n_nodes;
|
| 811 |
+
sc.lf = a.light_faces;
|
| 812 |
+
sc.lcdf = a.light_cdf;
|
| 813 |
+
sc.nl = a.n_lights;
|
| 814 |
+
sc.larea = a.total_light_area;
|
| 815 |
+
sc.tex = a.tex;
|
| 816 |
+
sc.thdr = a.tex_hdr;
|
| 817 |
+
sc.nt = a.n_texels;
|
| 818 |
+
sc.emi = a.emission;
|
| 819 |
+
sc.mtype = a.mat_type;
|
| 820 |
+
sc.mrough = a.mat_rough;
|
| 821 |
+
sc.mior = a.mat_ior;
|
| 822 |
+
sc.nm = a.n_mats;
|
| 823 |
+
sc.env = a.env;
|
| 824 |
+
sc.ew = a.env_w;
|
| 825 |
+
sc.eh = a.env_h;
|
| 826 |
+
sc.ecdf_m = a.env_cdf_m;
|
| 827 |
+
sc.ecdf_c = a.env_cdf_c;
|
| 828 |
+
sc.epdf = a.env_pdf;
|
| 829 |
return sc;
|
| 830 |
}
|
| 831 |
|
|
|
|
| 842 |
|
| 843 |
} // namespace
|
| 844 |
|
| 845 |
+
extern "C" void ptd_forward_launch(const PtdSceneArgs* args, const float* cam,
|
| 846 |
+
int H, int W, int spp, int max_bounces,
|
| 847 |
+
int mode, long long seed, float* image,
|
| 848 |
+
cudaStream_t stream) {
|
| 849 |
+
DevScene sc = make_scene(*args);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 850 |
DevCam dc = make_cam(cam);
|
| 851 |
int blocks = (H * W + kThreads - 1) / kThreads;
|
| 852 |
k_forward<<<blocks, kThreads, 0, stream>>>(
|
| 853 |
+
sc, dc, H, W, spp, max_bounces, mode, (unsigned long long)seed, image);
|
| 854 |
+
}
|
| 855 |
+
|
| 856 |
+
extern "C" void ptd_backward_launch(const PtdSceneArgs* args, const float* cam,
|
| 857 |
+
int H, int W, int spp, int max_bounces,
|
| 858 |
+
int mode, long long seed,
|
| 859 |
+
const float* grad_image, float* grad_tex,
|
| 860 |
+
float* grad_emission, float* grad_env,
|
| 861 |
+
cudaStream_t stream) {
|
| 862 |
+
DevScene sc = make_scene(*args);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 863 |
DevCam dc = make_cam(cam);
|
| 864 |
int blocks = (H * W + kThreads - 1) / kThreads;
|
| 865 |
+
bool staged = args->n_texels <= kSharedTexels;
|
| 866 |
+
size_t smem = (size_t)(args->n_mats * 3 +
|
| 867 |
+
(staged ? args->n_texels * 3 : 0)) * sizeof(float);
|
| 868 |
k_backward<<<blocks, kThreads, smem, stream>>>(
|
| 869 |
+
sc, dc, H, W, spp, max_bounces, mode, (unsigned long long)seed,
|
| 870 |
+
grad_image, grad_tex, grad_emission, grad_env);
|
| 871 |
}
|
pathtracer_diff_cuda/pathtracer_launch.h
CHANGED
|
@@ -6,27 +6,43 @@
|
|
| 6 |
|
| 7 |
extern "C" {
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
-
void
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
long long seed, const float* grad_image,
|
| 30 |
-
float* grad_tex, float* grad_emission, cudaStream_t stream);
|
| 31 |
|
| 32 |
} // extern "C"
|
|
|
|
| 6 |
|
| 7 |
extern "C" {
|
| 8 |
|
| 9 |
+
// All device pointers; ints/floats are host scalars. env* may be null/0
|
| 10 |
+
// when the scene has no environment map.
|
| 11 |
+
struct PtdSceneArgs {
|
| 12 |
+
const float* tris; // [F, 9]
|
| 13 |
+
const int* mat_ids; // [F]
|
| 14 |
+
const float* uvs; // [F, 3, 2]
|
| 15 |
+
int n_faces;
|
| 16 |
+
const float* nodes_f; // [N, 6]
|
| 17 |
+
const int* nodes_i; // [N, 3]
|
| 18 |
+
int n_nodes;
|
| 19 |
+
const int* light_faces; // [L]
|
| 20 |
+
const float* light_cdf; // [L]
|
| 21 |
+
int n_lights;
|
| 22 |
+
float total_light_area;
|
| 23 |
+
const float* tex; // [T, 3]
|
| 24 |
+
const int* tex_hdr; // [M, 3]
|
| 25 |
+
int n_texels;
|
| 26 |
+
const float* emission; // [M, 3]
|
| 27 |
+
const int* mat_type; // [M] 0 diffuse | 1 conductor | 2 dielectric
|
| 28 |
+
const float* mat_rough; // [M]
|
| 29 |
+
const float* mat_ior; // [M]
|
| 30 |
+
int n_mats;
|
| 31 |
+
const float* env; // [Eh*Ew, 3] or null
|
| 32 |
+
int env_w, env_h;
|
| 33 |
+
const float* env_cdf_m; // [Eh]
|
| 34 |
+
const float* env_cdf_c; // [Eh*Ew]
|
| 35 |
+
const float* env_pdf; // [Eh*Ew], sums to 1
|
| 36 |
+
};
|
| 37 |
|
| 38 |
+
void ptd_forward_launch(const PtdSceneArgs* args, const float* cam, int H,
|
| 39 |
+
int W, int spp, int max_bounces, int mode,
|
| 40 |
+
long long seed, float* image, cudaStream_t stream);
|
| 41 |
+
|
| 42 |
+
void ptd_backward_launch(const PtdSceneArgs* args, const float* cam, int H,
|
| 43 |
+
int W, int spp, int max_bounces, int mode,
|
| 44 |
+
long long seed, const float* grad_image,
|
| 45 |
+
float* grad_tex, float* grad_emission,
|
| 46 |
+
float* grad_env, cudaStream_t stream);
|
|
|
|
|
|
|
| 47 |
|
| 48 |
} // extern "C"
|
tests/test_pathtracer_diff.py
CHANGED
|
@@ -353,6 +353,208 @@ def test_texture_inverse_recovery():
|
|
| 353 |
assert err.mean().item() < 0.08, err.mean().item()
|
| 354 |
|
| 355 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
@requires_cuda
|
| 357 |
@pytest.mark.kernels_ci
|
| 358 |
def test_validation():
|
|
@@ -366,6 +568,16 @@ def test_validation():
|
|
| 366 |
cam = ptd.Camera(position=(0.5, 0.5, 0.5), look_at=(0.5, 0.5, 1.0))
|
| 367 |
with pytest.raises(ValueError):
|
| 368 |
ptd.render(scene, cam, 8, 8, spp=1, max_bounces=17)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
img = ptd.render(scene, cam, 8, 8, spp=2, max_bounces=2)
|
| 370 |
assert img.shape == (8, 8, 3) and img.dtype == torch.float32
|
| 371 |
assert torch.isfinite(img).all()
|
|
|
|
| 353 |
assert err.mean().item() < 0.08, err.mean().item()
|
| 354 |
|
| 355 |
|
| 356 |
+
@requires_cuda
|
| 357 |
+
@pytest.mark.kernels_ci
|
| 358 |
+
def test_estimators_agree_three_ways():
|
| 359 |
+
"""BRDF-only, NEE-only, and MIS target the same integral at the same
|
| 360 |
+
bounce budget; their image means must agree within Monte Carlo
|
| 361 |
+
tolerance."""
|
| 362 |
+
scene = cornell(with_box=True)
|
| 363 |
+
cam = cornell_camera()
|
| 364 |
+
m_mis = ptd.render(scene, cam, 64, 64, spp=64, max_bounces=4,
|
| 365 |
+
estimator="mis", seed=1).mean().item()
|
| 366 |
+
m_nee = ptd.render(scene, cam, 64, 64, spp=64, max_bounces=4,
|
| 367 |
+
estimator="nee", seed=2).mean().item()
|
| 368 |
+
m_brdf = ptd.render(scene, cam, 64, 64, spp=1024, max_bounces=4,
|
| 369 |
+
estimator="brdf", seed=3).mean().item()
|
| 370 |
+
assert abs(m_mis - m_nee) / m_nee < 0.03, (m_mis, m_nee)
|
| 371 |
+
assert abs(m_mis - m_brdf) / m_brdf < 0.05, (m_mis, m_brdf)
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
@requires_cuda
|
| 375 |
+
@pytest.mark.kernels_ci
|
| 376 |
+
def test_conductor_under_env_estimators_agree():
|
| 377 |
+
"""A GGX conductor under a constant environment: MIS and BRDF-only
|
| 378 |
+
means must agree, which jointly validates the VNDF weight, the GGX pdf,
|
| 379 |
+
and the environment pdf."""
|
| 380 |
+
verts, faces, mat = [], [], []
|
| 381 |
+
|
| 382 |
+
def quad_w(a, b, c, d, m):
|
| 383 |
+
base = len(verts)
|
| 384 |
+
verts.extend([a, b, c, d])
|
| 385 |
+
faces.extend(quad(base, base + 1, base + 2, base + 3))
|
| 386 |
+
mat.extend([m, m])
|
| 387 |
+
|
| 388 |
+
quad_w((-4, 0, -4), (4, 0, -4), (4, 0, 4), (-4, 0, 4), 0) # floor
|
| 389 |
+
add_box(verts, faces, mat, (-1, 0, -1), (1, 2, 1), 1) # metal box
|
| 390 |
+
env = torch.full((8, 16, 3), 1.0)
|
| 391 |
+
scene = ptd.Scene(verts, faces, mat,
|
| 392 |
+
albedo=[[0.5, 0.5, 0.5], [0.9, 0.6, 0.3]],
|
| 393 |
+
emission=[[0, 0, 0], [0, 0, 0]],
|
| 394 |
+
material_types=[ptd.DIFFUSE, ptd.CONDUCTOR],
|
| 395 |
+
roughness=[0.3, 0.25], env=env)
|
| 396 |
+
cam = ptd.Camera(position=(5, 3, 5), look_at=(0, 1, 0), vfov_deg=45.0)
|
| 397 |
+
m_mis = ptd.render(scene, cam, 64, 64, spp=64, max_bounces=4,
|
| 398 |
+
estimator="mis", seed=1).mean().item()
|
| 399 |
+
m_brdf = ptd.render(scene, cam, 64, 64, spp=512, max_bounces=4,
|
| 400 |
+
estimator="brdf", seed=2).mean().item()
|
| 401 |
+
assert abs(m_mis - m_brdf) / m_brdf < 0.04, (m_mis, m_brdf)
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
@requires_cuda
|
| 405 |
+
@pytest.mark.kernels_ci
|
| 406 |
+
def test_fresnel_slab_analytic():
|
| 407 |
+
"""A smooth glass slab in front of a uniform emitter, viewed at normal
|
| 408 |
+
incidence: the transmitted fraction sums the internal-bounce series to
|
| 409 |
+
(1-R)/(1+R) with R = ((n-1)/(n+1))^2."""
|
| 410 |
+
verts, faces, mat = [], [], []
|
| 411 |
+
|
| 412 |
+
def quad_w(a, b, c, d, m):
|
| 413 |
+
base = len(verts)
|
| 414 |
+
verts.extend([a, b, c, d])
|
| 415 |
+
faces.extend(quad(base, base + 1, base + 2, base + 3))
|
| 416 |
+
mat.extend([m, m])
|
| 417 |
+
|
| 418 |
+
quad_w((-10, -10, 5), (10, -10, 5), (10, 10, 5), (-10, 10, 5), 0) # emitter
|
| 419 |
+
add_box(verts, faces, mat, (-10, -10, 2.0), (10, 10, 2.2), 1) # slab
|
| 420 |
+
n_ior = 1.5
|
| 421 |
+
scene = ptd.Scene(verts, faces, mat,
|
| 422 |
+
albedo=[[0, 0, 0], [0, 0, 0]],
|
| 423 |
+
emission=[[1.0, 1.0, 1.0], [0, 0, 0]],
|
| 424 |
+
material_types=[ptd.DIFFUSE, ptd.DIELECTRIC],
|
| 425 |
+
ior=[1.5, n_ior])
|
| 426 |
+
cam = ptd.Camera(position=(0, 0, 0), look_at=(0, 0, 1), vfov_deg=10.0)
|
| 427 |
+
img = ptd.render(scene, cam, 32, 32, spp=256, max_bounces=10,
|
| 428 |
+
estimator="brdf", seed=5)
|
| 429 |
+
R = ((n_ior - 1) / (n_ior + 1)) ** 2
|
| 430 |
+
expected = (1 - R) / (1 + R)
|
| 431 |
+
got = img[12:20, 12:20].mean().item()
|
| 432 |
+
assert abs(got - expected) / expected < 0.01, (got, expected)
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
@requires_cuda
|
| 436 |
+
@pytest.mark.kernels_ci
|
| 437 |
+
def test_env_furnace_diffuse():
|
| 438 |
+
"""A diffuse plane under a constant environment c: with BRDF sampling
|
| 439 |
+
every sample returns exactly albedo * c (cosine pdf cancellation plus a
|
| 440 |
+
guaranteed env hit), and MIS agrees in expectation."""
|
| 441 |
+
verts, faces, mat = [], [], []
|
| 442 |
+
base = len(verts)
|
| 443 |
+
verts.extend([(-5, 0, -5), (5, 0, -5), (5, 0, 5), (-5, 0, 5)])
|
| 444 |
+
faces.extend(quad(base, base + 1, base + 2, base + 3))
|
| 445 |
+
mat.extend([0, 0])
|
| 446 |
+
c = 0.8
|
| 447 |
+
a = 0.6
|
| 448 |
+
env = torch.full((8, 16, 3), c)
|
| 449 |
+
scene = ptd.Scene(verts, faces, mat, albedo=[[a] * 3],
|
| 450 |
+
emission=[[0, 0, 0]], env=env)
|
| 451 |
+
cam = ptd.Camera(position=(0, 2.0, 0.01), look_at=(0, 0, 0.2),
|
| 452 |
+
vfov_deg=30.0)
|
| 453 |
+
img = ptd.render(scene, cam, 32, 32, spp=4, max_bounces=3,
|
| 454 |
+
estimator="brdf", seed=0)
|
| 455 |
+
assert (img - a * c).abs().max().item() < 2e-3, \
|
| 456 |
+
(img.min().item(), img.max().item(), a * c)
|
| 457 |
+
m_mis = ptd.render(scene, cam, 32, 32, spp=256, max_bounces=3,
|
| 458 |
+
estimator="mis", seed=1).mean().item()
|
| 459 |
+
assert abs(m_mis - a * c) / (a * c) < 0.02, (m_mis, a * c)
|
| 460 |
+
|
| 461 |
+
|
| 462 |
+
@requires_cuda
|
| 463 |
+
@pytest.mark.kernels_ci
|
| 464 |
+
def test_env_texel_gradients_match_fd():
|
| 465 |
+
"""Environment texels are linear in the estimate and the sampling CDF is
|
| 466 |
+
detached, so same-seed central differences are exact."""
|
| 467 |
+
torch.manual_seed(3)
|
| 468 |
+
env0 = torch.rand(4, 8, 3, device="cuda") * 1.5 + 0.2
|
| 469 |
+
verts, faces, mat = [], [], []
|
| 470 |
+
base = len(verts)
|
| 471 |
+
verts.extend([(-5, 0, -5), (5, 0, -5), (5, 0, 5), (-5, 0, 5)])
|
| 472 |
+
faces.extend(quad(base, base + 1, base + 2, base + 3))
|
| 473 |
+
mat.extend([0, 0])
|
| 474 |
+
cam = ptd.Camera(position=(0, 2.0, 0.01), look_at=(0, 0, 0.5),
|
| 475 |
+
vfov_deg=40.0)
|
| 476 |
+
torch.manual_seed(0)
|
| 477 |
+
Wr = torch.rand(32, 32, 3, device="cuda")
|
| 478 |
+
scene = ptd.Scene(verts, faces, mat, albedo=[[0.6] * 3],
|
| 479 |
+
emission=[[0, 0, 0]], env=env0)
|
| 480 |
+
|
| 481 |
+
def loss_of(e):
|
| 482 |
+
# swap texel values under the construction-time (detached) CDF, the
|
| 483 |
+
# frozen-sampling semantics the kernel defines
|
| 484 |
+
scene.env = e.to("cuda")
|
| 485 |
+
img = ptd.render(scene, cam, 32, 32, spp=8, max_bounces=3, seed=11)
|
| 486 |
+
return (img * Wr).sum()
|
| 487 |
+
|
| 488 |
+
e = env0.clone().requires_grad_(True)
|
| 489 |
+
loss_of(e).backward()
|
| 490 |
+
worst = 0.0
|
| 491 |
+
for (y, x, c) in [(0, 0, 0), (1, 4, 1), (1, 7, 2)]: # upper hemisphere
|
| 492 |
+
h = 5e-3
|
| 493 |
+
ep = env0.clone(); ep[y, x, c] += h
|
| 494 |
+
em = env0.clone(); em[y, x, c] -= h
|
| 495 |
+
fd = (loss_of(ep) - loss_of(em)).item() / (2 * h)
|
| 496 |
+
an = e.grad[y, x, c].item()
|
| 497 |
+
assert fd != 0.0, (y, x, c)
|
| 498 |
+
worst = max(worst, abs(an - fd) / abs(fd))
|
| 499 |
+
assert worst < 2e-2, worst
|
| 500 |
+
# a texel no path can see gets exactly zero from both sides
|
| 501 |
+
assert e.grad[3, 0, 0].item() == 0.0
|
| 502 |
+
|
| 503 |
+
|
| 504 |
+
@requires_cuda
|
| 505 |
+
@pytest.mark.kernels_ci
|
| 506 |
+
def test_conductor_albedo_gradients_match_fd():
|
| 507 |
+
"""Conductor F0 enters every term through Schlick Fresnel, which is
|
| 508 |
+
affine in F0, so replay gradients must match same-seed central
|
| 509 |
+
differences through GGX sampling and MIS."""
|
| 510 |
+
cam = cornell_camera()
|
| 511 |
+
H = W = 48
|
| 512 |
+
torch.manual_seed(0)
|
| 513 |
+
Wr = torch.rand(H, W, 3, device="cuda")
|
| 514 |
+
base_alb = torch.tensor([[0.73, 0.73, 0.73], [0.9, 0.5, 0.2],
|
| 515 |
+
[0.12, 0.45, 0.15], [0.78, 0.78, 0.78]],
|
| 516 |
+
device="cuda")
|
| 517 |
+
|
| 518 |
+
def loss_of(alb):
|
| 519 |
+
verts, faces, mat = [], [], []
|
| 520 |
+
|
| 521 |
+
def wall(a, b, c, d, m):
|
| 522 |
+
b0 = len(verts)
|
| 523 |
+
verts.extend([a, b, c, d])
|
| 524 |
+
faces.extend(quad(b0, b0 + 1, b0 + 2, b0 + 3))
|
| 525 |
+
mat.extend([m, m])
|
| 526 |
+
|
| 527 |
+
X, Y, Z = 5.56, 5.488, 5.592
|
| 528 |
+
wall((0, 0, 0), (X, 0, 0), (X, 0, Z), (0, 0, Z), 0)
|
| 529 |
+
wall((0, Y, 0), (0, Y, Z), (X, Y, Z), (X, Y, 0), 0)
|
| 530 |
+
wall((0, 0, Z), (X, 0, Z), (X, Y, Z), (0, Y, Z), 0)
|
| 531 |
+
wall((0, 0, 0), (0, 0, Z), (0, Y, Z), (0, Y, 0), 1) # conductor
|
| 532 |
+
wall((X, 0, 0), (X, Y, 0), (X, Y, Z), (X, 0, Z), 2)
|
| 533 |
+
wall((2.13, Y - 0.001, 2.27), (3.43, Y - 0.001, 2.27),
|
| 534 |
+
(3.43, Y - 0.001, 3.32), (2.13, Y - 0.001, 3.32), 3)
|
| 535 |
+
scene = ptd.Scene(verts, faces, mat, albedo=alb,
|
| 536 |
+
emission=[[0, 0, 0], [0, 0, 0], [0, 0, 0],
|
| 537 |
+
[18.4, 15.6, 8.0]],
|
| 538 |
+
material_types=[ptd.DIFFUSE, ptd.CONDUCTOR,
|
| 539 |
+
ptd.DIFFUSE, ptd.DIFFUSE],
|
| 540 |
+
roughness=[0.3, 0.2, 0.3, 0.3])
|
| 541 |
+
img = ptd.render(scene, cam, H, W, spp=8, max_bounces=3, seed=13)
|
| 542 |
+
return (img * Wr).sum()
|
| 543 |
+
|
| 544 |
+
alb = base_alb.clone().requires_grad_(True)
|
| 545 |
+
loss_of(alb).backward()
|
| 546 |
+
worst = 0.0
|
| 547 |
+
for (i, c) in [(1, 0), (1, 2), (0, 1)]:
|
| 548 |
+
h = 2e-3
|
| 549 |
+
ap = base_alb.clone(); ap[i, c] += h
|
| 550 |
+
am = base_alb.clone(); am[i, c] -= h
|
| 551 |
+
fd = (loss_of(ap) - loss_of(am)).item() / (2 * h)
|
| 552 |
+
an = alb.grad[i, c].item()
|
| 553 |
+
assert fd != 0.0, (i, c)
|
| 554 |
+
worst = max(worst, abs(an - fd) / abs(fd))
|
| 555 |
+
assert worst < 2e-2, worst
|
| 556 |
+
|
| 557 |
+
|
| 558 |
@requires_cuda
|
| 559 |
@pytest.mark.kernels_ci
|
| 560 |
def test_validation():
|
|
|
|
| 568 |
cam = ptd.Camera(position=(0.5, 0.5, 0.5), look_at=(0.5, 0.5, 1.0))
|
| 569 |
with pytest.raises(ValueError):
|
| 570 |
ptd.render(scene, cam, 8, 8, spp=1, max_bounces=17)
|
| 571 |
+
with pytest.raises(ValueError):
|
| 572 |
+
ptd.render(scene, cam, 8, 8, spp=1, estimator="mis", nee=True)
|
| 573 |
+
with pytest.raises(ValueError):
|
| 574 |
+
ptd.Scene(verts, faces, mat, albedo=[[0.5] * 3],
|
| 575 |
+
emission=[[1.0] * 3], material_types=[7])
|
| 576 |
+
env = torch.full((4, 8, 3), 1.0)
|
| 577 |
+
scene_env = ptd.Scene(verts, faces, mat, albedo=[[0.5] * 3],
|
| 578 |
+
emission=[[1.0] * 3], env=env)
|
| 579 |
+
with pytest.raises(ValueError):
|
| 580 |
+
ptd.render(scene_env, cam, 8, 8, spp=1, estimator="nee")
|
| 581 |
img = ptd.render(scene, cam, 8, 8, spp=2, max_bounces=2)
|
| 582 |
assert img.shape == (8, 8, 3) and img.dtype == torch.float32
|
| 583 |
assert torch.isfinite(img).all()
|
torch-ext/pathtracer_diff/__init__.py
CHANGED
|
@@ -1,28 +1,32 @@
|
|
| 1 |
"""pathtracer-diff: a differentiable Monte Carlo path tracer as one kernel.
|
| 2 |
|
| 3 |
-
Forward renders a linear-radiance image with a megakernel path tracer
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
from kernels import get_kernel
|
| 16 |
ptd = get_kernel("phanerozoic/pathtracer-diff", version=1, trust_remote_code=True)
|
| 17 |
|
| 18 |
-
scene = ptd.Scene(vertices, faces, material_ids, albedo, emission,
|
|
|
|
|
|
|
| 19 |
cam = ptd.Camera(position=(2.78, 2.73, -8.0), look_at=(2.78, 2.73, 2.8))
|
| 20 |
-
img = ptd.render(scene, cam,
|
| 21 |
-
img.sum().backward()
|
| 22 |
-
|
| 23 |
-
`albedo` is either an [M, 3] tensor of per-material constants or a list of
|
| 24 |
-
M per-material textures [Hm, Wm, 3]; constants are treated as 1x1 textures,
|
| 25 |
-
so the two forms share one code path and both receive gradients.
|
| 26 |
"""
|
| 27 |
import math
|
| 28 |
|
|
@@ -32,15 +36,172 @@ from ._ops import ops
|
|
| 32 |
|
| 33 |
MAX_MATERIALS = 64
|
| 34 |
MAX_BOUNCES = 16
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
tris = tris.to(torch.float32)
|
| 45 |
F = tris.shape[0]
|
| 46 |
v = tris.reshape(F, 3, 3)
|
|
@@ -50,35 +211,103 @@ def _build_bvh(tris, leaf_size=4):
|
|
| 50 |
|
| 51 |
nodes_f, nodes_i, order = [], [], []
|
| 52 |
|
| 53 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
nid = len(nodes_f)
|
| 55 |
nodes_f.append(None)
|
| 56 |
nodes_i.append(None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
blo = lo[idx].amin(dim=0)
|
| 58 |
bhi = hi[idx].amax(dim=0)
|
| 59 |
pad = 1e-5 * (1.0 + blo.abs() + bhi.abs())
|
| 60 |
nodes_f[nid] = torch.cat([blo - pad, bhi + pad])
|
| 61 |
-
if
|
| 62 |
start = len(order)
|
| 63 |
order.extend(idx.tolist())
|
| 64 |
-
nodes_i[nid] = (start,
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
return (torch.stack(nodes_f),
|
| 83 |
torch.tensor(nodes_i, dtype=torch.int32),
|
| 84 |
torch.tensor(order, dtype=torch.int64))
|
|
@@ -118,18 +347,22 @@ def _as_texture(t, device):
|
|
| 118 |
|
| 119 |
|
| 120 |
class Scene:
|
| 121 |
-
"""Triangle-mesh scene
|
| 122 |
-
|
| 123 |
-
vertices [V, 3]
|
| 124 |
-
|
| 125 |
-
textures
|
| 126 |
-
grad.
|
| 127 |
-
[
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
def __init__(self, vertices, faces, material_ids, albedo, emission,
|
| 132 |
-
uvs=None,
|
|
|
|
| 133 |
vertices = torch.as_tensor(vertices, dtype=torch.float32)
|
| 134 |
faces = torch.as_tensor(faces, dtype=torch.int64)
|
| 135 |
material_ids = torch.as_tensor(material_ids, dtype=torch.int64)
|
|
@@ -160,6 +393,20 @@ class Scene:
|
|
| 160 |
if int(material_ids.max()) >= M or int(material_ids.min()) < 0:
|
| 161 |
raise ValueError("material_ids out of range")
|
| 162 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
shapes = ([(1, 1)] * M if self.albedo is not None else
|
| 164 |
[(int(t.shape[0]), int(t.shape[1]))
|
| 165 |
for t in self.albedo_textures])
|
|
@@ -201,6 +448,35 @@ class Scene:
|
|
| 201 |
total = 0.0
|
| 202 |
cdf = torch.zeros(0, dtype=torch.float32)
|
| 203 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
self.device = device
|
| 205 |
self.tris = tris.to(device).contiguous()
|
| 206 |
self.mat_ids = mat_ids.to(device).contiguous()
|
|
@@ -217,11 +493,8 @@ class Scene:
|
|
| 217 |
return self.tris.shape[0]
|
| 218 |
|
| 219 |
def _flat_albedo(self):
|
| 220 |
-
"""Pack the albedo into the flat [T, 3] texel buffer inside the
|
| 221 |
-
autograd graph, so texel gradients flow back to the caller's
|
| 222 |
-
tensors. Texture shapes must match construction."""
|
| 223 |
if self.albedo is not None:
|
| 224 |
-
if self.albedo.shape[0]
|
| 225 |
raise ValueError("albedo/material count changed")
|
| 226 |
return self.albedo.reshape(-1, 3)
|
| 227 |
parts = []
|
|
@@ -232,52 +505,78 @@ class Scene:
|
|
| 232 |
parts.append(t.reshape(-1, 3))
|
| 233 |
return torch.cat(parts, dim=0)
|
| 234 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
|
| 236 |
class _RenderFn(torch.autograd.Function):
|
| 237 |
@staticmethod
|
| 238 |
-
def forward(ctx, tex_flat, emission, scene, cam_t, H, W, spp,
|
| 239 |
-
|
| 240 |
image = torch.empty(H, W, 3, device=tex_flat.device,
|
| 241 |
dtype=torch.float32)
|
| 242 |
ops.pt_forward(scene.tris, scene.mat_ids, scene.uvs, scene.nodes_f,
|
| 243 |
scene.nodes_i, scene.light_faces, scene.light_cdf,
|
| 244 |
float(scene.total_light_area),
|
| 245 |
tex_flat.detach().contiguous(), scene.tex_hdr,
|
| 246 |
-
emission.detach().contiguous(),
|
| 247 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
ctx.scene = scene
|
| 249 |
ctx.cam_t = cam_t
|
| 250 |
-
ctx.params = (H, W, spp, max_bounces,
|
| 251 |
-
ctx.save_for_backward(tex_flat, emission)
|
| 252 |
return image
|
| 253 |
|
| 254 |
@staticmethod
|
| 255 |
def backward(ctx, grad_image):
|
| 256 |
-
tex_flat, emission = ctx.saved_tensors
|
| 257 |
scene, cam_t = ctx.scene, ctx.cam_t
|
| 258 |
-
H, W, spp, max_bounces,
|
| 259 |
ga = torch.zeros_like(tex_flat)
|
| 260 |
ge = torch.zeros_like(emission)
|
|
|
|
| 261 |
ops.pt_backward(scene.tris, scene.mat_ids, scene.uvs, scene.nodes_f,
|
| 262 |
scene.nodes_i, scene.light_faces, scene.light_cdf,
|
| 263 |
float(scene.total_light_area),
|
| 264 |
tex_flat.detach().contiguous(), scene.tex_hdr,
|
| 265 |
-
emission.detach().contiguous(),
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
|
|
|
|
|
|
|
|
|
| 269 |
|
| 270 |
|
| 271 |
-
def render(scene, camera, height, width, spp=64, max_bounces=4,
|
| 272 |
-
seed=0):
|
| 273 |
"""Render a linear-radiance image [H, W, 3] float32, differentiable with
|
| 274 |
-
respect to the scene's albedo
|
| 275 |
-
|
| 276 |
-
|
|
|
|
|
|
|
| 277 |
if not (1 <= max_bounces <= MAX_BOUNCES):
|
| 278 |
raise ValueError(f"max_bounces must be in [1, {MAX_BOUNCES}]")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
cam_t = camera.tensor(height, width, device=scene.device)
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
int(seed))
|
|
|
|
| 1 |
"""pathtracer-diff: a differentiable Monte Carlo path tracer as one kernel.
|
| 2 |
|
| 3 |
+
Forward renders a linear-radiance image with a megakernel path tracer:
|
| 4 |
+
Lambertian diffuse, GGX conductor (VNDF-sampled, height-correlated Smith),
|
| 5 |
+
and smooth dielectric materials; area lights and an importance-sampled
|
| 6 |
+
equirectangular environment map; multiple importance sampling by default;
|
| 7 |
+
per-texel albedo with bilinear filtering; binned-SAH BVH. One thread owns
|
| 8 |
+
one pixel, so the image is bitwise deterministic.
|
| 9 |
+
|
| 10 |
+
Backward is exact path replay: sampling distributions depend only on
|
| 11 |
+
geometry, frozen material parameters (roughness, ior), the environment CDF
|
| 12 |
+
(detached at Scene construction), and the counter-based Philox stream,
|
| 13 |
+
never on the differentiable parameters. Every radiance term is a product
|
| 14 |
+
of per-bounce factors that are affine in their vertex's albedo texels
|
| 15 |
+
(Schlick Fresnel is affine in F0), times a linear emission or environment
|
| 16 |
+
texel, so the replay differentiates in closed form and scatters through
|
| 17 |
+
the exact bilinear-footprint adjoint. Gradients flow to albedo texels,
|
| 18 |
+
per-material emission, and environment texels. No visibility/silhouette
|
| 19 |
+
gradients: geometry, camera, roughness, and ior are frozen.
|
| 20 |
|
| 21 |
from kernels import get_kernel
|
| 22 |
ptd = get_kernel("phanerozoic/pathtracer-diff", version=1, trust_remote_code=True)
|
| 23 |
|
| 24 |
+
scene = ptd.Scene(vertices, faces, material_ids, albedo, emission,
|
| 25 |
+
uvs=uvs, material_types=types, roughness=rough,
|
| 26 |
+
ior=ior, env=env_map)
|
| 27 |
cam = ptd.Camera(position=(2.78, 2.73, -8.0), look_at=(2.78, 2.73, 2.8))
|
| 28 |
+
img = ptd.render(scene, cam, 512, 512, spp=64)
|
| 29 |
+
img.sum().backward() # grads on albedo texels / emission / env texels
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
"""
|
| 31 |
import math
|
| 32 |
|
|
|
|
| 36 |
|
| 37 |
MAX_MATERIALS = 64
|
| 38 |
MAX_BOUNCES = 16
|
| 39 |
+
DIFFUSE, CONDUCTOR, DIELECTRIC = 0, 1, 2
|
| 40 |
+
_MODES = {"brdf": 0, "nee": 1, "mis": 2}
|
| 41 |
+
|
| 42 |
+
__all__ = ["Scene", "Camera", "render", "ops", "MAX_MATERIALS", "MAX_BOUNCES",
|
| 43 |
+
"DIFFUSE", "CONDUCTOR", "DIELECTRIC"]
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _build_bvh(tris, leaf_size=4, sah_bins=16):
|
| 47 |
+
"""Binned-SAH BVH over [F, 9] triangles, built level-wise with fully
|
| 48 |
+
vectorized torch ops (segmented reductions + stable-sort partitioning),
|
| 49 |
+
so million-triangle scenes build in seconds. Returns (nodes_f [N, 6],
|
| 50 |
+
nodes_i [N, 3], order [F]). nodes_i rows are (left, right, axis << 1)
|
| 51 |
+
for internal nodes and (start, count, 1) for leaves; internal children
|
| 52 |
+
are the (lower, upper) coordinate halves along the split axis, so
|
| 53 |
+
traversal can visit the near child first."""
|
| 54 |
+
dev = "cuda" if torch.cuda.is_available() else "cpu"
|
| 55 |
+
tris = tris.to(torch.float32).to(dev)
|
| 56 |
+
F = tris.shape[0]
|
| 57 |
+
v = tris.reshape(F, 3, 3)
|
| 58 |
+
tlo = v.amin(dim=1)
|
| 59 |
+
thi = v.amax(dim=1)
|
| 60 |
+
cen = (tlo + thi) * 0.5
|
| 61 |
+
|
| 62 |
+
perm = torch.arange(F, device=dev)
|
| 63 |
+
# active segments: contiguous [start, end) ranges of perm, one per node
|
| 64 |
+
starts = torch.zeros(1, dtype=torch.long, device=dev)
|
| 65 |
+
ends = torch.full((1,), F, dtype=torch.long, device=dev)
|
| 66 |
+
ids = torch.zeros(1, dtype=torch.long, device=dev)
|
| 67 |
+
|
| 68 |
+
INF = float("inf")
|
| 69 |
+
nf_parts, ni_parts = [], [] # per-node rows appended in id order
|
| 70 |
+
n_nodes = 1
|
| 71 |
+
|
| 72 |
+
def half_area(lo, hi):
|
| 73 |
+
d = (hi - lo).clamp(min=0)
|
| 74 |
+
return d[..., 0] * d[..., 1] + d[..., 1] * d[..., 2] + \
|
| 75 |
+
d[..., 2] * d[..., 0]
|
| 76 |
+
|
| 77 |
+
while ids.numel() > 0:
|
| 78 |
+
A = ids.numel()
|
| 79 |
+
seg_len = ends - starts
|
| 80 |
+
total = int(seg_len.sum())
|
| 81 |
+
# active positions and per-position segment index
|
| 82 |
+
csum = torch.cumsum(seg_len, 0) - seg_len
|
| 83 |
+
pos = starts.repeat_interleave(seg_len) + \
|
| 84 |
+
(torch.arange(total, device=dev) -
|
| 85 |
+
csum.repeat_interleave(seg_len))
|
| 86 |
+
s = torch.arange(A, device=dev).repeat_interleave(seg_len)
|
| 87 |
+
tri = perm[pos]
|
| 88 |
+
|
| 89 |
+
# per-segment node bounds and centroid bounds
|
| 90 |
+
nlo = torch.full((A, 3), INF, device=dev)
|
| 91 |
+
nhi = torch.full((A, 3), -INF, device=dev)
|
| 92 |
+
clo = torch.full((A, 3), INF, device=dev)
|
| 93 |
+
chi = torch.full((A, 3), -INF, device=dev)
|
| 94 |
+
s3 = s.unsqueeze(1).expand(-1, 3)
|
| 95 |
+
nlo.scatter_reduce_(0, s3, tlo[tri], reduce="amin", include_self=True)
|
| 96 |
+
nhi.scatter_reduce_(0, s3, thi[tri], reduce="amax", include_self=True)
|
| 97 |
+
clo.scatter_reduce_(0, s3, cen[tri], reduce="amin", include_self=True)
|
| 98 |
+
chi.scatter_reduce_(0, s3, cen[tri], reduce="amax", include_self=True)
|
| 99 |
+
pad = 1e-5 * (1.0 + nlo.abs() + nhi.abs())
|
| 100 |
+
bounds = torch.cat([nlo - pad, nhi + pad], dim=1) # [A, 6]
|
| 101 |
+
|
| 102 |
+
leaf_mask = seg_len <= leaf_size
|
| 103 |
+
split_mask = ~leaf_mask
|
| 104 |
+
|
| 105 |
+
# choose axis (largest centroid extent) and bin the centroids
|
| 106 |
+
ext = (chi - clo).clamp(min=0)
|
| 107 |
+
axis = ext.argmax(dim=1) # [A]
|
| 108 |
+
ax_pos = axis[s]
|
| 109 |
+
coord = cen[tri].gather(1, ax_pos.unsqueeze(1)).squeeze(1)
|
| 110 |
+
seg_lo = clo.gather(1, axis.unsqueeze(1)).squeeze(1)[s]
|
| 111 |
+
seg_ext = ext.gather(1, axis.unsqueeze(1)).squeeze(1)[s]
|
| 112 |
+
t = (coord - seg_lo) / seg_ext.clamp(min=1e-30)
|
| 113 |
+
bin_i = (t * sah_bins).long().clamp(0, sah_bins - 1)
|
| 114 |
+
degen = seg_ext <= 1e-12 # all centroids equal on the axis
|
| 115 |
+
|
| 116 |
+
# per-(segment, bin) counts and bounds
|
| 117 |
+
key = s * sah_bins + bin_i
|
| 118 |
+
counts = torch.bincount(key, minlength=A * sah_bins) \
|
| 119 |
+
.reshape(A, sah_bins)
|
| 120 |
+
blo = torch.full((A * sah_bins, 3), INF, device=dev)
|
| 121 |
+
bhi = torch.full((A * sah_bins, 3), -INF, device=dev)
|
| 122 |
+
k3 = key.unsqueeze(1).expand(-1, 3)
|
| 123 |
+
blo.scatter_reduce_(0, k3, tlo[tri], reduce="amin", include_self=True)
|
| 124 |
+
bhi.scatter_reduce_(0, k3, thi[tri], reduce="amax", include_self=True)
|
| 125 |
+
blo = blo.reshape(A, sah_bins, 3)
|
| 126 |
+
bhi = bhi.reshape(A, sah_bins, 3)
|
| 127 |
+
|
| 128 |
+
# prefix (left) and suffix (right) running bounds and counts
|
| 129 |
+
plo = torch.cummin(blo, dim=1).values
|
| 130 |
+
phi = torch.cummax(bhi, dim=1).values
|
| 131 |
+
pn = torch.cumsum(counts, dim=1)
|
| 132 |
+
slo = torch.flip(torch.cummin(torch.flip(blo, [1]), dim=1).values, [1])
|
| 133 |
+
shi = torch.flip(torch.cummax(torch.flip(bhi, [1]), dim=1).values, [1])
|
| 134 |
+
sn = torch.flip(torch.cumsum(torch.flip(counts, [1]), dim=1), [1])
|
| 135 |
+
|
| 136 |
+
# SAH cost of splitting after bin b (b = 0..bins-2)
|
| 137 |
+
nl = pn[:, :-1].float()
|
| 138 |
+
nr = sn[:, 1:].float()
|
| 139 |
+
cost = half_area(plo[:, :-1], phi[:, :-1]) * nl + \
|
| 140 |
+
half_area(slo[:, 1:], shi[:, 1:]) * nr
|
| 141 |
+
cost = torch.where((nl > 0) & (nr > 0), cost,
|
| 142 |
+
torch.full_like(cost, INF))
|
| 143 |
+
best_cost, best_bin = cost.min(dim=1)
|
| 144 |
+
sah_ok = torch.isfinite(best_cost) & ~degen[csum] # per segment
|
| 145 |
+
|
| 146 |
+
# side: left if bin <= best_bin (SAH) else lower positional half
|
| 147 |
+
# (median fallback for degenerate segments)
|
| 148 |
+
side_left = bin_i <= best_bin[s]
|
| 149 |
+
within = pos - starts[s]
|
| 150 |
+
med_left = within < (seg_len[s] // 2)
|
| 151 |
+
use_sah = sah_ok[s]
|
| 152 |
+
side_left = torch.where(use_sah, side_left, med_left)
|
| 153 |
+
side_left = side_left & split_mask[s]
|
| 154 |
+
|
| 155 |
+
# per-segment left count; guard SAH splits that put everything on
|
| 156 |
+
# one side (possible only via numerics) with the median fallback
|
| 157 |
+
nl_seg = torch.zeros(A, dtype=torch.long, device=dev)
|
| 158 |
+
nl_seg.scatter_add_(0, s, side_left.long())
|
| 159 |
+
bad = split_mask & ((nl_seg == 0) | (nl_seg == seg_len))
|
| 160 |
+
if bool(bad.any()):
|
| 161 |
+
fix = bad[s]
|
| 162 |
+
side_left = torch.where(fix, med_left & split_mask[s], side_left)
|
| 163 |
+
nl_seg = torch.zeros(A, dtype=torch.long, device=dev)
|
| 164 |
+
nl_seg.scatter_add_(0, s, side_left.long())
|
| 165 |
+
|
| 166 |
+
# partition each segment in place: stable sort by (segment, right?)
|
| 167 |
+
k2 = s * 2 + (~side_left).long()
|
| 168 |
+
order2 = torch.argsort(k2, stable=True)
|
| 169 |
+
perm[pos] = tri[order2]
|
| 170 |
+
|
| 171 |
+
# allocate children for splitting segments, emit node rows
|
| 172 |
+
n_split = int(split_mask.sum())
|
| 173 |
+
child_rank = torch.cumsum(split_mask.long(), 0) - 1
|
| 174 |
+
left_ids = n_nodes + 2 * child_rank
|
| 175 |
+
right_ids = left_ids + 1
|
| 176 |
+
|
| 177 |
+
ni = torch.empty(A, 3, dtype=torch.int64, device=dev)
|
| 178 |
+
ni[:, 0] = torch.where(split_mask, left_ids, starts)
|
| 179 |
+
ni[:, 1] = torch.where(split_mask, right_ids, seg_len)
|
| 180 |
+
ni[:, 2] = torch.where(split_mask, axis * 2,
|
| 181 |
+
torch.ones_like(axis))
|
| 182 |
+
nf_parts.append((ids, bounds))
|
| 183 |
+
ni_parts.append((ids, ni))
|
| 184 |
+
|
| 185 |
+
# next level
|
| 186 |
+
mid = starts + nl_seg
|
| 187 |
+
new_starts = torch.cat([starts[split_mask], mid[split_mask]])
|
| 188 |
+
new_ends = torch.cat([mid[split_mask], ends[split_mask]])
|
| 189 |
+
new_ids = torch.cat([left_ids[split_mask], right_ids[split_mask]])
|
| 190 |
+
n_nodes += 2 * n_split
|
| 191 |
+
starts, ends, ids = new_starts, new_ends, new_ids
|
| 192 |
+
|
| 193 |
+
nodes_f = torch.empty(n_nodes, 6, device=dev)
|
| 194 |
+
nodes_i = torch.empty(n_nodes, 3, dtype=torch.int64, device=dev)
|
| 195 |
+
for idv, rows in nf_parts:
|
| 196 |
+
nodes_f[idv] = rows
|
| 197 |
+
for idv, rows in ni_parts:
|
| 198 |
+
nodes_i[idv] = rows
|
| 199 |
+
return (nodes_f.cpu(), nodes_i.to(torch.int32).cpu(), perm.cpu())
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def _build_bvh_reference(tris, leaf_size=4, force_split=8, sah_bins=16):
|
| 203 |
+
"""Recursive reference builder (kept for cross-checking the vectorized
|
| 204 |
+
build in development)."""
|
| 205 |
tris = tris.to(torch.float32)
|
| 206 |
F = tris.shape[0]
|
| 207 |
v = tris.reshape(F, 3, 3)
|
|
|
|
| 211 |
|
| 212 |
nodes_f, nodes_i, order = [], [], []
|
| 213 |
|
| 214 |
+
def half_area(blo, bhi):
|
| 215 |
+
d = (bhi - blo).clamp(min=0)
|
| 216 |
+
return d[0] * d[1] + d[1] * d[2] + d[2] * d[0]
|
| 217 |
+
|
| 218 |
+
stack = []
|
| 219 |
+
|
| 220 |
+
def alloc(idx):
|
| 221 |
nid = len(nodes_f)
|
| 222 |
nodes_f.append(None)
|
| 223 |
nodes_i.append(None)
|
| 224 |
+
stack.append((nid, idx))
|
| 225 |
+
return nid
|
| 226 |
+
|
| 227 |
+
root = alloc(torch.arange(F))
|
| 228 |
+
while stack:
|
| 229 |
+
nid, idx = stack.pop()
|
| 230 |
+
n = idx.numel()
|
| 231 |
blo = lo[idx].amin(dim=0)
|
| 232 |
bhi = hi[idx].amax(dim=0)
|
| 233 |
pad = 1e-5 * (1.0 + blo.abs() + bhi.abs())
|
| 234 |
nodes_f[nid] = torch.cat([blo - pad, bhi + pad])
|
| 235 |
+
if n <= leaf_size:
|
| 236 |
start = len(order)
|
| 237 |
order.extend(idx.tolist())
|
| 238 |
+
nodes_i[nid] = (start, n, 1)
|
| 239 |
+
continue
|
| 240 |
+
|
| 241 |
+
cb_lo = cen[idx].amin(dim=0)
|
| 242 |
+
cb_hi = cen[idx].amax(dim=0)
|
| 243 |
+
ext = cb_hi - cb_lo
|
| 244 |
+
axis = int(ext.argmax())
|
| 245 |
+
split_pos = None
|
| 246 |
+
if float(ext[axis]) > 1e-12:
|
| 247 |
+
c = cen[idx, axis]
|
| 248 |
+
edges = torch.linspace(float(cb_lo[axis]), float(cb_hi[axis]),
|
| 249 |
+
sah_bins + 1)
|
| 250 |
+
b = torch.bucketize(c, edges[1:-1])
|
| 251 |
+
counts = torch.bincount(b, minlength=sah_bins)
|
| 252 |
+
# per-bin bounds over the node's triangles
|
| 253 |
+
binlo = torch.full((sah_bins, 3), float("inf"))
|
| 254 |
+
binhi = torch.full((sah_bins, 3), float("-inf"))
|
| 255 |
+
binlo.scatter_reduce_(0, b.unsqueeze(1).expand(-1, 3), lo[idx],
|
| 256 |
+
reduce="amin", include_self=True)
|
| 257 |
+
binhi.scatter_reduce_(0, b.unsqueeze(1).expand(-1, 3), hi[idx],
|
| 258 |
+
reduce="amax", include_self=True)
|
| 259 |
+
best_cost, best_bin = None, None
|
| 260 |
+
nl = 0
|
| 261 |
+
llo = torch.full((3,), float("inf"))
|
| 262 |
+
lhi = torch.full((3,), float("-inf"))
|
| 263 |
+
pre = []
|
| 264 |
+
for i in range(sah_bins - 1):
|
| 265 |
+
if counts[i] > 0:
|
| 266 |
+
llo = torch.minimum(llo, binlo[i])
|
| 267 |
+
lhi = torch.maximum(lhi, binhi[i])
|
| 268 |
+
nl += int(counts[i])
|
| 269 |
+
pre.append((nl, llo.clone(), lhi.clone()))
|
| 270 |
+
nr = 0
|
| 271 |
+
rlo = torch.full((3,), float("inf"))
|
| 272 |
+
rhi = torch.full((3,), float("-inf"))
|
| 273 |
+
for i in range(sah_bins - 1, 0, -1):
|
| 274 |
+
if counts[i] > 0:
|
| 275 |
+
rlo = torch.minimum(rlo, binlo[i])
|
| 276 |
+
rhi = torch.maximum(rhi, binhi[i])
|
| 277 |
+
nr += int(counts[i])
|
| 278 |
+
nl_i, llo_i, lhi_i = pre[i - 1]
|
| 279 |
+
if nl_i == 0 or nr == 0:
|
| 280 |
+
continue
|
| 281 |
+
cost = (half_area(llo_i, lhi_i) * nl_i +
|
| 282 |
+
half_area(rlo, rhi) * nr)
|
| 283 |
+
if best_cost is None or float(cost) < best_cost:
|
| 284 |
+
best_cost = float(cost)
|
| 285 |
+
best_bin = i - 1
|
| 286 |
+
if best_bin is not None:
|
| 287 |
+
parent_area = half_area(blo, bhi)
|
| 288 |
+
leaf_cost = float(n) * float(parent_area)
|
| 289 |
+
split_cost = 0.125 * float(parent_area) + best_cost
|
| 290 |
+
if split_cost < leaf_cost or n > force_split:
|
| 291 |
+
mask = b <= best_bin
|
| 292 |
+
left_idx = idx[mask]
|
| 293 |
+
right_idx = idx[~mask]
|
| 294 |
+
if left_idx.numel() > 0 and right_idx.numel() > 0:
|
| 295 |
+
split_pos = (left_idx, right_idx)
|
| 296 |
+
if split_pos is None:
|
| 297 |
+
if n > force_split and float(ext[axis]) > 1e-12:
|
| 298 |
+
srt = idx[torch.argsort(cen[idx, axis], stable=True)]
|
| 299 |
+
mid = n // 2
|
| 300 |
+
split_pos = (srt[:mid], srt[mid:])
|
| 301 |
+
else:
|
| 302 |
+
start = len(order)
|
| 303 |
+
order.extend(idx.tolist())
|
| 304 |
+
nodes_i[nid] = (start, n, 1)
|
| 305 |
+
continue
|
| 306 |
+
left = alloc(split_pos[0])
|
| 307 |
+
right = alloc(split_pos[1])
|
| 308 |
+
nodes_i[nid] = (left, right, axis << 1)
|
| 309 |
+
|
| 310 |
+
assert root == 0
|
| 311 |
return (torch.stack(nodes_f),
|
| 312 |
torch.tensor(nodes_i, dtype=torch.int32),
|
| 313 |
torch.tensor(order, dtype=torch.int64))
|
|
|
|
| 347 |
|
| 348 |
|
| 349 |
class Scene:
|
| 350 |
+
"""Triangle-mesh scene.
|
| 351 |
+
|
| 352 |
+
vertices [V, 3], faces [F, 3], material_ids [F]. albedo: [M, 3]
|
| 353 |
+
constants or a list of M textures [Hm, Wm, 3] (constants are 1x1
|
| 354 |
+
textures; both forms receive gradients). emission [M, 3] (may require
|
| 355 |
+
grad). uvs: None, [V, 2], or [F, 3, 2]; textures wrap. material_types
|
| 356 |
+
[M] of DIFFUSE|CONDUCTOR|DIELECTRIC (default all DIFFUSE); roughness
|
| 357 |
+
[M] GGX alpha for conductors (default 0.3, clamped >= 0.01); ior [M]
|
| 358 |
+
for dielectrics (default 1.5). env: optional [Eh, Ew, 3] equirect
|
| 359 |
+
radiance map (may require grad); its importance-sampling CDF is built
|
| 360 |
+
detached at construction. Texture and env shapes are fixed at
|
| 361 |
+
construction."""
|
| 362 |
|
| 363 |
def __init__(self, vertices, faces, material_ids, albedo, emission,
|
| 364 |
+
uvs=None, material_types=None, roughness=None, ior=None,
|
| 365 |
+
env=None, device="cuda"):
|
| 366 |
vertices = torch.as_tensor(vertices, dtype=torch.float32)
|
| 367 |
faces = torch.as_tensor(faces, dtype=torch.int64)
|
| 368 |
material_ids = torch.as_tensor(material_ids, dtype=torch.int64)
|
|
|
|
| 393 |
if int(material_ids.max()) >= M or int(material_ids.min()) < 0:
|
| 394 |
raise ValueError("material_ids out of range")
|
| 395 |
|
| 396 |
+
def mvec(x, default, dtype):
|
| 397 |
+
if x is None:
|
| 398 |
+
return torch.full((M,), default, dtype=dtype, device=device)
|
| 399 |
+
t = torch.as_tensor(x, dtype=dtype).reshape(-1).to(device)
|
| 400 |
+
if t.numel() != M:
|
| 401 |
+
raise ValueError("per-material array must have M entries")
|
| 402 |
+
return t
|
| 403 |
+
|
| 404 |
+
self.mat_type = mvec(material_types, DIFFUSE, torch.int32).contiguous()
|
| 405 |
+
if int(self.mat_type.max()) > 2 or int(self.mat_type.min()) < 0:
|
| 406 |
+
raise ValueError("material_types must be 0|1|2")
|
| 407 |
+
self.mat_rough = mvec(roughness, 0.3, torch.float32).contiguous()
|
| 408 |
+
self.mat_ior = mvec(ior, 1.5, torch.float32).contiguous()
|
| 409 |
+
|
| 410 |
shapes = ([(1, 1)] * M if self.albedo is not None else
|
| 411 |
[(int(t.shape[0]), int(t.shape[1]))
|
| 412 |
for t in self.albedo_textures])
|
|
|
|
| 448 |
total = 0.0
|
| 449 |
cdf = torch.zeros(0, dtype=torch.float32)
|
| 450 |
|
| 451 |
+
# environment map + detached sampling tables
|
| 452 |
+
if env is not None:
|
| 453 |
+
env = torch.as_tensor(env, dtype=torch.float32)
|
| 454 |
+
if env.dim() != 3 or env.shape[2] != 3 or env.shape[0] < 2 or \
|
| 455 |
+
env.shape[1] < 2:
|
| 456 |
+
raise ValueError("env must be [Eh, Ew, 3]")
|
| 457 |
+
self.env = env.to(device)
|
| 458 |
+
eh, ew = int(env.shape[0]), int(env.shape[1])
|
| 459 |
+
lum = self.env.detach().mean(dim=2).cpu().double() + 1e-8
|
| 460 |
+
sint = torch.sin((torch.arange(eh, dtype=torch.float64) + 0.5)
|
| 461 |
+
* math.pi / eh).clamp(min=1e-4)
|
| 462 |
+
roww = (lum.sum(dim=1) * sint)
|
| 463 |
+
row_p = roww / roww.sum()
|
| 464 |
+
cdf_m = row_p.cumsum(0).float()
|
| 465 |
+
col_p = lum / lum.sum(dim=1, keepdim=True)
|
| 466 |
+
cdf_c = col_p.cumsum(1).float()
|
| 467 |
+
pdf_img = (row_p.unsqueeze(1) * col_p).float() # sums to 1
|
| 468 |
+
self.env_w, self.env_h = ew, eh
|
| 469 |
+
self.env_cdf_m = cdf_m.to(device).contiguous()
|
| 470 |
+
self.env_cdf_c = cdf_c.reshape(-1).to(device).contiguous()
|
| 471 |
+
self.env_pdf = pdf_img.reshape(-1).to(device).contiguous()
|
| 472 |
+
else:
|
| 473 |
+
self.env = None
|
| 474 |
+
self.env_w = self.env_h = 0
|
| 475 |
+
z = torch.zeros(0, dtype=torch.float32, device=device)
|
| 476 |
+
self.env_cdf_m = z
|
| 477 |
+
self.env_cdf_c = z
|
| 478 |
+
self.env_pdf = z
|
| 479 |
+
|
| 480 |
self.device = device
|
| 481 |
self.tris = tris.to(device).contiguous()
|
| 482 |
self.mat_ids = mat_ids.to(device).contiguous()
|
|
|
|
| 493 |
return self.tris.shape[0]
|
| 494 |
|
| 495 |
def _flat_albedo(self):
|
|
|
|
|
|
|
|
|
|
| 496 |
if self.albedo is not None:
|
| 497 |
+
if self.albedo.shape[0] != self.tex_hdr.shape[0]:
|
| 498 |
raise ValueError("albedo/material count changed")
|
| 499 |
return self.albedo.reshape(-1, 3)
|
| 500 |
parts = []
|
|
|
|
| 505 |
parts.append(t.reshape(-1, 3))
|
| 506 |
return torch.cat(parts, dim=0)
|
| 507 |
|
| 508 |
+
def _flat_env(self):
|
| 509 |
+
if self.env is None:
|
| 510 |
+
return torch.zeros(0, 3, dtype=torch.float32, device=self.device)
|
| 511 |
+
if (int(self.env.shape[0]), int(self.env.shape[1])) != \
|
| 512 |
+
(self.env_h, self.env_w):
|
| 513 |
+
raise ValueError("env shape is fixed at construction")
|
| 514 |
+
return self.env.reshape(-1, 3)
|
| 515 |
+
|
| 516 |
|
| 517 |
class _RenderFn(torch.autograd.Function):
|
| 518 |
@staticmethod
|
| 519 |
+
def forward(ctx, tex_flat, emission, env_flat, scene, cam_t, H, W, spp,
|
| 520 |
+
max_bounces, mode, seed):
|
| 521 |
image = torch.empty(H, W, 3, device=tex_flat.device,
|
| 522 |
dtype=torch.float32)
|
| 523 |
ops.pt_forward(scene.tris, scene.mat_ids, scene.uvs, scene.nodes_f,
|
| 524 |
scene.nodes_i, scene.light_faces, scene.light_cdf,
|
| 525 |
float(scene.total_light_area),
|
| 526 |
tex_flat.detach().contiguous(), scene.tex_hdr,
|
| 527 |
+
emission.detach().contiguous(), scene.mat_type,
|
| 528 |
+
scene.mat_rough, scene.mat_ior,
|
| 529 |
+
env_flat.detach().contiguous(), scene.env_cdf_m,
|
| 530 |
+
scene.env_cdf_c, scene.env_pdf, scene.env_w,
|
| 531 |
+
scene.env_h, cam_t, spp, max_bounces, mode, seed,
|
| 532 |
+
image)
|
| 533 |
ctx.scene = scene
|
| 534 |
ctx.cam_t = cam_t
|
| 535 |
+
ctx.params = (H, W, spp, max_bounces, mode, seed)
|
| 536 |
+
ctx.save_for_backward(tex_flat, emission, env_flat)
|
| 537 |
return image
|
| 538 |
|
| 539 |
@staticmethod
|
| 540 |
def backward(ctx, grad_image):
|
| 541 |
+
tex_flat, emission, env_flat = ctx.saved_tensors
|
| 542 |
scene, cam_t = ctx.scene, ctx.cam_t
|
| 543 |
+
H, W, spp, max_bounces, mode, seed = ctx.params
|
| 544 |
ga = torch.zeros_like(tex_flat)
|
| 545 |
ge = torch.zeros_like(emission)
|
| 546 |
+
genv = torch.zeros_like(env_flat)
|
| 547 |
ops.pt_backward(scene.tris, scene.mat_ids, scene.uvs, scene.nodes_f,
|
| 548 |
scene.nodes_i, scene.light_faces, scene.light_cdf,
|
| 549 |
float(scene.total_light_area),
|
| 550 |
tex_flat.detach().contiguous(), scene.tex_hdr,
|
| 551 |
+
emission.detach().contiguous(), scene.mat_type,
|
| 552 |
+
scene.mat_rough, scene.mat_ior,
|
| 553 |
+
env_flat.detach().contiguous(), scene.env_cdf_m,
|
| 554 |
+
scene.env_cdf_c, scene.env_pdf, scene.env_w,
|
| 555 |
+
scene.env_h, cam_t, spp, max_bounces, mode, seed,
|
| 556 |
+
grad_image.contiguous(), ga, ge, genv)
|
| 557 |
+
return (ga, ge, genv, None, None, None, None, None, None, None, None)
|
| 558 |
|
| 559 |
|
| 560 |
+
def render(scene, camera, height, width, spp=64, max_bounces=4,
|
| 561 |
+
estimator=None, nee=None, seed=0):
|
| 562 |
"""Render a linear-radiance image [H, W, 3] float32, differentiable with
|
| 563 |
+
respect to the scene's albedo texels, emission, and environment texels.
|
| 564 |
+
estimator: "mis" (default), "nee", or "brdf"; the legacy `nee` bool maps
|
| 565 |
+
True->"nee", False->"brdf". A fixed seed renders the same paths every
|
| 566 |
+
call, so the Monte Carlo objective is a deterministic function of the
|
| 567 |
+
parameters."""
|
| 568 |
if not (1 <= max_bounces <= MAX_BOUNCES):
|
| 569 |
raise ValueError(f"max_bounces must be in [1, {MAX_BOUNCES}]")
|
| 570 |
+
if estimator is not None and nee is not None:
|
| 571 |
+
raise ValueError("pass estimator or nee, not both")
|
| 572 |
+
if estimator is None:
|
| 573 |
+
estimator = "mis" if nee is None else ("nee" if nee else "brdf")
|
| 574 |
+
if estimator not in _MODES:
|
| 575 |
+
raise ValueError('estimator must be "mis", "nee", or "brdf"')
|
| 576 |
+
if estimator == "nee" and scene.env is not None:
|
| 577 |
+
raise ValueError('environment maps require the "mis" or "brdf" estimator')
|
| 578 |
cam_t = camera.tensor(height, width, device=scene.device)
|
| 579 |
+
return _RenderFn.apply(scene._flat_albedo(), scene.emission,
|
| 580 |
+
scene._flat_env(), scene, cam_t, height, width,
|
| 581 |
+
int(spp), int(max_bounces), _MODES[estimator],
|
| 582 |
int(seed))
|
torch-ext/torch_binding.cpp
CHANGED
|
@@ -10,13 +10,28 @@
|
|
| 10 |
|
| 11 |
namespace {
|
| 12 |
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
TORCH_CHECK(tris.is_cuda() && tris.is_contiguous() &&
|
| 21 |
tris.dtype() == torch::kFloat32 && tris.dim() == 2 &&
|
| 22 |
tris.size(1) == 9,
|
|
@@ -37,13 +52,9 @@ void check_scene(const torch::Tensor& tris, const torch::Tensor& mat_ids,
|
|
| 37 |
nodes_i.dtype() == torch::kInt32 && nodes_i.dim() == 2 &&
|
| 38 |
nodes_i.size(1) == 3 && nodes_i.size(0) == nodes_f.size(0),
|
| 39 |
"nodes_i must be contiguous CUDA i32 [N, 3]");
|
| 40 |
-
TORCH_CHECK(light_faces.
|
| 41 |
-
light_faces.dtype() == torch::kInt32,
|
| 42 |
-
"light_faces must be contiguous CUDA i32 [L]");
|
| 43 |
-
TORCH_CHECK(light_cdf.is_cuda() && light_cdf.is_contiguous() &&
|
| 44 |
-
light_cdf.dtype() == torch::kFloat32 &&
|
| 45 |
light_cdf.numel() == light_faces.numel(),
|
| 46 |
-
"
|
| 47 |
TORCH_CHECK(tex.is_cuda() && tex.is_contiguous() &&
|
| 48 |
tex.dtype() == torch::kFloat32 && tex.dim() == 2 &&
|
| 49 |
tex.size(1) == 3,
|
|
@@ -52,16 +63,60 @@ void check_scene(const torch::Tensor& tris, const torch::Tensor& mat_ids,
|
|
| 52 |
tex_hdr.dtype() == torch::kInt32 && tex_hdr.dim() == 2 &&
|
| 53 |
tex_hdr.size(1) == 3,
|
| 54 |
"tex_hdr must be contiguous CUDA i32 [M, 3]");
|
|
|
|
|
|
|
| 55 |
TORCH_CHECK(emission.is_cuda() && emission.is_contiguous() &&
|
| 56 |
emission.dtype() == torch::kFloat32 &&
|
| 57 |
-
emission.
|
| 58 |
-
emission.size(0) == tex_hdr.size(0),
|
| 59 |
"emission must be contiguous CUDA f32 [M, 3]");
|
| 60 |
-
TORCH_CHECK(
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
TORCH_CHECK(spp >= 1, "spp must be >= 1");
|
| 63 |
TORCH_CHECK(max_bounces >= 1 && max_bounces <= 16,
|
| 64 |
"max_bounces must be in [1, 16]");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
}
|
| 66 |
|
| 67 |
} // namespace
|
|
@@ -71,30 +126,28 @@ void pt_forward(torch::Tensor tris, torch::Tensor mat_ids, torch::Tensor uvs,
|
|
| 71 |
torch::Tensor light_faces, torch::Tensor light_cdf,
|
| 72 |
double total_light_area, torch::Tensor tex,
|
| 73 |
torch::Tensor tex_hdr, torch::Tensor emission,
|
| 74 |
-
torch::Tensor
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
TORCH_CHECK(image.is_cuda() && image.is_contiguous() &&
|
| 79 |
image.dtype() == torch::kFloat32 && image.dim() == 3 &&
|
| 80 |
image.size(2) == 3,
|
| 81 |
"image must be contiguous CUDA f32 [H, W, 3]");
|
| 82 |
const at::cuda::CUDAGuard guard(tris.device());
|
| 83 |
cudaStream_t stream = at::cuda::getCurrentCUDAStream();
|
| 84 |
-
int L = (int)light_faces.numel();
|
| 85 |
torch::Tensor cam_h = cam.to(torch::kFloat32).to(torch::kCPU).contiguous();
|
| 86 |
-
ptd_forward_launch(
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
(int)nodes_f.size(0), L ? light_faces.const_data_ptr<int>() : nullptr,
|
| 91 |
-
L ? light_cdf.const_data_ptr<float>() : nullptr, L,
|
| 92 |
-
(float)total_light_area, tex.const_data_ptr<float>(),
|
| 93 |
-
tex_hdr.const_data_ptr<int>(), (int)tex.size(0),
|
| 94 |
-
emission.const_data_ptr<float>(),
|
| 95 |
-
(int)tex_hdr.size(0), cam_h.const_data_ptr<float>(),
|
| 96 |
-
(int)image.size(0), (int)image.size(1), (int)spp, (int)max_bounces,
|
| 97 |
-
nee ? 1 : 0, (long long)seed, image.data_ptr<float>(), stream);
|
| 98 |
C10_CUDA_KERNEL_LAUNCH_CHECK();
|
| 99 |
}
|
| 100 |
|
|
@@ -103,11 +156,19 @@ void pt_backward(torch::Tensor tris, torch::Tensor mat_ids, torch::Tensor uvs,
|
|
| 103 |
torch::Tensor light_faces, torch::Tensor light_cdf,
|
| 104 |
double total_light_area, torch::Tensor tex,
|
| 105 |
torch::Tensor tex_hdr, torch::Tensor emission,
|
| 106 |
-
torch::Tensor
|
| 107 |
-
|
| 108 |
-
torch::Tensor
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
TORCH_CHECK(grad_image.is_cuda() && grad_image.is_contiguous() &&
|
| 112 |
grad_image.dtype() == torch::kFloat32 &&
|
| 113 |
grad_image.dim() == 3 && grad_image.size(2) == 3,
|
|
@@ -118,24 +179,19 @@ void pt_backward(torch::Tensor tris, torch::Tensor mat_ids, torch::Tensor uvs,
|
|
| 118 |
TORCH_CHECK(grad_emission.is_cuda() && grad_emission.is_contiguous() &&
|
| 119 |
grad_emission.sizes() == emission.sizes(),
|
| 120 |
"grad_emission must match emission");
|
|
|
|
|
|
|
| 121 |
const at::cuda::CUDAGuard guard(tris.device());
|
| 122 |
cudaStream_t stream = at::cuda::getCurrentCUDAStream();
|
| 123 |
-
int L = (int)light_faces.numel();
|
| 124 |
torch::Tensor cam_h = cam.to(torch::kFloat32).to(torch::kCPU).contiguous();
|
| 125 |
-
ptd_backward_launch(
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
emission.const_data_ptr<float>(),
|
| 134 |
-
(int)tex_hdr.size(0), cam_h.const_data_ptr<float>(),
|
| 135 |
-
(int)grad_image.size(0), (int)grad_image.size(1), (int)spp,
|
| 136 |
-
(int)max_bounces, nee ? 1 : 0, (long long)seed,
|
| 137 |
-
grad_image.const_data_ptr<float>(), grad_tex.data_ptr<float>(),
|
| 138 |
-
grad_emission.data_ptr<float>(), stream);
|
| 139 |
C10_CUDA_KERNEL_LAUNCH_CHECK();
|
| 140 |
}
|
| 141 |
|
|
@@ -144,7 +200,9 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
|
|
| 144 |
"pt_forward(Tensor tris, Tensor mat_ids, Tensor uvs, Tensor nodes_f,"
|
| 145 |
" Tensor nodes_i, Tensor light_faces, Tensor light_cdf,"
|
| 146 |
" float total_light_area, Tensor tex, Tensor tex_hdr, Tensor emission,"
|
| 147 |
-
" Tensor
|
|
|
|
|
|
|
| 148 |
" Tensor! image) -> ()");
|
| 149 |
ops.impl("pt_forward", torch::kCUDA, &pt_forward);
|
| 150 |
|
|
@@ -152,8 +210,11 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
|
|
| 152 |
"pt_backward(Tensor tris, Tensor mat_ids, Tensor uvs, Tensor nodes_f,"
|
| 153 |
" Tensor nodes_i, Tensor light_faces, Tensor light_cdf,"
|
| 154 |
" float total_light_area, Tensor tex, Tensor tex_hdr, Tensor emission,"
|
| 155 |
-
" Tensor
|
| 156 |
-
" Tensor
|
|
|
|
|
|
|
|
|
|
| 157 |
ops.impl("pt_backward", torch::kCUDA, &pt_backward);
|
| 158 |
}
|
| 159 |
|
|
|
|
| 10 |
|
| 11 |
namespace {
|
| 12 |
|
| 13 |
+
const float* fptr(const torch::Tensor& t) {
|
| 14 |
+
return t.numel() ? t.const_data_ptr<float>() : nullptr;
|
| 15 |
+
}
|
| 16 |
+
const int* iptr(const torch::Tensor& t) {
|
| 17 |
+
return t.numel() ? t.const_data_ptr<int>() : nullptr;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
PtdSceneArgs pack_args(const torch::Tensor& tris, const torch::Tensor& mat_ids,
|
| 21 |
+
const torch::Tensor& uvs, const torch::Tensor& nodes_f,
|
| 22 |
+
const torch::Tensor& nodes_i,
|
| 23 |
+
const torch::Tensor& light_faces,
|
| 24 |
+
const torch::Tensor& light_cdf, double total_light_area,
|
| 25 |
+
const torch::Tensor& tex, const torch::Tensor& tex_hdr,
|
| 26 |
+
const torch::Tensor& emission,
|
| 27 |
+
const torch::Tensor& mat_type,
|
| 28 |
+
const torch::Tensor& mat_rough,
|
| 29 |
+
const torch::Tensor& mat_ior, const torch::Tensor& env,
|
| 30 |
+
const torch::Tensor& env_cdf_m,
|
| 31 |
+
const torch::Tensor& env_cdf_c,
|
| 32 |
+
const torch::Tensor& env_pdf, int64_t env_w,
|
| 33 |
+
int64_t env_h, int64_t spp, int64_t max_bounces,
|
| 34 |
+
int64_t mode) {
|
| 35 |
TORCH_CHECK(tris.is_cuda() && tris.is_contiguous() &&
|
| 36 |
tris.dtype() == torch::kFloat32 && tris.dim() == 2 &&
|
| 37 |
tris.size(1) == 9,
|
|
|
|
| 52 |
nodes_i.dtype() == torch::kInt32 && nodes_i.dim() == 2 &&
|
| 53 |
nodes_i.size(1) == 3 && nodes_i.size(0) == nodes_f.size(0),
|
| 54 |
"nodes_i must be contiguous CUDA i32 [N, 3]");
|
| 55 |
+
TORCH_CHECK(light_faces.dtype() == torch::kInt32 &&
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
light_cdf.numel() == light_faces.numel(),
|
| 57 |
+
"light list mismatch");
|
| 58 |
TORCH_CHECK(tex.is_cuda() && tex.is_contiguous() &&
|
| 59 |
tex.dtype() == torch::kFloat32 && tex.dim() == 2 &&
|
| 60 |
tex.size(1) == 3,
|
|
|
|
| 63 |
tex_hdr.dtype() == torch::kInt32 && tex_hdr.dim() == 2 &&
|
| 64 |
tex_hdr.size(1) == 3,
|
| 65 |
"tex_hdr must be contiguous CUDA i32 [M, 3]");
|
| 66 |
+
int64_t M = tex_hdr.size(0);
|
| 67 |
+
TORCH_CHECK(M <= 64, "at most 64 materials");
|
| 68 |
TORCH_CHECK(emission.is_cuda() && emission.is_contiguous() &&
|
| 69 |
emission.dtype() == torch::kFloat32 &&
|
| 70 |
+
emission.sizes() == torch::IntArrayRef({M, 3}),
|
|
|
|
| 71 |
"emission must be contiguous CUDA f32 [M, 3]");
|
| 72 |
+
TORCH_CHECK(mat_type.dtype() == torch::kInt32 && mat_type.numel() == M,
|
| 73 |
+
"mat_type must be i32 [M]");
|
| 74 |
+
TORCH_CHECK(mat_rough.dtype() == torch::kFloat32 && mat_rough.numel() == M,
|
| 75 |
+
"mat_rough must be f32 [M]");
|
| 76 |
+
TORCH_CHECK(mat_ior.dtype() == torch::kFloat32 && mat_ior.numel() == M,
|
| 77 |
+
"mat_ior must be f32 [M]");
|
| 78 |
+
if (env.numel()) {
|
| 79 |
+
TORCH_CHECK(env.is_cuda() && env.is_contiguous() &&
|
| 80 |
+
env.dtype() == torch::kFloat32 &&
|
| 81 |
+
env.numel() == env_w * env_h * 3,
|
| 82 |
+
"env must be contiguous CUDA f32 [Eh*Ew, 3]");
|
| 83 |
+
TORCH_CHECK(env_cdf_m.numel() == env_h &&
|
| 84 |
+
env_cdf_c.numel() == env_w * env_h &&
|
| 85 |
+
env_pdf.numel() == env_w * env_h,
|
| 86 |
+
"env tables mismatch");
|
| 87 |
+
}
|
| 88 |
TORCH_CHECK(spp >= 1, "spp must be >= 1");
|
| 89 |
TORCH_CHECK(max_bounces >= 1 && max_bounces <= 16,
|
| 90 |
"max_bounces must be in [1, 16]");
|
| 91 |
+
TORCH_CHECK(mode >= 0 && mode <= 2, "mode must be 0|1|2");
|
| 92 |
+
|
| 93 |
+
PtdSceneArgs a;
|
| 94 |
+
a.tris = tris.const_data_ptr<float>();
|
| 95 |
+
a.mat_ids = mat_ids.const_data_ptr<int>();
|
| 96 |
+
a.uvs = uvs.const_data_ptr<float>();
|
| 97 |
+
a.n_faces = (int)tris.size(0);
|
| 98 |
+
a.nodes_f = nodes_f.const_data_ptr<float>();
|
| 99 |
+
a.nodes_i = nodes_i.const_data_ptr<int>();
|
| 100 |
+
a.n_nodes = (int)nodes_f.size(0);
|
| 101 |
+
a.light_faces = iptr(light_faces);
|
| 102 |
+
a.light_cdf = fptr(light_cdf);
|
| 103 |
+
a.n_lights = (int)light_faces.numel();
|
| 104 |
+
a.total_light_area = (float)total_light_area;
|
| 105 |
+
a.tex = tex.const_data_ptr<float>();
|
| 106 |
+
a.tex_hdr = tex_hdr.const_data_ptr<int>();
|
| 107 |
+
a.n_texels = (int)tex.size(0);
|
| 108 |
+
a.emission = emission.const_data_ptr<float>();
|
| 109 |
+
a.mat_type = mat_type.const_data_ptr<int>();
|
| 110 |
+
a.mat_rough = mat_rough.const_data_ptr<float>();
|
| 111 |
+
a.mat_ior = mat_ior.const_data_ptr<float>();
|
| 112 |
+
a.n_mats = (int)M;
|
| 113 |
+
a.env = fptr(env);
|
| 114 |
+
a.env_w = (int)env_w;
|
| 115 |
+
a.env_h = (int)env_h;
|
| 116 |
+
a.env_cdf_m = fptr(env_cdf_m);
|
| 117 |
+
a.env_cdf_c = fptr(env_cdf_c);
|
| 118 |
+
a.env_pdf = fptr(env_pdf);
|
| 119 |
+
return a;
|
| 120 |
}
|
| 121 |
|
| 122 |
} // namespace
|
|
|
|
| 126 |
torch::Tensor light_faces, torch::Tensor light_cdf,
|
| 127 |
double total_light_area, torch::Tensor tex,
|
| 128 |
torch::Tensor tex_hdr, torch::Tensor emission,
|
| 129 |
+
torch::Tensor mat_type, torch::Tensor mat_rough,
|
| 130 |
+
torch::Tensor mat_ior, torch::Tensor env,
|
| 131 |
+
torch::Tensor env_cdf_m, torch::Tensor env_cdf_c,
|
| 132 |
+
torch::Tensor env_pdf, int64_t env_w, int64_t env_h,
|
| 133 |
+
torch::Tensor cam, int64_t spp, int64_t max_bounces,
|
| 134 |
+
int64_t mode, int64_t seed, torch::Tensor image) {
|
| 135 |
+
PtdSceneArgs a = pack_args(tris, mat_ids, uvs, nodes_f, nodes_i, light_faces,
|
| 136 |
+
light_cdf, total_light_area, tex, tex_hdr,
|
| 137 |
+
emission, mat_type, mat_rough, mat_ior, env,
|
| 138 |
+
env_cdf_m, env_cdf_c, env_pdf, env_w, env_h, spp,
|
| 139 |
+
max_bounces, mode);
|
| 140 |
TORCH_CHECK(image.is_cuda() && image.is_contiguous() &&
|
| 141 |
image.dtype() == torch::kFloat32 && image.dim() == 3 &&
|
| 142 |
image.size(2) == 3,
|
| 143 |
"image must be contiguous CUDA f32 [H, W, 3]");
|
| 144 |
const at::cuda::CUDAGuard guard(tris.device());
|
| 145 |
cudaStream_t stream = at::cuda::getCurrentCUDAStream();
|
|
|
|
| 146 |
torch::Tensor cam_h = cam.to(torch::kFloat32).to(torch::kCPU).contiguous();
|
| 147 |
+
ptd_forward_launch(&a, cam_h.const_data_ptr<float>(), (int)image.size(0),
|
| 148 |
+
(int)image.size(1), (int)spp, (int)max_bounces,
|
| 149 |
+
(int)mode, (long long)seed, image.data_ptr<float>(),
|
| 150 |
+
stream);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
C10_CUDA_KERNEL_LAUNCH_CHECK();
|
| 152 |
}
|
| 153 |
|
|
|
|
| 156 |
torch::Tensor light_faces, torch::Tensor light_cdf,
|
| 157 |
double total_light_area, torch::Tensor tex,
|
| 158 |
torch::Tensor tex_hdr, torch::Tensor emission,
|
| 159 |
+
torch::Tensor mat_type, torch::Tensor mat_rough,
|
| 160 |
+
torch::Tensor mat_ior, torch::Tensor env,
|
| 161 |
+
torch::Tensor env_cdf_m, torch::Tensor env_cdf_c,
|
| 162 |
+
torch::Tensor env_pdf, int64_t env_w, int64_t env_h,
|
| 163 |
+
torch::Tensor cam, int64_t spp, int64_t max_bounces,
|
| 164 |
+
int64_t mode, int64_t seed, torch::Tensor grad_image,
|
| 165 |
+
torch::Tensor grad_tex, torch::Tensor grad_emission,
|
| 166 |
+
torch::Tensor grad_env) {
|
| 167 |
+
PtdSceneArgs a = pack_args(tris, mat_ids, uvs, nodes_f, nodes_i, light_faces,
|
| 168 |
+
light_cdf, total_light_area, tex, tex_hdr,
|
| 169 |
+
emission, mat_type, mat_rough, mat_ior, env,
|
| 170 |
+
env_cdf_m, env_cdf_c, env_pdf, env_w, env_h, spp,
|
| 171 |
+
max_bounces, mode);
|
| 172 |
TORCH_CHECK(grad_image.is_cuda() && grad_image.is_contiguous() &&
|
| 173 |
grad_image.dtype() == torch::kFloat32 &&
|
| 174 |
grad_image.dim() == 3 && grad_image.size(2) == 3,
|
|
|
|
| 179 |
TORCH_CHECK(grad_emission.is_cuda() && grad_emission.is_contiguous() &&
|
| 180 |
grad_emission.sizes() == emission.sizes(),
|
| 181 |
"grad_emission must match emission");
|
| 182 |
+
TORCH_CHECK(grad_env.is_contiguous() && grad_env.sizes() == env.sizes(),
|
| 183 |
+
"grad_env must match env");
|
| 184 |
const at::cuda::CUDAGuard guard(tris.device());
|
| 185 |
cudaStream_t stream = at::cuda::getCurrentCUDAStream();
|
|
|
|
| 186 |
torch::Tensor cam_h = cam.to(torch::kFloat32).to(torch::kCPU).contiguous();
|
| 187 |
+
ptd_backward_launch(&a, cam_h.const_data_ptr<float>(),
|
| 188 |
+
(int)grad_image.size(0), (int)grad_image.size(1),
|
| 189 |
+
(int)spp, (int)max_bounces, (int)mode, (long long)seed,
|
| 190 |
+
grad_image.const_data_ptr<float>(),
|
| 191 |
+
grad_tex.data_ptr<float>(),
|
| 192 |
+
grad_emission.data_ptr<float>(),
|
| 193 |
+
grad_env.numel() ? grad_env.data_ptr<float>() : nullptr,
|
| 194 |
+
stream);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
C10_CUDA_KERNEL_LAUNCH_CHECK();
|
| 196 |
}
|
| 197 |
|
|
|
|
| 200 |
"pt_forward(Tensor tris, Tensor mat_ids, Tensor uvs, Tensor nodes_f,"
|
| 201 |
" Tensor nodes_i, Tensor light_faces, Tensor light_cdf,"
|
| 202 |
" float total_light_area, Tensor tex, Tensor tex_hdr, Tensor emission,"
|
| 203 |
+
" Tensor mat_type, Tensor mat_rough, Tensor mat_ior, Tensor env,"
|
| 204 |
+
" Tensor env_cdf_m, Tensor env_cdf_c, Tensor env_pdf, int env_w,"
|
| 205 |
+
" int env_h, Tensor cam, int spp, int max_bounces, int mode, int seed,"
|
| 206 |
" Tensor! image) -> ()");
|
| 207 |
ops.impl("pt_forward", torch::kCUDA, &pt_forward);
|
| 208 |
|
|
|
|
| 210 |
"pt_backward(Tensor tris, Tensor mat_ids, Tensor uvs, Tensor nodes_f,"
|
| 211 |
" Tensor nodes_i, Tensor light_faces, Tensor light_cdf,"
|
| 212 |
" float total_light_area, Tensor tex, Tensor tex_hdr, Tensor emission,"
|
| 213 |
+
" Tensor mat_type, Tensor mat_rough, Tensor mat_ior, Tensor env,"
|
| 214 |
+
" Tensor env_cdf_m, Tensor env_cdf_c, Tensor env_pdf, int env_w,"
|
| 215 |
+
" int env_h, Tensor cam, int spp, int max_bounces, int mode, int seed,"
|
| 216 |
+
" Tensor grad_image, Tensor! grad_tex, Tensor! grad_emission,"
|
| 217 |
+
" Tensor! grad_env) -> ()");
|
| 218 |
ops.impl("pt_backward", torch::kCUDA, &pt_backward);
|
| 219 |
}
|
| 220 |
|
torch-ext/torch_binding.h
CHANGED
|
@@ -7,14 +7,23 @@ void pt_forward(torch::Tensor tris, torch::Tensor mat_ids, torch::Tensor uvs,
|
|
| 7 |
torch::Tensor light_faces, torch::Tensor light_cdf,
|
| 8 |
double total_light_area, torch::Tensor tex,
|
| 9 |
torch::Tensor tex_hdr, torch::Tensor emission,
|
| 10 |
-
torch::Tensor
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
void pt_backward(torch::Tensor tris, torch::Tensor mat_ids, torch::Tensor uvs,
|
| 14 |
torch::Tensor nodes_f, torch::Tensor nodes_i,
|
| 15 |
torch::Tensor light_faces, torch::Tensor light_cdf,
|
| 16 |
double total_light_area, torch::Tensor tex,
|
| 17 |
torch::Tensor tex_hdr, torch::Tensor emission,
|
| 18 |
-
torch::Tensor
|
| 19 |
-
|
| 20 |
-
torch::Tensor
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
torch::Tensor light_faces, torch::Tensor light_cdf,
|
| 8 |
double total_light_area, torch::Tensor tex,
|
| 9 |
torch::Tensor tex_hdr, torch::Tensor emission,
|
| 10 |
+
torch::Tensor mat_type, torch::Tensor mat_rough,
|
| 11 |
+
torch::Tensor mat_ior, torch::Tensor env,
|
| 12 |
+
torch::Tensor env_cdf_m, torch::Tensor env_cdf_c,
|
| 13 |
+
torch::Tensor env_pdf, int64_t env_w, int64_t env_h,
|
| 14 |
+
torch::Tensor cam, int64_t spp, int64_t max_bounces,
|
| 15 |
+
int64_t mode, int64_t seed, torch::Tensor image);
|
| 16 |
|
| 17 |
void pt_backward(torch::Tensor tris, torch::Tensor mat_ids, torch::Tensor uvs,
|
| 18 |
torch::Tensor nodes_f, torch::Tensor nodes_i,
|
| 19 |
torch::Tensor light_faces, torch::Tensor light_cdf,
|
| 20 |
double total_light_area, torch::Tensor tex,
|
| 21 |
torch::Tensor tex_hdr, torch::Tensor emission,
|
| 22 |
+
torch::Tensor mat_type, torch::Tensor mat_rough,
|
| 23 |
+
torch::Tensor mat_ior, torch::Tensor env,
|
| 24 |
+
torch::Tensor env_cdf_m, torch::Tensor env_cdf_c,
|
| 25 |
+
torch::Tensor env_pdf, int64_t env_w, int64_t env_h,
|
| 26 |
+
torch::Tensor cam, int64_t spp, int64_t max_bounces,
|
| 27 |
+
int64_t mode, int64_t seed, torch::Tensor grad_image,
|
| 28 |
+
torch::Tensor grad_tex, torch::Tensor grad_emission,
|
| 29 |
+
torch::Tensor grad_env);
|