#[path = "utils/example.rs"]
mod example;
use example::{quality::*, *};
use bevy::prelude::*;
use bevy_asset::RenderAssetUsages;
use bevy_camera::visibility::RenderLayers;
use bevy_ecs::lifecycle::HookContext;
use bevy_ecs::world::DeferredWorld;
use bevy_eidolon::prelude::*;
use bevy_feronia::asset::backend::scene_backend::SceneAssetBackendPlugin;
use bevy_feronia::prelude::*;
use bevy_image::*;
use bevy_mesh::PlaneMeshBuilder;
use bevy_render::render_resource::{Extent3d, TextureDimension, TextureFormat};
use noise::{NoiseFn, Perlin};
use rand::{Rng, RngCore, SeedableRng, rng};
use rand_pcg::Pcg64;
fn main() -> AppExit {
App::new()
.insert_resource(HeightMapConfig {
render_layers: RenderLayers::layer(1),
..default()
})
.insert_resource(ExamplePluginOptions {
show_quality_settings: true,
show_wind_settings: true,
show_inspector: true,
show_debug_options: true,
})
.init_resource::<GlobalWind>()
.insert_resource(DensityMapConfig { size: 128 })
.add_plugins((
QualityPlugin,
ExamplePlugin,
SceneAssetBackendPlugin,
StandardScatterPlugin,
InstancedWindAffectedScatterPlugin,
ExtendedWindAffectedScatterPlugin,
GpuComputeCullCorePlugin,
GpuCullComputePlugin::<InstancedWindAffectedMaterial>::default(),
))
.init_state::<AppState>()
.add_systems(OnEnter(AppState::Setup), setup_loading_screen)
.add_systems(OnExit(AppState::Loading), teardown_loading_screen)
.add_systems(Startup, setup_density_map)
.add_systems(
Update,
(
load_assets.run_if(in_state(AppState::Setup)),
check_assets_loaded.run_if(in_state(AppState::Loading)),
),
)
.add_systems(OnEnter(AppState::Loading), spawn_landscape)
.add_systems(OnEnter(HeightMapState::Ready), spawn_scene)
.add_systems(OnEnter(ScatterState::Ready), scatter)
.add_systems(
Update,
(
setup_density_map_inspection.run_if(resource_added::<DensityMap>),
setup_height_map_inspection.run_if(resource_added::<HeightMapTexture>),
scatter_on_keypress.in_set(ScatterSet::Ready),
respawn_scene
.run_if(in_state(AppState::InGame))
.run_if(resource_changed::<QualitySettings>)
.after(QualitySettingsUpdating),
),
)
.add_observer(scatter_extended)
.add_observer(scatter_instanced)
.add_observer(update_density_map)
.run()
}
#[derive(States, Debug, Clone, Eq, PartialEq, Hash, Default)]
enum AppState {
#[default]
Setup,
Loading,
InGame,
}
fn spawn_scene(
mut cmd: Commands,
landscape: Single<Entity, With<Landscape>>,
mut images: ResMut<Assets<Image>>,
settings: Res<QualitySettings>,
) {
info!("Spawning scene with quality: {:?}", settings.quality);
let fog_texture = create_spherical_fog_texture(64);
let fog_texture_handle = images.add(fog_texture);
if let Some(image) = images.get_mut(&fog_texture_handle) {
image.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor::linear());
}
cmd.spawn((RockLayer, ChildOf(*landscape)));
cmd.spawn((TreeLayer, ChildOf(*landscape)));
cmd.spawn((FoliageLayer, ChildOf(*landscape)));
cmd.spawn((GrassLayer, ChildOf(*landscape)));
}
#[derive(Resource, Clone)]
struct Scenes {
landscape: Handle<Scene>,
audio: Handle<AudioSource>,
trees_lod_low: Handle<Scene>,
trees_billboards: Handle<Scene>,
foliage_lod_low: Handle<Scene>,
grass_lod_low: Handle<Scene>,
grass_lod_medium: Handle<Scene>,
rocks_lod_low: Handle<Scene>,
grass_lod_high: Handle<Scene>,
foliage_lod_high: Handle<Scene>,
foliage_lod_medium: Handle<Scene>,
trees_lod_high: Handle<Scene>,
trees_lod_medium: Handle<Scene>,
rocks_lod_medium: Handle<Scene>,
rocks_lod_high: Handle<Scene>,
}
impl Scenes {
fn active_handles(&self) -> Vec<&Handle<Scene>> {
[
&self.landscape,
&self.trees_lod_low,
&self.trees_billboards,
&self.foliage_lod_low,
&self.grass_lod_low,
&self.grass_lod_medium,
&self.rocks_lod_low,
&self.foliage_lod_medium,
&self.trees_lod_medium,
&self.rocks_lod_medium,
&self.foliage_lod_high,
&self.foliage_lod_medium,
&self.trees_lod_high,
&self.trees_lod_medium,
&self.rocks_lod_medium,
&self.rocks_lod_high,
]
.into_iter()
.filter(|x| x.id() != default::<Handle<Scene>>().id())
.collect()
}
}
fn load_assets(
mut commands: Commands,
asset_server: Res<AssetServer>,
quality: Res<QualitySettings>,
mut ns_app: ResMut<NextState<AppState>>,
mut ns_scatter: ResMut<NextState<ScatterState>>,
scenes: Option<ResMut<Scenes>>,
) {
let load_opt =
|existing: Option<Handle<Scene>>, condition: bool, path: &'static str| -> Handle<Scene> {
existing
.and_then(|h| {
asset_server
.get_load_state(h.id())
.is_some_and(|s| s.is_loaded())
.then_some(h)
})
.or_else(|| condition.then(|| asset_server.load(path)))
.unwrap_or_default()
};
let is_high_tier = quality.model_quality == ModelQuality::High;
let is_med_tier = quality.model_quality == ModelQuality::Medium || is_high_tier;
let is_grass_high_needed = quality.model_quality != ModelQuality::Low;
commands.insert_resource(Scenes {
landscape: scenes.as_ref().map_or_else(
|| asset_server.load("landscape_large.glb#Scene0"),
|s| s.landscape.clone(),
),
audio: scenes.as_ref().map_or_else(
|| {
asset_server
.load("sounds/birds-singing-in-and-leaves-rustling-with-the-wind-14557.mp3")
},
|s| s.audio.clone(),
),
trees_lod_low: scenes.as_ref().map_or_else(
|| asset_server.load("trees.glb#Scene2"),
|s| s.trees_lod_low.clone(),
),
trees_billboards: scenes.as_ref().map_or_else(
|| asset_server.load("trees.glb#Scene0"),
|s| s.trees_billboards.clone(),
),
foliage_lod_low: scenes.as_ref().map_or_else(
|| asset_server.load("foliage_complex_low_lod.glb#Scene0"),
|s| s.foliage_lod_low.clone(),
),
grass_lod_low: scenes.as_ref().map_or_else(
|| asset_server.load("grass_low_lod.glb#Scene0"),
|s| s.grass_lod_low.clone(),
),
grass_lod_medium: scenes.as_ref().map_or_else(
|| asset_server.load("grass_medium_lod.glb#Scene0"),
|s| s.grass_lod_medium.clone(),
),
rocks_lod_low: scenes.as_ref().map_or_else(
|| asset_server.load("rocks_low_lod.glb#Scene0"),
|s| s.rocks_lod_low.clone(),
),
grass_lod_high: load_opt(
scenes.as_ref().map(|s| s.grass_lod_high.clone()),
is_grass_high_needed,
"grass.glb#Scene0",
),
foliage_lod_high: load_opt(
scenes.as_ref().map(|s| s.foliage_lod_high.clone()),
is_high_tier,
"foliage_complex.glb#Scene0",
),
foliage_lod_medium: load_opt(
scenes.as_ref().map(|s| s.foliage_lod_medium.clone()),
is_med_tier,
"foliage_complex_medium_lod.glb#Scene0",
),
trees_lod_high: load_opt(
scenes.as_ref().map(|s| s.trees_lod_high.clone()),
is_high_tier,
"trees.glb#Scene1",
),
trees_lod_medium: load_opt(
scenes.as_ref().map(|s| s.trees_lod_medium.clone()),
is_med_tier,
"trees.glb#Scene3",
),
rocks_lod_high: load_opt(
scenes.as_ref().map(|s| s.rocks_lod_high.clone()),
is_high_tier,
"rocks_high_lod.glb#Scene0",
),
rocks_lod_medium: load_opt(
scenes.as_ref().map(|s| s.rocks_lod_medium.clone()),
is_med_tier,
"rocks_medium_lod.glb#Scene0",
),
});
ns_app.set(AppState::Loading);
ns_scatter.set(ScatterState::Loading);
}
fn setup_density_map_inspection(
mut cmd: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
density_map: Res<DensityMap>,
) {
cmd.spawn((
Transform::from_xyz(10.0, 7.0, 5.0).looking_at(Vec3::new(0.0, 14.0, 1.0), Vec3::Y),
Mesh3d(meshes.add(PlaneMeshBuilder::from_length(1.))),
MeshMaterial3d(materials.add(StandardMaterial {
base_color_texture: Some(density_map.0.clone()),
unlit: true,
..default()
})),
));
}
fn setup_height_map_inspection(
mut cmd: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
height_map: Res<HeightMapTexture>,
) {
cmd.spawn((
Transform::from_xyz(10.0, 5.0, 5.0).looking_at(Vec3::new(0.0, 14.0, 1.0), Vec3::Y),
Mesh3d(meshes.add(PlaneMeshBuilder::from_length(1.))),
MeshMaterial3d(materials.add(StandardMaterial {
base_color_texture: Some(height_map.0.clone()),
unlit: true,
..default()
})),
));
}
fn check_assets_loaded(
mut next_state: ResMut<NextState<AppState>>,
asset_server: Res<AssetServer>,
handles: Res<Scenes>,
) {
let all_loaded = handles.active_handles().iter().all(|h| {
asset_server
.get_load_state(h.id())
.is_some_and(|s| s.is_loaded())
});
if all_loaded {
next_state.set(AppState::InGame);
}
}
fn scatter(mut cmd: Commands, root: Single<Entity, With<ScatterRoot>>) {
cmd.trigger(Scatter::<StandardMaterial>::new(*root));
}
fn respawn_scene(
mut cmd: Commands,
root: Single<Entity, With<ScatterRoot>>,
mut ns_app: ResMut<NextState<AppState>>,
) {
info!("Quality settings changed, despawning and respawning scene...");
cmd.entity(*root).despawn();
ns_app.set(AppState::Setup);
}
#[derive(Component)]
#[component(on_add = Self::on_add)]
#[require(
Name::new("Landscape"),
ScatterRoot::default(),
MapHeight,
ChunkRoot::default(),
ChunkRootSizeDim(2),
PlaybackSettings {
mode: bevy::audio::PlaybackMode::Loop,
..default()
},
)]
pub struct Landscape;
impl Landscape {
fn on_add(mut world: DeferredWorld, ctx: HookContext) {
let Scenes {
landscape, audio, ..
} = world
.get_resource::<Scenes>()
.cloned()
.expect("Scene handles should be added!");
let QualitySettings { range_quality, .. } = world
.get_resource::<QualitySettings>()
.cloned()
.unwrap_or_default();
world.commands().entity(ctx.entity).insert((
SceneRoot(landscape.clone()),
LodConfig::from(range_quality),
AudioPlayer::new(audio.clone()),
));
}
}
fn spawn_landscape(
mut cmd: Commands,
mut ns_height_map: ResMut<NextState<HeightMapState>>,
mut ns_scatter: ResMut<NextState<ScatterState>>,
mut images: ResMut<Assets<Image>>,
settings: Res<QualitySettings>,
) {
info!("Spawning scene with quality: {:?}", settings.quality);
let fog_texture = create_spherical_fog_texture(64);
let fog_texture_handle = images.add(fog_texture);
if let Some(image) = images.get_mut(&fog_texture_handle) {
image.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor::linear());
}
cmd.spawn((
Landscape,
Transform::from_xyz(80., 5., 80.)
.with_rotation(Quat::from_rotation_y(std::f32::consts::FRAC_PI_4)),
));
ns_scatter.set(ScatterState::Setup);
ns_height_map.set(HeightMapState::Setup);
}
#[derive(Component)]
#[component(on_add = Self::on_add)]
#[require(
Name::new("Rock Layer"),
ScatterLayerType::<StandardMaterial>,
InstanceRotationYaw,
InstanceScale,
InstanceJitter,
DistributionDensity(10.),
Avoidance(2.),
)]
struct RockLayer;
impl RockLayer {
fn on_add(mut world: DeferredWorld, ctx: HookContext) {
let QualitySettings { model_quality, .. } = world
.get_resource::<QualitySettings>()
.cloned()
.unwrap_or_default();
let handles = world
.get_resource::<Scenes>()
.cloned()
.expect("Scene handles should be added!");
let mut cmd = world.commands();
cmd.spawn((
LevelOfDetail(0),
ChildOf(ctx.entity),
SceneRoot(match model_quality {
ModelQuality::Low => handles.rocks_lod_low.clone(),
ModelQuality::Medium => handles.rocks_lod_medium.clone(),
ModelQuality::High => handles.rocks_lod_high.clone(),
}),
));
if matches!(model_quality, ModelQuality::High | ModelQuality::Medium) {
cmd.spawn((
LevelOfDetail(1),
ChildOf(ctx.entity),
SceneRoot(match model_quality {
ModelQuality::High => handles.rocks_lod_medium.clone(),
ModelQuality::Medium => handles.rocks_lod_low.clone(),
_ => default(),
}),
));
}
if model_quality == ModelQuality::High {
cmd.spawn((
LevelOfDetail(2),
ChildOf(ctx.entity),
SceneRoot(handles.rocks_lod_low.clone()),
));
}
}
}
#[derive(Component)]
#[component(on_add = Self::on_add)]
#[require(
Name::new("Tree Layer"),
ScatterLayerType::<ExtendedWindAffectedMaterial>,
InstanceJitter,
InstanceRotationYaw,
DistributionDensity(5.),
InstanceScaleRange { min: 1., max: 2. },
Avoidance(1.),
)]
struct TreeLayer;
impl TreeLayer {
fn on_add(mut world: DeferredWorld, ctx: HookContext) {
let QualitySettings {
disable_sss,
disable_wind_displacement,
static_shadows,
model_quality,
..
} = world
.get_resource::<QualitySettings>()
.cloned()
.unwrap_or_default();
let handles = world
.get_resource::<Scenes>()
.cloned()
.expect("Scene handles should be added!");
let mut cmd = world.commands();
if !disable_sss {
cmd.entity(ctx.entity).insert(SssBundle::default());
}
if !disable_wind_displacement {
cmd.entity(ctx.entity).insert(WindAffected);
}
if static_shadows {
cmd.entity(ctx.entity).insert(StaticShadow);
}
match model_quality {
ModelQuality::Low => {
cmd.spawn((
LevelOfDetail(0),
ChildOf(ctx.entity),
SceneRoot(handles.trees_lod_low.clone()),
));
cmd.spawn((
LevelOfDetail(1),
ChildOf(ctx.entity),
SceneRoot(handles.trees_billboards.clone()),
Unlit,
));
}
ModelQuality::Medium => {
cmd.spawn((
LevelOfDetail(0),
ChildOf(ctx.entity),
SceneRoot(handles.trees_lod_medium.clone()),
));
cmd.spawn((
LevelOfDetail(1),
ChildOf(ctx.entity),
SceneRoot(handles.trees_lod_low.clone()),
));
cmd.spawn((
LevelOfDetail(2),
ChildOf(ctx.entity),
SceneRoot(handles.trees_billboards.clone()),
Unlit,
));
}
ModelQuality::High => {
cmd.spawn((
LevelOfDetail(0),
ChildOf(ctx.entity),
SceneRoot(handles.trees_lod_high.clone()),
));
cmd.spawn((
LevelOfDetail(1),
ChildOf(ctx.entity),
SceneRoot(handles.trees_lod_medium.clone()),
));
cmd.spawn((
LevelOfDetail(2),
ChildOf(ctx.entity),
SceneRoot(handles.trees_lod_low.clone()),
));
cmd.spawn((
LevelOfDetail(3),
ChildOf(ctx.entity),
SceneRoot(handles.trees_billboards.clone()),
Unlit,
));
}
}
}
}
#[derive(Component)]
#[component(on_add = Self::on_add)]
#[require(
Name::new("Foliage Layer"),
ScatterLayerType::<ExtendedWindAffectedMaterial>,
DistributionDensity(15.),
InstanceRotationYaw,
InstanceJitter,
InstanceScaleRange { min: 4., max: 8. },
Avoidance(0.2),
)]
struct FoliageLayer;
impl FoliageLayer {
fn on_add(mut world: DeferredWorld, ctx: HookContext) {
let QualitySettings {
disable_sss,
disable_wind_displacement,
static_shadows,
model_quality,
..
} = world
.get_resource::<QualitySettings>()
.cloned()
.unwrap_or_default();
let handles = world
.get_resource::<Scenes>()
.cloned()
.expect("Scene handles should be added!");
let mut cmd = world.commands();
if !disable_sss {
cmd.entity(ctx.entity).insert((
SubsurfaceScattering,
SubsurfaceScatteringIntensity(4.),
SubsurfaceScatteringScale(5.),
));
}
if !disable_wind_displacement {
cmd.entity(ctx.entity).insert(WindAffected);
}
if static_shadows {
cmd.entity(ctx.entity).insert(StaticShadow);
}
match model_quality {
ModelQuality::Low => {
cmd.spawn((
LevelOfDetail(0),
ChildOf(ctx.entity),
SceneRoot(handles.foliage_lod_low.clone()),
));
}
ModelQuality::Medium => {
cmd.spawn((
LevelOfDetail(0),
ChildOf(ctx.entity),
SceneRoot(handles.foliage_lod_medium.clone()),
));
cmd.spawn((
LevelOfDetail(1),
ChildOf(ctx.entity),
SceneRoot(handles.foliage_lod_low.clone()),
));
}
ModelQuality::High => {
cmd.spawn((
LevelOfDetail(0),
ChildOf(ctx.entity),
SceneRoot(handles.foliage_lod_high.clone()),
));
cmd.spawn((
LevelOfDetail(1),
ChildOf(ctx.entity),
SceneRoot(handles.foliage_lod_medium.clone()),
));
cmd.spawn((
LevelOfDetail(2),
ChildOf(ctx.entity),
SceneRoot(handles.foliage_lod_low.clone()),
));
}
}
}
}
#[derive(Component)]
#[component(on_add = Self::on_add)]
#[require(
Name::new("Grass Layer"),
ScatterLayerType::<InstancedWindAffectedMaterial>,
// Scatter options
InstanceJitter,
InstanceScale,
ScatterChunked,
ScaleDensity,
// Material options
GpuCullCompute,
// EdgeCorrectionFactor,
CurveNormals,
Strength(1.2),
SCurveStrength(1.2),
BopStrength(1.2),
AnalyticalNormals,
InstanceRotationYaw,
StandardPbr,
SubsurfaceScattering,
InstanceColor::new(Srgba::hex("#1f3114").unwrap()),
InstanceColorGradient {
end: 0.7,
start: 0.2,
..InstanceColorGradient::new(
Srgba::hex("#3e6328").unwrap(),
Srgba::hex("#0f190a").unwrap()
)
},
StaticBend,
AmbientOcclusion,
)]
struct GrassLayer;
impl GrassLayer {
fn on_add(mut world: DeferredWorld, ctx: HookContext) {
let QualitySettings {
disable_grass_point_lights,
disable_grass_directional_lights,
disable_wind_displacement,
grass_density,
model_quality,
..
} = world
.get_resource::<QualitySettings>()
.cloned()
.unwrap_or_default();
let handles = world
.get_resource::<Scenes>()
.cloned()
.expect("Scene handles should be added!");
let density_map = world
.get_resource::<DensityMap>()
.map(|x| x.0.clone())
.expect("Density map should be added!");
let mut cmd = world.commands();
cmd.entity(ctx.entity).insert((
DistributionDensity::from(grass_density),
DistributionPattern(density_map),
));
if !disable_grass_directional_lights {
cmd.entity(ctx.entity).insert(DirectionalLights);
}
if !disable_grass_point_lights {
cmd.entity(ctx.entity).insert(PointLights);
}
if !disable_wind_displacement {
cmd.entity(ctx.entity).insert(WindAffected);
}
match model_quality {
ModelQuality::Low => {
cmd.spawn((
LevelOfDetail(0),
ChildOf(ctx.entity),
SceneRoot(handles.grass_lod_low.clone()),
));
}
ModelQuality::Medium => {
cmd.spawn((
LevelOfDetail(0),
ChildOf(ctx.entity),
SceneRoot(handles.grass_lod_medium.clone()),
));
cmd.spawn((
LevelOfDetail(1),
ChildOf(ctx.entity),
SceneRoot(handles.grass_lod_low.clone()),
));
}
ModelQuality::High => {
cmd.spawn((
LevelOfDetail(0),
ChildOf(ctx.entity),
SceneRoot(handles.grass_lod_high.clone()),
));
cmd.spawn((
LevelOfDetail(1),
ChildOf(ctx.entity),
SceneRoot(handles.grass_lod_medium.clone()),
));
cmd.spawn((
LevelOfDetail(2),
ChildOf(ctx.entity),
SceneRoot(handles.grass_lod_low.clone()),
));
}
}
}
}
fn scatter_on_keypress(
mut cmd: Commands,
keyboard_input: Res<ButtonInput<KeyCode>>,
mut world_seed: ResMut<WorldSeed>,
root: Single<Entity, With<ScatterRoot>>,
mut mw_clear_root: MessageWriter<ClearScatterRoot>,
) {
if !keyboard_input.just_pressed(KeyCode::Space) {
return;
};
mw_clear_root.write((*root).into());
**world_seed = rng().next_u64();
cmd.trigger(UpdateDensityMap);
cmd.trigger(Scatter::<StandardMaterial>::new(*root));
}
fn scatter_extended(
_: On<ScatterFinished<StandardMaterial>>,
mut cmd: Commands,
root: Single<Entity, With<ScatterRoot>>,
) {
cmd.trigger(Scatter::<ExtendedWindAffectedMaterial>::new(*root));
}
fn scatter_instanced(
_: On<ScatterFinished<ExtendedWindAffectedMaterial>>,
mut cmd: Commands,
root: Single<Entity, With<ScatterRoot>>,
) {
cmd.trigger(Scatter::<InstancedWindAffectedMaterial>::new(*root));
}
#[derive(Resource)]
pub struct DensityMapConfig {
pub size: u32,
}
#[derive(Resource, Deref, DerefMut)]
struct DensityMap(Handle<Image>);
#[derive(Event)]
struct UpdateDensityMap;
fn setup_density_map(mut commands: Commands) {
commands.trigger(UpdateDensityMap);
}
fn update_density_map(
_: On<UpdateDensityMap>,
mut cmd: Commands,
mut images: ResMut<Assets<Image>>,
cfg: Res<DensityMapConfig>,
seed: Res<WorldSeed>,
) {
let size = cfg.size;
let mut data_buffer = vec![0; (size * size) as usize];
let mut rng = Pcg64::seed_from_u64(**seed);
let perlin = Perlin::new(**seed as u32);
let sample_scale = 8.0;
for y in 0..size {
for x in 0..size {
let point = [
x as f64 / size as f64 * sample_scale,
y as f64 / size as f64 * sample_scale,
];
let noise = perlin.get(point);
let byte = ((noise * 0.5 + 0.5).clamp(0.0, 1.0) * 255.0) as u8;
data_buffer[(y * size + x) as usize] = byte;
}
}
let num_spots = 20;
let max_spot_radius = (size / 16) as f32;
let min_spot_radius = (size / 32) as f32;
for _ in 0..num_spots {
let center_x = rng.random_range(0..size) as f32;
let center_y = rng.random_range(0..size) as f32;
let radius = rng.random_range(min_spot_radius..max_spot_radius);
let radius_sq = radius * radius;
let x_min = (center_x - radius).floor().max(0.0) as u32;
let x_max = (center_x + radius).ceil().min(size as f32) as u32;
let y_min = (center_y - radius).floor().max(0.0) as u32;
let y_max = (center_y + radius).ceil().min(size as f32) as u32;
for y in y_min..y_max {
for x in x_min..x_max {
let dx = x as f32 - center_x;
let dy = y as f32 - center_y;
let dist_sq = dx * dx + dy * dy;
if dist_sq < radius_sq {
let t = dist_sq / radius_sq;
let intensity = (1.0 - t).clamp(0.0, 1.0);
let byte = ((1.0 - intensity) * 255.0) as u8;
let index = (y * size + x) as usize;
data_buffer[index] = data_buffer[index].min(byte);
}
}
}
}
let mut density_image = Image::new(
Extent3d {
width: size,
height: size,
depth_or_array_layers: 1,
},
TextureDimension::D2,
data_buffer,
TextureFormat::R8Unorm,
default(),
);
density_image.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor {
label: Some("Foliage Density Sampler".into()),
address_mode_u: ImageAddressMode::MirrorRepeat,
address_mode_v: ImageAddressMode::MirrorRepeat,
..default()
});
let handle = images.add(density_image);
cmd.insert_resource(DensityMap(handle));
}
fn create_spherical_fog_texture(size: u32) -> Image {
let mut data = vec![0; (size * size * size) as usize];
let center = size as f32 / 2.0;
for z in 0..size {
for y in 0..size {
for x in 0..size {
let dx = x as f32 - center;
let dy = y as f32 - center;
let dz = z as f32 - center;
let distance = (dx * dx + dy * dy + dz * dz).sqrt() / center;
let density = (1.0 - distance).clamp(0.0, 1.0).powf(2.0);
let value = (density * 255.0) as u8;
let index = (x + y * size + z * size * size) as usize;
data[index] = value;
}
}
}
Image::new(
Extent3d {
width: size,
height: size,
depth_or_array_layers: size,
},
TextureDimension::D3,
data,
TextureFormat::R8Unorm,
RenderAssetUsages::default(),
)
}
#[derive(Component)]
struct LoadingScreen;
fn setup_loading_screen(mut commands: Commands) {
let text_style = TextFont {
font_size: 67.0,
..default()
};
commands
.spawn((
Node {
height: percent(100),
width: percent(100),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(Color::BLACK),
LoadingScreen,
))
.with_child((Text::new("Loading..."), text_style.clone()));
}
fn teardown_loading_screen(mut cmd: Commands, loading_screen: Single<Entity, With<LoadingScreen>>) {
cmd.entity(*loading_screen).despawn();
}