Blender V5.0
sky_multiple_scattering.cpp
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2022 Fernando García Liñán
2 * SPDX-FileCopyrightText: 2011-2025 Blender Authors
3 *
4 * SPDX-License-Identifier: MIT */
5
9
10/*
11 * This code is a converted version of the ShaderToy written by Fernando García Liñán.
12 *
13 * This shader is the final result of my Master's Thesis.
14 * The main contributions are:
15 *
16 * 1. A spectral rendering technique that only requires 4 wavelength samples to
17 * get accurate results.
18 * 2. A multiple scattering approximation.
19 *
20 * Both of these approximations rely on an analytical fit, so they only work for
21 * Earth's atmosphere. We make up for it by using a very flexible atmosphere
22 * model that is able to represent a wide variety of atmospheric conditions.
23 *
24 * A brief description of this spectral rendering technique can be found in the
25 * following article:
26 * https://fgarlin.com/posts/2024-12-06-spectral_sky/
27 *
28 * The path tracer that has been used as a ground truth can be found at:
29 * https://github.com/fgarlin/skytracer
30 */
31
32#include <algorithm>
33
34#include "sky_math.h"
35#include "sky_nishita.h"
36
37using std::min;
38
39/* Earth's atmosphere parameters. */
40/* Ground reflectance. */
41static const float4 GROUND_ALBEDO = make_float4(0.3f, 0.3f, 0.3f, 0.3f);
42static const float PHASE_ISOTROPIC = M_1_4PI_F;
43static const float RAYLEIGH_PHASE_SCALE = (3.0f / 16.0f) * M_1_PI_F;
44/* Aerosols anisotropy. */
45static const float G = 0.8f;
46static const float SQR_G = G * G;
47/* Earth radius (km). */
48static const float EARTH_RADIUS = 6371.0f;
49/* Atmosphere thickness (km). */
50static const float ATMOSPHERE_THICKNESS = 100.0f;
52/* Ray marching steps. Higher steps means increased accuracy but worse performance. */
53static const int TRANSMITTANCE_STEPS = 64;
54static const int IN_SCATTERING_STEPS = 64;
55
56/* LUTs. */
57static const int TRANSMITTANCE_RES_X = 256;
58static const int TRANSMITTANCE_RES_Y = 64;
59
60/* Spectral data sampled at 630, 560, 490, 430 nm for urban area. */
61static const float4 SUN_SPECTRAL_IRRADIANCE = make_float4(1.679f, 1.828f, 1.986f, 1.307f);
63 6.605e-3f, 1.067e-2f, 1.842e-2f, 3.156e-2f);
65 3.472e-25f, 3.914e-25f, 1.349e-25f, 11.03e-27f);
66/* Average ozone dobson of monthly mean values. */
67static const float OZONE_MEAN_DOBSON = 334.5f;
69 2.8722e-24f, 4.6168e-24f, 7.9706e-24f, 1.3578e-23f);
71 1.5908e-22f, 1.7711e-22f, 2.0942e-22f, 2.4033e-22f);
72static const float AEROSOL_BASE_DENSITY = 1.3681e20f;
73static const float AEROSOL_BACKGROUND_DENSITY = 2e6f;
74static const float AEROSOL_HEIGHT_SCALE = 0.73f;
75/* Spectral to XYZ space conversion matrix. */
76static const float3 SPECTRAL_XYZ[4] = {
77 make_float3(53.386917738564668023f, 22.981337506691024754f, 0.0f),
78 make_float3(43.904844466369358263f, 71.347795700053393866f, 0.102506867965741307f),
79 make_float3(1.6137278251608962005f, 18.422960591455485011f, 31.742921188390805758f),
80 make_float3(20.762668673810577145f, 2.3614213523314368527f, 110.48009643252140334f),
81};
82
83inline float molecular_phase_function(const float cos_theta)
84{
85 return RAYLEIGH_PHASE_SCALE * (1.0f + sqr(cos_theta));
86}
87
88inline float aerosol_phase_function(const float cos_theta)
89{
90 float den = 1.0f + SQR_G + 2.0f * G * cos_theta;
91 return M_1_4PI_F * (1.0f - SQR_G) / (den * sqrtf(den));
92}
93
95{
96 return MOLECULAR_SCATTERING_COEFFICIENT_BASE * expf(-0.07771971f * powf(h, 1.16364243f));
97}
98
100{
101 const float log_h = logf(fmaxf(h, 1e-4f));
102 float density = 3.78547397e20f * expf(-sqr(log_h - 3.22261f) * 5.55555555f - log_h);
104}
105
106inline float get_aerosol_density(const float h)
107{
109 return AEROSOL_BASE_DENSITY * (expf(-h / AEROSOL_HEIGHT_SCALE) + division);
110}
111
113{
114 float3 xyz = make_float3(0.0f, 0.0f, 0.0f);
115 for (int i = 0; i < 4; i++) {
116 xyz += SPECTRAL_XYZ[i] * L[i];
117 }
118 return xyz;
119}
120
121/* Precomputed data. */
123 public:
130
131 /* Compute atmosphere's transmittance from the given altitude to the sun. */
132 inline float4 get_transmittance(const float cos_theta, const float normalized_altitude) const
133 {
134 const float3 sun_dir = sun_direction(cos_theta);
135 const float distance_to_earth_center = mix(
136 EARTH_RADIUS, ATMOSPHERE_RADIUS, normalized_altitude);
137 const float3 ray_origin = make_float3(0.0f, 0.0f, distance_to_earth_center);
138 const float t_d = ray_sphere_intersection(ray_origin, sun_dir, ATMOSPHERE_RADIUS);
139 const float t_step = t_d / TRANSMITTANCE_STEPS;
140
141 float4 result = make_float4(0.0f, 0.0f, 0.0f, 0.0f);
142 for (int step = 0; step < TRANSMITTANCE_STEPS; step++) {
143 const float t = (step + 0.5f) * t_step;
144 const float3 x_t = ray_origin + sun_dir * t;
145 const float altitude = fmaxf(x_t.length() - EARTH_RADIUS, 0.0f);
146 float4 aerosol_absorption, aerosol_scattering, molecular_absorption, molecular_scattering;
148 aerosol_absorption,
149 aerosol_scattering,
150 molecular_absorption,
151 molecular_scattering);
152 const float4 extinction = aerosol_absorption + aerosol_scattering + molecular_absorption +
153 molecular_scattering;
154 result += extinction * t_step;
155 }
156
157 return exp(-result);
158 }
159
160 /* Compute in-scattered radiance for the given ray. */
162 const float3 ray_origin,
163 const float3 ray_dir,
164 const float t_d) const
165 {
166 const float cos_theta = dot(-ray_dir, sun_dir);
167 const float molecular_phase = molecular_phase_function(cos_theta);
168 const float aerosol_phase = aerosol_phase_function(cos_theta);
169 const float dt = t_d / IN_SCATTERING_STEPS;
170 float4 L_inscattering = make_float4(0.0f, 0.0f, 0.0f, 0.0f);
171 float4 transmittance = make_float4(1.0f, 1.0f, 1.0f, 1.0f);
172 for (int i = 0; i < IN_SCATTERING_STEPS; i++) {
173 const float t = (i + 0.5f) * dt;
174 const float3 x_t = ray_origin + ray_dir * t;
175 const float distance_to_earth_center = x_t.length();
176 const float3 zenith_dir = x_t / distance_to_earth_center;
177 const float altitude = fmaxf(distance_to_earth_center - EARTH_RADIUS, 0.0f);
178 const float normalized_altitude = altitude / ATMOSPHERE_THICKNESS;
179 const float sample_cos_theta = dot(zenith_dir, sun_dir);
180 float4 aerosol_absorption, aerosol_scattering, molecular_absorption, molecular_scattering;
182 aerosol_absorption,
183 aerosol_scattering,
184 molecular_absorption,
185 molecular_scattering);
186 const float4 extinction = aerosol_absorption + aerosol_scattering + molecular_absorption +
187 molecular_scattering;
188 const float4 transmittance_to_sun = lookup_transmittance(sample_cos_theta,
189 normalized_altitude);
191 sample_cos_theta, normalized_altitude, distance_to_earth_center);
193 (molecular_scattering * (molecular_phase * transmittance_to_sun + ms) +
194 aerosol_scattering * (aerosol_phase * transmittance_to_sun + ms));
195 const float4 step_transmittance = exp(-dt * extinction);
196 /* Energy-conserving analytical integration "Physically Based Sky, Atmosphere and Cloud
197 * Rendering in Frostbite" by Sébastien Hillaire. */
198 const float4 cut_ext = max(extinction, 1e-7f);
199 const float4 S_int = (S - S * step_transmittance) / cut_ext;
200 L_inscattering = L_inscattering + transmittance * S_int;
201 transmittance *= step_transmittance;
202 }
203
204 return L_inscattering;
205 }
206
207 /* Precompute the transmittance LUT. Must be called before get_inscattering(). */
209 {
210 SKY_parallel_for(0, TRANSMITTANCE_RES_Y, 4, [&](const size_t begin, const size_t end) {
211 for (int y = begin; y < end; y++) {
212 for (int x = 0; x < TRANSMITTANCE_RES_X; x++) {
213 const float2 uv = make_float2(x / float(TRANSMITTANCE_RES_X - 1),
214 y / float(TRANSMITTANCE_RES_Y - 1));
215 transmittance_lut[y][x] = get_transmittance(uv.x * 2.0f - 1.0f, uv.y);
216 }
217 }
218 });
219 }
220
221 protected:
226
227 /* Compute absorption/scattering coeffients at the given altitude. */
228 inline void get_atmosphere_collision_coefficients(const float altitude,
229 float4 &aerosol_absorption,
230 float4 &aerosol_scattering,
231 float4 &molecular_absorption,
232 float4 &molecular_scattering) const
233 {
234 const float local_aerosol_density = get_aerosol_density(altitude) * aerosol_density;
235 aerosol_absorption = AEROSOL_ABSORPTION_CROSS_SECTION * local_aerosol_density;
236 aerosol_scattering = AEROSOL_SCATTERING_CROSS_SECTION * local_aerosol_density;
237 molecular_absorption = get_molecular_absorption_coefficient(altitude) * ozone_density;
238 molecular_scattering = get_molecular_scattering_coefficient(altitude) * air_density;
239 }
240
241 inline float4 lookup_multiscattering(float cos_theta, float normalized_height, float d) const
242 {
243 /* Solid angle subtended by the planet from a point at d distance from the planet center. */
244 const float omega = M_2PI_F * (1.0f - safe_sqrtf(1.0f - sqr(EARTH_RADIUS / d)));
246 /* We can split the path into Ground <-> Sample <-> Sun.
247 * The LUT gives us both T(Sample,Sun) and T(Ground,Sun) = T(Ground,Sample)*T(Sample,Sun),
248 * so we can easily compute T(Ground,Sample) from those two. */
249 const float4 T_ground_to_sample = lookup_transmittance_to_sun(0.0f) /
250 lookup_transmittance_to_sun(normalized_height);
251 /* 2nd order scattering from the ground. */
252 const float4 L_ground = PHASE_ISOTROPIC * omega * (GROUND_ALBEDO * M_1_PI_F) * T_to_ground *
253 T_ground_to_sample * cos_theta;
254 /* Fit of Earth's multiple scattering coming from other points in the atmosphere. */
255 const float4 L_ms = 0.02f * make_float4(0.217f, 0.347f, 0.594f, 1.0f) *
256 (1.0f / (1.0f + 5.0f * expf(-17.92f * cos_theta)));
257 return L_ms + L_ground;
258 }
259
260 /* Look up a transmittance from the precomputed LUT. */
261 inline float4 lookup_transmittance(const float cos_theta, const float normalized_altitude) const
262 {
263 const float u = saturate(cos_theta * 0.5f + 0.5f);
264 const float v = saturate(normalized_altitude);
265 const float x = float(TRANSMITTANCE_RES_X - 1) * u;
266 const float y = float(TRANSMITTANCE_RES_Y - 1) * v;
267 const int x1 = int(x);
268 const int y1 = int(y);
269 const int x2 = min(x1 + 1, TRANSMITTANCE_RES_X - 1);
270 const int y2 = min(y1 + 1, TRANSMITTANCE_RES_Y - 1);
271 const float fx = x - x1;
272 const float fy = y - y1;
273 const float4 bottom = mix(transmittance_lut[y1][x1], transmittance_lut[y1][x2], fx);
274 const float4 top = mix(transmittance_lut[y2][x1], transmittance_lut[y2][x2], fx);
275 return mix(bottom, top, fy);
276 }
277
278 /* Specialized versions of lookup_transmittance that skip one interpolation. */
280 {
281 const float u = saturate(cos_theta * 0.5f + 0.5f);
282 const float x = float(TRANSMITTANCE_RES_X - 1) * u;
283 const int x1 = int(x);
284 const int x2 = min(x1 + 1, TRANSMITTANCE_RES_X - 1);
285 const int y = 0;
286 const float fx = x - x1;
287 return mix(transmittance_lut[y][x1], transmittance_lut[y][x2], fx);
288 }
289
290 inline float4 lookup_transmittance_to_sun(const float normalized_altitude) const
291 {
292 const float v = saturate(normalized_altitude);
293 const float y = float(TRANSMITTANCE_RES_Y - 1) * v;
294 const int x = TRANSMITTANCE_RES_X - 1;
295 const int y1 = int(y);
296 const int y2 = min(y1 + 1, TRANSMITTANCE_RES_Y - 1);
297 const float fy = y - y1;
298 return mix(transmittance_lut[y1][x], transmittance_lut[y2][x], fy);
299 }
300};
301
303 int stride,
304 int width,
305 int height,
306 float sun_elevation,
307 float altitude,
308 float air_density,
309 float aerosol_density,
310 float ozone_density)
311{
312 SkyMultipleScattering sms(air_density, aerosol_density, ozone_density);
313 sms.precompute_lut();
314
315 /* Clamp altitude to avoid numerical issues. */
316 altitude = clamp(altitude, 1.0f, 99999.0f) / 1000.0f;
317 const int half_width = width / 2;
318 const float sun_zenith_cos_angle = cosf(M_PI_2_F - sun_elevation);
319 const float3 sun_dir = sun_direction(sun_zenith_cos_angle);
320 const int rows_per_task = std::max(1024 / width, 1);
321
322 SKY_parallel_for(0, height, rows_per_task, [&](const size_t begin, const size_t end) {
323 for (int y = begin; y < end; y++) {
324 float *pixel_row = pixels + (y * width * stride);
325 for (int x = 0; x < half_width; x++) {
326 float2 uv = make_float2((x + 0.5f) / width, (y + 0.5f) / height);
327
328 const float azimuth = M_2PI_F * uv.x;
329 /* Apply a non-linear transformation to the elevation to dedicate more texels to the
330 * horizon, where having more detail matters. */
331 const float l = uv.y * 2.0f - 1.0f;
332 /* [-pi/2, pi/2]. */
333 const float elev = copysignf(sqr(l), l) * M_PI_2_F;
334 const float3 ray_dir = make_float3(
335 cosf(elev) * cosf(azimuth), cosf(elev) * sinf(azimuth), sinf(elev));
336 const float3 ray_origin = make_float3(0.0f, 0.0f, EARTH_RADIUS + altitude);
337 const float atmos_dist = ray_sphere_intersection(ray_origin, ray_dir, ATMOSPHERE_RADIUS);
338 const float ground_dist = ray_sphere_intersection(ray_origin, ray_dir, EARTH_RADIUS);
339 /* If no ground collision then use the distance to the outer atmosphere, else we have a
340 * collision with the ground so we use the distance to it. */
341 const float t_d = (ground_dist < 0.0f) ? atmos_dist : ground_dist;
342 const float4 L = sms.get_inscattering(sun_dir, ray_origin, ray_dir, t_d);
343 const float3 sky = spectral_to_xyz(L);
344
345 /* Store pixels. */
346 const int pos_x = x * stride;
347 pixel_row[pos_x] = sky.x;
348 pixel_row[pos_x + 1] = sky.y;
349 pixel_row[pos_x + 2] = sky.z;
350 /* Mirror pixels. */
351 const int mirror_x = (width - x - 1) * stride;
352 pixel_row[mirror_x] = sky.x;
353 pixel_row[mirror_x + 1] = sky.y;
354 pixel_row[mirror_x + 2] = sky.z;
355 }
356 }
357 });
358}
359
361 float angular_diameter,
362 float altitude,
363 float air_density,
364 float aerosol_density,
365 float ozone_density,
366 float r_pixel_bottom[3],
367 float r_pixel_top[3])
368{
369 const SkyMultipleScattering sms(air_density, aerosol_density, ozone_density);
370
371 /* Clamp altitude to avoid numerical issues. */
372 altitude = clamp(altitude, 1.0f, 99999.0f) / 1000.0f;
373 const float half_angular = angular_diameter / 2.0f;
374 const float solid_angle = M_2PI_F * (1.0f - cosf(half_angular));
375 const float normalized_altitude = altitude / ATMOSPHERE_THICKNESS;
376
377 /* Compute 2 pixels for Sun disc: one is the lowest point of the disc, one is the highest. */
378 auto get_sun_xyz = [&](const float elevation) {
379 const float sun_zenith_cos_angle = cosf(M_PI_2_F - elevation);
380 const float4 transmittance_to_sun = sms.get_transmittance(sun_zenith_cos_angle,
381 normalized_altitude);
382 const float4 spectrum = SUN_SPECTRAL_IRRADIANCE * transmittance_to_sun / solid_angle;
383 return spectral_to_xyz(spectrum);
384 };
385 const float3 bottom = get_sun_xyz(sun_elevation - half_angular);
386 const float3 top = get_sun_xyz(sun_elevation + half_angular);
387
388 /* Store pixels */
389 r_pixel_bottom[0] = bottom.x;
390 r_pixel_bottom[1] = bottom.y;
391 r_pixel_bottom[2] = bottom.z;
392 r_pixel_top[0] = top.x;
393 r_pixel_top[1] = top.y;
394 r_pixel_top[2] = top.z;
395}
396
397float SKY_earth_intersection_angle(float altitude)
398{
399 /* Calculate intersection angle between line passing through viewpoint and Earth surface. */
400 return M_PI_2_F - asinf(EARTH_RADIUS / (EARTH_RADIUS + altitude / 1000.0f));
401}
MINLINE float safe_sqrtf(float a)
iter begin(iter)
ATTR_WARN_UNUSED_RESULT const BMLoop * l
ATTR_WARN_UNUSED_RESULT const BMVert * v
ccl_device_inline float cos_theta(const float3 w)
float4 get_inscattering(const float3 sun_dir, const float3 ray_origin, const float3 ray_dir, const float t_d) const
float4 get_transmittance(const float cos_theta, const float normalized_altitude) const
float4 lookup_transmittance(const float cos_theta, const float normalized_altitude) const
float4 lookup_transmittance_at_ground(const float cos_theta) const
void get_atmosphere_collision_coefficients(const float altitude, float4 &aerosol_absorption, float4 &aerosol_scattering, float4 &molecular_absorption, float4 &molecular_scattering) const
SkyMultipleScattering(const float air_density, const float aerosol_density, const float ozone_density)
float4 lookup_multiscattering(float cos_theta, float normalized_height, float d) const
float4 transmittance_lut[TRANSMITTANCE_RES_Y][TRANSMITTANCE_RES_X]
float4 lookup_transmittance_to_sun(const float normalized_altitude) const
nullptr float
dot(value.rgb, luminance_coefficients)") DEFINE_VALUE("REDUCE(lhs
#define M_PI_2_F
#define M_1_4PI_F
#define M_1_PI_F
#define logf(x)
#define expf(x)
#define powf(x, y)
ccl_device_forceinline float3 make_float3(const float x, const float y, const float z)
#define asinf(x)
#define copysignf(x, y)
uint top
#define exp
VecBase< float, D > step(VecOp< float, D >, VecOp< float, D >) RET
constexpr T clamp(T, U, U) RET
#define L
#define G(x, y, z)
#define mix
#define sqr
#define sqrtf
#define M_2PI_F
#define fmaxf
#define sinf
#define make_float2
#define make_float4
#define cosf
void SKY_parallel_for(const size_t begin, const size_t end, const size_t grainsize, const Function &function)
Definition sky_math.h:482
float ray_sphere_intersection(float3 pos, float3 dir, float radius)
Definition sky_math.h:462
float3 sun_direction(float sun_cos_theta)
Definition sky_math.h:457
static const float4 SUN_SPECTRAL_IRRADIANCE
float3 spectral_to_xyz(const float4 L)
static const int IN_SCATTERING_STEPS
static const float SQR_G
static const float4 MOLECULAR_SCATTERING_COEFFICIENT_BASE
void SKY_multiple_scattering_precompute_texture(float *pixels, int stride, int width, int height, float sun_elevation, float altitude, float air_density, float aerosol_density, float ozone_density)
static const int TRANSMITTANCE_RES_Y
static const float RAYLEIGH_PHASE_SCALE
float molecular_phase_function(const float cos_theta)
static const float PHASE_ISOTROPIC
static const int TRANSMITTANCE_RES_X
static const float EARTH_RADIUS
static const float ATMOSPHERE_RADIUS
static const float AEROSOL_BASE_DENSITY
static const float4 GROUND_ALBEDO
static const float3 SPECTRAL_XYZ[4]
float4 get_molecular_absorption_coefficient(const float h)
static const float4 AEROSOL_ABSORPTION_CROSS_SECTION
static const float AEROSOL_HEIGHT_SCALE
static const float4 AEROSOL_SCATTERING_CROSS_SECTION
static const int TRANSMITTANCE_STEPS
float get_aerosol_density(const float h)
static const float OZONE_MEAN_DOBSON
static const float AEROSOL_BACKGROUND_DENSITY
static const float4 OZONE_ABSORPTION_CROSS_SECTION
static const float ATMOSPHERE_THICKNESS
float aerosol_phase_function(const float cos_theta)
float4 get_molecular_scattering_coefficient(const float h)
void SKY_multiple_scattering_precompute_sun(float sun_elevation, float angular_diameter, float altitude, float air_density, float aerosol_density, float ozone_density, float r_pixel_bottom[3], float r_pixel_top[3])
float SKY_earth_intersection_angle(float altitude)
#define saturate(a)
Definition smaa.cc:315
#define min(a, b)
Definition sort.cc:36
float x
float y
float length() const
Definition sky_math.h:195
float z
Definition sky_math.h:136
float y
Definition sky_math.h:136
float x
Definition sky_math.h:136
i
Definition text_draw.cc:230
max
Definition text_draw.cc:251