use crate::vulkan::{Texture2DHolder, VulkanContext};
use anyhow::{anyhow, bail, Error, Result};
use image::imageops::FilterType;
use image::{DynamicImage, GenericImageView, Pixel, RgbaImage};
use image::{GenericImage, Rgba};
use lazy_static::lazy_static;
use perovskite_core::protocol::render::TextureReference;
use rustc_hash::FxHashMap;
use std::fmt::{Debug, Formatter};
use std::ops::Deref;
use texture_packer::{Rect, TexturePacker};
use vulkano::format::Format;
lazy_static! {
static ref UNKNOWN_TEX: RgbaImage = {
image::load_from_memory(include_bytes!("block_unknown.png"))
.unwrap()
.into_rgba8()
};
static ref SELECTION_TEX: RgbaImage = {
image::load_from_memory(include_bytes!("selection.png"))
.unwrap()
.into_rgba8()
};
}
#[derive(Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Debug)]
pub(crate) enum TextureKey {
Named(NamedTextureKey),
FallbackUnknownTex,
SelectionRectangle,
}
impl TextureKey {
fn diffuse<'a>(&self, textures: &'a FxHashMap<String, RgbaImage>) -> Option<&'a RgbaImage> {
match self {
TextureKey::Named(key) => key
.diffuse
.as_ref()
.map(|x| textures.get(x).unwrap_or(&UNKNOWN_TEX)),
TextureKey::FallbackUnknownTex => Some(&UNKNOWN_TEX),
TextureKey::SelectionRectangle => Some(&SELECTION_TEX),
}
}
fn specular<'a>(&self, textures: &'a FxHashMap<String, RgbaImage>) -> Option<&'a RgbaImage> {
match self {
TextureKey::Named(key) => key
.specular
.as_ref()
.map(|x| textures.get(x).unwrap_or(&UNKNOWN_TEX)),
_ => None,
}
}
fn emissive<'a>(&self, textures: &'a FxHashMap<String, RgbaImage>) -> Option<&'a RgbaImage> {
match self {
TextureKey::Named(key) => key
.emissive
.as_ref()
.map(|x| textures.get(x).unwrap_or(&UNKNOWN_TEX)),
_ => None,
}
}
fn normal_map<'a>(&self, textures: &'a FxHashMap<String, RgbaImage>) -> Option<&'a RgbaImage> {
match self {
TextureKey::Named(key) => key
.normal_map
.as_ref()
.map(|x| textures.get(x).unwrap_or(&UNKNOWN_TEX)),
_ => None,
}
}
fn alt_diffuse<'a>(&self, textures: &'a FxHashMap<String, RgbaImage>) -> Option<&'a RgbaImage> {
match self {
TextureKey::Named(key) => key
.alt_diffuse
.as_ref()
.map(|x| textures.get(x).unwrap_or(&UNKNOWN_TEX)),
TextureKey::SelectionRectangle => Some(&SELECTION_TEX),
_ => None,
}
}
fn size(&self, textures: &FxHashMap<String, RgbaImage>) -> Result<(u32, u32)> {
let diffuse_size = self
.diffuse(&textures)
.map(|x| (x.width(), x.height()))
.unwrap_or((1, 1));
let specular_size = self
.specular(&textures)
.map(|x| (x.width(), x.height()))
.unwrap_or((1, 1));
let emissive_size = self
.emissive(&textures)
.map(|x| (x.width(), x.height()))
.unwrap_or((1, 1));
let normal_map_size = self
.normal_map(&textures)
.map(|x| (x.width(), x.height()))
.unwrap_or((1, 1));
let alt_diffuse_size = self
.alt_diffuse(&textures)
.map(|x| (x.width(), x.height()))
.unwrap_or((1, 1));
let max_x = diffuse_size
.0
.max(specular_size.0)
.max(emissive_size.0)
.max(normal_map_size.0)
.max(alt_diffuse_size.0);
let max_y = diffuse_size
.1
.max(specular_size.1)
.max(emissive_size.1)
.max(normal_map_size.1)
.max(alt_diffuse_size.1);
if max_x % diffuse_size.0 != 0 {
bail!("{self:?}: Target width of {max_x} pixels not divisible by diffuse texture width {}", diffuse_size.0);
}
if max_y % diffuse_size.1 != 0 {
bail!("{self:?}: Target height of {max_x} pixels not divisible by diffuse texture height {}", diffuse_size.0);
}
if max_x % specular_size.0 != 0 {
bail!("{self:?}: Target width of {max_x} pixels not divisible by specular texture width {}", diffuse_size.0);
}
if max_y % specular_size.1 != 0 {
bail!("{self:?}: Target height of {max_y} pixels not divisible by specular texture height {}", diffuse_size.0);
}
if max_x % emissive_size.0 != 0 {
bail!("{self:?}: Target width of {max_x} pixels not divisible by emissive texture width {}", diffuse_size.0);
}
if max_y % emissive_size.1 != 0 {
bail!("{self:?}: Target height of {max_y} pixels not divisible by emissive texture height {}", diffuse_size.0);
}
if max_x % normal_map_size.0 != 0 {
bail!("{self:?}: Target width of {max_x} pixels not divisible by normal_map texture width {}", diffuse_size.0);
}
if max_y % normal_map_size.1 != 0 {
bail!("{self:?}: Target height of {max_y} pixels not divisible by normal_map texture height {}", diffuse_size.0);
}
if max_x % alt_diffuse_size.0 != 0 {
bail!("{self:?}: Target width of {max_x} pixels not divisible by alt_diffuse texture width {}", diffuse_size.0);
}
if max_y % alt_diffuse_size.1 != 0 {
bail!("{self:?}: Target height of {max_y} pixels not divisible by alt_diffuse texture height {}", diffuse_size.0);
}
Ok((max_x, max_y))
}
}
#[derive(Clone, Hash, Ord, PartialOrd, Eq, PartialEq)]
pub(crate) struct NamedTextureKey {
pub(crate) diffuse: Option<String>,
pub(crate) specular: Option<String>,
pub(crate) emissive: Option<String>,
pub(crate) normal_map: Option<String>,
pub(crate) alt_diffuse: Option<String>,
}
impl Debug for NamedTextureKey {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
"[{} / {} / {} / {} / {}]",
self.diffuse.as_ref().map(String::as_str).unwrap_or(""),
self.specular.as_ref().map(String::as_str).unwrap_or(""),
self.emissive.as_ref().map(String::as_str).unwrap_or(""),
self.normal_map.as_ref().map(String::as_str).unwrap_or(""),
self.alt_diffuse.as_ref().map(String::as_str).unwrap_or("")
))
}
}
impl From<&TextureReference> for TextureKey {
fn from(value: &TextureReference) -> Self {
Self::Named(value.into())
}
}
impl From<&TextureReference> for NamedTextureKey {
fn from(value: &TextureReference) -> Self {
fn if_nonempty(s: &str) -> Option<String> {
if s.is_empty() {
None
} else {
Some(s.to_string())
}
}
Self {
diffuse: if_nonempty(value.diffuse.as_ref()),
specular: if_nonempty(value.rt_specular.as_ref()),
emissive: if_nonempty(value.emissive.as_ref()),
normal_map: if_nonempty(value.normal_map.as_ref()),
alt_diffuse: if_nonempty(value.alt_diffuse.as_ref()),
}
}
}
pub(crate) struct TextureAtlas {
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) diffuse: Texture2DHolder,
pub(crate) specular: Texture2DHolder,
pub(crate) emissive: Texture2DHolder,
pub(crate) normal_map: Texture2DHolder,
pub(crate) alt_diffuse: Texture2DHolder,
pub(crate) texel_coords: FxHashMap<TextureKey, texture_packer::Rect>,
}
impl TextureAtlas {
pub(crate) fn new(
ctx: &VulkanContext,
keys: impl IntoIterator<Item = TextureKey>,
textures: FxHashMap<String, RgbaImage>,
) -> Result<Self> {
const ATLAS_DIM: u32 = 4096;
let config = texture_packer::TexturePackerConfig {
allow_rotation: false,
max_width: ATLAS_DIM,
max_height: ATLAS_DIM,
border_padding: 1,
texture_padding: 1,
texture_extrusion: 1,
trim: false,
texture_outlines: false,
force_max_dimensions: false,
};
let mut diffuse_packer = TexturePacker::new_skyline(config);
for key in keys.into_iter().chain([
TextureKey::FallbackUnknownTex,
TextureKey::SelectionRectangle,
]) {
let target_size = key.size(&textures)?;
let diffuse = key
.diffuse(&textures)
.map(|x| {
image::imageops::resize(x, target_size.0, target_size.1, FilterType::Nearest)
})
.unwrap_or(empty_black(target_size.0, target_size.1));
diffuse_packer
.pack_own(key, diffuse)
.map_err(|x| anyhow!("{x:?}"))?;
}
let diffuse_image = texture_packer::exporter::ImageExporter::export(&diffuse_packer, None)
.map_err(|x| Error::msg(format!("Texture atlas export failed: {:?}", x)))?;
let texel_coords: FxHashMap<TextureKey, Rect> = diffuse_packer
.get_frames()
.iter()
.map(|(k, v)| (k.clone(), v.frame))
.collect();
let mut specular_image = RgbaImage::new(diffuse_image.width(), diffuse_image.height());
let mut emissive_image = RgbaImage::new(diffuse_image.width(), diffuse_image.height());
let mut normal_map_image = empty_normal_map(diffuse_image.width(), diffuse_image.height());
let mut alt_diffuse_image = RgbaImage::new(diffuse_image.width(), diffuse_image.height());
for (key, rect) in &texel_coords {
let target_size = key.size(&textures)?;
assert_eq!(rect.w, target_size.0);
assert_eq!(rect.h, target_size.1);
let specular = key
.specular(&textures)
.map(|x| {
image::imageops::resize(x, target_size.0, target_size.1, FilterType::Nearest)
})
.unwrap_or(empty_black(target_size.0, target_size.1));
specular_image.copy_from(&specular, rect.x, rect.y)?;
let emissive = key
.emissive(&textures)
.map(|x| {
image::imageops::resize(x, target_size.0, target_size.1, FilterType::Nearest)
})
.unwrap_or(empty_black(target_size.0, target_size.1));
emissive_image.copy_from(&emissive, rect.x, rect.y)?;
let normal_map = key
.normal_map(&textures)
.map(|x| {
image::imageops::resize(x, target_size.0, target_size.1, FilterType::Nearest)
})
.unwrap_or(empty_normal_map(target_size.0, target_size.1));
normal_map_image.copy_from(&normal_map, rect.x, rect.y)?;
let mut alt_diffuse = make_alt_diffuse(&diffuse_image, rect)?;
if let Some(overlay) = key.alt_diffuse(&textures).map(|x| {
image::imageops::resize(x, target_size.0, target_size.1, FilterType::Nearest)
}) {
assert!(alt_diffuse.dimensions() == overlay.dimensions());
for (dst, src) in alt_diffuse.pixels_mut().zip(overlay.pixels()) {
dst.blend(src);
}
}
alt_diffuse_image.copy_from(&alt_diffuse, rect.x, rect.y)?;
}
let sampler_info = vulkano::image::sampler::SamplerCreateInfo {
mag_filter: vulkano::image::sampler::Filter::Nearest,
min_filter: vulkano::image::sampler::Filter::Linear,
..Default::default()
};
Ok(TextureAtlas {
width: diffuse_image.width(),
height: diffuse_image.height(),
diffuse: Texture2DHolder::from_image(
ctx,
diffuse_image.into_rgba8(),
Format::R8G8B8A8_SRGB,
sampler_info.clone(),
None,
)?,
specular: Texture2DHolder::from_image(
ctx,
specular_image,
Format::R8G8B8A8_SRGB,
sampler_info.clone(),
None,
)?,
emissive: Texture2DHolder::from_image(
ctx,
emissive_image,
Format::R8G8B8A8_SRGB,
sampler_info.clone(),
None,
)?,
normal_map: Texture2DHolder::from_image(
ctx,
normal_map_image,
Format::R8G8B8A8_UNORM,
sampler_info.clone(),
None,
)?,
alt_diffuse: Texture2DHolder::from_image(
ctx,
alt_diffuse_image,
Format::R8G8B8A8_SRGB,
sampler_info,
None,
)?,
texel_coords,
})
}
}
fn empty_black(width: u32, height: u32) -> RgbaImage {
let mut image = RgbaImage::new(width, height);
image.pixels_mut().for_each(|pixel| {
*pixel = Rgba([0, 0, 0, 255]);
});
image
}
fn empty_normal_map(width: u32, height: u32) -> RgbaImage {
let mut image = RgbaImage::new(width, height);
image.pixels_mut().for_each(|pixel| {
*pixel = Rgba([128, 128, 0, 0]);
});
image
}
fn make_alt_diffuse(source: &DynamicImage, src_rect: &Rect) -> Result<RgbaImage> {
let mut image = RgbaImage::new(src_rect.w, src_rect.h);
image.copy_from(
source
.view(src_rect.x, src_rect.y, src_rect.w, src_rect.h)
.deref(),
0,
0,
)?;
#[inline]
fn lighten(x: u8) -> u8 {
x / 4 + 96
}
image
.pixels_mut()
.for_each(|p| *p = Rgba([lighten(p[0]), lighten(p[1]), lighten(p[2]), lighten(p[3])]));
Ok(image)
}