Avoid sqrt of a negative number. Pull Request: https://projects.blender.org/blender/blender/pulls/146718
402 lines
18 KiB
C++
402 lines
18 KiB
C++
/* SPDX-FileCopyrightText: 2022 Fernando García Liñán
|
|
* SPDX-FileCopyrightText: 2011-2025 Blender Authors
|
|
*
|
|
* SPDX-License-Identifier: MIT */
|
|
|
|
/** \file
|
|
* \ingroup intern_sky_modal
|
|
*/
|
|
|
|
/*
|
|
* This code is a converted version of the ShaderToy written by Fernando García Liñán.
|
|
*
|
|
* This shader is the final result of my Master's Thesis.
|
|
* The main contributions are:
|
|
*
|
|
* 1. A spectral rendering technique that only requires 4 wavelength samples to
|
|
* get accurate results.
|
|
* 2. A multiple scattering approximation.
|
|
*
|
|
* Both of these approximations rely on an analytical fit, so they only work for
|
|
* Earth's atmosphere. We make up for it by using a very flexible atmosphere
|
|
* model that is able to represent a wide variety of atmospheric conditions.
|
|
*
|
|
* A brief description of this spectral rendering technique can be found in the
|
|
* following article:
|
|
* https://fgarlin.com/posts/2024-12-06-spectral_sky/
|
|
*
|
|
* The path tracer that has been used as a ground truth can be found at:
|
|
* https://github.com/fgarlin/skytracer
|
|
*/
|
|
|
|
#include <algorithm>
|
|
|
|
#include "sky_math.h"
|
|
#include "sky_nishita.h"
|
|
|
|
using std::min;
|
|
|
|
/* Earth's atmosphere parameters. */
|
|
/* Ground reflectance. */
|
|
static const float4 GROUND_ALBEDO = make_float4(0.3f, 0.3f, 0.3f, 0.3f);
|
|
static const float PHASE_ISOTROPIC = M_1_4PI_F;
|
|
static const float RAYLEIGH_PHASE_SCALE = (3.0f / 16.0f) * M_1_PI_F;
|
|
/* Aerosols anisotropy. */
|
|
static const float G = 0.8f;
|
|
static const float SQR_G = G * G;
|
|
/* Earth radius (km). */
|
|
static const float EARTH_RADIUS = 6371.0f;
|
|
/* Atmosphere thickness (km). */
|
|
static const float ATMOSPHERE_THICKNESS = 100.0f;
|
|
static const float ATMOSPHERE_RADIUS = EARTH_RADIUS + ATMOSPHERE_THICKNESS;
|
|
/* Ray marching steps. Higher steps means increased accuracy but worse performance. */
|
|
static const int TRANSMITTANCE_STEPS = 64;
|
|
static const int IN_SCATTERING_STEPS = 64;
|
|
|
|
/* LUTs. */
|
|
static const int TRANSMITTANCE_RES_X = 256;
|
|
static const int TRANSMITTANCE_RES_Y = 64;
|
|
|
|
/* Spectral data sampled at 630, 560, 490, 430 nm for urban area. */
|
|
static const float4 SUN_SPECTRAL_IRRADIANCE = make_float4(1.679f, 1.828f, 1.986f, 1.307f);
|
|
static const float4 MOLECULAR_SCATTERING_COEFFICIENT_BASE = make_float4(
|
|
6.605e-3f, 1.067e-2f, 1.842e-2f, 3.156e-2f);
|
|
static const float4 OZONE_ABSORPTION_CROSS_SECTION = make_float4(
|
|
3.472e-25f, 3.914e-25f, 1.349e-25f, 11.03e-27f);
|
|
/* Average ozone dobson of monthly mean values. */
|
|
static const float OZONE_MEAN_DOBSON = 334.5f;
|
|
static const float4 AEROSOL_ABSORPTION_CROSS_SECTION = make_float4(
|
|
2.8722e-24f, 4.6168e-24f, 7.9706e-24f, 1.3578e-23f);
|
|
static const float4 AEROSOL_SCATTERING_CROSS_SECTION = make_float4(
|
|
1.5908e-22f, 1.7711e-22f, 2.0942e-22f, 2.4033e-22f);
|
|
static const float AEROSOL_BASE_DENSITY = 1.3681e20f;
|
|
static const float AEROSOL_BACKGROUND_DENSITY = 2e6f;
|
|
static const float AEROSOL_HEIGHT_SCALE = 0.73f;
|
|
/* Spectral to XYZ space conversion matrix. */
|
|
static const float3 SPECTRAL_XYZ[4] = {
|
|
make_float3(53.386917738564668023f, 22.981337506691024754f, 0.0f),
|
|
make_float3(43.904844466369358263f, 71.347795700053393866f, 0.102506867965741307f),
|
|
make_float3(1.6137278251608962005f, 18.422960591455485011f, 31.742921188390805758f),
|
|
make_float3(20.762668673810577145f, 2.3614213523314368527f, 110.48009643252140334f),
|
|
};
|
|
|
|
inline float molecular_phase_function(const float cos_theta)
|
|
{
|
|
return RAYLEIGH_PHASE_SCALE * (1.0f + sqr(cos_theta));
|
|
}
|
|
|
|
inline float aerosol_phase_function(const float cos_theta)
|
|
{
|
|
float den = 1.0f + SQR_G + 2.0f * G * cos_theta;
|
|
return M_1_4PI_F * (1.0f - SQR_G) / (den * sqrtf(den));
|
|
}
|
|
|
|
inline float4 get_molecular_scattering_coefficient(const float h)
|
|
{
|
|
return MOLECULAR_SCATTERING_COEFFICIENT_BASE * expf(-0.07771971f * powf(h, 1.16364243f));
|
|
}
|
|
|
|
inline float4 get_molecular_absorption_coefficient(const float h)
|
|
{
|
|
const float log_h = logf(fmaxf(h, 1e-4f));
|
|
float density = 3.78547397e20f * expf(-sqr(log_h - 3.22261f) * 5.55555555f - log_h);
|
|
return OZONE_ABSORPTION_CROSS_SECTION * OZONE_MEAN_DOBSON * density;
|
|
}
|
|
|
|
inline float get_aerosol_density(const float h)
|
|
{
|
|
float division = AEROSOL_BACKGROUND_DENSITY / AEROSOL_BASE_DENSITY;
|
|
return AEROSOL_BASE_DENSITY * (expf(-h / AEROSOL_HEIGHT_SCALE) + division);
|
|
}
|
|
|
|
inline float3 spectral_to_xyz(const float4 L)
|
|
{
|
|
float3 xyz = make_float3(0.0f, 0.0f, 0.0f);
|
|
for (int i = 0; i < 4; i++) {
|
|
xyz += SPECTRAL_XYZ[i] * L[i];
|
|
}
|
|
return xyz;
|
|
}
|
|
|
|
/* Precomputed data. */
|
|
class SkyMultipleScattering {
|
|
public:
|
|
SkyMultipleScattering(const float air_density,
|
|
const float aerosol_density,
|
|
const float ozone_density)
|
|
: air_density(air_density), aerosol_density(aerosol_density), ozone_density(ozone_density)
|
|
{
|
|
}
|
|
|
|
/* Compute atmosphere's transmittance from the given altitude to the sun. */
|
|
inline float4 get_transmittance(const float cos_theta, const float normalized_altitude) const
|
|
{
|
|
const float3 sun_dir = sun_direction(cos_theta);
|
|
const float distance_to_earth_center = mix(
|
|
EARTH_RADIUS, ATMOSPHERE_RADIUS, normalized_altitude);
|
|
const float3 ray_origin = make_float3(0.0f, 0.0f, distance_to_earth_center);
|
|
const float t_d = ray_sphere_intersection(ray_origin, sun_dir, ATMOSPHERE_RADIUS);
|
|
const float t_step = t_d / TRANSMITTANCE_STEPS;
|
|
|
|
float4 result = make_float4(0.0f, 0.0f, 0.0f, 0.0f);
|
|
for (int step = 0; step < TRANSMITTANCE_STEPS; step++) {
|
|
const float t = (step + 0.5f) * t_step;
|
|
const float3 x_t = ray_origin + sun_dir * t;
|
|
const float altitude = fmaxf(x_t.length() - EARTH_RADIUS, 0.0f);
|
|
float4 aerosol_absorption, aerosol_scattering, molecular_absorption, molecular_scattering;
|
|
get_atmosphere_collision_coefficients(altitude,
|
|
aerosol_absorption,
|
|
aerosol_scattering,
|
|
molecular_absorption,
|
|
molecular_scattering);
|
|
const float4 extinction = aerosol_absorption + aerosol_scattering + molecular_absorption +
|
|
molecular_scattering;
|
|
result += extinction * t_step;
|
|
}
|
|
|
|
return exp(-result);
|
|
}
|
|
|
|
/* Compute in-scattered radiance for the given ray. */
|
|
float4 get_inscattering(const float3 sun_dir,
|
|
const float3 ray_origin,
|
|
const float3 ray_dir,
|
|
const float t_d) const
|
|
{
|
|
const float cos_theta = dot(-ray_dir, sun_dir);
|
|
const float molecular_phase = molecular_phase_function(cos_theta);
|
|
const float aerosol_phase = aerosol_phase_function(cos_theta);
|
|
const float dt = t_d / IN_SCATTERING_STEPS;
|
|
float4 L_inscattering = make_float4(0.0f, 0.0f, 0.0f, 0.0f);
|
|
float4 transmittance = make_float4(1.0f, 1.0f, 1.0f, 1.0f);
|
|
for (int i = 0; i < IN_SCATTERING_STEPS; i++) {
|
|
const float t = (i + 0.5f) * dt;
|
|
const float3 x_t = ray_origin + ray_dir * t;
|
|
const float distance_to_earth_center = x_t.length();
|
|
const float3 zenith_dir = x_t / distance_to_earth_center;
|
|
const float altitude = fmaxf(distance_to_earth_center - EARTH_RADIUS, 0.0f);
|
|
const float normalized_altitude = altitude / ATMOSPHERE_THICKNESS;
|
|
const float sample_cos_theta = dot(zenith_dir, sun_dir);
|
|
float4 aerosol_absorption, aerosol_scattering, molecular_absorption, molecular_scattering;
|
|
get_atmosphere_collision_coefficients(altitude,
|
|
aerosol_absorption,
|
|
aerosol_scattering,
|
|
molecular_absorption,
|
|
molecular_scattering);
|
|
const float4 extinction = aerosol_absorption + aerosol_scattering + molecular_absorption +
|
|
molecular_scattering;
|
|
const float4 transmittance_to_sun = lookup_transmittance(sample_cos_theta,
|
|
normalized_altitude);
|
|
const float4 ms = lookup_multiscattering(
|
|
sample_cos_theta, normalized_altitude, distance_to_earth_center);
|
|
const float4 S = SUN_SPECTRAL_IRRADIANCE *
|
|
(molecular_scattering * (molecular_phase * transmittance_to_sun + ms) +
|
|
aerosol_scattering * (aerosol_phase * transmittance_to_sun + ms));
|
|
const float4 step_transmittance = exp(-dt * extinction);
|
|
/* Energy-conserving analytical integration "Physically Based Sky, Atmosphere and Cloud
|
|
* Rendering in Frostbite" by Sébastien Hillaire. */
|
|
const float4 cut_ext = max(extinction, 1e-7f);
|
|
const float4 S_int = (S - S * step_transmittance) / cut_ext;
|
|
L_inscattering = L_inscattering + transmittance * S_int;
|
|
transmittance *= step_transmittance;
|
|
}
|
|
|
|
return L_inscattering;
|
|
}
|
|
|
|
/* Precompute the transmittance LUT. Must be called before get_inscattering(). */
|
|
void precompute_lut()
|
|
{
|
|
SKY_parallel_for(0, TRANSMITTANCE_RES_Y, 4, [&](const size_t begin, const size_t end) {
|
|
for (int y = begin; y < end; y++) {
|
|
for (int x = 0; x < TRANSMITTANCE_RES_X; x++) {
|
|
const float2 uv = make_float2(x / float(TRANSMITTANCE_RES_X - 1),
|
|
y / float(TRANSMITTANCE_RES_Y - 1));
|
|
transmittance_lut[y][x] = get_transmittance(uv.x * 2.0f - 1.0f, uv.y);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
protected:
|
|
float4 transmittance_lut[TRANSMITTANCE_RES_Y][TRANSMITTANCE_RES_X];
|
|
float air_density;
|
|
float aerosol_density;
|
|
float ozone_density;
|
|
|
|
/* Compute absorption/scattering coeffients at the given altitude. */
|
|
inline void get_atmosphere_collision_coefficients(const float altitude,
|
|
float4 &aerosol_absorption,
|
|
float4 &aerosol_scattering,
|
|
float4 &molecular_absorption,
|
|
float4 &molecular_scattering) const
|
|
{
|
|
const float local_aerosol_density = get_aerosol_density(altitude) * aerosol_density;
|
|
aerosol_absorption = AEROSOL_ABSORPTION_CROSS_SECTION * local_aerosol_density;
|
|
aerosol_scattering = AEROSOL_SCATTERING_CROSS_SECTION * local_aerosol_density;
|
|
molecular_absorption = get_molecular_absorption_coefficient(altitude) * ozone_density;
|
|
molecular_scattering = get_molecular_scattering_coefficient(altitude) * air_density;
|
|
}
|
|
|
|
inline float4 lookup_multiscattering(float cos_theta, float normalized_height, float d) const
|
|
{
|
|
/* Solid angle subtended by the planet from a point at d distance from the planet center. */
|
|
const float omega = M_2PI_F * (1.0f - safe_sqrtf(1.0f - sqr(EARTH_RADIUS / d)));
|
|
const float4 T_to_ground = lookup_transmittance_at_ground(cos_theta);
|
|
/* We can split the path into Ground <-> Sample <-> Sun.
|
|
* The LUT gives us both T(Sample,Sun) and T(Ground,Sun) = T(Ground,Sample)*T(Sample,Sun),
|
|
* so we can easily compute T(Ground,Sample) from those two. */
|
|
const float4 T_ground_to_sample = lookup_transmittance_to_sun(0.0f) /
|
|
lookup_transmittance_to_sun(normalized_height);
|
|
/* 2nd order scattering from the ground. */
|
|
const float4 L_ground = PHASE_ISOTROPIC * omega * (GROUND_ALBEDO * M_1_PI_F) * T_to_ground *
|
|
T_ground_to_sample * cos_theta;
|
|
/* Fit of Earth's multiple scattering coming from other points in the atmosphere. */
|
|
const float4 L_ms = 0.02f * make_float4(0.217f, 0.347f, 0.594f, 1.0f) *
|
|
(1.0f / (1.0f + 5.0f * expf(-17.92f * cos_theta)));
|
|
return L_ms + L_ground;
|
|
}
|
|
|
|
/* Look up a transmittance from the precomputed LUT. */
|
|
inline float4 lookup_transmittance(const float cos_theta, const float normalized_altitude) const
|
|
{
|
|
const float u = saturate(cos_theta * 0.5f + 0.5f);
|
|
const float v = saturate(normalized_altitude);
|
|
const float x = float(TRANSMITTANCE_RES_X - 1) * u;
|
|
const float y = float(TRANSMITTANCE_RES_Y - 1) * v;
|
|
const int x1 = int(x);
|
|
const int y1 = int(y);
|
|
const int x2 = min(x1 + 1, TRANSMITTANCE_RES_X - 1);
|
|
const int y2 = min(y1 + 1, TRANSMITTANCE_RES_Y - 1);
|
|
const float fx = x - x1;
|
|
const float fy = y - y1;
|
|
const float4 bottom = mix(transmittance_lut[y1][x1], transmittance_lut[y1][x2], fx);
|
|
const float4 top = mix(transmittance_lut[y2][x1], transmittance_lut[y2][x2], fx);
|
|
return mix(bottom, top, fy);
|
|
}
|
|
|
|
/* Specialized versions of lookup_transmittance that skip one interpolation. */
|
|
inline float4 lookup_transmittance_at_ground(const float cos_theta) const
|
|
{
|
|
const float u = saturate(cos_theta * 0.5f + 0.5f);
|
|
const float x = float(TRANSMITTANCE_RES_X - 1) * u;
|
|
const int x1 = int(x);
|
|
const int x2 = min(x1 + 1, TRANSMITTANCE_RES_X - 1);
|
|
const int y = 0;
|
|
const float fx = x - x1;
|
|
return mix(transmittance_lut[y][x1], transmittance_lut[y][x2], fx);
|
|
}
|
|
|
|
inline float4 lookup_transmittance_to_sun(const float normalized_altitude) const
|
|
{
|
|
const float v = saturate(normalized_altitude);
|
|
const float y = float(TRANSMITTANCE_RES_Y - 1) * v;
|
|
const int x = TRANSMITTANCE_RES_X - 1;
|
|
const int y1 = int(y);
|
|
const int y2 = min(y1 + 1, TRANSMITTANCE_RES_Y - 1);
|
|
const float fy = y - y1;
|
|
return mix(transmittance_lut[y1][x], transmittance_lut[y2][x], fy);
|
|
}
|
|
};
|
|
|
|
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)
|
|
{
|
|
SkyMultipleScattering sms(air_density, aerosol_density, ozone_density);
|
|
sms.precompute_lut();
|
|
|
|
/* Clamp altitude to avoid numerical issues. */
|
|
altitude = clamp(altitude, 1.0f, 99999.0f) / 1000.0f;
|
|
const int half_width = width / 2;
|
|
const float sun_zenith_cos_angle = cosf(M_PI_2_F - sun_elevation);
|
|
const float3 sun_dir = sun_direction(sun_zenith_cos_angle);
|
|
const int rows_per_task = std::max(1024 / width, 1);
|
|
|
|
SKY_parallel_for(0, height, rows_per_task, [=](const size_t begin, const size_t end) {
|
|
for (int y = begin; y < end; y++) {
|
|
float *pixel_row = pixels + (y * width * stride);
|
|
for (int x = 0; x < half_width; x++) {
|
|
float2 uv = make_float2((x + 0.5f) / width, (y + 0.5f) / height);
|
|
|
|
const float azimuth = M_2PI_F * uv.x;
|
|
/* Apply a non-linear transformation to the elevation to dedicate more texels to the
|
|
* horizon, where having more detail matters. */
|
|
const float l = uv.y * 2.0f - 1.0f;
|
|
/* [-pi/2, pi/2]. */
|
|
const float elev = copysignf(sqr(l), l) * M_PI_2_F;
|
|
const float3 ray_dir = make_float3(
|
|
cosf(elev) * cosf(azimuth), cosf(elev) * sinf(azimuth), sinf(elev));
|
|
const float3 ray_origin = make_float3(0.0f, 0.0f, EARTH_RADIUS + altitude);
|
|
const float atmos_dist = ray_sphere_intersection(ray_origin, ray_dir, ATMOSPHERE_RADIUS);
|
|
const float ground_dist = ray_sphere_intersection(ray_origin, ray_dir, EARTH_RADIUS);
|
|
/* If no ground collision then use the distance to the outer atmosphere, else we have a
|
|
* collision with the ground so we use the distance to it. */
|
|
const float t_d = (ground_dist < 0.0f) ? atmos_dist : ground_dist;
|
|
const float4 L = sms.get_inscattering(sun_dir, ray_origin, ray_dir, t_d);
|
|
const float3 sky = spectral_to_xyz(L);
|
|
|
|
/* Store pixels. */
|
|
const int pos_x = x * stride;
|
|
pixel_row[pos_x] = sky.x;
|
|
pixel_row[pos_x + 1] = sky.y;
|
|
pixel_row[pos_x + 2] = sky.z;
|
|
/* Mirror pixels. */
|
|
const int mirror_x = (width - x - 1) * stride;
|
|
pixel_row[mirror_x] = sky.x;
|
|
pixel_row[mirror_x + 1] = sky.y;
|
|
pixel_row[mirror_x + 2] = sky.z;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
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])
|
|
{
|
|
const SkyMultipleScattering sms(air_density, aerosol_density, ozone_density);
|
|
|
|
/* Clamp altitude to avoid numerical issues. */
|
|
altitude = clamp(altitude, 1.0f, 99999.0f) / 1000.0f;
|
|
const float half_angular = angular_diameter / 2.0f;
|
|
const float solid_angle = M_2PI_F * (1.0f - cosf(half_angular));
|
|
const float normalized_altitude = altitude / ATMOSPHERE_THICKNESS;
|
|
|
|
/* Compute 2 pixels for Sun disc: one is the lowest point of the disc, one is the highest. */
|
|
auto get_sun_xyz = [&](const float elevation) {
|
|
const float sun_zenith_cos_angle = cosf(M_PI_2_F - elevation);
|
|
const float4 transmittance_to_sun = sms.get_transmittance(sun_zenith_cos_angle,
|
|
normalized_altitude);
|
|
const float4 spectrum = SUN_SPECTRAL_IRRADIANCE * transmittance_to_sun / solid_angle;
|
|
return spectral_to_xyz(spectrum);
|
|
};
|
|
const float3 bottom = get_sun_xyz(sun_elevation - half_angular);
|
|
const float3 top = get_sun_xyz(sun_elevation + half_angular);
|
|
|
|
/* Store pixels */
|
|
r_pixel_bottom[0] = bottom.x;
|
|
r_pixel_bottom[1] = bottom.y;
|
|
r_pixel_bottom[2] = bottom.z;
|
|
r_pixel_top[0] = top.x;
|
|
r_pixel_top[1] = top.y;
|
|
r_pixel_top[2] = top.z;
|
|
}
|
|
|
|
float SKY_earth_intersection_angle(float altitude)
|
|
{
|
|
/* Calculate intersection angle between line passing through viewpoint and Earth surface. */
|
|
return M_PI_2_F - asinf(EARTH_RADIUS / (EARTH_RADIUS + altitude / 1000.0f));
|
|
}
|