use std::collections::HashSet;
use std::f32::consts::TAU;
use bevy::asset::{AssetId, Assets, Handle};
use bevy::camera::visibility::RenderLayers;
use bevy::camera::{
Camera, Camera3d, CameraOutputMode, ClearColorConfig, OrthographicProjection, Projection,
RenderTarget, ScalingMode, Viewport,
};
use bevy::color::Color;
use bevy::core_pipeline::tonemapping::DebandDither;
use bevy::ecs::prelude::*;
use bevy::image::Image;
use bevy::log::warn;
use bevy::math::{Quat, UVec2, Vec3};
use bevy::mesh::{Mesh, Mesh3d};
use bevy::pbr::MeshMaterial3d;
use bevy::render::render_resource::{BlendState, TextureFormat};
use bevy::render::view::screenshot::{Screenshot, ScreenshotCaptured};
use bevy::render::view::{Msaa, Tonemapping};
use bevy::transform::components::Transform;
use super::material::ImpostorBakeMaterial;
use super::{
IMPOSTOR_VIEWS, ImpostorAtlas, ImpostorFrame, ImpostorMaterial, TILE_PIXELS, atlas_image,
linearize_surface, surface_image,
};
use crate::pool::{PoolVariant, TreePool};
use crate::shading::{FloraUniform, FloraWindBuffer};
const BAKE_LAYER: usize = 31;
const BAKE_SURFACE_LAYER: usize = 30;
const SETTLE_FRAMES: u32 = 6;
const MAX_ATTEMPTS: u32 = 60;
const MAX_CONCURRENT_BAKES: usize = 4;
const SPACING: f32 = 400.0;
const BAKE_ORIGIN: Vec3 = Vec3::new(0.0, 0.0, 30_000.0);
const CAMERA_DISTANCE: f32 = 100.0;
const MIN_COVERAGE: f32 = 0.01;
#[derive(Resource, Default)]
pub(crate) struct ImpostorBakes {
jobs: Vec<Job>,
settled: HashSet<(AssetId<TreePool>, usize)>,
started: u32,
}
impl ImpostorBakes {
pub(crate) fn is_settled(&self, pool: AssetId<TreePool>, variant: usize) -> bool {
self.settled.contains(&(pool, variant))
}
}
#[derive(Resource, Default)]
pub(crate) struct ImpostorsBaked(pub(crate) u32);
struct Job {
pool: AssetId<TreePool>,
variant: usize,
frame: ImpostorFrame,
target: Handle<Image>,
scene: Vec<Entity>,
frames: u32,
capture: Option<Entity>,
attempts: u32,
}
#[derive(Component)]
struct BakeShot {
pool: AssetId<TreePool>,
variant: usize,
}
struct BakeMaterials {
wood_colour: Handle<ImpostorBakeMaterial>,
leaf_colour: Handle<ImpostorBakeMaterial>,
wood_surface: Handle<ImpostorBakeMaterial>,
leaf_surface: Handle<ImpostorBakeMaterial>,
}
pub(crate) fn start_impostor_bakes(
mut commands: Commands,
pools: Res<Assets<TreePool>>,
mut bakes: ResMut<ImpostorBakes>,
mut images: ResMut<Assets<Image>>,
mut bake_materials: ResMut<Assets<ImpostorBakeMaterial>>,
) {
for (pool_id, pool) in pools.iter() {
if !pool.lod.distances.impostor.is_finite() {
continue;
}
for (index, variant) in pool.variants.iter().enumerate() {
let busy = bakes
.jobs
.iter()
.any(|j| j.pool == pool_id && j.variant == index);
if variant.impostor.is_some() || busy || bakes.is_settled(pool_id, index) {
continue;
}
if bakes.jobs.len() >= MAX_CONCURRENT_BAKES {
return;
}
let target = images.add(Image::new_target_texture(
IMPOSTOR_VIEWS * TILE_PIXELS,
2 * TILE_PIXELS,
TextureFormat::Rgba8Unorm,
Some(TextureFormat::Rgba8UnormSrgb),
));
let frame = ImpostorFrame::new(variant.height, variant.radius);
let slot = bakes.started;
bakes.started += 1;
let wood = pool.params.trunk_color.to_linear();
let leaf = pool.params.foliage.color.to_linear();
let materials = BakeMaterials {
wood_colour: bake_materials.add(ImpostorBakeMaterial::colour(wood)),
leaf_colour: bake_materials.add(ImpostorBakeMaterial::colour(leaf)),
wood_surface: bake_materials.add(ImpostorBakeMaterial::surface(wood, 0.0)),
leaf_surface: bake_materials.add(ImpostorBakeMaterial::surface(leaf, 1.0)),
};
let scene = spawn_scene(&mut commands, variant, &frame, &target, &materials, slot);
bakes.jobs.push(Job {
pool: pool_id,
variant: index,
frame,
target,
scene,
frames: 0,
capture: None,
attempts: 0,
});
}
}
}
fn spawn_scene(
commands: &mut Commands,
variant: &PoolVariant,
frame: &ImpostorFrame,
target: &Handle<Image>,
materials: &BakeMaterials,
slot: u32,
) -> Vec<Entity> {
let mut scene = Vec::new();
let origin = BAKE_ORIGIN + Vec3::Z * (slot as f32 * SPACING);
for view in 0..IMPOSTOR_VIEWS {
let yaw = view as f32 / IMPOSTOR_VIEWS as f32 * TAU;
let spot = origin + Vec3::X * (view as f32 * SPACING);
let turned = Transform::from_translation(spot).with_rotation(Quat::from_rotation_y(-yaw));
let look_at = spot + Vec3::Y * frame.center_y;
let transparent = ClearColorConfig::Custom(Color::NONE);
for (row, layer, wood, leaf) in [
(0u32, BAKE_LAYER, &materials.wood_colour, &materials.leaf_colour),
(1u32, BAKE_SURFACE_LAYER, &materials.wood_surface, &materials.leaf_surface),
] {
let layers = RenderLayers::layer(layer);
scene.push(
commands
.spawn((
Mesh3d(variant.lods[1].clone()),
MeshMaterial3d(wood.clone()),
turned,
layers.clone(),
))
.id(),
);
if let Some(leaves) = &variant.leaves[1] {
scene.push(
commands
.spawn((
Mesh3d(leaves.clone()),
MeshMaterial3d(leaf.clone()),
turned,
layers.clone(),
))
.id(),
);
}
scene.push(
commands
.spawn((
Camera3d::default(),
Camera {
order: -10_000
+ (slot * 2 * IMPOSTOR_VIEWS + row * IMPOSTOR_VIEWS + view) as isize,
viewport: Some(Viewport {
physical_position: UVec2::new(view * TILE_PIXELS, row * TILE_PIXELS),
physical_size: UVec2::splat(TILE_PIXELS),
..Default::default()
}),
clear_color: transparent,
output_mode: CameraOutputMode::Write {
blend_state: Some(BlendState::REPLACE),
clear_color: transparent,
},
..Default::default()
},
RenderTarget::Image(target.clone().into()),
Projection::Orthographic(OrthographicProjection {
scaling_mode: ScalingMode::Fixed {
width: frame.view.x,
height: frame.view.y,
},
..OrthographicProjection::default_3d()
}),
Transform::from_translation(
look_at + ImpostorFrame::toward_camera() * CAMERA_DISTANCE,
)
.looking_at(look_at, Vec3::Y),
Msaa::Off,
Tonemapping::None,
DebandDither::Disabled,
layers,
))
.id(),
);
}
}
scene
}
pub(crate) fn drive_impostor_bakes(
mut commands: Commands,
pools: Res<Assets<TreePool>>,
mut bakes: ResMut<ImpostorBakes>,
) {
let bakes = &mut *bakes;
let mut gone = Vec::new();
for (index, job) in bakes.jobs.iter_mut().enumerate() {
if pools.get(job.pool).is_none() {
gone.push(index);
continue;
}
if job.capture.is_none() {
job.frames += 1;
if job.frames >= SETTLE_FRAMES {
let shot = BakeShot {
pool: job.pool,
variant: job.variant,
};
let entity = commands
.spawn((Screenshot::image(job.target.clone()), shot))
.observe(finish_bake)
.id();
job.capture = Some(entity);
}
}
}
for index in gone.into_iter().rev() {
let job = bakes.jobs.remove(index);
for entity in job.scene {
commands.entity(entity).despawn();
}
}
}
#[allow(clippy::too_many_arguments)]
fn finish_bake(
captured: On<ScreenshotCaptured>,
shots: Query<&BakeShot>,
mut commands: Commands,
mut bakes: ResMut<ImpostorBakes>,
mut images: ResMut<Assets<Image>>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ImpostorMaterial>>,
mut pools: ResMut<Assets<TreePool>>,
mut baked: ResMut<ImpostorsBaked>,
wind: Res<FloraWindBuffer>,
) {
let Ok(shot) = shots.get(captured.entity) else {
return;
};
let bakes = &mut *bakes;
let Some(index) = bakes
.jobs
.iter()
.position(|j| j.pool == shot.pool && j.variant == shot.variant)
else {
return;
};
let image = &captured.image;
let (width, height) = (IMPOSTOR_VIEWS * TILE_PIXELS, 2 * TILE_PIXELS);
let row = (width * TILE_PIXELS * 4) as usize;
let pixels = image
.data
.as_deref()
.filter(|data| data.len() == (width * height * 4) as usize);
let covered = pixels.map_or(0, |data| {
data[..row]
.as_chunks::<4>()
.0
.iter()
.filter(|px| px[3] >= 128)
.count()
});
let enough = covered as f32 >= MIN_COVERAGE * (width * TILE_PIXELS) as f32;
let job = &mut bakes.jobs[index];
if !enough {
job.attempts += 1;
if job.attempts < MAX_ATTEMPTS {
job.capture = None;
job.frames = SETTLE_FRAMES.saturating_sub(2);
return;
}
warn!(
"bevy_flora: no impostor for variant {} of a tree pool: {MAX_ATTEMPTS} captures of its baking scene came back empty",
job.variant
);
}
let job = bakes.jobs.remove(index);
for entity in &job.scene {
commands.entity(*entity).despawn();
}
bakes.settled.insert((job.pool, job.variant));
if let (true, Some(pixels)) = (enough, pixels)
&& let Some(mut pool) = pools.get_mut(job.pool)
{
let flora = FloraUniform::new(&pool.params);
let albedo = images.add(atlas_image(&pixels[..row], width, TILE_PIXELS));
let surface = images.add(surface_image(
&linearize_surface(&pixels[row..]),
&pixels[..row],
width,
TILE_PIXELS,
));
pool.variants[job.variant].impostor = Some(ImpostorAtlas {
views: IMPOSTOR_VIEWS,
size: job.frame.size,
center_y: job.frame.center_y,
material: materials.add(ImpostorMaterial::new(
albedo.clone(),
surface.clone(),
IMPOSTOR_VIEWS,
pool.params.style,
flora,
&wind,
)),
quad: meshes.add(job.frame.quad()),
albedo,
surface,
});
}
baked.0 += 1;
}