This commit is contained in:
Gorochu
2026-06-24 00:05:37 -07:00
parent 6db83e559b
commit 38151eb233
30 changed files with 1129 additions and 373 deletions

View File

@ -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

View File

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

View File

@ -348,7 +348,13 @@ typedef TWorldData = {
@:optional public var turbidity: Null<FastFloat>;
@:optional public var ground_albedo: Null<FastFloat>;
@: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<FastFloat>;
@:optional public var sky_sun_rotation: Null<FastFloat>;
@:optional public var sky_sun_size: Null<FastFloat>;
@:optional public var sky_sun_intensity: Null<FastFloat>;
@:optional public var sky_altitude: Null<FastFloat>;
@:optional public var sky_sun_disc: Null<Int>; // 0 or 1
}
#if js

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<kha.Image> {
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<iron.math.Vec4> {
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<iron.math.Vec4> {
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<kha.FastFloat> {
switch (link) {
#if rp_dynres

View File

@ -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<Float> = [0.3, 0.3, 0.3, 0.3];
// Spectral data sampled at 630, 560, 490, 430 nm
static var SUN_SPECTRAL_IRRADIANCE: Array<Float> = [1.679, 1.828, 1.986, 1.307];
static var MOLECULAR_SCATTERING_COEFFICIENT_BASE: Array<Float> = [6.605e-3, 1.067e-2, 1.842e-2, 3.156e-2];
static var OZONE_ABSORPTION_CROSS_SECTION: Array<Float> = [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<Float> = [2.8722e-24, 4.6168e-24, 7.9706e-24, 1.3578e-23];
static var AEROSOL_SCATTERING_CROSS_SECTION: Array<Float> = [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<Array<Float>> = [
[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<Float>): Array<Float> {
return [Math.exp(a[0]), Math.exp(a[1]), Math.exp(a[2]), Math.exp(a[3])];
}
static inline function scale4(a: Array<Float>, s: Float): Array<Float> {
return [a[0] * s, a[1] * s, a[2] * s, a[3] * s];
}
static inline function add4(a: Array<Float>, b: Array<Float>): Array<Float> {
return [a[0] + b[0], a[1] + b[1], a[2] + b[2], a[3] + b[3]];
}
static inline function mult4(a: Array<Float>, b: Array<Float>): Array<Float> {
return [a[0] * b[0], a[1] * b[1], a[2] * b[2], a[3] * b[3]];
}
static inline function div4(a: Array<Float>, b: Array<Float>): Array<Float> {
return [a[0] / b[0], a[1] / b[1], a[2] / b[2], a[3] / b[3]];
}
static inline function max4(a: Array<Float>, b: Float): Array<Float> {
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<Float>, b: Array<Float>, t: Float): Array<Float> {
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<Float> {
// 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<Float> {
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<Float> = [];
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<Float>, aerosolSca: Array<Float>, molAbs: Array<Float>, molSca: Array<Float>
} {
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<Float>): 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<Float> {
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<Float> = [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<Float> {
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<Float> {
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<Float> {
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<Float> {
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<Float> {
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<Float> {
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<Float> {
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<Float> {
var cosTheta = rayDir.clone().mult(-1.0).dot(sunDir);
var molPhase = molecularPhaseFunction(cosTheta);
var aerPhase = aerosolPhaseFunction(cosTheta);
var dt = tD / inScatteringSteps;
var lInscattering: Array<Float> = [0.0, 0.0, 0.0, 0.0];
var transmittance: Array<Float> = [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<Float> = [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<Float> = [];
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<Float> = [];
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);
}
}
}

View File

@ -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) {

View File

@ -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')

View File

@ -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')

View File

@ -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')

View File

@ -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')

View File

@ -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')

View File

@ -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')

View File

@ -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')

View File

@ -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')

View File

@ -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')

View File

@ -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')

View File

@ -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')

View File

@ -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

View File

@ -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:

View File

@ -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)