tileable_volume_noise/
lib.rs

1#[cfg(feature = "images")]
2use std::{fs, path::Path, sync::Arc};
3
4use glam::Vec3;
5use rayon::prelude::{IntoParallelIterator, ParallelIterator};
6
7mod glm_functions;
8mod tileable_3d_noise;
9
10pub use tileable_3d_noise::Tileable3dNoise;
11
12pub struct TileableCloudNoise {
13    pub data: Vec<u8>,
14    pub resolution: u32,
15    pub num_channels: u32,
16    pub bytes_per_channel: u32,
17}
18
19#[cfg(feature = "images")]
20fn write_to_png(noise_texture: &TileableCloudNoise, filename_without_extension: &str) {
21    // Create the output directory, if it doesn't already exist
22    let target_dir = Path::new("./example_images/");
23    if !target_dir.exists() {
24        fs::create_dir(target_dir).expect("failed to create `example_images` directory");
25    }
26
27    let file_path = target_dir.join(format!("{}.png", filename_without_extension));
28    let _ = image::save_buffer(
29        file_path,
30        &noise_texture.data,
31        noise_texture.resolution * noise_texture.resolution,
32        noise_texture.resolution,
33        image::ColorType::Rgba8,
34    );
35}
36
37#[allow(clippy::let_and_return)]
38impl TileableCloudNoise {
39    fn remap(og_value: f32, og_min: f32, og_max: f32, new_min: f32, new_max: f32) -> f32 {
40        new_min + (((og_value - og_min) / (og_max - og_min)) * (new_max - new_min))
41    }
42
43    // RGBA8 Unorm
44    //
45    // R: PerlinWorley noise
46    // G: Worley0
47    // B: Worley1
48    // A: Worley2
49    pub fn cloud_shape_and_erosion_texture() -> Self {
50        // As SebH mentions in the reference material, frequency values should be reduced if using a smaller resolution.
51        let frequence_mul = [2.0f32, 8.0f32, 14.0f32, 20.0f32, 26.0f32, 32.0f32]; // special weight for perlin-worley
52
53        // Cloud base shape (will be used to generate Perlin-Worley noise in the shader)
54        // Note: all channels could be combined once here to reduce memory bandwith requirements.
55
56        // !!! If this is reduced, you should also reduce the number of frequencies in the fmb noise  !!!
57        let resolution = 128u32;
58        let num_channels = 4u32;
59        let bytes_per_channel = 1u32;
60
61        let norm_factor = 1.0 / resolution as f32;
62
63        let cloud_base_shape_texels_unpadded = (0..resolution)
64            .into_par_iter()
65            .flat_map(|s| {
66                let mut slice: Vec<u8> = Vec::with_capacity(
67                    (resolution * resolution * num_channels * bytes_per_channel) as usize,
68                );
69
70                for t in 0..resolution {
71                    for r in 0..resolution {
72                        let coords = Vec3::new(s as f32, t as f32, r as f32) * norm_factor;
73
74                        // Perlin FBM noise
75                        let octave_count = 3u32;
76                        let frequency = 8.0f32;
77
78                        let perlin_noise =
79                            Tileable3dNoise::perlin_noise(coords, frequency, octave_count);
80
81                        let cell_count = 4f32;
82                        let worley_noise_0 = 1.0f32
83                            - Tileable3dNoise::worley_noise(coords, cell_count * frequence_mul[0]);
84                        let worley_noise_1 = 1.0f32
85                            - Tileable3dNoise::worley_noise(coords, cell_count * frequence_mul[1]);
86                        let worley_noise_2 = 1.0f32
87                            - Tileable3dNoise::worley_noise(coords, cell_count * frequence_mul[2]);
88
89                        let worley_fbm = worley_noise_0 * 0.625f32
90                            + worley_noise_1 * 0.25f32
91                            + worley_noise_2 * 0.125f32;
92
93                        // Perlin Worley is based on description in GPU Pro 7: Real Time Volumetric Cloudscapes.
94                        // However, it is not clear the text and the image are matching: images does not seem to match what the result from the description in text would give.
95                        // Also there are a lot of fudge factor in the code, e.g. * 0.2, so it is really up to you to fine the formula you like.
96
97                        // mapping perlin noise in between worley as minimum and 1.0 as maximum (as described in text of p.101 of GPU Pro 7)
98                        let perlin_worley = Self::remap(perlin_noise, 0.0, 1.0, worley_fbm, 1.0);
99
100                        // Matches better what figure 4.7 (not the following up text description p.101). Maps worley between newMin as 0 and perlin as maximum.
101                        // let perlin_worley = Self::remap(worleyFBM, 0.0, 1.0, 0.0, perlinNoise);
102
103                        let cell_count = 4f32;
104                        // let worley_noise_0 =
105                        //     1.0f32 - Tileable3dNoise::worley_noise(coords, cell_count * 1.0f32);
106                        let worley_noise_1 =
107                            1.0f32 - Tileable3dNoise::worley_noise(coords, cell_count * 2.0f32);
108                        let worley_noise_2 =
109                            1.0f32 - Tileable3dNoise::worley_noise(coords, cell_count * 4.0f32);
110                        let worley_noise_3 =
111                            1.0f32 - Tileable3dNoise::worley_noise(coords, cell_count * 8.0f32);
112                        let worley_noise_4 =
113                            1.0f32 - Tileable3dNoise::worley_noise(coords, cell_count * 16.0f32);
114                        // cell_count=2 -> half the frequency of texel, we should not go further (with cellCount = 32 and texture size = 64)
115                        // let worley_noise_5 =
116                        //     1.0f32 - Tileable3dNoise::worley_noise(coords, cell_count * 32.0f32);
117
118                        // Three frequency of Worley FBM noise
119                        let worley_fbm_0 = worley_noise_1 * 0.625f32
120                            + worley_noise_2 * 0.25f32
121                            + worley_noise_3 * 0.125f32;
122                        let worley_fbm_1 = worley_noise_2 * 0.625f32
123                            + worley_noise_3 * 0.25f32
124                            + worley_noise_4 * 0.125f32;
125
126                        // let worley_fbm_2 = worley_noise_3 * 0.625f32
127                        // + worley_noise_4 * 0.25f32
128                        // + worley_noise_5 * 0.125f32;
129
130                        // cell_count=4 -> worleyNoise5 is just noise due to sampling frequency=texel frequency. So only take into account 2 frequencies for FBM
131                        let worley_fbm_2 = worley_noise_3 * 0.75f32 + worley_noise_4 * 0.25f32;
132
133                        slice.push((perlin_worley * 255.0) as u8);
134                        slice.push((worley_fbm_0 * 255.0) as u8);
135                        slice.push((worley_fbm_1 * 255.0) as u8);
136                        slice.push((worley_fbm_2 * 255.0) as u8);
137                    }
138                }
139
140                slice
141            })
142            .collect::<Vec<_>>();
143
144        let output = Self {
145            data: cloud_base_shape_texels_unpadded,
146            resolution,
147            num_channels,
148            bytes_per_channel,
149        };
150
151        #[cfg(feature = "images")]
152        write_to_png(&output, "cloudShapeAndErosion");
153
154        output
155    }
156
157    // RGBA8 Unorm
158    //
159    // R: Worley FBM 0
160    // G: Worley FBM 1
161    // B: Worley FBM 2
162    // A: Unused - Set to 255
163    pub fn details_texture() -> Self {
164        // Detail texture behing different frequency of Worley noise
165        // Note: all channels could be combined once here to reduce memory bandwith requirements.
166        let num_channels = 4u32;
167        let bytes_per_channel = 1u32;
168        let resolution = 32u32;
169
170        let norm_factor = 1.0 / resolution as f32;
171
172        let cloud_detail_texels_unpadded = (0..resolution)
173            .into_par_iter()
174            .flat_map(|s| {
175                let mut slice: Vec<u8> = Vec::with_capacity(
176                    (resolution * resolution * num_channels * bytes_per_channel) as usize,
177                );
178
179                for t in 0..resolution {
180                    for r in 0..resolution {
181                        let coords = Vec3::new(s as f32, t as f32, r as f32) * norm_factor;
182
183                        // 3 octaves
184                        let cell_count = 2f32;
185                        let worley_noise_0 =
186                            1.0f32 - Tileable3dNoise::worley_noise(coords, cell_count * 1.0f32);
187                        let worley_noise_1 =
188                            1.0f32 - Tileable3dNoise::worley_noise(coords, cell_count * 2.0f32);
189                        let worley_noise_2 =
190                            1.0f32 - Tileable3dNoise::worley_noise(coords, cell_count * 4.0f32);
191                        let worley_noise_3 =
192                            1.0f32 - Tileable3dNoise::worley_noise(coords, cell_count * 8.0f32);
193
194                        let worley_fbm_0 = worley_noise_0 * 0.625f32
195                            + worley_noise_1 * 0.25f32
196                            + worley_noise_2 * 0.125f32;
197                        let worley_fbm_1 = worley_noise_1 * 0.625f32
198                            + worley_noise_2 * 0.25f32
199                            + worley_noise_3 * 0.125f32;
200                        // cell_count=4 -> worleyNoise4 is just noise due to sampling frequency=texel frequency. So only take into account 2 frequencies for FBM
201                        let worley_fbm_2 = worley_noise_2 * 0.75f32 + worley_noise_3 * 0.25f32;
202
203                        // 2 octaves - unused
204                        // let worley_noise_0 = 1.0f32 - Tileable3dNoise::worley_noise(coords, 4.0f32);
205                        // let worley_noise_1 = 1.0f32 - Tileable3dNoise::worley_noise(coords, 7.0f32);
206                        // let worley_noise_2 = 1.0f32 - Tileable3dNoise::worley_noise(coords, 10.0f32);
207                        // let worley_noise_3 = 1.0f32 - Tileable3dNoise::worley_noise(coords, 13.0f32);
208                        // let worley_fbm_0 = worley_noise_0 * 0.75f32 + worley_noise_1 * 0.25f32;
209                        // let worley_fbm_1 = worley_noise_1 * 0.75f32 + worley_noise_2 * 0.25f32;
210                        // let worley_fbm_2 = worley_noise_2 * 0.75f32 + worley_noise_3 * 0.25f32;
211
212                        slice.push((worley_fbm_0 * 255.0) as u8);
213                        slice.push((worley_fbm_1 * 255.0) as u8);
214                        slice.push((worley_fbm_2 * 255.0) as u8);
215                        slice.push(255u8);
216                    }
217                }
218
219                slice
220            })
221            .collect::<Vec<_>>();
222
223        let output = Self {
224            data: cloud_detail_texels_unpadded,
225            resolution,
226            num_channels,
227            bytes_per_channel,
228        };
229
230        #[cfg(feature = "images")]
231        write_to_png(&output, "cloudDetails");
232
233        output
234    }
235}