pathtracer-diff v1: differentiable path tracer source tree
Browse files- CARD.md +130 -0
- README.md +130 -0
- build.toml +26 -0
- dev/report_local.py +180 -0
- dev/run_local.py +20 -0
- flake.nix +17 -0
- load_local.py +41 -0
- local_bind.cpp +78 -0
- pathtracer_diff_cuda/pathtracer.cu +483 -0
- pathtracer_diff_cuda/pathtracer_launch.h +30 -0
- tests/test_pathtracer_diff.py +227 -0
- torch-ext/pathtracer_diff/__init__.py +212 -0
- torch-ext/torch_binding.cpp +149 -0
- torch-ext/torch_binding.h +20 -0
CARD.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
library_name: kernels
|
| 3 |
+
license: apache-2.0
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# pathtracer-diff
|
| 7 |
+
|
| 8 |
+
A differentiable Monte Carlo path tracer as one kernel, loadable through
|
| 9 |
+
`kernels`. The forward pass renders a linear-radiance image with a megakernel
|
| 10 |
+
unidirectional path tracer (diffuse + emissive materials, cosine-weighted BRDF
|
| 11 |
+
sampling, optional next-event estimation, BVH traversal); the backward pass
|
| 12 |
+
replays the identical paths with the identical counter-based random draws and
|
| 13 |
+
emits analytic gradients to per-material albedo and emission. Inverse
|
| 14 |
+
rendering then runs as a bare torch loop around the kernel, with no rendering
|
| 15 |
+
framework in the loop: render, compare to a target, `backward()`, step.
|
| 16 |
+
|
| 17 |
+
## Usage
|
| 18 |
+
|
| 19 |
+
```python
|
| 20 |
+
import torch
|
| 21 |
+
from kernels import get_kernel
|
| 22 |
+
|
| 23 |
+
ptd = get_kernel("phanerozoic/pathtracer-diff", version=1, trust_remote_code=True)
|
| 24 |
+
|
| 25 |
+
scene = ptd.Scene(vertices, faces, material_ids,
|
| 26 |
+
albedo=albedo, # [M, 3] float32, may require grad
|
| 27 |
+
emission=emission) # [M, 3] float32, may require grad
|
| 28 |
+
cam = ptd.Camera(position=(2.78, 2.73, -8.0), look_at=(2.78, 2.73, 2.8),
|
| 29 |
+
vfov_deg=39.0)
|
| 30 |
+
|
| 31 |
+
img = ptd.render(scene, cam, 512, 512, spp=64, max_bounces=4) # [H, W, 3] linear
|
| 32 |
+
loss = (img - target).square().mean()
|
| 33 |
+
loss.backward() # gradients on scene.albedo / scene.emission
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
`version` selects the release branch; `trust_remote_code` is required by
|
| 37 |
+
`kernels` for publishers without the trusted-publisher mark.
|
| 38 |
+
|
| 39 |
+
A fixed `seed` renders with the same paths every call, so the Monte Carlo
|
| 40 |
+
objective is a deterministic function of the parameters and gradient descent
|
| 41 |
+
sees a smooth landscape at any spp; that is the configuration an
|
| 42 |
+
inverse-rendering loop wants.
|
| 43 |
+
|
| 44 |
+
## API
|
| 45 |
+
|
| 46 |
+
| Symbol | Purpose |
|
| 47 |
+
|---|---|
|
| 48 |
+
| `Scene(vertices, faces, material_ids, albedo, emission)` | triangle-mesh scene; the BVH and the emissive-face light list are built at construction |
|
| 49 |
+
| `Camera(position, look_at, up, vfov_deg)` | pinhole camera |
|
| 50 |
+
| `render(scene, camera, height, width, spp, max_bounces, nee, seed)` | differentiable render to `[H, W, 3]` f32 linear radiance; autograd to `scene.albedo` and `scene.emission` |
|
| 51 |
+
| `ops.pt_forward` / `ops.pt_backward` | raw kernel launches |
|
| 52 |
+
|
| 53 |
+
## How it works
|
| 54 |
+
|
| 55 |
+
One thread owns one pixel and accumulates its spp samples serially, so the
|
| 56 |
+
image is bitwise deterministic with no atomics in the forward pass. Each
|
| 57 |
+
sample is a unidirectional path: cosine-weighted hemisphere sampling makes
|
| 58 |
+
the diffuse throughput multiplier exactly the albedo per bounce, and with
|
| 59 |
+
next-event estimation on, emission is gathered at the camera hit while every
|
| 60 |
+
later vertex estimates direct light by area sampling over the emissive-face
|
| 61 |
+
list. NEE is skipped at the final vertex so the NEE and BRDF-only estimators
|
| 62 |
+
cover camera-to-light paths of the same maximum length and agree in
|
| 63 |
+
expectation at any bounce budget, not just in the limit.
|
| 64 |
+
|
| 65 |
+
The backward pass is exact path replay. The random stream is counter-based
|
| 66 |
+
Philox keyed by `(seed, pixel, sample)`, and sampling directions depend only
|
| 67 |
+
on geometry and that stream, never on albedo or emission, so the backward
|
| 68 |
+
kernel re-traces the identical paths with the identical draws and no stored
|
| 69 |
+
path state. Each radiance term is a product of per-bounce albedo factors, a
|
| 70 |
+
geometry scalar, and an emission value; the replay differentiates it in
|
| 71 |
+
closed form with prefix/suffix exclusion products (no division, so zero
|
| 72 |
+
albedos are safe) and accumulates into per-material gradient buffers through
|
| 73 |
+
block-shared memory. Parameter gradients are exactly the derivative of the
|
| 74 |
+
rendered estimate: interior terms only, no visibility/silhouette gradients,
|
| 75 |
+
which is the correct and complete derivative for materials and emission under
|
| 76 |
+
fixed geometry.
|
| 77 |
+
|
| 78 |
+
## Correctness
|
| 79 |
+
|
| 80 |
+
Measured on RTX 6000 Ada (sm89) from a source build:
|
| 81 |
+
|
| 82 |
+
- **Analytic furnace, zero variance.** In a closed uniform box with BRDF
|
| 83 |
+
sampling, every path returns exactly `E * sum a^k` independent of the
|
| 84 |
+
random walk; at `a = 0.5`, `E = 0.25`, 8 bounces the analytic value is
|
| 85 |
+
0.498046875 and the worst pixel deviates 3.4e-3 (64x64, 4 spp).
|
| 86 |
+
- **Estimator agreement.** The NEE and BRDF-only estimators agree to 0.10%
|
| 87 |
+
on a diffuse room with an area light (128x128; 128 vs 512 spp).
|
| 88 |
+
- **Gradients match finite differences.** With a fixed seed the loss is
|
| 89 |
+
smooth in the parameters along the same paths; analytic replay gradients
|
| 90 |
+
match central differences to a maximum relative error of 3.0e-4 over nine
|
| 91 |
+
albedo and emission entries in a Cornell scene.
|
| 92 |
+
- **Inverse rendering.** Recovering a wall albedo (0.2, 0.5, 0.7) from a
|
| 93 |
+
rendered target by Adam through the kernel reaches a maximum absolute
|
| 94 |
+
error of 0.003 in 80 steps, driving the image loss from 2.6e-4 to 4.1e-8.
|
| 95 |
+
- **Deterministic.** Repeated forward renders are bitwise identical
|
| 96 |
+
(`torch.equal`); gradient buffers accumulate through float atomics and are
|
| 97 |
+
deterministic in expectation but not bitwise.
|
| 98 |
+
|
| 99 |
+
## Measured
|
| 100 |
+
|
| 101 |
+
RTX 6000 Ada, Cornell box with an interior blocker (24 triangles, 4
|
| 102 |
+
materials), 512x512, 64 spp, 4 bounces, NEE on:
|
| 103 |
+
|
| 104 |
+
| pass | time | rate |
|
| 105 |
+
|---|---|---|
|
| 106 |
+
| forward | 8.7 ms | 1.94 Gpaths/s |
|
| 107 |
+
| backward (replay) | 20.1 ms | 2.32x forward |
|
| 108 |
+
|
| 109 |
+
## Requirements and limits
|
| 110 |
+
|
| 111 |
+
- NVIDIA GPU with compute capability 8.0+; float32 throughout.
|
| 112 |
+
- Diffuse (Lambertian) + emissive materials; pinhole camera; at most 64
|
| 113 |
+
materials and 16 bounces; two-sided surfaces.
|
| 114 |
+
- Differentiable parameters are per-material albedo and emission only.
|
| 115 |
+
Geometry, camera, and per-vertex quantities are not differentiable inputs,
|
| 116 |
+
and there are no visibility/silhouette gradients.
|
| 117 |
+
- Published variants are Linux x86_64; on Windows `load_local.py` JIT-builds
|
| 118 |
+
the same source and exposes the identical API.
|
| 119 |
+
|
| 120 |
+
## References
|
| 121 |
+
|
| 122 |
+
Kajiya, "The Rendering Equation" (SIGGRAPH 1986); Vicini, Speierer, Jakob,
|
| 123 |
+
"Path Replay Backpropagation" (SIGGRAPH 2021); Nimier-David, Speierer,
|
| 124 |
+
Ruiz, Jakob, "Radiative Backpropagation" (SIGGRAPH 2020); Möller and
|
| 125 |
+
Trumbore, "Fast, Minimum Storage Ray-Triangle Intersection" (1997); Duff et
|
| 126 |
+
al., "Building an Orthonormal Basis, Revisited" (JCGT 2017).
|
| 127 |
+
|
| 128 |
+
## License
|
| 129 |
+
|
| 130 |
+
Apache-2.0.
|
README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
library_name: kernels
|
| 3 |
+
license: apache-2.0
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# pathtracer-diff
|
| 7 |
+
|
| 8 |
+
A differentiable Monte Carlo path tracer as one kernel, loadable through
|
| 9 |
+
`kernels`. The forward pass renders a linear-radiance image with a megakernel
|
| 10 |
+
unidirectional path tracer (diffuse + emissive materials, cosine-weighted BRDF
|
| 11 |
+
sampling, optional next-event estimation, BVH traversal); the backward pass
|
| 12 |
+
replays the identical paths with the identical counter-based random draws and
|
| 13 |
+
emits analytic gradients to per-material albedo and emission. Inverse
|
| 14 |
+
rendering then runs as a bare torch loop around the kernel, with no rendering
|
| 15 |
+
framework in the loop: render, compare to a target, `backward()`, step.
|
| 16 |
+
|
| 17 |
+
## Usage
|
| 18 |
+
|
| 19 |
+
```python
|
| 20 |
+
import torch
|
| 21 |
+
from kernels import get_kernel
|
| 22 |
+
|
| 23 |
+
ptd = get_kernel("phanerozoic/pathtracer-diff", version=1, trust_remote_code=True)
|
| 24 |
+
|
| 25 |
+
scene = ptd.Scene(vertices, faces, material_ids,
|
| 26 |
+
albedo=albedo, # [M, 3] float32, may require grad
|
| 27 |
+
emission=emission) # [M, 3] float32, may require grad
|
| 28 |
+
cam = ptd.Camera(position=(2.78, 2.73, -8.0), look_at=(2.78, 2.73, 2.8),
|
| 29 |
+
vfov_deg=39.0)
|
| 30 |
+
|
| 31 |
+
img = ptd.render(scene, cam, 512, 512, spp=64, max_bounces=4) # [H, W, 3] linear
|
| 32 |
+
loss = (img - target).square().mean()
|
| 33 |
+
loss.backward() # gradients on scene.albedo / scene.emission
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
`version` selects the release branch; `trust_remote_code` is required by
|
| 37 |
+
`kernels` for publishers without the trusted-publisher mark.
|
| 38 |
+
|
| 39 |
+
A fixed `seed` renders with the same paths every call, so the Monte Carlo
|
| 40 |
+
objective is a deterministic function of the parameters and gradient descent
|
| 41 |
+
sees a smooth landscape at any spp; that is the configuration an
|
| 42 |
+
inverse-rendering loop wants.
|
| 43 |
+
|
| 44 |
+
## API
|
| 45 |
+
|
| 46 |
+
| Symbol | Purpose |
|
| 47 |
+
|---|---|
|
| 48 |
+
| `Scene(vertices, faces, material_ids, albedo, emission)` | triangle-mesh scene; the BVH and the emissive-face light list are built at construction |
|
| 49 |
+
| `Camera(position, look_at, up, vfov_deg)` | pinhole camera |
|
| 50 |
+
| `render(scene, camera, height, width, spp, max_bounces, nee, seed)` | differentiable render to `[H, W, 3]` f32 linear radiance; autograd to `scene.albedo` and `scene.emission` |
|
| 51 |
+
| `ops.pt_forward` / `ops.pt_backward` | raw kernel launches |
|
| 52 |
+
|
| 53 |
+
## How it works
|
| 54 |
+
|
| 55 |
+
One thread owns one pixel and accumulates its spp samples serially, so the
|
| 56 |
+
image is bitwise deterministic with no atomics in the forward pass. Each
|
| 57 |
+
sample is a unidirectional path: cosine-weighted hemisphere sampling makes
|
| 58 |
+
the diffuse throughput multiplier exactly the albedo per bounce, and with
|
| 59 |
+
next-event estimation on, emission is gathered at the camera hit while every
|
| 60 |
+
later vertex estimates direct light by area sampling over the emissive-face
|
| 61 |
+
list. NEE is skipped at the final vertex so the NEE and BRDF-only estimators
|
| 62 |
+
cover camera-to-light paths of the same maximum length and agree in
|
| 63 |
+
expectation at any bounce budget, not just in the limit.
|
| 64 |
+
|
| 65 |
+
The backward pass is exact path replay. The random stream is counter-based
|
| 66 |
+
Philox keyed by `(seed, pixel, sample)`, and sampling directions depend only
|
| 67 |
+
on geometry and that stream, never on albedo or emission, so the backward
|
| 68 |
+
kernel re-traces the identical paths with the identical draws and no stored
|
| 69 |
+
path state. Each radiance term is a product of per-bounce albedo factors, a
|
| 70 |
+
geometry scalar, and an emission value; the replay differentiates it in
|
| 71 |
+
closed form with prefix/suffix exclusion products (no division, so zero
|
| 72 |
+
albedos are safe) and accumulates into per-material gradient buffers through
|
| 73 |
+
block-shared memory. Parameter gradients are exactly the derivative of the
|
| 74 |
+
rendered estimate: interior terms only, no visibility/silhouette gradients,
|
| 75 |
+
which is the correct and complete derivative for materials and emission under
|
| 76 |
+
fixed geometry.
|
| 77 |
+
|
| 78 |
+
## Correctness
|
| 79 |
+
|
| 80 |
+
Measured on RTX 6000 Ada (sm89) from a source build:
|
| 81 |
+
|
| 82 |
+
- **Analytic furnace, zero variance.** In a closed uniform box with BRDF
|
| 83 |
+
sampling, every path returns exactly `E * sum a^k` independent of the
|
| 84 |
+
random walk; at `a = 0.5`, `E = 0.25`, 8 bounces the analytic value is
|
| 85 |
+
0.498046875 and the worst pixel deviates 3.4e-3 (64x64, 4 spp).
|
| 86 |
+
- **Estimator agreement.** The NEE and BRDF-only estimators agree to 0.10%
|
| 87 |
+
on a diffuse room with an area light (128x128; 128 vs 512 spp).
|
| 88 |
+
- **Gradients match finite differences.** With a fixed seed the loss is
|
| 89 |
+
smooth in the parameters along the same paths; analytic replay gradients
|
| 90 |
+
match central differences to a maximum relative error of 3.0e-4 over nine
|
| 91 |
+
albedo and emission entries in a Cornell scene.
|
| 92 |
+
- **Inverse rendering.** Recovering a wall albedo (0.2, 0.5, 0.7) from a
|
| 93 |
+
rendered target by Adam through the kernel reaches a maximum absolute
|
| 94 |
+
error of 0.003 in 80 steps, driving the image loss from 2.6e-4 to 4.1e-8.
|
| 95 |
+
- **Deterministic.** Repeated forward renders are bitwise identical
|
| 96 |
+
(`torch.equal`); gradient buffers accumulate through float atomics and are
|
| 97 |
+
deterministic in expectation but not bitwise.
|
| 98 |
+
|
| 99 |
+
## Measured
|
| 100 |
+
|
| 101 |
+
RTX 6000 Ada, Cornell box with an interior blocker (24 triangles, 4
|
| 102 |
+
materials), 512x512, 64 spp, 4 bounces, NEE on:
|
| 103 |
+
|
| 104 |
+
| pass | time | rate |
|
| 105 |
+
|---|---|---|
|
| 106 |
+
| forward | 8.7 ms | 1.94 Gpaths/s |
|
| 107 |
+
| backward (replay) | 20.1 ms | 2.32x forward |
|
| 108 |
+
|
| 109 |
+
## Requirements and limits
|
| 110 |
+
|
| 111 |
+
- NVIDIA GPU with compute capability 8.0+; float32 throughout.
|
| 112 |
+
- Diffuse (Lambertian) + emissive materials; pinhole camera; at most 64
|
| 113 |
+
materials and 16 bounces; two-sided surfaces.
|
| 114 |
+
- Differentiable parameters are per-material albedo and emission only.
|
| 115 |
+
Geometry, camera, and per-vertex quantities are not differentiable inputs,
|
| 116 |
+
and there are no visibility/silhouette gradients.
|
| 117 |
+
- Published variants are Linux x86_64; on Windows `load_local.py` JIT-builds
|
| 118 |
+
the same source and exposes the identical API.
|
| 119 |
+
|
| 120 |
+
## References
|
| 121 |
+
|
| 122 |
+
Kajiya, "The Rendering Equation" (SIGGRAPH 1986); Vicini, Speierer, Jakob,
|
| 123 |
+
"Path Replay Backpropagation" (SIGGRAPH 2021); Nimier-David, Speierer,
|
| 124 |
+
Ruiz, Jakob, "Radiative Backpropagation" (SIGGRAPH 2020); Möller and
|
| 125 |
+
Trumbore, "Fast, Minimum Storage Ray-Triangle Intersection" (1997); Duff et
|
| 126 |
+
al., "Building an Orthonormal Basis, Revisited" (JCGT 2017).
|
| 127 |
+
|
| 128 |
+
## License
|
| 129 |
+
|
| 130 |
+
Apache-2.0.
|
build.toml
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[general]
|
| 2 |
+
name = "pathtracer-diff"
|
| 3 |
+
version = 1
|
| 4 |
+
edition = 5
|
| 5 |
+
license = "Apache-2.0"
|
| 6 |
+
backends = [
|
| 7 |
+
"cuda",
|
| 8 |
+
]
|
| 9 |
+
|
| 10 |
+
[general.hub]
|
| 11 |
+
repo-id = "phanerozoic/pathtracer-diff"
|
| 12 |
+
|
| 13 |
+
[torch]
|
| 14 |
+
src = [
|
| 15 |
+
"torch-ext/torch_binding.cpp",
|
| 16 |
+
"torch-ext/torch_binding.h",
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
[kernel.pathtracer-diff]
|
| 20 |
+
backend = "cuda"
|
| 21 |
+
cuda-capabilities = ["8.0", "8.6", "8.9", "9.0", "10.0", "12.0"]
|
| 22 |
+
depends = ["torch"]
|
| 23 |
+
src = [
|
| 24 |
+
"pathtracer_diff_cuda/pathtracer.cu",
|
| 25 |
+
"pathtracer_diff_cuda/pathtracer_launch.h",
|
| 26 |
+
]
|
dev/report_local.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Measured numbers for the card, run on the local GPU from source."""
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
import time
|
| 5 |
+
|
| 6 |
+
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 7 |
+
sys.path.insert(0, ROOT)
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
|
| 11 |
+
import load_local
|
| 12 |
+
|
| 13 |
+
ptd = load_local.load()
|
| 14 |
+
|
| 15 |
+
print("GPU:", torch.cuda.get_device_name(0))
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def quad(a, b, c, d):
|
| 19 |
+
return [[a, b, c], [a, c, d]]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def add_box(verts, faces, mat, lo, hi, mat_id):
|
| 23 |
+
x0, y0, z0 = lo
|
| 24 |
+
x1, y1, z1 = hi
|
| 25 |
+
base = len(verts)
|
| 26 |
+
verts.extend([(x0, y0, z0), (x1, y0, z0), (x1, y0, z1), (x0, y0, z1),
|
| 27 |
+
(x0, y1, z0), (x1, y1, z0), (x1, y1, z1), (x0, y1, z1)])
|
| 28 |
+
i = lambda k: base + k
|
| 29 |
+
for q in [quad(i(0), i(1), i(2), i(3)), quad(i(4), i(7), i(6), i(5)),
|
| 30 |
+
quad(i(0), i(4), i(5), i(1)), quad(i(3), i(2), i(6), i(7)),
|
| 31 |
+
quad(i(0), i(3), i(7), i(4)), quad(i(1), i(5), i(6), i(2))]:
|
| 32 |
+
faces.extend(q)
|
| 33 |
+
mat.extend([mat_id, mat_id])
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def cornell(with_box=True):
|
| 37 |
+
verts, faces, mat = [], [], []
|
| 38 |
+
|
| 39 |
+
def wall(a, b, c, d, m):
|
| 40 |
+
base = len(verts)
|
| 41 |
+
verts.extend([a, b, c, d])
|
| 42 |
+
faces.extend(quad(base, base + 1, base + 2, base + 3))
|
| 43 |
+
mat.extend([m, m])
|
| 44 |
+
|
| 45 |
+
X, Y, Z = 5.56, 5.488, 5.592
|
| 46 |
+
wall((0, 0, 0), (X, 0, 0), (X, 0, Z), (0, 0, Z), 0)
|
| 47 |
+
wall((0, Y, 0), (0, Y, Z), (X, Y, Z), (X, Y, 0), 0)
|
| 48 |
+
wall((0, 0, Z), (X, 0, Z), (X, Y, Z), (0, Y, Z), 0)
|
| 49 |
+
wall((0, 0, 0), (0, 0, Z), (0, Y, Z), (0, Y, 0), 1)
|
| 50 |
+
wall((X, 0, 0), (X, Y, 0), (X, Y, Z), (X, 0, Z), 2)
|
| 51 |
+
wall((2.13, Y - 0.001, 2.27), (3.43, Y - 0.001, 2.27),
|
| 52 |
+
(3.43, Y - 0.001, 3.32), (2.13, Y - 0.001, 3.32), 3)
|
| 53 |
+
if with_box:
|
| 54 |
+
add_box(verts, faces, mat, (1.3, 0.0, 0.65), (2.4, 1.65, 1.7), 0)
|
| 55 |
+
albedo = [[0.73, 0.73, 0.73], [0.65, 0.05, 0.05], [0.12, 0.45, 0.15],
|
| 56 |
+
[0.78, 0.78, 0.78]]
|
| 57 |
+
emission = [[0, 0, 0], [0, 0, 0], [0, 0, 0], [18.4, 15.6, 8.0]]
|
| 58 |
+
return ptd.Scene(verts, faces, mat, albedo=albedo, emission=emission)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
cam = ptd.Camera(position=(2.78, 2.73, -8.0), look_at=(2.78, 2.73, 2.8),
|
| 62 |
+
vfov_deg=39.0)
|
| 63 |
+
|
| 64 |
+
# ---- furnace exactness
|
| 65 |
+
a, E, B = 0.5, 0.25, 8
|
| 66 |
+
verts, faces, mat = [], [], []
|
| 67 |
+
add_box(verts, faces, mat, (-2, -2, -2), (2, 2, 2), 0)
|
| 68 |
+
fs = ptd.Scene(verts, faces, mat, albedo=[[a] * 3], emission=[[E] * 3])
|
| 69 |
+
fc = ptd.Camera(position=(0, 0, 0), look_at=(0, 0, 1), vfov_deg=60.0)
|
| 70 |
+
img = ptd.render(fs, fc, 64, 64, spp=4, max_bounces=B, nee=False, seed=0)
|
| 71 |
+
exp = E * (1 - a ** B) / (1 - a)
|
| 72 |
+
print(f"furnace: analytic {exp:.9f}, max abs err {(img - exp).abs().max().item():.2e}")
|
| 73 |
+
|
| 74 |
+
# ---- estimator agreement
|
| 75 |
+
verts, faces, mat = [], [], []
|
| 76 |
+
add_box(verts, faces, mat, (0, 0, 0), (5, 5, 5), 0)
|
| 77 |
+
base = len(verts)
|
| 78 |
+
verts.extend([(1.0, 4.999, 1.0), (4.5, 4.999, 1.0), (4.5, 4.999, 4.5),
|
| 79 |
+
(1.0, 4.999, 4.5)])
|
| 80 |
+
faces.extend(quad(base, base + 1, base + 2, base + 3))
|
| 81 |
+
mat.extend([1, 1])
|
| 82 |
+
es = ptd.Scene(verts, faces, mat, albedo=[[0.6] * 3, [0.0] * 3],
|
| 83 |
+
emission=[[0] * 3, [3.0] * 3])
|
| 84 |
+
ec = ptd.Camera(position=(2.5, 2.5, 0.2), look_at=(2.5, 2.5, 5.0), vfov_deg=70.0)
|
| 85 |
+
m_nee = ptd.render(es, ec, 128, 128, spp=128, max_bounces=4, nee=True, seed=1).mean().item()
|
| 86 |
+
m_brdf = ptd.render(es, ec, 128, 128, spp=512, max_bounces=4, nee=False, seed=2).mean().item()
|
| 87 |
+
print(f"estimators: NEE {m_nee:.5f} vs BRDF {m_brdf:.5f}, rel diff {abs(m_nee-m_brdf)/m_brdf:.4%}")
|
| 88 |
+
|
| 89 |
+
# ---- FD gradient agreement
|
| 90 |
+
scene = cornell(with_box=False)
|
| 91 |
+
H = W = 48
|
| 92 |
+
torch.manual_seed(0)
|
| 93 |
+
Wr = torch.rand(H, W, 3, device="cuda")
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def loss_of(alb, emi):
|
| 97 |
+
scene.albedo = alb
|
| 98 |
+
scene.emission = emi
|
| 99 |
+
return (ptd.render(scene, cam, H, W, spp=8, max_bounces=3, seed=7) * Wr).sum()
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
alb0 = scene.albedo.clone()
|
| 103 |
+
emi0 = scene.emission.clone()
|
| 104 |
+
alb = alb0.clone().requires_grad_(True)
|
| 105 |
+
emi = emi0.clone().requires_grad_(True)
|
| 106 |
+
loss_of(alb, emi).backward()
|
| 107 |
+
worst = 0.0
|
| 108 |
+
for (i, c) in [(0, 0), (0, 1), (0, 2), (1, 0), (2, 1), (2, 2)]:
|
| 109 |
+
h = 2e-3
|
| 110 |
+
ap = alb0.clone(); ap[i, c] += h
|
| 111 |
+
am = alb0.clone(); am[i, c] -= h
|
| 112 |
+
fd = (loss_of(ap, emi0) - loss_of(am, emi0)).item() / (2 * h)
|
| 113 |
+
worst = max(worst, abs(alb.grad[i, c].item() - fd) / abs(fd))
|
| 114 |
+
h = 0.05
|
| 115 |
+
for c in range(3):
|
| 116 |
+
ep = emi0.clone(); ep[3, c] += h
|
| 117 |
+
em = emi0.clone(); em[3, c] -= h
|
| 118 |
+
fd = (loss_of(alb0, ep) - loss_of(alb0, em)).item() / (2 * h)
|
| 119 |
+
worst = max(worst, abs(emi.grad[3, c].item() - fd) / abs(fd))
|
| 120 |
+
print(f"gradients vs central differences: max rel err {worst:.2e} over 9 entries")
|
| 121 |
+
|
| 122 |
+
# ---- inverse rendering
|
| 123 |
+
target_row = torch.tensor([0.2, 0.5, 0.7], device="cuda")
|
| 124 |
+
scene = cornell(with_box=False)
|
| 125 |
+
t_alb = scene.albedo.clone(); t_alb[1] = target_row
|
| 126 |
+
scene.albedo = t_alb
|
| 127 |
+
target = ptd.render(scene, cam, 48, 48, spp=8, max_bounces=3, seed=3).detach()
|
| 128 |
+
alb = t_alb.clone(); alb[1] = torch.tensor([0.5, 0.5, 0.5], device="cuda")
|
| 129 |
+
alb = alb.requires_grad_(True)
|
| 130 |
+
opt = torch.optim.Adam([alb], lr=0.05)
|
| 131 |
+
first = None
|
| 132 |
+
for it in range(80):
|
| 133 |
+
opt.zero_grad()
|
| 134 |
+
scene.albedo = alb
|
| 135 |
+
loss = (ptd.render(scene, cam, 48, 48, spp=8, max_bounces=3, seed=3) - target).square().mean()
|
| 136 |
+
loss.backward()
|
| 137 |
+
alb.grad[0] = 0; alb.grad[2] = 0; alb.grad[3] = 0
|
| 138 |
+
opt.step()
|
| 139 |
+
with torch.no_grad():
|
| 140 |
+
alb.clamp_(0.02, 0.98)
|
| 141 |
+
if first is None:
|
| 142 |
+
first = loss.item()
|
| 143 |
+
print(f"inverse: target {target_row.tolist()} recovered "
|
| 144 |
+
f"{[round(x, 3) for x in alb[1].detach().tolist()]}, "
|
| 145 |
+
f"max abs err {(alb[1].detach() - target_row).abs().max().item():.3f}, "
|
| 146 |
+
f"loss {first:.2e} -> {loss.item():.2e} in 80 steps")
|
| 147 |
+
|
| 148 |
+
# ---- throughput
|
| 149 |
+
scene = cornell(with_box=True)
|
| 150 |
+
H, W, spp, B = 512, 512, 64, 4
|
| 151 |
+
img = ptd.render(scene, cam, H, W, spp=spp, max_bounces=B) # warmup + autograd off
|
| 152 |
+
torch.cuda.synchronize()
|
| 153 |
+
ts = []
|
| 154 |
+
for _ in range(3):
|
| 155 |
+
t0 = time.perf_counter()
|
| 156 |
+
img = ptd.render(scene, cam, H, W, spp=spp, max_bounces=B)
|
| 157 |
+
torch.cuda.synchronize()
|
| 158 |
+
ts.append(time.perf_counter() - t0)
|
| 159 |
+
fwd = sorted(ts)[1]
|
| 160 |
+
paths = H * W * spp
|
| 161 |
+
print(f"forward: {H}x{W} spp {spp} bounces {B} ({scene.n_faces} tris): "
|
| 162 |
+
f"{fwd*1e3:.1f} ms, {paths/fwd/1e6:.0f} Mpaths/s")
|
| 163 |
+
|
| 164 |
+
alb = scene.albedo.clone().requires_grad_(True)
|
| 165 |
+
scene.albedo = alb
|
| 166 |
+
g = torch.ones(H, W, 3, device="cuda")
|
| 167 |
+
img = ptd.render(scene, cam, H, W, spp=spp, max_bounces=B)
|
| 168 |
+
img.backward(g) # warmup
|
| 169 |
+
torch.cuda.synchronize()
|
| 170 |
+
ts = []
|
| 171 |
+
for _ in range(3):
|
| 172 |
+
alb.grad = None
|
| 173 |
+
img = ptd.render(scene, cam, H, W, spp=spp, max_bounces=B)
|
| 174 |
+
torch.cuda.synchronize()
|
| 175 |
+
t0 = time.perf_counter()
|
| 176 |
+
img.backward(g)
|
| 177 |
+
torch.cuda.synchronize()
|
| 178 |
+
ts.append(time.perf_counter() - t0)
|
| 179 |
+
bwd = sorted(ts)[1]
|
| 180 |
+
print(f"backward (replay): {bwd*1e3:.1f} ms, {bwd/fwd:.2f}x forward")
|
dev/run_local.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Build the extension locally and run the test suite against it, stubbing
|
| 2 |
+
kernels.get_kernel so the published-variant tests run natively."""
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
|
| 6 |
+
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 7 |
+
sys.path.insert(0, ROOT)
|
| 8 |
+
|
| 9 |
+
import load_local
|
| 10 |
+
|
| 11 |
+
mod = load_local.load(verbose="-v" in sys.argv)
|
| 12 |
+
|
| 13 |
+
import kernels
|
| 14 |
+
|
| 15 |
+
kernels.get_kernel = lambda *a, **k: mod
|
| 16 |
+
|
| 17 |
+
import pytest
|
| 18 |
+
|
| 19 |
+
sys.exit(pytest.main([os.path.join(ROOT, "tests"), "-x", "-q", "-p",
|
| 20 |
+
"no:cacheprovider"]))
|
flake.nix
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
description = "Flake for pathtracer-diff kernel";
|
| 3 |
+
|
| 4 |
+
inputs = {
|
| 5 |
+
builder.url = "github:huggingface/kernels";
|
| 6 |
+
};
|
| 7 |
+
|
| 8 |
+
outputs =
|
| 9 |
+
{
|
| 10 |
+
self,
|
| 11 |
+
builder,
|
| 12 |
+
}:
|
| 13 |
+
builder.lib.genKernelFlakeOutputs {
|
| 14 |
+
inherit self;
|
| 15 |
+
path = ./.;
|
| 16 |
+
};
|
| 17 |
+
}
|
load_local.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Load pathtracer-diff natively on this machine (Windows/SAURON included).
|
| 2 |
+
|
| 3 |
+
The published Hugging Face kernel carries `*-linux` variants only, so on
|
| 4 |
+
Windows `get_kernel` has no matching build. The source is right here, though:
|
| 5 |
+
JIT-build it with `torch.utils.cpp_extension` and expose the identical
|
| 6 |
+
`Scene` / `Camera` / `render` API.
|
| 7 |
+
|
| 8 |
+
import load_local
|
| 9 |
+
ptd = load_local.load()
|
| 10 |
+
"""
|
| 11 |
+
import os
|
| 12 |
+
import sys
|
| 13 |
+
import types
|
| 14 |
+
|
| 15 |
+
_cached = None
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def load(verbose=False):
|
| 19 |
+
"""Return the pathtracer_diff module, JIT-built from the local source."""
|
| 20 |
+
global _cached
|
| 21 |
+
if _cached is not None:
|
| 22 |
+
return _cached
|
| 23 |
+
from torch.utils.cpp_extension import load as _jit
|
| 24 |
+
|
| 25 |
+
root = os.path.dirname(os.path.abspath(__file__))
|
| 26 |
+
cuda_dir = os.path.join(root, "pathtracer_diff_cuda")
|
| 27 |
+
ext = _jit(name="pathtracer_diff_ext",
|
| 28 |
+
sources=[os.path.join(root, "local_bind.cpp"),
|
| 29 |
+
os.path.join(cuda_dir, "pathtracer.cu")],
|
| 30 |
+
extra_include_paths=[cuda_dir],
|
| 31 |
+
verbose=verbose)
|
| 32 |
+
ops_mod = types.ModuleType("pathtracer_diff._ops")
|
| 33 |
+
ops_mod.ops = type("_Ops", (), {
|
| 34 |
+
"pt_forward": staticmethod(ext.pt_forward),
|
| 35 |
+
"pt_backward": staticmethod(ext.pt_backward),
|
| 36 |
+
})()
|
| 37 |
+
sys.modules["pathtracer_diff._ops"] = ops_mod
|
| 38 |
+
sys.path.insert(0, os.path.join(root, "torch-ext"))
|
| 39 |
+
import pathtracer_diff
|
| 40 |
+
_cached = pathtracer_diff
|
| 41 |
+
return pathtracer_diff
|
local_bind.cpp
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Local development binding (torch.utils.cpp_extension JIT; Windows/MSVC).
|
| 2 |
+
// Mirrors the torch-ext binding's argument order exactly.
|
| 3 |
+
#include <torch/extension.h>
|
| 4 |
+
|
| 5 |
+
#include <ATen/cuda/CUDAContext.h>
|
| 6 |
+
#include <c10/cuda/CUDAGuard.h>
|
| 7 |
+
|
| 8 |
+
#include "pathtracer_launch.h"
|
| 9 |
+
|
| 10 |
+
namespace {
|
| 11 |
+
|
| 12 |
+
void pt_forward(torch::Tensor tris, torch::Tensor mat_ids,
|
| 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 albedo,
|
| 16 |
+
torch::Tensor emission, torch::Tensor cam, int64_t spp,
|
| 17 |
+
int64_t max_bounces, bool nee, int64_t seed,
|
| 18 |
+
torch::Tensor image) {
|
| 19 |
+
TORCH_CHECK(tris.is_cuda() && tris.is_contiguous(), "tris");
|
| 20 |
+
TORCH_CHECK(image.is_cuda() && image.is_contiguous() && image.dim() == 3,
|
| 21 |
+
"image [H, W, 3]");
|
| 22 |
+
TORCH_CHECK(albedo.size(0) <= 64, "at most 64 materials");
|
| 23 |
+
TORCH_CHECK(max_bounces >= 1 && max_bounces <= 16, "bounces in [1,16]");
|
| 24 |
+
const at::cuda::CUDAGuard guard(tris.device());
|
| 25 |
+
cudaStream_t stream = at::cuda::getCurrentCUDAStream();
|
| 26 |
+
int L = (int)light_faces.numel();
|
| 27 |
+
torch::Tensor cam_h =
|
| 28 |
+
cam.to(torch::kFloat32).to(torch::kCPU).contiguous();
|
| 29 |
+
ptd_forward_launch(
|
| 30 |
+
tris.const_data_ptr<float>(), mat_ids.const_data_ptr<int>(),
|
| 31 |
+
(int)tris.size(0), nodes_f.const_data_ptr<float>(),
|
| 32 |
+
nodes_i.const_data_ptr<int>(), (int)nodes_f.size(0),
|
| 33 |
+
L ? light_faces.const_data_ptr<int>() : nullptr,
|
| 34 |
+
L ? light_cdf.const_data_ptr<float>() : nullptr, L,
|
| 35 |
+
(float)total_light_area, albedo.const_data_ptr<float>(),
|
| 36 |
+
emission.const_data_ptr<float>(), (int)albedo.size(0),
|
| 37 |
+
cam_h.const_data_ptr<float>(), (int)image.size(0), (int)image.size(1),
|
| 38 |
+
(int)spp, (int)max_bounces, nee ? 1 : 0, (long long)seed,
|
| 39 |
+
image.data_ptr<float>(), stream);
|
| 40 |
+
C10_CUDA_KERNEL_LAUNCH_CHECK();
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
void pt_backward(torch::Tensor tris, torch::Tensor mat_ids,
|
| 44 |
+
torch::Tensor nodes_f, torch::Tensor nodes_i,
|
| 45 |
+
torch::Tensor light_faces, torch::Tensor light_cdf,
|
| 46 |
+
double total_light_area, torch::Tensor albedo,
|
| 47 |
+
torch::Tensor emission, torch::Tensor cam, int64_t spp,
|
| 48 |
+
int64_t max_bounces, bool nee, int64_t seed,
|
| 49 |
+
torch::Tensor grad_image, torch::Tensor grad_albedo,
|
| 50 |
+
torch::Tensor grad_emission) {
|
| 51 |
+
TORCH_CHECK(grad_image.is_cuda() && grad_image.is_contiguous(), "grad_image");
|
| 52 |
+
TORCH_CHECK(albedo.size(0) <= 64, "at most 64 materials");
|
| 53 |
+
const at::cuda::CUDAGuard guard(tris.device());
|
| 54 |
+
cudaStream_t stream = at::cuda::getCurrentCUDAStream();
|
| 55 |
+
int L = (int)light_faces.numel();
|
| 56 |
+
torch::Tensor cam_h =
|
| 57 |
+
cam.to(torch::kFloat32).to(torch::kCPU).contiguous();
|
| 58 |
+
ptd_backward_launch(
|
| 59 |
+
tris.const_data_ptr<float>(), mat_ids.const_data_ptr<int>(),
|
| 60 |
+
(int)tris.size(0), nodes_f.const_data_ptr<float>(),
|
| 61 |
+
nodes_i.const_data_ptr<int>(), (int)nodes_f.size(0),
|
| 62 |
+
L ? light_faces.const_data_ptr<int>() : nullptr,
|
| 63 |
+
L ? light_cdf.const_data_ptr<float>() : nullptr, L,
|
| 64 |
+
(float)total_light_area, albedo.const_data_ptr<float>(),
|
| 65 |
+
emission.const_data_ptr<float>(), (int)albedo.size(0),
|
| 66 |
+
cam_h.const_data_ptr<float>(), (int)grad_image.size(0),
|
| 67 |
+
(int)grad_image.size(1), (int)spp, (int)max_bounces, nee ? 1 : 0,
|
| 68 |
+
(long long)seed, grad_image.const_data_ptr<float>(),
|
| 69 |
+
grad_albedo.data_ptr<float>(), grad_emission.data_ptr<float>(), stream);
|
| 70 |
+
C10_CUDA_KERNEL_LAUNCH_CHECK();
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
} // namespace
|
| 74 |
+
|
| 75 |
+
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
|
| 76 |
+
m.def("pt_forward", &pt_forward, "pathtracer-diff forward");
|
| 77 |
+
m.def("pt_backward", &pt_backward, "pathtracer-diff backward");
|
| 78 |
+
}
|
pathtracer_diff_cuda/pathtracer.cu
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// pathtracer-diff: a differentiable Monte Carlo path tracer as one kernel.
|
| 2 |
+
//
|
| 3 |
+
// Forward: a megakernel unidirectional path tracer (diffuse + emissive
|
| 4 |
+
// materials, cosine-weighted BRDF sampling, optional next-event estimation
|
| 5 |
+
// against an area-sampled light list, BVH traversal). One thread owns one
|
| 6 |
+
// pixel and accumulates its spp samples serially, so the image is bitwise
|
| 7 |
+
// deterministic.
|
| 8 |
+
//
|
| 9 |
+
// Backward: exact path replay. Sampling directions depend only on geometry
|
| 10 |
+
// and the counter-based Philox stream, never on the differentiable
|
| 11 |
+
// parameters (per-material albedo and emission), so the backward kernel
|
| 12 |
+
// re-traces the identical paths with the identical RNG draws and
|
| 13 |
+
// accumulates dLoss/dAlbedo and dLoss/dEmission analytically, storing no
|
| 14 |
+
// path state between the passes (path-replay backpropagation, restricted
|
| 15 |
+
// to interior terms: no visibility/silhouette gradients).
|
| 16 |
+
//
|
| 17 |
+
// Radiance estimator with NEE on: emission is gathered at the camera hit
|
| 18 |
+
// only; every later vertex estimates direct light by area sampling, so no
|
| 19 |
+
// contribution is double-counted. With NEE off: emission is gathered at
|
| 20 |
+
// every hit (with cosine sampling the diffuse throughput factor is exactly
|
| 21 |
+
// the albedo, which makes a closed uniform furnace a zero-variance analytic
|
| 22 |
+
// test case).
|
| 23 |
+
|
| 24 |
+
#include <cuda_runtime.h>
|
| 25 |
+
#include <curand_kernel.h>
|
| 26 |
+
|
| 27 |
+
#include <cstdint>
|
| 28 |
+
|
| 29 |
+
#include "pathtracer_launch.h"
|
| 30 |
+
|
| 31 |
+
namespace {
|
| 32 |
+
|
| 33 |
+
constexpr int kThreads = 128;
|
| 34 |
+
constexpr int kMaxBounces = 16;
|
| 35 |
+
constexpr int kMaxMats = 64;
|
| 36 |
+
constexpr int kStack = 64;
|
| 37 |
+
constexpr float kInvPi = 0.31830988618379067154f;
|
| 38 |
+
constexpr float kRayEps = 1e-4f;
|
| 39 |
+
constexpr float kShadowEps = 1e-3f;
|
| 40 |
+
|
| 41 |
+
// ---------------------------------------------------------------- float3 ops
|
| 42 |
+
__device__ __forceinline__ float3 f3(float x, float y, float z) {
|
| 43 |
+
return make_float3(x, y, z);
|
| 44 |
+
}
|
| 45 |
+
__device__ __forceinline__ float3 operator+(float3 a, float3 b) {
|
| 46 |
+
return f3(a.x + b.x, a.y + b.y, a.z + b.z);
|
| 47 |
+
}
|
| 48 |
+
__device__ __forceinline__ float3 operator-(float3 a, float3 b) {
|
| 49 |
+
return f3(a.x - b.x, a.y - b.y, a.z - b.z);
|
| 50 |
+
}
|
| 51 |
+
__device__ __forceinline__ float3 operator*(float3 a, float s) {
|
| 52 |
+
return f3(a.x * s, a.y * s, a.z * s);
|
| 53 |
+
}
|
| 54 |
+
__device__ __forceinline__ float3 operator*(float3 a, float3 b) {
|
| 55 |
+
return f3(a.x * b.x, a.y * b.y, a.z * b.z);
|
| 56 |
+
}
|
| 57 |
+
__device__ __forceinline__ float dot(float3 a, float3 b) {
|
| 58 |
+
return a.x * b.x + a.y * b.y + a.z * b.z;
|
| 59 |
+
}
|
| 60 |
+
__device__ __forceinline__ float3 cross(float3 a, float3 b) {
|
| 61 |
+
return f3(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x);
|
| 62 |
+
}
|
| 63 |
+
__device__ __forceinline__ float3 normalize(float3 a) {
|
| 64 |
+
return a * rsqrtf(fmaxf(dot(a, a), 1e-30f));
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
// Duff et al. 2017, "Building an Orthonormal Basis, Revisited".
|
| 68 |
+
__device__ __forceinline__ void onb(float3 n, float3& t, float3& b) {
|
| 69 |
+
float s = copysignf(1.0f, n.z);
|
| 70 |
+
float a = -1.0f / (s + n.z);
|
| 71 |
+
float c = n.x * n.y * a;
|
| 72 |
+
t = f3(1.0f + s * n.x * n.x * a, s * c, -s * n.x);
|
| 73 |
+
b = f3(c, s + n.y * n.y * a, -n.y);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// ---------------------------------------------------------------- scene view
|
| 77 |
+
struct DevScene {
|
| 78 |
+
const float* tris; // [F, 9] v0 v1 v2
|
| 79 |
+
const int* mats; // [F]
|
| 80 |
+
const float* nf; // [N, 6] bbox lo.xyz hi.xyz
|
| 81 |
+
const int* ni; // [N, 3] (left,right,0) | (start,count,1)
|
| 82 |
+
int n_nodes;
|
| 83 |
+
const int* lf; // [L] emissive face indices
|
| 84 |
+
const float* lcdf; // [L] normalized area cdf
|
| 85 |
+
int nl;
|
| 86 |
+
float larea; // total emissive area
|
| 87 |
+
const float* alb; // [M, 3]
|
| 88 |
+
const float* emi; // [M, 3]
|
| 89 |
+
int nm;
|
| 90 |
+
};
|
| 91 |
+
|
| 92 |
+
struct DevCam { // pos, fwd, right*tan(v/2)*aspect, up*tan(v/2)
|
| 93 |
+
float p[3], f[3], r[3], u[3];
|
| 94 |
+
};
|
| 95 |
+
|
| 96 |
+
// ------------------------------------------------------------- intersection
|
| 97 |
+
__device__ __forceinline__ bool tri_hit(const float* v, float3 ro, float3 rd,
|
| 98 |
+
float tmin, float tmax, float& t,
|
| 99 |
+
float3& ng) {
|
| 100 |
+
float3 v0 = f3(v[0], v[1], v[2]);
|
| 101 |
+
float3 e1 = f3(v[3], v[4], v[5]) - v0;
|
| 102 |
+
float3 e2 = f3(v[6], v[7], v[8]) - v0;
|
| 103 |
+
float3 p = cross(rd, e2);
|
| 104 |
+
float det = dot(e1, p);
|
| 105 |
+
if (fabsf(det) < 1e-12f) return false;
|
| 106 |
+
float inv = 1.0f / det;
|
| 107 |
+
float3 s = ro - v0;
|
| 108 |
+
float u = dot(s, p) * inv;
|
| 109 |
+
if (u < -1e-6f || u > 1.0f + 1e-6f) return false;
|
| 110 |
+
float3 q = cross(s, e1);
|
| 111 |
+
float w = dot(rd, q) * inv;
|
| 112 |
+
if (w < -1e-6f || u + w > 1.0f + 1e-6f) return false;
|
| 113 |
+
float tt = dot(e2, q) * inv;
|
| 114 |
+
if (tt < tmin || tt > tmax) return false;
|
| 115 |
+
t = tt;
|
| 116 |
+
ng = cross(e1, e2);
|
| 117 |
+
return true;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
__device__ __forceinline__ bool slab(const float* b, const float ro[3],
|
| 121 |
+
const float inv[3], float tmax) {
|
| 122 |
+
float t0 = kRayEps, t1 = tmax;
|
| 123 |
+
#pragma unroll
|
| 124 |
+
for (int a = 0; a < 3; ++a) {
|
| 125 |
+
float lo = (b[a] - ro[a]) * inv[a];
|
| 126 |
+
float hi = (b[3 + a] - ro[a]) * inv[a];
|
| 127 |
+
if (lo > hi) {
|
| 128 |
+
float tmp = lo;
|
| 129 |
+
lo = hi;
|
| 130 |
+
hi = tmp;
|
| 131 |
+
}
|
| 132 |
+
t0 = fmaxf(t0, lo);
|
| 133 |
+
t1 = fminf(t1, hi);
|
| 134 |
+
}
|
| 135 |
+
return t0 <= t1;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
__device__ __forceinline__ void inv_dir(float3 rd, float inv[3]) {
|
| 139 |
+
float d;
|
| 140 |
+
d = rd.x; if (fabsf(d) < 1e-12f) d = copysignf(1e-12f, d); inv[0] = 1.0f / d;
|
| 141 |
+
d = rd.y; if (fabsf(d) < 1e-12f) d = copysignf(1e-12f, d); inv[1] = 1.0f / d;
|
| 142 |
+
d = rd.z; if (fabsf(d) < 1e-12f) d = copysignf(1e-12f, d); inv[2] = 1.0f / d;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
__device__ int bvh_closest(const DevScene& sc, float3 ro, float3 rd,
|
| 146 |
+
float tmin, float& tbest, float3& ngbest) {
|
| 147 |
+
float roa[3] = {ro.x, ro.y, ro.z};
|
| 148 |
+
float inv[3];
|
| 149 |
+
inv_dir(rd, inv);
|
| 150 |
+
int stack[kStack];
|
| 151 |
+
int sp = 0;
|
| 152 |
+
stack[sp++] = 0;
|
| 153 |
+
int best = -1;
|
| 154 |
+
while (sp > 0) {
|
| 155 |
+
int nid = stack[--sp];
|
| 156 |
+
if (!slab(&sc.nf[nid * 6], roa, inv, tbest)) continue;
|
| 157 |
+
const int* n = &sc.ni[nid * 3];
|
| 158 |
+
if (n[2]) { // leaf: faces n[0] .. n[0]+n[1]
|
| 159 |
+
for (int f = n[0]; f < n[0] + n[1]; ++f) {
|
| 160 |
+
float t;
|
| 161 |
+
float3 ng;
|
| 162 |
+
if (tri_hit(&sc.tris[f * 9], ro, rd, tmin, tbest, t, ng)) {
|
| 163 |
+
tbest = t;
|
| 164 |
+
ngbest = ng;
|
| 165 |
+
best = f;
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
} else if (sp + 2 <= kStack) {
|
| 169 |
+
stack[sp++] = n[0];
|
| 170 |
+
stack[sp++] = n[1];
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
return best;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
__device__ bool bvh_occluded(const DevScene& sc, float3 ro, float3 rd,
|
| 177 |
+
float tmax) {
|
| 178 |
+
float roa[3] = {ro.x, ro.y, ro.z};
|
| 179 |
+
float inv[3];
|
| 180 |
+
inv_dir(rd, inv);
|
| 181 |
+
int stack[kStack];
|
| 182 |
+
int sp = 0;
|
| 183 |
+
stack[sp++] = 0;
|
| 184 |
+
while (sp > 0) {
|
| 185 |
+
int nid = stack[--sp];
|
| 186 |
+
if (!slab(&sc.nf[nid * 6], roa, inv, tmax)) continue;
|
| 187 |
+
const int* n = &sc.ni[nid * 3];
|
| 188 |
+
if (n[2]) {
|
| 189 |
+
for (int f = n[0]; f < n[0] + n[1]; ++f) {
|
| 190 |
+
float t;
|
| 191 |
+
float3 ng;
|
| 192 |
+
if (tri_hit(&sc.tris[f * 9], ro, rd, kRayEps, tmax, t, ng)) return true;
|
| 193 |
+
}
|
| 194 |
+
} else if (sp + 2 <= kStack) {
|
| 195 |
+
stack[sp++] = n[0];
|
| 196 |
+
stack[sp++] = n[1];
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
return false;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
__device__ __forceinline__ int pick_light(const float* cdf, int nl, float r) {
|
| 203 |
+
int lo = 0, hi = nl - 1;
|
| 204 |
+
while (lo < hi) {
|
| 205 |
+
int mid = (lo + hi) >> 1;
|
| 206 |
+
if (cdf[mid] < r) lo = mid + 1; else hi = mid;
|
| 207 |
+
}
|
| 208 |
+
return lo;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
// ------------------------------------------------------------- path tracing
|
| 212 |
+
// One camera path: forward accumulates radiance into acc; backward (GRAD)
|
| 213 |
+
// accumulates parameter gradients into the shared buffers instead. The RNG
|
| 214 |
+
// draw order is identical in both instantiations by construction.
|
| 215 |
+
template <bool GRAD>
|
| 216 |
+
__device__ void trace_path(const DevScene& sc, float3 ro, float3 rd,
|
| 217 |
+
curandStatePhilox4_32_10_t& st, int B, int nee,
|
| 218 |
+
float3& acc, const float gs[3], float* s_ga,
|
| 219 |
+
float* s_ge) {
|
| 220 |
+
float T[3] = {1.0f, 1.0f, 1.0f}; // prefix albedo product
|
| 221 |
+
float albs[kMaxBounces][3]; // per-bounce albedo (GRAD replay)
|
| 222 |
+
int mats[kMaxBounces];
|
| 223 |
+
float suf[kMaxBounces + 1][3]; // suffix products workspace
|
| 224 |
+
|
| 225 |
+
for (int k = 0; k < B; ++k) {
|
| 226 |
+
float tbest = 3.0e38f;
|
| 227 |
+
float3 ng;
|
| 228 |
+
int face = bvh_closest(sc, ro, rd, kRayEps, tbest, ng);
|
| 229 |
+
if (face < 0) break;
|
| 230 |
+
|
| 231 |
+
int m = sc.mats[face];
|
| 232 |
+
float3 x = ro + rd * tbest;
|
| 233 |
+
float3 n = normalize(ng);
|
| 234 |
+
if (dot(n, rd) > 0.0f) n = f3(-n.x, -n.y, -n.z);
|
| 235 |
+
|
| 236 |
+
const float* am = &sc.alb[m * 3];
|
| 237 |
+
const float* em = &sc.emi[m * 3];
|
| 238 |
+
|
| 239 |
+
// Emission gathered at the camera hit (NEE on) or at every hit (NEE off).
|
| 240 |
+
if (k == 0 || !nee) {
|
| 241 |
+
if (!GRAD) {
|
| 242 |
+
acc.x += T[0] * em[0];
|
| 243 |
+
acc.y += T[1] * em[1];
|
| 244 |
+
acc.z += T[2] * em[2];
|
| 245 |
+
} else {
|
| 246 |
+
// dC/dE[m] = T; dC/da[m_j] (j<k) = E * excl_j.
|
| 247 |
+
for (int c = 0; c < 3; ++c)
|
| 248 |
+
atomicAdd(&s_ge[m * 3 + c], gs[c] * T[c]);
|
| 249 |
+
if (k > 0) {
|
| 250 |
+
suf[k][0] = suf[k][1] = suf[k][2] = 1.0f;
|
| 251 |
+
for (int i = k - 1; i >= 0; --i)
|
| 252 |
+
for (int c = 0; c < 3; ++c) suf[i][c] = suf[i + 1][c] * albs[i][c];
|
| 253 |
+
float pref[3] = {1.0f, 1.0f, 1.0f};
|
| 254 |
+
for (int j = 0; j < k; ++j) {
|
| 255 |
+
for (int c = 0; c < 3; ++c) {
|
| 256 |
+
float excl = pref[c] * suf[j + 1][c];
|
| 257 |
+
atomicAdd(&s_ga[mats[j] * 3 + c], gs[c] * em[c] * excl);
|
| 258 |
+
pref[c] *= albs[j][c];
|
| 259 |
+
}
|
| 260 |
+
}
|
| 261 |
+
}
|
| 262 |
+
}
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
if (GRAD) {
|
| 266 |
+
albs[k][0] = am[0];
|
| 267 |
+
albs[k][1] = am[1];
|
| 268 |
+
albs[k][2] = am[2];
|
| 269 |
+
mats[k] = m;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
// Next-event estimation against the area-sampled light list. NEE at
|
| 273 |
+
// vertex k reaches the light at hit k+1, so it is skipped at the final
|
| 274 |
+
// vertex: both estimators then cover camera-to-light paths of at most
|
| 275 |
+
// max_bounces surface hits and agree in expectation, not just in the
|
| 276 |
+
// limit.
|
| 277 |
+
if (nee && sc.nl > 0 && k + 1 < B) {
|
| 278 |
+
float r1 = curand_uniform(&st);
|
| 279 |
+
float r2 = curand_uniform(&st);
|
| 280 |
+
float r3 = curand_uniform(&st);
|
| 281 |
+
int li = pick_light(sc.lcdf, sc.nl, r1);
|
| 282 |
+
int lface = sc.lf[li];
|
| 283 |
+
const float* lv = &sc.tris[lface * 9];
|
| 284 |
+
float3 lv0 = f3(lv[0], lv[1], lv[2]);
|
| 285 |
+
float3 le1 = f3(lv[3], lv[4], lv[5]) - lv0;
|
| 286 |
+
float3 le2 = f3(lv[6], lv[7], lv[8]) - lv0;
|
| 287 |
+
float su = sqrtf(r2);
|
| 288 |
+
float b0 = 1.0f - su;
|
| 289 |
+
float b1 = r3 * su;
|
| 290 |
+
float3 y = lv0 + le1 * b0 + le2 * b1; // uniform: v0 + e1*(1-sqrt(r2)) + e2*(r3*sqrt(r2))
|
| 291 |
+
float3 ln = normalize(cross(le1, le2));
|
| 292 |
+
float3 dvec = y - x;
|
| 293 |
+
float d2 = fmaxf(dot(dvec, dvec), 1e-12f);
|
| 294 |
+
float d = sqrtf(d2);
|
| 295 |
+
float3 wi = dvec * (1.0f / d);
|
| 296 |
+
if (dot(ln, dvec) > 0.0f) ln = f3(-ln.x, -ln.y, -ln.z); // face the shading point
|
| 297 |
+
float cx = dot(n, wi);
|
| 298 |
+
float cy = -dot(ln, wi);
|
| 299 |
+
int lm = sc.mats[lface];
|
| 300 |
+
if (cx > 1e-6f && cy > 1e-6f && d > kShadowEps * 2.0f) {
|
| 301 |
+
float3 xo = x + n * kRayEps;
|
| 302 |
+
if (!bvh_occluded(sc, xo, wi, d - kShadowEps)) {
|
| 303 |
+
float S = cx * cy / d2 * sc.larea * kInvPi;
|
| 304 |
+
const float* el = &sc.emi[lm * 3];
|
| 305 |
+
if (!GRAD) {
|
| 306 |
+
acc.x += T[0] * am[0] * el[0] * S;
|
| 307 |
+
acc.y += T[1] * am[1] * el[1] * S;
|
| 308 |
+
acc.z += T[2] * am[2] * el[2] * S;
|
| 309 |
+
} else {
|
| 310 |
+
// Term = prod_{j<=k} a_j * E_l * S; the vertex albedo a_k is
|
| 311 |
+
// one of the factors (albs[k] is already stored above).
|
| 312 |
+
suf[k + 1][0] = suf[k + 1][1] = suf[k + 1][2] = 1.0f;
|
| 313 |
+
for (int i = k; i >= 0; --i)
|
| 314 |
+
for (int c = 0; c < 3; ++c)
|
| 315 |
+
suf[i][c] = suf[i + 1][c] * albs[i][c];
|
| 316 |
+
for (int c = 0; c < 3; ++c)
|
| 317 |
+
atomicAdd(&s_ge[lm * 3 + c], gs[c] * suf[0][c] * S);
|
| 318 |
+
float pref[3] = {1.0f, 1.0f, 1.0f};
|
| 319 |
+
for (int j = 0; j <= k; ++j) {
|
| 320 |
+
for (int c = 0; c < 3; ++c) {
|
| 321 |
+
float excl = pref[c] * suf[j + 1][c];
|
| 322 |
+
atomicAdd(&s_ga[mats[j] * 3 + c], gs[c] * el[c] * S * excl);
|
| 323 |
+
pref[c] *= albs[j][c];
|
| 324 |
+
}
|
| 325 |
+
}
|
| 326 |
+
}
|
| 327 |
+
}
|
| 328 |
+
}
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
// Cosine-weighted continuation: BRDF*cos/pdf = albedo exactly.
|
| 332 |
+
float rd1 = curand_uniform(&st);
|
| 333 |
+
float rd2 = curand_uniform(&st);
|
| 334 |
+
float rr = sqrtf(rd1);
|
| 335 |
+
float phi = 6.2831853071795864769f * rd2;
|
| 336 |
+
float3 tang, bit;
|
| 337 |
+
onb(n, tang, bit);
|
| 338 |
+
float3 nd = tang * (rr * cosf(phi)) + bit * (rr * sinf(phi)) +
|
| 339 |
+
n * sqrtf(fmaxf(0.0f, 1.0f - rd1));
|
| 340 |
+
rd = normalize(nd);
|
| 341 |
+
ro = x + n * kRayEps;
|
| 342 |
+
T[0] *= am[0];
|
| 343 |
+
T[1] *= am[1];
|
| 344 |
+
T[2] *= am[2];
|
| 345 |
+
}
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
__device__ __forceinline__ float3 camera_ray(const DevCam& cam, int px, int py,
|
| 349 |
+
int W, int H, float jx, float jy) {
|
| 350 |
+
float nx = 2.0f * ((px + jx) / (float)W) - 1.0f;
|
| 351 |
+
float ny = 1.0f - 2.0f * ((py + jy) / (float)H);
|
| 352 |
+
float3 d = f3(cam.f[0] + nx * cam.r[0] + ny * cam.u[0],
|
| 353 |
+
cam.f[1] + nx * cam.r[1] + ny * cam.u[1],
|
| 354 |
+
cam.f[2] + nx * cam.r[2] + ny * cam.u[2]);
|
| 355 |
+
return normalize(d);
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
__global__ void k_forward(DevScene sc, DevCam cam, int H, int W, int spp,
|
| 359 |
+
int B, int nee, unsigned long long seed,
|
| 360 |
+
float* img) {
|
| 361 |
+
int pid = blockIdx.x * blockDim.x + threadIdx.x;
|
| 362 |
+
if (pid >= H * W) return;
|
| 363 |
+
int px = pid % W, py = pid / W;
|
| 364 |
+
float3 ro = f3(cam.p[0], cam.p[1], cam.p[2]);
|
| 365 |
+
float3 acc = f3(0.0f, 0.0f, 0.0f);
|
| 366 |
+
for (int s = 0; s < spp; ++s) {
|
| 367 |
+
curandStatePhilox4_32_10_t st;
|
| 368 |
+
curand_init(seed, (unsigned long long)pid * spp + s, 0, &st);
|
| 369 |
+
float jx = curand_uniform(&st);
|
| 370 |
+
float jy = curand_uniform(&st);
|
| 371 |
+
float3 rd = camera_ray(cam, px, py, W, H, jx, jy);
|
| 372 |
+
trace_path<false>(sc, ro, rd, st, B, nee, acc, nullptr, nullptr, nullptr);
|
| 373 |
+
}
|
| 374 |
+
float inv_spp = 1.0f / (float)spp;
|
| 375 |
+
img[pid * 3 + 0] = acc.x * inv_spp;
|
| 376 |
+
img[pid * 3 + 1] = acc.y * inv_spp;
|
| 377 |
+
img[pid * 3 + 2] = acc.z * inv_spp;
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
__global__ void k_backward(DevScene sc, DevCam cam, int H, int W, int spp,
|
| 381 |
+
int B, int nee, unsigned long long seed,
|
| 382 |
+
const float* gimg, float* ga, float* ge) {
|
| 383 |
+
__shared__ float s_ga[kMaxMats * 3];
|
| 384 |
+
__shared__ float s_ge[kMaxMats * 3];
|
| 385 |
+
int nm3 = sc.nm * 3;
|
| 386 |
+
for (int i = threadIdx.x; i < nm3; i += blockDim.x) {
|
| 387 |
+
s_ga[i] = 0.0f;
|
| 388 |
+
s_ge[i] = 0.0f;
|
| 389 |
+
}
|
| 390 |
+
__syncthreads();
|
| 391 |
+
|
| 392 |
+
int pid = blockIdx.x * blockDim.x + threadIdx.x;
|
| 393 |
+
if (pid < H * W) {
|
| 394 |
+
int px = pid % W, py = pid / W;
|
| 395 |
+
float inv_spp = 1.0f / (float)spp;
|
| 396 |
+
float gs[3] = {gimg[pid * 3 + 0] * inv_spp, gimg[pid * 3 + 1] * inv_spp,
|
| 397 |
+
gimg[pid * 3 + 2] * inv_spp};
|
| 398 |
+
float3 ro = f3(cam.p[0], cam.p[1], cam.p[2]);
|
| 399 |
+
float3 acc = f3(0.0f, 0.0f, 0.0f);
|
| 400 |
+
for (int s = 0; s < spp; ++s) {
|
| 401 |
+
curandStatePhilox4_32_10_t st;
|
| 402 |
+
curand_init(seed, (unsigned long long)pid * spp + s, 0, &st);
|
| 403 |
+
float jx = curand_uniform(&st);
|
| 404 |
+
float jy = curand_uniform(&st);
|
| 405 |
+
float3 rd = camera_ray(cam, px, py, W, H, jx, jy);
|
| 406 |
+
trace_path<true>(sc, ro, rd, st, B, nee, acc, gs, s_ga, s_ge);
|
| 407 |
+
}
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
__syncthreads();
|
| 411 |
+
for (int i = threadIdx.x; i < nm3; i += blockDim.x) {
|
| 412 |
+
atomicAdd(&ga[i], s_ga[i]);
|
| 413 |
+
atomicAdd(&ge[i], s_ge[i]);
|
| 414 |
+
}
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
DevScene make_scene(const float* tris, const int* mat_ids,
|
| 418 |
+
const float* nodes_f, const int* nodes_i, int n_nodes,
|
| 419 |
+
const int* light_faces, const float* light_cdf,
|
| 420 |
+
int n_lights, float total_light_area, const float* albedo,
|
| 421 |
+
const float* emission, int n_mats) {
|
| 422 |
+
DevScene sc;
|
| 423 |
+
sc.tris = tris;
|
| 424 |
+
sc.mats = mat_ids;
|
| 425 |
+
sc.nf = nodes_f;
|
| 426 |
+
sc.ni = nodes_i;
|
| 427 |
+
sc.n_nodes = n_nodes;
|
| 428 |
+
sc.lf = light_faces;
|
| 429 |
+
sc.lcdf = light_cdf;
|
| 430 |
+
sc.nl = n_lights;
|
| 431 |
+
sc.larea = total_light_area;
|
| 432 |
+
sc.alb = albedo;
|
| 433 |
+
sc.emi = emission;
|
| 434 |
+
sc.nm = n_mats;
|
| 435 |
+
return sc;
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
DevCam make_cam(const float* cam) {
|
| 439 |
+
DevCam c;
|
| 440 |
+
for (int i = 0; i < 3; ++i) {
|
| 441 |
+
c.p[i] = cam[i];
|
| 442 |
+
c.f[i] = cam[3 + i];
|
| 443 |
+
c.r[i] = cam[6 + i];
|
| 444 |
+
c.u[i] = cam[9 + i];
|
| 445 |
+
}
|
| 446 |
+
return c;
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
} // namespace
|
| 450 |
+
|
| 451 |
+
extern "C" void ptd_forward_launch(
|
| 452 |
+
const float* tris, const int* mat_ids, int /*n_faces*/,
|
| 453 |
+
const float* nodes_f, const int* nodes_i, int n_nodes,
|
| 454 |
+
const int* light_faces, const float* light_cdf, int n_lights,
|
| 455 |
+
float total_light_area, const float* albedo, const float* emission,
|
| 456 |
+
int n_mats, const float* cam, int H, int W, int spp, int max_bounces,
|
| 457 |
+
int nee, long long seed, float* image, cudaStream_t stream) {
|
| 458 |
+
DevScene sc = make_scene(tris, mat_ids, nodes_f, nodes_i, n_nodes,
|
| 459 |
+
light_faces, light_cdf, n_lights, total_light_area,
|
| 460 |
+
albedo, emission, n_mats);
|
| 461 |
+
DevCam dc = make_cam(cam);
|
| 462 |
+
int blocks = (H * W + kThreads - 1) / kThreads;
|
| 463 |
+
k_forward<<<blocks, kThreads, 0, stream>>>(
|
| 464 |
+
sc, dc, H, W, spp, max_bounces, nee, (unsigned long long)seed, image);
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
extern "C" void ptd_backward_launch(
|
| 468 |
+
const float* tris, const int* mat_ids, int /*n_faces*/,
|
| 469 |
+
const float* nodes_f, const int* nodes_i, int n_nodes,
|
| 470 |
+
const int* light_faces, const float* light_cdf, int n_lights,
|
| 471 |
+
float total_light_area, const float* albedo, const float* emission,
|
| 472 |
+
int n_mats, const float* cam, int H, int W, int spp, int max_bounces,
|
| 473 |
+
int nee, long long seed, const float* grad_image, float* grad_albedo,
|
| 474 |
+
float* grad_emission, cudaStream_t stream) {
|
| 475 |
+
DevScene sc = make_scene(tris, mat_ids, nodes_f, nodes_i, n_nodes,
|
| 476 |
+
light_faces, light_cdf, n_lights, total_light_area,
|
| 477 |
+
albedo, emission, n_mats);
|
| 478 |
+
DevCam dc = make_cam(cam);
|
| 479 |
+
int blocks = (H * W + kThreads - 1) / kThreads;
|
| 480 |
+
k_backward<<<blocks, kThreads, 0, stream>>>(
|
| 481 |
+
sc, dc, H, W, spp, max_bounces, nee, (unsigned long long)seed,
|
| 482 |
+
grad_image, grad_albedo, grad_emission);
|
| 483 |
+
}
|
pathtracer_diff_cuda/pathtracer_launch.h
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Raw-pointer launch interface for the pathtracer-diff kernels. Torch-free so
|
| 2 |
+
// the .cu never sees libtorch headers (required for local MSVC builds).
|
| 3 |
+
#pragma once
|
| 4 |
+
|
| 5 |
+
#include <cuda_runtime_api.h>
|
| 6 |
+
|
| 7 |
+
extern "C" {
|
| 8 |
+
|
| 9 |
+
void ptd_forward_launch(
|
| 10 |
+
const float* tris, const int* mat_ids, int n_faces,
|
| 11 |
+
const float* nodes_f, const int* nodes_i, int n_nodes,
|
| 12 |
+
const int* light_faces, const float* light_cdf, int n_lights,
|
| 13 |
+
float total_light_area,
|
| 14 |
+
const float* albedo, const float* emission, int n_mats,
|
| 15 |
+
const float* cam,
|
| 16 |
+
int H, int W, int spp, int max_bounces, int nee,
|
| 17 |
+
long long seed, float* image, cudaStream_t stream);
|
| 18 |
+
|
| 19 |
+
void ptd_backward_launch(
|
| 20 |
+
const float* tris, const int* mat_ids, int n_faces,
|
| 21 |
+
const float* nodes_f, const int* nodes_i, int n_nodes,
|
| 22 |
+
const int* light_faces, const float* light_cdf, int n_lights,
|
| 23 |
+
float total_light_area,
|
| 24 |
+
const float* albedo, const float* emission, int n_mats,
|
| 25 |
+
const float* cam,
|
| 26 |
+
int H, int W, int spp, int max_bounces, int nee,
|
| 27 |
+
long long seed, const float* grad_image,
|
| 28 |
+
float* grad_albedo, float* grad_emission, cudaStream_t stream);
|
| 29 |
+
|
| 30 |
+
} // extern "C"
|
tests/test_pathtracer_diff.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
import kernels
|
| 5 |
+
|
| 6 |
+
ptd = kernels.get_kernel("phanerozoic/pathtracer-diff", version=1,
|
| 7 |
+
trust_remote_code=True)
|
| 8 |
+
|
| 9 |
+
requires_cuda = pytest.mark.skipif(not torch.cuda.is_available(),
|
| 10 |
+
reason="CUDA required")
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def quad(a, b, c, d):
|
| 14 |
+
return [[a, b, c], [a, c, d]]
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def add_box(verts, faces, mat, lo, hi, mat_id):
|
| 18 |
+
x0, y0, z0 = lo
|
| 19 |
+
x1, y1, z1 = hi
|
| 20 |
+
base = len(verts)
|
| 21 |
+
verts.extend([(x0, y0, z0), (x1, y0, z0), (x1, y0, z1), (x0, y0, z1),
|
| 22 |
+
(x0, y1, z0), (x1, y1, z0), (x1, y1, z1), (x0, y1, z1)])
|
| 23 |
+
i = lambda k: base + k
|
| 24 |
+
for q in [quad(i(0), i(1), i(2), i(3)), # bottom
|
| 25 |
+
quad(i(4), i(7), i(6), i(5)), # top
|
| 26 |
+
quad(i(0), i(4), i(5), i(1)), # z0
|
| 27 |
+
quad(i(3), i(2), i(6), i(7)), # z1
|
| 28 |
+
quad(i(0), i(3), i(7), i(4)), # x0
|
| 29 |
+
quad(i(1), i(5), i(6), i(2))]: # x1
|
| 30 |
+
faces.extend(q)
|
| 31 |
+
mat.extend([mat_id, mat_id])
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def furnace_scene(albedo=0.5, emission=0.25):
|
| 35 |
+
verts, faces, mat = [], [], []
|
| 36 |
+
add_box(verts, faces, mat, (-2, -2, -2), (2, 2, 2), 0)
|
| 37 |
+
return ptd.Scene(verts, faces, mat,
|
| 38 |
+
albedo=[[albedo] * 3], emission=[[emission] * 3])
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def cornell(with_box=True, albedo=None, emission=None):
|
| 42 |
+
verts, faces, mat = [], [], []
|
| 43 |
+
|
| 44 |
+
def wall(a, b, c, d, m):
|
| 45 |
+
base = len(verts)
|
| 46 |
+
verts.extend([a, b, c, d])
|
| 47 |
+
faces.extend(quad(base, base + 1, base + 2, base + 3))
|
| 48 |
+
mat.extend([m, m])
|
| 49 |
+
|
| 50 |
+
X, Y, Z = 5.56, 5.488, 5.592
|
| 51 |
+
wall((0, 0, 0), (X, 0, 0), (X, 0, Z), (0, 0, Z), 0) # floor
|
| 52 |
+
wall((0, Y, 0), (0, Y, Z), (X, Y, Z), (X, Y, 0), 0) # ceiling
|
| 53 |
+
wall((0, 0, Z), (X, 0, Z), (X, Y, Z), (0, Y, Z), 0) # back
|
| 54 |
+
wall((0, 0, 0), (0, 0, Z), (0, Y, Z), (0, Y, 0), 1) # left (red)
|
| 55 |
+
wall((X, 0, 0), (X, Y, 0), (X, Y, Z), (X, 0, Z), 2) # right (green)
|
| 56 |
+
wall((2.13, Y - 0.001, 2.27), (3.43, Y - 0.001, 2.27),
|
| 57 |
+
(3.43, Y - 0.001, 3.32), (2.13, Y - 0.001, 3.32), 3) # light
|
| 58 |
+
if with_box:
|
| 59 |
+
add_box(verts, faces, mat, (1.3, 0.0, 0.65), (2.4, 1.65, 1.7), 0)
|
| 60 |
+
if albedo is None:
|
| 61 |
+
albedo = [[0.73, 0.73, 0.73], [0.65, 0.05, 0.05],
|
| 62 |
+
[0.12, 0.45, 0.15], [0.78, 0.78, 0.78]]
|
| 63 |
+
if emission is None:
|
| 64 |
+
emission = [[0, 0, 0], [0, 0, 0], [0, 0, 0], [18.4, 15.6, 8.0]]
|
| 65 |
+
return ptd.Scene(verts, faces, mat, albedo=albedo, emission=emission)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def cornell_camera():
|
| 69 |
+
return ptd.Camera(position=(2.78, 2.73, -8.0), look_at=(2.78, 2.73, 2.8),
|
| 70 |
+
vfov_deg=39.0)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@requires_cuda
|
| 74 |
+
@pytest.mark.kernels_ci
|
| 75 |
+
def test_furnace_analytic_zero_variance():
|
| 76 |
+
"""Closed uniform box, BRDF sampling only: with cosine sampling the
|
| 77 |
+
diffuse throughput multiplier is exactly the albedo, so every path
|
| 78 |
+
returns exactly E * sum_{k<B} a^k and every pixel equals the analytic
|
| 79 |
+
series regardless of the random walk."""
|
| 80 |
+
a, E, B = 0.5, 0.25, 8
|
| 81 |
+
scene = furnace_scene(a, E)
|
| 82 |
+
cam = ptd.Camera(position=(0, 0, 0), look_at=(0, 0, 1), vfov_deg=60.0)
|
| 83 |
+
img = ptd.render(scene, cam, 32, 32, spp=2, max_bounces=B, nee=False,
|
| 84 |
+
seed=0)
|
| 85 |
+
expected = E * (1 - a ** B) / (1 - a)
|
| 86 |
+
assert torch.isfinite(img).all()
|
| 87 |
+
assert (img - expected).abs().max().item() < 1e-3, \
|
| 88 |
+
(img.min().item(), img.max().item(), expected)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
@requires_cuda
|
| 92 |
+
@pytest.mark.kernels_ci
|
| 93 |
+
def test_forward_bitwise_deterministic():
|
| 94 |
+
scene = cornell()
|
| 95 |
+
cam = cornell_camera()
|
| 96 |
+
i1 = ptd.render(scene, cam, 96, 96, spp=8, max_bounces=4, seed=11)
|
| 97 |
+
i2 = ptd.render(scene, cam, 96, 96, spp=8, max_bounces=4, seed=11)
|
| 98 |
+
assert torch.equal(i1, i2)
|
| 99 |
+
i3 = ptd.render(scene, cam, 96, 96, spp=8, max_bounces=4, seed=12)
|
| 100 |
+
assert not torch.equal(i1, i3)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
@requires_cuda
|
| 104 |
+
@pytest.mark.kernels_ci
|
| 105 |
+
def test_nee_matches_brdf_estimator():
|
| 106 |
+
"""The NEE-only and BRDF-only estimators target the same integral; their
|
| 107 |
+
image means must agree within Monte Carlo tolerance."""
|
| 108 |
+
verts, faces, mat = [], [], []
|
| 109 |
+
add_box(verts, faces, mat, (0, 0, 0), (5, 5, 5), 0)
|
| 110 |
+
base = len(verts)
|
| 111 |
+
verts.extend([(1.0, 4.999, 1.0), (4.5, 4.999, 1.0), (4.5, 4.999, 4.5),
|
| 112 |
+
(1.0, 4.999, 4.5)])
|
| 113 |
+
faces.extend(quad(base, base + 1, base + 2, base + 3))
|
| 114 |
+
mat.extend([1, 1])
|
| 115 |
+
scene = ptd.Scene(verts, faces, mat,
|
| 116 |
+
albedo=[[0.6, 0.6, 0.6], [0.0, 0.0, 0.0]],
|
| 117 |
+
emission=[[0, 0, 0], [3.0, 3.0, 3.0]])
|
| 118 |
+
cam = ptd.Camera(position=(2.5, 2.5, 0.2), look_at=(2.5, 2.5, 5.0),
|
| 119 |
+
vfov_deg=70.0)
|
| 120 |
+
m_nee = ptd.render(scene, cam, 64, 64, spp=64, max_bounces=4, nee=True,
|
| 121 |
+
seed=1).mean().item()
|
| 122 |
+
m_brdf = ptd.render(scene, cam, 64, 64, spp=256, max_bounces=4, nee=False,
|
| 123 |
+
seed=2).mean().item()
|
| 124 |
+
assert abs(m_nee - m_brdf) / m_brdf < 0.03, (m_nee, m_brdf)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
@requires_cuda
|
| 128 |
+
@pytest.mark.kernels_ci
|
| 129 |
+
def test_gradients_match_finite_differences():
|
| 130 |
+
"""Sampling directions never depend on albedo/emission, so with a fixed
|
| 131 |
+
seed the loss is smooth in the parameters along the same paths and
|
| 132 |
+
central differences must match the analytic replay gradients."""
|
| 133 |
+
scene = cornell(with_box=False)
|
| 134 |
+
cam = cornell_camera()
|
| 135 |
+
H = W = 48
|
| 136 |
+
torch.manual_seed(0)
|
| 137 |
+
Wr = torch.rand(H, W, 3, device="cuda")
|
| 138 |
+
|
| 139 |
+
def loss_of(albedo, emission):
|
| 140 |
+
scene.albedo = albedo
|
| 141 |
+
scene.emission = emission
|
| 142 |
+
img = ptd.render(scene, cam, H, W, spp=8, max_bounces=3, nee=True,
|
| 143 |
+
seed=7)
|
| 144 |
+
return (img * Wr).sum()
|
| 145 |
+
|
| 146 |
+
alb0 = scene.albedo.clone()
|
| 147 |
+
emi0 = scene.emission.clone()
|
| 148 |
+
|
| 149 |
+
alb = alb0.clone().requires_grad_(True)
|
| 150 |
+
emi = emi0.clone().requires_grad_(True)
|
| 151 |
+
loss_of(alb, emi).backward()
|
| 152 |
+
|
| 153 |
+
checks = []
|
| 154 |
+
for (i, c) in [(0, 1), (1, 0), (2, 2)]: # white g, red r, green b
|
| 155 |
+
h = 2e-3
|
| 156 |
+
ap = alb0.clone(); ap[i, c] += h
|
| 157 |
+
am = alb0.clone(); am[i, c] -= h
|
| 158 |
+
fd = (loss_of(ap, emi0) - loss_of(am, emi0)).item() / (2 * h)
|
| 159 |
+
an = alb.grad[i, c].item()
|
| 160 |
+
checks.append(("albedo", i, c, an, fd))
|
| 161 |
+
h = 0.05
|
| 162 |
+
ep = emi0.clone(); ep[3, 1] += h
|
| 163 |
+
em = emi0.clone(); em[3, 1] -= h
|
| 164 |
+
fd = (loss_of(alb0, ep) - loss_of(alb0, em)).item() / (2 * h)
|
| 165 |
+
an = emi.grad[3, 1].item()
|
| 166 |
+
checks.append(("emission", 3, 1, an, fd))
|
| 167 |
+
|
| 168 |
+
for kind, i, c, an, fd in checks:
|
| 169 |
+
assert fd != 0.0, (kind, i, c)
|
| 170 |
+
assert abs(an - fd) / max(abs(fd), 1e-6) < 2e-2, \
|
| 171 |
+
(kind, i, c, an, fd)
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
@requires_cuda
|
| 175 |
+
@pytest.mark.kernels_ci
|
| 176 |
+
def test_inverse_rendering_recovers_wall_albedo():
|
| 177 |
+
"""Recover the left-wall albedo from a rendered target by gradient
|
| 178 |
+
descent through the kernel: the flagship no-framework-in-the-loop use."""
|
| 179 |
+
target_row = torch.tensor([0.2, 0.5, 0.7], device="cuda")
|
| 180 |
+
scene = cornell(with_box=False)
|
| 181 |
+
cam = cornell_camera()
|
| 182 |
+
H = W = 48
|
| 183 |
+
|
| 184 |
+
tgt_alb = scene.albedo.clone()
|
| 185 |
+
tgt_alb[1] = target_row
|
| 186 |
+
scene.albedo = tgt_alb
|
| 187 |
+
target = ptd.render(scene, cam, H, W, spp=8, max_bounces=3, seed=3).detach()
|
| 188 |
+
|
| 189 |
+
alb = scene.albedo.clone()
|
| 190 |
+
alb[1] = torch.tensor([0.5, 0.5, 0.5], device="cuda")
|
| 191 |
+
alb = alb.requires_grad_(True)
|
| 192 |
+
opt = torch.optim.Adam([alb], lr=0.05)
|
| 193 |
+
losses = []
|
| 194 |
+
for _ in range(80):
|
| 195 |
+
opt.zero_grad()
|
| 196 |
+
scene.albedo = alb
|
| 197 |
+
img = ptd.render(scene, cam, H, W, spp=8, max_bounces=3, seed=3)
|
| 198 |
+
loss = (img - target).square().mean()
|
| 199 |
+
loss.backward()
|
| 200 |
+
alb.grad[0] = 0
|
| 201 |
+
alb.grad[2] = 0
|
| 202 |
+
alb.grad[3] = 0
|
| 203 |
+
opt.step()
|
| 204 |
+
with torch.no_grad():
|
| 205 |
+
alb.clamp_(0.02, 0.98)
|
| 206 |
+
losses.append(loss.item())
|
| 207 |
+
err = (alb[1].detach() - target_row).abs().max().item()
|
| 208 |
+
assert err < 0.05, (alb[1].detach().tolist(), losses[0], losses[-1])
|
| 209 |
+
assert losses[-1] < losses[0] * 0.1, (losses[0], losses[-1])
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
@requires_cuda
|
| 213 |
+
@pytest.mark.kernels_ci
|
| 214 |
+
def test_validation():
|
| 215 |
+
verts, faces, mat = [], [], []
|
| 216 |
+
add_box(verts, faces, mat, (0, 0, 0), (1, 1, 1), 0)
|
| 217 |
+
with pytest.raises(ValueError):
|
| 218 |
+
ptd.Scene(verts, faces, mat, albedo=[[0.5] * 3] * 65,
|
| 219 |
+
emission=[[0.0] * 3] * 65)
|
| 220 |
+
scene = ptd.Scene(verts, faces, mat, albedo=[[0.5] * 3],
|
| 221 |
+
emission=[[1.0] * 3])
|
| 222 |
+
cam = ptd.Camera(position=(0.5, 0.5, 0.5), look_at=(0.5, 0.5, 1.0))
|
| 223 |
+
with pytest.raises(ValueError):
|
| 224 |
+
ptd.render(scene, cam, 8, 8, spp=1, max_bounces=17)
|
| 225 |
+
img = ptd.render(scene, cam, 8, 8, spp=2, max_bounces=2)
|
| 226 |
+
assert img.shape == (8, 8, 3) and img.dtype == torch.float32
|
| 227 |
+
assert torch.isfinite(img).all()
|
torch-ext/pathtracer_diff/__init__.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
(diffuse + emissive materials, cosine-weighted BRDF sampling, optional
|
| 5 |
+
next-event estimation, BVH); one thread owns one pixel, so the image is
|
| 6 |
+
bitwise deterministic. Backward replays the identical paths with the
|
| 7 |
+
identical counter-based Philox draws and accumulates analytic gradients to
|
| 8 |
+
per-material albedo and emission, storing no path state between the passes.
|
| 9 |
+
Sampling directions never depend on the differentiable parameters, so the
|
| 10 |
+
replay is exact. No visibility/silhouette gradients: geometry is not a
|
| 11 |
+
differentiable input.
|
| 12 |
+
|
| 13 |
+
from kernels import get_kernel
|
| 14 |
+
ptd = get_kernel("phanerozoic/pathtracer-diff", version=1, trust_remote_code=True)
|
| 15 |
+
|
| 16 |
+
scene = ptd.Scene(vertices, faces, material_ids, albedo, emission)
|
| 17 |
+
cam = ptd.Camera(position=(2.78, 2.73, -8.0), look_at=(2.78, 2.73, 2.8))
|
| 18 |
+
img = ptd.render(scene, cam, 256, 256, spp=64) # autograd to albedo/emission
|
| 19 |
+
img.sum().backward()
|
| 20 |
+
"""
|
| 21 |
+
import math
|
| 22 |
+
|
| 23 |
+
import torch
|
| 24 |
+
|
| 25 |
+
from ._ops import ops
|
| 26 |
+
|
| 27 |
+
MAX_MATERIALS = 64
|
| 28 |
+
MAX_BOUNCES = 16
|
| 29 |
+
|
| 30 |
+
__all__ = ["Scene", "Camera", "render", "ops", "MAX_MATERIALS", "MAX_BOUNCES"]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _build_bvh(tris, leaf_size=4):
|
| 34 |
+
"""Median-split BVH over [F, 9] triangles. Returns (nodes_f [N, 6],
|
| 35 |
+
nodes_i [N, 3], order [F]) with leaves as contiguous ranges of the
|
| 36 |
+
reordered face list. nodes_i rows are (left, right, 0) or
|
| 37 |
+
(start, count, 1)."""
|
| 38 |
+
import numpy as np
|
| 39 |
+
|
| 40 |
+
tris = np.asarray(tris, dtype=np.float32)
|
| 41 |
+
F = tris.shape[0]
|
| 42 |
+
v = tris.reshape(F, 3, 3)
|
| 43 |
+
lo = v.min(axis=1)
|
| 44 |
+
hi = v.max(axis=1)
|
| 45 |
+
cen = (lo + hi) * 0.5
|
| 46 |
+
|
| 47 |
+
nodes_f, nodes_i, order = [], [], []
|
| 48 |
+
|
| 49 |
+
def rec(idx):
|
| 50 |
+
nid = len(nodes_f)
|
| 51 |
+
nodes_f.append(None)
|
| 52 |
+
nodes_i.append(None)
|
| 53 |
+
blo = lo[idx].min(axis=0)
|
| 54 |
+
bhi = hi[idx].max(axis=0)
|
| 55 |
+
pad = 1e-5 * (1.0 + np.abs(blo) + np.abs(bhi))
|
| 56 |
+
nodes_f[nid] = np.concatenate([blo - pad, bhi + pad])
|
| 57 |
+
if len(idx) <= leaf_size:
|
| 58 |
+
start = len(order)
|
| 59 |
+
order.extend(idx.tolist())
|
| 60 |
+
nodes_i[nid] = (start, len(idx), 1)
|
| 61 |
+
return nid
|
| 62 |
+
ax = int(np.argmax(bhi - blo))
|
| 63 |
+
srt = idx[np.argsort(cen[idx, ax], kind="stable")]
|
| 64 |
+
mid = len(srt) // 2
|
| 65 |
+
left = rec(srt[:mid])
|
| 66 |
+
right = rec(srt[mid:])
|
| 67 |
+
nodes_i[nid] = (left, right, 0)
|
| 68 |
+
return nid
|
| 69 |
+
|
| 70 |
+
import sys
|
| 71 |
+
|
| 72 |
+
old = sys.getrecursionlimit()
|
| 73 |
+
sys.setrecursionlimit(max(old, 64 + 2 * int(math.log2(max(F, 2))) * 64))
|
| 74 |
+
try:
|
| 75 |
+
rec(np.arange(F))
|
| 76 |
+
finally:
|
| 77 |
+
sys.setrecursionlimit(old)
|
| 78 |
+
return (np.stack(nodes_f).astype(np.float32),
|
| 79 |
+
np.asarray(nodes_i, dtype=np.int32),
|
| 80 |
+
np.asarray(order, dtype=np.int64))
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class Camera:
|
| 84 |
+
"""Pinhole camera. `tensor(H, W)` packs (pos, forward, right*tan(v/2)*aspect,
|
| 85 |
+
up*tan(v/2)) as a float32 [12] tensor for the kernel."""
|
| 86 |
+
|
| 87 |
+
def __init__(self, position, look_at, up=(0.0, 1.0, 0.0), vfov_deg=40.0):
|
| 88 |
+
p = torch.tensor(position, dtype=torch.float64)
|
| 89 |
+
t = torch.tensor(look_at, dtype=torch.float64)
|
| 90 |
+
u = torch.tensor(up, dtype=torch.float64)
|
| 91 |
+
f = t - p
|
| 92 |
+
f = f / f.norm()
|
| 93 |
+
r = torch.linalg.cross(f, u)
|
| 94 |
+
r = r / r.norm()
|
| 95 |
+
uu = torch.linalg.cross(r, f)
|
| 96 |
+
self.position, self.forward, self.right, self.up = p, f, r, uu
|
| 97 |
+
self.vfov_deg = float(vfov_deg)
|
| 98 |
+
|
| 99 |
+
def tensor(self, H, W, device="cuda"):
|
| 100 |
+
th = math.tan(math.radians(self.vfov_deg) * 0.5)
|
| 101 |
+
rs = self.right * (th * W / H)
|
| 102 |
+
us = self.up * th
|
| 103 |
+
return torch.cat([self.position, self.forward, rs, us]).to(
|
| 104 |
+
device=device, dtype=torch.float32).contiguous()
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
class Scene:
|
| 108 |
+
"""Triangle-mesh scene with per-material albedo and emission.
|
| 109 |
+
|
| 110 |
+
vertices [V, 3] float, faces [F, 3] int, material_ids [F] int,
|
| 111 |
+
albedo [M, 3] float32 (may require grad), emission [M, 3] float32
|
| 112 |
+
(may require grad). Faces are reordered by the BVH build; emissive
|
| 113 |
+
faces become the area-sampled light list."""
|
| 114 |
+
|
| 115 |
+
def __init__(self, vertices, faces, material_ids, albedo, emission,
|
| 116 |
+
device="cuda"):
|
| 117 |
+
vertices = torch.as_tensor(vertices, dtype=torch.float32)
|
| 118 |
+
faces = torch.as_tensor(faces, dtype=torch.int64)
|
| 119 |
+
material_ids = torch.as_tensor(material_ids, dtype=torch.int64)
|
| 120 |
+
if not torch.is_tensor(albedo):
|
| 121 |
+
albedo = torch.tensor(albedo, dtype=torch.float32, device=device)
|
| 122 |
+
if not torch.is_tensor(emission):
|
| 123 |
+
emission = torch.tensor(emission, dtype=torch.float32, device=device)
|
| 124 |
+
M = albedo.shape[0]
|
| 125 |
+
if M > MAX_MATERIALS:
|
| 126 |
+
raise ValueError(f"at most {MAX_MATERIALS} materials, got {M}")
|
| 127 |
+
if albedo.shape != (M, 3) or emission.shape != (M, 3):
|
| 128 |
+
raise ValueError("albedo and emission must be [M, 3]")
|
| 129 |
+
if int(material_ids.max()) >= M or int(material_ids.min()) < 0:
|
| 130 |
+
raise ValueError("material_ids out of range")
|
| 131 |
+
|
| 132 |
+
tris = vertices[faces].reshape(-1, 9)
|
| 133 |
+
nodes_f, nodes_i, order = _build_bvh(tris.numpy())
|
| 134 |
+
order_t = torch.from_numpy(order)
|
| 135 |
+
tris = tris[order_t]
|
| 136 |
+
mat_ids = material_ids[order_t].to(torch.int32)
|
| 137 |
+
|
| 138 |
+
emissive = (emission.detach().cpu().amax(dim=1) > 0)[mat_ids.long()]
|
| 139 |
+
lf = torch.nonzero(emissive, as_tuple=False).flatten().to(torch.int32)
|
| 140 |
+
if lf.numel() > 0:
|
| 141 |
+
t = tris[lf.long()].reshape(-1, 3, 3).double()
|
| 142 |
+
e1 = t[:, 1] - t[:, 0]
|
| 143 |
+
e2 = t[:, 2] - t[:, 0]
|
| 144 |
+
areas = 0.5 * torch.linalg.cross(e1, e2).norm(dim=1)
|
| 145 |
+
total = float(areas.sum())
|
| 146 |
+
cdf = (areas.cumsum(0) / areas.sum()).float()
|
| 147 |
+
else:
|
| 148 |
+
total = 0.0
|
| 149 |
+
cdf = torch.zeros(0, dtype=torch.float32)
|
| 150 |
+
|
| 151 |
+
self.device = device
|
| 152 |
+
self.tris = tris.to(device).contiguous()
|
| 153 |
+
self.mat_ids = mat_ids.to(device).contiguous()
|
| 154 |
+
self.nodes_f = torch.from_numpy(nodes_f).to(device).contiguous()
|
| 155 |
+
self.nodes_i = torch.from_numpy(nodes_i).to(device).contiguous()
|
| 156 |
+
self.light_faces = lf.to(device).contiguous()
|
| 157 |
+
self.light_cdf = cdf.to(device).contiguous()
|
| 158 |
+
self.total_light_area = total
|
| 159 |
+
self.albedo = albedo.to(device)
|
| 160 |
+
self.emission = emission.to(device)
|
| 161 |
+
|
| 162 |
+
@property
|
| 163 |
+
def n_faces(self):
|
| 164 |
+
return self.tris.shape[0]
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
class _RenderFn(torch.autograd.Function):
|
| 168 |
+
@staticmethod
|
| 169 |
+
def forward(ctx, albedo, emission, scene, cam_t, H, W, spp, max_bounces,
|
| 170 |
+
nee, seed):
|
| 171 |
+
image = torch.empty(H, W, 3, device=albedo.device, dtype=torch.float32)
|
| 172 |
+
ops.pt_forward(scene.tris, scene.mat_ids, scene.nodes_f, scene.nodes_i,
|
| 173 |
+
scene.light_faces, scene.light_cdf,
|
| 174 |
+
float(scene.total_light_area),
|
| 175 |
+
albedo.detach().contiguous(),
|
| 176 |
+
emission.detach().contiguous(), cam_t, spp, max_bounces,
|
| 177 |
+
nee, seed, image)
|
| 178 |
+
ctx.scene = scene
|
| 179 |
+
ctx.cam_t = cam_t
|
| 180 |
+
ctx.params = (H, W, spp, max_bounces, nee, seed)
|
| 181 |
+
ctx.save_for_backward(albedo, emission)
|
| 182 |
+
return image
|
| 183 |
+
|
| 184 |
+
@staticmethod
|
| 185 |
+
def backward(ctx, grad_image):
|
| 186 |
+
albedo, emission = ctx.saved_tensors
|
| 187 |
+
scene, cam_t = ctx.scene, ctx.cam_t
|
| 188 |
+
H, W, spp, max_bounces, nee, seed = ctx.params
|
| 189 |
+
ga = torch.zeros_like(albedo)
|
| 190 |
+
ge = torch.zeros_like(emission)
|
| 191 |
+
ops.pt_backward(scene.tris, scene.mat_ids, scene.nodes_f,
|
| 192 |
+
scene.nodes_i, scene.light_faces, scene.light_cdf,
|
| 193 |
+
float(scene.total_light_area),
|
| 194 |
+
albedo.detach().contiguous(),
|
| 195 |
+
emission.detach().contiguous(), cam_t, spp,
|
| 196 |
+
max_bounces, nee, seed, grad_image.contiguous(), ga,
|
| 197 |
+
ge)
|
| 198 |
+
return ga, ge, None, None, None, None, None, None, None, None
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def render(scene, camera, height, width, spp=64, max_bounces=4, nee=True,
|
| 202 |
+
seed=0):
|
| 203 |
+
"""Render a linear-radiance image [H, W, 3] float32, differentiable with
|
| 204 |
+
respect to scene.albedo and scene.emission. Fixed seed makes the estimate
|
| 205 |
+
a deterministic function of the parameters (correlated samples), which is
|
| 206 |
+
what inverse-rendering loops want."""
|
| 207 |
+
if not (1 <= max_bounces <= MAX_BOUNCES):
|
| 208 |
+
raise ValueError(f"max_bounces must be in [1, {MAX_BOUNCES}]")
|
| 209 |
+
cam_t = camera.tensor(height, width, device=scene.device)
|
| 210 |
+
return _RenderFn.apply(scene.albedo, scene.emission, scene, cam_t, height,
|
| 211 |
+
width, int(spp), int(max_bounces), bool(nee),
|
| 212 |
+
int(seed))
|
torch-ext/torch_binding.cpp
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#include <torch/library.h>
|
| 2 |
+
#include <torch/all.h>
|
| 3 |
+
|
| 4 |
+
#include <ATen/cuda/CUDAContext.h>
|
| 5 |
+
#include <c10/cuda/CUDAGuard.h>
|
| 6 |
+
|
| 7 |
+
#include "registration.h"
|
| 8 |
+
#include "torch_binding.h"
|
| 9 |
+
#include "../pathtracer_diff_cuda/pathtracer_launch.h"
|
| 10 |
+
|
| 11 |
+
namespace {
|
| 12 |
+
|
| 13 |
+
void check_scene(const torch::Tensor& tris, const torch::Tensor& mat_ids,
|
| 14 |
+
const torch::Tensor& nodes_f, const torch::Tensor& nodes_i,
|
| 15 |
+
const torch::Tensor& light_faces,
|
| 16 |
+
const torch::Tensor& light_cdf, const torch::Tensor& albedo,
|
| 17 |
+
const torch::Tensor& emission, const torch::Tensor& cam,
|
| 18 |
+
int64_t spp, int64_t max_bounces) {
|
| 19 |
+
TORCH_CHECK(tris.is_cuda() && tris.is_contiguous() &&
|
| 20 |
+
tris.dtype() == torch::kFloat32 && tris.dim() == 2 &&
|
| 21 |
+
tris.size(1) == 9,
|
| 22 |
+
"tris must be contiguous CUDA f32 [F, 9]");
|
| 23 |
+
TORCH_CHECK(mat_ids.is_cuda() && mat_ids.is_contiguous() &&
|
| 24 |
+
mat_ids.dtype() == torch::kInt32 &&
|
| 25 |
+
mat_ids.numel() == tris.size(0),
|
| 26 |
+
"mat_ids must be contiguous CUDA i32 [F]");
|
| 27 |
+
TORCH_CHECK(nodes_f.is_cuda() && nodes_f.is_contiguous() &&
|
| 28 |
+
nodes_f.dtype() == torch::kFloat32 && nodes_f.dim() == 2 &&
|
| 29 |
+
nodes_f.size(1) == 6,
|
| 30 |
+
"nodes_f must be contiguous CUDA f32 [N, 6]");
|
| 31 |
+
TORCH_CHECK(nodes_i.is_cuda() && nodes_i.is_contiguous() &&
|
| 32 |
+
nodes_i.dtype() == torch::kInt32 && nodes_i.dim() == 2 &&
|
| 33 |
+
nodes_i.size(1) == 3 && nodes_i.size(0) == nodes_f.size(0),
|
| 34 |
+
"nodes_i must be contiguous CUDA i32 [N, 3]");
|
| 35 |
+
TORCH_CHECK(light_faces.is_cuda() && light_faces.is_contiguous() &&
|
| 36 |
+
light_faces.dtype() == torch::kInt32,
|
| 37 |
+
"light_faces must be contiguous CUDA i32 [L]");
|
| 38 |
+
TORCH_CHECK(light_cdf.is_cuda() && light_cdf.is_contiguous() &&
|
| 39 |
+
light_cdf.dtype() == torch::kFloat32 &&
|
| 40 |
+
light_cdf.numel() == light_faces.numel(),
|
| 41 |
+
"light_cdf must be contiguous CUDA f32 [L]");
|
| 42 |
+
TORCH_CHECK(albedo.is_cuda() && albedo.is_contiguous() &&
|
| 43 |
+
albedo.dtype() == torch::kFloat32 && albedo.dim() == 2 &&
|
| 44 |
+
albedo.size(1) == 3,
|
| 45 |
+
"albedo must be contiguous CUDA f32 [M, 3]");
|
| 46 |
+
TORCH_CHECK(emission.is_cuda() && emission.is_contiguous() &&
|
| 47 |
+
emission.dtype() == torch::kFloat32 &&
|
| 48 |
+
emission.sizes() == albedo.sizes(),
|
| 49 |
+
"emission must be contiguous CUDA f32 [M, 3]");
|
| 50 |
+
TORCH_CHECK(albedo.size(0) <= 64, "at most 64 materials");
|
| 51 |
+
TORCH_CHECK(cam.numel() == 12, "cam must have 12 elements");
|
| 52 |
+
TORCH_CHECK(spp >= 1, "spp must be >= 1");
|
| 53 |
+
TORCH_CHECK(max_bounces >= 1 && max_bounces <= 16,
|
| 54 |
+
"max_bounces must be in [1, 16]");
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
} // namespace
|
| 58 |
+
|
| 59 |
+
void pt_forward(torch::Tensor tris, torch::Tensor mat_ids,
|
| 60 |
+
torch::Tensor nodes_f, torch::Tensor nodes_i,
|
| 61 |
+
torch::Tensor light_faces, torch::Tensor light_cdf,
|
| 62 |
+
double total_light_area, torch::Tensor albedo,
|
| 63 |
+
torch::Tensor emission, torch::Tensor cam, int64_t spp,
|
| 64 |
+
int64_t max_bounces, bool nee, int64_t seed,
|
| 65 |
+
torch::Tensor image) {
|
| 66 |
+
check_scene(tris, mat_ids, nodes_f, nodes_i, light_faces, light_cdf, albedo,
|
| 67 |
+
emission, cam, spp, max_bounces);
|
| 68 |
+
TORCH_CHECK(image.is_cuda() && image.is_contiguous() &&
|
| 69 |
+
image.dtype() == torch::kFloat32 && image.dim() == 3 &&
|
| 70 |
+
image.size(2) == 3,
|
| 71 |
+
"image must be contiguous CUDA f32 [H, W, 3]");
|
| 72 |
+
const at::cuda::CUDAGuard guard(tris.device());
|
| 73 |
+
cudaStream_t stream = at::cuda::getCurrentCUDAStream();
|
| 74 |
+
int L = (int)light_faces.numel();
|
| 75 |
+
// The launcher reads the 12 camera floats on the host.
|
| 76 |
+
torch::Tensor cam_h =
|
| 77 |
+
cam.to(torch::kFloat32).to(torch::kCPU).contiguous();
|
| 78 |
+
ptd_forward_launch(
|
| 79 |
+
tris.const_data_ptr<float>(), mat_ids.const_data_ptr<int>(),
|
| 80 |
+
(int)tris.size(0), nodes_f.const_data_ptr<float>(),
|
| 81 |
+
nodes_i.const_data_ptr<int>(), (int)nodes_f.size(0),
|
| 82 |
+
L ? light_faces.const_data_ptr<int>() : nullptr,
|
| 83 |
+
L ? light_cdf.const_data_ptr<float>() : nullptr, L,
|
| 84 |
+
(float)total_light_area, albedo.const_data_ptr<float>(),
|
| 85 |
+
emission.const_data_ptr<float>(), (int)albedo.size(0),
|
| 86 |
+
cam_h.const_data_ptr<float>(), (int)image.size(0), (int)image.size(1),
|
| 87 |
+
(int)spp, (int)max_bounces, nee ? 1 : 0, (long long)seed,
|
| 88 |
+
image.data_ptr<float>(), stream);
|
| 89 |
+
C10_CUDA_KERNEL_LAUNCH_CHECK();
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
void pt_backward(torch::Tensor tris, torch::Tensor mat_ids,
|
| 93 |
+
torch::Tensor nodes_f, torch::Tensor nodes_i,
|
| 94 |
+
torch::Tensor light_faces, torch::Tensor light_cdf,
|
| 95 |
+
double total_light_area, torch::Tensor albedo,
|
| 96 |
+
torch::Tensor emission, torch::Tensor cam, int64_t spp,
|
| 97 |
+
int64_t max_bounces, bool nee, int64_t seed,
|
| 98 |
+
torch::Tensor grad_image, torch::Tensor grad_albedo,
|
| 99 |
+
torch::Tensor grad_emission) {
|
| 100 |
+
check_scene(tris, mat_ids, nodes_f, nodes_i, light_faces, light_cdf, albedo,
|
| 101 |
+
emission, cam, spp, max_bounces);
|
| 102 |
+
TORCH_CHECK(grad_image.is_cuda() && grad_image.is_contiguous() &&
|
| 103 |
+
grad_image.dtype() == torch::kFloat32 &&
|
| 104 |
+
grad_image.dim() == 3 && grad_image.size(2) == 3,
|
| 105 |
+
"grad_image must be contiguous CUDA f32 [H, W, 3]");
|
| 106 |
+
TORCH_CHECK(grad_albedo.is_cuda() && grad_albedo.is_contiguous() &&
|
| 107 |
+
grad_albedo.sizes() == albedo.sizes(),
|
| 108 |
+
"grad_albedo must match albedo");
|
| 109 |
+
TORCH_CHECK(grad_emission.is_cuda() && grad_emission.is_contiguous() &&
|
| 110 |
+
grad_emission.sizes() == emission.sizes(),
|
| 111 |
+
"grad_emission must match emission");
|
| 112 |
+
const at::cuda::CUDAGuard guard(tris.device());
|
| 113 |
+
cudaStream_t stream = at::cuda::getCurrentCUDAStream();
|
| 114 |
+
int L = (int)light_faces.numel();
|
| 115 |
+
torch::Tensor cam_h =
|
| 116 |
+
cam.to(torch::kFloat32).to(torch::kCPU).contiguous();
|
| 117 |
+
ptd_backward_launch(
|
| 118 |
+
tris.const_data_ptr<float>(), mat_ids.const_data_ptr<int>(),
|
| 119 |
+
(int)tris.size(0), nodes_f.const_data_ptr<float>(),
|
| 120 |
+
nodes_i.const_data_ptr<int>(), (int)nodes_f.size(0),
|
| 121 |
+
L ? light_faces.const_data_ptr<int>() : nullptr,
|
| 122 |
+
L ? light_cdf.const_data_ptr<float>() : nullptr, L,
|
| 123 |
+
(float)total_light_area, albedo.const_data_ptr<float>(),
|
| 124 |
+
emission.const_data_ptr<float>(), (int)albedo.size(0),
|
| 125 |
+
cam_h.const_data_ptr<float>(), (int)grad_image.size(0),
|
| 126 |
+
(int)grad_image.size(1), (int)spp, (int)max_bounces, nee ? 1 : 0,
|
| 127 |
+
(long long)seed, grad_image.const_data_ptr<float>(),
|
| 128 |
+
grad_albedo.data_ptr<float>(), grad_emission.data_ptr<float>(), stream);
|
| 129 |
+
C10_CUDA_KERNEL_LAUNCH_CHECK();
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
|
| 133 |
+
ops.def(
|
| 134 |
+
"pt_forward(Tensor tris, Tensor mat_ids, Tensor nodes_f, Tensor nodes_i,"
|
| 135 |
+
" Tensor light_faces, Tensor light_cdf, float total_light_area,"
|
| 136 |
+
" Tensor albedo, Tensor emission, Tensor cam, int spp, int max_bounces,"
|
| 137 |
+
" bool nee, int seed, Tensor! image) -> ()");
|
| 138 |
+
ops.impl("pt_forward", torch::kCUDA, &pt_forward);
|
| 139 |
+
|
| 140 |
+
ops.def(
|
| 141 |
+
"pt_backward(Tensor tris, Tensor mat_ids, Tensor nodes_f,"
|
| 142 |
+
" Tensor nodes_i, Tensor light_faces, Tensor light_cdf,"
|
| 143 |
+
" float total_light_area, Tensor albedo, Tensor emission, Tensor cam,"
|
| 144 |
+
" int spp, int max_bounces, bool nee, int seed, Tensor grad_image,"
|
| 145 |
+
" Tensor! grad_albedo, Tensor! grad_emission) -> ()");
|
| 146 |
+
ops.impl("pt_backward", torch::kCUDA, &pt_backward);
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
REGISTER_EXTENSION(TORCH_EXTENSION_NAME)
|
torch-ext/torch_binding.h
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#pragma once
|
| 2 |
+
|
| 3 |
+
#include <torch/torch.h>
|
| 4 |
+
|
| 5 |
+
void pt_forward(torch::Tensor tris, torch::Tensor mat_ids,
|
| 6 |
+
torch::Tensor nodes_f, torch::Tensor nodes_i,
|
| 7 |
+
torch::Tensor light_faces, torch::Tensor light_cdf,
|
| 8 |
+
double total_light_area, torch::Tensor albedo,
|
| 9 |
+
torch::Tensor emission, torch::Tensor cam, int64_t spp,
|
| 10 |
+
int64_t max_bounces, bool nee, int64_t seed,
|
| 11 |
+
torch::Tensor image);
|
| 12 |
+
|
| 13 |
+
void pt_backward(torch::Tensor tris, torch::Tensor mat_ids,
|
| 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 albedo,
|
| 17 |
+
torch::Tensor emission, torch::Tensor cam, int64_t spp,
|
| 18 |
+
int64_t max_bounces, bool nee, int64_t seed,
|
| 19 |
+
torch::Tensor grad_image, torch::Tensor grad_albedo,
|
| 20 |
+
torch::Tensor grad_emission);
|