From a46bd3fa9f85f273e30af80a0214f86d90d6aad0 Mon Sep 17 00:00:00 2001 From: chazmm Date: Wed, 9 Sep 2026 02:02:53 +0100 Subject: [PATCH] Enlarge celestial bodies and add solar corona and lunar surface detail --- crates/skate-game/src/map_render.rs | 71 ++++++++++++++++++++++++++--- docs/custom-map-lighting.md | 8 +++- 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/crates/skate-game/src/map_render.rs b/crates/skate-game/src/map_render.rs index 5c094851..858ab4ad 100644 --- a/crates/skate-game/src/map_render.rs +++ b/crates/skate-game/src/map_render.rs @@ -102,7 +102,7 @@ impl PreparedScene { &mut self.meshes, &mut self.images, &mut self.sky, &mut self.retail); } else { custom_lighting(map, &mut self.commands); - celestial_bodies(&mut self.commands, &mut self.meshes, &mut self.materials); + celestial_bodies(&mut self.commands, &mut self.meshes, &mut self.materials, &mut self.images); } } else { crate::world::spawn_test_world(&mut self.commands, &mut self.meshes, &mut self.materials); @@ -142,24 +142,79 @@ fn orbit(azimuth: f32, hour: f32) -> Vec3 { #[derive(Component)] pub(crate) struct CelestialBody { moon: bool } -fn celestial_bodies(commands: &mut SceneCommands, meshes: &mut StagedAssets, materials: &mut StagedAssets) { - let disc = meshes.add(Circle::new(1.).mesh().resolution(64).build()); +fn celestial_bodies(commands: &mut SceneCommands, meshes: &mut StagedAssets, materials: &mut StagedAssets, images: &mut StagedAssets) { + let disc = meshes.add(Rectangle::new(2., 2.).into()); for moon in [false, true] { - let color = if moon { Color::srgb(0.78, 0.84, 0.96) } else { Color::srgb(1., 0.9, 0.55) }; + let texture = images.add(celestial_texture(moon)); commands.spawn(( Name::new(if moon { "Custom moon" } else { "Custom sun" }), CelestialBody { moon }, Mesh3d(disc.clone()), MeshMaterial3d(materials.add(StandardMaterial { - base_color: color, unlit: true, cull_mode: None, ..default() + base_color_texture: Some(texture), alpha_mode: AlphaMode::Blend, + unlit: true, cull_mode: None, ..default() })), Transform::default(), Visibility::Hidden, bevy::light::NotShadowCaster, bevy::light::NotShadowReceiver, )); } } +/// Generated once during off-thread map preparation, never during gameplay. +/// A transparent border contains the corona/halo without a hard square edge. +fn celestial_texture(moon: bool) -> Image { + const SIZE: u32 = 256; + let mut pixels = Vec::with_capacity((SIZE * SIZE * 4) as usize); + let craters = [(-0.35f32, 0.2f32, 0.22f32), (0.28, -0.3, 0.16), + (0.35, 0.4, 0.12), (-0.12, -0.58, 0.1), (-0.5, -0.28, 0.13), + (0.05, 0.15, 0.08), (0.62, 0.02, 0.1), (-0.15, 0.62, 0.09)]; + for y in 0..SIZE { + for x in 0..SIZE { + let u = (x as f32 + 0.5) * 2. / SIZE as f32 - 1.; + let v = (y as f32 + 0.5) * 2. / SIZE as f32 - 1.; + let r = u.hypot(v); + let q = r / 0.35; + let edge = ((1. - q) * 60.).clamp(0., 1.); + let (rgb, alpha) = if q <= 1. { + if moon { + let px = u / 0.35; + let py = v / 0.35; + let mut tone = 0.73 + 0.055 * (px * 19. + (py * 13.).sin()).sin() + + 0.035 * (py * 37. + px * 29.).cos(); + for (cx, cy, radius) in craters { + let d = (px - cx).hypot(py - cy) / radius; + tone -= 0.19 * (-d * d * 2.).exp(); + tone += 0.12 * (-((d - 1.) * 7.).powi(2)).exp(); + } + tone *= 0.65 + 0.35 * (1. - q * q).max(0.).sqrt(); + ([tone * 0.94, tone * 0.98, tone * 1.07], edge) + } else { + ([1., 0.96 - q * 0.13, 0.78 - q * 0.32], edge) + } + } else { + let angle = v.atan2(u); + let rays = if moon { 1. } else { + 0.65 + 0.2 * (angle * 12. + r * 9.).sin().powi(2) + + 0.15 * (angle * 23. - r * 6.).cos().powi(2) + }; + let glow = (-(q - 1.) * if moon { 4. } else { 2.4 }).exp() + * (1. - r).max(0.).powi(2) * rays; + (if moon { [0.55, 0.68, 1.] } else { [1., 0.63, 0.16] }, + glow * if moon { 0.36 } else { 0.85 }) + }; + for c in rgb { pixels.push((c.clamp(0., 1.) * 255.) as u8); } + pixels.push((alpha.clamp(0., 1.) * 255.) as u8); + } + } + Image::new( + bevy::render::render_resource::Extent3d { width: SIZE, height: SIZE, depth_or_array_layers: 1 }, + bevy::render::render_resource::TextureDimension::D2, pixels, + bevy::render::render_resource::TextureFormat::Rgba8UnormSrgb, + bevy::asset::RenderAssetUsages::RENDER_WORLD, + ) +} /// Follow the final gameplay camera before transform propagation, with no /// parallax. Ordinary depth testing lets buildings and terrain obscure them. pub(crate) fn position_celestial_bodies( + time: Res>, environment: Query<&DayEnvironment>, camera: Query<(&Transform, &Projection), (With, Without)>, mut bodies: Query<(&CelestialBody, &mut Transform, &mut Visibility), Without>, @@ -172,7 +227,11 @@ pub(crate) fn position_celestial_bodies( *visibility = if direction.y > 0. { Visibility::Inherited } else { Visibility::Hidden }; *transform = Transform::from_translation(camera.translation + direction * distance) .looking_at(camera.translation, if direction.y.abs() > 0.99 { Vec3::Z } else { Vec3::Y }) - .with_scale(Vec3::splat(distance * if body.moon { 0.014 } else { 0.012 })); + .with_scale(Vec3::splat(distance * if body.moon { 0.13 } else { 0.115 })); + if !body.moon { + transform.rotate_local_z(time.elapsed_secs() * 0.025); + transform.scale *= 1. + 0.012 * (time.elapsed_secs() * 0.8).sin(); + } } } fn daylight_values(e: &[f32], hour: f32) -> (Color, f32, f32, Transform, f32) { diff --git a/docs/custom-map-lighting.md b/docs/custom-map-lighting.md index 1abaedea..b553f40c 100644 --- a/docs/custom-map-lighting.md +++ b/docs/custom-map-lighting.md @@ -179,4 +179,10 @@ are hidden below the horizon, and use normal depth testing for scenery occlusion They are placed just inside the camera far plane and have fixed angular sizes. These are lightweight unlit meshes, not extra lights or shadow casters. They remain visible with zero ambient. Their scene-owned meshes/materials retire on -map switches; retail skies are unchanged. Moon phases and textures are not added. +map switches; retail skies are unchanged. Moon phases are not added. + +The sun and moon now have roughly 3.3x their original apparent diameter. +Two scene-owned 256px procedural textures add a feathered golden solar corona +and cool lunar halo with crater rims and mottled surface detail. The solar +corona rotates slowly with a subtle pulse while gameplay runs. Texture creation +stays in map preparation; runtime still uses two mesh draws and transform updates.