nightshade-api 0.46.0

Procedural high level API for the nightshade game engine
Documentation
//! Per-entity looks. The first call on an entity gives it its own material,
//! so coloring one cube never recolors another.

use crate::scene::api_material_name;
use nightshade::ecs::material::components::AlphaMode;
use nightshade::prelude::*;
use nightshade::render::wgpu::texture_cache::{
    SamplerSettings, TextureUsage, texture_cache_remove_reference,
};

/// Sets the entity's base color as linear RGBA.
pub fn set_color(world: &mut World, entity: Entity, color: [f32; 4]) {
    mutate_material(world, entity, |material| {
        material.base_color = color;
    });
}

/// Sets the entity's metallic and roughness factors, both 0.0 to 1.0.
pub fn set_metallic_roughness(world: &mut World, entity: Entity, metallic: f32, roughness: f32) {
    mutate_material(world, entity, |material| {
        material.metallic = metallic;
        material.roughness = roughness;
    });
}

/// Makes the entity glow with the given RGB color and strength. Pairs well
/// with [`set_bloom`](crate::prelude::set_bloom).
pub fn set_emissive(world: &mut World, entity: Entity, color: [f32; 3], strength: f32) {
    mutate_material(world, entity, |material| {
        material.emissive_factor = color;
        material.emissive_strength = strength;
    });
}

/// Disables lighting on the entity so its color renders as is.
pub fn set_unlit(world: &mut World, entity: Entity, unlit: bool) {
    mutate_material(world, entity, |material| {
        material.unlit = unlit;
    });
}

/// Sets the entity's base color texture by name. The built in procedural
/// textures `"checkerboard"`, `"gradient"`, and `"uv_test"` are always
/// available, and [`load_texture`] registers your own.
pub fn set_texture(world: &mut World, entity: Entity, texture_name: &str) {
    let name = texture_name.to_string();
    mutate_material(world, entity, move |material| {
        material.base_texture = Some(name);
    });
}

/// Tiles the entity's base color texture `repeats` times across each axis, so
/// a small prototype texture reads as a fine grid on a large surface instead of
/// stretching. The built in textures sample with a repeating wrap, so any value
/// above 1.0 tiles cleanly.
pub fn set_texture_tiling(world: &mut World, entity: Entity, repeats: f32) {
    mutate_material(world, entity, |material| {
        material.base_texture_transform.scale = [repeats, repeats];
    });
}

/// Sets the entity's normal map by texture name. Load normal maps with
/// [`load_texture_linear`], not [`load_texture`], so they are not gamma
/// decoded.
pub fn set_normal_texture(world: &mut World, entity: Entity, texture_name: &str) {
    let name = texture_name.to_string();
    mutate_material(world, entity, move |material| {
        material.normal_texture = Some(name);
    });
}

/// Sets the entity's metallic and roughness map by texture name. The engine
/// reads metalness from the blue channel and roughness from the green, the glTF
/// convention. Load it with [`load_texture_linear`].
pub fn set_metallic_roughness_texture(world: &mut World, entity: Entity, texture_name: &str) {
    let name = texture_name.to_string();
    mutate_material(world, entity, move |material| {
        material.metallic_roughness_texture = Some(name);
    });
}

/// Sets the entity's emissive map by texture name, the glow pattern modulated by
/// [`set_emissive`]. Load it with [`load_texture`], it is color data.
pub fn set_emissive_texture(world: &mut World, entity: Entity, texture_name: &str) {
    let name = texture_name.to_string();
    mutate_material(world, entity, move |material| {
        material.emissive_texture = Some(name);
    });
}

/// Sets the entity's ambient occlusion map by texture name. Load it with
/// [`load_texture_linear`].
pub fn set_occlusion_texture(world: &mut World, entity: Entity, texture_name: &str) {
    let name = texture_name.to_string();
    mutate_material(world, entity, move |material| {
        material.occlusion_texture = Some(name);
    });
}

/// Registers a texture under `name` from encoded png or jpeg bytes. The
/// texture decodes in the background and stays resident until shutdown. Use this
/// for color data (base color, emissive).
pub fn load_texture(world: &mut World, name: &str, image_bytes: &[u8]) {
    load_texture_with_usage(world, name, image_bytes, TextureUsage::Color);
}

/// Registers a texture under `name` in linear space, for data that is not color:
/// normal maps, metallic-roughness maps, and ambient occlusion. Same lifetime as
/// [`load_texture`].
pub fn load_texture_linear(world: &mut World, name: &str, image_bytes: &[u8]) {
    load_texture_with_usage(world, name, image_bytes, TextureUsage::Linear);
}

fn load_texture_with_usage(world: &mut World, name: &str, image_bytes: &[u8], usage: TextureUsage) {
    nightshade::ecs::loading::queue_encoded_texture(
        world,
        name.to_string(),
        image_bytes.to_vec(),
        usage,
        SamplerSettings::DEFAULT,
    );
    texture_cache_add_reference(&mut world.resources.texture_cache, name);
}

/// Registers a texture under `name` from raw RGBA8 pixels, four bytes per pixel
/// in row-major order, `width` by `height`. For textures generated at runtime,
/// where [`load_texture`] (which decodes png or jpeg bytes) does not fit. Treated
/// as color data; the texture stays resident until shutdown.
pub fn register_texture(world: &mut World, name: &str, width: u32, height: u32, rgba: &[u8]) {
    nightshade::ecs::loading::queue_decoded_texture(
        world,
        name.to_string(),
        rgba.to_vec(),
        width,
        height,
        TextureUsage::Color,
        SamplerSettings::DEFAULT,
    );
    texture_cache_add_reference(&mut world.resources.texture_cache, name);
}

/// Turns alpha blending on or off for the entity. Blended surfaces are
/// transparent by their base color alpha and sort back to front, for glass,
/// water, and fades. Off restores opaque rendering.
pub fn set_alpha_blend(world: &mut World, entity: Entity, enabled: bool) {
    mutate_material(world, entity, move |material| {
        material.alpha_mode = if enabled {
            AlphaMode::Blend
        } else {
            AlphaMode::Opaque
        };
    });
}

/// Switches the entity to alpha cutout: any texel below `cutoff` alpha is
/// discarded with a hard edge, for foliage, fences, and decals. No sorting cost,
/// unlike blending.
pub fn set_alpha_cutoff(world: &mut World, entity: Entity, cutoff: f32) {
    mutate_material(world, entity, move |material| {
        material.alpha_mode = AlphaMode::Mask;
        material.alpha_cutoff = cutoff;
    });
}

/// Renders both faces of the entity's triangles, so a single sided mesh like a
/// plane or a leaf card is lit and visible from behind.
pub fn set_double_sided(world: &mut World, entity: Entity, double_sided: bool) {
    mutate_material(world, entity, move |material| {
        material.double_sided = double_sided;
    });
}

/// Sets the index of refraction for the entity's surface, which shapes Fresnel
/// reflectance and transmission. Glass is around 1.5, water around 1.33.
pub fn set_ior(world: &mut World, entity: Entity, ior: f32) {
    mutate_material(world, entity, move |material| {
        material.ior = ior;
    });
}

/// Sets how much light passes through the entity, 0.0 opaque to 1.0 fully
/// transmissive, for glass and clear plastics. Pair with [`set_ior`].
pub fn set_transmission(world: &mut World, entity: Entity, factor: f32) {
    mutate_material(world, entity, move |material| {
        material.transmission_factor = factor;
    });
}

/// Adds a clearcoat layer over the entity: a thin glossy coat with its own
/// `factor` (0.0 to 1.0) and `roughness`, for car paint and lacquer.
pub fn set_clearcoat(world: &mut World, entity: Entity, factor: f32, roughness: f32) {
    mutate_material(world, entity, move |material| {
        material.clearcoat_factor = factor;
        material.clearcoat_roughness_factor = roughness;
    });
}

/// Sets anisotropic reflection on the entity: `strength` (0.0 to 1.0) stretches
/// highlights along `rotation` radians, for brushed metal and hair.
pub fn set_anisotropy(world: &mut World, entity: Entity, strength: f32, rotation: f32) {
    mutate_material(world, entity, move |material| {
        material.anisotropy_strength = strength;
        material.anisotropy_rotation = rotation;
    });
}

/// Transforms the entity's base color texture coordinates: `offset` shifts,
/// `scale` tiles, `rotation` spins in radians. The companion to
/// [`set_texture_tiling`] when you need offset or rotation too.
pub fn set_uv_transform(
    world: &mut World,
    entity: Entity,
    offset: [f32; 2],
    scale: [f32; 2],
    rotation: f32,
) {
    mutate_material(world, entity, move |material| {
        material.base_texture_transform.offset = offset;
        material.base_texture_transform.scale = scale;
        material.base_texture_transform.rotation = rotation;
    });
}

/// Adds a sheen layer to the entity: a soft retroreflective tint of linear RGB
/// `color` at `roughness`, for cloth, velvet, and satin.
pub fn set_sheen(world: &mut World, entity: Entity, color: [f32; 3], roughness: f32) {
    mutate_material(world, entity, move |material| {
        material.sheen_color_factor = color;
        material.sheen_roughness_factor = roughness;
    });
}

/// Adds a thin-film iridescence to the entity: `factor` (0.0 to 1.0) strength at
/// the given `ior`, for soap bubbles, oil slicks, and beetle shells.
pub fn set_iridescence(world: &mut World, entity: Entity, factor: f32, ior: f32) {
    mutate_material(world, entity, move |material| {
        material.iridescence_factor = factor;
        material.iridescence_ior = ior;
    });
}

/// Sets the entity's specular reflectance: `factor` scales it, `color` tints it
/// (linear RGB), for fine control over the non-metallic highlight.
pub fn set_specular(world: &mut World, entity: Entity, factor: f32, color: [f32; 3]) {
    mutate_material(world, entity, move |material| {
        material.specular_factor = factor;
        material.specular_color_factor = color;
    });
}

/// Scales the strength of the entity's normal map, exaggerating or flattening
/// its surface detail.
pub fn set_normal_scale(world: &mut World, entity: Entity, scale: f32) {
    mutate_material(world, entity, move |material| {
        material.normal_scale = scale;
    });
}

/// Scales how strongly the entity's ambient occlusion map darkens it.
pub fn set_occlusion_strength(world: &mut World, entity: Entity, strength: f32) {
    mutate_material(world, entity, move |material| {
        material.occlusion_strength = strength;
    });
}

/// Sets the entity's emissive strength on its own, the multiplier on the glow
/// color, without retinting it the way [`set_emissive`] does.
pub fn set_emissive_strength(world: &mut World, entity: Entity, strength: f32) {
    mutate_material(world, entity, move |material| {
        material.emissive_strength = strength;
    });
}

/// Sets the volume thickness of a transmissive entity, how far light travels
/// through it before the attenuation color takes hold. Pairs with
/// [`set_transmission`] and [`set_ior`] for tinted glass.
pub fn set_thickness(world: &mut World, entity: Entity, thickness: f32) {
    mutate_material(world, entity, move |material| {
        material.thickness = thickness;
    });
}

fn owns_material(material_name: &str, entity: Entity) -> bool {
    material_name
        .strip_prefix(crate::runner::MATERIAL_PREFIX)
        .and_then(|suffix| suffix.parse().ok())
        == Some(entity.id)
}

pub(crate) fn owned_color(world: &mut World, entity: Entity) -> Option<[f32; 4]> {
    let material_ref = world.core.get_material_ref(entity).cloned()?;
    if !owns_material(&material_ref.name, entity) {
        let current = registry_entry_by_name(
            &world.resources.assets.material_registry.registry,
            &material_ref.name,
        )
        .map(|material| material.base_color)
        .unwrap_or([1.0, 1.0, 1.0, 1.0]);
        set_color(world, entity, current);
        return Some(current);
    }
    registry_entry_by_name(
        &world.resources.assets.material_registry.registry,
        &material_ref.name,
    )
    .map(|material| material.base_color)
}

fn mutate_material(world: &mut World, entity: Entity, apply: impl FnOnce(&mut Material)) {
    let Some(material_ref) = world.core.get_material_ref(entity).cloned() else {
        return;
    };

    if owns_material(&material_ref.name, entity) {
        let Some(mut material) = registry_entry_by_name(
            &world.resources.assets.material_registry.registry,
            &material_ref.name,
        )
        .cloned() else {
            return;
        };
        let old_textures: Vec<String> = material.texture_names().map(str::to_string).collect();
        apply(&mut material);
        let new_textures: Vec<String> = material.texture_names().map(str::to_string).collect();
        swap_texture_references(world, &old_textures, &new_textures);
        if let Some(existing) = registry_entry_by_name_mut(
            &mut world.resources.assets.material_registry.registry,
            &material_ref.name,
        ) {
            *existing = material;
        }
        world
            .resources
            .mesh_render_state
            .mark_material_dirty(entity);
    } else {
        let mut material = registry_entry_by_name(
            &world.resources.assets.material_registry.registry,
            &material_ref.name,
        )
        .cloned()
        .unwrap_or_default();
        apply(&mut material);
        let textures: Vec<String> = material.texture_names().map(str::to_string).collect();
        for texture in &textures {
            texture_cache_add_reference(&mut world.resources.texture_cache, texture);
        }
        if let Some((index, _)) = registry_lookup_index(
            &world.resources.assets.material_registry.registry,
            &material_ref.name,
        ) {
            registry_remove_reference(
                &mut world.resources.assets.material_registry.registry,
                index,
            );
        }
        register_material(world, entity, api_material_name(entity), material);
    }
}

fn swap_texture_references(world: &mut World, old_textures: &[String], new_textures: &[String]) {
    for texture in old_textures {
        if !new_textures.contains(texture) {
            texture_cache_remove_reference(&mut world.resources.texture_cache, texture);
        }
    }
    for texture in new_textures {
        if !old_textures.contains(texture) {
            texture_cache_add_reference(&mut world.resources.texture_cache, texture);
        }
    }
}