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::ecs::material::resources::material_registry_mutate;
6use nightshade::prelude::*;
7use nightshade::render::material::AlphaMode;
8use nightshade::render::texture_data::{SamplerSettings, TextureUsage};
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/// Sets the entity's normal map by texture name. Load normal maps with
62/// [`load_texture_linear`], not [`load_texture`], so they are not gamma
63/// decoded.
64pub fn set_normal_texture(world: &mut World, entity: Entity, texture_name: &str) {
65    let name = texture_name.to_string();
66    mutate_material(world, entity, move |material| {
67        material.normal_texture = Some(name);
68    });
69}
70
71/// Sets the entity's metallic and roughness map by texture name. The engine
72/// reads metalness from the blue channel and roughness from the green, the glTF
73/// convention. Load it with [`load_texture_linear`].
74pub fn set_metallic_roughness_texture(world: &mut World, entity: Entity, texture_name: &str) {
75    let name = texture_name.to_string();
76    mutate_material(world, entity, move |material| {
77        material.metallic_roughness_texture = Some(name);
78    });
79}
80
81/// Sets the entity's emissive map by texture name, the glow pattern modulated by
82/// [`set_emissive`]. Load it with [`load_texture`], it is color data.
83pub fn set_emissive_texture(world: &mut World, entity: Entity, texture_name: &str) {
84    let name = texture_name.to_string();
85    mutate_material(world, entity, move |material| {
86        material.emissive_texture = Some(name);
87    });
88}
89
90/// Sets the entity's ambient occlusion map by texture name. Load it with
91/// [`load_texture_linear`].
92pub fn set_occlusion_texture(world: &mut World, entity: Entity, texture_name: &str) {
93    let name = texture_name.to_string();
94    mutate_material(world, entity, move |material| {
95        material.occlusion_texture = Some(name);
96    });
97}
98
99/// Registers a texture under `name` from encoded png or jpeg bytes. The
100/// texture decodes in the background and stays resident until shutdown. Use this
101/// for color data (base color, emissive).
102pub fn load_texture(world: &mut World, name: &str, image_bytes: &[u8]) {
103    load_texture_with_usage(world, name, image_bytes, TextureUsage::Color);
104}
105
106/// Registers a texture under `name` in linear space, for data that is not color:
107/// normal maps, metallic-roughness maps, and ambient occlusion. Same lifetime as
108/// [`load_texture`].
109pub fn load_texture_linear(world: &mut World, name: &str, image_bytes: &[u8]) {
110    load_texture_with_usage(world, name, image_bytes, TextureUsage::Linear);
111}
112
113fn load_texture_with_usage(world: &mut World, name: &str, image_bytes: &[u8], usage: TextureUsage) {
114    nightshade::ecs::loading::queue_encoded_texture(
115        world,
116        name.to_string(),
117        image_bytes.to_vec(),
118        usage,
119        SamplerSettings::DEFAULT,
120    );
121    texture_cache_acquire(
122        world.res_mut::<nightshade::render::wgpu::texture_cache::TextureCache>(),
123        TextureOwner::Named(name.to_string()),
124        vec![name.to_string()],
125    );
126}
127
128/// Registers a texture under `name` from raw RGBA8 pixels, four bytes per pixel
129/// in row-major order, `width` by `height`. For textures generated at runtime,
130/// where [`load_texture`] (which decodes png or jpeg bytes) does not fit. Treated
131/// as color data; the texture stays resident until shutdown.
132pub fn register_texture(world: &mut World, name: &str, width: u32, height: u32, rgba: &[u8]) {
133    nightshade::ecs::loading::queue_decoded_texture(
134        world,
135        name.to_string(),
136        rgba.to_vec(),
137        width,
138        height,
139        TextureUsage::Color,
140        SamplerSettings::DEFAULT,
141    );
142    texture_cache_acquire(
143        world.res_mut::<nightshade::render::wgpu::texture_cache::TextureCache>(),
144        TextureOwner::Named(name.to_string()),
145        vec![name.to_string()],
146    );
147}
148
149/// Turns alpha blending on or off for the entity. Blended surfaces are
150/// transparent by their base color alpha and sort back to front, for glass,
151/// water, and fades. Off restores opaque rendering.
152pub fn set_alpha_blend(world: &mut World, entity: Entity, enabled: bool) {
153    mutate_material(world, entity, move |material| {
154        material.alpha_mode = if enabled {
155            AlphaMode::Blend
156        } else {
157            AlphaMode::Opaque
158        };
159    });
160}
161
162/// Switches the entity to alpha cutout: any texel below `cutoff` alpha is
163/// discarded with a hard edge, for foliage, fences, and decals. No sorting cost,
164/// unlike blending.
165pub fn set_alpha_cutoff(world: &mut World, entity: Entity, cutoff: f32) {
166    mutate_material(world, entity, move |material| {
167        material.alpha_mode = AlphaMode::Mask;
168        material.alpha_cutoff = cutoff;
169    });
170}
171
172/// Renders both faces of the entity's triangles, so a single sided mesh like a
173/// plane or a leaf card is lit and visible from behind.
174pub fn set_double_sided(world: &mut World, entity: Entity, double_sided: bool) {
175    mutate_material(world, entity, move |material| {
176        material.double_sided = double_sided;
177    });
178}
179
180/// Sets the index of refraction for the entity's surface, which shapes Fresnel
181/// reflectance and transmission. Glass is around 1.5, water around 1.33.
182pub fn set_ior(world: &mut World, entity: Entity, ior: f32) {
183    mutate_material(world, entity, move |material| {
184        material.ior = ior;
185    });
186}
187
188/// Sets how much light passes through the entity, 0.0 opaque to 1.0 fully
189/// transmissive, for glass and clear plastics. Pair with [`set_ior`].
190pub fn set_transmission(world: &mut World, entity: Entity, factor: f32) {
191    mutate_material(world, entity, move |material| {
192        material.transmission_factor = factor;
193    });
194}
195
196/// Adds a clearcoat layer over the entity: a thin glossy coat with its own
197/// `factor` (0.0 to 1.0) and `roughness`, for car paint and lacquer.
198pub fn set_clearcoat(world: &mut World, entity: Entity, factor: f32, roughness: f32) {
199    mutate_material(world, entity, move |material| {
200        material.clearcoat_factor = factor;
201        material.clearcoat_roughness_factor = roughness;
202    });
203}
204
205/// Sets anisotropic reflection on the entity: `strength` (0.0 to 1.0) stretches
206/// highlights along `rotation` radians, for brushed metal and hair.
207pub fn set_anisotropy(world: &mut World, entity: Entity, strength: f32, rotation: f32) {
208    mutate_material(world, entity, move |material| {
209        material.anisotropy_strength = strength;
210        material.anisotropy_rotation = rotation;
211    });
212}
213
214/// Transforms the entity's base color texture coordinates: `offset` shifts,
215/// `scale` tiles, `rotation` spins in radians. The companion to
216/// [`set_texture_tiling`] when you need offset or rotation too.
217pub fn set_uv_transform(
218    world: &mut World,
219    entity: Entity,
220    offset: [f32; 2],
221    scale: [f32; 2],
222    rotation: f32,
223) {
224    mutate_material(world, entity, move |material| {
225        material.base_texture_transform.offset = offset;
226        material.base_texture_transform.scale = scale;
227        material.base_texture_transform.rotation = rotation;
228    });
229}
230
231/// Adds a sheen layer to the entity: a soft retroreflective tint of linear RGB
232/// `color` at `roughness`, for cloth, velvet, and satin.
233pub fn set_sheen(world: &mut World, entity: Entity, color: [f32; 3], roughness: f32) {
234    mutate_material(world, entity, move |material| {
235        material.sheen_color_factor = color;
236        material.sheen_roughness_factor = roughness;
237    });
238}
239
240/// Adds a thin-film iridescence to the entity: `factor` (0.0 to 1.0) strength at
241/// the given `ior`, for soap bubbles, oil slicks, and beetle shells.
242pub fn set_iridescence(world: &mut World, entity: Entity, factor: f32, ior: f32) {
243    mutate_material(world, entity, move |material| {
244        material.iridescence_factor = factor;
245        material.iridescence_ior = ior;
246    });
247}
248
249/// Sets the entity's specular reflectance: `factor` scales it, `color` tints it
250/// (linear RGB), for fine control over the non-metallic highlight.
251pub fn set_specular(world: &mut World, entity: Entity, factor: f32, color: [f32; 3]) {
252    mutate_material(world, entity, move |material| {
253        material.specular_factor = factor;
254        material.specular_color_factor = color;
255    });
256}
257
258/// Scales the strength of the entity's normal map, exaggerating or flattening
259/// its surface detail.
260pub fn set_normal_scale(world: &mut World, entity: Entity, scale: f32) {
261    mutate_material(world, entity, move |material| {
262        material.normal_scale = scale;
263    });
264}
265
266/// Scales how strongly the entity's ambient occlusion map darkens it.
267pub fn set_occlusion_strength(world: &mut World, entity: Entity, strength: f32) {
268    mutate_material(world, entity, move |material| {
269        material.occlusion_strength = strength;
270    });
271}
272
273/// Sets the entity's emissive strength on its own, the multiplier on the glow
274/// color, without retinting it the way [`set_emissive`] does.
275pub fn set_emissive_strength(world: &mut World, entity: Entity, strength: f32) {
276    mutate_material(world, entity, move |material| {
277        material.emissive_strength = strength;
278    });
279}
280
281/// Sets the volume thickness of a transmissive entity, how far light travels
282/// through it before the attenuation color takes hold. Pairs with
283/// [`set_transmission`] and [`set_ior`] for tinted glass.
284pub fn set_thickness(world: &mut World, entity: Entity, thickness: f32) {
285    mutate_material(world, entity, move |material| {
286        material.thickness = thickness;
287    });
288}
289
290fn owns_material(material_name: &str, entity: Entity) -> bool {
291    material_name
292        .strip_prefix(crate::runner::MATERIAL_PREFIX)
293        .and_then(|suffix| suffix.parse().ok())
294        == Some(entity.id)
295}
296
297pub(crate) fn owned_color(world: &mut World, entity: Entity) -> Option<[f32; 4]> {
298    let material_ref = world
299        .get::<nightshade::ecs::material::components::MaterialRef>(entity)
300        .cloned()?;
301    if !owns_material(&material_ref.name, entity) {
302        let current = registry_entry_by_name(
303            &world
304                .res::<nightshade::ecs::asset_state::AssetState>()
305                .material_registry
306                .registry,
307            &material_ref.name,
308        )
309        .map(|material| material.base_color)
310        .unwrap_or([1.0, 1.0, 1.0, 1.0]);
311        set_color(world, entity, current);
312        return Some(current);
313    }
314    registry_entry_by_name(
315        &world
316            .res::<nightshade::ecs::asset_state::AssetState>()
317            .material_registry
318            .registry,
319        &material_ref.name,
320    )
321    .map(|material| material.base_color)
322}
323
324fn mutate_material(world: &mut World, entity: Entity, apply: impl FnOnce(&mut Material)) {
325    let Some(material_ref) = world
326        .get::<nightshade::ecs::material::components::MaterialRef>(entity)
327        .cloned()
328    else {
329        return;
330    };
331
332    if owns_material(&material_ref.name, entity) {
333        if !material_registry_mutate(
334            &mut world
335                .res_mut::<nightshade::ecs::asset_state::AssetState>()
336                .material_registry,
337            &material_ref.name,
338            apply,
339        ) {
340            return;
341        }
342        let new_textures: Vec<String> = registry_entry_by_name(
343            &world
344                .res::<nightshade::ecs::asset_state::AssetState>()
345                .material_registry
346                .registry,
347            &material_ref.name,
348        )
349        .map(|material| material.texture_names().map(str::to_string).collect())
350        .unwrap_or_default();
351        texture_cache_acquire(
352            world.res_mut::<nightshade::render::wgpu::texture_cache::TextureCache>(),
353            TextureOwner::EntityMaterial(render_entity(entity)),
354            new_textures,
355        );
356        world
357            .res_mut::<nightshade::render::mesh_state::MeshRenderState>()
358            .mark_material_dirty(render_entity(entity));
359    } else {
360        let mut material = registry_entry_by_name(
361            &world
362                .res::<nightshade::ecs::asset_state::AssetState>()
363                .material_registry
364                .registry,
365            &material_ref.name,
366        )
367        .cloned()
368        .unwrap_or_default();
369        apply(&mut material);
370        let textures: Vec<String> = material.texture_names().map(str::to_string).collect();
371        texture_cache_acquire(
372            world.res_mut::<nightshade::render::wgpu::texture_cache::TextureCache>(),
373            TextureOwner::EntityMaterial(render_entity(entity)),
374            textures,
375        );
376        if let Some((index, _)) = registry_lookup_index(
377            &world
378                .res::<nightshade::ecs::asset_state::AssetState>()
379                .material_registry
380                .registry,
381            &material_ref.name,
382        ) {
383            registry_remove_reference(
384                &mut world
385                    .res_mut::<nightshade::ecs::asset_state::AssetState>()
386                    .material_registry
387                    .registry,
388                index,
389            );
390        }
391        register_material(world, entity, api_material_name(entity), material);
392    }
393}