use bevy::{
asset::{AssetPath, RenderAssetUsages, embedded_asset, embedded_path},
ecs::system::SystemParam,
image::{
CompressedImageFormats, ImageAddressMode, ImageFilterMode, ImageSampler,
ImageSamplerDescriptor, ImageType,
},
prelude::*,
render::render_resource::{
AsBindGroup, Extent3d, ShaderType, TextureDimension, TextureFormat, TextureUsages,
TextureViewDescriptor, TextureViewDimension,
},
shader::ShaderRef,
};
use crate::fields::FieldParams;
use crate::{
AquaDebug, AquaSettings, OceanWaves, ViewDetail, ViewPos, ViewSeaLevel, WaterOptics, bed,
};
pub use bevy_aqua_geom::{LOD_COUNT, TILE_RESOLUTION};
pub const BASE_SCALE: f32 = 24.0;
const LOD_SCALE_MULTIPLIER: f32 = 2.0;
pub const RESOLUTION: u32 = 256;
const COVERAGE_MULTIPLIER: f32 = 4.0;
const CASCADE_COUNT: usize = LOD_COUNT + 1;
const MAX_WAVELENGTH_TEXELS: f32 = 4.0;
const DEBUG_MODE_WATER_PATH: f32 = 1.0;
const DEBUG_MODE_REFRACTION_VALIDITY: f32 = 2.0;
const DEBUG_MODE_TRANSMISSION: f32 = 3.0;
const DEBUG_MODE_UNREFRACTED: f32 = 4.0;
const DEBUG_MODE_BEER_LAMBERT: f32 = 5.0;
const DEBUG_MODE_SEA_FLOOR: f32 = 6.0;
const DEBUG_MODE_BEAUTY: f32 = 7.0;
const DEBUG_MODE_REFLECTION: f32 = 8.0;
const DEBUG_MODE_FOAM: f32 = 9.0;
const DEBUG_MODE_WAVE_HEIGHT: f32 = 10.0;
const DEBUG_MODE_LIGHT_RADIANCE: f32 = 11.0;
const DEBUG_MODE_REFLECTION_FRACTION: f32 = 12.0;
const DEBUG_MODE_FAR_TIER: f32 = 13.0;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Cascade {
pub center: Vec2,
pub scale: f32,
pub texel_width: f32,
}
#[derive(Resource, Debug, Clone, bevy::render::extract_resource::ExtractResource)]
pub struct Data {
material: Handle<CascadeMaterial>,
texture: Handle<Image>,
fft_surface: Handle<Image>,
layout: GpuLayout,
}
impl Data {
pub fn new(
material: Handle<CascadeMaterial>,
texture: Handle<Image>,
fft_surface: Handle<Image>,
layout: GpuLayout,
) -> Self {
Self {
material,
texture,
fft_surface,
layout,
}
}
pub fn material(&self) -> Handle<CascadeMaterial> {
self.material.clone()
}
pub fn texture(&self) -> Handle<Image> {
self.texture.clone()
}
pub fn fft_surface(&self) -> Handle<Image> {
self.fft_surface.clone()
}
pub fn layout(&self) -> &GpuLayout {
&self.layout
}
}
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
pub struct CascadeMaterial {
#[texture(0, dimension = "2d_array")]
#[sampler(1)]
pub texture: Handle<Image>,
#[uniform(2)]
pub layout: GpuLayout,
#[uniform(3)]
pub surface: SurfaceParams,
#[texture(4, dimension = "2d")]
pub sea_floor: Handle<Image>,
#[texture(5)]
#[sampler(6)]
pub detail_normal: Handle<Image>,
#[texture(7, dimension = "2d_array")]
#[sampler(8)]
pub foam: Handle<Image>,
#[texture(9)]
#[sampler(10)]
pub foam_pattern: Handle<Image>,
#[texture(11, dimension = "2d_array")]
pub fft_surface: Handle<Image>,
#[uniform(15)]
pub fields: FieldParams,
#[texture(16)]
#[sampler(17)]
pub field_level_id: Handle<Image>,
#[texture(18)]
pub field_flow: Handle<Image>,
#[texture(19)]
#[sampler(20)]
pub reflection_a: Handle<Image>,
#[texture(21)]
pub reflection_b: Handle<Image>,
#[uniform(22)]
pub reflections: PlanarReflectionParams,
#[texture(23)]
#[sampler(24)]
pub caustics: Handle<Image>,
}
#[derive(ShaderType, Debug, Clone, Copy, PartialEq)]
pub struct PlanarReflectionView {
pub view_projection: Mat4,
pub level: f32,
}
#[derive(ShaderType, Debug, Clone, Copy, PartialEq)]
pub struct PlanarReflectionParams {
pub views: [PlanarReflectionView; 2],
pub view_count: u32,
pub distortion: f32,
}
impl Default for PlanarReflectionParams {
fn default() -> Self {
Self {
views: [PlanarReflectionView {
view_projection: Mat4::IDENTITY,
level: 0.0,
}; 2],
view_count: 0,
distortion: 0.0,
}
}
}
impl Material for CascadeMaterial {
fn vertex_shader() -> ShaderRef {
shader_ref()
}
fn fragment_shader() -> ShaderRef {
shader_ref()
}
fn reads_view_transmission_texture(&self) -> bool {
true
}
}
#[derive(ShaderType, Debug, Clone, Copy, PartialEq)]
pub struct BodyParams {
pub(crate) flags: Vec4,
extent: Vec4,
aabb_min: Vec4,
aabb_size: Vec4,
optics_a: Vec4,
optics_b: Vec4,
}
impl BodyParams {
pub const fn ocean() -> Self {
Self {
flags: Vec4::ZERO,
extent: Vec4::ZERO,
aabb_min: Vec4::ZERO,
aabb_size: Vec4::ZERO,
optics_a: Vec4::ZERO,
optics_b: Vec4::ZERO,
}
}
pub const fn bounded(
center: Vec2,
radius: f32,
aabb_min: Vec2,
aabb_size: Vec2,
has_flow: bool,
optics: Option<BodyOptics>,
) -> Self {
let (extinction, scale, roughness, schlick, enabled) = match optics {
Some(optics) => (
optics.extinction,
optics.scatter_scale,
optics.sun_roughness,
1.0,
1.0,
),
None => (Vec3::ZERO, 1.0, -1.0, 0.0, 0.0),
};
Self {
flags: Vec4::new(1.0, if has_flow { 1.0 } else { 0.0 }, 0.0, 0.0),
extent: Vec4::new(center.x, center.y, 0.0, radius),
aabb_min: Vec4::new(aabb_min.x, aabb_min.y, 0.0, 0.0),
aabb_size: Vec4::new(aabb_size.x, aabb_size.y, 0.0, 0.0),
optics_a: Vec4::new(extinction.x, extinction.y, extinction.z, enabled),
optics_b: Vec4::new(scale, roughness, schlick, 0.0),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BodyOptics {
pub extinction: Vec3,
pub scatter_scale: f32,
pub sun_roughness: f32,
}
#[derive(ShaderType, Debug, Clone, Copy, PartialEq)]
pub struct SurfaceParams {
pub deep_color: Vec4,
pub grazing_color: Vec4,
pub shallow_color: Vec4,
pub fresnel: Vec4,
pub reflection: Vec4,
pub sun: Vec4,
pub debug: Vec4,
pub fog_density: Vec4,
pub sea_floor: Vec4,
pub sss_tint: Vec4,
pub sss: Vec4,
pub detail: Vec4,
pub capillary: Vec4,
pub foam: Vec4,
pub advection: Vec4,
pub far_tier: Vec4,
pub caustics: Vec4,
}
impl SurfaceParams {
pub fn apply_optics(&mut self, optics: &WaterOptics) {
self.deep_color = optics.deep_color.extend(1.0);
self.grazing_color = optics.grazing_color.extend(1.0);
self.shallow_color = optics.shallow_color.extend(7.0);
self.fog_density = optics.extinction.extend(0.0);
self.sss_tint = optics.sss_tint.extend(0.0);
}
}
impl Default for SurfaceParams {
fn default() -> Self {
Self {
deep_color: Vec4::new(0.0, 0.002_695_407_3, 0.169_811_31, 1.0),
grazing_color: Vec4::new(0.0, 0.003_921_569, 0.168_627_4, 1.0),
shallow_color: Vec4::new(0.012, 0.13, 0.115, 7.0),
fresnel: Vec4::new(0.020_373_19, 5.0, 1.0, 0.0),
reflection: Vec4::new(0.0, 1.0, 10_000.0, 0.28),
sun: Vec4::new(1.0, 0.4, 0.0, 1.0),
debug: Vec4::new(0.0, 0.5, 32.0, 0.0),
fog_density: Vec4::new(0.9, 0.3, 0.35, 0.0),
sea_floor: Vec4::new(32.0, 10.0, 1.0, 0.0),
sss_tint: Vec4::new(0.088_506_84, 0.497, 0.456_150_74, 0.0),
sss: Vec4::new(0.0, 1.7, 5.0, 1.0),
detail: Vec4::new(40.0, 0.08, 1.0, 1.0),
capillary: Vec4::new(16.0, 0.08, 30.0, 50.0),
foam: Vec4::new(10.0, 0.4, 1.35, 1.0),
advection: Vec4::ZERO,
far_tier: Vec4::new(320.0, 512.0, 0.0, 0.0),
caustics: Vec4::ZERO,
}
}
}
#[derive(ShaderType, Debug, Default, Clone, Copy, PartialEq)]
pub struct GpuCascade {
pub center: Vec2,
pub scale: f32,
pub texture_res: f32,
pub inv_texture_res: f32,
pub texel_width: f32,
pub weight: f32,
pub max_wavelength: f32,
}
#[derive(ShaderType, Debug, Clone)]
pub struct GpuLayout {
pub cascades: [GpuCascade; CASCADE_COUNT],
pub center: Vec4,
pub bed_transform: Vec4,
pub bed_range: Vec4,
}
impl GpuLayout {
pub fn new(cascades: &[Cascade; LOD_COUNT], center: Vec2, detail_lod: f32) -> Self {
let mut gpu = [GpuCascade::default(); CASCADE_COUNT];
for (target, source) in gpu.iter_mut().zip(cascades) {
*target = GpuCascade {
center: source.center,
scale: source.scale,
texture_res: RESOLUTION as f32,
inv_texture_res: (RESOLUTION as f32).recip(),
texel_width: source.texel_width,
weight: 1.0,
max_wavelength: MAX_WAVELENGTH_TEXELS * source.texel_width,
};
}
gpu[LOD_COUNT] = gpu[LOD_COUNT - 1];
gpu[LOD_COUNT].weight = 0.0;
Self {
cascades: gpu,
center: center.extend(detail_lod).extend(LOD_COUNT as f32),
bed_transform: Vec4::ZERO,
bed_range: Vec4::new(0.0, bed::NO_BED_SPAN, 0.0, 0.0),
}
}
pub fn set_bed(&mut self, bed: Option<&bed::BedHeightMap>, sea_level: f32) {
match bed {
Some(map) => {
self.bed_transform = map
.origin
.extend(map.size.x.max(f32::MIN_POSITIVE).recip())
.extend(map.size.y.max(f32::MIN_POSITIVE).recip());
self.bed_range = Vec4::new(
map.height_range[0],
(map.height_range[1] - map.height_range[0]).max(f32::MIN_POSITIVE),
sea_level,
0.0,
);
}
None => {
self.bed_transform = Vec4::ZERO;
self.bed_range = Vec4::new(0.0, bed::NO_BED_SPAN, sea_level, 0.0);
}
}
}
}
fn shader_ref() -> ShaderRef {
ShaderRef::Path(
AssetPath::from_path_buf(embedded_path!("cascade/material.wgsl")).with_source("embedded"),
)
}
#[derive(Resource)]
struct ShaderLibraries {
_handles: Vec<Handle<Shader>>,
}
pub fn add_shader(app: &mut App) {
embedded_asset!(app, "cascade/common.wgsl");
embedded_asset!(app, "cascade/river.wgsl");
embedded_asset!(app, "cascade/deform.wgsl");
embedded_asset!(app, "cascade/types.wgsl");
embedded_asset!(app, "cascade/material.wgsl");
let server = app.world().resource::<AssetServer>();
let handles = vec![
server.load("embedded://bevy_aqua_core/cascade/common.wgsl"),
server.load("embedded://bevy_aqua_core/cascade/river.wgsl"),
server.load("embedded://bevy_aqua_core/cascade/deform.wgsl"),
server.load("embedded://bevy_aqua_core/cascade/types.wgsl"),
];
app.insert_resource(ShaderLibraries { _handles: handles });
bevy_aqua_optics::add_shader(app);
}
#[derive(SystemParam, Debug)]
pub struct UpdateInputs<'w> {
pub view: Res<'w, ViewPos>,
pub detail: Res<'w, ViewDetail>,
pub debug: Res<'w, AquaDebug>,
pub settings: Res<'w, AquaSettings>,
pub caustic_sun: Res<'w, crate::CausticsSunVisibility>,
pub waves: Res<'w, OceanWaves>,
pub sea_level: Res<'w, ViewSeaLevel>,
pub bed: Option<Res<'w, crate::bed::BedHeightMap>>,
}
pub fn update(
inputs: UpdateInputs,
mut data: ResMut<Data>,
mut materials: ResMut<Assets<CascadeMaterial>>,
) {
let UpdateInputs {
view,
detail,
debug,
settings,
caustic_sun,
waves,
sea_level,
bed,
} = inputs;
if !view.is_changed()
&& !detail.is_changed()
&& !debug.is_changed()
&& !settings.is_changed()
&& !caustic_sun.is_changed()
&& !waves.is_changed()
&& !sea_level.is_changed()
&& !bed
.as_ref()
.map(bevy::ecs::change_detection::DetectChanges::is_changed)
.unwrap_or(false)
{
return;
}
let mut layout = GpuLayout::new(&layout(view.0), view.0, detail.0);
layout.set_bed(bed.as_deref(), sea_level.0);
data.layout = layout.clone();
let apply_globals = |material: &mut CascadeMaterial| {
material.surface.apply_optics(&settings.water_optics);
material.surface.debug.x = match *debug {
AquaDebug::Shaded | AquaDebug::ShallowComposite => DEBUG_MODE_BEAUTY,
AquaDebug::ReflectionSanity => DEBUG_MODE_REFLECTION,
AquaDebug::FoamDensity | AquaDebug::FoamDensityBilinear => DEBUG_MODE_FOAM,
AquaDebug::WaveHeight => DEBUG_MODE_WAVE_HEIGHT,
AquaDebug::LightRadiance => DEBUG_MODE_LIGHT_RADIANCE,
AquaDebug::ReflectionFraction => DEBUG_MODE_REFLECTION_FRACTION,
AquaDebug::FarTier => DEBUG_MODE_FAR_TIER,
AquaDebug::WaterPath => DEBUG_MODE_WATER_PATH,
AquaDebug::RefractionValidity => DEBUG_MODE_REFRACTION_VALIDITY,
AquaDebug::Transmission => DEBUG_MODE_TRANSMISSION,
AquaDebug::TransmissionUnrefracted => DEBUG_MODE_UNREFRACTED,
AquaDebug::BeerLambert => DEBUG_MODE_BEER_LAMBERT,
AquaDebug::SeaFloorDepth => DEBUG_MODE_SEA_FLOOR,
};
material.surface.debug.w = if *debug == AquaDebug::FoamDensityBilinear {
1.0
} else {
0.0
};
material.surface.reflection.x = if waves.model == crate::WaveModel::Spectral {
1.0
} else {
0.0
};
let detail = settings.detail_strength.clamp(0.0, 2.0);
material.surface.reflection.y = (detail * 12.5).clamp(0.0, 2.0);
material.surface.sun.z = if settings.atmospheric_sunlight {
1.0
} else {
0.0
};
material.surface.detail.y = detail;
material.surface.capillary.y = detail.min(0.5);
material.surface.advection = Vec4::new(waves.flow.x, waves.flow.y, 0.0, 0.0);
let far_start = settings.far_tier_start.max(0.0);
let far_end = settings.far_tier_end.max(far_start + 1.0);
material.surface.far_tier = Vec4::new(far_start, far_end, 0.0, 0.0);
material.surface.sea_floor.w = caustic_sun.0.clamp(0.0, 1.0);
material.surface.caustics = settings.caustics.map_or(Vec4::ZERO, |caustics| {
Vec4::new(
caustics.strength.max(0.0),
caustics.scale.max(0.01),
caustics.speed,
caustics.depth_max.max(0.0),
)
});
};
{
let mut material = materials
.get_mut(&data.material)
.expect("Aqua's cascade material must remain loaded");
material.layout = layout.clone();
if settings.caustics.is_some()
&& let Some(bed) = bed.as_deref()
{
material.sea_floor = bed.image.clone();
}
apply_globals(&mut material);
}
}
pub fn layout(camera: Vec2) -> [Cascade; LOD_COUNT] {
std::array::from_fn(|lod| {
let scale = lod_scale(lod);
let texel_width = COVERAGE_MULTIPLIER * scale / RESOLUTION as f32;
let center = (camera / texel_width).floor() * texel_width;
Cascade {
center,
scale,
texel_width,
}
})
}
pub fn make_texture() -> Image {
make_array_texture(LOD_COUNT as u32)
}
pub fn make_fft_surface_texture() -> Image {
make_array_texture(LOD_COUNT as u32)
}
fn make_array_texture(layers: u32) -> Image {
let bytes_per_pixel = TextureFormat::Rgba16Float
.block_copy_size(None)
.expect("Rgba16Float must have a fixed block size") as usize;
let pixel_count = RESOLUTION as usize * RESOLUTION as usize * layers as usize;
let mut image = Image::new(
Extent3d {
width: RESOLUTION,
height: RESOLUTION,
depth_or_array_layers: layers,
},
TextureDimension::D2,
vec![0; pixel_count * bytes_per_pixel],
TextureFormat::Rgba16Float,
RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
);
image.texture_descriptor.usage = TextureUsages::COPY_DST
| TextureUsages::COPY_SRC
| TextureUsages::STORAGE_BINDING
| TextureUsages::TEXTURE_BINDING;
image.texture_view_descriptor = Some(TextureViewDescriptor {
dimension: Some(TextureViewDimension::D2Array),
..default()
});
image.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor {
address_mode_u: ImageAddressMode::ClampToEdge,
address_mode_v: ImageAddressMode::ClampToEdge,
address_mode_w: ImageAddressMode::ClampToEdge,
mag_filter: ImageFilterMode::Linear,
min_filter: ImageFilterMode::Linear,
mipmap_filter: ImageFilterMode::Nearest,
..default()
});
image
}
const DETAIL_NORMAL_SIZE: u32 = 1024;
const DETAIL_NORMAL_BYTES: &[u8] = include_bytes!("../assets/WaveNormals.png");
pub fn make_detail_normal_texture() -> Image {
let sampler = ImageSampler::Descriptor(ImageSamplerDescriptor {
address_mode_u: ImageAddressMode::Repeat,
address_mode_v: ImageAddressMode::Repeat,
mag_filter: ImageFilterMode::Linear,
min_filter: ImageFilterMode::Linear,
mipmap_filter: ImageFilterMode::Linear,
..default()
});
let mut image = Image::from_buffer(
DETAIL_NORMAL_BYTES,
ImageType::Extension("png"),
CompressedImageFormats::NONE,
false,
sampler,
RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
)
.expect("bundled Crest WaveNormals.png must decode");
assert_eq!(image.texture_descriptor.size.width, DETAIL_NORMAL_SIZE);
assert_eq!(image.texture_descriptor.size.height, DETAIL_NORMAL_SIZE);
assert_eq!(image.texture_descriptor.format, TextureFormat::Rgba8Unorm);
let mut levels = Vec::new();
let mut size = DETAIL_NORMAL_SIZE;
let mut previous = image
.data
.take()
.expect("decoded Crest normal map must have CPU pixels");
encode_detail_moments(&mut previous);
loop {
levels.extend_from_slice(&previous);
if size == 1 {
break;
}
previous = downsample_detail_normals(&previous, size);
size /= 2;
}
image.data = Some(levels);
image.texture_descriptor.mip_level_count = DETAIL_NORMAL_SIZE.ilog2() + 1;
image.texture_descriptor.usage = TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING;
image
}
fn downsample_detail_normals(source: &[u8], source_size: u32) -> Vec<u8> {
let target_size = source_size / 2;
let mut target = Vec::with_capacity((target_size * target_size * 4) as usize);
for y in 0..target_size {
for x in 0..target_size {
let mut slope = Vec2::ZERO;
let mut second_moment = 0.0;
for offset_y in 0..2 {
for offset_x in 0..2 {
let source_x = 2 * x + offset_x;
let source_y = 2 * y + offset_y;
let index = ((source_y * source_size + source_x) * 4) as usize;
slope += Vec2::new(
source[index] as f32 / 127.5 - 1.0,
source[index + 1] as f32 / 127.5 - 1.0,
);
second_moment += 2.0 * source[index + 2] as f32 / 255.0;
}
}
slope *= 0.25;
second_moment *= 0.25;
if slope.length_squared() > 1.0 {
slope = slope.normalize();
}
target.extend_from_slice(&encode_detail_normal(slope, second_moment));
}
}
target
}
fn encode_detail_moments(pixels: &mut [u8]) {
let (pixels, remainder) = pixels.as_chunks_mut::<4>();
debug_assert!(remainder.is_empty());
for pixel in pixels {
let slope = Vec2::new(pixel[0] as f32 / 127.5 - 1.0, pixel[1] as f32 / 127.5 - 1.0);
pixel[2] = encode_detail_second_moment(slope.length_squared());
pixel[3] = 255;
}
}
fn encode_detail_normal(slope: Vec2, second_moment: f32) -> [u8; 4] {
let encoded = ((slope.clamp(Vec2::splat(-1.0), Vec2::ONE) + Vec2::ONE) * 127.5).round();
[
encoded.x as u8,
encoded.y as u8,
encode_detail_second_moment(second_moment),
255,
]
}
fn encode_detail_second_moment(value: f32) -> u8 {
(value.clamp(0.0, 2.0) * 127.5).round() as u8
}
pub fn lod_scale(lod: usize) -> f32 {
BASE_SCALE * LOD_SCALE_MULTIPLIER.powi(lod as i32)
}
#[cfg(test)]
#[path = "cascade_tests.rs"]
mod tests;