227 lines
7.5 KiB
Haxe
227 lines
7.5 KiB
Haxe
package armory.renderpath;
|
|
|
|
import kha.FastFloat;
|
|
import kha.arrays.Float32Array;
|
|
import kha.graphics4.TextureFormat;
|
|
import kha.graphics4.Usage;
|
|
|
|
import iron.data.WorldData;
|
|
import iron.math.Vec2;
|
|
import iron.math.Vec3;
|
|
|
|
import armory.math.Helper;
|
|
|
|
/**
|
|
Utility class to control the Nishita sky model.
|
|
**/
|
|
class Nishita {
|
|
|
|
public static var data: NishitaData = null;
|
|
|
|
/**
|
|
Recomputes the nishita lookup table after the density settings changed.
|
|
Do not call this method on every frame (it's slow)!
|
|
**/
|
|
public static function recompute(world: WorldData) {
|
|
if (world == null || world.raw.nishita_density == null) return;
|
|
if (data == null) data = new NishitaData();
|
|
|
|
var density = world.raw.nishita_density;
|
|
data.computeLUT(new Vec3(density[0], density[1], density[2]));
|
|
}
|
|
|
|
/** Sets the sky's density parameters and calls `recompute()` afterwards. **/
|
|
public static function setDensity(world: WorldData, densityAir: FastFloat, densityDust: FastFloat, densityOzone: FastFloat) {
|
|
if (world == null) return;
|
|
|
|
if (world.raw.nishita_density == null) world.raw.nishita_density = new Float32Array(3);
|
|
var density = world.raw.nishita_density;
|
|
density[0] = Helper.clamp(densityAir, 0, 10);
|
|
density[1] = Helper.clamp(densityDust, 0, 10);
|
|
density[2] = Helper.clamp(densityOzone, 0, 10);
|
|
|
|
recompute(world);
|
|
}
|
|
}
|
|
|
|
/**
|
|
This class holds the precalculated result of the inner scattering integral
|
|
of the Nishita sky model. The outer integral is calculated in
|
|
[`armory/Shaders/std/sky.glsl`](https://github.com/armory3d/armory/blob/master/Shaders/std/sky.glsl).
|
|
|
|
@see `armory.renderpath.Nishita`
|
|
**/
|
|
class NishitaData {
|
|
|
|
public var lut: kha.Image;
|
|
|
|
/**
|
|
The amount of individual sample heights stored in the LUT (and the width
|
|
of the LUT image).
|
|
**/
|
|
public static var lutHeightSteps = 128;
|
|
/**
|
|
The amount of individual sun angle steps stored in the LUT (and the
|
|
height of the LUT image).
|
|
**/
|
|
public static var lutAngleSteps = 128;
|
|
|
|
/**
|
|
Amount of steps for calculating the inner scattering integral. Heigher
|
|
values are more precise but take longer to compute.
|
|
**/
|
|
public static var jSteps = 8;
|
|
|
|
/** Radius of the atmosphere in meters. **/
|
|
public static var radiusAtmo = 6420000;
|
|
/**
|
|
Radius of the planet in meters. The default value is the earth radius as
|
|
defined in Cycles.
|
|
**/
|
|
public static var radiusPlanet = 6360000;
|
|
|
|
/** Rayleigh scattering coefficient. **/
|
|
public static var rayleighCoeff = new Vec3(5.5e-6, 13.0e-6, 22.4e-6);
|
|
/** Rayleigh scattering scale parameter. **/
|
|
public static var rayleighScale = 8e3;
|
|
|
|
/** Mie scattering coefficient. **/
|
|
public static var mieCoeff = 2e-5;
|
|
/** Mie scattering scale parameter. **/
|
|
public static var mieScale = 1.2e3;
|
|
|
|
/** Ozone scattering coefficient. **/
|
|
// The ozone absorption coefficients are taken from Cycles code.
|
|
// Because Cycles calculates 21 wavelengths, we use the coefficients
|
|
// which are closest to the RGB wavelengths (645nm, 510nm, 440nm).
|
|
// Precalculating values by simulating Blender's spec_to_xyz() function
|
|
// to include all 21 wavelengths gave unrealistic results.
|
|
public static var ozoneCoeff = new Vec3(1.59051840791988e-6, 0.00000096707041180970, 0.00000007309568762914);
|
|
|
|
public function new() {}
|
|
|
|
/** Approximates the density of ozone for a given sample height. **/
|
|
function getOzoneDensity(height: FastFloat): FastFloat {
|
|
// Values are taken from Cycles code
|
|
if (height < 10000.0 || height >= 40000.0) {
|
|
return 0.0;
|
|
}
|
|
if (height < 25000.0) {
|
|
return (height - 10000.0) / 15000.0;
|
|
}
|
|
return -((height - 40000.0) / 15000.0);
|
|
}
|
|
|
|
/**
|
|
Ray-sphere intersection test that assumes the sphere is centered at the
|
|
origin. There is no intersection when result.x > result.y. Otherwise
|
|
this function returns the distances to the two intersection points,
|
|
which might be equal.
|
|
**/
|
|
function raySphereIntersection(rayOrigin: Vec3, rayDirection: Vec3, sphereRadius: Int): Vec2 {
|
|
// Algorithm is described here: https://en.wikipedia.org/wiki/Line%E2%80%93sphere_intersection
|
|
var a = rayDirection.dot(rayDirection);
|
|
var b = 2.0 * rayDirection.dot(rayOrigin);
|
|
var c = rayOrigin.dot(rayOrigin) - (sphereRadius * sphereRadius);
|
|
var d = (b * b) - 4.0 * a * c;
|
|
|
|
// Ray does not intersect the sphere
|
|
if (d < 0.0) return new Vec2(1e5, -1e5);
|
|
|
|
return new Vec2(
|
|
(-b - Math.sqrt(d)) / (2.0 * a),
|
|
(-b + Math.sqrt(d)) / (2.0 * a)
|
|
);
|
|
}
|
|
|
|
/**
|
|
Computes the LUT texture for the given density values.
|
|
@param density 3D vector of air density, dust density, ozone density
|
|
**/
|
|
public function computeLUT(density: Vec3) {
|
|
var imageData = new haxe.io.Float32Array(lutHeightSteps * lutAngleSteps * 4);
|
|
|
|
for (x in 0...lutHeightSteps) {
|
|
var height = (x / (lutHeightSteps - 1));
|
|
|
|
// Use quadratic height for better horizon precision
|
|
height *= height;
|
|
height *= radiusAtmo; // Denormalize
|
|
|
|
for (y in 0...lutAngleSteps) {
|
|
var sunTheta = y / (lutAngleSteps - 1) * 2 - 1;
|
|
|
|
// Improve horizon precision
|
|
// See https://sebh.github.io/publications/egsr2020.pdf (5.3)
|
|
sunTheta = Helper.sign(sunTheta) * sunTheta * sunTheta;
|
|
sunTheta = sunTheta * Math.PI / 2 + Math.PI / 2; // Denormalize
|
|
|
|
var jODepth = sampleSecondaryRay(height, sunTheta, density);
|
|
|
|
var pixelIndex = (x + y * lutHeightSteps) * 4;
|
|
imageData[pixelIndex + 0] = jODepth.x;
|
|
imageData[pixelIndex + 1] = jODepth.y;
|
|
imageData[pixelIndex + 2] = jODepth.z;
|
|
imageData[pixelIndex + 3] = 1.0; // Unused
|
|
}
|
|
}
|
|
|
|
lut = kha.Image.fromBytes(imageData.view.buffer, lutHeightSteps, lutAngleSteps, TextureFormat.RGBA128, Usage.StaticUsage);
|
|
}
|
|
|
|
/**
|
|
Calculates the integral for the secondary ray.
|
|
**/
|
|
public function sampleSecondaryRay(height: FastFloat, sunTheta: FastFloat, density: Vec3): Vec3 {
|
|
// Reconstruct values from the shader
|
|
var iPos = new Vec3(0, 0, height + radiusPlanet);
|
|
var pSun = new Vec3(0.0, Math.sin(sunTheta), Math.cos(sunTheta)).normalize();
|
|
|
|
var jTime: FastFloat = 0.0;
|
|
var jStepSize: FastFloat = raySphereIntersection(iPos, pSun, radiusAtmo).y / jSteps;
|
|
|
|
// Optical depth accumulators for the secondary ray (Rayleigh, Mie, ozone)
|
|
var jODepth = new Vec3();
|
|
|
|
for (i in 0...jSteps) {
|
|
|
|
// Calculate the secondary ray sample position and height
|
|
var jPos = iPos.clone().add(pSun.clone().mult(jTime + jStepSize * 0.5));
|
|
var jHeight = jPos.length() - radiusPlanet;
|
|
|
|
// Accumulate optical depth
|
|
var optDepthRayleigh = Math.exp(-jHeight / rayleighScale) * density.x;
|
|
var optDepthMie = Math.exp(-jHeight / mieScale) * density.y;
|
|
var optDepthOzone = getOzoneDensity(jHeight) * density.z;
|
|
jODepth.addf(optDepthRayleigh, optDepthMie, optDepthOzone);
|
|
|
|
jTime += jStepSize;
|
|
}
|
|
|
|
jODepth.mult(jStepSize);
|
|
|
|
// Precalculate a part of the secondary attenuation.
|
|
// For one variable (e.g. x) in the vector, the formula is as follows:
|
|
//
|
|
// attn.x = exp(-(coeffX * (firstOpticalDepth.x + secondOpticalDepth.x)))
|
|
//
|
|
// We can split that up via:
|
|
//
|
|
// attn.x = exp(-(coeffX * firstOpticalDepth.x + coeffX * secondOpticalDepth.x))
|
|
// = exp(-(coeffX * firstOpticalDepth.x)) * exp(-(coeffX * secondOpticalDepth.x))
|
|
//
|
|
// The first factor of the resulting multiplication is calculated in the
|
|
// shader, but we can already precalculate the second one. As a side
|
|
// effect this keeps the range of the LUT values small because we don't
|
|
// store the optical depth but the attenuation.
|
|
var jAttenuation = new Vec3();
|
|
var mie = mieCoeff * jODepth.y;
|
|
jAttenuation.addf(mie, mie, mie);
|
|
jAttenuation.add(rayleighCoeff.clone().mult(jODepth.x));
|
|
jAttenuation.add(ozoneCoeff.clone().mult(jODepth.z));
|
|
jAttenuation.exp(jAttenuation.mult(-1));
|
|
|
|
return jAttenuation;
|
|
}
|
|
}
|