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; } }