diff --git a/leenkx/Shaders/std/sky.glsl b/leenkx/Shaders/std/sky.glsl index f793984e..cb79bb27 100644 --- a/leenkx/Shaders/std/sky.glsl +++ b/leenkx/Shaders/std/sky.glsl @@ -1,11 +1,11 @@ /* Various sky functions * ===================== * - * Nishita model is based on https://github.com/wwwtyro/glsl-atmosphere (Unlicense License) + * Single scattering model is based on https://github.com/wwwtyro/glsl-atmosphere (Unlicense License) * * Changes to the original implementation: - * - r and pSun parameters of nishita_atmosphere() are already normalized - * - Some original parameters of nishita_atmosphere() are replaced with pre-defined values + * - r and pSun parameters of single_scatter_atmosphere() are already normalized + * - Some original parameters of single_scatter_atmosphere() are replaced with pre-defined values * - Implemented air, dust and ozone density node parameters (see Blender source) * - Replaced the inner integral calculation with a LUT lookup * @@ -22,8 +22,8 @@ #include "std/math.glsl" -uniform sampler2D nishitaLUT; -uniform vec2 nishitaDensity; +uniform sampler2D singleScatterLUT; +uniform vec2 skyDensity; #ifndef PI #define PI 3.141592 @@ -32,33 +32,33 @@ uniform vec2 nishitaDensity; #define HALF_PI 1.570796 #endif -#define nishita_iSteps 16 +#define single_scatter_iSteps 16 // These values are taken from Cycles code if they // exist there, otherwise they are taken from the example // in the glsl-atmosphere repo -#define nishita_sun_intensity 22.0 -#define nishita_atmo_radius 6420e3 -#define nishita_rayleigh_scale 8e3 -#define nishita_rayleigh_coeff vec3(5.5e-6, 13.0e-6, 22.4e-6) -#define nishita_mie_scale 1.2e3 -#define nishita_mie_coeff 2e-5 -#define nishita_mie_dir 0.76 // Aerosols anisotropy ("direction") -#define nishita_mie_dir_sq 0.5776 // Squared aerosols anisotropy +#define single_scatter_sun_intensity 22.0 +#define single_scatter_atmo_radius 6420e3 +#define single_scatter_rayleigh_scale 8e3 +#define single_scatter_rayleigh_coeff vec3(5.5e-6, 13.0e-6, 22.4e-6) +#define single_scatter_mie_scale 1.2e3 +#define single_scatter_mie_coeff 2e-5 +#define single_scatter_mie_dir 0.76 // Aerosols anisotropy ("direction") +#define single_scatter_mie_dir_sq 0.5776 // Squared aerosols anisotropy // Values from [Hill: 60] #define sun_limb_darkening_col vec3(0.397, 0.503, 0.652) -vec3 nishita_lookupLUT(const float height, const float sunTheta) { +vec3 single_scatter_lookupLUT(const float height, const float sunTheta) { vec2 coords = vec2( - sqrt(height * (1 / nishita_atmo_radius)), + sqrt(height * (1 / single_scatter_atmo_radius)), 0.5 + 0.5 * sign(sunTheta - HALF_PI) * sqrt(abs(sunTheta * (1 / HALF_PI) - 1)) ); - return textureLod(nishitaLUT, coords, 0.0).rgb; + return textureLod(singleScatterLUT, coords, 0.0).rgb; } -/* See raySphereIntersection() in leenkx/Sources/renderpath/Nishita.hx */ -vec2 nishita_rsi(const vec3 r0, const vec3 rd, const float sr) { +/* See raySphereIntersection() in leenkx/Sources/renderpath/Sky.hx */ +vec2 single_scatter_rsi(const vec3 r0, const vec3 rd, const float sr) { float a = dot(rd, rd); float b = 2.0 * dot(rd, r0); float c = dot(r0, r0) - (sr * sr); @@ -74,12 +74,12 @@ vec2 nishita_rsi(const vec3 r0, const vec3 rd, const float sr) { * pSun: normalized sun direction * rPlanet: planet radius */ -vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const float rPlanet) { +vec3 single_scatter_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const float rPlanet) { // Calculate the step size of the primary ray - vec2 p = nishita_rsi(r0, r, nishita_atmo_radius); + vec2 p = single_scatter_rsi(r0, r, single_scatter_atmo_radius); if (p.x > p.y) return vec3(0.0); - p.y = min(p.y, nishita_rsi(r0, r, rPlanet).x); - float iStepSize = (p.y - p.x) / float(nishita_iSteps); + p.y = min(p.y, single_scatter_rsi(r0, r, rPlanet).x); + float iStepSize = (p.y - p.x) / float(single_scatter_iSteps); // Primary ray time float iTime = 0.0; @@ -96,18 +96,18 @@ vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const floa float mu = dot(r, pSun); float mumu = mu * mu; float pRlh = 3.0 / (16.0 * PI) * (1.0 + mumu); - float pMie = 3.0 / (8.0 * PI) * ((1.0 - nishita_mie_dir_sq) * (mumu + 1.0)) / (pow(1.0 + nishita_mie_dir_sq - 2.0 * mu * nishita_mie_dir, 1.5) * (2.0 + nishita_mie_dir_sq)); + float pMie = 3.0 / (8.0 * PI) * ((1.0 - single_scatter_mie_dir_sq) * (mumu + 1.0)) / (pow(1.0 + single_scatter_mie_dir_sq - 2.0 * mu * single_scatter_mie_dir, 1.5) * (2.0 + single_scatter_mie_dir_sq)); // Sample the primary ray - for (int i = 0; i < nishita_iSteps; i++) { + for (int i = 0; i < single_scatter_iSteps; i++) { // Calculate the primary ray sample position and height vec3 iPos = r0 + r * (iTime + iStepSize * 0.5); 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) * nishitaDensity.x * iStepSize; - float odStepMie = exp(-iHeight / nishita_mie_scale) * nishitaDensity.y * iStepSize; + float odStepRlh = exp(-iHeight / single_scatter_rayleigh_scale) * skyDensity.x * iStepSize; + float odStepMie = exp(-iHeight / single_scatter_mie_scale) * skyDensity.y * iStepSize; // Accumulate optical depth iOdRlh += odStepRlh; @@ -116,12 +116,12 @@ vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const floa // Idea behind this: "Rotate" everything by iPos (-> iPos is the new zenith) and then all calculations for the // inner integral only depend on the sample height (iHeight) and sunTheta (angle between sun and new zenith). float sunTheta = safe_acos(dot(normalize(iPos), normalize(pSun))); - vec3 jAttn = nishita_lookupLUT(iHeight, sunTheta); + vec3 jAttn = single_scatter_lookupLUT(iHeight, sunTheta); // Calculate attenuation vec3 iAttn = exp(-( - nishita_mie_coeff * iOdMie - + nishita_rayleigh_coeff * iOdRlh + single_scatter_mie_coeff * iOdMie + + single_scatter_rayleigh_coeff * iOdRlh // + 0 for ozone )); vec3 attn = iAttn * jAttn; @@ -136,7 +136,7 @@ vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const floa iTime += iStepSize; } - return nishita_sun_intensity * (pRlh * nishita_rayleigh_coeff * totalRlh + pMie * nishita_mie_coeff * totalMie); + return single_scatter_sun_intensity * (pRlh * single_scatter_rayleigh_coeff * totalRlh + pMie * single_scatter_mie_coeff * totalMie); } vec3 sun_disk(const vec3 n, const vec3 light_dir, const float disk_size, const float intensity) { @@ -149,7 +149,76 @@ vec3 sun_disk(const vec3 n, const vec3 light_dir, const float disk_size, const f float mu = sqrt(invDist * invDist); vec3 limb_darkening = 1.0 - (1.0 - pow(vec3(mu), sun_limb_darkening_col)); - return 1 + (1.0 - step(1.0, dist)) * nishita_sun_intensity * intensity * limb_darkening; + return 1 + (1.0 - step(1.0, dist)) * single_scatter_sun_intensity * intensity * limb_darkening; +} + +uniform sampler2D multiScatterLUT; +uniform vec4 multiScatterParams; // x=elevation, y=rotation, z=angular_diameter, w=intensity +uniform vec4 multiScatterSunBottom; // xyz=sun_bottom, w=earth_intersection_angle +uniform vec3 multiScatterSunTop; + +// XYZ to sRGB/Rec.709 conversion (D65 white point) +vec3 xyz_to_rgb(vec3 xyz) { + return vec3( + 3.2406 * xyz.x - 1.5372 * xyz.y - 0.4986 * xyz.z, + -0.9689 * xyz.x + 1.8758 * xyz.y + 0.0415 * xyz.z, + 0.0557 * xyz.x - 0.2040 * xyz.y + 1.0570 * xyz.z + ); +} + +float sky_elevation_to_v(float elevation) { + float abs_el = abs(elevation); + float l = sign(elevation) * sqrt(abs_el / 1.5707963); + float v = (l + 1.0) * 0.5; + return clamp(v, 0.0, 1.0); +} + +vec3 multi_scatter_sample_lut(vec3 dir, float sun_rotation) { + float azimuth = atan(dir.x, dir.y); + float elevation = asin(clamp(dir.z, -1.0, 1.0)); + + azimuth -= sun_rotation; + float u = fract(azimuth / (2.0 * PI)); + float v = sky_elevation_to_v(elevation); + + return textureLod(multiScatterLUT, vec2(u, v), 0.0).rgb; +} + +vec3 multi_scatter_sun_disc(vec3 dir, vec3 sun_dir, float angular_diameter, float intensity) { + float dist = distance(dir, sun_dir) / (angular_diameter * 0.5); + if (dist > 1.0) return vec3(0.0); + + float invDist = 1.0 - dist; + float mu = sqrt(invDist * invDist); + vec3 limb_darkening = 1.0 - (1.0 - pow(vec3(mu), sun_limb_darkening_col)); + + float sun_elev = multiScatterParams.x; + float dir_elev = asin(clamp(dir.z, -1.0, 1.0)); + float t = clamp((dir_elev - (sun_elev - angular_diameter * 0.5)) / angular_diameter, 0.0, 1.0); + vec3 sun_color = mix(multiScatterSunBottom.rgb, multiScatterSunTop, t) * intensity * limb_darkening; + + return xyz_to_rgb(sun_color); +} + +vec3 multi_scatter_atmosphere(vec3 dir) { + float sun_elevation = multiScatterParams.x; + float sun_rotation = multiScatterParams.y; + float angular_diameter = multiScatterParams.z; + float sun_intensity = multiScatterParams.w; + + vec3 xyz = multi_scatter_sample_lut(dir, sun_rotation); + vec3 radiance = xyz_to_rgb(xyz); + + if (sun_intensity > 0.0) { + vec3 computed_sun_dir = vec3( + sin(sun_rotation) * cos(sun_elevation), + cos(sun_rotation) * cos(sun_elevation), + sin(sun_elevation) + ); + radiance += multi_scatter_sun_disc(dir, computed_sun_dir, angular_diameter, sun_intensity); + } + + return radiance; } #endif diff --git a/leenkx/Shaders/water_pass/water_pass.frag.glsl b/leenkx/Shaders/water_pass/water_pass.frag.glsl index 2bf016bb..2c21f25c 100644 --- a/leenkx/Shaders/water_pass/water_pass.frag.glsl +++ b/leenkx/Shaders/water_pass/water_pass.frag.glsl @@ -88,58 +88,103 @@ vec4 rayCast(vec3 dir) { } #endif //SSR +vec3 sampleWaterNormals(vec2 hitXY, float speed, out vec2 tcnor0, out vec2 tcnor1) { + tcnor0 = hitXY / 3.0; + vec3 n0 = textureLod(sdetail, tcnor0 + vec2(speed / 60.0, speed / 120.0), 0.0).rgb; + tcnor1 = hitXY / 6.0 + n0.xy / 20.0; + vec3 n1 = textureLod(sbase, tcnor1 + vec2(speed / 40.0, speed / 80.0), 0.0).rgb; + return normalize(n0 + n1 - 1.0); +} + void main() { float gdepth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; - if (gdepth == 1.0) { - fragColor = vec4(0.0); - return; - } - // Eye below water - if (eye.z < waterLevel) { - fragColor = vec4(0.0); - return; - } - // Displace surface vec3 vray = normalize(viewRay); - vec3 p = getPos(eye, eyeLook, vray, gdepth, cameraProj); float speed = time * 2.0 * waterSpeed; - p.z += sin(p.x * 10.0 / waterDisplace + speed) * cos(p.y * 10.0 / waterDisplace + speed) / 50.0 * waterDisplace; + bool isSky = (gdepth == 1.0); + + // Ray-plane intersection with water surface (z = waterLevel) + float denom = dot(vray, vec3(0.0, 0.0, 1.0)); + float tWater = (waterLevel - eye.z) / denom; + bool hasWaterHit = (abs(denom) > 0.0001) && (tWater > 0.0); + + if (eye.z < waterLevel) { + vec2 tc = texCoord; + float fogFactor; + + if (hasWaterHit && denom > 0.0) { + // Looking up at water surface - apply normal distortion + vec3 hit = eye + tWater * vray; + vec2 tc0, tc1; + vec3 n2 = sampleWaterNormals(hit.xy * waterFreq, speed, tc0, tc1); + tc = texCoord + (n2.xy * n2.z) / 30.0 * waterRefract; + fogFactor = clamp(tWater * waterDensity, 0.0, 0.95); + } else { + // Looking forward/down - distort via water surface above fragment + vec3 p = getPos(eye, eyeLook, vray, gdepth, cameraProj); + vec2 wxy = isSky ? (eye.xy + vray.xy * 50.0) : p.xy; + vec2 tc0, tc1; + vec3 n2 = sampleWaterNormals(wxy * waterFreq, speed, tc0, tc1); + tc = texCoord + (n2.xy * n2.z) / 30.0 * waterRefract; + float waterDist = isSky ? 50.0 : length(p - eye); + fogFactor = clamp(waterDist * waterDensity, 0.0, 0.95); + } + + vec3 refracted = textureLod(tex, tc, 0.0).rgb; + fragColor.rgb = mix(refracted, waterColor, fogFactor); + fragColor.a = 1.0; + return; + } // Above water - if (p.z > waterLevel) { + if (!hasWaterHit || denom >= 0.0) { fragColor = vec4(0.0); return; } + + if (isSky) tWater = min(tWater, 100.0); // Clamp to prevent aliasing at horizon + vec3 p = isSky ? (eye + tWater * vray) : getPos(eye, eyeLook, vray, gdepth, cameraProj); + + float horizonFactor = clamp(1.0 - tWater / 60.0, 0.0, 1.0); + if (!isSky && p.z > waterLevel) { + fragColor = vec4(0.0); + return; + } + + // Displace surface + p.z += (sin(p.x * 10.0 / waterDisplace + speed) * cos(p.y * 10.0 / waterDisplace + speed) + + sin(p.x * 20.0 / waterDisplace + speed * 1.3) * cos(p.y * 20.0 / waterDisplace + speed * 1.3) * 0.5) + / 50.0 * waterDisplace; + // Hit plane to determine uvs - vec3 v = normalize(eye - p.xyz); - float t = -(dot(eye, vec3(0.0, 0.0, 1.0)) - waterLevel) / dot(v, vec3(0.0, 0.0, 1.0)); + vec3 v = normalize(eye - p); + float t = (waterLevel - eye.z) / dot(v, vec3(0.0, 0.0, 1.0)); vec3 hit = eye + t * v; - hit.xy *= waterFreq; - hit.z += waterLevel; // Sample normal maps - vec2 tcnor0 = hit.xy / 3.0; - vec3 n0 = textureLod(sdetail, tcnor0 + vec2(speed / 60.0, speed / 120.0), 0.0).rgb; + vec2 tcnor0, tcnor1; + vec3 n2 = sampleWaterNormals(hit.xy * waterFreq, speed, tcnor0, tcnor1); - vec2 tcnor1 = hit.xy / 6.0 + n0.xy / 20.0; - vec3 n1 = textureLod(sbase, tcnor1 + vec2(speed / 40.0, speed / 80.0), 0.0).rgb; - vec3 n2 = normalize(((n1 + n0) / 2.0) * 2.0 - 1.0); - - float ddepth = textureLod(gbufferD, texCoord + (n2.xy * n2.z) / 40.0, 0.0).r * 2.0 - 1.0; - vec3 p2 = getPos(eye, eyeLook, vray, ddepth, cameraProj); - vec2 tc = p2.z > waterLevel ? texCoord : texCoord + (n2.xy * n2.z) / 30.0 * waterRefract; + // Refraction + vec2 tc; + if (isSky) { + tc = texCoord + (n2.xy * n2.z) / 30.0 * waterRefract; + } else { + float ddepth = textureLod(gbufferD, texCoord + (n2.xy * n2.z) / 40.0, 0.0).r * 2.0 - 1.0; + vec3 p2 = getPos(eye, eyeLook, vray, ddepth, cameraProj); + tc = p2.z > waterLevel ? texCoord : texCoord + (n2.xy * n2.z) / 30.0 * waterRefract; + } // Light float fresnel = 1.0 - max(dot(n2, v), 0.0); - fresnel = pow(fresnel, 30.0) * 0.45; + fresnel = 0.02 + 0.98 * pow(fresnel, 5.0); vec3 r = reflect(-v, n2); #ifdef _Rad - vec3 reflectedEnv = textureLod(senvmapRadiance, envMapEquirect(r), 0).rgb; + vec3 reflectedEnv = textureLod(senvmapRadiance, envMapEquirect(r), 0).rgb; #else const vec3 reflectedEnv = vec3(0.5); #endif vec3 refracted = textureLod(tex, tc, 0.0).rgb; - + #ifdef _SSR float roughness = 0.1;//unpackFloat(g0.b).y; //if (roughness == 1.0) { fragColor.rgb = vec3(0.0); return; } @@ -147,8 +192,8 @@ void main() { float spec = 0.9;//fract(textureLod(gbuffer1, texCoord, 0.0).a); //if (spec == 0.0) { fragColor.rgb = vec3(0.0); return; } - vec3 viewNormal = n2; - vec3 viewPos = getPosView(viewRay, gdepth, cameraProj); + vec3 viewNormal = V3 * n2; + vec3 viewPos = isSky ? vec3(0.0) : getPosView(viewRay, gdepth, cameraProj); vec3 reflected = reflect(normalize(viewPos), viewNormal); hitCoord = viewPos; @@ -158,7 +203,6 @@ void main() { vec3 dir = reflected * (1.0 - rand(texCoord) * ssrJitter * roughness) * 2.0; #endif - // * max(ssrMinRayStep, -viewPos.z) vec4 coords = rayCast(dir); vec2 deltaCoords = abs(vec2(0.5, 0.5) - coords.xy); @@ -177,20 +221,32 @@ void main() { #else fragColor.rgb = mix(refracted, reflectedEnv, waterReflect * fresnel); #endif - fragColor.rgb *= waterColor; - fragColor.rgb += clamp(pow(max(dot(r, ld), 0.0), 200.0) * (200.0 + 8.0) / (PI * 8.0), 0.0, 2.0); - fragColor.rgb *= 1.0 - (clamp(-(p.z - waterLevel) * waterDensity, 0.0, 0.9)); - fragColor.a = clamp(abs(p.z - waterLevel) * 5.0, 0.0, 1.0); + // Water color tint - blend rather than multiply to preserve brightness + float colorMix = isSky ? 0.7 : 0.5; + fragColor.rgb = mix(fragColor.rgb, fragColor.rgb * waterColor, colorMix * horizonFactor); + // Blinn-Phong specular using half-vector, faded at horizon + vec3 h = normalize(v + ld); + float specAmount = pow(max(dot(n2, h), 0.0), 200.0) * (200.0 + 8.0) / (PI * 8.0); + fragColor.rgb += specAmount * (isSky ? 0.3 : 1.0) * horizonFactor; + // Depth fog - blend toward waterColor with depth, faded at horizon + float depthFog = clamp(-(p.z - waterLevel) * waterDensity, 0.0, 0.9); + fragColor.rgb = mix(fragColor.rgb, waterColor, depthFog * horizonFactor); + // Alpha fades smoothly at horizon instead of hard cut + fragColor.a = isSky ? horizonFactor : clamp(abs(p.z - waterLevel) * 5.0, 0.0, 1.0); - // Foam - float fd = abs(p.z - waterLevel); + // Foam - based on actual geometry depth below water surface + float fd = isSky ? 1.0 : abs(p.z - waterLevel); if (fd < 0.1) { // Based on foam by Owen Deery // http://fire-face.com/personal/water - vec3 foamMask0 = textureLod(sfoam, tcnor0 * 10, 0.0).rgb; - vec3 foamMask1 = textureLod(sfoam, tcnor1 * 11, 0.0).rgb; - vec3 foam = vec3(1.0) - foamMask0.rrr - foamMask1.bbb; - float fac = 1.0 - (fd * (1.0 / 0.1)); - fragColor.rgb = mix(fragColor.rgb, clamp(foam, 0.0, 1.0), clamp(fac, 0.0, 1.0)); + // Distance-based LOD blurs foam at range to reduce noise + float foamLod = clamp(tWater / 15.0, 0.0, 5.0); + vec2 foamUV0 = tcnor0 * 3.0 + vec2(speed / 30.0, speed / 50.0); + vec2 foamUV1 = tcnor1 * 4.0 + vec2(-speed / 35.0, speed / 45.0); + vec3 foamMask0 = textureLod(sfoam, foamUV0, foamLod).rgb; + vec3 foamMask1 = textureLod(sfoam, foamUV1, foamLod).rgb; + float foamStrength = clamp(1.0 - foamMask0.r * 0.5 - foamMask1.b * 0.5, 0.0, 1.0); + float fac = (1.0 - (fd * (1.0 / 0.1))) * horizonFactor; + fragColor.rgb = mix(fragColor.rgb, mix(fragColor.rgb, waterColor + 0.2, foamStrength), clamp(fac, 0.0, 1.0) * 0.5); } } diff --git a/leenkx/Sources/iron/data/SceneFormat.hx b/leenkx/Sources/iron/data/SceneFormat.hx index 9b5bdf30..eaf3aeb9 100644 --- a/leenkx/Sources/iron/data/SceneFormat.hx +++ b/leenkx/Sources/iron/data/SceneFormat.hx @@ -348,7 +348,13 @@ typedef TWorldData = { @:optional public var turbidity: Null; @:optional public var ground_albedo: Null; @:optional public var envmap: String; - @:optional public var nishita_density: Float32Array; // Rayleigh, Mie, ozone + @:optional public var sky_density: Float32Array; // Air, dust/aerosol, ozone density + @:optional public var sky_sun_elevation: Null; + @:optional public var sky_sun_rotation: Null; + @:optional public var sky_sun_size: Null; + @:optional public var sky_sun_intensity: Null; + @:optional public var sky_altitude: Null; + @:optional public var sky_sun_disc: Null; // 0 or 1 } #if js diff --git a/leenkx/Sources/leenkx/logicnode/GetHosekWilkiePropertiesNode.hx b/leenkx/Sources/leenkx/logicnode/GetHosekWilkiePropertiesNode.hx deleted file mode 100644 index 6401d888..00000000 --- a/leenkx/Sources/leenkx/logicnode/GetHosekWilkiePropertiesNode.hx +++ /dev/null @@ -1,27 +0,0 @@ -package leenkx.logicnode; - -import iron.math.Vec4; - -class GetHosekWilkiePropertiesNode extends LogicNode { - - public function new(tree: LogicTree) { - super(tree); - } - - override function get(from: Int): Dynamic { - - var world = iron.Scene.active.world.raw; - - return switch (from) { - case 0: - world.turbidity; - case 1: - world.ground_albedo; - case 2: - new Vec4(world.sun_direction[0], world.sun_direction[1], world.sun_direction[2]); - default: - null; - } - return null; - } -} \ No newline at end of file diff --git a/leenkx/Sources/leenkx/logicnode/GetNishitaPropertiesNode.hx b/leenkx/Sources/leenkx/logicnode/GetNishitaPropertiesNode.hx deleted file mode 100644 index d1b10f49..00000000 --- a/leenkx/Sources/leenkx/logicnode/GetNishitaPropertiesNode.hx +++ /dev/null @@ -1,29 +0,0 @@ -package leenkx.logicnode; - -import iron.math.Vec4; - -class GetNishitaPropertiesNode extends LogicNode { - - public function new(tree: LogicTree) { - super(tree); - } - - override function get(from: Int): Dynamic { - - var world = iron.Scene.active.world.raw; - - return switch (from) { - case 0: - world.nishita_density[0]; - case 1: - world.nishita_density[1]; - case 2: - world.nishita_density[2]; - case 3: - new Vec4(world.sun_direction[0], world.sun_direction[1], world.sun_direction[2]); - default: - null; - } - return null; - } -} \ No newline at end of file diff --git a/leenkx/Sources/leenkx/logicnode/GetWorldColorNode.hx b/leenkx/Sources/leenkx/logicnode/GetWorldColorNode.hx new file mode 100644 index 00000000..78aef2e2 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/GetWorldColorNode.hx @@ -0,0 +1,15 @@ +package leenkx.logicnode; + +import iron.math.Vec4; + +class GetWorldColorNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var col = iron.Scene.active.world.raw.background_color; + return new Vec4(((col >> 16) & 0xff) / 255, ((col >> 8) & 0xff) / 255, (col & 0xff) / 255, 1.0); + } +} diff --git a/leenkx/Sources/leenkx/logicnode/GetWorldSkyNode.hx b/leenkx/Sources/leenkx/logicnode/GetWorldSkyNode.hx new file mode 100644 index 00000000..84abf283 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/GetWorldSkyNode.hx @@ -0,0 +1,50 @@ +package leenkx.logicnode; + +import iron.math.Vec4; + +class GetWorldSkyNode extends LogicNode { + + public var property0:String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var world = iron.Scene.active.world.raw; + + if (property0 == 'hosek') { + return switch (from) { + case 0: world.turbidity != null ? world.turbidity : 1.0; + case 1: world.ground_albedo != null ? world.ground_albedo : 0.0; + case 2: world.sun_direction != null ? new Vec4(world.sun_direction[0], world.sun_direction[1], world.sun_direction[2]) : new Vec4(0, 0, 1); + default: null; + } + } + else if (property0 == 'single') { + return switch (from) { + case 0: world.sky_density != null ? world.sky_density[0] : 1.0; + case 1: world.sky_density != null ? world.sky_density[1] : 1.0; + case 2: world.sky_density != null ? world.sky_density[2] : 1.0; + case 3: world.sky_altitude != null ? world.sky_altitude : 0.0; + case 4: world.sun_direction != null ? new Vec4(world.sun_direction[0], world.sun_direction[1], world.sun_direction[2]) : new Vec4(0, 0, 1); + default: null; + } + } + else { // multi + return switch (from) { + case 0: world.sky_density != null ? world.sky_density[0] : 1.0; + case 1: world.sky_density != null ? world.sky_density[1] : 1.0; + case 2: world.sky_density != null ? world.sky_density[2] : 1.0; + case 3: world.sky_sun_elevation != null ? world.sky_sun_elevation : 0.0; + case 4: world.sky_sun_rotation != null ? world.sky_sun_rotation : 0.0; + case 5: world.sky_sun_size != null ? world.sky_sun_size : 0.545; + case 6: world.sky_sun_intensity != null ? world.sky_sun_intensity : 1.0; + case 7: world.sky_altitude != null ? world.sky_altitude : 0.0; + case 8: world.sky_sun_disc != null ? world.sky_sun_disc : 1; + case 9: world.sun_direction != null ? new Vec4(world.sun_direction[0], world.sun_direction[1], world.sun_direction[2]) : new Vec4(0, 0, 1); + default: null; + } + } + } +} diff --git a/leenkx/Sources/leenkx/logicnode/GetWorldTextureNode.hx b/leenkx/Sources/leenkx/logicnode/GetWorldTextureNode.hx new file mode 100644 index 00000000..0d32cf59 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/GetWorldTextureNode.hx @@ -0,0 +1,17 @@ +package leenkx.logicnode; + +class GetWorldTextureNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + var world = iron.Scene.active.world; + return switch (from) { + case 0: world.probe != null ? world.probe.raw.strength : 1.0; + case 1: world.raw.envmap != null ? world.raw.envmap : ''; + default: null; + } + } +} diff --git a/leenkx/Sources/leenkx/logicnode/SetHosekWilkiePropertiesNode.hx b/leenkx/Sources/leenkx/logicnode/SetHosekWilkiePropertiesNode.hx deleted file mode 100644 index 946791fc..00000000 --- a/leenkx/Sources/leenkx/logicnode/SetHosekWilkiePropertiesNode.hx +++ /dev/null @@ -1,41 +0,0 @@ -package leenkx.logicnode; - -import leenkx.renderpath.HosekWilkie; -import iron.math.Vec4; - -class SetHosekWilkiePropertiesNode extends LogicNode { - - public var property0:String; - - public function new(tree: LogicTree) { - super(tree); - } - - override function run(from: Int) { - - var world = iron.Scene.active.world; - - if(property0 == 'Turbidity/Ground Albedo'){ - world.raw.turbidity = inputs[1].get(); - world.raw.ground_albedo = inputs[2].get(); - } - - if(property0 == 'Turbidity') - world.raw.turbidity = inputs[1].get(); - - if(property0 == 'Ground Albedo') - world.raw.ground_albedo = inputs[1].get(); - - if(property0 == 'Sun Direction'){ - var vec:Vec4 = inputs[1].get(); - world.raw.sun_direction[0] = vec.x; - world.raw.sun_direction[1] = vec.y; - world.raw.sun_direction[2] = vec.z; - } - - HosekWilkie.recompute(world); - - runOutput(0); - - } -} diff --git a/leenkx/Sources/leenkx/logicnode/SetNishitaPropertiesNode.hx b/leenkx/Sources/leenkx/logicnode/SetNishitaPropertiesNode.hx deleted file mode 100644 index acaed1e7..00000000 --- a/leenkx/Sources/leenkx/logicnode/SetNishitaPropertiesNode.hx +++ /dev/null @@ -1,45 +0,0 @@ -package leenkx.logicnode; - -import leenkx.renderpath.Nishita; -import iron.math.Vec4; - -class SetNishitaPropertiesNode extends LogicNode { - - public var property0:String; - - public function new(tree: LogicTree) { - super(tree); - } - - override function run(from: Int) { - - var world = iron.Scene.active.world; - - if(property0 == 'Density'){ - world.raw.nishita_density[0] = inputs[1].get(); - world.raw.nishita_density[1] = inputs[2].get(); - world.raw.nishita_density[2] = inputs[3].get(); - } - - if(property0 == 'Air') - world.raw.nishita_density[0] = inputs[1].get(); - - if(property0 == 'Dust') - world.raw.nishita_density[1] = inputs[1].get(); - - if(property0 == 'Ozone') - world.raw.nishita_density[2] = inputs[1].get(); - - if(property0 == 'Sun Direction'){ - var vec:Vec4 = inputs[1].get(); - world.raw.sun_direction[0] = vec.x; - world.raw.sun_direction[1] = vec.y; - world.raw.sun_direction[2] = vec.z; - } - - Nishita.recompute(world); - - runOutput(0); - - } -} diff --git a/leenkx/Sources/leenkx/logicnode/SetWorldColorNode.hx b/leenkx/Sources/leenkx/logicnode/SetWorldColorNode.hx new file mode 100644 index 00000000..4bb94877 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/SetWorldColorNode.hx @@ -0,0 +1,23 @@ +package leenkx.logicnode; + +import iron.math.Vec4; + +class SetWorldColorNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var world = iron.Scene.active.world; + var raw = world.raw; + + var vec:Vec4 = inputs[1].get(); + var r = Std.int(Math.max(0, Math.min(1, vec.x)) * 255); + var g = Std.int(Math.max(0, Math.min(1, vec.y)) * 255); + var b = Std.int(Math.max(0, Math.min(1, vec.z)) * 255); + raw.background_color = (r << 16) | (g << 8) | b; + + runOutput(0); + } +} diff --git a/leenkx/Sources/leenkx/logicnode/SetWorldSkyNode.hx b/leenkx/Sources/leenkx/logicnode/SetWorldSkyNode.hx new file mode 100644 index 00000000..5af1d289 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/SetWorldSkyNode.hx @@ -0,0 +1,63 @@ +package leenkx.logicnode; + +import leenkx.renderpath.Sky; +import leenkx.renderpath.HosekWilkie; +import iron.math.Vec4; + +class SetWorldSkyNode extends LogicNode { + + public var property0:String; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var world = iron.Scene.active.world; + var raw = world.raw; + + if (property0 == 'hosek') { + raw.turbidity = inputs[1].get(); + raw.ground_albedo = inputs[2].get(); + var vec:Vec4 = inputs[3].get(); + if (raw.sun_direction == null) raw.sun_direction = new kha.arrays.Float32Array(3); + raw.sun_direction[0] = vec.x; + raw.sun_direction[1] = vec.y; + raw.sun_direction[2] = vec.z; + HosekWilkie.recompute(world); + } + else if (property0 == 'single') { + if (raw.sky_density == null) raw.sky_density = new kha.arrays.Float32Array(3); + raw.sky_density[0] = inputs[1].get(); + raw.sky_density[1] = inputs[2].get(); + raw.sky_density[2] = inputs[3].get(); + raw.sky_altitude = inputs[4].get(); + var vec:Vec4 = inputs[5].get(); + if (raw.sun_direction == null) raw.sun_direction = new kha.arrays.Float32Array(3); + raw.sun_direction[0] = vec.x; + raw.sun_direction[1] = vec.y; + raw.sun_direction[2] = vec.z; + Sky.recomputeSingleScatter(world); + } + else if (property0 == 'multi') { + if (raw.sky_density == null) raw.sky_density = new kha.arrays.Float32Array(3); + raw.sky_density[0] = inputs[1].get(); + raw.sky_density[1] = inputs[2].get(); + raw.sky_density[2] = inputs[3].get(); + raw.sky_sun_elevation = inputs[4].get(); + raw.sky_sun_rotation = inputs[5].get(); + raw.sky_sun_size = inputs[6].get(); + raw.sky_sun_intensity = inputs[7].get(); + raw.sky_altitude = inputs[8].get(); + raw.sky_sun_disc = inputs[9].get() ? 1 : 0; + var vec:Vec4 = inputs[10].get(); + if (raw.sun_direction == null) raw.sun_direction = new kha.arrays.Float32Array(3); + raw.sun_direction[0] = vec.x; + raw.sun_direction[1] = vec.y; + raw.sun_direction[2] = vec.z; + Sky.recomputeMultiScatter(world); + } + + runOutput(0); + } +} diff --git a/leenkx/Sources/leenkx/logicnode/SetWorldTextureNode.hx b/leenkx/Sources/leenkx/logicnode/SetWorldTextureNode.hx new file mode 100644 index 00000000..09786b61 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/SetWorldTextureNode.hx @@ -0,0 +1,25 @@ +package leenkx.logicnode; + +class SetWorldTextureNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + var world = iron.Scene.active.world; + var raw = world.raw; + + if (world.probe != null) { + world.probe.raw.strength = inputs[1].get(); + } + + var envmap:String = inputs[2].get(); + if (envmap != null && envmap != '' && envmap != raw.envmap) { + raw.envmap = envmap; + world.loadEnvmap(function(w) {}); + } + + runOutput(0); + } +} diff --git a/leenkx/Sources/leenkx/object/Uniforms.hx b/leenkx/Sources/leenkx/object/Uniforms.hx index 1c992a8d..95cc9ab4 100644 --- a/leenkx/Sources/leenkx/object/Uniforms.hx +++ b/leenkx/Sources/leenkx/object/Uniforms.hx @@ -18,7 +18,7 @@ class Uniforms { iron.object.Uniforms.externalTextureLinks = [textureLink]; iron.object.Uniforms.externalVec2Links = [vec2Link]; iron.object.Uniforms.externalVec3Links = [vec3Link]; - iron.object.Uniforms.externalVec4Links = []; + iron.object.Uniforms.externalVec4Links = [vec4Link]; iron.object.Uniforms.externalFloatLinks = [floatLink]; iron.object.Uniforms.externalFloatsLinks = [floatsLink]; iron.object.Uniforms.externalIntLinks = [intLink]; @@ -26,9 +26,13 @@ class Uniforms { public static function textureLink(object: Object, mat: MaterialData, link: String): Null { switch (link) { - case "_nishitaLUT": { - if (leenkx.renderpath.Nishita.data == null) leenkx.renderpath.Nishita.recompute(Scene.active.world); - return leenkx.renderpath.Nishita.data.lut; + case "_singleScatterLUT": { + if (leenkx.renderpath.Sky.singleScatterData == null) leenkx.renderpath.Sky.recomputeSingleScatter(Scene.active.world); + return leenkx.renderpath.Sky.singleScatterData.lut; + } + case "_multiScatterLUT": { + if (leenkx.renderpath.Sky.multiScatterData == null) leenkx.renderpath.Sky.recomputeMultiScatter(Scene.active.world); + return leenkx.renderpath.Sky.multiScatterData.lut; } #if lnx_ltc case "_ltcMat": { @@ -169,6 +173,17 @@ class Uniforms { } } #end + case "_multiScatterSunTop": { + if (leenkx.renderpath.Sky.multiScatterData == null) { + leenkx.renderpath.Sky.recomputeMultiScatter(Scene.active.world); + } + if (leenkx.renderpath.Sky.multiScatterData != null) { + v = iron.object.Uniforms.helpVec; + v.x = leenkx.renderpath.Sky.multiScatterData.sunTop.x; + v.y = leenkx.renderpath.Sky.multiScatterData.sunTop.y; + v.z = leenkx.renderpath.Sky.multiScatterData.sunTop.z; + } + } } return v; } @@ -176,13 +191,13 @@ class Uniforms { public static function vec2Link(object: Object, mat: MaterialData, link: String): Null { var v: Vec4 = null; switch (link) { - case "_nishitaDensity": { + case "_skyDensity": { var w = Scene.active.world; if (w != null) { v = iron.object.Uniforms.helpVec; // We only need Rayleigh and Mie density in the sky shader -> Vec2 - v.x = w.raw.nishita_density[0]; - v.y = w.raw.nishita_density[1]; + v.x = w.raw.sky_density[0]; + v.y = w.raw.sky_density[1]; } } } @@ -190,6 +205,35 @@ class Uniforms { return v; } + public static function vec4Link(object: Object, mat: MaterialData, link: String): Null { + var v: Vec4 = null; + switch (link) { + case "_multiScatterParams": { + var w = Scene.active.world; + if (w != null && w.raw.sky_sun_elevation != null) { + v = iron.object.Uniforms.helpVec; + v.x = w.raw.sky_sun_elevation; + v.y = w.raw.sky_sun_rotation != null ? w.raw.sky_sun_rotation : 0.0; + v.z = w.raw.sky_sun_size != null ? w.raw.sky_sun_size : 0.545; + v.w = w.raw.sky_sun_intensity != null ? w.raw.sky_sun_intensity : 1.0; + } + } + case "_multiScatterSunBottom": { + if (leenkx.renderpath.Sky.multiScatterData == null) { + leenkx.renderpath.Sky.recomputeMultiScatter(Scene.active.world); + } + if (leenkx.renderpath.Sky.multiScatterData != null) { + v = iron.object.Uniforms.helpVec; + v.x = leenkx.renderpath.Sky.multiScatterData.sunBottom.x; + v.y = leenkx.renderpath.Sky.multiScatterData.sunBottom.y; + v.z = leenkx.renderpath.Sky.multiScatterData.sunBottom.z; + v.w = leenkx.renderpath.Sky.multiScatterData.earthIntersectionAngle; + } + } + } + return v; + } + public static function floatLink(object: Object, mat: MaterialData, link: String): Null { switch (link) { #if rp_dynres diff --git a/leenkx/Sources/leenkx/renderpath/Nishita.hx b/leenkx/Sources/leenkx/renderpath/Nishita.hx index ab0420d0..8ab18659 100644 --- a/leenkx/Sources/leenkx/renderpath/Nishita.hx +++ b/leenkx/Sources/leenkx/renderpath/Nishita.hx @@ -8,50 +8,99 @@ import kha.graphics4.Usage; import iron.data.WorldData; import iron.math.Vec2; import iron.math.Vec3; +import iron.math.Vec4; import leenkx.math.Helper; /** - Utility class to control the Nishita sky model. + Utility class to control the sky models (single and multiple scattering). **/ -class Nishita { +class Sky { - public static var data: NishitaData = null; + public static var singleScatterData: SingleScatteringData = null; + public static var multiScatterData: MultipleScatteringData = null; /** - Recomputes the nishita lookup table after the density settings changed. + Recomputes the single scattering 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(); + public static function recomputeSingleScatter(world: WorldData) { + if (world == null || world.raw.sky_density == null) return; + if (singleScatterData == null) singleScatterData = new SingleScatteringData(); - var density = world.raw.nishita_density; - data.computeLUT(new Vec3(density[0], density[1], density[2])); + var density = world.raw.sky_density; + singleScatterData.computeLUT(new Vec3(density[0], density[1], density[2])); } - /** Sets the sky's density parameters and calls `recompute()` afterwards. **/ + /** Sets the sky's density parameters and calls `recomputeSingleScatter()` 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; + if (world.raw.sky_density == null) world.raw.sky_density = new Float32Array(3); + var density = world.raw.sky_density; density[0] = Helper.clamp(densityAir, 0, 10); density[1] = Helper.clamp(densityDust, 0, 10); density[2] = Helper.clamp(densityOzone, 0, 10); - recompute(world); + recomputeSingleScatter(world); + } + + public static function recomputeMultiScatter(world: WorldData) { + if (world == null) return; + var raw = world.raw; + if (raw.sky_density == null) return; + + if (multiScatterData == null) multiScatterData = new MultipleScatteringData(); + + var density = raw.sky_density; + var sunElevation = raw.sky_sun_elevation != null ? raw.sky_sun_elevation : 0.0; + var sunRotation = raw.sky_sun_rotation != null ? raw.sky_sun_rotation : 0.0; + var sunSize = raw.sky_sun_size != null ? raw.sky_sun_size : 0.545; + var sunIntensity = raw.sky_sun_intensity != null ? raw.sky_sun_intensity : 1.0; + var altitude = raw.sky_altitude != null ? raw.sky_altitude : 0.0; + var sunDisc = raw.sky_sun_disc != null ? raw.sky_sun_disc : 1; + + multiScatterData.compute( + sunElevation, + sunRotation, + sunSize, + sunIntensity, + altitude, + sunDisc != 0, + new Vec3(density[0], density[1], density[2]) + ); + } + + public static function setMultiScatterParams(world: WorldData, sunElevation: FastFloat, sunRotation: FastFloat, + sunSize: FastFloat, sunIntensity: FastFloat, altitude: FastFloat, sunDisc: Bool, + densityAir: FastFloat, densityDust: FastFloat, densityOzone: FastFloat) { + if (world == null) return; + + if (world.raw.sky_density == null) world.raw.sky_density = new Float32Array(3); + var density = world.raw.sky_density; + density[0] = Helper.clamp(densityAir, 0, 10); + density[1] = Helper.clamp(densityDust, 0, 10); + density[2] = Helper.clamp(densityOzone, 0, 10); + + world.raw.sky_sun_elevation = sunElevation; + world.raw.sky_sun_rotation = sunRotation; + world.raw.sky_sun_size = sunSize; + world.raw.sky_sun_intensity = sunIntensity; + world.raw.sky_altitude = altitude; + world.raw.sky_sun_disc = sunDisc ? 1 : 0; + + recomputeMultiScatter(world); } } /** This class holds the precalculated result of the inner scattering integral - of the Nishita sky model. The outer integral is calculated in + of the single scattering sky model. The outer integral is calculated in [`leenkx/Shaders/std/sky.glsl`](https://github.com/leenkx3d/leenkx/blob/master/Shaders/std/sky.glsl). - @see `leenkx.renderpath.Nishita` + @see `leenkx.renderpath.Sky` **/ -class NishitaData { +class SingleScatteringData { public var lut: kha.Image; @@ -229,3 +278,397 @@ class NishitaData { return jAttenuation; } } + +/** + This class precomputes the full sky radiance LUT + Unlike the single scattering model matching Blenders implementation. +**/ + +class MultipleScatteringData { + + public var lut: kha.Image; + + public var sunBottom: Vec3; + public var sunTop: Vec3; + public var earthIntersectionAngle: FastFloat; + + public static var lutWidth = 256; + public static var lutHeight = 128; + + static var transmittanceResX = 256; + static var transmittanceResY = 64; + var transmittanceLUT: haxe.io.Float32Array; + + static var transmittanceSteps = 64; + static var inScatteringSteps = 64; + + // Atmosphere constants sky_multiple_scattering.cpp + static var EARTH_RADIUS = 6371.0; + static var ATMOSPHERE_THICKNESS = 100.0; + static var ATMOSPHERE_RADIUS = 6471.0; // EARTH_RADIUS + ATMOSPHERE_THICKNESS + + static var PHASE_ISOTROPIC = 1.0 / (4.0 * Math.PI); + static var RAYLEIGH_PHASE_SCALE = (3.0 / 16.0) * (1.0 / Math.PI); + static var G = 0.8; + static var SQR_G = 0.64; + + static var GROUND_ALBEDO: Array = [0.3, 0.3, 0.3, 0.3]; + + // Spectral data sampled at 630, 560, 490, 430 nm + static var SUN_SPECTRAL_IRRADIANCE: Array = [1.679, 1.828, 1.986, 1.307]; + static var MOLECULAR_SCATTERING_COEFFICIENT_BASE: Array = [6.605e-3, 1.067e-2, 1.842e-2, 3.156e-2]; + static var OZONE_ABSORPTION_CROSS_SECTION: Array = [3.472e-25, 3.914e-25, 1.349e-25, 11.03e-27]; + static var OZONE_MEAN_DOBSON = 334.5; + static var AEROSOL_ABSORPTION_CROSS_SECTION: Array = [2.8722e-24, 4.6168e-24, 7.9706e-24, 1.3578e-23]; + static var AEROSOL_SCATTERING_CROSS_SECTION: Array = [1.5908e-22, 1.7711e-22, 2.0942e-22, 2.4033e-22]; + static var AEROSOL_BASE_DENSITY = 1.3681e20; + static var AEROSOL_BACKGROUND_DENSITY = 2e6; + static var AEROSOL_HEIGHT_SCALE = 0.73; + + static var SPECTRAL_XYZ: Array> = [ + [53.386917738564668023, 22.981337506691024754, 0.0], + [43.904844466369358263, 71.347795700053393866, 0.102506867965741307], + [1.6137278251608962005, 18.422960591455485011, 31.742921188390805758], + [20.762668673810577145, 2.3614213523314368527, 110.48009643252140334] + ]; + + var airDensity: Float = 1.0; + var aerosolDensity: Float = 1.0; + var ozoneDensity: Float = 1.0; + + public function new() { + sunBottom = new Vec3(); + sunTop = new Vec3(); + earthIntersectionAngle = 0.0; + transmittanceLUT = new haxe.io.Float32Array(transmittanceResY * transmittanceResX * 4); + } + + static inline function exp4(a: Array): Array { + return [Math.exp(a[0]), Math.exp(a[1]), Math.exp(a[2]), Math.exp(a[3])]; + } + + static inline function scale4(a: Array, s: Float): Array { + return [a[0] * s, a[1] * s, a[2] * s, a[3] * s]; + } + + static inline function add4(a: Array, b: Array): Array { + return [a[0] + b[0], a[1] + b[1], a[2] + b[2], a[3] + b[3]]; + } + + static inline function mult4(a: Array, b: Array): Array { + return [a[0] * b[0], a[1] * b[1], a[2] * b[2], a[3] * b[3]]; + } + + static inline function div4(a: Array, b: Array): Array { + return [a[0] / b[0], a[1] / b[1], a[2] / b[2], a[3] / b[3]]; + } + + static inline function max4(a: Array, b: Float): Array { + return [Math.max(a[0], b), Math.max(a[1], b), Math.max(a[2], b), Math.max(a[3], b)]; + } + + static inline function mix4(a: Array, b: Array, t: Float): Array { + return [a[0] + t * (b[0] - a[0]), a[1] + t * (b[1] - a[1]), a[2] + t * (b[2] - a[2]), a[3] + t * (b[3] - a[3])]; + } + + // Atmospheric functions + function getMolecularScatteringCoefficient(h: Float): Array { + // MOLECULAR_SCATTERING_COEFFICIENT_BASE * exp(-0.07771971 * h^1.16364243) + var f = Math.exp(-0.07771971 * Math.pow(h, 1.16364243)); + return scale4(MOLECULAR_SCATTERING_COEFFICIENT_BASE, f); + } + + function getMolecularAbsorptionCoefficient(h: Float): Array { + var logH = Math.log(Math.max(h, 1e-4)); + var density = 3.78547397e20 * Math.exp(-(logH - 3.22261) * (logH - 3.22261) * 5.55555555 - logH); + var result: Array = []; + for (i in 0...4) { + result[i] = OZONE_ABSORPTION_CROSS_SECTION[i] * OZONE_MEAN_DOBSON * density; + } + return result; + } + + function getAerosolDensity(h: Float): Float { + var division = AEROSOL_BACKGROUND_DENSITY / AEROSOL_BASE_DENSITY; + return AEROSOL_BASE_DENSITY * (Math.exp(-h / AEROSOL_HEIGHT_SCALE) + division); + } + + function getAtmosphereCollisionCoefficients(h: Float): { + aerosolAbs: Array, aerosolSca: Array, molAbs: Array, molSca: Array + } { + var localAerosolDensity = getAerosolDensity(h) * aerosolDensity; + var aerosolAbs = scale4(AEROSOL_ABSORPTION_CROSS_SECTION, localAerosolDensity); + var aerosolSca = scale4(AEROSOL_SCATTERING_CROSS_SECTION, localAerosolDensity); + var molAbs = scale4(getMolecularAbsorptionCoefficient(h), ozoneDensity); + var molSca = scale4(getMolecularScatteringCoefficient(h), airDensity); + return { aerosolAbs: aerosolAbs, aerosolSca: aerosolSca, molAbs: molAbs, molSca: molSca }; + } + + function molecularPhaseFunction(cosTheta: Float): Float { + return RAYLEIGH_PHASE_SCALE * (1.0 + cosTheta * cosTheta); + } + + function aerosolPhaseFunction(cosTheta: Float): Float { + var den = 1.0 + SQR_G + 2.0 * G * cosTheta; + return PHASE_ISOTROPIC * (1.0 - SQR_G) / (den * Math.sqrt(den)); + } + + + // nearest positive intersection distance, or -1 if no intersection + function raySphereIntersection(pos: Vec3, dir: Vec3, radius: Float): Float { + var b = pos.dot(dir); + var c = pos.dot(pos) - radius * radius; + if (c > 0.0 && b > 0.0) return -1.0; + var d = b * b - c; + if (d < 0.0) return -1.0; + if (d >= b * b) return -b + Math.sqrt(d); + return -b - Math.sqrt(d); + } + + function sunDirection(cosTheta: Float): Vec3 { + // Place sun at azimuth=0 in the atan(dir.x, dir.y) convention + // Blender uses (-sqrt(1-cos²θ), 0, cosθ) but our azimuth convention + // requires (0, sqrt(1-cos²θ), cosθ) for the sun to be at azimuth=0 + return new Vec3(0.0, Math.sqrt(1.0 - cosTheta * cosTheta), cosTheta); + } + + + function spectralToXYZ(L: Array): Vec3 { + var x = 0.0, y = 0.0, z = 0.0; + for (i in 0...4) { + x += SPECTRAL_XYZ[i][0] * L[i]; + y += SPECTRAL_XYZ[i][1] * L[i]; + z += SPECTRAL_XYZ[i][2] * L[i]; + } + return new Vec3(x, y, z); + } + + + function getTransmittance(cosTheta: Float, normalizedAltitude: Float): Array { + var sunDir = sunDirection(cosTheta); + var distToCenter = EARTH_RADIUS + (ATMOSPHERE_RADIUS - EARTH_RADIUS) * normalizedAltitude; + var rayOrigin = new Vec3(0.0, 0.0, distToCenter); + var tD = raySphereIntersection(rayOrigin, sunDir, ATMOSPHERE_RADIUS); + var tStep = tD / transmittanceSteps; + + var result: Array = [0.0, 0.0, 0.0, 0.0]; + for (step in 0...transmittanceSteps) { + var t = (step + 0.5) * tStep; + var xT = rayOrigin.clone().add(sunDir.clone().mult(t)); + var altitude = Math.max(xT.length() - EARTH_RADIUS, 0.0); + var coeffs = getAtmosphereCollisionCoefficients(altitude); + var extinction = add4(add4(coeffs.aerosolAbs, coeffs.aerosolSca), add4(coeffs.molAbs, coeffs.molSca)); + result = add4(result, scale4(extinction, tStep)); + } + return exp4(scale4(result, -1.0)); + } + + function precomputeTransmittanceLUT() { + for (y in 0...transmittanceResY) { + for (x in 0...transmittanceResX) { + var u = x / (transmittanceResX - 1); + var v = y / (transmittanceResY - 1); + var t = getTransmittance(u * 2.0 - 1.0, v); + var idx = (y * transmittanceResX + x) * 4; + transmittanceLUT[idx] = t[0]; + transmittanceLUT[idx + 1] = t[1]; + transmittanceLUT[idx + 2] = t[2]; + transmittanceLUT[idx + 3] = t[3]; + } + } + } + + function lookupTransmittance(cosTheta: Float, normalizedAltitude: Float): Array { + var u = Math.max(0.0, Math.min(1.0, cosTheta * 0.5 + 0.5)); + var v = Math.max(0.0, Math.min(1.0, normalizedAltitude)); + var fx = u * (transmittanceResX - 1); + var fy = v * (transmittanceResY - 1); + var x1 = Std.int(fx); + var y1 = Std.int(fy); + var x2 = Std.int(Math.min(x1 + 1, transmittanceResX - 1)); + var y2 = Std.int(Math.min(y1 + 1, transmittanceResY - 1)); + var dxF = fx - x1; + var dyF = fy - y1; + + function getPixel(px: Int, py: Int): Array { + var idx = (py * transmittanceResX + px) * 4; + return [transmittanceLUT[idx], transmittanceLUT[idx + 1], transmittanceLUT[idx + 2], transmittanceLUT[idx + 3]]; + } + + var bottom = mix4(getPixel(x1, y1), getPixel(x2, y1), dxF); + var top = mix4(getPixel(x1, y2), getPixel(x2, y2), dxF); + return mix4(bottom, top, dyF); + } + + function lookupTransmittanceAtGround(cosTheta: Float): Array { + var u = Math.max(0.0, Math.min(1.0, cosTheta * 0.5 + 0.5)); + var fx = u * (transmittanceResX - 1); + var x1 = Std.int(fx); + var x2 = Std.int(Math.min(x1 + 1, transmittanceResX - 1)); + var dxF = fx - x1; + var y = 0; + function getPixel(px: Int): Array { + var idx = (y * transmittanceResX + px) * 4; + return [transmittanceLUT[idx], transmittanceLUT[idx + 1], transmittanceLUT[idx + 2], transmittanceLUT[idx + 3]]; + } + return mix4(getPixel(x1), getPixel(x2), dxF); + } + + function lookupTransmittanceToSun(normalizedAltitude: Float): Array { + var v = Math.max(0.0, Math.min(1.0, normalizedAltitude)); + var fy = v * (transmittanceResY - 1); + var y1 = Std.int(fy); + var y2 = Std.int(Math.min(y1 + 1, transmittanceResY - 1)); + var dyF = fy - y1; + var x = transmittanceResX - 1; + function getPixel(py: Int): Array { + var idx = (py * transmittanceResX + x) * 4; + return [transmittanceLUT[idx], transmittanceLUT[idx + 1], transmittanceLUT[idx + 2], transmittanceLUT[idx + 3]]; + } + return mix4(getPixel(y1), getPixel(y2), dyF); + } + + // approximation + + function lookupMultiscattering(cosTheta: Float, normalizedHeight: Float, d: Float): Array { + var omega = 2.0 * Math.PI * (1.0 - Math.sqrt(Math.max(0.0, 1.0 - (EARTH_RADIUS / d) * (EARTH_RADIUS / d)))); + var tToGround = lookupTransmittanceAtGround(cosTheta); + var tGroundToSample = div4(lookupTransmittanceToSun(0.0), lookupTransmittanceToSun(normalizedHeight)); + // 2nd order scattering from the ground + var lGround = scale4(mult4(mult4(mult4(scale4(GROUND_ALBEDO, 1.0 / Math.PI), tToGround), tGroundToSample), [cosTheta, cosTheta, cosTheta, cosTheta]), PHASE_ISOTROPIC * omega); + // Fit of Earth's multiple scattering from other points in the atmosphere + var msFactor = 1.0 / (1.0 + 5.0 * Math.exp(-17.92 * cosTheta)); + var lMs = scale4([0.217, 0.347, 0.594, 1.0], 0.02 * msFactor); + return add4(lMs, lGround); + } + + function getInscattering(sunDir: Vec3, rayOrigin: Vec3, rayDir: Vec3, tD: Float): Array { + var cosTheta = rayDir.clone().mult(-1.0).dot(sunDir); + var molPhase = molecularPhaseFunction(cosTheta); + var aerPhase = aerosolPhaseFunction(cosTheta); + var dt = tD / inScatteringSteps; + var lInscattering: Array = [0.0, 0.0, 0.0, 0.0]; + var transmittance: Array = [1.0, 1.0, 1.0, 1.0]; + + for (i in 0...inScatteringSteps) { + var t = (i + 0.5) * dt; + var xT = rayOrigin.clone().add(rayDir.clone().mult(t)); + var distToCenter = xT.length(); + var zenithDir = xT.clone().mult(1.0 / distToCenter); + var altitude = Math.max(distToCenter - EARTH_RADIUS, 0.0); + var normalizedAltitude = altitude / ATMOSPHERE_THICKNESS; + var sampleCosTheta = zenithDir.dot(sunDir); + + var coeffs = getAtmosphereCollisionCoefficients(altitude); + var extinction = add4(add4(coeffs.aerosolAbs, coeffs.aerosolSca), add4(coeffs.molAbs, coeffs.molSca)); + var tToSun = lookupTransmittance(sampleCosTheta, normalizedAltitude); + var ms = lookupMultiscattering(sampleCosTheta, normalizedAltitude, distToCenter); + + // S = SUN_SPECTRAL_IRRADIANCE * (molSca * (molPhase * tToSun + ms) + aerSca * (aerPhase * tToSun + ms)) + var s: Array = [0.0, 0.0, 0.0, 0.0]; + for (w in 0...4) { + var molTerm = coeffs.molSca[w] * (molPhase * tToSun[w] + ms[w]); + var aerTerm = coeffs.aerosolSca[w] * (aerPhase * tToSun[w] + ms[w]); + s[w] = SUN_SPECTRAL_IRRADIANCE[w] * (molTerm + aerTerm); + } + + var stepTransmittance = exp4(scale4(extinction, -dt)); + var cutExt = max4(extinction, 1e-7); + var sInt: Array = []; + for (w in 0...4) { + sInt[w] = (s[w] - s[w] * stepTransmittance[w]) / cutExt[w]; + } + lInscattering = add4(lInscattering, mult4(transmittance, sInt)); + transmittance = mult4(transmittance, stepTransmittance); + } + return lInscattering; + } + + + function computeEarthAngle(altitude: Float): Float { + return Math.PI / 2.0 - Math.asin(EARTH_RADIUS / (EARTH_RADIUS + altitude / 1000.0)); + } + + function precomputeSun(sunElevation: Float, angularDiameter: Float, altitude: Float): { bottom: Vec3, top: Vec3 } { + var halfAngular = angularDiameter / 2.0; + var solidAngle = 2.0 * Math.PI * (1.0 - Math.cos(halfAngular)); + var normalizedAltitude = altitude / ATMOSPHERE_THICKNESS; + + function getSunXYZ(elevation: Float): Vec3 { + var sunZenithCosAngle = Math.cos(Math.PI / 2.0 - elevation); + var tToSun = getTransmittance(sunZenithCosAngle, normalizedAltitude); + var spectrum: Array = []; + for (w in 0...4) { + spectrum[w] = SUN_SPECTRAL_IRRADIANCE[w] * tToSun[w] / solidAngle; + } + return spectralToXYZ(spectrum); + } + + var bottom = getSunXYZ(sunElevation - halfAngular); + var top = getSunXYZ(sunElevation + halfAngular); + return { bottom: bottom, top: top }; + } + + public function compute(sunElevation: Float, sunRotation: Float, sunSize: Float, + sunIntensity: Float, altitude: Float, sunDisc: Bool, density: Vec3) { + + airDensity = density.x; + aerosolDensity = density.y; + ozoneDensity = density.z; + + precomputeTransmittanceLUT(); + + var altKm = Math.max(1.0, Math.min(99999.0, altitude)) / 1000.0; + var sunZenithCosAngle = Math.cos(Math.PI / 2.0 - sunElevation); + var sunDir = sunDirection(sunZenithCosAngle); + var rayOrigin = new Vec3(0.0, 0.0, EARTH_RADIUS + altKm); + + earthIntersectionAngle = computeEarthAngle(altitude); + + var imageData = new haxe.io.Float32Array(lutWidth * lutHeight * 4); + + for (y in 0...lutHeight) { + var uvY = (y + 0.5) / lutHeight; + var l = uvY * 2.0 - 1.0; + var elevation = (l < 0 ? -1.0 : 1.0) * l * l * Math.PI / 2.0; + var cosEl = Math.cos(elevation); + var sinEl = Math.sin(elevation); + + for (x in 0...lutWidth) { + var uvX = (x + 0.5) / lutWidth; + var azimuth = 2.0 * Math.PI * uvX; + + // atan(dir.x, dir.y) convention + var rayDir = new Vec3( + Math.sin(azimuth) * cosEl, + Math.cos(azimuth) * cosEl, + sinEl + ).normalize(); + + var atmosDist = raySphereIntersection(rayOrigin, rayDir, ATMOSPHERE_RADIUS); + var groundDist = raySphereIntersection(rayOrigin, rayDir, EARTH_RADIUS); + var tD = (groundDist < 0.0) ? atmosDist : groundDist; + + var radiance = getInscattering(sunDir, rayOrigin, rayDir, tD); + var xyz = spectralToXYZ(radiance); + + var pixelIndex = (x + y * lutWidth) * 4; + imageData[pixelIndex + 0] = xyz.x; + imageData[pixelIndex + 1] = xyz.y; + imageData[pixelIndex + 2] = xyz.z; + imageData[pixelIndex + 3] = 1.0; + } + } + + lut = kha.Image.fromBytes(imageData.view.buffer, lutWidth, lutHeight, TextureFormat.RGBA128, Usage.StaticUsage); + + if (sunDisc) { + var sunData = precomputeSun(sunElevation, sunSize, altKm); + sunBottom = sunData.bottom; + sunTop = sunData.top; + } else { + sunBottom.set(0, 0, 0); + sunTop.set(0, 0, 0); + } + } +} diff --git a/leenkx/Sources/leenkx/renderpath/RenderPathDeferred.hx b/leenkx/Sources/leenkx/renderpath/RenderPathDeferred.hx index 5efcec2c..61ba7492 100644 --- a/leenkx/Sources/leenkx/renderpath/RenderPathDeferred.hx +++ b/leenkx/Sources/leenkx/renderpath/RenderPathDeferred.hx @@ -841,18 +841,6 @@ class RenderPathDeferred { } #end - #if rp_water - { - path.setTarget("buf"); - path.bindTarget("tex", "tex"); - path.drawShader("shader_datas/copy_pass/copy_pass"); - path.setTarget("tex"); - path.bindTarget("_main", "gbufferD"); - path.bindTarget("buf", "tex"); - path.drawShader("shader_datas/water_pass/water_pass"); - } - #end - #if (!kha_opengl) path.setDepthFrom("tex", "gbuffer0"); // Re-bind depth #end @@ -879,6 +867,18 @@ class RenderPathDeferred { } #end + #if rp_water + { + path.setTarget("buf"); + path.bindTarget("tex", "tex"); + path.drawShader("shader_datas/copy_pass/copy_pass"); + path.setTarget("tex"); + path.bindTarget("_main", "gbufferD"); + path.bindTarget("buf", "tex"); + path.drawShader("shader_datas/water_pass/water_pass"); + } + #end + #if rp_ssr { if (leenkx.data.Config.raw.rp_ssr != false) { diff --git a/leenkx/blender/lnx/exporter.py b/leenkx/blender/lnx/exporter.py index 6187c6cc..a77f1677 100644 --- a/leenkx/blender/lnx/exporter.py +++ b/leenkx/blender/lnx/exporter.py @@ -3817,7 +3817,13 @@ class LeenkxExporter: out_world['sun_direction'] = list(world.lnx_envtex_sun_direction) out_world['turbidity'] = world.lnx_envtex_turbidity out_world['ground_albedo'] = world.lnx_envtex_ground_albedo - out_world['nishita_density'] = list(world.lnx_nishita_density) + out_world['sky_density'] = list(world.lnx_sky_density) + out_world['sky_sun_elevation'] = world.lnx_sky_sun_elevation + out_world['sky_sun_rotation'] = world.lnx_sky_sun_rotation + out_world['sky_sun_size'] = world.lnx_sky_sun_size + out_world['sky_sun_intensity'] = world.lnx_sky_sun_intensity + out_world['sky_altitude'] = world.lnx_sky_altitude + out_world['sky_sun_disc'] = world.lnx_sky_sun_disc disable_hdr = world.lnx_envtex_name.endswith('.jpg') diff --git a/leenkx/blender/lnx/logicnode/world/LN_get_hosekwilkie_properties.py b/leenkx/blender/lnx/logicnode/world/LN_get_hosekwilkie_properties.py deleted file mode 100644 index a36529c3..00000000 --- a/leenkx/blender/lnx/logicnode/world/LN_get_hosekwilkie_properties.py +++ /dev/null @@ -1,12 +0,0 @@ -from lnx.logicnode.lnx_nodes import * - -class GetHosekWilkiePropertiesNode(LnxLogicTreeNode): - """Gets the HosekWilkie properties.""" - bl_idname = 'LNGetHosekWilkiePropertiesNode' - bl_label = 'Get HosekWilkie Properties' - lnx_version = 1 - - def lnx_init(self, context): - self.add_output('LnxFloatSocket', 'Turbidity') - self.add_output('LnxFloatSocket', 'Ground Albedo') - self.add_output('LnxVectorSocket', 'Sun Direction') diff --git a/leenkx/blender/lnx/logicnode/world/LN_get_nishita_properties.py b/leenkx/blender/lnx/logicnode/world/LN_get_nishita_properties.py deleted file mode 100644 index 87dc680c..00000000 --- a/leenkx/blender/lnx/logicnode/world/LN_get_nishita_properties.py +++ /dev/null @@ -1,14 +0,0 @@ -from lnx.logicnode.lnx_nodes import * - -class GetNishitaPropertiesNode(LnxLogicTreeNode): - """Gets the Nishita properties.""" - bl_idname = 'LNGetNishitaPropertiesNode' - bl_label = 'Get Nishita Properties' - lnx_version = 1 - - def lnx_init(self, context): - self.add_output('LnxFloatSocket', 'Air') - self.add_output('LnxFloatSocket', 'Dust') - self.add_output('LnxFloatSocket', 'Ozone') - self.add_output('LnxVectorSocket', 'Sun Direction') - diff --git a/leenkx/blender/lnx/logicnode/world/LN_get_world_color.py b/leenkx/blender/lnx/logicnode/world/LN_get_world_color.py new file mode 100644 index 00000000..c986e5b2 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/world/LN_get_world_color.py @@ -0,0 +1,11 @@ +from lnx.logicnode.lnx_nodes import * + + +class GetWorldColorNode(LnxLogicTreeNode): + """Gets the background color of the active world.""" + bl_idname = 'LNGetWorldColorNode' + bl_label = 'Get World Color' + lnx_version = 1 + + def lnx_init(self, context): + self.add_output('LnxColorSocket', 'Color') diff --git a/leenkx/blender/lnx/logicnode/world/LN_get_world_sky.py b/leenkx/blender/lnx/logicnode/world/LN_get_world_sky.py new file mode 100644 index 00000000..51162d00 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/world/LN_get_world_sky.py @@ -0,0 +1,51 @@ +from lnx.logicnode.lnx_nodes import * + + +class GetWorldSkyNode(LnxLogicTreeNode): + """Gets the sky properties for the selected sky model.""" + bl_idname = 'LNGetWorldSkyNode' + bl_label = 'Get World Sky' + lnx_version = 1 + legacy = 'Nishita ' if bpy.app.version < (5, 0, 0) else '' + + def update_inputs(self, context): + while len(self.outputs) > 0: + self.outputs.remove(self.outputs[0]) + if self.property0 == 'hosek': + self.add_output('LnxFloatSocket', 'Turbidity') + self.add_output('LnxFloatSocket', 'Ground Albedo') + self.add_output('LnxVectorSocket', 'Sun Direction') + elif self.property0 == 'single': + self.add_output('LnxFloatSocket', 'Air') + self.add_output('LnxFloatSocket', 'Dust') + self.add_output('LnxFloatSocket', 'Ozone') + self.add_output('LnxFloatSocket', 'Altitude') + self.add_output('LnxVectorSocket', 'Sun Direction') + elif self.property0 == 'multi': + self.add_output('LnxFloatSocket', 'Air') + self.add_output('LnxFloatSocket', 'Dust') + self.add_output('LnxFloatSocket', 'Ozone') + self.add_output('LnxFloatSocket', 'Sun Elevation') + self.add_output('LnxFloatSocket', 'Sun Rotation') + self.add_output('LnxFloatSocket', 'Sun Size') + self.add_output('LnxFloatSocket', 'Sun Intensity') + self.add_output('LnxFloatSocket', 'Altitude') + self.add_output('LnxIntSocket', 'Sun Disc') + self.add_output('LnxVectorSocket', 'Sun Direction') + + property0: HaxeEnumProperty( + 'property0', + items=[('hosek', 'Hosek Wilkie', 'Hosek-Wilkie / Preetham sky model'), + ('single', f'{legacy}Single Scattering', 'Single scattering sky model'), + ('multi', f'{legacy}Multiple Scattering', 'Multiple scattering sky model')], + name='', default='single', update=update_inputs) + + def lnx_init(self, context): + self.add_output('LnxFloatSocket', 'Air') + self.add_output('LnxFloatSocket', 'Dust') + self.add_output('LnxFloatSocket', 'Ozone') + self.add_output('LnxFloatSocket', 'Altitude') + self.add_output('LnxVectorSocket', 'Sun Direction') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/leenkx/blender/lnx/logicnode/world/LN_get_world_texture.py b/leenkx/blender/lnx/logicnode/world/LN_get_world_texture.py new file mode 100644 index 00000000..76436964 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/world/LN_get_world_texture.py @@ -0,0 +1,12 @@ +from lnx.logicnode.lnx_nodes import * + + +class GetWorldTextureNode(LnxLogicTreeNode): + """Gets the texture properties of the active world (strength, envmap).""" + bl_idname = 'LNGetWorldTextureNode' + bl_label = 'Get World Texture' + lnx_version = 1 + + def lnx_init(self, context): + self.add_output('LnxFloatSocket', 'Strength') + self.add_output('LnxStringSocket', 'Envmap') diff --git a/leenkx/blender/lnx/logicnode/world/LN_set_hosekwilkie_properties.py b/leenkx/blender/lnx/logicnode/world/LN_set_hosekwilkie_properties.py deleted file mode 100644 index 0fac76dd..00000000 --- a/leenkx/blender/lnx/logicnode/world/LN_set_hosekwilkie_properties.py +++ /dev/null @@ -1,38 +0,0 @@ -from lnx.logicnode.lnx_nodes import * - -class SetHosekWilkiePropertiesNode(LnxLogicTreeNode): - """Sets the HosekWilkie properties.""" - bl_idname = 'LNSetHosekWilkiePropertiesNode' - bl_label = 'Set HosekWilkie Properties' - lnx_version = 1 - - def remove_extra_inputs(self, context): - while len(self.inputs) > 1: - self.inputs.remove(self.inputs[-1]) - if self.property0 == 'Turbidity/Ground Albedo': - self.add_input('LnxFloatSocket', 'Turbidity') - self.add_input('LnxFloatSocket', 'Ground Albedo') - if self.property0 == 'Turbidity': - self.add_input('LnxFloatSocket', 'Turbidity') - if self.property0 == 'Ground Albedo': - self.add_input('LnxFloatSocket', 'Ground Albedo') - if self.property0 == 'Sun Direction': - self.add_input('LnxVectorSocket', 'Sun Direction') - - property0: HaxeEnumProperty( - 'property0', - items = [('Turbidity/Ground Albedo', 'Turbidity/Ground Albedo', 'Turbidity, Ground Albedo'), - ('Turbidity', 'Turbidity', 'Turbidity'), - ('Ground Albedo', 'Ground Albedo', 'Ground Albedo'), - ('Sun Direction', 'Sun Direction', 'Sun Direction')], - name='', default='Turbidity/Ground Albedo', update=remove_extra_inputs) - - def lnx_init(self, context): - self.add_input('LnxNodeSocketAction', 'In') - self.add_input('LnxFloatSocket', 'Turbidity') - self.add_input('LnxFloatSocket', 'Ground_Albedo') - - self.add_output('LnxNodeSocketAction', 'Out') - - def draw_buttons(self, context, layout): - layout.prop(self, 'property0') \ No newline at end of file diff --git a/leenkx/blender/lnx/logicnode/world/LN_set_nishita_properties.py b/leenkx/blender/lnx/logicnode/world/LN_set_nishita_properties.py deleted file mode 100644 index db0cfcd8..00000000 --- a/leenkx/blender/lnx/logicnode/world/LN_set_nishita_properties.py +++ /dev/null @@ -1,43 +0,0 @@ -from lnx.logicnode.lnx_nodes import * - -class SetNishitaPropertiesNode(LnxLogicTreeNode): - """Sets the Nishita properties""" - bl_idname = 'LNSetNishitaPropertiesNode' - bl_label = 'Set Nishita Properties' - lnx_version = 1 - - def remove_extra_inputs(self, context): - while len(self.inputs) > 1: - self.inputs.remove(self.inputs[-1]) - if self.property0 == 'Density': - self.add_input('LnxFloatSocket', 'Air') - self.add_input('LnxFloatSocket', 'Dust') - self.add_input('LnxFloatSocket', 'Ozone') - if self.property0 == 'Air': - self.add_input('LnxFloatSocket', 'Air') - if self.property0 == 'Dust': - self.add_input('LnxFloatSocket', 'Dust') - if self.property0 == 'Ozone': - self.add_input('LnxFloatSocket', 'Ozone') - if self.property0 == 'Sun Direction': - self.add_input('LnxVectorSocket', 'Sun Direction') - - property0: HaxeEnumProperty( - 'property0', - items = [('Density', 'Density', 'Air, Dust, Ozone'), - ('Air', 'Air', 'Air'), - ('Dust', 'Dust', 'Dust'), - ('Ozone', 'Ozone', 'Ozone'), - ('Sun Direction', 'Sun Direction', 'Sun Direction')], - name='', default='Density', update=remove_extra_inputs) - - def lnx_init(self, context): - self.add_input('LnxNodeSocketAction', 'In') - self.add_input('LnxFloatSocket', 'Air') - self.add_input('LnxFloatSocket', 'Dust') - self.add_input('LnxFloatSocket', 'Ozone') - - self.add_output('LnxNodeSocketAction', 'Out') - - def draw_buttons(self, context, layout): - layout.prop(self, 'property0') \ No newline at end of file diff --git a/leenkx/blender/lnx/logicnode/world/LN_set_world_color.py b/leenkx/blender/lnx/logicnode/world/LN_set_world_color.py new file mode 100644 index 00000000..8d5ccc46 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/world/LN_set_world_color.py @@ -0,0 +1,14 @@ +from lnx.logicnode.lnx_nodes import * + + +class SetWorldColorNode(LnxLogicTreeNode): + """Sets the background color of the active world.""" + bl_idname = 'LNSetWorldColorNode' + bl_label = 'Set World Color' + lnx_version = 1 + + def lnx_init(self, context): + self.add_input('LnxNodeSocketAction', 'In') + self.add_input('LnxColorSocket', 'Color', default_value=[0.0, 0.0, 0.0, 1.0]) + + self.add_output('LnxNodeSocketAction', 'Out') diff --git a/leenkx/blender/lnx/logicnode/world/LN_set_world_sky.py b/leenkx/blender/lnx/logicnode/world/LN_set_world_sky.py new file mode 100644 index 00000000..84ab6a97 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/world/LN_set_world_sky.py @@ -0,0 +1,54 @@ +from lnx.logicnode.lnx_nodes import * + + +class SetWorldSkyNode(LnxLogicTreeNode): + """Sets the sky properties for the selected sky model.""" + bl_idname = 'LNSetWorldSkyNode' + bl_label = 'Set World Sky' + lnx_version = 1 + legacy = 'Nishita ' if bpy.app.version < (5, 0, 0) else '' + + def update_inputs(self, context): + while len(self.inputs) > 1: + self.inputs.remove(self.inputs[-1]) + if self.property0 == 'hosek': + self.add_input('LnxFloatSocket', 'Turbidity', default_value=1.0) + self.add_input('LnxFloatSocket', 'Ground Albedo', default_value=0.0) + self.add_input('LnxVectorSocket', 'Sun Direction', default_value=[0.0, 0.0, 1.0]) + elif self.property0 == 'single': + self.add_input('LnxFloatSocket', 'Air', default_value=1.0) + self.add_input('LnxFloatSocket', 'Dust', default_value=1.0) + self.add_input('LnxFloatSocket', 'Ozone', default_value=1.0) + self.add_input('LnxFloatSocket', 'Altitude', default_value=1.0) + self.add_input('LnxVectorSocket', 'Sun Direction', default_value=[0.0, 0.0, 1.0]) + elif self.property0 == 'multi': + self.add_input('LnxFloatSocket', 'Air', default_value=1.0) + self.add_input('LnxFloatSocket', 'Dust', default_value=1.0) + self.add_input('LnxFloatSocket', 'Ozone', default_value=1.0) + self.add_input('LnxFloatSocket', 'Sun Elevation', default_value=0.0) + self.add_input('LnxFloatSocket', 'Sun Rotation', default_value=0.0) + self.add_input('LnxFloatSocket', 'Sun Size', default_value=0.545) + self.add_input('LnxFloatSocket', 'Sun Intensity', default_value=1.0) + self.add_input('LnxFloatSocket', 'Altitude', default_value=1.0) + self.add_input('LnxBoolSocket', 'Sun Disc', default_value=True) + self.add_input('LnxVectorSocket', 'Sun Direction', default_value=[0.0, 0.0, 1.0]) + + property0: HaxeEnumProperty( + 'property0', + items=[('hosek', 'Hosek Wilkie', 'Hosek-Wilkie / Preetham sky model'), + ('single', f'{legacy}Single Scattering', 'Single scattering sky model'), + ('multi', f'{legacy}Multiple Scattering', 'Multiple scattering sky model')], + name='', default='single', update=update_inputs) + + def lnx_init(self, context): + self.add_input('LnxNodeSocketAction', 'In') + self.add_input('LnxFloatSocket', 'Air', default_value=1.0) + self.add_input('LnxFloatSocket', 'Dust', default_value=1.0) + self.add_input('LnxFloatSocket', 'Ozone', default_value=1.0) + self.add_input('LnxFloatSocket', 'Altitude', default_value=1.0) + self.add_input('LnxVectorSocket', 'Sun Direction', default_value=[0.0, 0.0, 1.0]) + + self.add_output('LnxNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') diff --git a/leenkx/blender/lnx/logicnode/world/LN_set_world_texture.py b/leenkx/blender/lnx/logicnode/world/LN_set_world_texture.py new file mode 100644 index 00000000..50325537 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/world/LN_set_world_texture.py @@ -0,0 +1,15 @@ +from lnx.logicnode.lnx_nodes import * + + +class SetWorldTextureNode(LnxLogicTreeNode): + """Sets the texture properties of the active world (strength, envmap).""" + bl_idname = 'LNSetWorldTextureNode' + bl_label = 'Set World Texture' + lnx_version = 1 + + def lnx_init(self, context): + self.add_input('LnxNodeSocketAction', 'In') + self.add_input('LnxFloatSocket', 'Strength', default_value=1.0) + self.add_input('LnxStringSocket', 'Envmap') + + self.add_output('LnxNodeSocketAction', 'Out') diff --git a/leenkx/blender/lnx/make_world.py b/leenkx/blender/lnx/make_world.py index 9bdf0ddf..6c9dec74 100644 --- a/leenkx/blender/lnx/make_world.py +++ b/leenkx/blender/lnx/make_world.py @@ -370,7 +370,7 @@ def frag_write_clouds(world: bpy.types.World, frag: Shader): func_cloud_radiance = 'float cloudRadiance(vec3 p, vec3 dir) {\n' if '_EnvSky' in world.world_defs: - # Nishita sky + # Single scattering sky if 'vec3 sunDir' in frag.uniforms: func_cloud_radiance += '\tvec3 sun_dir = sunDir;\n' # Hosek @@ -413,7 +413,7 @@ def frag_write_clouds(world: bpy.types.World, frag: Shader): if world.lnx_darken_clouds: func_trace_clouds += '\t// Darken clouds when the sun is low\n' if '_EnvSky' in world.world_defs: - # Nishita sky + # Single scattering sky if 'vec3 sunDir' in frag.uniforms: func_trace_clouds += '\tC *= smoothstep(-0.02, 0.25, sunDir.z);\n' # Hosek diff --git a/leenkx/blender/lnx/material/cycles_nodes/nodes_texture.py b/leenkx/blender/lnx/material/cycles_nodes/nodes_texture.py index daa8e20b..26984947 100644 --- a/leenkx/blender/lnx/material/cycles_nodes/nodes_texture.py +++ b/leenkx/blender/lnx/material/cycles_nodes/nodes_texture.py @@ -328,12 +328,13 @@ def parse_tex_sky(node: bpy.types.ShaderNodeTexSky, out_socket: bpy.types.NodeSo state.world.world_defs += '_EnvSky' if node.sky_type == 'PREETHAM' or node.sky_type == 'HOSEK_WILKIE': - if node.sky_type == 'PREETHAM': - log.info('Info: Preetham sky model is not supported, using Hosek Wilkie sky model instead') return parse_sky_hosekwilkie(node, state) - elif node.sky_type == 'NISHITA': - return parse_sky_nishita(node, state) + elif node.sky_type == 'NISHITA' or node.sky_type == 'SINGLE_SCATTERING': + return parse_sky_single_scattering(node, state) + + elif node.sky_type == 'MULTIPLE_SCATTERING': + return parse_sky_multiple_scattering(node, state) else: log.error(f'Unsupported sky model: {node.sky_type}!') @@ -397,18 +398,20 @@ def parse_sky_hosekwilkie(node: bpy.types.ShaderNodeTexSky, state: ParserState) return 'Z * hosekWilkie(cos_theta, gamma_val, cos_gamma) * envmapStrength;' -def parse_sky_nishita(node: bpy.types.ShaderNodeTexSky, state: ParserState) -> vec3str: +def parse_sky_single_scattering(node: bpy.types.ShaderNodeTexSky, state: ParserState) -> vec3str: curshader = state.curshader curshader.add_include('std/sky.glsl') curshader.add_uniform('vec3 sunDir', link='_sunDirection') - curshader.add_uniform('sampler2D nishitaLUT', link='_nishitaLUT', included=True, + curshader.add_uniform('sampler2D singleScatterLUT', link='_singleScatterLUT', included=True, tex_addr_u='clamp', tex_addr_v='clamp') - curshader.add_uniform('vec2 nishitaDensity', link='_nishitaDensity', included=True) + curshader.add_uniform('vec2 skyDensity', link='_skyDensity', included=True) planet_radius = 6360e3 # Earth radius used in Blender ray_origin_z = planet_radius + node.altitude - state.world.lnx_nishita_density = [node.air_density, node.dust_density, node.ozone_density] + dust_density = node.aerosol_density if bpy.app.version >= (5, 0, 0) else node.dust_density + state.world.lnx_sky_density = [node.air_density, dust_density, node.ozone_density] + state.world.lnx_envtex_sun_direction = [node.sun_direction[0], node.sun_direction[1], node.sun_direction[2]] sun = '' if node.sun_disc: @@ -428,7 +431,29 @@ def parse_sky_nishita(node: bpy.types.ShaderNodeTexSky, state: ParserState) -> v size = math.cos(theta) sun = f'* sun_disk(pos, sunDir, {size}, {node.sun_intensity})' - return f'nishita_atmosphere(pos, vec3(0, 0, {ray_origin_z}), sunDir, {planet_radius}){sun}' + return f'single_scatter_atmosphere(pos, vec3(0, 0, {ray_origin_z}), sunDir, {planet_radius}){sun}' + + +def parse_sky_multiple_scattering(node: bpy.types.ShaderNodeTexSky, state: ParserState) -> vec3str: + curshader = state.curshader + curshader.add_include('std/sky.glsl') + curshader.add_uniform('vec3 sunDir', link='_sunDirection') + curshader.add_uniform('sampler2D multiScatterLUT', link='_multiScatterLUT', included=True, tex_addr_u='repeat', tex_addr_v='clamp') + curshader.add_uniform('vec4 multiScatterParams', link='_multiScatterParams', included=True) + curshader.add_uniform('vec4 multiScatterSunBottom', link='_multiScatterSunBottom', included=True) + curshader.add_uniform('vec3 multiScatterSunTop', link='_multiScatterSunTop', included=True) + + dust_density = node.aerosol_density if bpy.app.version >= (5, 0, 0) else node.dust_density + state.world.lnx_sky_density = [node.air_density, dust_density, node.ozone_density] + state.world.lnx_sky_sun_elevation = node.sun_elevation + state.world.lnx_sky_sun_rotation = node.sun_rotation + state.world.lnx_sky_sun_size = node.sun_size + state.world.lnx_sky_sun_intensity = node.sun_intensity if node.sun_disc else 0.0 + state.world.lnx_sky_altitude = node.altitude + state.world.lnx_sky_sun_disc = 1 if node.sun_disc else 0 + state.world.lnx_envtex_sun_direction = [node.sun_direction[0], node.sun_direction[1], node.sun_direction[2]] + + return f'multi_scatter_atmosphere(pos)' def parse_tex_environment(node: bpy.types.ShaderNodeTexEnvironment, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: diff --git a/leenkx/blender/lnx/props.py b/leenkx/blender/lnx/props.py index b4db41cd..6c792540 100644 --- a/leenkx/blender/lnx/props.py +++ b/leenkx/blender/lnx/props.py @@ -464,7 +464,13 @@ def init_properties(): bpy.types.World.lnx_envtex_sun_direction = FloatVectorProperty(name="Sun Direction", size=3, default=[0,0,0]) bpy.types.World.lnx_envtex_turbidity = FloatProperty(name="Turbidity", default=1.0) bpy.types.World.lnx_envtex_ground_albedo = FloatProperty(name="Ground Albedo", default=0.0) - bpy.types.World.lnx_nishita_density = FloatVectorProperty(name="Nishita Density", size=3, default=[1, 1, 1]) + bpy.types.World.lnx_sky_density = FloatVectorProperty(name="Sky Density", size=3, default=[1, 1, 1]) + bpy.types.World.lnx_sky_sun_elevation = FloatProperty(name="Sky Sun Elevation", default=0.0) + bpy.types.World.lnx_sky_sun_rotation = FloatProperty(name="Sky Sun Rotation", default=0.0) + bpy.types.World.lnx_sky_sun_size = FloatProperty(name="Sky Sun Size", default=0.545) + bpy.types.World.lnx_sky_sun_intensity = FloatProperty(name="Sky Sun Intensity", default=1.0) + bpy.types.World.lnx_sky_altitude = FloatProperty(name="Sky Altitude", default=0.0) + bpy.types.World.lnx_sky_sun_disc = IntProperty(name="Sky Sun Disc", default=1) bpy.types.Material.lnx_cast_shadow = BoolProperty(name="Cast Shadow", default=True) bpy.types.Material.lnx_receive_shadow = BoolProperty(name="Receive Shadow", description="Requires forward render path", default=True) bpy.types.Material.lnx_depth_write = BoolProperty(name="Write Depth", description="Allow this material to write to the depth buffer", default=True)