Skip to main content

nightshade_api/
draw.rs

1//! Immediate mode drawing. Everything drawn with `draw_` is visible for
2//! exactly one frame, so redraw it every frame you want it on screen. For
3//! geometry that should persist, use the `spawn_` functions instead.
4//!
5//! Backed by pooled instanced meshes and a shared lines entity, so heavy use
6//! costs one instance buffer refill per frame, not entity churn.
7
8use crate::runner::{
9    DRAW_CUBE_POOL, DRAW_LINES_POOL, DRAW_MATERIAL, DRAW_SPHERE_POOL, lookup_named, register_named,
10};
11use nightshade::prelude::nalgebra_glm::Quat;
12use nightshade::prelude::*;
13
14/// Draws a cube at `position` with the given size and color for one frame.
15pub fn draw_cube(world: &mut World, position: Vec3, scale: Vec3, color: [f32; 4]) {
16    push_instance(
17        world,
18        DRAW_CUBE_POOL,
19        InstanceTransform::new(position, Quat::identity(), scale),
20        color,
21    );
22}
23
24/// Draws a sphere at `position` with the given radius and color for one frame.
25pub fn draw_sphere(world: &mut World, position: Vec3, radius: f32, color: [f32; 4]) {
26    push_instance(
27        world,
28        DRAW_SPHERE_POOL,
29        InstanceTransform::new(
30            position,
31            Quat::identity(),
32            Vec3::new(radius, radius, radius),
33        ),
34        color,
35    );
36}
37
38/// Draws a line from `start` to `end` for one frame.
39pub fn draw_line(world: &mut World, start: Vec3, end: Vec3, color: [f32; 4]) {
40    let Some(entity) = lookup_named(world, DRAW_LINES_POOL) else {
41        return;
42    };
43    if let Some(lines) = world.core.get_lines_mut(entity) {
44        lines.push(Line {
45            start,
46            end,
47            color: Vec4::new(color[0], color[1], color[2], color[3]),
48        });
49    }
50}
51
52/// Draws billboard text at a 3d position for one frame. Damage numbers,
53/// debug readouts over entities, anything that should track live data.
54/// Up to 64 labels per frame, drawn from a reused pool so a label with
55/// stable content costs nothing to redraw.
56pub fn draw_text_3d(world: &mut World, text: &str, position: Vec3) {
57    for (index, name) in TEXT_POOL_NAMES.iter().enumerate() {
58        match lookup_named(world, name) {
59            Some(entity) => {
60                let parked = world
61                    .core
62                    .get_local_transform(entity)
63                    .is_some_and(|transform| transform.translation.y < PARKED_THRESHOLD);
64                if parked || index == TEXT_POOL_NAMES.len() - 1 {
65                    crate::text::set_text(world, entity, text);
66                    crate::placement::set_position(world, entity, position);
67                    return;
68                }
69            }
70            None => {
71                let entity = crate::text::spawn_label(world, text, position);
72                world.core.add_components(entity, NAME);
73                world.core.set_name(entity, Name(name.to_string()));
74                register_named(world, name, entity);
75                return;
76            }
77        }
78    }
79}
80
81/// Spawns a retained set of lines that stays until you
82/// [`despawn`](crate::prelude::despawn) it.
83pub fn spawn_lines(world: &mut World, lines: Vec<Line>) -> Entity {
84    let entity = spawn_entities(world, LINES | VISIBILITY | GLOBAL_TRANSFORM, 1)[0];
85    world.core.set_lines(entity, Lines::new(lines));
86    entity
87}
88
89fn push_instance(
90    world: &mut World,
91    pool_name: &str,
92    transform: InstanceTransform,
93    color: [f32; 4],
94) {
95    let Some(entity) = lookup_named(world, pool_name) else {
96        return;
97    };
98    if let Some(instanced) = world.core.get_instanced_mesh_mut(entity) {
99        instanced.add_instance(transform);
100        let index = instanced.instance_count() - 1;
101        instanced.set_instance_tint(index, color);
102    }
103}
104
105pub(crate) fn initialize_draw_pools(world: &mut World) {
106    crate::scene::ensure_primitive_mesh(world, "Cube");
107    crate::scene::ensure_primitive_mesh(world, "Sphere");
108
109    material_registry_insert(
110        &mut world.resources.assets.material_registry,
111        DRAW_MATERIAL.to_string(),
112        Material::default(),
113    );
114    if let Some((index, _)) = registry_lookup_index(
115        &world.resources.assets.material_registry.registry,
116        DRAW_MATERIAL,
117    ) {
118        registry_add_reference(
119            &mut world.resources.assets.material_registry.registry,
120            index,
121        );
122    }
123
124    let cube_pool = spawn_instanced_mesh_with_material(world, "Cube", Vec::new(), DRAW_MATERIAL);
125    world
126        .core
127        .set_name(cube_pool, Name(DRAW_CUBE_POOL.to_string()));
128    register_named(world, DRAW_CUBE_POOL, cube_pool);
129    let sphere_pool =
130        spawn_instanced_mesh_with_material(world, "Sphere", Vec::new(), DRAW_MATERIAL);
131    world
132        .core
133        .set_name(sphere_pool, Name(DRAW_SPHERE_POOL.to_string()));
134    register_named(world, DRAW_SPHERE_POOL, sphere_pool);
135
136    let lines_pool = spawn_entities(world, LINES | NAME | VISIBILITY | GLOBAL_TRANSFORM, 1)[0];
137    world
138        .core
139        .set_name(lines_pool, Name(DRAW_LINES_POOL.to_string()));
140    world.core.set_lines(lines_pool, Lines::new(Vec::new()));
141    register_named(world, DRAW_LINES_POOL, lines_pool);
142}
143
144pub(crate) fn clear_draw_pools(world: &mut World) {
145    for pool_name in [DRAW_CUBE_POOL, DRAW_SPHERE_POOL] {
146        if let Some(entity) = lookup_named(world, pool_name)
147            && let Some(instanced) = world.core.get_instanced_mesh_mut(entity)
148            && instanced.instance_count() > 0
149        {
150            instanced.clear_instances();
151        }
152    }
153    if let Some(entity) = lookup_named(world, DRAW_LINES_POOL)
154        && let Some(lines) = world.core.get_lines_mut(entity)
155        && !lines.lines.is_empty()
156    {
157        lines.clear();
158    }
159    for name in TEXT_POOL_NAMES.iter() {
160        let Some(entity) = lookup_named(world, name) else {
161            break;
162        };
163        let used = world
164            .core
165            .get_local_transform(entity)
166            .is_some_and(|transform| transform.translation.y > PARKED_THRESHOLD);
167        if used {
168            crate::placement::set_position(world, entity, Vec3::new(0.0, -100000.0, 0.0));
169        }
170    }
171}
172
173const PARKED_THRESHOLD: f32 = -90000.0;
174
175const TEXT_POOL_NAMES: [&str; 64] = [
176    "api::draw::text::0",
177    "api::draw::text::1",
178    "api::draw::text::2",
179    "api::draw::text::3",
180    "api::draw::text::4",
181    "api::draw::text::5",
182    "api::draw::text::6",
183    "api::draw::text::7",
184    "api::draw::text::8",
185    "api::draw::text::9",
186    "api::draw::text::10",
187    "api::draw::text::11",
188    "api::draw::text::12",
189    "api::draw::text::13",
190    "api::draw::text::14",
191    "api::draw::text::15",
192    "api::draw::text::16",
193    "api::draw::text::17",
194    "api::draw::text::18",
195    "api::draw::text::19",
196    "api::draw::text::20",
197    "api::draw::text::21",
198    "api::draw::text::22",
199    "api::draw::text::23",
200    "api::draw::text::24",
201    "api::draw::text::25",
202    "api::draw::text::26",
203    "api::draw::text::27",
204    "api::draw::text::28",
205    "api::draw::text::29",
206    "api::draw::text::30",
207    "api::draw::text::31",
208    "api::draw::text::32",
209    "api::draw::text::33",
210    "api::draw::text::34",
211    "api::draw::text::35",
212    "api::draw::text::36",
213    "api::draw::text::37",
214    "api::draw::text::38",
215    "api::draw::text::39",
216    "api::draw::text::40",
217    "api::draw::text::41",
218    "api::draw::text::42",
219    "api::draw::text::43",
220    "api::draw::text::44",
221    "api::draw::text::45",
222    "api::draw::text::46",
223    "api::draw::text::47",
224    "api::draw::text::48",
225    "api::draw::text::49",
226    "api::draw::text::50",
227    "api::draw::text::51",
228    "api::draw::text::52",
229    "api::draw::text::53",
230    "api::draw::text::54",
231    "api::draw::text::55",
232    "api::draw::text::56",
233    "api::draw::text::57",
234    "api::draw::text::58",
235    "api::draw::text::59",
236    "api::draw::text::60",
237    "api::draw::text::61",
238    "api::draw::text::62",
239    "api::draw::text::63",
240];