using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
namespace BrunetonsImprovedAtmosphere
{
public class Model
{
private const int READ = 0;
private const int WRITE = 1;
private const double kLambdaR = 680.0;
private const double kLambdaG = 550.0;
private const double kLambdaB = 440.0;
private const int kLambdaMin = 360;
private const int kLambdaMax = 830;
///
/// The wavelength values, in nanometers, and sorted in increasing order, for
/// which the solar_irradiance, rayleigh_scattering, mie_scattering,
/// mie_extinction and ground_albedo samples are provided. If your shaders
/// use luminance values (as opposed to radiance values, see above), use a
/// large number of wavelengths (e.g. between 15 and 50) to get accurate
/// results (this number of wavelengths has absolutely no impact on the
/// shader performance).
///
public IList Wavelengths { get; set; }
///
/// The solar irradiance at the top of the atmosphere, in W/m^2/nm. This
/// vector must have the same size as the wavelengths parameter.
///
public IList SolarIrradiance { get; set; }
///
/// The sun's angular radius, in radians. Warning: the implementation uses
/// approximations that are valid only if this value is smaller than 0.1.
///
public double SunAngularRadius { get; set; }
///
/// The distance between the planet center and the bottom of the atmosphere in m.
///
public double BottomRadius { get; set; }
///
/// The distance between the planet center and the top of the atmosphere in m.
///
public double TopRadius { get; set; }
///
/// The density profile of air molecules, i.e. a function from altitude to
/// dimensionless values between 0 (null density) and 1 (maximum density).
/// Layers must be sorted from bottom to top. The width of the last layer is
/// ignored, i.e. it always extend to the top atmosphere boundary. At most 2
/// layers can be specified.
///
public DensityProfileLayer RayleighDensity { get; set; }
///
/// The scattering coefficient of air molecules at the altitude where their
/// density is maximum (usually the bottom of the atmosphere), as a function
/// of wavelength, in m^-1. The scattering coefficient at altitude h is equal
/// to 'rayleigh_scattering' times 'rayleigh_density' at this altitude. This
/// vector must have the same size as the wavelengths parameter.
///
public IList RayleighScattering { get; set; }
///
/// The density profile of aerosols, i.e. a function from altitude to
/// dimensionless values between 0 (null density) and 1 (maximum density).
/// Layers must be sorted from bottom to top. The width of the last layer is
/// ignored, i.e. it always extend to the top atmosphere boundary. At most 2
/// layers can be specified.
///
public DensityProfileLayer MieDensity { get; set; }
///
/// The scattering coefficient of aerosols at the altitude where their
/// density is maximum (usually the bottom of the atmosphere), as a function
/// of wavelength, in m^-1. The scattering coefficient at altitude h is equal
/// to 'mie_scattering' times 'mie_density' at this altitude. This vector
/// must have the same size as the wavelengths parameter.
///
public IList MieScattering { get; set; }
///
/// The extinction coefficient of aerosols at the altitude where their
/// density is maximum (usually the bottom of the atmosphere), as a function
/// of wavelength, in m^-1. The extinction coefficient at altitude h is equal
/// to 'mie_extinction' times 'mie_density' at this altitude. This vector
/// must have the same size as the wavelengths parameter.
///
public IList MieExtinction { get; set; }
///
/// The asymetry parameter for the Cornette-Shanks phase function for the aerosols.
///
public double MiePhaseFunctionG { get; set; }
///
/// The density profile of air molecules that absorb light (e.g. ozone), i.e.
/// a function from altitude to dimensionless values between 0 (null density)
/// and 1 (maximum density). Layers must be sorted from bottom to top. The
/// width of the last layer is ignored, i.e. it always extend to the top
/// atmosphere boundary. At most 2 layers can be specified.
///
public IList AbsorptionDensity { get; set; }
///
/// The extinction coefficient of molecules that absorb light (e.g. ozone) at
/// the altitude where their density is maximum, as a function of wavelength,
/// in m^-1. The extinction coefficient at altitude h is equal to
/// 'absorption_extinction' times 'absorption_density' at this altitude. This
/// vector must have the same size as the wavelengths parameter.
///
public IList AbsorptionExtinction { get; set; }
///
/// The average albedo of the ground, as a function of wavelength. This
/// vector must have the same size as the wavelengths parameter.
///
public IList GroundAlbedo { get; set; }
///
/// The maximum Sun zenith angle for which atmospheric scattering must be
/// precomputed, in radians (for maximum precision, use the smallest Sun
/// zenith angle yielding negligible sky light radiance values. For instance,
/// for the Earth case, 102 degrees is a good choice for most cases (120
/// degrees is necessary for very high exposure values).
///
public double MaxSunZenithAngle { get; set; }
///
/// The length unit used in your shaders and meshes. This is the length unit
/// which must be used when calling the atmosphere model shader functions.
///
public double LengthUnitInMeters { get; set; }
///
/// The number of wavelengths for which atmospheric scattering must be
/// precomputed (the temporary GPU memory used during precomputations, and
/// the GPU memory used by the precomputed results, is independent of this
/// number, but the precomputation time is directly proportional to this number):
/// - if this number is less than or equal to 3, scattering is precomputed
/// for 3 wavelengths, and stored as irradiance values. Then both the
/// radiance-based and the luminance-based API functions are provided (see
/// the above note).
/// - otherwise, scattering is precomputed for this number of wavelengths
/// (rounded up to a multiple of 3), integrated with the CIE color matching
/// functions, and stored as illuminance values. Then only the
/// luminance-based API functions are provided (see the above note).
///
public int NumPrecomputedWavelengths { get { return UseLuminance == LUMINANCE.PRECOMPUTED ? 15 : 3; } }
///
/// Whether to pack the (red component of the) single Mie scattering with the
/// Rayleigh and multiple scattering in a single texture, or to store the
/// (3 components of the) single Mie scattering in a separate texture.
///
public bool CombineScatteringTextures { get; set; }
///
/// Use radiance or luminance mode.
///
public LUMINANCE UseLuminance { get; set; }
///
/// Whether to use half precision floats (16 bits) or single precision floats
/// (32 bits) for the precomputed textures. Half precision is sufficient for
/// most cases, except for very high exposure values.
///
public bool HalfPrecision { get; set; }
public RenderTexture TransmittanceTexture { get; private set; }
public RenderTexture ScatteringTexture { get; private set; }
public RenderTexture IrradianceTexture { get; private set; }
public RenderTexture OptionalSingleMieScatteringTexture { get; private set; }
public Model()
{
}
///
/// Bind to a pixel shader for rendering.
///
public void BindToMaterial(Material mat)
{
if (UseLuminance == LUMINANCE.NONE)
mat.EnableKeyword("RADIANCE_API_ENABLED");
else
mat.DisableKeyword("RADIANCE_API_ENABLED");
if (CombineScatteringTextures)
mat.EnableKeyword("COMBINED_SCATTERING_TEXTURES");
else
mat.DisableKeyword("COMBINED_SCATTERING_TEXTURES");
mat.SetTexture("transmittance_texture", TransmittanceTexture);
mat.SetTexture("scattering_texture", ScatteringTexture);
mat.SetTexture("irradiance_texture", IrradianceTexture);
if(CombineScatteringTextures)
mat.SetTexture("single_mie_scattering_texture", Texture2D.blackTexture);
else
mat.SetTexture("single_mie_scattering_texture", OptionalSingleMieScatteringTexture);
mat.SetInt("TRANSMITTANCE_TEXTURE_WIDTH", CONSTANTS.TRANSMITTANCE_WIDTH);
mat.SetInt("TRANSMITTANCE_TEXTURE_HEIGHT", CONSTANTS.TRANSMITTANCE_HEIGHT);
mat.SetInt("SCATTERING_TEXTURE_R_SIZE", CONSTANTS.SCATTERING_R);
mat.SetInt("SCATTERING_TEXTURE_MU_SIZE", CONSTANTS.SCATTERING_MU);
mat.SetInt("SCATTERING_TEXTURE_MU_S_SIZE", CONSTANTS.SCATTERING_MU_S);
mat.SetInt("SCATTERING_TEXTURE_NU_SIZE", CONSTANTS.SCATTERING_NU);
mat.SetInt("SCATTERING_TEXTURE_WIDTH", CONSTANTS.SCATTERING_WIDTH);
mat.SetInt("SCATTERING_TEXTURE_HEIGHT", CONSTANTS.SCATTERING_HEIGHT);
mat.SetInt("SCATTERING_TEXTURE_DEPTH", CONSTANTS.SCATTERING_DEPTH);
mat.SetInt("IRRADIANCE_TEXTURE_WIDTH", CONSTANTS.IRRADIANCE_WIDTH);
mat.SetInt("IRRADIANCE_TEXTURE_HEIGHT", CONSTANTS.IRRADIANCE_HEIGHT);
mat.SetFloat("sun_angular_radius", (float)SunAngularRadius);
mat.SetFloat("bottom_radius", (float)(BottomRadius / LengthUnitInMeters));
mat.SetFloat("top_radius", (float)(TopRadius / LengthUnitInMeters));
mat.SetFloat("mie_phase_function_g", (float)MiePhaseFunctionG);
mat.SetFloat("mu_s_min", (float)Math.Cos(MaxSunZenithAngle));
Vector3 skySpectralRadianceToLuminance, sunSpectralRadianceToLuminance;
SkySunRadianceToLuminance(out skySpectralRadianceToLuminance, out sunSpectralRadianceToLuminance);
mat.SetVector("SKY_SPECTRAL_RADIANCE_TO_LUMINANCE", skySpectralRadianceToLuminance);
mat.SetVector("SUN_SPECTRAL_RADIANCE_TO_LUMINANCE", sunSpectralRadianceToLuminance);
double[] lambdas = new double[] { kLambdaR, kLambdaG, kLambdaB };
Vector3 solarIrradiance = ToVector(Wavelengths, SolarIrradiance, lambdas, 1.0);
mat.SetVector("solar_irradiance", solarIrradiance);
Vector3 rayleighScattering = ToVector(Wavelengths, RayleighScattering, lambdas, LengthUnitInMeters);
mat.SetVector("rayleigh_scattering", rayleighScattering);
Vector3 mieScattering = ToVector(Wavelengths, MieScattering, lambdas, LengthUnitInMeters);
mat.SetVector("mie_scattering", mieScattering);
}
// Added by Dan Shervheim.
// danielshervheim.com
// August, 2019
public void GetTextures(out RenderTexture transmittance, out RenderTexture scattering, out RenderTexture singleMieScattering, out RenderTexture irradiance)
{
transmittance = TransmittanceTexture;
scattering = ScatteringTexture;
singleMieScattering = OptionalSingleMieScatteringTexture;
irradiance = IrradianceTexture;
}
public void Release()
{
ReleaseTexture(TransmittanceTexture);
ReleaseTexture(ScatteringTexture);
ReleaseTexture(IrradianceTexture);
ReleaseTexture(OptionalSingleMieScatteringTexture);
}
///
/// The Init method precomputes the atmosphere textures. It first allocates the
/// temporary resources it needs, then calls Precompute to do the
/// actual precomputations, and finally destroys the temporary resources.
///
/// Note that there are two precomputation modes here, depending on whether we
/// want to store precomputed irradiance or illuminance values:
///
/// In precomputed irradiance mode, we simply need to call
/// Precompute with the 3 wavelengths for which we want to precompute
/// irradiance, namely kLambdaR, kLambdaG, kLambdaB(with the identity matrix for
/// luminance_from_radiance, since we don't want any conversion from radiance to luminance).
///
/// In precomputed illuminance mode, we need to precompute irradiance for
/// num_precomputed_wavelengths, and then integrate the results,
/// multiplied with the 3 CIE xyz color matching functions and the XYZ to sRGB
/// matrix to get sRGB illuminance values.
/// A naive solution would be to allocate temporary textures for the
/// intermediate irradiance results, then perform the integration from irradiance
/// to illuminance and store the result in the final precomputed texture. In
/// pseudo-code (and assuming one wavelength per texture instead of 3):
///
/// create n temporary irradiance textures
/// for each wavelength lambda in the n wavelengths:
/// precompute irradiance at lambda into one of the temporary textures
/// initializes the final illuminance texture with zeros
/// for each wavelength lambda in the n wavelengths:
/// accumulate in the final illuminance texture the product of the
/// precomputed irradiance at lambda (read from the temporary textures)
/// with the value of the 3 sRGB color matching functions at lambda
/// (i.e. the product of the XYZ to sRGB matrix with the CIE xyz color matching functions).
///
/// However, this be would waste GPU memory. Instead, we can avoid allocating
/// temporary irradiance textures, by merging the two above loops:
///
/// for each wavelength lambda in the n wavelengths:
/// accumulate in the final illuminance texture (or, for the first
/// iteration, set this texture to) the product of the precomputed
/// irradiance at lambda (computed on the fly) with the value of the 3
/// sRGB color matching functions at lambda.
///
/// This is the method we use below, with 3 wavelengths per iteration instead
/// of 1, using Precompute to compute 3 irradiances values per
/// iteration, and luminance_from_radiance to multiply 3 irradiances
/// with the values of the 3 sRGB color matching functions at 3 different
/// wavelengths (yielding a 3x3 matrix).
///
/// This yields the following implementation:
///
public void Init(ComputeShader compute, int num_scattering_orders)
{
// The precomputations require temporary textures, in particular to store the
// contribution of one scattering order, which is needed to compute the next
// order of scattering (the final precomputed textures store the sum of all
// the scattering orders). We allocate them here, and destroy them at the end
// of this method.
TextureBuffer buffer = new TextureBuffer(HalfPrecision);
buffer.Clear(compute);
// The actual precomputations depend on whether we want to store precomputed
// irradiance or illuminance values.
if (NumPrecomputedWavelengths <= 3)
{
Precompute(compute, buffer, null, null, false, num_scattering_orders);
}
else
{
int num_iterations = (NumPrecomputedWavelengths + 2) / 3;
double dlambda = (kLambdaMax - kLambdaMin) / (3.0 * num_iterations);
for (int i = 0; i < num_iterations; ++i)
{
double[] lambdas = new double[]
{
kLambdaMin + (3 * i + 0.5) * dlambda,
kLambdaMin + (3 * i + 1.5) * dlambda,
kLambdaMin + (3 * i + 2.5) * dlambda
};
double[] luminance_from_radiance = new double[]
{
Coeff(lambdas[0], 0) * dlambda, Coeff(lambdas[1], 0) * dlambda, Coeff(lambdas[2], 0) * dlambda,
Coeff(lambdas[0], 1) * dlambda, Coeff(lambdas[1], 1) * dlambda, Coeff(lambdas[2], 1) * dlambda,
Coeff(lambdas[0], 2) * dlambda, Coeff(lambdas[1], 2) * dlambda, Coeff(lambdas[2], 2) * dlambda
};
bool blend = i > 0;
Precompute(compute, buffer, lambdas, luminance_from_radiance, blend, num_scattering_orders);
}
// After the above iterations, the transmittance texture contains the
// transmittance for the 3 wavelengths used at the last iteration. But we
// want the transmittance at kLambdaR, kLambdaG, kLambdaB instead, so we
// must recompute it here for these 3 wavelengths:
int compute_transmittance = compute.FindKernel("ComputeTransmittance");
BindToCompute(compute, null, null);
compute.SetTexture(compute_transmittance, "transmittanceWrite", buffer.TransmittanceArray[WRITE]);
compute.SetVector("blend", new Vector4(0, 0, 0, 0));
int NUM = CONSTANTS.NUM_THREADS;
compute.Dispatch(compute_transmittance, CONSTANTS.TRANSMITTANCE_WIDTH / NUM, CONSTANTS.TRANSMITTANCE_HEIGHT / NUM, 1);
Swap(buffer.TransmittanceArray);
}
//Grab ref to textures and mark as null in buffer so they are not released.
TransmittanceTexture = buffer.TransmittanceArray[READ];
buffer.TransmittanceArray[READ] = null;
ScatteringTexture = buffer.ScatteringArray[READ];
buffer.ScatteringArray[READ] = null;
IrradianceTexture = buffer.IrradianceArray[READ];
buffer.IrradianceArray[READ] = null;
if(CombineScatteringTextures)
{
OptionalSingleMieScatteringTexture = null;
}
else
{
OptionalSingleMieScatteringTexture = buffer.OptionalSingleMieScatteringArray[READ];
buffer.OptionalSingleMieScatteringArray[READ] = null;
}
// Delete the temporary resources allocated at the begining of this method.
buffer.Release();
}
private double Coeff(double lambda, int component)
{
// Note that we don't include MAX_LUMINOUS_EFFICACY here, to avoid
// artefacts due to too large values when using half precision on GPU.
// We add this term back in kAtmosphereShader, via
// SKY_SPECTRAL_RADIANCE_TO_LUMINANCE (see also the comments in the
// Model constructor).
double x = CieColorMatchingFunctionTableValue(lambda, 1);
double y = CieColorMatchingFunctionTableValue(lambda, 2);
double z = CieColorMatchingFunctionTableValue(lambda, 3);
double sRGB = CONSTANTS.XYZ_TO_SRGB[component * 3 + 0] * x +
CONSTANTS.XYZ_TO_SRGB[component * 3 + 1] * y +
CONSTANTS.XYZ_TO_SRGB[component * 3 + 2] * z;
return sRGB;
}
///
/// Bind to a compute shader for precomutation of textures.
///
private void BindToCompute(ComputeShader compute, double[] lambdas, double[] luminance_from_radiance)
{
if(lambdas == null)
lambdas = new double[] { kLambdaR, kLambdaG, kLambdaB };
if(luminance_from_radiance == null)
luminance_from_radiance = new double[] { 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0 };
compute.SetInt("TRANSMITTANCE_TEXTURE_WIDTH", CONSTANTS.TRANSMITTANCE_WIDTH);
compute.SetInt("TRANSMITTANCE_TEXTURE_HEIGHT", CONSTANTS.TRANSMITTANCE_HEIGHT);
compute.SetInt("SCATTERING_TEXTURE_R_SIZE", CONSTANTS.SCATTERING_R);
compute.SetInt("SCATTERING_TEXTURE_MU_SIZE", CONSTANTS.SCATTERING_MU);
compute.SetInt("SCATTERING_TEXTURE_MU_S_SIZE", CONSTANTS.SCATTERING_MU_S);
compute.SetInt("SCATTERING_TEXTURE_NU_SIZE", CONSTANTS.SCATTERING_NU);
compute.SetInt("SCATTERING_TEXTURE_WIDTH", CONSTANTS.SCATTERING_WIDTH);
compute.SetInt("SCATTERING_TEXTURE_HEIGHT", CONSTANTS.SCATTERING_HEIGHT);
compute.SetInt("SCATTERING_TEXTURE_DEPTH", CONSTANTS.SCATTERING_DEPTH);
compute.SetInt("IRRADIANCE_TEXTURE_WIDTH", CONSTANTS.IRRADIANCE_WIDTH);
compute.SetInt("IRRADIANCE_TEXTURE_HEIGHT", CONSTANTS.IRRADIANCE_HEIGHT);
Vector3 skySpectralRadianceToLuminance, sunSpectralRadianceToLuminance;
SkySunRadianceToLuminance(out skySpectralRadianceToLuminance, out sunSpectralRadianceToLuminance);
compute.SetVector("SKY_SPECTRAL_RADIANCE_TO_LUMINANCE", skySpectralRadianceToLuminance);
compute.SetVector("SUN_SPECTRAL_RADIANCE_TO_LUMINANCE", sunSpectralRadianceToLuminance);
Vector3 solarIrradiance = ToVector(Wavelengths, SolarIrradiance, lambdas, 1.0);
compute.SetVector("solar_irradiance", solarIrradiance);
Vector3 rayleighScattering = ToVector(Wavelengths, RayleighScattering, lambdas, LengthUnitInMeters);
BindDensityLayer(compute, RayleighDensity);
compute.SetVector("rayleigh_scattering", rayleighScattering);
Vector3 mieScattering = ToVector(Wavelengths, MieScattering, lambdas, LengthUnitInMeters);
Vector3 mieExtinction = ToVector(Wavelengths, MieExtinction, lambdas, LengthUnitInMeters);
BindDensityLayer(compute, MieDensity);
compute.SetVector("mie_scattering", mieScattering);
compute.SetVector("mie_extinction", mieExtinction);
Vector3 absorptionExtinction = ToVector(Wavelengths, AbsorptionExtinction, lambdas, LengthUnitInMeters);
BindDensityLayer(compute, AbsorptionDensity[0]);
BindDensityLayer(compute, AbsorptionDensity[1]);
compute.SetVector("absorption_extinction", absorptionExtinction);
Vector3 groundAlbedo = ToVector(Wavelengths, GroundAlbedo, lambdas, 1.0);
compute.SetVector("ground_albedo", groundAlbedo);
compute.SetFloats("luminanceFromRadiance", ToMatrix(luminance_from_radiance));
compute.SetFloat("sun_angular_radius", (float)SunAngularRadius);
compute.SetFloat("bottom_radius", (float)(BottomRadius / LengthUnitInMeters));
compute.SetFloat("top_radius", (float)(TopRadius / LengthUnitInMeters));
compute.SetFloat("mie_phase_function_g", (float)MiePhaseFunctionG);
compute.SetFloat("mu_s_min", (float)Math.Cos(MaxSunZenithAngle));
}
private void BindDensityLayer(ComputeShader compute, DensityProfileLayer layer)
{
compute.SetFloat(layer.Name + "_width", (float)(layer.Width / LengthUnitInMeters));
compute.SetFloat(layer.Name + "_exp_term", (float)layer.ExpTerm);
compute.SetFloat(layer.Name + "_exp_scale", (float)(layer.ExpScale * LengthUnitInMeters));
compute.SetFloat(layer.Name + "_linear_term", (float)(layer.LinearTerm * LengthUnitInMeters));
compute.SetFloat(layer.Name + "_constant_term", (float)layer.ConstantTerm);
}
private Vector3 ToVector(IList wavelengths, IList v, IList lambdas, double scale)
{
double r = Interpolate(wavelengths, v, lambdas[0]) * scale;
double g = Interpolate(wavelengths, v, lambdas[1]) * scale;
double b = Interpolate(wavelengths, v, lambdas[2]) * scale;
return new Vector3((float)r, (float)g, (float)b);
}
///
/// Finally, we need a utility function to compute the value of the conversion
/// constants *_RADIANCE_TO_LUMINANCE, used above to convert the
/// spectral results into luminance values. These are the constants k_r, k_g, k_b
/// described in Section 14.3 of A
/// Qualitative and Quantitative Evaluation of 8 Clear Sky Models.
///
/// Computing their value requires an integral of a function times a CIE color
/// matching function. Thus, we first need functions to interpolate an arbitrary
/// function (specified by some samples), and a CIE color matching function
/// (specified by tabulated values), at an arbitrary wavelength. This is the purpose
/// of the following two functions:
///
private static double CieColorMatchingFunctionTableValue(double wavelength, int column)
{
if (wavelength <= kLambdaMin || wavelength >= kLambdaMax) return 0.0;
double u = (wavelength - kLambdaMin) / 5.0;
int row = (int)Math.Floor(u);
u -= row;
return CONSTANTS.CIE_2_DEG_COLOR_MATCHING_FUNCTIONS[4 * row + column] * (1.0 - u) + CONSTANTS.CIE_2_DEG_COLOR_MATCHING_FUNCTIONS[4 * (row + 1) + column] * u;
}
private static double Interpolate(IList wavelengths, IList wavelength_function, double wavelength)
{
if (wavelength < wavelengths[0]) return wavelength_function[0];
for (int i = 0; i < wavelengths.Count - 1; ++i)
{
if (wavelength < wavelengths[i + 1])
{
double u = (wavelength - wavelengths[i]) / (wavelengths[i + 1] - wavelengths[i]);
return wavelength_function[i] * (1.0 - u) + wavelength_function[i + 1] * u;
}
}
return wavelength_function[wavelength_function.Count - 1];
}
///
/// Compute the values for the SKY_RADIANCE_TO_LUMINANCE constant. In theory
/// this should be 1 in precomputed illuminance mode (because the precomputed
/// textures already contain illuminance values). In practice, however, storing
/// true illuminance values in half precision textures yields artefacts
/// (because the values are too large), so we store illuminance values divided
/// by MAX_LUMINOUS_EFFICACY instead. This is why, in precomputed illuminance
/// mode, we set SKY_RADIANCE_TO_LUMINANCE to MAX_LUMINOUS_EFFICACY.
///
private void SkySunRadianceToLuminance(out Vector3 skySpectralRadianceToLuminance, out Vector3 sunSpectralRadianceToLuminance)
{
bool precompute_illuminance = NumPrecomputedWavelengths > 3;
double sky_k_r, sky_k_g, sky_k_b;
if (precompute_illuminance)
sky_k_r = sky_k_g = sky_k_b = CONSTANTS.MAX_LUMINOUS_EFFICACY;
else
ComputeSpectralRadianceToLuminanceFactors(Wavelengths, SolarIrradiance, -3, out sky_k_r, out sky_k_g, out sky_k_b);
// Compute the values for the SUN_RADIANCE_TO_LUMINANCE constant.
double sun_k_r, sun_k_g, sun_k_b;
ComputeSpectralRadianceToLuminanceFactors(Wavelengths, SolarIrradiance, 0, out sun_k_r, out sun_k_g, out sun_k_b);
skySpectralRadianceToLuminance = new Vector3((float)sky_k_r, (float)sky_k_g, (float)sky_k_b);
sunSpectralRadianceToLuminance = new Vector3((float)sun_k_r, (float)sun_k_g, (float)sun_k_b);
}
///
/// We can then implement a utility function to compute the "spectral radiance to
/// luminance" conversion constants (see Section 14.3 in A Qualitative and Quantitative
/// Evaluation of 8 Clear Sky Models for their definitions):
/// The returned constants are in lumen.nm / watt.
///
private static void ComputeSpectralRadianceToLuminanceFactors(IList wavelengths, IList solar_irradiance, double lambda_power, out double k_r, out double k_g, out double k_b)
{
k_r = 0.0;
k_g = 0.0;
k_b = 0.0;
double solar_r = Interpolate(wavelengths, solar_irradiance, kLambdaR);
double solar_g = Interpolate(wavelengths, solar_irradiance, kLambdaG);
double solar_b = Interpolate(wavelengths, solar_irradiance, kLambdaB);
int dlambda = 1;
for (int lambda = kLambdaMin; lambda < kLambdaMax; lambda += dlambda)
{
double x_bar = CieColorMatchingFunctionTableValue(lambda, 1);
double y_bar = CieColorMatchingFunctionTableValue(lambda, 2);
double z_bar = CieColorMatchingFunctionTableValue(lambda, 3);
double[] xyz2srgb = CONSTANTS.XYZ_TO_SRGB;
double r_bar = xyz2srgb[0] * x_bar + xyz2srgb[1] * y_bar + xyz2srgb[2] * z_bar;
double g_bar = xyz2srgb[3] * x_bar + xyz2srgb[4] * y_bar + xyz2srgb[5] * z_bar;
double b_bar = xyz2srgb[6] * x_bar + xyz2srgb[7] * y_bar + xyz2srgb[8] * z_bar;
double irradiance = Interpolate(wavelengths, solar_irradiance, lambda);
k_r += r_bar * irradiance / solar_r * Math.Pow(lambda / kLambdaR, lambda_power);
k_g += g_bar * irradiance / solar_g * Math.Pow(lambda / kLambdaG, lambda_power);
k_b += b_bar * irradiance / solar_b * Math.Pow(lambda / kLambdaB, lambda_power);
}
k_r *= CONSTANTS.MAX_LUMINOUS_EFFICACY * dlambda;
k_g *= CONSTANTS.MAX_LUMINOUS_EFFICACY * dlambda;
k_b *= CONSTANTS.MAX_LUMINOUS_EFFICACY * dlambda;
}
///
/// The utility method ConvertSpectrumToLinearSrgb is implemented
/// with a simple numerical integration of the given function, times the CIE color
/// matching funtions(with an integration step of 1nm), followed by a matrix
/// multiplication:
///
public void ConvertSpectrumToLinearSrgb(out double r, out double g, out double b)
{
double x = 0.0;
double y = 0.0;
double z = 0.0;
const int dlambda = 1;
for (int lambda = kLambdaMin; lambda < kLambdaMax; lambda += dlambda)
{
double value = Interpolate(Wavelengths, SolarIrradiance, lambda);
x += CieColorMatchingFunctionTableValue(lambda, 1) * value;
y += CieColorMatchingFunctionTableValue(lambda, 2) * value;
z += CieColorMatchingFunctionTableValue(lambda, 3) * value;
}
double[] XYZ_TO_SRGB = CONSTANTS.XYZ_TO_SRGB;
r = CONSTANTS.MAX_LUMINOUS_EFFICACY * (XYZ_TO_SRGB[0] * x + XYZ_TO_SRGB[1] * y + XYZ_TO_SRGB[2] * z) * dlambda;
g = CONSTANTS.MAX_LUMINOUS_EFFICACY * (XYZ_TO_SRGB[3] * x + XYZ_TO_SRGB[4] * y + XYZ_TO_SRGB[5] * z) * dlambda;
b = CONSTANTS.MAX_LUMINOUS_EFFICACY * (XYZ_TO_SRGB[6] * x + XYZ_TO_SRGB[7] * y + XYZ_TO_SRGB[8] * z) * dlambda;
}
///
/// Finally, we provide the actual implementation of the precomputation algorithm
/// described in Algorithm 4.1 of
/// our paper. Each step is
/// explained by the inline comments below.
///
void Precompute(
ComputeShader compute,
TextureBuffer buffer,
double[] lambdas,
double[] luminance_from_radiance,
bool blend,
int num_scattering_orders)
{
int BLEND = blend ? 1 : 0;
int NUM_THREADS = CONSTANTS.NUM_THREADS;
BindToCompute(compute, lambdas, luminance_from_radiance);
int compute_transmittance = compute.FindKernel("ComputeTransmittance");
int compute_direct_irradiance = compute.FindKernel("ComputeDirectIrradiance");
int compute_single_scattering = compute.FindKernel("ComputeSingleScattering");
int compute_scattering_density = compute.FindKernel("ComputeScatteringDensity");
int compute_indirect_irradiance = compute.FindKernel("ComputeIndirectIrradiance");
int compute_multiple_scattering = compute.FindKernel("ComputeMultipleScattering");
// Compute the transmittance, and store it in transmittance_texture
compute.SetTexture(compute_transmittance, "transmittanceWrite", buffer.TransmittanceArray[WRITE]);
compute.SetVector("blend", new Vector4(0, 0, 0, 0));
compute.Dispatch(compute_transmittance, CONSTANTS.TRANSMITTANCE_WIDTH / NUM_THREADS, CONSTANTS.TRANSMITTANCE_HEIGHT / NUM_THREADS, 1);
Swap(buffer.TransmittanceArray);
// Compute the direct irradiance, store it in delta_irradiance_texture and,
// depending on 'blend', either initialize irradiance_texture_ with zeros or
// leave it unchanged (we don't want the direct irradiance in
// irradiance_texture_, but only the irradiance from the sky).
compute.SetTexture(compute_direct_irradiance, "deltaIrradianceWrite", buffer.DeltaIrradianceTexture); //0
compute.SetTexture(compute_direct_irradiance, "irradianceWrite", buffer.IrradianceArray[WRITE]); //1
compute.SetTexture(compute_direct_irradiance, "irradianceRead", buffer.IrradianceArray[READ]);
compute.SetTexture(compute_direct_irradiance, "transmittanceRead", buffer.TransmittanceArray[READ]);
compute.SetVector("blend", new Vector4(0, BLEND, 0, 0));
compute.Dispatch(compute_direct_irradiance, CONSTANTS.IRRADIANCE_WIDTH / NUM_THREADS, CONSTANTS.IRRADIANCE_HEIGHT / NUM_THREADS, 1);
Swap(buffer.IrradianceArray);
// Compute the rayleigh and mie single scattering, store them in
// delta_rayleigh_scattering_texture and delta_mie_scattering_texture, and
// either store them or accumulate them in scattering_texture_ and
// optional_single_mie_scattering_texture_.
compute.SetTexture(compute_single_scattering, "deltaRayleighScatteringWrite", buffer.DeltaRayleighScatteringTexture); //0
compute.SetTexture(compute_single_scattering, "deltaMieScatteringWrite", buffer.DeltaMieScatteringTexture); //1
compute.SetTexture(compute_single_scattering, "scatteringWrite", buffer.ScatteringArray[WRITE]); //2
compute.SetTexture(compute_single_scattering, "scatteringRead", buffer.ScatteringArray[READ]);
compute.SetTexture(compute_single_scattering, "singleMieScatteringWrite", buffer.OptionalSingleMieScatteringArray[WRITE]); //3
compute.SetTexture(compute_single_scattering, "singleMieScatteringRead", buffer.OptionalSingleMieScatteringArray[READ]);
compute.SetTexture(compute_single_scattering, "transmittanceRead", buffer.TransmittanceArray[READ]);
compute.SetVector("blend", new Vector4(0, 0, BLEND, BLEND));
for (int layer = 0; layer < CONSTANTS.SCATTERING_DEPTH; ++layer)
{
compute.SetInt("layer", layer);
compute.Dispatch(compute_single_scattering, CONSTANTS.SCATTERING_WIDTH / NUM_THREADS, CONSTANTS.SCATTERING_HEIGHT / NUM_THREADS, 1);
}
Swap(buffer.ScatteringArray);
Swap(buffer.OptionalSingleMieScatteringArray);
// Compute the 2nd, 3rd and 4th order of scattering, in sequence.
for (int scattering_order = 2; scattering_order <= num_scattering_orders; ++scattering_order)
{
// Compute the scattering density, and store it in
// delta_scattering_density_texture.
compute.SetTexture(compute_scattering_density, "deltaScatteringDensityWrite", buffer.DeltaScatteringDensityTexture); //0
compute.SetTexture(compute_scattering_density, "transmittanceRead", buffer.TransmittanceArray[READ]);
compute.SetTexture(compute_scattering_density, "singleRayleighScatteringRead", buffer.DeltaRayleighScatteringTexture);
compute.SetTexture(compute_scattering_density, "singleMieScatteringRead", buffer.DeltaMieScatteringTexture);
compute.SetTexture(compute_scattering_density, "multipleScatteringRead", buffer.DeltaMultipleScatteringTexture);
compute.SetTexture(compute_scattering_density, "irradianceRead", buffer.DeltaIrradianceTexture);
compute.SetInt("scatteringOrder", scattering_order);
compute.SetVector("blend", new Vector4(0, 0, 0, 0));
for (int layer = 0; layer < CONSTANTS.SCATTERING_DEPTH; ++layer)
{
compute.SetInt("layer", layer);
compute.Dispatch(compute_scattering_density, CONSTANTS.SCATTERING_WIDTH / NUM_THREADS, CONSTANTS.SCATTERING_HEIGHT / NUM_THREADS, 1);
}
// Compute the indirect irradiance, store it in delta_irradiance_texture and
// accumulate it in irradiance_texture_.
compute.SetTexture(compute_indirect_irradiance, "deltaIrradianceWrite", buffer.DeltaIrradianceTexture); //0
compute.SetTexture(compute_indirect_irradiance, "irradianceWrite", buffer.IrradianceArray[WRITE]); //1
compute.SetTexture(compute_indirect_irradiance, "irradianceRead", buffer.IrradianceArray[READ]);
compute.SetTexture(compute_indirect_irradiance, "singleRayleighScatteringRead", buffer.DeltaRayleighScatteringTexture);
compute.SetTexture(compute_indirect_irradiance, "singleMieScatteringRead", buffer.DeltaMieScatteringTexture);
compute.SetTexture(compute_indirect_irradiance, "multipleScatteringRead", buffer.DeltaMultipleScatteringTexture);
compute.SetInt("scatteringOrder", scattering_order - 1);
compute.SetVector("blend", new Vector4(0, 1, 0, 0));
compute.Dispatch(compute_indirect_irradiance, CONSTANTS.IRRADIANCE_WIDTH / NUM_THREADS, CONSTANTS.IRRADIANCE_HEIGHT / NUM_THREADS, 1);
Swap(buffer.IrradianceArray);
// Compute the multiple scattering, store it in
// delta_multiple_scattering_texture, and accumulate it in
// scattering_texture_.
compute.SetTexture(compute_multiple_scattering, "deltaMultipleScatteringWrite", buffer.DeltaMultipleScatteringTexture); //0
compute.SetTexture(compute_multiple_scattering, "scatteringWrite", buffer.ScatteringArray[WRITE]); //1
compute.SetTexture(compute_multiple_scattering, "scatteringRead", buffer.ScatteringArray[READ]);
compute.SetTexture(compute_multiple_scattering, "transmittanceRead", buffer.TransmittanceArray[READ]);
compute.SetTexture(compute_multiple_scattering, "deltaScatteringDensityRead", buffer.DeltaScatteringDensityTexture);
compute.SetVector("blend", new Vector4(0, 1, 0, 0));
for (int layer = 0; layer < CONSTANTS.SCATTERING_DEPTH; ++layer)
{
compute.SetInt("layer", layer);
compute.Dispatch(compute_multiple_scattering, CONSTANTS.SCATTERING_WIDTH / NUM_THREADS, CONSTANTS.SCATTERING_HEIGHT / NUM_THREADS, 1);
}
Swap(buffer.ScatteringArray);
}
return;
}
private void Swap(RenderTexture[] arr)
{
RenderTexture tmp = arr[READ];
arr[READ] = arr[WRITE];
arr[WRITE] = tmp;
}
private void ReleaseTexture(RenderTexture tex)
{
if (tex == null) return;
GameObject.DestroyImmediate(tex);
}
///
/// Convert a double array to a float matrix to bind to compute shader.
/// Array is transposed.
///
private float[] ToMatrix(double[] arr)
{
return new float[]
{
(float)arr[0], (float)arr[3], (float)arr[6], 0,
(float)arr[1], (float)arr[4], (float)arr[7], 0,
(float)arr[2], (float)arr[5], (float)arr[8], 0,
0, 0, 0, 1
};
}
}
}