use std::f32::consts::{PI, TAU};
use bevy::{
asset::RenderAssetUsages,
image::ImageSampler,
prelude::*,
render::render_resource::{Extent3d, TextureDimension, TextureFormat},
};
use crate::HideWorldLine;
#[derive(Component)]
pub struct RelativisticSkybox {
pub x: [Handle<Image>; 2],
pub y: [Handle<Image>; 2],
pub z: [Handle<Image>; 2],
pub radius: f32,
}
pub(crate) fn assemble_skybox(
mut commands: Commands,
mut images: ResMut<Assets<Image>>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
query: Query<(Entity, &RelativisticSkybox), Without<Mesh3d>>,
) {
for (entity, skybox) in &query {
let handles = [
&skybox.x[0],
&skybox.x[1],
&skybox.y[0],
&skybox.y[1],
&skybox.z[0],
&skybox.z[1],
];
let face_images: Vec<&Image> = handles.iter().filter_map(|h| images.get(*h)).collect();
if face_images.len() != 6 {
continue;
}
let face_size = face_images[0].width();
assert_eq!(
face_size,
face_images[0].height(),
"Skybox faces must be square"
);
let faces: [&Image; 6] = face_images.try_into().unwrap();
let equirect = cubemap_to_equirectangular(faces, face_size);
commands.entity(entity).insert((
Mesh3d(meshes.add(Sphere::new(skybox.radius).mesh().uv(128, 64))),
MeshMaterial3d(materials.add(StandardMaterial {
base_color_texture: Some(images.add(equirect)),
unlit: true,
double_sided: true,
cull_mode: None,
..default()
})),
Transform::from_rotation(
Quat::from_rotation_y(core::f32::consts::PI)
* Quat::from_rotation_x(core::f32::consts::FRAC_PI_2),
),
HideWorldLine,
));
commands.entity(entity).remove::<RelativisticSkybox>();
}
}
fn cubemap_to_equirectangular(faces: [&Image; 6], face_size: u32) -> Image {
let width = face_size * 4;
let height = face_size * 2;
let mut data = vec![0u8; (width * height * 4) as usize];
let max_coord = (face_size - 1) as f32;
for py in 0..height {
for px in 0..width {
let u = (px as f32 + 0.5) / width as f32;
let v = (py as f32 + 0.5) / height as f32;
let theta = v * PI;
let phi = u * TAU;
let (sin_theta, cos_theta) = theta.sin_cos();
let (sin_phi, cos_phi) = phi.sin_cos();
let dir = Vec3::new(sin_theta * cos_phi, -cos_theta, sin_theta * sin_phi);
let (face_index, face_u, face_v) = cubemap_face_uv(dir);
let face_data = rgba8_bytes(faces[face_index]);
let pixel =
bilinear_sample(face_data, face_size, face_u * max_coord, face_v * max_coord);
let dst = ((py * width + px) * 4) as usize;
data[dst..dst + 4].copy_from_slice(&pixel);
}
}
let mut image = Image::new(
Extent3d {
width,
height,
depth_or_array_layers: 1,
},
TextureDimension::D2,
data,
TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::RENDER_WORLD,
);
image.sampler = ImageSampler::linear();
image
}
fn cubemap_face_uv(dir: Vec3) -> (usize, f32, f32) {
let abs = dir.abs();
let (face, sc, tc, ma) = if abs.x >= abs.y && abs.x >= abs.z {
if dir.x > 0. {
(0, -dir.z, -dir.y, abs.x)
} else {
(1, dir.z, -dir.y, abs.x)
}
} else if abs.y >= abs.z {
if dir.y > 0. {
(2, dir.x, dir.z, abs.y)
} else {
(3, dir.x, -dir.z, abs.y)
}
} else if dir.z > 0. {
(4, dir.x, -dir.y, abs.z)
} else {
(5, -dir.x, -dir.y, abs.z)
};
(face, (sc / ma + 1.) / 2., (tc / ma + 1.) / 2.)
}
fn bilinear_sample(data: &[u8], size: u32, fx: f32, fy: f32) -> [u8; 4] {
let x0 = (fx.floor() as u32).min(size - 1);
let y0 = (fy.floor() as u32).min(size - 1);
let x1 = (x0 + 1).min(size - 1);
let y1 = (y0 + 1).min(size - 1);
let tx = fx - fx.floor();
let ty = fy - fy.floor();
let fetch = |x: u32, y: u32| -> [f32; 4] {
let i = ((y * size + x) * 4) as usize;
[
data[i] as f32,
data[i + 1] as f32,
data[i + 2] as f32,
data[i + 3] as f32,
]
};
let p00 = fetch(x0, y0);
let p10 = fetch(x1, y0);
let p01 = fetch(x0, y1);
let p11 = fetch(x1, y1);
let mut result = [0u8; 4];
for c in 0..4 {
let top = p00[c] * (1. - tx) + p10[c] * tx;
let bottom = p01[c] * (1. - tx) + p11[c] * tx;
result[c] = (top * (1. - ty) + bottom * ty).round() as u8;
}
result
}
fn rgba8_bytes(image: &Image) -> &[u8] {
match image.texture_descriptor.format {
TextureFormat::Rgba8UnormSrgb | TextureFormat::Rgba8Unorm => {}
format => panic!("Unexpected skybox face format {format:?}"),
}
image.data.as_deref().unwrap_or(&[])
}