use std::collections::VecDeque;
use crate::ecs::asset_id::TextureId;
use crate::ecs::input::resources::DroppedFile;
use crate::ecs::particles::components::ParticleTextureUpload;
use crate::ecs::text::commands::PendingFontLoad;
use crate::render::wgpu::texture_cache::{SamplerSettings, TextureUsage};
pub const DEFAULT_TASKS_PER_FRAME: usize = 1;
#[derive(Default)]
pub struct LoadingState {
pub pipeline: LoadingPipeline,
pub pending_font_loads: Vec<PendingFontLoad>,
pub pending_particle_textures: Vec<ParticleTextureUpload>,
pub dropped_files: Vec<DroppedFile>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct SourceImageId(pub u32);
pub struct DecodedImage {
pub rgba: Vec<u8>,
pub width: u32,
pub height: u32,
}
pub enum TextureRecipe {
Direct {
image: SourceImageId,
},
PackRg {
a: Option<SourceImageId>,
b: Option<SourceImageId>,
},
PackRgbA {
rgb: Option<SourceImageId>,
alpha: Option<SourceImageId>,
},
SpecGlossBase {
diffuse: Option<SourceImageId>,
diffuse_factor: [f32; 4],
spec_gloss: Option<SourceImageId>,
specular_factor: [f32; 3],
glossiness_factor: f32,
},
SpecGlossMr {
diffuse: Option<SourceImageId>,
diffuse_factor: [f32; 4],
spec_gloss: Option<SourceImageId>,
specular_factor: [f32; 3],
glossiness_factor: f32,
},
}
impl TextureRecipe {
fn remap_sources(&mut self, remap: &[SourceImageId]) {
let map = |id: &mut SourceImageId| {
if let Some(target) = remap.get(id.0 as usize) {
*id = *target;
}
};
let map_opt = |opt: &mut Option<SourceImageId>| {
if let Some(id) = opt {
map(id);
}
};
match self {
TextureRecipe::Direct { image } => map(image),
TextureRecipe::PackRg { a, b } => {
map_opt(a);
map_opt(b);
}
TextureRecipe::PackRgbA { rgb, alpha } => {
map_opt(rgb);
map_opt(alpha);
}
TextureRecipe::SpecGlossBase {
diffuse,
spec_gloss,
..
}
| TextureRecipe::SpecGlossMr {
diffuse,
spec_gloss,
..
} => {
map_opt(diffuse);
map_opt(spec_gloss);
}
}
}
fn for_each_source(&self, mut visit: impl FnMut(SourceImageId)) {
match self {
TextureRecipe::Direct { image } => visit(*image),
TextureRecipe::PackRg { a, b } => {
if let Some(id) = a {
visit(*id);
}
if let Some(id) = b {
visit(*id);
}
}
TextureRecipe::PackRgbA { rgb, alpha } => {
if let Some(id) = rgb {
visit(*id);
}
if let Some(id) = alpha {
visit(*id);
}
}
TextureRecipe::SpecGlossBase {
diffuse,
spec_gloss,
..
}
| TextureRecipe::SpecGlossMr {
diffuse,
spec_gloss,
..
} => {
if let Some(id) = diffuse {
visit(*id);
}
if let Some(id) = spec_gloss {
visit(*id);
}
}
}
}
}
pub enum LoadingTask {
UploadDecodedTexture {
texture: TextureId,
rgba_data: Vec<u8>,
width: u32,
height: u32,
usage: TextureUsage,
sampler: SamplerSettings,
},
DecodeImage {
image: SourceImageId,
encoded_bytes: Vec<u8>,
},
MaterializeTexture {
texture: TextureId,
recipe: TextureRecipe,
usage: TextureUsage,
sampler: SamplerSettings,
},
}
impl LoadingTask {
pub fn category(&self) -> &'static str {
match self {
LoadingTask::UploadDecodedTexture { .. } => "texture",
LoadingTask::DecodeImage { .. } => "decode",
LoadingTask::MaterializeTexture { .. } => "texture",
}
}
}
#[derive(Default)]
pub struct LoadingPipeline {
pending: VecDeque<LoadingTask>,
completed: usize,
total: usize,
last_label: Option<String>,
last_category: Option<&'static str>,
pub tasks_per_frame: usize,
next_source_image: u32,
free_source_indices: Vec<u32>,
pub decoded_images: Vec<Option<DecodedImage>>,
decoded_image_refs: Vec<u32>,
}
pub fn loading_pipeline_new() -> LoadingPipeline {
LoadingPipeline {
pending: VecDeque::new(),
completed: 0,
total: 0,
last_label: None,
last_category: None,
tasks_per_frame: DEFAULT_TASKS_PER_FRAME,
next_source_image: 0,
free_source_indices: Vec::new(),
decoded_images: Vec::new(),
decoded_image_refs: Vec::new(),
}
}
pub fn loading_pipeline_push(pipeline: &mut LoadingPipeline, task: LoadingTask) {
if pipeline.pending.is_empty() && pipeline.completed == pipeline.total {
pipeline.completed = 0;
pipeline.total = 0;
}
pipeline.total += 1;
pipeline.pending.push_back(task);
}
pub fn loading_pipeline_pop(pipeline: &mut LoadingPipeline) -> Option<LoadingTask> {
pipeline.pending.pop_front()
}
pub fn loading_pipeline_mark_completed(
pipeline: &mut LoadingPipeline,
label: String,
category: &'static str,
) {
pipeline.last_label = Some(label);
pipeline.last_category = Some(category);
pipeline.completed += 1;
}
pub fn loading_pipeline_allocate_source_image(pipeline: &mut LoadingPipeline) -> SourceImageId {
if let Some(reused) = pipeline.free_source_indices.pop() {
let slot = reused as usize;
pipeline.decoded_images[slot] = None;
pipeline.decoded_image_refs[slot] = 0;
return SourceImageId(reused);
}
let id = SourceImageId(pipeline.next_source_image);
pipeline.next_source_image += 1;
loading_pipeline_grow_arena_to(pipeline, pipeline.next_source_image as usize);
id
}
pub fn loading_pipeline_grow_arena_to(pipeline: &mut LoadingPipeline, new_len: usize) {
if new_len > pipeline.decoded_images.len() {
pipeline.decoded_images.resize_with(new_len, || None);
pipeline.decoded_image_refs.resize(new_len, 0);
}
}
pub fn loading_pipeline_store_decoded(
pipeline: &mut LoadingPipeline,
id: SourceImageId,
image: DecodedImage,
) {
let slot = id.0 as usize;
loading_pipeline_grow_arena_to(pipeline, slot + 1);
pipeline.decoded_images[slot] = Some(image);
}
pub fn loading_pipeline_decoded(
pipeline: &LoadingPipeline,
id: SourceImageId,
) -> Option<&DecodedImage> {
pipeline
.decoded_images
.get(id.0 as usize)
.and_then(|slot| slot.as_ref())
}
pub fn loading_pipeline_acquire_source(pipeline: &mut LoadingPipeline, id: SourceImageId) {
let slot = id.0 as usize;
loading_pipeline_grow_arena_to(pipeline, slot + 1);
pipeline.decoded_image_refs[slot] += 1;
}
pub fn loading_pipeline_release_source(pipeline: &mut LoadingPipeline, id: SourceImageId) {
let slot = id.0 as usize;
let Some(count) = pipeline.decoded_image_refs.get_mut(slot) else {
return;
};
if *count == 0 {
return;
}
*count -= 1;
if *count == 0 {
if let Some(entry) = pipeline.decoded_images.get_mut(slot) {
*entry = None;
}
pipeline.free_source_indices.push(id.0);
}
}
pub fn loading_pipeline_acquire_recipe_sources(
pipeline: &mut LoadingPipeline,
recipe: &TextureRecipe,
) {
recipe.for_each_source(|id| loading_pipeline_acquire_source(pipeline, id));
}
pub fn loading_pipeline_release_recipe_sources(
pipeline: &mut LoadingPipeline,
recipe: &TextureRecipe,
) {
recipe.for_each_source(|id| loading_pipeline_release_source(pipeline, id));
}
pub fn loading_pipeline_reset_source_arena(pipeline: &mut LoadingPipeline) {
pipeline.next_source_image = 0;
pipeline.free_source_indices.clear();
pipeline.free_source_indices.shrink_to_fit();
pipeline.decoded_images.clear();
pipeline.decoded_images.shrink_to_fit();
pipeline.decoded_image_refs.clear();
pipeline.decoded_image_refs.shrink_to_fit();
}
pub fn loading_pipeline_pending_len(pipeline: &LoadingPipeline) -> usize {
pipeline.pending.len()
}
pub fn loading_pipeline_completed(pipeline: &LoadingPipeline) -> usize {
pipeline.completed
}
pub fn loading_pipeline_total(pipeline: &LoadingPipeline) -> usize {
pipeline.total
}
pub fn loading_pipeline_fraction(pipeline: &LoadingPipeline) -> f32 {
if pipeline.total == 0 {
return 1.0;
}
pipeline.completed as f32 / pipeline.total as f32
}
pub fn loading_pipeline_is_active(pipeline: &LoadingPipeline) -> bool {
!pipeline.pending.is_empty()
}
pub fn loading_pipeline_last_label(pipeline: &LoadingPipeline) -> Option<&str> {
pipeline.last_label.as_deref()
}
pub fn loading_pipeline_last_category(pipeline: &LoadingPipeline) -> Option<&'static str> {
pipeline.last_category
}
#[cfg(feature = "assets")]
pub fn load_texture_from_image_bytes(
world: &mut crate::ecs::world::World,
name: &str,
encoded_bytes: &[u8],
usage: TextureUsage,
sampler: SamplerSettings,
) -> Option<TextureId> {
let decoded = match image::load_from_memory(encoded_bytes) {
Ok(image) => image,
Err(error) => {
tracing::error!(?error, name, "failed to decode texture bytes");
return None;
}
};
let rgba = decoded.to_rgba8();
let (width, height) = rgba.dimensions();
let texture = queue_decoded_texture(
world,
name.to_string(),
rgba.into_raw(),
width,
height,
usage,
sampler,
);
crate::render::wgpu::texture_cache::texture_cache_add_reference(
&mut world.resources.texture_cache,
name,
);
Some(texture)
}
#[cfg(feature = "assets")]
pub fn load_texture_pack_from_image_bytes(
world: &mut crate::ecs::world::World,
entries: &[(&str, &[u8])],
usage: TextureUsage,
sampler: SamplerSettings,
) {
for (name, bytes) in entries {
load_texture_from_image_bytes(world, name, bytes, usage, sampler);
}
}
pub fn queue_decoded_texture(
world: &mut crate::ecs::world::World,
name: String,
rgba_data: Vec<u8>,
width: u32,
height: u32,
usage: TextureUsage,
sampler: SamplerSettings,
) -> TextureId {
use crate::ecs::asset_state::{TextureSourceBytes, TextureSourceData};
world.resources.assets.texture_sources.insert(
name.clone(),
TextureSourceBytes {
data: TextureSourceData::Rgba {
rgba: rgba_data.clone(),
width,
height,
},
usage,
sampler,
},
);
let texture = crate::render::wgpu::texture_cache::texture_cache_reserve_id(
&mut world.resources.texture_cache,
name,
);
loading_pipeline_push(
&mut world.resources.loading.pipeline,
LoadingTask::UploadDecodedTexture {
texture,
rgba_data,
width,
height,
usage,
sampler,
},
);
texture
}
pub fn queue_encoded_texture(
world: &mut crate::ecs::world::World,
name: String,
encoded_bytes: Vec<u8>,
usage: TextureUsage,
sampler: SamplerSettings,
) -> TextureId {
let texture = crate::render::wgpu::texture_cache::texture_cache_reserve_id(
&mut world.resources.texture_cache,
name,
);
let image = loading_pipeline_allocate_source_image(&mut world.resources.loading.pipeline);
loading_pipeline_push(
&mut world.resources.loading.pipeline,
LoadingTask::DecodeImage {
image,
encoded_bytes,
},
);
let recipe = TextureRecipe::Direct { image };
loading_pipeline_acquire_recipe_sources(&mut world.resources.loading.pipeline, &recipe);
loading_pipeline_push(
&mut world.resources.loading.pipeline,
LoadingTask::MaterializeTexture {
texture,
recipe,
usage,
sampler,
},
);
texture
}
#[cfg(feature = "assets")]
pub fn queue_gltf_load(
world: &mut crate::ecs::world::World,
result: &mut crate::ecs::prefab::GltfLoadResult,
) {
use crate::ecs::asset_state::{TextureSourceBytes, TextureSourceData};
use crate::ecs::prefab::resources::mesh_cache_insert;
let encoded = std::mem::take(&mut result.encoded_images);
let plans = std::mem::take(&mut result.texture_plan);
for plan in &plans {
if let TextureRecipe::Direct { image } = &plan.recipe {
let index = image.0 as usize;
if let Some(bytes) = encoded.get(index) {
world.resources.assets.texture_sources.insert(
plan.name.clone(),
TextureSourceBytes {
data: TextureSourceData::Png(bytes.clone()),
usage: plan.usage,
sampler: plan.sampler,
},
);
}
}
}
let mut remap: Vec<SourceImageId> = Vec::with_capacity(encoded.len());
for encoded_bytes in encoded {
let image = loading_pipeline_allocate_source_image(&mut world.resources.loading.pipeline);
remap.push(image);
loading_pipeline_push(
&mut world.resources.loading.pipeline,
LoadingTask::DecodeImage {
image,
encoded_bytes,
},
);
}
for mut plan in plans {
plan.recipe.remap_sources(&remap);
let name = plan.name.clone();
let texture = crate::render::wgpu::texture_cache::texture_cache_reserve_id(
&mut world.resources.texture_cache,
plan.name,
);
crate::render::wgpu::texture_cache::texture_cache_protect(
&mut world.resources.texture_cache,
name,
);
loading_pipeline_acquire_recipe_sources(
&mut world.resources.loading.pipeline,
&plan.recipe,
);
loading_pipeline_push(
&mut world.resources.loading.pipeline,
LoadingTask::MaterializeTexture {
texture,
recipe: plan.recipe,
usage: plan.usage,
sampler: plan.sampler,
},
);
}
for (name, mesh) in std::mem::take(&mut result.meshes) {
mesh_cache_insert(&mut world.resources.assets.mesh_cache, name, mesh);
}
}
pub fn decode_to_rgba8(encoded_bytes: &[u8]) -> Result<DecodedImage, image::ImageError> {
let dynamic = image::load_from_memory(encoded_bytes)?;
let width = dynamic.width();
let height = dynamic.height();
let rgba = dynamic.to_rgba8().into_raw();
Ok(DecodedImage {
rgba,
width,
height,
})
}
pub fn execute_texture_recipe(
recipe: &TextureRecipe,
decoded_images: &[Option<DecodedImage>],
) -> Option<DecodedImage> {
let lookup = |id: SourceImageId| -> Option<&DecodedImage> {
decoded_images
.get(id.0 as usize)
.and_then(|slot| slot.as_ref())
};
match recipe {
TextureRecipe::Direct { image } => lookup(*image).map(clone_decoded),
TextureRecipe::PackRg { a, b } => Some(pack_rg(a.and_then(lookup), b.and_then(lookup))),
TextureRecipe::PackRgbA { rgb, alpha } => {
Some(pack_rgb_a(rgb.and_then(lookup), alpha.and_then(lookup)))
}
TextureRecipe::SpecGlossBase {
diffuse,
diffuse_factor,
spec_gloss,
specular_factor,
glossiness_factor,
} => {
let (base, _, width, height) = convert_spec_gloss(
diffuse.and_then(lookup),
*diffuse_factor,
spec_gloss.and_then(lookup),
*specular_factor,
*glossiness_factor,
);
Some(DecodedImage {
rgba: base,
width,
height,
})
}
TextureRecipe::SpecGlossMr {
diffuse,
diffuse_factor,
spec_gloss,
specular_factor,
glossiness_factor,
} => {
let (_, mr, width, height) = convert_spec_gloss(
diffuse.and_then(lookup),
*diffuse_factor,
spec_gloss.and_then(lookup),
*specular_factor,
*glossiness_factor,
);
Some(DecodedImage {
rgba: mr,
width,
height,
})
}
}
}
fn clone_decoded(source: &DecodedImage) -> DecodedImage {
DecodedImage {
rgba: source.rgba.clone(),
width: source.width,
height: source.height,
}
}
fn resize_rgba(source: &DecodedImage, target_w: u32, target_h: u32) -> Vec<u8> {
if source.width == target_w && source.height == target_h {
return source.rgba.clone();
}
let mut out = vec![0u8; (target_w * target_h * 4) as usize];
for y in 0..target_h {
let sy = ((y as u64) * (source.height as u64) / (target_h as u64)) as u32;
for x in 0..target_w {
let sx = ((x as u64) * (source.width as u64) / (target_w as u64)) as u32;
let src_offset = ((sy * source.width + sx) * 4) as usize;
let dst_offset = ((y * target_w + x) * 4) as usize;
out[dst_offset..dst_offset + 4]
.copy_from_slice(&source.rgba[src_offset..src_offset + 4]);
}
}
out
}
fn pack_rg(a: Option<&DecodedImage>, b: Option<&DecodedImage>) -> DecodedImage {
let target_w = a
.map(|image| image.width)
.unwrap_or(1)
.max(b.map(|image| image.width).unwrap_or(1));
let target_h = a
.map(|image| image.height)
.unwrap_or(1)
.max(b.map(|image| image.height).unwrap_or(1));
let pixel_count = (target_w * target_h) as usize;
let mut packed = vec![255u8; pixel_count * 4];
if let Some(image) = a {
let resized = resize_rgba(image, target_w, target_h);
for index in 0..pixel_count {
packed[index * 4] = resized[index * 4];
}
}
if let Some(image) = b {
let resized = resize_rgba(image, target_w, target_h);
for index in 0..pixel_count {
packed[index * 4 + 1] = resized[index * 4 + 1];
}
}
DecodedImage {
rgba: packed,
width: target_w,
height: target_h,
}
}
fn pack_rgb_a(rgb: Option<&DecodedImage>, alpha: Option<&DecodedImage>) -> DecodedImage {
let target_w = rgb
.map(|image| image.width)
.unwrap_or(1)
.max(alpha.map(|image| image.width).unwrap_or(1));
let target_h = rgb
.map(|image| image.height)
.unwrap_or(1)
.max(alpha.map(|image| image.height).unwrap_or(1));
let pixel_count = (target_w * target_h) as usize;
let mut packed = vec![255u8; pixel_count * 4];
if let Some(image) = rgb {
let resized = resize_rgba(image, target_w, target_h);
for index in 0..pixel_count {
let offset = index * 4;
packed[offset] = resized[offset];
packed[offset + 1] = resized[offset + 1];
packed[offset + 2] = resized[offset + 2];
}
}
if let Some(image) = alpha {
let resized = resize_rgba(image, target_w, target_h);
for index in 0..pixel_count {
packed[index * 4 + 3] = resized[index * 4 + 3];
}
}
DecodedImage {
rgba: packed,
width: target_w,
height: target_h,
}
}
fn srgb_byte_to_linear(value: u8) -> f32 {
let normalized = value as f32 / 255.0;
if normalized <= 0.04045 {
normalized / 12.92
} else {
((normalized + 0.055) / 1.055).powf(2.4)
}
}
fn linear_to_srgb_byte(value: f32) -> u8 {
let clamped = value.clamp(0.0, 1.0);
let encoded = if clamped <= 0.0031308 {
clamped * 12.92
} else {
1.055 * clamped.powf(1.0 / 2.4) - 0.055
};
(encoded * 255.0).round().clamp(0.0, 255.0) as u8
}
fn sample_image_linear(
image: Option<&DecodedImage>,
x: u32,
y: u32,
target_width: u32,
target_height: u32,
rgb_is_srgb: bool,
) -> [f32; 4] {
let Some(image) = image else {
return [1.0, 1.0, 1.0, 1.0];
};
let src_x = ((x as f32 / target_width.max(1) as f32) * image.width as f32) as u32;
let src_y = ((y as f32 / target_height.max(1) as f32) * image.height as f32) as u32;
let src_x = src_x.min(image.width.saturating_sub(1));
let src_y = src_y.min(image.height.saturating_sub(1));
let pixel_index = ((src_y * image.width + src_x) * 4) as usize;
if pixel_index + 3 >= image.rgba.len() {
return [1.0, 1.0, 1.0, 1.0];
}
let r = image.rgba[pixel_index];
let g = image.rgba[pixel_index + 1];
let b = image.rgba[pixel_index + 2];
let a = image.rgba[pixel_index + 3];
let r_lin = if rgb_is_srgb {
srgb_byte_to_linear(r)
} else {
r as f32 / 255.0
};
let g_lin = if rgb_is_srgb {
srgb_byte_to_linear(g)
} else {
g as f32 / 255.0
};
let b_lin = if rgb_is_srgb {
srgb_byte_to_linear(b)
} else {
b as f32 / 255.0
};
[r_lin, g_lin, b_lin, a as f32 / 255.0]
}
fn solve_metallic_for_spec_gloss(
diffuse: f32,
specular: f32,
one_minus_specular_strength: f32,
) -> f32 {
const DIELECTRIC_SPECULAR: f32 = 0.04;
if specular < DIELECTRIC_SPECULAR {
return 0.0;
}
let a = DIELECTRIC_SPECULAR;
let b = diffuse * one_minus_specular_strength / (1.0 - DIELECTRIC_SPECULAR) + specular
- 2.0 * DIELECTRIC_SPECULAR;
let c = DIELECTRIC_SPECULAR - specular;
let discriminant = (b * b - 4.0 * a * c).max(0.0);
((-b + discriminant.sqrt()) / (2.0 * a)).clamp(0.0, 1.0)
}
fn spec_gloss_scalar(diffuse: [f32; 4], specular: [f32; 3]) -> ([f32; 3], f32) {
const DIELECTRIC_SPECULAR: f32 = 0.04;
const EPSILON: f32 = 1e-6;
let max_specular = specular[0].max(specular[1]).max(specular[2]);
let one_minus_specular_strength = 1.0 - max_specular;
let max_diffuse = diffuse[0].max(diffuse[1]).max(diffuse[2]);
let metallic =
solve_metallic_for_spec_gloss(max_diffuse, max_specular, one_minus_specular_strength);
let base_from_diffuse = |c: f32| -> f32 {
c * one_minus_specular_strength
/ (1.0 - DIELECTRIC_SPECULAR).max(EPSILON)
/ (1.0 - metallic).max(EPSILON)
};
let base_from_specular =
|c: f32| -> f32 { (c - DIELECTRIC_SPECULAR * (1.0 - metallic)) / metallic.max(EPSILON) };
let blend = metallic * metallic;
let lerp = |a: f32, b: f32, t: f32| a + (b - a) * t;
let base_color = [
lerp(
base_from_diffuse(diffuse[0]),
base_from_specular(specular[0]),
blend,
)
.clamp(0.0, 1.0),
lerp(
base_from_diffuse(diffuse[1]),
base_from_specular(specular[1]),
blend,
)
.clamp(0.0, 1.0),
lerp(
base_from_diffuse(diffuse[2]),
base_from_specular(specular[2]),
blend,
)
.clamp(0.0, 1.0),
];
(base_color, metallic)
}
fn convert_spec_gloss(
diffuse_image: Option<&DecodedImage>,
diffuse_factor: [f32; 4],
spec_gloss_image: Option<&DecodedImage>,
specular_factor: [f32; 3],
glossiness_factor: f32,
) -> (Vec<u8>, Vec<u8>, u32, u32) {
let width = diffuse_image
.map(|image| image.width)
.unwrap_or(1)
.max(spec_gloss_image.map(|image| image.width).unwrap_or(1));
let height = diffuse_image
.map(|image| image.height)
.unwrap_or(1)
.max(spec_gloss_image.map(|image| image.height).unwrap_or(1));
let texel_count = (width as usize) * (height as usize);
let mut base_rgba = vec![0u8; texel_count * 4];
let mut mr_rgba = vec![0u8; texel_count * 4];
for y in 0..height {
for x in 0..width {
let dst_idx = ((y * width + x) * 4) as usize;
let diffuse_sample = sample_image_linear(diffuse_image, x, y, width, height, true);
let spec_gloss_sample =
sample_image_linear(spec_gloss_image, x, y, width, height, true);
let diffuse = [
diffuse_sample[0] * diffuse_factor[0],
diffuse_sample[1] * diffuse_factor[1],
diffuse_sample[2] * diffuse_factor[2],
diffuse_sample[3] * diffuse_factor[3],
];
let specular = [
spec_gloss_sample[0] * specular_factor[0],
spec_gloss_sample[1] * specular_factor[1],
spec_gloss_sample[2] * specular_factor[2],
];
let glossiness = (spec_gloss_sample[3] * glossiness_factor).clamp(0.0, 1.0);
let (base_rgb, metallic) = spec_gloss_scalar(diffuse, specular);
base_rgba[dst_idx] = linear_to_srgb_byte(base_rgb[0]);
base_rgba[dst_idx + 1] = linear_to_srgb_byte(base_rgb[1]);
base_rgba[dst_idx + 2] = linear_to_srgb_byte(base_rgb[2]);
base_rgba[dst_idx + 3] = (diffuse[3].clamp(0.0, 1.0) * 255.0).round() as u8;
let roughness = 1.0 - glossiness;
mr_rgba[dst_idx] = 0;
mr_rgba[dst_idx + 1] = (roughness.clamp(0.0, 1.0) * 255.0).round() as u8;
mr_rgba[dst_idx + 2] = (metallic.clamp(0.0, 1.0) * 255.0).round() as u8;
mr_rgba[dst_idx + 3] = 255;
}
}
(base_rgba, mr_rgba, width, height)
}