solstrale/material/
texture.rs1use std::error::Error;
3use std::sync::Arc;
4
5use enum_dispatch::enum_dispatch;
6use image::io::Reader;
7use image::RgbImage;
8use simple_error::SimpleError;
9
10use crate::geo::Uv;
11use crate::geo::vec3::Vec3;
12use crate::material::texture::BumpMap::{Height, Normal};
13use crate::material::texture::Textures::{ImageMapType, SolidColorType};
14use crate::util::height_map;
15use crate::util::rgb_color::rgb_to_vec3;
16
17#[enum_dispatch]
20pub trait Texture {
21 fn color(&self, uv: Uv) -> Vec3;
23}
24
25#[enum_dispatch(Texture)]
26#[derive(Debug)]
27pub enum Textures {
29 SolidColorType(SolidColor),
31 ImageMapType(ImageMap),
33}
34
35impl Clone for Textures {
36 fn clone(&self) -> Self {
37 match self {
38 SolidColorType(t) => SolidColorType(t.clone()),
39 ImageMapType(t) => ImageMapType(t.clone()),
40 }
41 }
42}
43
44pub enum BumpMap {
46 Normal(RgbImage),
48 Height(RgbImage),
50}
51
52fn load_bump_map(path: &str) -> Result<BumpMap, Box<dyn Error>> {
54 let mut reader = Reader::open(path).map_err(|err| {
55 SimpleError::new(format!("Failed to open bump texture {}: {}", path, err))
56 })?;
57 reader.no_limits();
58 reader = reader.with_guessed_format().map_err(|err| {
59 SimpleError::new(format!("Failed to load bump texture {}: {}", path, err))
60 })?;
61 let image = reader
62 .decode()
63 .map_err(|err| {
64 SimpleError::new(format!("Failed to decode bump texture {}: {}", path, err))
65 })?
66 .into_rgb8();
67
68 let mut num_normal = 0;
69 let mut num_height = 0;
70
71 for pixel in image.pixels() {
72 let p = rgb_to_vec3(pixel);
73 if (p.length() - 1.).abs() < 0.05 {
74 num_normal += 1;
75 }
76 if (p.x - p.y).abs() < 0.05 && (p.y - p.z).abs() < 0.05 {
77 num_height += 1;
78 }
79 }
80
81 if num_height > num_normal {
82 Ok(Height(image))
83 } else {
84 Ok(Normal(image))
85 }
86}
87
88pub fn load_normal_texture(path: &str) -> Result<Textures, Box<dyn Error>> {
90 match load_bump_map(path)? {
91 Normal(n) => Ok(ImageMap::new(Arc::new(n))),
92 Height(h) => {
93 let n = height_map::to_normal_map(h);
94 Ok(ImageMap::new(Arc::new(n)))
95 }
96 }
97}
98
99#[derive(Clone, Debug)]
101pub struct SolidColor(Vec3);
102
103impl SolidColor {
104 #![allow(clippy::new_ret_no_self)]
105 pub fn new(r: f64, g: f64, b: f64) -> Textures {
107 SolidColor::new_from_vec3(Vec3::new(r, g, b))
108 }
109 pub fn new_from_f32_array(c: [f32; 3]) -> Textures {
112 SolidColor::new(c[0] as f64, c[1] as f64, c[2] as f64)
113 }
114 pub fn new_from_vec3(color: Vec3) -> Textures {
116 Textures::from(SolidColor(color))
117 }
118}
119
120impl Texture for SolidColor {
121 fn color(&self, _: Uv) -> Vec3 {
122 self.0
123 }
124}
125
126#[derive(Clone, Debug)]
128pub struct ImageMap {
129 image: Arc<RgbImage>,
130 max_x: f32,
131 max_y: f32,
132}
133
134impl ImageMap {
135 #![allow(clippy::new_ret_no_self)]
136 pub fn load(path: &str) -> Result<Textures, Box<dyn Error>> {
138 let mut reader = Reader::open(path).map_err(|err| {
139 SimpleError::new(format!("Failed to open image texture {}: {}", path, err))
140 })?;
141 reader.no_limits();
142 reader = reader.with_guessed_format().map_err(|err| {
143 SimpleError::new(format!("Failed to load image texture {}: {}", path, err))
144 })?;
145 let image = reader
146 .decode()
147 .map_err(|err| {
148 SimpleError::new(format!("Failed to decode image texture {}: {}", path, err))
149 })?
150 .into_rgb8();
151
152 Ok(Self::new(Arc::new(image)))
153 }
154
155 pub fn new(image: Arc<RgbImage>) -> Textures {
157 let w = image.width();
158 let h = image.height();
159 Textures::from(ImageMap {
160 image,
161 max_x: w as f32 - 1.,
162 max_y: h as f32 - 1.,
163 })
164 }
165}
166
167impl Texture for ImageMap {
168 fn color(&self, uv: Uv) -> Vec3 {
171 let u = uv.u.abs() % 1.;
172 let v = 1. - uv.v.abs() % 1.;
173
174 let x = u * self.max_x;
175 let y = v * self.max_y;
176
177 let pixel = self.image.get_pixel(x as u32, y as u32);
178 rgb_to_vec3(pixel)
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use crate::material::texture::{BumpMap, load_bump_map};
185
186 #[test]
187 fn test_load_normal_bump_map() {
188 let res = load_bump_map("resources/textures/wall_n.png").unwrap();
189 match res {
190 BumpMap::Normal(n) => assert!(n.width() > 0 && n.height() > 0),
191 BumpMap::Height(_) => panic!("Should not be a height map"),
192 }
193 }
194
195 #[test]
196 fn test_load_height_bump_map() {
197 let res = load_bump_map("resources/textures/sponza-h.jpg").unwrap();
198 match res {
199 BumpMap::Normal(_) => panic!("Should not be a height map"),
200 BumpMap::Height(n) => assert!(n.width() > 0 && n.height() > 0),
201 }
202 }
203}