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