Atmosphere

This commit is contained in:
connor committed 2026-10-17 22:02:21 -04:00
1 parent a3f62301f4
commit 4b1198a351
21 files changed
+2253 -204

No files matched your search

+4 -4
View File
@@ -51,10 +51,10 @@ struct RenderArgs {
/// rolls highlights off to white at saturation. --out is unchanged.
#[arg(long)]
pretty: bool,
/// Lighting fidelity tier: L0, or L1 (adds Earth's surface, shadows on targets,
/// light from Earth and the Moon, eclipses on them, the Sun's limb darkening, and
/// stars, which camera C1 shows; needs Blue Marble data, and with C1 the star
/// catalog).
/// Lighting fidelity tier: L0, or L1 (adds Earth's surface and atmosphere, shadows
/// on targets, light from Earth and the Moon, eclipses on them, the Sun's limb
/// darkening, and stars, which camera C1 shows; needs Blue Marble data, and with C1
/// the star catalog).
#[arg(long, default_value = "L0")]
lighting: LightingTier,
/// Camera fidelity tier: C0, C1 (adds pixels that collect light over their area,
File diff suppressed because it is too large. Load diff
+9 -19
View File
@@ -14,16 +14,18 @@ pub const MOON_RADIUS: f64 = 1_737_400.0;
/// Without their maps (lighting L0) both are Lambertian with one reflectance spectrum
/// over the whole body. That gives phase-dependent shading, but no oceans, clouds or
/// maria, and not the Moon's steep brightening toward full Moon. At lighting L1 both
/// are read from maps in the colors set here, and the Moon is scaled to its measured
/// phase curve ([`crate::moon::phase_factor`]).
/// are read from maps in the colors set here, the Moon is scaled to its measured phase
/// curve ([`crate::moon::phase_factor`]), and Earth is seen through its atmosphere
/// ([`crate::atmosphere`]).
#[derive(Clone, Debug, PartialEq)]
pub struct Appearance {
/// Earth's reflectance, used when no Earth texture is set.
pub earth_albedo: Spectrum,
/// Added to an Earth texture's reflectance: a stand-in for the light Earth's
/// atmosphere scatters toward the camera, until the atmosphere is modeled.
/// Not used with `earth_albedo`, which already includes the atmosphere's effect.
pub earth_haze: Spectrum,
/// Whether Earth is seen through its atmosphere at lighting L1: the light it
/// scatters, the light it takes out, and the glow above the limb. On by default;
/// off leaves the bare surface, for comparisons and for tests of the surface alone.
/// `earth_albedo` (below L1) already includes the atmosphere's effect.
pub earth_atmosphere: bool,
/// The Moon's reflectance, used when the Moon has no map (below lighting L1).
pub moon_albedo: Spectrum,
/// The shape of the Moon's reflectance spectrum, 1 at 643 nm, which a Moon map's
@@ -50,25 +52,13 @@ impl Default for Appearance {
Self {
earth_albedo: Spectrum::sampled(nm.map(|x| x * 1e-9).to_vec(), earth.to_vec())
.expect("fixed samples"),
earth_haze: rayleigh_haze(),
earth_atmosphere: true,
moon_albedo: Spectrum::constant(0.12),
moon_color: crate::moon::moon_spectrum(),
}
}
}
/// Reflectance shaped like Rayleigh scattering, falling as 1/lambda^4: 0.07 at 450 nm,
/// 0.017 at 650 nm. Sampled every 10 nm from 300 to 1100 nm, zero outside.
///
/// The size is a round figure of the right order: clear air's Rayleigh optical depth is
/// about 0.2 at 450 nm, and the light it scatters toward a camera above it amounts to a
/// reflectance of a few hundredths to a tenth, depending on the angles.
fn rayleigh_haze() -> Spectrum {
let nm: Vec<f64> = (30..=110).map(|i| i as f64 * 10.0).collect();
let value = nm.iter().map(|l| 0.07 * (450.0 / l).powi(4)).collect();
Spectrum::sampled(nm.iter().map(|l| l * 1e-9).collect(), value).expect("increasing samples")
}
#[cfg(test)]
mod tests {
use super::*;
+3 -1
View File
@@ -19,7 +19,9 @@ pub enum LightingTier {
#[default]
L0,
/// L0, plus Earth's surface from an image (`RendererOptions::earth_texture`, NASA's
/// Blue Marble) with a stand-in for the atmosphere's blue; the Moon's surface from
/// Blue Marble) seen through Earth's atmosphere, which scatters sunlight toward the
/// camera, dims what lies behind it and glows over the limb
/// ([`crate::atmosphere`]; a camera inside it is refused); the Moon's surface from
/// the LRO Camera's map (`RendererOptions::moon_texture`), turned by
/// `Scene::moon_attitude`; shadows that targets cast on themselves and on each
/// other, with the penumbra the Sun's disk gives them; sunlight reflected onto
+162
View File
@@ -0,0 +1,162 @@
//! Earth's atmosphere's tables on the GPU ([`crate::atmosphere`]), which the bodies pass
//! and the Earthshine pass read through `shaders/atmosphere.wgsl`.
use half::f16;
use super::GpuContext;
use crate::atmosphere::{AtmosphereTables, IRRADIANCE_SIZE, SCATTERING_SIZE, TRANSMITTANCE_SIZE};
/// The tables as f16 textures, and the sampler that reads them: linear, clamped to the
/// edges, as the tables' texture coordinates assume.
#[derive(Debug)]
pub struct AtmosphereTextures {
transmittance: wgpu::TextureView,
rayleigh: wgpu::TextureView,
mie: wgpu::TextureView,
irradiance: wgpu::TextureView,
sampler: wgpu::Sampler,
}
impl AtmosphereTextures {
/// Uploads `tables`.
pub fn new(ctx: &GpuContext, tables: &AtmosphereTables) -> Self {
let [tw, th] = TRANSMITTANCE_SIZE;
let [nu, mu_s, mu, r] = SCATTERING_SIZE;
let [iw, ih] = IRRADIANCE_SIZE;
let scattering = [(nu * mu_s) as u32, mu as u32, r as u32];
Self::from_texels(
ctx,
[
(&tables.transmittance, [tw as u32, th as u32, 1]),
(&tables.rayleigh, scattering),
(&tables.mie, scattering),
(&tables.irradiance, [iw as u32, ih as u32, 1]),
],
)
}
/// One-texel tables of zeros, which passes bind until the atmosphere is set; the
/// shaders do not read them then.
pub fn placeholder(ctx: &GpuContext) -> Self {
let zero = vec![[0.0f32; 4]];
Self::from_texels(ctx, [(&zero, [1, 1, 1]); 4])
}
fn from_texels(ctx: &GpuContext, tables: [(&Vec<[f32; 4]>, [u32; 3]); 4]) -> Self {
let upload = |label: &str, (texels, [w, h, d]): (&Vec<[f32; 4]>, [u32; 3]), three: bool| {
let half: Vec<[f16; 4]> = texels.iter().map(|t| t.map(f16::from_f32)).collect();
let size = wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: d,
};
let texture = ctx.device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size,
mip_level_count: 1,
sample_count: 1,
dimension: if three {
wgpu::TextureDimension::D3
} else {
wgpu::TextureDimension::D2
},
format: wgpu::TextureFormat::Rgba16Float,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
ctx.queue.write_texture(
texture.as_image_copy(),
bytemuck::cast_slice(&half),
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(8 * w),
rows_per_image: Some(h),
},
size,
);
texture.create_view(&Default::default())
};
let [transmittance, rayleigh, mie, irradiance] = tables;
let sampler = ctx.device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("atmosphere"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
..Default::default()
});
Self {
transmittance: upload("atmosphere transmittance", transmittance, false),
rayleigh: upload("atmosphere rayleigh", rayleigh, true),
mie: upload("atmosphere mie", mie, true),
irradiance: upload("atmosphere irradiance", irradiance, false),
sampler,
}
}
/// The bind group entries for the tables and sampler, at bindings `first` to
/// `first + 4` in the order the shaders declare them: transmittance, Rayleigh,
/// Mie, irradiance, sampler.
pub fn entries(&self, first: u32) -> [wgpu::BindGroupEntry<'_>; 5] {
let view = |binding, view| wgpu::BindGroupEntry {
binding,
resource: wgpu::BindingResource::TextureView(view),
};
[
view(first, &self.transmittance),
view(first + 1, &self.rayleigh),
view(first + 2, &self.mie),
view(first + 3, &self.irradiance),
wgpu::BindGroupEntry {
binding: first + 4,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
]
}
/// The matching layout entries, visible to `stages`.
pub fn layout_entries(
first: u32,
stages: wgpu::ShaderStages,
) -> [wgpu::BindGroupLayoutEntry; 5] {
let texture = |binding, view_dimension| wgpu::BindGroupLayoutEntry {
binding,
visibility: stages,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension,
multisampled: false,
},
count: None,
};
[
texture(first, wgpu::TextureViewDimension::D2),
texture(first + 1, wgpu::TextureViewDimension::D3),
texture(first + 2, wgpu::TextureViewDimension::D3),
texture(first + 3, wgpu::TextureViewDimension::D2),
wgpu::BindGroupLayoutEntry {
binding: first + 4,
visibility: stages,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
]
}
}
/// The shader declarations of the tables and sampler at bindings `first` to `first + 4`
/// of group 0, the constants they need, and `atmosphere.wgsl`.
pub(crate) fn wgsl(first: u32) -> String {
format!(
"{}@group(0) @binding({}) var atmosphere_transmittance_table: texture_2d<f32>;\n\
@group(0) @binding({}) var atmosphere_rayleigh_table: texture_3d<f32>;\n\
@group(0) @binding({}) var atmosphere_mie_table: texture_3d<f32>;\n\
@group(0) @binding({}) var atmosphere_irradiance_table: texture_2d<f32>;\n\
@group(0) @binding({}) var atmosphere_sampler: sampler;\n{}",
crate::atmosphere::wgsl_constants(),
first,
first + 1,
first + 2,
first + 3,
first + 4,
include_str!("../shaders/atmosphere.wgsl"),
)
}
+67 -28
View File
@@ -8,9 +8,11 @@
use nalgebra::Vector3;
use super::atmosphere::{self, AtmosphereTextures};
use super::profile::PassTimestamps;
use super::targets::{FrameTargets, DEPTH_FORMAT, RADIANCE_FORMAT};
use super::GpuContext;
use crate::atmosphere::AtmosphereTables;
use crate::bodies::{Appearance, EARTH_EQUATORIAL_RADIUS, EARTH_POLAR_RADIUS, MOON_RADIUS};
use crate::camera::Intrinsics;
use crate::earth::EarthTexture;
@@ -77,8 +79,9 @@ pub struct BodyRadiometry {
/// Per channel, the albedo of each RGB basis spectrum (see [`rgb_spectrum`]): column
/// j maps a texture's channel j (red, green, blue) to the camera's channels.
pub earth_rgb: [[f32; 4]; 3],
/// Per channel, the albedo of `Appearance::earth_haze`, added to a texture's.
pub earth_haze: [f32; 4],
/// Whether Earth is seen through its atmosphere: lighting L1 with
/// `Appearance::earth_atmosphere`, once the tables are set.
pub atmosphere: bool,
/// Whether Earth is drawn from a texture.
pub earth_textured: bool,
/// Whether Earth and the Moon shadow each other: lunar and solar eclipses.
@@ -138,7 +141,7 @@ impl BodyRadiometry {
earth_lambert_1au: lambert(radiometry.effective_albedo(&appearance.earth_albedo)),
earth_irradiance_1au: lambert(vec![1.0; n]),
earth_rgb: [basis(0), basis(1), basis(2)],
earth_haze: albedo(&appearance.earth_haze),
atmosphere: false,
earth_textured: false,
bodies_eclipsed: false,
earth_albedo: albedo(&appearance.earth_albedo),
@@ -167,7 +170,9 @@ pub struct BodiesUniforms {
earth_irradiance: [f32; 4],
// mat3x4 columns: texture red, green, blue to channel albedo.
earth_rgb: [[f32; 4]; 3],
earth_haze: [f32; 4],
// xyz: unit vector toward the Sun in Earth-fixed axes stretched along the polar
// axis, the atmosphere's frame.
earth_sun_stretched: [f32; 4],
moon_center: [f32; 4],
moon_sun: [f32; 4],
moon_lambert: [f32; 4],
@@ -221,7 +226,7 @@ impl BodiesUniforms {
near as f32,
f32::from(u8::from(radiometry.earth_textured)),
f32::from(u8::from(radiometry.bodies_eclipsed)),
0.0,
f32::from(u8::from(radiometry.atmosphere)),
],
earth_center: vec4(rel.earth.translation, EARTH_EQUATORIAL_RADIUS),
earth_axes: columns(rel.earth.rotation.to_rotation_matrix()),
@@ -235,7 +240,7 @@ impl BodiesUniforms {
earth_lambert: scaled(radiometry.earth_lambert_1au, scene.sun.norm()),
earth_irradiance: scaled(radiometry.earth_irradiance_1au, scene.sun.norm()),
earth_rgb: radiometry.earth_rgb,
earth_haze: radiometry.earth_haze,
earth_sun_stretched: vec4(sun_stretched(scene).cast::<f32>(), 0.0),
moon_center: vec4(rel.moon, MOON_RADIUS),
moon_sun: vec4(sun_from(scene.moon), 0.0),
moon_lambert: scaled(radiometry.moon_lambert_1au, moon_distance),
@@ -257,6 +262,18 @@ impl BodiesUniforms {
}
}
/// The unit vector toward the Sun from Earth's center in Earth-fixed axes stretched
/// along the polar axis, where Earth is a sphere: the frame of `atmosphere.wgsl`.
pub(crate) fn sun_stretched(scene: &Scene) -> Vector3<f64> {
let fixed = scene.earth_attitude.inverse() * scene.sun.normalize();
Vector3::new(
fixed.x,
fixed.y,
fixed.z * EARTH_EQUATORIAL_RADIUS / EARTH_POLAR_RADIUS,
)
.normalize()
}
/// The compiled bodies pipeline and its uniform buffer.
#[derive(Debug)]
pub struct BodiesPass {
@@ -268,6 +285,7 @@ pub struct BodiesPass {
earth_height: u32,
moon: wgpu::TextureView,
moon_height: u32,
atmosphere: AtmosphereTextures,
bind_group: wgpu::BindGroup,
}
@@ -317,6 +335,7 @@ impl BodiesPass {
source: wgpu::ShaderSource::Wgsl(
(prelude
+ include_str!("../shaders/common.wgsl")
+ &atmosphere::wgsl(5)
+ include_str!("../shaders/bodies.wgsl"))
.into(),
),
@@ -374,6 +393,7 @@ impl BodiesPass {
let sun_disk = SunDisk::buffer(ctx, &LimbDarkening::uniform());
let earth_view = upload_earth(ctx, &earth);
let moon_view = upload_moon(ctx, &moon);
let atmosphere = AtmosphereTextures::placeholder(ctx);
let bind_group = bind(
ctx,
&pipeline,
@@ -382,6 +402,7 @@ impl BodiesPass {
&sampler,
&earth_view,
&moon_view,
&atmosphere,
);
Self {
pipeline,
@@ -392,6 +413,7 @@ impl BodiesPass {
earth_height: earth.height(),
moon: moon_view,
moon_height: moon.height(),
atmosphere,
bind_group,
}
}
@@ -410,6 +432,18 @@ impl BodiesPass {
self.rebind(ctx);
}
/// Uploads Earth's atmosphere's tables and binds them for later frames; the
/// uniforms' atmosphere flag turns them on.
pub fn set_atmosphere(&mut self, ctx: &GpuContext, tables: &AtmosphereTables) {
self.atmosphere = AtmosphereTextures::new(ctx, tables);
self.rebind(ctx);
}
/// The atmosphere's tables (placeholders until set), for the Earthshine pass.
pub fn atmosphere(&self) -> &AtmosphereTextures {
&self.atmosphere
}
fn rebind(&mut self, ctx: &GpuContext) {
self.bind_group = bind(
ctx,
@@ -419,6 +453,7 @@ impl BodiesPass {
&self.sampler,
&self.earth,
&self.moon,
&self.atmosphere,
);
}
@@ -580,6 +615,7 @@ fn upload<'a>(
}
/// The bodies pass's bind group.
#[allow(clippy::too_many_arguments)]
fn bind(
ctx: &GpuContext,
pipeline: &wgpu::RenderPipeline,
@@ -588,31 +624,34 @@ fn bind(
sampler: &wgpu::Sampler,
earth: &wgpu::TextureView,
moon: &wgpu::TextureView,
atmosphere: &AtmosphereTextures,
) -> wgpu::BindGroup {
let entries = [
wgpu::BindGroupEntry {
binding: 0,
resource: uniforms.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(earth),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(sampler),
},
wgpu::BindGroupEntry {
binding: 3,
resource: sun_disk.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::TextureView(moon),
},
];
let entries: Vec<_> = entries.into_iter().chain(atmosphere.entries(5)).collect();
ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("bodies"),
layout: &pipeline.get_bind_group_layout(0),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: uniforms.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(earth),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(sampler),
},
wgpu::BindGroupEntry {
binding: 3,
resource: sun_disk.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::TextureView(moon),
},
],
entries: &entries,
})
}
+92 -81
View File
@@ -7,6 +7,7 @@
use nalgebra::{UnitQuaternion, Vector3};
use super::atmosphere::{self, AtmosphereTextures};
use super::bodies::{BodiesPass, BodyRadiometry, SunDisk};
use super::profile::PassTimestamps;
use super::GpuContext;
@@ -104,7 +105,6 @@ impl TargetLight {
struct Globals {
earth_albedo: [f32; 4],
earth_rgb: [[f32; 4]; 3],
earth_haze: [f32; 4],
moon_albedo: [f32; 4],
params: [f32; 4],
grid: [u32; 4],
@@ -141,48 +141,56 @@ impl EarthshinePass {
has_dynamic_offset: false,
min_binding_size: None,
};
let entries = [
entry(0, buffer(wgpu::BufferBindingType::Uniform)),
entry(
1,
buffer(wgpu::BufferBindingType::Storage { read_only: true }),
),
entry(
2,
buffer(wgpu::BufferBindingType::Storage { read_only: false }),
),
entry(
3,
wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
),
entry(
4,
wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
),
entry(
5,
wgpu::BindingType::StorageTexture {
access: wgpu::StorageTextureAccess::WriteOnly,
format: CUBE_FORMAT,
view_dimension: wgpu::TextureViewDimension::D2Array,
},
),
entry(6, buffer(wgpu::BufferBindingType::Uniform)),
entry(
7,
wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
),
];
let entries: Vec<_> = entries
.into_iter()
.chain(AtmosphereTextures::layout_entries(
8,
wgpu::ShaderStages::COMPUTE,
))
.collect();
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("earthshine"),
entries: &[
entry(0, buffer(wgpu::BufferBindingType::Uniform)),
entry(
1,
buffer(wgpu::BufferBindingType::Storage { read_only: true }),
),
entry(
2,
buffer(wgpu::BufferBindingType::Storage { read_only: false }),
),
entry(
3,
wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
),
entry(
4,
wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
),
entry(
5,
wgpu::BindingType::StorageTexture {
access: wgpu::StorageTextureAccess::WriteOnly,
format: CUBE_FORMAT,
view_dimension: wgpu::TextureViewDimension::D2Array,
},
),
entry(6, buffer(wgpu::BufferBindingType::Uniform)),
entry(
7,
wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
),
],
entries: &entries,
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("earthshine"),
@@ -192,10 +200,9 @@ impl EarthshinePass {
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("earthshine"),
source: wgpu::ShaderSource::Wgsl(
concat!(
include_str!("../shaders/common.wgsl"),
include_str!("../shaders/earthshine.wgsl")
)
(include_str!("../shaders/common.wgsl").to_owned()
+ &atmosphere::wgsl(8)
+ include_str!("../shaders/earthshine.wgsl"))
.into(),
),
});
@@ -316,13 +323,12 @@ impl EarthshinePass {
let globals = Globals {
earth_albedo: radiometry.earth_albedo,
earth_rgb: radiometry.earth_rgb,
earth_haze: radiometry.earth_haze,
moon_albedo: radiometry.moon_albedo,
params: [
f32::from(u8::from(radiometry.earth_textured)),
(std::f64::consts::PI * EARTH_EQUATORIAL_RADIUS / height as f64) as f32,
(EARTH_EQUATORIAL_RADIUS / EARTH_POLAR_RADIUS) as f32,
0.0,
f32::from(u8::from(radiometry.atmosphere)),
],
grid: [RINGS, SECTORS, CUBE_SIZE, lights.len() as u32],
moon_color: radiometry.moon_color,
@@ -341,43 +347,48 @@ impl EarthshinePass {
dimension: Some(wgpu::TextureViewDimension::D2Array),
..Default::default()
});
let entries = [
wgpu::BindGroupEntry {
binding: 0,
resource: self.globals.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: self.lights.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: self.samples.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(texture),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::Sampler(sampler),
},
wgpu::BindGroupEntry {
binding: 5,
resource: wgpu::BindingResource::TextureView(&cube),
},
wgpu::BindGroupEntry {
binding: 6,
resource: self.sun_disk.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 7,
resource: wgpu::BindingResource::TextureView(moon.0),
},
];
let entries: Vec<_> = entries
.into_iter()
.chain(bodies.atmosphere().entries(8))
.collect();
let group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("earthshine"),
layout: &self.layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: self.globals.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: self.lights.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: self.samples.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(texture),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::Sampler(sampler),
},
wgpu::BindGroupEntry {
binding: 5,
resource: wgpu::BindingResource::TextureView(&cube),
},
wgpu::BindGroupEntry {
binding: 6,
resource: self.sun_disk.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 7,
resource: wgpu::BindingResource::TextureView(moon.0),
},
],
entries: &entries,
});
let n = lights.len() as u32;
pass.set_bind_group(0, &group, &[]);
+1
View File
@@ -1,5 +1,6 @@
//! GPU plumbing: adapter selection, device setup, render targets and passes.
pub mod atmosphere;
pub mod blur;
pub mod bodies;
pub mod context;
+1
View File
@@ -17,6 +17,7 @@ pub use nalgebra;
/// [`gpu::GpuContext`] hands out wgpu types.
pub use wgpu;
pub mod atmosphere;
pub mod bodies;
pub mod camera;
pub mod earth;
+18
View File
@@ -15,6 +15,7 @@ use crate::bodies::{Appearance, MOON_RADIUS};
use crate::camera::{Blooming, CameraConfig, Intrinsics, Psf};
use std::sync::Arc;
use crate::atmosphere::AtmosphereTables;
use crate::earth::EarthTexture;
use crate::eclipse::{sun_visible_fraction, sun_visible_light};
use crate::fidelity::{CameraTier, Fidelity, LightingTier};
@@ -431,6 +432,11 @@ impl Renderer {
renderer
.earthshine
.set_limb_darkening(&renderer.ctx, &renderer.limb);
if options.appearance.earth_atmosphere {
let tables = AtmosphereTables::new(&renderer.radiometry);
renderer.bodies.set_atmosphere(&renderer.ctx, &tables);
renderer.body_radiometry.atmosphere = true;
}
}
if let Some(texture) = &options.earth_texture {
renderer.set_earth_texture(texture);
@@ -541,6 +547,18 @@ impl Renderer {
)));
}
let camera = observer.then(&self.camera.mount);
if self.body_radiometry.atmosphere {
let altitude = crate::atmosphere::altitude(scene, camera.position);
if altitude < crate::atmosphere::TOP_ALTITUDE {
return Err(Error::Unsupported(format!(
"the camera is {:.0} km up, inside Earth's atmosphere, which lighting {} \
draws only from above its top at {:.0} km",
altitude / 1e3,
self.fidelity.lighting,
crate::atmosphere::TOP_ALTITUDE / 1e3
)));
}
}
let intrinsics = self.render_intrinsics;
let near = self.camera.near_clip;
let occulters = [
+235
View File
@@ -0,0 +1,235 @@
// Earth's atmosphere, from the tables atmosphere.rs computes: the sunlight it scatters
// toward a viewer in space, the light it lets through, and the skylight on the ground.
// Bruneton's lookups (functions.glsl of his 2017 implementation), in kilometers.
//
// Positions are relative to Earth's center in Earth-fixed axes stretched along the
// polar axis, where Earth is a sphere of radius ATMOSPHERE_BOTTOM (common.wgsl,
// `ellipsoid_hit`). Scattered light is radiance per unit of solar irradiance, per
// channel; transmittance is a fraction.
//
// The code that includes this file first declares the ATMOSPHERE_ constants
// (atmosphere.rs, `wgsl_constants`), the textures `atmosphere_transmittance_table` and
// `atmosphere_irradiance_table` (texture_2d<f32>), `atmosphere_rayleigh_table` and
// `atmosphere_mie_table` (texture_3d<f32>), and the sampler `atmosphere_sampler`,
// linear and clamped to the edges (gpu/atmosphere.rs, `wgsl`).
fn atmosphere_coord(x: f32, size: f32) -> f32 {
return 0.5 / size + x * (1.0 - 1.0 / size);
}
// The distance to the top of the atmosphere for a horizontal ray at the ground.
fn atmosphere_span() -> f32 {
return sqrt(ATMOSPHERE_TOP * ATMOSPHERE_TOP - ATMOSPHERE_BOTTOM * ATMOSPHERE_BOTTOM);
}
fn atmosphere_to_top(r: f32, mu: f32) -> f32 {
let discriminant = r * r * (mu * mu - 1.0) + ATMOSPHERE_TOP * ATMOSPHERE_TOP;
return max(-r * mu + sqrt(max(discriminant, 0.0)), 0.0);
}
fn atmosphere_transmittance_uv(r: f32, mu: f32) -> vec2<f32> {
let h = atmosphere_span();
let rho = sqrt(max(r * r - ATMOSPHERE_BOTTOM * ATMOSPHERE_BOTTOM, 0.0));
let d = atmosphere_to_top(r, mu);
let d_min = ATMOSPHERE_TOP - r;
let d_max = rho + h;
return vec2<f32>(
atmosphere_coord((d - d_min) / (d_max - d_min), ATMOSPHERE_TRANSMITTANCE_SIZE.x),
atmosphere_coord(rho / h, ATMOSPHERE_TRANSMITTANCE_SIZE.y),
);
}
// Per channel, the transmittance from radius `r` to the top along cosine `mu`, for a
// ray that does not meet the ground.
fn atmosphere_transmittance_to_top(r: f32, mu: f32) -> vec4<f32> {
let uv = atmosphere_transmittance_uv(r, mu);
return textureSampleLevel(atmosphere_transmittance_table, atmosphere_sampler, uv, 0.0);
}
// Per channel, the transmittance between radius `r` and the point `d` along cosine
// `mu`, from two lookups (`ground`: the ray meets the ground).
fn atmosphere_transmittance(r: f32, mu: f32, d: f32, ground: bool) -> vec4<f32> {
let r_d = clamp(sqrt(d * d + 2.0 * r * mu * d + r * r), ATMOSPHERE_BOTTOM, ATMOSPHERE_TOP);
let mu_d = clamp((r * mu + d) / r_d, -1.0, 1.0);
if ground {
return min(
atmosphere_transmittance_to_top(r_d, -mu_d) / atmosphere_transmittance_to_top(r, -mu),
vec4<f32>(1.0),
);
}
return min(
atmosphere_transmittance_to_top(r, mu) / atmosphere_transmittance_to_top(r_d, mu_d),
vec4<f32>(1.0),
);
}
// Per channel, the transmittance toward the Sun from radius `r`, with the Sun at cosine
// `mu_s` from the zenith, times the fraction of its disk above the horizon.
fn atmosphere_transmittance_to_sun(r: f32, mu_s: f32) -> vec4<f32> {
let sin_h = ATMOSPHERE_BOTTOM / r;
let cos_h = -sqrt(max(1.0 - sin_h * sin_h, 0.0));
let edge = sin_h * ATMOSPHERE_SUN_ANGULAR_RADIUS;
return atmosphere_transmittance_to_top(r, mu_s) * smoothstep(-edge, edge, mu_s - cos_h);
}
fn atmosphere_scattering_uvwz(r: f32, mu: f32, mu_s: f32, nu: f32, ground: bool) -> vec4<f32> {
let size = ATMOSPHERE_SCATTERING_SIZE;
let h = atmosphere_span();
let rho = sqrt(max(r * r - ATMOSPHERE_BOTTOM * ATMOSPHERE_BOTTOM, 0.0));
let u_r = atmosphere_coord(rho / h, size.w);
let r_mu = r * mu;
let discriminant = r_mu * r_mu - r * r + ATMOSPHERE_BOTTOM * ATMOSPHERE_BOTTOM;
var u_mu: f32;
if ground {
let d = -r_mu - sqrt(max(discriminant, 0.0));
let d_min = r - ATMOSPHERE_BOTTOM;
let d_max = rho;
let x = select((d - d_min) / (d_max - d_min), 0.0, d_max == d_min);
u_mu = 0.5 - 0.5 * atmosphere_coord(x, size.z / 2.0);
} else {
let d = -r_mu + sqrt(max(discriminant + h * h, 0.0));
let d_min = ATMOSPHERE_TOP - r;
let d_max = rho + h;
u_mu = 0.5 + 0.5 * atmosphere_coord((d - d_min) / (d_max - d_min), size.z / 2.0);
}
let d = atmosphere_to_top(ATMOSPHERE_BOTTOM, mu_s);
let d_min = ATMOSPHERE_TOP - ATMOSPHERE_BOTTOM;
let d_max = h;
let a = (d - d_min) / (d_max - d_min);
let big_a = (atmosphere_to_top(ATMOSPHERE_BOTTOM, ATMOSPHERE_MU_S_MIN) - d_min) / (d_max - d_min);
let u_mu_s = atmosphere_coord(max(1.0 - a / big_a, 0.0) / (1.0 + a), size.y);
return vec4<f32>((nu + 1.0) / 2.0, u_mu_s, u_mu, u_r);
}
// A scattering table at `uvwz`: trilinear in the 3D texture, and linear between the
// two slices of nu around it.
fn atmosphere_lookup(table: texture_3d<f32>, uvwz: vec4<f32>) -> vec4<f32> {
let size = ATMOSPHERE_SCATTERING_SIZE;
let x = uvwz.x * (size.x - 1.0);
let slice = min(floor(x), size.x - 2.0);
let lerp = x - slice;
let a = vec3<f32>((slice + uvwz.y) / size.x, uvwz.z, uvwz.w);
let b = vec3<f32>((slice + 1.0 + uvwz.y) / size.x, uvwz.z, uvwz.w);
return mix(
textureSampleLevel(table, atmosphere_sampler, a, 0.0),
textureSampleLevel(table, atmosphere_sampler, b, 0.0),
lerp,
);
}
fn atmosphere_rayleigh_phase(nu: f32) -> f32 {
return 3.0 / (16.0 * PI) * (1.0 + nu * nu);
}
fn atmosphere_mie_phase(nu: f32) -> f32 {
let g = ATMOSPHERE_MIE_G;
let k = 3.0 / (8.0 * PI) * (1.0 - g * g) / (2.0 + g * g);
return k * (1.0 + nu * nu) / pow(1.0 + g * g - 2.0 * g * nu, 1.5);
}
// The sky's irradiance on a horizontal surface at radius `r`, with the Sun at cosine
// `mu_s` from the zenith, as a fraction of the Sun's irradiance.
fn atmosphere_sky_irradiance(r: f32, mu_s: f32) -> vec4<f32> {
let uv = vec2<f32>(
atmosphere_coord(mu_s * 0.5 + 0.5, ATMOSPHERE_IRRADIANCE_SIZE.x),
atmosphere_coord(
(r - ATMOSPHERE_BOTTOM) / (ATMOSPHERE_TOP - ATMOSPHERE_BOTTOM),
ATMOSPHERE_IRRADIANCE_SIZE.y,
),
);
return textureSampleLevel(atmosphere_irradiance_table, atmosphere_sampler, uv, 0.0);
}
// What the atmosphere does to a ray from a viewer in space: the light it scatters into
// the ray, per unit of sunlight, and the transmittance along it.
struct AtmosphereLight {
scattered: vec4<f32>,
transmittance: vec4<f32>,
}
// Where the ray from `camera` along unit `view` enters the atmosphere: the distance to
// it, or MISS if the ray passes outside. The miss distance comes from the point of
// closest approach, as `sphere_hit` takes it, so a camera far from Earth keeps its
// precision. A camera inside the atmosphere enters at distance 0.
fn atmosphere_entry(camera: vec3<f32>, view: vec3<f32>) -> f32 {
let b = dot(camera, view);
let q = length(camera - b * view);
if q > ATMOSPHERE_TOP {
return MISS;
}
if length(camera) <= ATMOSPHERE_TOP {
return 0.0;
}
let t = -b - sqrt((ATMOSPHERE_TOP - q) * (ATMOSPHERE_TOP + q));
return select(MISS, t, t > 0.0);
}
// Along a ray from `camera` in unit direction `view` that crosses the atmosphere without
// meeting the ground, with the Sun along unit `sun`. Zero light and full transmittance
// for a ray that misses it.
fn atmosphere_sky(camera: vec3<f32>, view: vec3<f32>, sun: vec3<f32>) -> AtmosphereLight {
let entry = atmosphere_entry(camera, view);
if entry < 0.0 {
return AtmosphereLight(vec4<f32>(0.0), vec4<f32>(1.0));
}
let x = camera + entry * view;
let r = max(length(x), ATMOSPHERE_BOTTOM);
let mu = clamp(dot(x, view) / r, -1.0, 1.0);
let mu_s = clamp(dot(x, sun) / r, -1.0, 1.0);
let nu = clamp(dot(view, sun), -1.0, 1.0);
let uvwz = atmosphere_scattering_uvwz(r, mu, mu_s, nu, false);
let rayleigh = atmosphere_lookup(atmosphere_rayleigh_table, uvwz);
let mie = atmosphere_lookup(atmosphere_mie_table, uvwz);
var light: AtmosphereLight;
light.scattered = rayleigh * atmosphere_rayleigh_phase(nu) + mie * atmosphere_mie_phase(nu);
light.transmittance = atmosphere_transmittance_to_top(r, mu);
return light;
}
// Along a ray from `camera` in unit direction `view` to the point `ground` on the
// ground, with the Sun along unit `sun`: the scattering along the whole ray from where
// it enters the atmosphere, less that beyond the ground point seen through the
// transmittance to it (Bruneton's `GetSkyRadianceToPoint`).
fn atmosphere_to_ground(
camera: vec3<f32>,
view: vec3<f32>,
ground: vec3<f32>,
sun: vec3<f32>,
) -> AtmosphereLight {
let entry = max(atmosphere_entry(camera, view), 0.0);
let x = camera + entry * view;
let r = clamp(length(x), ATMOSPHERE_BOTTOM, ATMOSPHERE_TOP);
let mu = clamp(dot(x, view) / r, -1.0, 1.0);
let mu_s = clamp(dot(x, sun) / r, -1.0, 1.0);
let nu = clamp(dot(view, sun), -1.0, 1.0);
let d = length(ground - x);
var light: AtmosphereLight;
light.transmittance = atmosphere_transmittance(r, mu, d, true);
let uvwz = atmosphere_scattering_uvwz(r, mu, mu_s, nu, true);
let rayleigh = atmosphere_lookup(atmosphere_rayleigh_table, uvwz);
let mie = atmosphere_lookup(atmosphere_mie_table, uvwz);
let r_p = clamp(sqrt(d * d + 2.0 * r * mu * d + r * r), ATMOSPHERE_BOTTOM, ATMOSPHERE_TOP);
let mu_p = clamp((r * mu + d) / r_p, -1.0, 1.0);
let mu_s_p = clamp((r * mu_s + d * nu) / r_p, -1.0, 1.0);
let uvwz_p = atmosphere_scattering_uvwz(r_p, mu_p, mu_s_p, nu, true);
let rayleigh_p = atmosphere_lookup(atmosphere_rayleigh_table, uvwz_p);
let mie_p = atmosphere_lookup(atmosphere_mie_table, uvwz_p);
let rayleigh_between = max(rayleigh - light.transmittance * rayleigh_p, vec4<f32>(0.0));
// Bruneton's guard against artifacts in the Mie term with the Sun below the
// horizon, where it is all but zero anyway.
let mie_between = max(mie - light.transmittance * mie_p, vec4<f32>(0.0))
* smoothstep(0.0, 0.01, mu_s);
light.scattered = rayleigh_between * atmosphere_rayleigh_phase(nu)
+ mie_between * atmosphere_mie_phase(nu);
return light;
}
// Sunlight on the ground at `point` with the Sun along unit `sun`, as a fraction of
// the Sun's irradiance: x per channel the transmittance toward the Sun (times the
// fraction of its disk above the horizon), y the sky's irradiance on a horizontal
// surface there.
fn atmosphere_ground_light(point: vec3<f32>, sun: vec3<f32>) -> mat2x4<f32> {
let r = clamp(length(point), ATMOSPHERE_BOTTOM, ATMOSPHERE_TOP);
let mu_s = clamp(dot(point, sun) / length(point), -1.0, 1.0);
return mat2x4<f32>(atmosphere_transmittance_to_sun(r, mu_s), atmosphere_sky_irradiance(r, mu_s));
}
+58 -15
View File
@@ -16,7 +16,7 @@ struct Uniforms {
// fx, fy, cx, cy in pixels.
intrinsics: vec4<f32>,
// x: near clip distance. y: 1 if Earth is drawn from the texture. z: 1 if Earth
// and the Moon shadow each other.
// and the Moon shadow each other. w: 1 if Earth is seen through its atmosphere.
params: vec4<f32>,
// xyz: Earth center. w: equatorial radius.
earth_center: vec4<f32>,
@@ -33,8 +33,9 @@ struct Uniforms {
earth_irradiance: vec4<f32>,
// Columns: per-channel albedo of texture red, green and blue reflectance.
earth_rgb: mat3x4<f32>,
// Per channel: albedo added to the texture's (stand-in for the atmosphere).
earth_haze: vec4<f32>,
// xyz: unit vector toward the Sun in Earth-fixed axes stretched along the polar
// axis, the atmosphere's frame.
earth_sun_stretched: vec4<f32>,
// xyz: Moon center. w: radius.
moon_center: vec4<f32>,
moon_sun: vec4<f32>,
@@ -138,23 +139,57 @@ fn read_map(map: texture_2d<f32>, at: MapLookup) -> vec4<f32> {
return textureSampleGrad(map, earth_sampler, at.uv, at.dx, at.dy);
}
// The camera's position relative to Earth's center in the atmosphere's frame, in km.
fn camera_stretched() -> vec3<f32> {
return earth_stretched_km(transpose(u.earth_axes) * (-u.earth_center.xyz), u.earth_shape.x);
}
// Earth's channel radiance at camera-frame point `p`, where its Earth-fixed normal is
// `n_fixed`, with the texture read at `at`.
// `n_fixed`, with the texture read at `at`. Through the atmosphere, the ground is lit
// by the sunlight that reaches it and by the sky, and seen through the air between it
// and the camera, which adds the sunlight it scatters along the way.
fn earth_radiance(p: vec3<f32>, n_fixed: vec3<f32>, at: MapLookup) -> vec4<f32> {
let n = u.earth_axes * n_fixed;
let lit = max(dot(n, u.earth_sun.xyz), 0.0);
var radiance: vec4<f32>;
if u.params.y > 0.5 {
let rgb = read_map(earth_texture, at).rgb;
radiance = u.earth_irradiance * textured_albedo(rgb, u.earth_rgb, u.earth_haze) * lit;
} else {
radiance = u.earth_lambert * lit;
// The Moon's shadow: a solar eclipse. Through the atmosphere it also dims the sky
// and the air between, taken as at the ground point.
var eclipse = vec4<f32>(1.0);
if u.params.z > 0.5 && (lit > 0.0 || u.params.w > 0.5) {
eclipse = sun_visible(p, u.sun_center, u.moon_center);
}
// The Moon's shadow: a solar eclipse.
if u.params.z > 0.5 && lit > 0.0 {
radiance *= sun_visible(p, u.sun_center, u.moon_center);
if u.params.y < 0.5 {
return u.earth_lambert * lit * eclipse;
}
return radiance;
let albedo = textured_albedo(read_map(earth_texture, at).rgb, u.earth_rgb);
if u.params.w < 0.5 {
return u.earth_irradiance * albedo * lit * eclipse;
}
let camera = camera_stretched();
let ground = earth_stretched_km(transpose(u.earth_axes) * (p - u.earth_center.xyz), u.earth_shape.x);
let sun = u.earth_sun_stretched.xyz;
let air = atmosphere_to_ground(camera, normalize(ground - camera), ground, sun);
let light = atmosphere_ground_light(ground, sun);
let surface = u.earth_irradiance * albedo * (light[0] * lit + light[1]);
return (surface * air.transmittance + PI * u.earth_irradiance * air.scattered) * eclipse;
}
// The light Earth's atmosphere scatters toward the camera along `d` where the ray does
// not meet the ground: the glow over the limb. Zero where the nearest body along the
// ray, at distance `nearest`, lies in front of the atmosphere, or without it.
fn earth_glow(d: vec3<f32>, nearest: f32) -> vec4<f32> {
if u.params.w < 0.5 {
return vec4<f32>(0.0);
}
let stretched = transpose(u.earth_axes) * seen(d, 0u) * vec3<f32>(1.0, 1.0, u.earth_shape.x);
let length_s = length(stretched);
let view = stretched / length_s;
let camera = camera_stretched();
let entry = atmosphere_entry(camera, view);
// The entry's distance in meters along the unstretched ray.
if entry < 0.0 || nearest < entry * 1000.0 / length_s {
return vec4<f32>(0.0);
}
return PI * u.earth_irradiance * atmosphere_sky(camera, view, u.earth_sun_stretched.xyz).scattered;
}
// The Moon's channel radiance at camera-frame point `p`, seen along `d`, where its
@@ -263,6 +298,9 @@ fn trace(d: vec3<f32>, spacing: f32) -> Hit {
best = ts;
radiance = sun_radiance(ds);
}
if !(te > 0.0 && te == best) {
radiance += earth_glow(d, best);
}
return Hit(radiance, best);
}
@@ -381,6 +419,11 @@ fn fs_main(@builtin(position) pos: vec4<f32>) -> Out {
best = ts;
radiance = sun_radiance(ds);
}
var glow = vec4<f32>(0.0);
if !(te > 0.0 && te == best) {
glow = earth_glow(d, best);
radiance += glow;
}
let body_depth = select(0.0, u.params.x / (best * d.z), best < FAR);
let pixel_index = vec2<i32>(pos.xy);
@@ -444,7 +487,7 @@ fn fs_main(@builtin(position) pos: vec4<f32>) -> Out {
}
}
}
if best == FAR && mesh_count == 0u {
if best == FAR && mesh_count == 0u && all(glow == vec4<f32>(0.0)) {
discard;
}
let covered = f32(mesh_count) / f32(SAMPLES);
+9 -3
View File
@@ -67,9 +67,15 @@ fn moon_phase_curve(to_sun: vec3<f32>, to_viewer: vec3<f32>) -> f32 {
// Per-channel albedo of Earth's surface from a texel's linear red, green and blue
// reflectance: `rgb_to_channels` folds the RGB basis spectra into the camera's
// channels, and `haze` stands in for the atmosphere.
fn textured_albedo(rgb: vec3<f32>, rgb_to_channels: mat3x4<f32>, haze: vec4<f32>) -> vec4<f32> {
return rgb_to_channels * rgb + haze;
// channels.
fn textured_albedo(rgb: vec3<f32>, rgb_to_channels: mat3x4<f32>) -> vec4<f32> {
return rgb_to_channels * rgb;
}
// Earth-fixed coordinates stretched along the polar axis by `flattening` (equatorial
// over polar radius), where Earth is a sphere: the frame of atmosphere.wgsl, in km.
fn earth_stretched_km(fixed: vec3<f32>, flattening: f32) -> vec3<f32> {
return fixed * vec3<f32>(1.0, 1.0, flattening) * 0.001;
}
// How the solar disk's brightness is spread, per channel (sun.rs, `LimbDarkening`). A
+23 -5
View File
@@ -49,11 +49,10 @@ struct Globals {
earth_albedo: vec4<f32>,
// Columns: per-channel albedo of texture red, green and blue reflectance.
earth_rgb: mat3x4<f32>,
// Per channel: albedo added to the texture's (stand-in for the atmosphere).
earth_haze: vec4<f32>,
moon_albedo: vec4<f32>,
// x: 1 if Earth is textured. y: meters per texel of the texture in latitude, at
// its full size. z: Earth's equatorial / polar radius.
// its full size. z: Earth's equatorial / polar radius. w: 1 if Earth is seen
// through its atmosphere.
params: vec4<f32>,
// Rings and sectors per body, cube face size in texels, number of targets.
grid: vec4<u32>,
@@ -107,7 +106,7 @@ fn earth_surface(t: TargetLight, d: vec3<f32>, te: f32) -> mat2x4<f32> {
let footprint = te * sqrt(t.earth.cone.y / slant);
let level = max(log2(footprint / g.params.y), 0.0);
let rgb = textureSampleLevel(earth_texture, earth_sampler, equirectangular(n_fixed), level).rgb;
albedo = textured_albedo(rgb, g.earth_rgb, g.earth_haze);
albedo = textured_albedo(rgb, g.earth_rgb);
}
return mat2x4<f32>(albedo, vec4<f32>(n, 0.0));
}
@@ -127,6 +126,21 @@ fn moon_albedo(t: TargetLight, d: vec3<f32>, tm: f32, n: vec3<f32>) -> vec4<f32>
return g.moon_color * (reflectance * moon_phase_curve(t.moon.sun.xyz, -d));
}
// Earth's radiance, in units of sunlight / pi, where the ray from the target along `d`
// meets the ground at distance `te`, with albedo `albedo` and the Sun at cosine
// `cos_sun` to the ground's normal: lit by the sunlight that reaches the ground and by
// the sky, and seen through the air, which adds the sunlight it scatters (as the bodies
// pass draws it).
fn through_air(t: TargetLight, d: vec3<f32>, te: f32, albedo: vec4<f32>, cos_sun: f32) -> vec4<f32> {
let to_fixed = transpose(t.earth_axes);
let camera = earth_stretched_km(to_fixed * (-t.earth.center.xyz), g.params.z);
let ground = earth_stretched_km(to_fixed * (te * d - t.earth.center.xyz), g.params.z);
let sun = normalize(to_fixed * t.earth.sun.xyz * vec3<f32>(1.0, 1.0, g.params.z));
let air = atmosphere_to_ground(camera, normalize(ground - camera), ground, sun);
let light = atmosphere_ground_light(ground, sun);
return albedo * (light[0] * cos_sun + light[1]) * air.transmittance + PI * air.scattered;
}
@compute @workgroup_size(64)
fn sources(@builtin(global_invocation_id) id: vec3<u32>) {
let cells = g.grid.x * g.grid.y;
@@ -169,8 +183,12 @@ fn sources(@builtin(global_invocation_id) id: vec3<u32>) {
visible = sun_visible(tm * d, t.sun, t.earth.center);
}
let lit = max(dot(normal, source.sun.xyz), 0.0) * visible;
var light = albedo * lit;
if !is_moon && g.params.w > 0.5 && te > 0.0 && !(tm > 0.0 && tm < te) {
light = through_air(t, d, te, albedo, max(dot(normal, source.sun.xyz), 0.0)) * visible;
}
var out: Sample;
out.light = albedo * lit * (source.sun.w * source.cone.y);
out.light = light * (source.sun.w * source.cone.y);
out.direction = vec4<f32>(d, 0.0);
samples[target_index * 2u * cells + id.x] = out;
}
+227
View File
@@ -0,0 +1,227 @@
//! Earth's atmosphere at lighting L1: what the shaders draw against the tables they
//! read (evaluated in f64 on the CPU), and the limb's glow against the physics, on the
//! GPU and on the CPU (lavapipe).
use crate::common;
use resolved::atmosphere::{AtmosphereTables, BOTTOM_RADIUS, TOP_RADIUS};
use resolved::gpu::AdapterChoice;
use resolved::nalgebra::{UnitQuaternion, Vector3};
use resolved::radiometry::{Radiometry, AU};
use std::f64::consts::PI;
use std::sync::Arc;
use resolved::{
CameraConfig, EarthTexture, Fidelity, LightingTier, Pose, Renderer, RendererOptions, Scene,
};
const ADAPTERS: [AdapterChoice; 2] = [AdapterChoice::Gpu, AdapterChoice::Cpu];
/// Earth's surface: one sRGB gray level, 128, everywhere.
const GRAY: u8 = 128;
fn renderer(adapter: &AdapterChoice) -> Renderer {
let pixels = [GRAY, GRAY, GRAY, 255].repeat(2);
let options = RendererOptions {
adapter: adapter.clone(),
fidelity: Fidelity {
lighting: LightingTier::L1,
..Default::default()
},
earth_texture: Some(Arc::new(EarthTexture::from_rgba8(2, 1, pixels).unwrap())),
moon_texture: Some(common::uniform_moon()),
read_radiance: true,
..Default::default()
};
Renderer::new(CameraConfig::example(), options).unwrap()
}
/// Earth unturned, so Earth-fixed axes are inertial, the Sun along `sun` at 1 AU, and the
/// camera at `camera` looking at `at`. Everything here is in the equatorial plane, where
/// Earth's flattening does not enter.
fn view(sun: Vector3<f64>, camera: Vector3<f64>, at: Vector3<f64>) -> (Scene, Pose) {
let scene = Scene {
earth_attitude: UnitQuaternion::identity(),
sun: sun.normalize() * AU,
moon: Vector3::new(0.0, 0.0, -3.844e8),
moon_attitude: UnitQuaternion::identity(),
targets: vec![],
velocities: Default::default(),
};
(scene, Pose::looking_at(camera, at, Vector3::z()).unwrap())
}
/// The mean of the four pixels around the boresight, whose lines of sight are
/// symmetric about it: to second order, the radiance along the boresight. (One pixel's
/// is half a pixel off, which at the limb moves the tangent point by 140 m and the glow
/// by 1.7 %.)
fn center(frame: &resolved::Frame) -> [f32; 4] {
let radiance = frame.radiance.as_ref().unwrap();
let mut sum = [0.0f32; 4];
for (x, y) in [(959, 539), (960, 539), (959, 540), (960, 540)] {
for (k, v) in radiance[y * 1920 + x].iter().enumerate() {
sum[k] += v / 4.0;
}
}
sum
}
/// Per channel: the Sun's irradiance at 1 AU over pi, and the gray surface's albedo.
fn constants() -> (Radiometry, [f64; 3], [f64; 3]) {
let r = Radiometry::new(&CameraConfig::example());
let v = f64::from(GRAY) / 255.0;
let linear = ((v + 0.055) / 1.055).powf(2.4);
let albedo = r.effective_albedo(&resolved::model::rgb_spectrum([linear; 3]));
let sun = [0, 1, 2].map(|k| r.lambert_radiance(k, 1.0, 1.0, AU));
(r, sun, [albedo[0], albedo[1], albedo[2]])
}
/// Where the ray from `c` along unit `v` meets a sphere of radius `radius` first.
fn hit(c: Vector3<f64>, v: Vector3<f64>, radius: f64) -> f64 {
let b = c.dot(&v);
let q = (c - b * v).norm();
-b - ((radius - q) * (radius + q)).sqrt()
}
/// What atmosphere.wgsl computes for the ground seen from `camera` along unit `view`,
/// with the Sun along unit `sun`, from the tables in f64.
fn ground_reference(
tables: &AtmosphereTables,
camera: Vector3<f64>,
view: Vector3<f64>,
sun: Vector3<f64>,
) -> [f64; 3] {
let (_, irradiance, albedo) = constants();
let x = camera + hit(camera, view, TOP_RADIUS) * view;
let ground = camera + hit(camera, view, BOTTOM_RADIUS) * view;
let r = TOP_RADIUS;
let (mu, mu_s, nu) = (x.dot(&view) / r, x.dot(&sun) / r, view.dot(&sun));
let d = (ground - x).norm();
let r_d = BOTTOM_RADIUS;
let mu_d = (r * mu + d) / r_d;
let mu_s_d = (r * mu_s + d * nu) / r_d;
let (near, far) = (
tables.transmittance(r_d, -mu_d),
tables.transmittance(r, -mu),
);
let s = tables.scattering(r, mu, mu_s, nu, true);
let s_ground = tables.scattering(r_d, mu_d, mu_s_d, nu, true);
let mu_g = ground.dot(&sun) / ground.norm();
let to_sun = tables.transmittance(BOTTOM_RADIUS, mu_g);
let sky = tables.sky_irradiance(BOTTOM_RADIUS, mu_g);
std::array::from_fn(|k| {
let through = (near[k] / far[k]).min(1.0);
let between = (s[k] - through * s_ground[k]).max(0.0);
irradiance[k] * (albedo[k] * (to_sun[k] * mu_g + sky[k]) * through + PI * between)
})
}
fn assert_close(got: [f32; 4], want: [f64; 3], tolerance: f64, context: &str) {
for k in 0..3 {
let error = f64::from(got[k]) / want[k] - 1.0;
assert!(
error.abs() < tolerance,
"{context} channel {k}: {} vs {} ({:+.3} %)",
got[k],
want[k],
100.0 * error
);
}
}
#[test]
fn the_ground_through_the_air_matches_the_tables() {
// The ground at 0 N 0 E seen 60 degrees from its zenith from 20,000 km, with the Sun
// 30 degrees from its zenith on the other side.
let _devices = common::device_lock();
let tables = AtmosphereTables::new(&constants().0);
let ground = Vector3::new(BOTTOM_RADIUS, 0.0, 0.0);
let toward = Vector3::new(60f64.to_radians().cos(), 60f64.to_radians().sin(), 0.0);
let camera = ground + 2e7 * toward;
let sun = Vector3::new(30f64.to_radians().cos(), -(30f64.to_radians().sin()), 0.0);
let want = ground_reference(&tables, camera, -toward, sun);
// The air adds blue and takes red: more scattered than transmitted in blue.
let bare = constants();
let surface = [0, 1, 2].map(|k| bare.1[k] * bare.2[k] * 30f64.to_radians().cos());
assert!(
want[2] > surface[2] && want[0] < 1.3 * surface[0],
"{want:?} {surface:?}"
);
for adapter in &ADAPTERS {
let mut renderer = renderer(adapter);
let (scene, pose) = view(sun, camera, ground);
let frame = renderer.submit(&scene, &pose).unwrap().wait().unwrap();
// Measured within 2.5e-4 on both devices, with the tables in f16.
assert_close(center(&frame), want, 1e-3, &renderer.adapter_info().name);
}
}
/// The light the atmosphere scatters toward a camera looking horizontally past the
/// tangent point `height` above the ground at 0 N 0 E, with the Sun at its zenith:
/// from the renderer's center pixel, and from the tables.
fn limb(renderer: &mut Renderer, tables: &AtmosphereTables, height: f64) -> ([f32; 4], [f64; 3]) {
let tangent = Vector3::new(BOTTOM_RADIUS + height, 0.0, 0.0);
let along = Vector3::y();
let camera = tangent - 2.5e6 * along;
let sun = Vector3::x();
let (scene, pose) = view(sun, camera, tangent);
let frame = renderer.submit(&scene, &pose).unwrap().wait().unwrap();
let x = camera + hit(camera, along, TOP_RADIUS) * along;
let r = TOP_RADIUS;
let s = tables.scattering(
r,
x.dot(&along) / r,
x.dot(&sun) / r,
along.dot(&sun),
false,
);
let irradiance = constants().1;
(
center(&frame),
std::array::from_fn(|k| PI * irradiance[k] * s[k]),
)
}
#[test]
fn the_limb_glows_and_fades_with_the_scale_height() {
let _devices = common::device_lock();
let tables = AtmosphereTables::new(&constants().0);
for adapter in &ADAPTERS {
let mut renderer = renderer(adapter);
let name = renderer.adapter_info().name.clone();
let (low, low_want) = limb(&mut renderer, &tables, 40e3);
let (high, high_want) = limb(&mut renderer, &tables, 50e3);
assert_close(low, low_want, 1e-3, &format!("{name} 40 km"));
assert_close(high, high_want, 1e-3, &format!("{name} 50 km"));
// Thin air seen edge on: the glow is in proportion to the density at the
// tangent point, which falls by e over Rayleigh's 8 km scale height, so 10 km
// higher is e^1.25 = 3.49 times fainter.
let ratio = f64::from(low[1]) / f64::from(high[1]);
assert!((ratio - 3.49).abs() < 0.2, "{name}: {ratio}");
// And blue: air scatters blue light more.
assert!(low[2] > 2.0 * low[0], "{name}: {low:?}");
}
}
#[test]
fn space_beside_the_limb_stays_dark() {
// 150 km above the ground, over the top of the atmosphere: nothing.
let _devices = common::device_lock();
let tables = AtmosphereTables::new(&constants().0);
let mut renderer = renderer(&AdapterChoice::Gpu);
let (glow, _) = limb(&mut renderer, &tables, 150e3);
assert_eq!(glow, [0.0; 4]);
}
#[test]
fn a_camera_inside_the_atmosphere_is_refused() {
let _devices = common::device_lock();
let mut renderer = renderer(&AdapterChoice::Gpu);
let ground = Vector3::new(BOTTOM_RADIUS, 0.0, 0.0);
let (scene, pose) = view(Vector3::x(), ground * (1.0 + 80e3 / BOTTOM_RADIUS), ground);
let error = renderer.submit(&scene, &pose).err().unwrap().to_string();
assert!(
error.contains("80 km up, inside Earth's atmosphere"),
"{error}"
);
}
+12 -29
View File
@@ -12,7 +12,6 @@ use std::sync::Arc;
use resolved::{
CameraConfig, EarthTexture, Fidelity, LightingTier, Pose, Renderer, RendererOptions, Scene,
Spectrum,
};
const ADAPTERS: [AdapterChoice; 2] = [AdapterChoice::Gpu, AdapterChoice::Cpu];
@@ -20,12 +19,13 @@ const ADAPTERS: [AdapterChoice; 2] = [AdapterChoice::Gpu, AdapterChoice::Cpu];
/// The camera's distance from Earth's center: Earth is about 580 px across.
const RANGE: f64 = 2.0e8;
/// A lighting tier L1 renderer drawing Earth from `texture`.
fn renderer(adapter: &AdapterChoice, haze: bool, texture: &EarthTexture) -> Renderer {
let mut appearance = Appearance::default();
if !haze {
appearance.earth_haze = Spectrum::constant(0.0);
}
/// A lighting tier L1 renderer drawing Earth's bare surface from `texture`, without its
/// atmosphere (tests/gpu/atmosphere.rs tests that).
fn renderer(adapter: &AdapterChoice, texture: &EarthTexture) -> Renderer {
let appearance = Appearance {
earth_atmosphere: false,
..Default::default()
};
let options = RendererOptions {
adapter: adapter.clone(),
fidelity: Fidelity {
@@ -78,7 +78,7 @@ fn linear(byte: u8) -> f64 {
}
/// Channel radiance of Earth's surface with sRGB-encoded reflectance `rgb`, facing the
/// Sun at 1 AU, without haze.
/// Sun at 1 AU.
fn expected(rgb: [u8; 3]) -> [f64; 3] {
let r = Radiometry::new(&CameraConfig::example());
let albedo = r.effective_albedo(&rgb_spectrum([rgb[0], rgb[1], rgb[2]].map(linear)));
@@ -133,7 +133,7 @@ fn texel_under_the_camera_sets_the_radiance() {
// Over 45 N 45 E the camera sees the middle of region 2 (longitude 0..90, north).
let _devices = common::device_lock();
for adapter in &ADAPTERS {
let mut renderer = renderer(adapter, false, &regions());
let mut renderer = renderer(adapter, &regions());
let (scene, camera) = view(45.0, 45.0, UnitQuaternion::identity());
let frame = renderer.submit(&scene, &camera).unwrap().wait().unwrap();
assert_close(
@@ -151,7 +151,7 @@ fn earth_rotation_moves_the_surface() {
let _devices = common::device_lock();
let turn = UnitQuaternion::from_axis_angle(&Vector3::z_axis(), 90f64.to_radians());
for adapter in &ADAPTERS {
let mut renderer = renderer(adapter, false, &regions());
let mut renderer = renderer(adapter, &regions());
for (lat, region) in [(45.0, 1), (-45.0, 5)] {
let (scene, camera) = view(lat, 45.0, turn);
let frame = renderer.submit(&scene, &camera).unwrap().wait().unwrap();
@@ -165,23 +165,6 @@ fn earth_rotation_moves_the_surface() {
}
}
#[test]
fn haze_adds_its_spectrum() {
let _devices = common::device_lock();
let r = Radiometry::new(&CameraConfig::example());
let haze = r.effective_albedo(&Appearance::default().earth_haze);
let surface = r.effective_albedo(&rgb_spectrum(REGIONS[2].map(linear)));
let want = [0, 1, 2].map(|k| r.lambert_radiance(k, surface[k] + haze[k], 1.0, AU));
// Bluer haze: it raises blue the most.
assert!(haze[2] > haze[1] && haze[1] > haze[0], "{haze:?}");
for adapter in &ADAPTERS {
let mut renderer = renderer(adapter, true, &regions());
let (scene, camera) = view(45.0, 45.0, UnitQuaternion::identity());
let frame = renderer.submit(&scene, &camera).unwrap().wait().unwrap();
assert_close(center(&frame), want, &renderer.adapter_info().name);
}
}
#[test]
fn no_seam_at_180_degrees() {
// The four texel columns either side of 180 degrees (22.5 degrees each way) are one
@@ -206,7 +189,7 @@ fn no_seam_at_180_degrees() {
let texture = EarthTexture::from_rgba8(w, h, pixels).unwrap();
let want = expected(edge);
for adapter in &ADAPTERS {
let mut renderer = renderer(adapter, false, &texture);
let mut renderer = renderer(adapter, &texture);
// Over 179.8 E the seam is about one pixel right of center, inside a 2 x 2 pixel
// quad (screen-space derivatives are taken per quad). Over exactly 180 it would
// fall on a quad boundary, where even unhandled it shows nothing.
@@ -251,7 +234,7 @@ fn far_views_average_rather_than_alias() {
// 50 % linear reflectance is sRGB 188.
let gray = EarthTexture::from_rgba8(1, 1, vec![188, 188, 188, 255]).unwrap();
for adapter in &ADAPTERS {
let mut renderer = renderer(adapter, false, &gray);
let mut renderer = renderer(adapter, &gray);
let (scene, mut camera) = view(10.0, 30.0, UnitQuaternion::identity());
camera.position *= 2.0;
let mut block = |texture: &EarthTexture| {
+3 -3
View File
@@ -53,8 +53,8 @@ fn plate() -> Mesh {
Mesh::from_stl(&mut Cursor::new(s), "plate", &options).unwrap()
}
/// What a test varies about the bodies: Earth's texture (one sRGB gray level, no haze)
/// and the Moon's albedo.
/// What a test varies about the bodies: Earth's texture (one sRGB gray level, without
/// the atmosphere) and the Moon's albedo.
#[derive(Clone, Copy)]
struct Surfaces {
earth_level: u8,
@@ -69,7 +69,7 @@ impl Surfaces {
fn appearance(&self) -> Appearance {
Appearance {
earth_haze: Spectrum::constant(0.0),
earth_atmosphere: false,
moon_albedo: Spectrum::constant(self.moon_albedo),
moon_color: Spectrum::constant(1.0),
..Default::default()
+1
View File
@@ -6,6 +6,7 @@ mod common;
mod analytic;
mod apparent;
mod atmosphere;
mod blooming;
mod bodies;
mod convolve;