engine/renderer/
sprite.rs

1// SPDX-FileCopyrightText: 2025 Jens Pitkänen <jens.pitkanen@helsinki.fi>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4
5//! Sprite drawing specifics.
6//!
7//! This is the "runtime-half" of [`SpriteAsset`], the other half being the
8//! "import-half" implemented in `import_asset::importers::sprite`. These two
9//! modules are very tightly linked: this module assumes the sprite chunks are
10//! laid out in a specific way, and the importer is responsible for writing the
11//! sprite chunks out in said layout.
12
13use core::ops::Range;
14
15use platform::BlendMode;
16
17use crate::{
18    geom::Rect,
19    resources::{
20        sprite::{SpriteAsset, SpriteMipLevel},
21        ResourceDatabase, ResourceLoader, SPRITE_CHUNK_DIMENSIONS,
22    },
23};
24
25use super::{DrawQueue, SpriteQuad};
26
27const CHUNK_WIDTH: u16 = SPRITE_CHUNK_DIMENSIONS.0;
28const CHUNK_HEIGHT: u16 = SPRITE_CHUNK_DIMENSIONS.1;
29
30impl SpriteAsset {
31    /// Draw this sprite into the `dst` rectangle.
32    ///
33    /// Returns false if the sprite couldn't be drawn due to the draw queue
34    /// filling up. Note that one draw may cause multiple draws in the queue,
35    /// since sprites are split into chunks, each of which gets drawn as a
36    /// separate quad.
37    #[must_use]
38    pub fn draw(
39        &self,
40        dst: Rect,
41        draw_order: u8,
42        draw_queue: &mut DrawQueue,
43        resources: &ResourceDatabase,
44        resource_loader: &mut ResourceLoader,
45    ) -> bool {
46        draw(
47            RenderableSprite {
48                mip_chain: &self.mip_chain,
49                transparent: self.transparent,
50                draw_order,
51            },
52            dst,
53            draw_queue,
54            resources,
55            resource_loader,
56        )
57    }
58}
59
60/// Render-time relevant parts of a sprite.
61struct RenderableSprite<'a> {
62    /// A list of the sprite's mipmaps, with index 0 being the original sprite,
63    /// and the indices after that each having half the width and height of the
64    /// previous level.
65    pub mip_chain: &'a [SpriteMipLevel],
66    /// Should be set to true if the sprite has any non-opaque pixels to avoid
67    /// rendering artifacts.
68    pub transparent: bool,
69    /// The draw order used when drawing this sprite. See
70    /// [`TexQuad::draw_order`].
71    pub draw_order: u8,
72}
73
74/// The main sprite rendering function.
75///
76/// May push more than one draw command into the [`DrawQueue`] when rendering
77/// large sprites at large sizes, as the sprite may consist of multiple
78/// sprite chunks (see [`SPRITE_CHUNK_DIMENSIONS`] for the size of each
79/// chunk).
80///
81/// Returns false if the draw queue does not have enough free space to draw this
82/// sprite.
83fn draw(
84    src: RenderableSprite,
85    dst: Rect,
86    draw_queue: &mut DrawQueue,
87    resources: &ResourceDatabase,
88    resource_loader: &mut ResourceLoader,
89) -> bool {
90    profiling::function_scope!();
91    let draws_left = draw_queue.sprites.spare_capacity();
92
93    let mut draw_chunk = |chunk_index: u32, dst: Rect, tex: Rect| {
94        profiling::scope!("draw_chunk");
95        if let Some(chunk) = resources.sprite_chunks.get(chunk_index) {
96            let quad = SpriteQuad {
97                position_top_left: (dst.x, dst.y),
98                position_bottom_right: (dst.x + dst.w, dst.y + dst.h),
99                texcoord_top_left: (tex.x, tex.y),
100                texcoord_bottom_right: (tex.x + tex.w, tex.y + tex.h),
101                draw_order: src.draw_order,
102                blend_mode: if src.transparent {
103                    BlendMode::Blend
104                } else {
105                    BlendMode::None
106                },
107                sprite: chunk.0,
108            };
109
110            draw_queue.sprites.push(quad).unwrap();
111        } else {
112            resource_loader.queue_sprite_chunk(chunk_index, resources);
113        }
114    };
115
116    // Get the sprite's size divided by the resolution it's being rendered at.
117    let rendering_scale_ratio = match &src.mip_chain[0] {
118        SpriteMipLevel::SingleChunkSprite { size, .. }
119        | SpriteMipLevel::MultiChunkSprite { size, .. } => {
120            let width_scale = size.0 / (dst.w * draw_queue.scale_factor) as u16;
121            let height_scale = size.1 / (dst.h * draw_queue.scale_factor) as u16;
122            width_scale.min(height_scale)
123        }
124    };
125
126    // Since every mip is half the resolution, with index 0 being the highest,
127    // log2 of the scale between the actual sprite and the rendered size matches
128    // the index of the mip that matches the rendered size the closest. ilog2
129    // rounds down, which is fine, as that'll end up picking the higher
130    // resolution mip of the two mips around the real log2 result.
131    let mip_level = rendering_scale_ratio.checked_ilog2().unwrap_or(0) as usize;
132
133    let max_mip = src.mip_chain.len() - 1;
134    let mip = &src.mip_chain[mip_level.min(max_mip)];
135
136    match mip {
137        SpriteMipLevel::SingleChunkSprite {
138            offset,
139            size,
140            sprite_chunk,
141        } => {
142            if draws_left == 0 {
143                return false;
144            }
145
146            let tex_src = Rect {
147                x: offset.0 as f32 / CHUNK_WIDTH as f32,
148                y: offset.1 as f32 / CHUNK_HEIGHT as f32,
149                w: size.0 as f32 / CHUNK_WIDTH as f32,
150                h: size.1 as f32 / CHUNK_HEIGHT as f32,
151            };
152            draw_chunk(*sprite_chunk, dst, tex_src);
153
154            true
155        }
156
157        SpriteMipLevel::MultiChunkSprite {
158            size,
159            sprite_chunks,
160        } => {
161            let chunks_x = size.0.div_ceil(CHUNK_WIDTH - 2) as u32;
162            let chunks_y = size.1.div_ceil(CHUNK_HEIGHT - 2) as u32;
163            assert_eq!(
164                chunks_x * chunks_y,
165                sprite_chunks.end - sprite_chunks.start,
166                "resource database has a corrupt chunk? the amount of chunks does not match the sprite size",
167            );
168
169            if draws_left < (chunks_x * chunks_y) as usize {
170                return false;
171            }
172
173            draw_multi_chunk_sprite(
174                dst,
175                *size,
176                sprite_chunks.clone(),
177                (chunks_x, chunks_y),
178                draw_chunk,
179            );
180
181            true
182        }
183    }
184}
185
186fn draw_multi_chunk_sprite(
187    Rect { x, y, w, h }: Rect,
188    (tex_width, tex_height): (u16, u16),
189    chunks: Range<u32>,
190    (chunks_x, chunks_y): (u32, u32),
191    mut draw: impl FnMut(u32, Rect, Rect),
192) {
193    let scale_x = w / tex_width as f32;
194    let scale_y = h / tex_height as f32;
195
196    let mut tex_x_pos = 0;
197    let mut tex_y_pos = 0;
198    for cy in 0..chunks_y {
199        let curr_chunk_h = (tex_height - tex_y_pos).min(CHUNK_HEIGHT - 2);
200        for cx in 0..chunks_x {
201            let curr_chunk_index = chunks.start + cx + cy * chunks_x;
202            let curr_chunk_w = (tex_width - tex_x_pos).min(CHUNK_WIDTH - 2);
203
204            let dst = Rect {
205                x: x + tex_x_pos as f32 * scale_x,
206                y: y + tex_y_pos as f32 * scale_y,
207                w: curr_chunk_w as f32 * scale_x,
208                h: curr_chunk_h as f32 * scale_y,
209            };
210
211            let tex_src = Rect {
212                x: 1. / CHUNK_WIDTH as f32,
213                y: 1. / CHUNK_HEIGHT as f32,
214                w: curr_chunk_w as f32 / CHUNK_WIDTH as f32,
215                h: curr_chunk_h as f32 / CHUNK_HEIGHT as f32,
216            };
217
218            draw(curr_chunk_index, dst, tex_src);
219
220            tex_x_pos += curr_chunk_w;
221        }
222        tex_y_pos += curr_chunk_h;
223        tex_x_pos = 0;
224    }
225}