Atmosphere
This commit is contained in:
1 parent
a3f62301f4
commit
4b1198a351
21 files changed
+2253
-204
No files matched your search
@@ -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
@@ -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::*;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,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, ®ions());
|
||||
let mut renderer = renderer(adapter, ®ions());
|
||||
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, ®ions());
|
||||
let mut renderer = renderer(adapter, ®ions());
|
||||
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, ®ions());
|
||||
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| {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -6,6 +6,7 @@ mod common;
|
||||
|
||||
mod analytic;
|
||||
mod apparent;
|
||||
mod atmosphere;
|
||||
mod blooming;
|
||||
mod bodies;
|
||||
mod convolve;
|
||||
|
||||
Reference in new issue
Block a user