Physics & optics
What Pleochroma actually computes, and where it lives in the source tree.
1What is modelled
The summary below is the whole model in one table; the sections after it take each row in turn.
| Aspect | Method | Notes |
|---|---|---|
| Sampling | Hero-wavelength spectral sampling, 8 channels per path | One hero wavelength plus 7 rotated companions per ray (NUM_CHANNELS, crates/pleochroma/src/optics/raytracer/mod.rs) |
| Dispersion | Sellmeier / Cauchy dispersion per material | 38 built-in materials, each with a cited or derived fit (crates/pleochroma/src/optics/materials/, see the materials page). A custom material can carry its own Sellmeier or Cauchy coefficients, which are validated before use (optics/dispersion.rs) |
| Polarization | Stokes vectors and Mueller matrices at every interface | Includes total-internal-reflection phase retardation (optics/polarization.rs, optics/birefringence/) |
| Anisotropy | Uniaxial and biaxial crystal optics | Ordinary/extraordinary ray splitting and Poynting walk-off, with a refractive-index curve per ray where one is published (optics/birefringence/, GPU port in renderer/gpu/transport_check/eigenmodes_uniaxial.rs) |
| Absorption | Pleochroic, per-direction absorption tensors | Beer–Lambert law along the ray chord (optics/raytracer/absorption.rs) |
| Geometry | Half-space intersection, less convex tool volumes | No triangle mesh. Flat facets are planes; a concave facet is a cylinder, cone, sphere or disc tool subtracted from the stone (geometry/plane.rs, simd/slab.rs, geometry/tool.rs, optics/raytracer/intersect_stone.rs) |
| Color | CIE 1931 tabulated color-matching functions to XYZ, then tone mapped | color/mod.rs, color/gamut.rs |
| Inclusions | Henyey–Greenstein volumetric scattering | Optional per-material silk/haze scattering parameters, defined in optics/materials/ and consumed by the Henyey–Greenstein estimator in optics/raytracer/scattering/hg.rs. Oriented needles and plates scatter by direction and wavelength (scattering/oriented.rs) |
| Structural color | Bragg reflection from a procedural domain map, by spectral delta tracking | Play-of-color of opal and schiller of labradorite (optics/inclusions/bragg.rs, scattering/delta_tracking.rs) |
| Surface finish | Total integrated scatter of a polish, GGX halo and ground facets | The exact interface physics at a sampled micro-normal (optics/raytracer/surface/) |
2Hero-wavelength spectral sampling
RGB rendering assigns each ray one of three fixed color channels, which cannot represent a continuously varying refractive index and produces banded color fringing inside a dispersive gemstone. Independently sampling one random wavelength per ray avoids the banding but converges far more slowly, since each pixel's channels are decorrelated.
Pleochroma’s Path-Traced engine instead traces 8 wavelengths per ray at once
(NUM_CHANNELS, crates/pleochroma/src/optics/raytracer/mod.rs):
a hero wavelength λ₀ drawn uniformly over the visible range 380–780 nm,
plus 7 companions spaced by a fixed fractional offset and wrapped into
the same range (wrapped_hero_wavelengths,
optics/raytracer/transport/mod.rs):
lambda_min = 380, lambda_max = 780
lambda_i = lambda_min + ((lambda_0 - lambda_min + i * delta) mod (lambda_max - lambda_min))
delta = (lambda_max - lambda_min) / 8
All 8 channels follow one shared, hero-driven geometric path. At an
interior dispersive event (an entry into the gem), a companion whose
own refracted direction diverges from the hero's beyond a fixed cosine
tolerance loses its radiance for the rest of the trace
(DIRECTION_MATCH_COS_TOL,
optics/raytracer/refraction/). At the bounce where the
hero-driven path actually leaves the gem, a still-diverged companion is
not simply dropped: it receives its own Fresnel transmission and a
one-off environment lookup along its own refracted direction
("exit-event spectral splitting"), which reduces variance without
introducing bias. The channels are then integrated into CIE XYZ with a
spectral multiple-importance-sampling weight per channel: Veach's
balance heuristic over exactly the hero choices under which that channel
would have been alive on the realized path, its own compatibility
family, which differs from channel to channel because the direction
tolerance is a band around each hero
(integrate_channels_to_xyz_families,
optics/raytracer/color.rs). Production renders always run
with exit splitting on, so this is the weighting they use. The single
shared weight over all 8 hero choices (spectral_mis_weight)
belongs to the no-splitting path, and is what the per-channel form
reduces to whenever every family is the full set.
3Sellmeier and Cauchy dispersion
Rather than three fixed indices for red, green, and blue, each built-in
material carries a dispersion model evaluated at the ray's actual
wavelength (crates/pleochroma/src/optics/dispersion.rs,
optics/materials/, one file per group of species).
3-term Sellmeier
n^2(lambda) = 1 + sum_i [ B_i * lambda^2 / (lambda^2 - C_i) ] for i in {1, 2, 3}
(lambda in micrometres)Used where a primary fit exists — Diamond (Peter 1923), Sapphire/Ruby (Malitson & Dodge 1972), Quartz (Ghosh 1999), Spinel, Cubic Zirconia, Moissanite, YAG, and the two reference optical glasses, among others. A fit with fewer than three poles (e.g. Diamond's original 2-term literature form) is stored in the same form by zeroing the unused pole(s), and a constant term is a pole at C = 0 (Moissanite, Rutile). DispersionModel::Sellmeier1, the single-pole variant, exists in optics/dispersion.rs; no built-in material uses it, but a custom material can (see below).
2-parameter Cauchy
n(lambda) = A + B / lambda^2 (lambda in micrometres)
Used as a lower-confidence fallback where no primary Sellmeier fit
exists in the optics literature (several garnets, Zircon, Topaz,
Tourmaline, Tanzanite, and others) — solved from a sourced n_D and a
gemological dispersion figure converted from the Fraunhofer B–G
convention to F–C. The underlying Cauchy model
(optics/dispersion.rs) also carries an optional
C/lambda^4 term; every built-in material sets C = 0,
reducing it to the 2-parameter form above. Each material's source code
comments cite the specific reference and confidence tier used.
Custom coefficients and their validation
A custom material made in the desktop studio can carry its own curve
instead of a mean index and an F−C figure: a 1-term or 3-term
Sellmeier fit, or a Cauchy fit with the C/lambda^4 term
(GemMaterial::new_custom_with_dispersion,
optics/materials/custom.rs). The evaluator floors any index
below 1 at 1, so a bad curve would render a clear block without a hint
why. The curve is therefore checked before it is used
(DispersionModel::validate, optics/dispersion.rs):
- every coefficient is a finite number;
- no Sellmeier term has its resonance, the square root of C in micrometres, between 300 and 800 nm, where the index would blow up (C is a wavelength squared, so 0.01 puts a resonance at 100 nm);
- the index is finite and above 1 at every 5 nm step from 380 to 780 nm.
A curve that fails cannot be saved. Three softer checks only warn
(DispersionModel::warnings): the index rises with
wavelength somewhere in the visible band, the Abbe number
Vd = (nD − 1) / (nF − nC)
lies outside 5–120, or the index is the same at the F and C lines,
so the stone would show no fire. nD, nF and
nC are read at 589.3, 486.1 and 656.3 nm.
4Stokes–Mueller polarized transport
Light inside a cut gemstone can undergo dozens of internal reflections
before it exits. Treating reflectance as an unpolarized scalar
probability misses the polarization state that builds up over those
bounces, including total-internal-reflection phase retardation.
Pleochroma represents light as a 4-component Stokes vector
(optics/polarization.rs):
S = [ I, Q, U, V ]- I — total intensity
- Q — horizontal vs. vertical linear polarization
- U — +45° vs. −45° linear polarization
- V — right- vs. left-handed circular polarization
Every facet interaction applies a 4×4 Mueller matrix that rotates into the interface's own reference frame, evaluates the parallel and perpendicular Fresnel amplitudes, and — under total internal reflection — applies the phase retardation between them:
tan(delta / 2) = cos(theta_i) * sqrt(sin^2(theta_i) - (n_t / n_i)^2) / sin^2(theta_i)
delta = delta_p - delta_s5Uniaxial and biaxial crystal optics
Non-cubic gemstones have an optical indicatrix: the refractive index
depends on the propagation direction relative to the crystal's own
axes (optics/birefringence/).
Uniaxial (Sapphire, Ruby, Quartz, Zircon, Moissanite, Tourmaline...)
A single optical axis. Incident light splits into an ordinary ray (constant index n_o) and an extraordinary ray whose index depends on the angle θ between the wave normal and the optical axis:
1 / n_e^2(theta) = cos^2(theta) / n_o^2 + sin^2(theta) / n_e^2The extraordinary ray's energy also walks off from its wave-normal direction (Poynting vector walk-off), which is what produces visible facet doubling in strongly birefringent stones such as high zircon.
Biaxial (Alexandrite, Topaz, Tanzanite, Peridot, Andalusite...)
Two optical axes and three distinct principal indices n_α ≤ n_β ≤ n_γ.
Pleochroma's BiaxialIndicatrix solver evaluates the wave
surface for both modes on CPU and, in the current build, in the
compute kernel as well — every built-in material, biaxial or uniaxial,
is GPU-supported (GemMaterial::gpu_supported is
unconditional).
A curve for each ray
Each principal index has its own dispersion. A uniaxial material may carry
a separate extraordinary-ray curve (uniaxial_extraordinary_dispersion),
and a biaxial one a curve for the alpha and the gamma ray
(biaxial_principal_dispersion, the beta ray being the main
curve), sorted at each wavelength so the magnitude labels cannot swap. The
published fits used are the extraordinary ray of sapphire and ruby
(Malitson and Dodge 1972), of 6H moissanite (Wang 2013), and the alpha and
gamma rays of alexandrite (Walling 1980, with no crossing between 380 and
780 nm), which chrysoberyl (yellow) also takes. The walk-off direction
uses the same index as the refraction at every wavelength, on the processor
and in the GPU kernels, so the doubled images separate by color. A material
without a curve is unchanged bit for bit.
6Directional pleochroic absorption
Pleochroic gemstones show different color along different
crystallographic directions because absorption depends on the electric
field's orientation relative to the crystal. Pleochroma evaluates a
per-direction absorption tensor via the Beer–Lambert law
(optics/raytracer/absorption.rs):
T(lambda) = exp( - A(lambda, E_hat) * d )where d is the chord length through the material and E_hat is the ray's electric-field direction.
The direction of the crystal in the cut stone decides which color you see face-up. A stone can set that direction (and, for a biaxial crystal, a second direction) and the color, the doubling and the metrics follow it; see the crystal axis page.
- Dichroism (uniaxial materials): distinct ordinary- and extraordinary-ray absorption bands.
- Trichroism (biaxial materials): three independent absorption spectra along α, β, γ — the mechanism behind Alexandrite's color change and Tanzanite's polarized absorption.
The color of a pleochroic stone is kept when you change it. A color variant (Pink or Yellow sapphire, Rubellite or Verdelite tourmaline, and so on) is a full per-direction absorption tensor: many are fitted to measured, mostly polarized spectra of the variety and the others are built from published band positions; almost all use a strength chosen for a typical face-up color and are marked as estimated. A color made in the custom color editor has one color for each direction (Along and Across for a one-axis crystal, Alpha, Beta and Gamma for a two-axis one). Only a material that has one color in every direction, such as a cubic crystal or a glass, is given a single even absorption.
7Half-space geometry and concave tools
Instead of tessellating a gemstone into a triangle mesh, Pleochroma
represents each facet as an infinite planar half-space with outward
unit normal ni and signed distance di
(geometry/plane.rs):
H_i = { x in R^3 | n_i . x + d_i <= 0 }The gemstone is the intersection of M half-spaces, P = ∩ H_i. Ray intersection is an analytical slab test over all M planes (simd/slab.rs), with no bounding-volume hierarchy and no mesh-junction rounding error. That is the whole stone for a design with flat facets only; concave facets add the tools described next.
Concave facets: tools subtracted from the stone
A concave facet is the surface a shaped tool leaves, so it is not a
plane. Pleochroma keeps the planes and subtracts the tools: the stone is
P minus the union of the tool volumes Tk
(geometry/tool.rs). Each Tk is convex: a ball (a
sphere or dimple), a capped cylinder (a cylinder, or a circle as a short
one), a cone frustum, or a bicone (a disc, two frusta sharing a rim). A
tool can also be swept along or across its axis, which models a tool that
reciprocates; the swept volume is what is subtracted. One stone holds at
most 128 tool placements (MAX_TOOL_PRIMITIVES).
Because each tool is convex, a ray's interval inside it has a closed
form and no iterative solver is needed. For one ray the slab test gives
the interval [a, b] inside the planes, and each tool gives its own
interval [c, d]. The hit is the first change of "inside the stone" along
the ray, past a small distance floor, where inside means inside the
planes and outside every tool interval
(optics/raytracer/intersect_stone.rs). A tool hit has its
own facet id, numbered after the planes, and its normal points into the
cavity. With no tools the result is bit for bit the plane-only result, so
a design without concave tiers renders as before.
The WGSL kernels trace the tools too. A tool is packed as five
vec4 records after the planes in the same storage buffer
(geometry/tool/pack.rs), and
renderer/shaders/transport_physics/10_concave_tools.wgsl is the
WGSL twin of the CPU tool code: the same closed-form intervals and the same
carve, with the CPU code as the reference. It differs only where WGSL
forces it: a tool is split into its convex pieces on demand instead of in an
array, and the carve scans the tools instead of sorting the boundaries.
The GPU traces up to 128 tool placements per stone
(GPU_MAX_TOOL_PRIMITIVES, equal to the CPU's
MAX_TOOL_PRIMITIVES), which is every placement a design can
hold, so every concave stone can go to the GPU. A remote render carries
the tool volumes in the scene (SceneState::tools), so local,
remote and combined renders trace the same stone.
8Inclusions and scattering
A stone is not always clear. A material can carry optional silk or haze
parameters (a scattering coefficient and a phase asymmetry) that the
tracer treats as homogeneous Henyey–Greenstein volumetric scattering
(optics/raytracer/scattering/hg.rs). At each bounce the path is
either extinguished or scattered inside the stone, decided once for the
shared path and reweighted per channel. A scattering point samples the
environment directly and combines that with the phase-sampled
continuation by a balance-heuristic weight (see the HDR paragraph in the
lighting section below), and a frosted facet’s exit does the same.
Oriented inclusions
Needles and plates that all point one way make the scattering coefficient
depend on the direction of travel and the wavelength:
a needle extinguishes in proportion to the sine of the angle to its axis, a
plate in proportion to the cosine of the angle to its normal, and each is
scaled by (550 nm / λ) to a power of 0, 1, 2 or 4 (fine silk
scatters blue). Along one straight segment the direction is constant and the
medium homogeneous, so the same exponential free-path sampler applies. The
event picks one component of the mixture and samples its phase function; the
direction is shared by all eight channels and each owes the ratio of its own
mixture to the hero’s. A needle scatters onto the cone that keeps the
component of the direction along its axis (a logistic kernel in that
component, a wrapped Cauchy distribution in the azimuth), which is the
physical origin of asterism and chatoyancy. A plate is a microflake mirror.
The inclusions are placed in the crystal frame, so they follow the crystal
axis (optics/inclusions/).
Bragg domains
Play-of-color and schiller are Bragg reflections from ordered domains. A jittered grid with integer hashes (identical on the processor and the graphics card) gives each domain a lattice normal, a period and an on/off state. The first-order wavelength is 2·d·n·cos θ (opal about 2.33 times the sphere diameter; labradorite 2·1.56 times the lamella period), with a weak second order, and the reflection direction does not depend on wavelength, so it fits hero-wavelength sampling. The free flight is sampled by spectral delta tracking: tentative collisions at a majorant rate, a real reflection accepted with the hero’s probability, and per-channel weights that are real-to-hero or null-to-hero ratios, both non-negative because the majorant is a true bound.
Rough surfaces
A polished facet with an RMS roughness Rq well below the wavelength keeps its sharp specular event and scatters a fraction 1 − exp(−(4π n Rq cos θ / λ)2) into a halo (Bennett and Porteus; Harvey and co-workers). With that probability a micro-normal is drawn from the visible-normal distribution of an anisotropic GGX lobe and the unchanged polished dispatch runs at that normal, so uniaxial entry, biaxial modes, total internal reflection and exit splitting stay exact; the throughput is then corrected channel by channel for the branch taken. The halo width is 2 Rq/ℓc, stretched across the scratches. The direction of the scratches comes from the faceting machine: the tangent of the lap at the contact point, with the contact radius drawn uniformly over the sweep of the last polishing pass, so the fan of directions is part of the image.
A ground (frosted) facet is a GGX rough dielectric (Walter and co-workers; Heitz), replacing the older cosine-weighted bounce. A single-scatter GGX interface loses energy, so each event is divided by a tabulated directional albedo (a Kulla–Conty-style compensation, indexed by roughness, incidence angle and index ratio, one table for both sides of the interface), and a lossless rough slab keeps its energy in the white furnace. Abraded edges are a ground band found with the same nearest-plane search as edge rounding. The effects are opt-in; with none switched on the trace is bit-identical to one without the code.
9Color and tone mapping
- A stone’s body color comes from its material’s absorption bands, applied per channel by the Beer–Lambert law above. A band is a Gaussian in energy (wavenumber) or in wavelength (
optics/absorption.rs). The colors of Ruby, Sapphire, Emerald, Peridot, Spessartine, Tsavorite, Demantoid, Aquamarine and Tanzanite (the heated stone) are fitted to published measured absorption spectra, with energy bands; Amethyst, Citrine and Tourmaline keep their earlier data. Aquamarine has its crystal c axis in the table plane, so a face-up stone shows its blue ray. The materials page lists the sources. - Spectral radiance in each of the 8 channels is integrated against the tabulated CIE 1931 2° color-matching functions (380–780 nm, 5 nm steps,
color/cie1931.rs) to obtain CIE XYZ (optics/raytracer/color.rs). - An ACES filmic tone-mapping curve (Narkowicz 2015 fit) is applied to the luminance channel before the vector is scaled back into RGB, avoiding hue shift on saturated dispersion fire (
color/space.rs). - Out-of-gamut colors are compressed back into the target space; supported output spaces are sRGB, Display P3, and Rec. 2020 (
color/gamut.rs,color/space.rs). - PNG exports from the desktop studio are 8-bit RGBA; any non-sRGB export (Display P3, Rec. 2020) embeds an ICC profile identifying that color space, while sRGB exports are written untagged (
apps/pleochroma-cut/src/bridge/export_thread/tonemap_png.rs; the ICC profile builder iscrates/pleochroma/src/render_setup/icc_profile.rs).
10Environment and lighting models
A ray that misses the stone, or exits it, samples an environment rather than a
light list. Thirteen presets cover the analytic models, and a real HDR
panorama can be loaded in their place
(optics/raytracer/environment/):
- Studio (
Daylight,Incandescent,IlluminantAat 2856 K,RingLights,DarkSpotlight) — a charcoal backdrop, one key softbox, one fill, and sixteen ring pinpoints. Its arithmetic is pinned bit for bit by tables incrates/pleochroma/tests/raytracer_tests/golden_regression.rs(which are re-pinned whenever a deliberate physics change moves them). - Grading tray (
IsoHemisphere, formerly “ISO hemisphere”; the preset is labelled D65 hemisphere + head shadow) — the whole upper hemisphere at radiance 1 and nothing below the girdle plane: the reference case, with no directional structure to flatter a cut. It is the scoring light: Optimize and Retarget score every design under it, and it is flat by design, so a tilt video under it shows no sparkle. - Light tent, the default preset — dim tent walls, one broad overhead softbox, three black cards on the ring positions away from the key, one small hard spark light, black velvet below the girdle. The black cards are the contrast a jewellery photographer adds so a bright stone reads as a facet pattern instead of a white blur.
- Light-tent variants (
WhiteTray,ShopLights,WindowDaylight) — the same model with other parameters, all lit by a D65 spectrum: a white tray lit from below with bright, perfectly even walls and no cards or spark; a bright diffuse jewellery-shop ceiling with small pinpoint spots; and a dim room lit by one broad window, a single soft key with no cards or spark. The built-in Window daylight view places that key at 30° elevation. - Daylight sky (no sun) (
DaylightDome) — a clear sky only, brighter at the horizon than at the zenith, with dark ground and no sun disc. - Daylight sky and direct sun (
DaylightSun) — the same sky plus a real sun, a hard-edged disc 0.27° across that supplies about 82 % of the light at the default key height. It is sampled directly, and a stone flashes with fire whenever a facet catches it. - Contrast view (
Aset) — an ASET-style diagnostic view that colors the sky by elevation — green from 0° to 45°, red from 45° to 75°, blue from 75° to 90° overhead, and black below the horizon — with no white balance, for seeing where in the stone the light returns from. Its figures are scored as the Grading tray.
The lit models also darken every exit direction that falls inside the observer's own head-shadow cone — the term that gives a face-up stone its dark table reflections; the readouts follow the same head-shadow cone and count every lit direction under a lit model, whereas under a Studio rig brilliance is measured against that rig's own lamps. The Studio rig ignores the observer. The size of the head shadow is the Head Shadow setting, 0–30° (default 16°, about a head at arm’s length; 0 switches it off). It changes the brilliance, windowing and extinction figures as well as the picture, so a wider shadow lowers face-up brilliance; the Studio rigs, the contrast view and an HDR map ignore it. Radiances are chosen so that at exposure 1 the ambient terms land near middle grey after the ACES curve and only a direct reflection of a light source clips to white.
In place of an analytic preset, a real HDR panorama can be loaded instead
(EnvironmentSource::HdrMap, renderer/env_map.rs): a 2D
piecewise-constant inverse-CDF sampler draws directions proportional to the
map's own solid-angle-corrected luminance, so samples land where the panorama is
actually bright rather than being spent on a typical capture's dark majority.
The solid-angle Jacobian is written symmetrically around both poles
(sin(min(v, 1−v) · π)) so texels near the
north and south pole get equally well-conditioned densities. The same
distribution backs a genuine next-event-estimation path on both CPU and the WGSL
compute kernel: volumetric scattering inside the stone and a frosted facet's
diffusely-sampled exit each sample the environment directly and combine it with
the phase/BSDF-sampled continuation via a Veach-style balance-heuristic MIS
weight, rather than waiting for a bounce to randomly find a bright pixel.
The main light is set by two angles. Light Azimuth runs 0–360° (default 48°) and Light Elevation about 8.6° to 88.8° (default 72°). They move the light tent’s softbox (its black cards turn with it), the sun of the direct-sun preset, the glow of the sky-only preset, the window of Window daylight and the Studio key softbox. The Grading tray does not depend on them, since the whole hemisphere is lit evenly, and an HDR map ignores them. Exposure (the Overall Brightness slider) scales the light of every built-in preset from 0.2× to 5× (default 1×) and is likewise not applied to an HDR map.
A view stores a lighting preset together with the light direction, the exposure and the camera distance, and for some also the camera pose. Seven ship with the app: Studio Softbox, Daylight Bright, Dramatic Spotlight, Light Tent, Grading tray (the camera looks almost straight down, as a grader would), Daylight Sun and Window daylight (the window at 30° elevation). Your own views are saved beside them, and each can be marked for the export dialog so that one export renders the stone under several lightings.
The backdrop is a physical object: a matte tray lying under the stone out
to the horizon and lit by the lighting rig, so one scene is seen around the stone
and through it. Its radiance is albedo × Erig / π,
where Erig is the rig’s irradiance on an
upward-facing plane, computed once on the host for the chosen lighting and light
direction. Every ray that leaves the stone, a camera ray that misses it or a ray
that exits through the pavilion at any bounce, looks the tray up through the same
environment function, so a window shows what is really there. There are four
choices: White (albedo 0.90, white paper; the default),
Grey (0.18, the photographic grey card), Black
(0.02, velvet) and As Lit, the lighting’s own ground
— for the Grading tray, nothing below the girdle, the ISO convention, so
windows look dark. The tray is lit, not a light source, so it follows the
exposure; in a view from the side the horizon appears where it physically is.
HDR environment maps ignore the backdrop (the map is the world). Two built-in
lightings bring their own tray because their look depends on it: Studio Softbox the
Grey one and Dramatic Spotlight the Black one; every other lighting starts on White,
and any tray can still be chosen afterwards. The metrics and
tilt curves do not read it: windowing is still the light that leaves through the
pavilion, and its numbers are unchanged. Renders made before 2026-10, including
the six-material comparison in the
overview’s renders section, show an older
rule, a grey card that only the camera ray saw.
11GPU equivalence harness
The optional GPU backend (crates/pleochroma/src/renderer/gpu) runs a hand-maintained
WGSL port of the transport physics, not a compiled translation: the shader units under
crates/pleochroma/src/renderer/shaders are concatenated by build.rs
and dispatched as a compute kernel. It is checked against
the CPU reference by a tiered harness rather than a single pass/fail comparison:
| Tier | Scope | Standard |
|---|---|---|
| Tier 1 | RNG seed hashing, integer state, sub-pixel jitter indices (renderer/gpu/rng_check.rs) |
Bit-exact |
| Tier 2 | Individual ported functions — Sellmeier evaluation, Fresnel/TIR, Stokes-Mueller rotation (renderer/gpu/transport_check/), and ray-plane polyhedron intersection (renderer/gpu/polyhedron_check.rs); renderer/gpu/ulp.rs holds the shared ULP-distance helper both use. The concave tool carve runs the production kernel code against the CPU intersection for every tool shape and sweep (renderer/gpu/tool_check.rs) |
Per-function ULP budget (case-bank match for the intersection test, whose result is a discrete hit record rather than a smooth scalar). The tool check uses a relative tolerance, because the GPU may fuse a multiply and an add |
| Tier 3 | Full rendered images, CPU vs. GPU, for a representative subset of the built-in materials, about 9 of the 38 (Spinel, Zircon, Tourmaline, Quartz, Rutile, Synthetic Moissanite, Alexandrite, Topaz, Tanzanite; renderer/gpu/estimator_check/image_comparison.rs), plus one concave stone, a round brilliant with a ball dimple and a cylinder groove (renderer/gpu/estimator_check/image_comparison_concave.rs), and further scenes for a frosted girdle, inclusion scattering, edge rounding, color zones, each lighting model, surface glare and HDR environment light |
Statistical (Welford mean/variance) equivalence |
| Furnace anchor | A stone in a uniform, wavelength-independent radiance environment — a "white furnace" test (renderer/gpu/furnace_check.rs) |
Per-tuple ULP budget against the known analytical result |
Every WGSL shader unit is also parsed with naga in
renderer/gpu/shader_validation_tests.rs, independently of
the numerical comparisons above. A cross-backend merge test renders one
sample range on the CPU and the next on the GPU, merges them the
production way and checks the result statistically against an independent
CPU reference (renderer/gpu/merge_tests.rs; every backend drops
a non-finite sample but still counts it, under one shared rule). The example harness can be run
locally against your own GPU adapter:
cargo run --profile probe -p pleochroma --features gpu --example gpu_equivalence_harnessRay-Traced and Path-Traced
The spectral derivation on this page describes Path-Traced. Ray-Traced deliberately trades those spectral and polarization effects for a fixed-index approximation; it does not replace the analytic optical metrics.
Ray-Traced is a deterministic approximation: one d-line index, or three fixed indices with Fire, RGB absorption tint and 1/4/8/16 AA taps. It finishes at the tap count without denoising. It omits polarization, birefringent doubling, scattering, zoning and edge rounding. Path-Traced retains the spectral model. Optical metrics, tilt sweeps and diagnostics keep their separate analytic tracer.