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}