moon_engine/
texture.rs

1//! The [`Texture`] and [`SubTexture`] structs.
2
3use std::rc::Rc;
4
5use wasm_bindgen::JsCast;
6use web_sys::HtmlImageElement;
7use web_sys::WebGlTexture;
8
9use crate::gl::Bind;
10use crate::Color32;
11use crate::{gl, GL};
12
13/// A [`Texture`] stores an Image that can be used while rendering, or to store data.
14#[derive(Debug)]
15pub struct Texture {
16    texture: Option<WebGlTexture>,
17    /// Width of the [`Texture`].
18    pub width: u32,
19    /// Height of the [`Texture`].
20    pub height: u32,
21    /// Slot the [`Texture`] will occupy.
22    pub slot: u32,
23}
24
25impl Default for Texture {
26    fn default() -> Self {
27        Self {
28            texture: None,
29            width: 1,
30            height: 1,
31            slot: 0,
32        }
33    }
34}
35
36impl Bind for Texture {
37    fn bind(&self, gl: &GL) {
38        gl.active_texture(GL::TEXTURE0 + self.slot);
39        gl.bind_texture(GL::TEXTURE_2D, self.texture.as_ref());
40    }
41    fn unbind(&self, gl: &GL) {
42        gl.active_texture(GL::TEXTURE0 + self.slot);
43        gl.bind_texture(GL::TEXTURE_2D, None);
44    }
45}
46
47impl Drop for Texture {
48    fn drop(&mut self) {
49        let gl = gl::get_context();
50
51        gl.delete_texture(self.texture.as_ref());
52    }
53}
54
55impl Texture {
56    /// Create a new [`Texture`] using an [`HtmlImageElement`].
57    pub fn new(gl: &GL, image: &HtmlImageElement) -> Self {
58        let (width, height) = (image.width(), image.height());
59
60        let texture = gl.create_texture();
61        gl.active_texture(GL::TEXTURE0);
62        gl.bind_texture(GL::TEXTURE_2D, texture.as_ref());
63        gl.tex_parameteri(GL::TEXTURE_2D, GL::TEXTURE_MIN_FILTER, GL::NEAREST as i32);
64        gl.tex_parameteri(GL::TEXTURE_2D, GL::TEXTURE_MAG_FILTER, GL::NEAREST as i32);
65        gl.tex_parameteri(GL::TEXTURE_2D, GL::TEXTURE_WRAP_S, GL::REPEAT as i32);
66        gl.tex_parameteri(GL::TEXTURE_2D, GL::TEXTURE_WRAP_T, GL::REPEAT as i32);
67        // Flip the Y-axis so the image displays the right way up
68        gl.pixel_storei(GL::UNPACK_FLIP_Y_WEBGL, 1);
69        gl.tex_image_2d_with_u32_and_u32_and_html_image_element(
70            GL::TEXTURE_2D,
71            0,
72            GL::RGBA as i32,
73            GL::RGBA,
74            GL::UNSIGNED_BYTE,
75            image,
76        )
77        .expect("Failed to load texture");
78        gl.generate_mipmap(GL::TEXTURE_2D);
79
80        Self {
81            width,
82            height,
83            texture,
84            ..Default::default()
85        }
86    }
87
88    /// Create a new [`Texture`] from an [`HtmlImageElement`] with an given element ID.
89    pub fn new_with_element_id(gl: &GL, image_src: &str) -> Self {
90        let document: web_sys::Document = web_sys::window().unwrap().document().unwrap();
91        let image = document
92            .get_element_by_id(image_src)
93            .unwrap()
94            .dyn_into::<web_sys::HtmlImageElement>()
95            .unwrap();
96        Self::new(gl, &image)
97    }
98
99    /// Create a new [`Texture`] from an [`HtmlImageElement`] with an element ID in the format **textureXX** where *XX* is a number.
100    pub fn new_with_texture_id(gl: &GL, count: u32) -> Self {
101        Self::new_with_element_id(gl, &format!("texture{}", count))
102    }
103
104    /// Create a new [`Texture`] using a slice of [`u8`]s.
105    pub fn new_from_pixels(gl: &GL, width: u32, height: u32, pixels: &[u8]) -> Self {
106        assert!(pixels.len() == (width * height * 4) as usize);
107        let texture = gl.create_texture();
108        gl.active_texture(GL::TEXTURE0);
109        gl.bind_texture(GL::TEXTURE_2D, texture.as_ref());
110        gl.tex_parameteri(GL::TEXTURE_2D, GL::TEXTURE_MIN_FILTER, GL::NEAREST as i32);
111        gl.tex_parameteri(GL::TEXTURE_2D, GL::TEXTURE_MAG_FILTER, GL::NEAREST as i32);
112        gl.tex_parameteri(GL::TEXTURE_2D, GL::TEXTURE_WRAP_S, GL::REPEAT as i32);
113        gl.tex_parameteri(GL::TEXTURE_2D, GL::TEXTURE_WRAP_T, GL::REPEAT as i32);
114        gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
115            GL::TEXTURE_2D,
116            0,
117            GL::RGBA as i32,
118            width as i32,
119            height as i32,
120            0,
121            GL::RGBA,
122            GL::UNSIGNED_BYTE,
123            Some(pixels),
124        )
125        .expect("Failed to generate texture");
126        gl.generate_mipmap(GL::TEXTURE_2D);
127
128        Self {
129            width,
130            height,
131            texture,
132            ..Default::default()
133        }
134    }
135
136    /// A colored [`Texture`].
137    ///
138    /// Create a single pixel sized [`Texture`] with the specified [`Color32`].
139    pub fn colored(gl: &GL, color: Color32) -> Self {
140        Self::new_from_pixels(gl, 1, 1, &<[u8; 4]>::from(color))
141    }
142
143    /// A fully-white [`Texture`].
144    pub fn white(gl: &GL) -> Self {
145        Self::colored(gl, Color32::WHITE)
146    }
147
148    /// A black and white checkerboard [`Texture`].
149    pub fn checkerboard(gl: &GL) -> Self {
150        Self::checkerboard_colored(gl, Color32::WHITE, Color32::BLACK)
151    }
152
153    /// A checkerboard [`Texture`] with two [`Color32`]s.
154    pub fn checkerboard_colored(gl: &GL, color1: Color32, color2: Color32) -> Self {
155        let size = 8;
156        let mut pixels = Vec::<u8>::with_capacity(size * size);
157        for x_offset in 0..size {
158            for y_offset in 0..size {
159                let color = if (x_offset + y_offset) % 2 == 0 {
160                    color1
161                } else {
162                    color2
163                };
164                pixels.append(&mut Vec::from(color));
165            }
166        }
167        Self::new_from_pixels(gl, size as u32, size as u32, &pixels)
168    }
169}
170
171/// A [`SubTexture`] is a part of a full [`Texture`].
172///
173/// It stores the UV co-ordinates of the part of the Texture it occupies.
174#[derive(Debug, Clone)]
175pub struct SubTexture {
176    texture: Option<Rc<Texture>>,
177    min: [f32; 2],
178    max: [f32; 2],
179}
180
181impl Default for SubTexture {
182    fn default() -> Self {
183        Self {
184            texture: None,
185            min: [0.0, 0.0],
186            max: [1.0, 1.0],
187        }
188    }
189}
190
191impl SubTexture {
192    /// Create a new [`SubTexture`] with the full UV co-ordinates.
193    pub fn new(texture: Rc<Texture>) -> Self {
194        Self {
195            texture: Some(texture),
196            ..Default::default()
197        }
198    }
199
200    /// Create a new [`SubTexture`] with the given UV co-ordinates.
201    pub fn new_with_coords(texture: Rc<Texture>, uv: Color32) -> Self {
202        Self {
203            texture: Some(texture),
204            min: [uv.x(), uv.z()],
205            max: [uv.y(), uv.w()],
206        }
207    }
208
209    /// Get the UV co-ordinates as an array of four two-component [`f32`].
210    pub fn get_uv_coords(&self) -> [[f32; 2]; 4] {
211        let (min, max) = (self.min, self.max);
212        [
213            [min[0], min[1]],
214            [min[0], max[1]],
215            [max[0], max[1]],
216            [max[0], min[1]],
217        ]
218    }
219
220    /// Create a [`Vec`] of [`SubTexture`]s from a sprite sheet.
221    ///
222    /// Uses the number of cells horizontally and vertically to devide the cells.
223    pub fn create_tiles_from_spritesheet(
224        texture: Rc<Texture>,
225        horizontal_cells: u32,
226        vertical_cells: u32,
227    ) -> Vec<SubTexture> {
228        assert!(horizontal_cells > 0 && vertical_cells > 0);
229        let mut tiles: Vec<SubTexture> = Vec::new();
230        for cell_x in 0..horizontal_cells {
231            for cell_y in 0..vertical_cells {
232                let uv = Color32(
233                    cell_x as f32 / horizontal_cells as f32,
234                    (cell_x + 1) as f32 / horizontal_cells as f32,
235                    cell_y as f32 / vertical_cells as f32,
236                    (cell_y + 1) as f32 / vertical_cells as f32,
237                );
238                let tile = SubTexture::new_with_coords(Rc::clone(&texture), uv);
239                tiles.push(tile);
240            }
241        }
242        tiles
243    }
244}
245
246impl Bind for SubTexture {
247    fn bind(&self, gl: &GL) {
248        if let Some(texture) = &self.texture {
249            texture.bind(gl);
250        }
251    }
252    fn unbind(&self, gl: &GL) {
253        gl.bind_texture(GL::TEXTURE_2D, None);
254    }
255}