diff --git a/Shaders/std/sky.glsl b/Shaders/std/sky.glsl index 9ad9964c..61f93c84 100644 --- a/Shaders/std/sky.glsl +++ b/Shaders/std/sky.glsl @@ -19,6 +19,9 @@ #ifndef _SKY_GLSL_ #define _SKY_GLSL_ +// OpenGl ES doesn't support 1D textures so we use a 1 px height sampler2D here... +uniform sampler2D nishitaLUT; + #define PI 3.141592 #define nishita_iSteps 16 @@ -46,6 +49,8 @@ // Values from [Hill: 60] #define sun_limb_darkening_col vec3(0.397, 0.503, 0.652) +#define heightToLUT(h) (textureLod(nishitaLUT, vec2(clamp(h * (1 / 60000.0), 0.0, 1.0), 0.0), 0.0).xyz * 10.0) + /* Approximates the density of ozone for a given sample height. Values taken from Cycles code. */ float nishita_density_ozone(const float height) { return (height < 10000.0 || height >= 40000.0) ? 0.0 : (height < 25000.0 ? (height - 10000.0) / 15000.0 : -((height - 40000.0) / 15000.0)); @@ -108,8 +113,9 @@ vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const floa float iHeight = length(iPos) - rPlanet; // Calculate the optical depth of the Rayleigh and Mie scattering for this step. - float odStepRlh = exp(-iHeight / nishita_rayleigh_scale) * density.x * iStepSize; - float odStepMie = exp(-iHeight / nishita_mie_scale) * density.y * iStepSize; + vec3 iLookup = heightToLUT(iHeight); + float odStepRlh = iLookup.x * iStepSize; + float odStepMie = iLookup.y * iStepSize; // Accumulate optical depth. iOdRlh += odStepRlh; @@ -134,11 +140,8 @@ vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const floa float jHeight = length(jPos) - rPlanet; // Accumulate the optical depth. - jODepth += vec3( - exp(-jHeight / nishita_rayleigh_scale) * density.x * jStepSize, - exp(-jHeight / nishita_mie_scale) * density.y * jStepSize, - nishita_density_ozone(jHeight) * density.z * jStepSize - ); + vec3 jLookup = heightToLUT(jHeight); + jODepth += jLookup * jStepSize; // Increment the secondary ray time. jTime += jStepSize; diff --git a/Sources/armory/math/Helper.hx b/Sources/armory/math/Helper.hx index 03351492..d3579d1e 100644 --- a/Sources/armory/math/Helper.hx +++ b/Sources/armory/math/Helper.hx @@ -72,4 +72,13 @@ class Helper { if (value <= leftMin) return rightMin; return map(value, leftMin, leftMax, rightMin, rightMax); } + + /** + Return the sign of the given value represented as `1.0` (positive value) + or `-1.0` (negative value). The sign of `0` is `0`. + **/ + public static inline function sign(value: Float): Float { + if (value == 0) return 0; + return (value < 0) ? -1.0 : 1.0; + } } diff --git a/Sources/armory/object/Uniforms.hx b/Sources/armory/object/Uniforms.hx index 91c55f30..64d20af2 100644 --- a/Sources/armory/object/Uniforms.hx +++ b/Sources/armory/object/Uniforms.hx @@ -19,8 +19,12 @@ class Uniforms { } public static function textureLink(object: Object, mat: MaterialData, link: String): kha.Image { + if (link == "_nishitaLUT") { + if (armory.renderpath.Nishita.data == null) armory.renderpath.Nishita.recompute(Scene.active.world); + return armory.renderpath.Nishita.data.optDepthLUT; + } #if arm_ltc - if (link == "_ltcMat") { + else if (link == "_ltcMat") { if (armory.data.ConstData.ltcMatTex == null) armory.data.ConstData.initLTC(); return armory.data.ConstData.ltcMatTex; } diff --git a/Sources/armory/renderpath/Nishita.hx b/Sources/armory/renderpath/Nishita.hx new file mode 100644 index 00000000..c968798d --- /dev/null +++ b/Sources/armory/renderpath/Nishita.hx @@ -0,0 +1,80 @@ +package armory.renderpath; + +import kha.FastFloat; +import kha.graphics4.TextureFormat; +import kha.graphics4.Usage; + +import iron.data.WorldData; + +import armory.math.Helper; + +class Nishita { + + public static var data: NishitaData = null; + + public static function recompute(world: WorldData) { + if (world == null || world.raw.sun_direction == null) return; + if (data == null) data = new NishitaData(); + + // TODO + data.recompute(1.0, 1.0, 1.0); + } +} + +class NishitaData { + static inline var LUT_WIDTH = 16; + + /** Maximum ray height as defined by Cycles **/ + static inline var MAX_HEIGHT = 60000; + + static inline var RAYLEIGH_SCALE = 8e3; + static inline var MIE_SCALE = 1.2e3; + + public var optDepthLUT: kha.Image; + + public function new() {} + + function getOzoneDensity(height: FastFloat): FastFloat { + 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); + } + + /** + The RGBA texture layout looks as follows: + R = Rayleigh optical depth at height \in [0, 60000] + G = Mie optical depth at height \in [0, 60000] + B = Ozone optical depth at height \in [0, 60000] + A = Unused + **/ + public function recompute(densityFacAir: FastFloat, densityFacDust: FastFloat, densityFacOzone: FastFloat) { + optDepthLUT = kha.Image.create(LUT_WIDTH, 1, TextureFormat.RGBA32, Usage.StaticUsage); + + var textureData = optDepthLUT.lock(); + for (i in 0...LUT_WIDTH) { + // Get the height for each LUT pixel i (in [-1, 1] range) + var height = (i / LUT_WIDTH) * 2 - 1; + + // Use quadratic height for better horizon precision + // See https://sebh.github.io/publications/egsr2020.pdf (5.3) + height = 0.5 + 0.5 * Helper.sign(height) * Math.sqrt(Math.abs(height)); + height *= MAX_HEIGHT; // Denormalize + + // Make sure we use 32 bit floats + var optDepthRayleigh: FastFloat = Math.exp(-height / RAYLEIGH_SCALE) * densityFacAir; + var optDepthMie: FastFloat = Math.exp(-height / MIE_SCALE) * densityFacDust; + var optDepthOzone: FastFloat = getOzoneDensity(height) * densityFacOzone; + + // 10 is the maximum density, so we divide by it to be able to use normalized values + textureData.set(i * 4 + 0, Std.int(optDepthRayleigh * 255 / 10)); + textureData.set(i * 4 + 1, Std.int(optDepthMie * 255 / 10)); + textureData.set(i * 4 + 2, Std.int(optDepthOzone * 255 / 10)); + textureData.set(i * 4 + 3, 255); // Unused + } + optDepthLUT.unlock(); + } +}