Skip to main content

nightshade_api/
appearance.rs

1//! Per-entity looks. The first call on an entity gives it its own material,
2//! so coloring one cube never recolors another.
3
4use crate::scene::api_material_name;
5use nightshade::prelude::*;
6use nightshade::render::wgpu::texture_cache::{
7    SamplerSettings, TextureUsage, texture_cache_remove_reference,
8};
9
10/// Sets the entity's base color as linear RGBA.
11pub fn set_color(world: &mut World, entity: Entity, color: [f32; 4]) {
12    mutate_material(world, entity, |material| {
13        material.base_color = color;
14    });
15}
16
17/// Sets the entity's metallic and roughness factors, both 0.0 to 1.0.
18pub fn set_metallic_roughness(world: &mut World, entity: Entity, metallic: f32, roughness: f32) {
19    mutate_material(world, entity, |material| {
20        material.metallic = metallic;
21        material.roughness = roughness;
22    });
23}
24
25/// Makes the entity glow with the given RGB color and strength. Pairs well
26/// with [`set_bloom`](crate::prelude::set_bloom).
27pub fn set_emissive(world: &mut World, entity: Entity, color: [f32; 3], strength: f32) {
28    mutate_material(world, entity, |material| {
29        material.emissive_factor = color;
30        material.emissive_strength = strength;
31    });
32}
33
34/// Disables lighting on the entity so its color renders as is.
35pub fn set_unlit(world: &mut World, entity: Entity, unlit: bool) {
36    mutate_material(world, entity, |material| {
37        material.unlit = unlit;
38    });
39}
40
41/// Sets the entity's base color texture by name. The built in procedural
42/// textures `"checkerboard"`, `"gradient"`, and `"uv_test"` are always
43/// available, and [`load_texture`] registers your own.
44pub fn set_texture(world: &mut World, entity: Entity, texture_name: &str) {
45    let name = texture_name.to_string();
46    mutate_material(world, entity, move |material| {
47        material.base_texture = Some(name);
48    });
49}
50
51/// Tiles the entity's base color texture `repeats` times across each axis, so
52/// a small prototype texture reads as a fine grid on a large surface instead of
53/// stretching. The built in textures sample with a repeating wrap, so any value
54/// above 1.0 tiles cleanly.
55pub fn set_texture_tiling(world: &mut World, entity: Entity, repeats: f32) {
56    mutate_material(world, entity, |material| {
57        material.base_texture_transform.scale = [repeats, repeats];
58    });
59}
60
61/// Registers a texture under `name` from encoded png or jpeg bytes. The
62/// texture decodes in the background and stays resident until shutdown.
63pub fn load_texture(world: &mut World, name: &str, image_bytes: &[u8]) {
64    nightshade::ecs::loading::queue_encoded_texture(
65        world,
66        name.to_string(),
67        image_bytes.to_vec(),
68        TextureUsage::Color,
69        SamplerSettings::DEFAULT,
70    );
71    texture_cache_add_reference(&mut world.resources.texture_cache, name);
72}
73
74fn owns_material(material_name: &str, entity: Entity) -> bool {
75    material_name
76        .strip_prefix(crate::runner::MATERIAL_PREFIX)
77        .and_then(|suffix| suffix.parse().ok())
78        == Some(entity.id)
79}
80
81pub(crate) fn owned_color(world: &mut World, entity: Entity) -> Option<[f32; 4]> {
82    let material_ref = world.core.get_material_ref(entity).cloned()?;
83    if !owns_material(&material_ref.name, entity) {
84        let current = registry_entry_by_name(
85            &world.resources.assets.material_registry.registry,
86            &material_ref.name,
87        )
88        .map(|material| material.base_color)
89        .unwrap_or([1.0, 1.0, 1.0, 1.0]);
90        set_color(world, entity, current);
91        return Some(current);
92    }
93    registry_entry_by_name(
94        &world.resources.assets.material_registry.registry,
95        &material_ref.name,
96    )
97    .map(|material| material.base_color)
98}
99
100fn mutate_material(world: &mut World, entity: Entity, apply: impl FnOnce(&mut Material)) {
101    let Some(material_ref) = world.core.get_material_ref(entity).cloned() else {
102        return;
103    };
104
105    if owns_material(&material_ref.name, entity) {
106        let Some(mut material) = registry_entry_by_name(
107            &world.resources.assets.material_registry.registry,
108            &material_ref.name,
109        )
110        .cloned() else {
111            return;
112        };
113        let old_textures: Vec<String> = material.texture_names().map(str::to_string).collect();
114        apply(&mut material);
115        let new_textures: Vec<String> = material.texture_names().map(str::to_string).collect();
116        swap_texture_references(world, &old_textures, &new_textures);
117        if let Some(existing) = registry_entry_by_name_mut(
118            &mut world.resources.assets.material_registry.registry,
119            &material_ref.name,
120        ) {
121            *existing = material;
122        }
123        world
124            .resources
125            .mesh_render_state
126            .mark_material_dirty(entity);
127    } else {
128        let mut material = registry_entry_by_name(
129            &world.resources.assets.material_registry.registry,
130            &material_ref.name,
131        )
132        .cloned()
133        .unwrap_or_default();
134        apply(&mut material);
135        let textures: Vec<String> = material.texture_names().map(str::to_string).collect();
136        for texture in &textures {
137            texture_cache_add_reference(&mut world.resources.texture_cache, texture);
138        }
139        if let Some((index, _)) = registry_lookup_index(
140            &world.resources.assets.material_registry.registry,
141            &material_ref.name,
142        ) {
143            registry_remove_reference(
144                &mut world.resources.assets.material_registry.registry,
145                index,
146            );
147        }
148        register_material(world, entity, api_material_name(entity), material);
149    }
150}
151
152fn swap_texture_references(world: &mut World, old_textures: &[String], new_textures: &[String]) {
153    for texture in old_textures {
154        if !new_textures.contains(texture) {
155            texture_cache_remove_reference(&mut world.resources.texture_cache, texture);
156        }
157    }
158    for texture in new_textures {
159        if !old_textures.contains(texture) {
160            texture_cache_add_reference(&mut world.resources.texture_cache, texture);
161        }
162    }
163}