Merge pull request #2149 from MoritzBrueckner/2.9-NishitaSky
[Blender 2.9] Nishita sky model & (ir)radiance support for all world shaders
This commit is contained in:
commit
c9182cc152
163
Shaders/std/sky.glsl
Normal file
163
Shaders/std/sky.glsl
Normal file
|
@ -0,0 +1,163 @@
|
|||
/* Various sky functions
|
||||
* =====================
|
||||
*
|
||||
* Nishita 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
|
||||
* - Implemented air, dust and ozone density node parameters (see Blender source)
|
||||
* - Replaced the inner integral calculation with a LUT lookup
|
||||
*
|
||||
* Reference for the sun's limb darkening and ozone calculations:
|
||||
* [Hill] Sebastien Hillaire. Physically Based Sky, Atmosphere and Cloud Rendering in Frostbite
|
||||
* (https://media.contentapi.ea.com/content/dam/eacom/frostbite/files/s2016-pbs-frostbite-sky-clouds-new.pdf)
|
||||
*
|
||||
* Cycles code used for reference: blender/intern/sky/source/sky_nishita.cpp
|
||||
* (https://github.com/blender/blender/blob/4429b4b77ef6754739a3c2b4fabd0537999e9bdc/intern/sky/source/sky_nishita.cpp)
|
||||
*/
|
||||
|
||||
#ifndef _SKY_GLSL_
|
||||
#define _SKY_GLSL_
|
||||
|
||||
uniform sampler2D nishitaLUT;
|
||||
uniform vec2 nishitaDensity;
|
||||
|
||||
#ifndef PI
|
||||
#define PI 3.141592
|
||||
#endif
|
||||
#ifndef HALF_PI
|
||||
#define HALF_PI 1.570796
|
||||
#endif
|
||||
|
||||
#define nishita_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
|
||||
|
||||
// The ozone absorption coefficients are taken from Cycles code.
|
||||
// Because Cycles calculates 21 wavelengths, we use the coefficients
|
||||
// which are closest to the RGB wavelengths (645nm, 510nm, 440nm).
|
||||
// Precalculating values by simulating Blender's spec_to_xyz() function
|
||||
// to include all 21 wavelengths gave unrealistic results
|
||||
#define nishita_ozone_coeff vec3(1.59051840791988e-6, 0.00000096707041180970, 0.00000007309568762914)
|
||||
|
||||
// Values from [Hill: 60]
|
||||
#define sun_limb_darkening_col vec3(0.397, 0.503, 0.652)
|
||||
|
||||
float random(vec2 coords) {
|
||||
return fract(sin(dot(coords.xy, vec2(12.9898,78.233))) * 43758.5453);
|
||||
}
|
||||
|
||||
vec3 nishita_lookupLUT(const float height, const float sunTheta) {
|
||||
vec2 coords = vec2(
|
||||
sqrt(height * (1 / nishita_atmo_radius)),
|
||||
0.5 + 0.5 * sign(sunTheta - HALF_PI) * sqrt(abs(sunTheta * (1 / HALF_PI) - 1))
|
||||
);
|
||||
return textureLod(nishitaLUT, coords, 0.0).rgb;
|
||||
}
|
||||
|
||||
/* See raySphereIntersection() in armory/Sources/renderpath/Nishita.hx */
|
||||
vec2 nishita_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);
|
||||
float d = (b*b) - 4.0*a*c;
|
||||
|
||||
// If d < 0.0 the ray does not intersect the sphere
|
||||
return (d < 0.0) ? vec2(1e5,-1e5) : vec2((-b - sqrt(d))/(2.0*a), (-b + sqrt(d))/(2.0*a));
|
||||
}
|
||||
|
||||
/*
|
||||
* r: normalized ray direction
|
||||
* r0: ray origin
|
||||
* pSun: normalized sun direction
|
||||
* rPlanet: planet radius
|
||||
*/
|
||||
vec3 nishita_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);
|
||||
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);
|
||||
|
||||
// Primary ray time
|
||||
float iTime = 0.0;
|
||||
|
||||
// Accumulators for Rayleigh and Mie scattering.
|
||||
vec3 totalRlh = vec3(0,0,0);
|
||||
vec3 totalMie = vec3(0,0,0);
|
||||
|
||||
// Optical depth accumulators for the primary ray
|
||||
float iOdRlh = 0.0;
|
||||
float iOdMie = 0.0;
|
||||
|
||||
// Calculate the Rayleigh and Mie phases
|
||||
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));
|
||||
|
||||
// Sample the primary ray
|
||||
for (int i = 0; i < nishita_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;
|
||||
|
||||
// Accumulate optical depth
|
||||
iOdRlh += odStepRlh;
|
||||
iOdMie += odStepMie;
|
||||
|
||||
// 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 = acos(dot(normalize(iPos), normalize(pSun)));
|
||||
vec3 jODepth = nishita_lookupLUT(iHeight, sunTheta);// * vec3(14000000 / 255, 14000000 / 255, 2000000 / 255);
|
||||
|
||||
// Apply dithering to reduce visible banding
|
||||
jODepth += mix(-1000, 1000, random(r.xy));
|
||||
|
||||
// Calculate attenuation
|
||||
vec3 attn = exp(-(
|
||||
nishita_mie_coeff * (iOdMie + jODepth.y)
|
||||
+ (nishita_rayleigh_coeff) * (iOdRlh + jODepth.x)
|
||||
+ nishita_ozone_coeff * jODepth.z
|
||||
));
|
||||
|
||||
// Accumulate scattering
|
||||
totalRlh += odStepRlh * attn;
|
||||
totalMie += odStepMie * attn;
|
||||
|
||||
iTime += iStepSize;
|
||||
}
|
||||
|
||||
return nishita_sun_intensity * (pRlh * nishita_rayleigh_coeff * totalRlh + pMie * nishita_mie_coeff * totalMie);
|
||||
}
|
||||
|
||||
vec3 sun_disk(const vec3 n, const vec3 light_dir, const float disk_size, const float intensity) {
|
||||
// Normalized SDF
|
||||
float dist = distance(n, light_dir) / disk_size;
|
||||
|
||||
// Darken the edges of the sun
|
||||
// [Hill: 28, 60] (according to [Nec96])
|
||||
float invDist = 1.0 - dist;
|
||||
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;
|
||||
}
|
||||
|
||||
#endif
|
|
@ -72,4 +72,13 @@ class Helper {
|
|||
if (value <= leftMin) return rightMin;
|
||||
return map(value, leftMin, leftMax, rightMin, rightMax);
|
||||
}
|
||||
|
||||
/**
|
||||
Return the sign of the given value represented as `1.0` (positive value)
|
||||
or `-1.0` (negative value). The sign of `0` is `0`.
|
||||
**/
|
||||
public static inline function sign(value: Float): Float {
|
||||
if (value == 0) return 0;
|
||||
return (value < 0) ? -1.0 : 1.0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,176 +11,204 @@ class Uniforms {
|
|||
|
||||
public static function register() {
|
||||
iron.object.Uniforms.externalTextureLinks = [textureLink];
|
||||
iron.object.Uniforms.externalVec2Links = [];
|
||||
iron.object.Uniforms.externalVec2Links = [vec2Link];
|
||||
iron.object.Uniforms.externalVec3Links = [vec3Link];
|
||||
iron.object.Uniforms.externalVec4Links = [];
|
||||
iron.object.Uniforms.externalFloatLinks = [floatLink];
|
||||
iron.object.Uniforms.externalIntLinks = [];
|
||||
}
|
||||
|
||||
public static function textureLink(object: Object, mat: MaterialData, link: String): kha.Image {
|
||||
#if arm_ltc
|
||||
if (link == "_ltcMat") {
|
||||
if (armory.data.ConstData.ltcMatTex == null) armory.data.ConstData.initLTC();
|
||||
return armory.data.ConstData.ltcMatTex;
|
||||
public static function textureLink(object: Object, mat: MaterialData, link: String): Null<kha.Image> {
|
||||
switch (link) {
|
||||
case "_nishitaLUT": {
|
||||
if (armory.renderpath.Nishita.data == null) armory.renderpath.Nishita.recompute(Scene.active.world);
|
||||
return armory.renderpath.Nishita.data.lut;
|
||||
}
|
||||
#if arm_ltc
|
||||
case "_ltcMat": {
|
||||
if (armory.data.ConstData.ltcMatTex == null) armory.data.ConstData.initLTC();
|
||||
return armory.data.ConstData.ltcMatTex;
|
||||
}
|
||||
case "_ltcMag": {
|
||||
if (armory.data.ConstData.ltcMagTex == null) armory.data.ConstData.initLTC();
|
||||
return armory.data.ConstData.ltcMagTex;
|
||||
}
|
||||
#end
|
||||
}
|
||||
else if (link == "_ltcMag") {
|
||||
if (armory.data.ConstData.ltcMagTex == null) armory.data.ConstData.initLTC();
|
||||
return armory.data.ConstData.ltcMagTex;
|
||||
}
|
||||
#end
|
||||
|
||||
var target = iron.RenderPath.active.renderTargets.get(link.endsWith("_depth") ? link.substr(0, link.length - 6) : link);
|
||||
return target != null ? target.image : null;
|
||||
}
|
||||
|
||||
public static function vec3Link(object: Object, mat: MaterialData, link: String): iron.math.Vec4 {
|
||||
public static function vec3Link(object: Object, mat: MaterialData, link: String): Null<iron.math.Vec4> {
|
||||
var v: Vec4 = null;
|
||||
#if arm_hosek
|
||||
if (link == "_hosekA") {
|
||||
if (armory.renderpath.HosekWilkie.data == null) {
|
||||
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
|
||||
switch (link) {
|
||||
#if arm_hosek
|
||||
case "_hosekA": {
|
||||
if (armory.renderpath.HosekWilkie.data == null) {
|
||||
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
|
||||
}
|
||||
if (armory.renderpath.HosekWilkie.data != null) {
|
||||
v = iron.object.Uniforms.helpVec;
|
||||
v.x = armory.renderpath.HosekWilkie.data.A.x;
|
||||
v.y = armory.renderpath.HosekWilkie.data.A.y;
|
||||
v.z = armory.renderpath.HosekWilkie.data.A.z;
|
||||
}
|
||||
}
|
||||
if (armory.renderpath.HosekWilkie.data != null) {
|
||||
case "_hosekB": {
|
||||
if (armory.renderpath.HosekWilkie.data == null) {
|
||||
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
|
||||
}
|
||||
if (armory.renderpath.HosekWilkie.data != null) {
|
||||
v = iron.object.Uniforms.helpVec;
|
||||
v.x = armory.renderpath.HosekWilkie.data.B.x;
|
||||
v.y = armory.renderpath.HosekWilkie.data.B.y;
|
||||
v.z = armory.renderpath.HosekWilkie.data.B.z;
|
||||
}
|
||||
}
|
||||
case "_hosekC": {
|
||||
if (armory.renderpath.HosekWilkie.data == null) {
|
||||
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
|
||||
}
|
||||
if (armory.renderpath.HosekWilkie.data != null) {
|
||||
v = iron.object.Uniforms.helpVec;
|
||||
v.x = armory.renderpath.HosekWilkie.data.C.x;
|
||||
v.y = armory.renderpath.HosekWilkie.data.C.y;
|
||||
v.z = armory.renderpath.HosekWilkie.data.C.z;
|
||||
}
|
||||
}
|
||||
case "_hosekD": {
|
||||
if (armory.renderpath.HosekWilkie.data == null) {
|
||||
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
|
||||
}
|
||||
if (armory.renderpath.HosekWilkie.data != null) {
|
||||
v = iron.object.Uniforms.helpVec;
|
||||
v.x = armory.renderpath.HosekWilkie.data.D.x;
|
||||
v.y = armory.renderpath.HosekWilkie.data.D.y;
|
||||
v.z = armory.renderpath.HosekWilkie.data.D.z;
|
||||
}
|
||||
}
|
||||
case "_hosekE": {
|
||||
if (armory.renderpath.HosekWilkie.data == null) {
|
||||
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
|
||||
}
|
||||
if (armory.renderpath.HosekWilkie.data != null) {
|
||||
v = iron.object.Uniforms.helpVec;
|
||||
v.x = armory.renderpath.HosekWilkie.data.E.x;
|
||||
v.y = armory.renderpath.HosekWilkie.data.E.y;
|
||||
v.z = armory.renderpath.HosekWilkie.data.E.z;
|
||||
}
|
||||
}
|
||||
case "_hosekF": {
|
||||
if (armory.renderpath.HosekWilkie.data == null) {
|
||||
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
|
||||
}
|
||||
if (armory.renderpath.HosekWilkie.data != null) {
|
||||
v = iron.object.Uniforms.helpVec;
|
||||
v.x = armory.renderpath.HosekWilkie.data.F.x;
|
||||
v.y = armory.renderpath.HosekWilkie.data.F.y;
|
||||
v.z = armory.renderpath.HosekWilkie.data.F.z;
|
||||
}
|
||||
}
|
||||
case "_hosekG": {
|
||||
if (armory.renderpath.HosekWilkie.data == null) {
|
||||
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
|
||||
}
|
||||
if (armory.renderpath.HosekWilkie.data != null) {
|
||||
v = iron.object.Uniforms.helpVec;
|
||||
v.x = armory.renderpath.HosekWilkie.data.G.x;
|
||||
v.y = armory.renderpath.HosekWilkie.data.G.y;
|
||||
v.z = armory.renderpath.HosekWilkie.data.G.z;
|
||||
}
|
||||
}
|
||||
case "_hosekH": {
|
||||
if (armory.renderpath.HosekWilkie.data == null) {
|
||||
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
|
||||
}
|
||||
if (armory.renderpath.HosekWilkie.data != null) {
|
||||
v = iron.object.Uniforms.helpVec;
|
||||
v.x = armory.renderpath.HosekWilkie.data.H.x;
|
||||
v.y = armory.renderpath.HosekWilkie.data.H.y;
|
||||
v.z = armory.renderpath.HosekWilkie.data.H.z;
|
||||
}
|
||||
}
|
||||
case "_hosekI": {
|
||||
if (armory.renderpath.HosekWilkie.data == null) {
|
||||
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
|
||||
}
|
||||
if (armory.renderpath.HosekWilkie.data != null) {
|
||||
v = iron.object.Uniforms.helpVec;
|
||||
v.x = armory.renderpath.HosekWilkie.data.I.x;
|
||||
v.y = armory.renderpath.HosekWilkie.data.I.y;
|
||||
v.z = armory.renderpath.HosekWilkie.data.I.z;
|
||||
}
|
||||
}
|
||||
case "_hosekZ": {
|
||||
if (armory.renderpath.HosekWilkie.data == null) {
|
||||
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
|
||||
}
|
||||
if (armory.renderpath.HosekWilkie.data != null) {
|
||||
v = iron.object.Uniforms.helpVec;
|
||||
v.x = armory.renderpath.HosekWilkie.data.Z.x;
|
||||
v.y = armory.renderpath.HosekWilkie.data.Z.y;
|
||||
v.z = armory.renderpath.HosekWilkie.data.Z.z;
|
||||
}
|
||||
}
|
||||
#end
|
||||
#if rp_voxelao
|
||||
case "_cameraPositionSnap": {
|
||||
v = iron.object.Uniforms.helpVec;
|
||||
v.x = armory.renderpath.HosekWilkie.data.A.x;
|
||||
v.y = armory.renderpath.HosekWilkie.data.A.y;
|
||||
v.z = armory.renderpath.HosekWilkie.data.A.z;
|
||||
var camera = iron.Scene.active.camera;
|
||||
v.set(camera.transform.worldx(), camera.transform.worldy(), camera.transform.worldz());
|
||||
var l = camera.lookWorld();
|
||||
var e = Main.voxelgiHalfExtents;
|
||||
v.x += l.x * e * 0.9;
|
||||
v.y += l.y * e * 0.9;
|
||||
var f = Main.voxelgiVoxelSize * 8; // Snaps to 3 mip-maps range
|
||||
v.set(Math.floor(v.x / f) * f, Math.floor(v.y / f) * f, Math.floor(v.z / f) * f);
|
||||
}
|
||||
#end
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
public static function vec2Link(object: Object, mat: MaterialData, link: String): Null<iron.math.Vec4> {
|
||||
var v: Vec4 = null;
|
||||
switch (link) {
|
||||
case "_nishitaDensity": {
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (link == "_hosekB") {
|
||||
if (armory.renderpath.HosekWilkie.data == null) {
|
||||
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
|
||||
}
|
||||
if (armory.renderpath.HosekWilkie.data != null) {
|
||||
v = iron.object.Uniforms.helpVec;
|
||||
v.x = armory.renderpath.HosekWilkie.data.B.x;
|
||||
v.y = armory.renderpath.HosekWilkie.data.B.y;
|
||||
v.z = armory.renderpath.HosekWilkie.data.B.z;
|
||||
}
|
||||
}
|
||||
else if (link == "_hosekC") {
|
||||
if (armory.renderpath.HosekWilkie.data == null) {
|
||||
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
|
||||
}
|
||||
if (armory.renderpath.HosekWilkie.data != null) {
|
||||
v = iron.object.Uniforms.helpVec;
|
||||
v.x = armory.renderpath.HosekWilkie.data.C.x;
|
||||
v.y = armory.renderpath.HosekWilkie.data.C.y;
|
||||
v.z = armory.renderpath.HosekWilkie.data.C.z;
|
||||
}
|
||||
}
|
||||
else if (link == "_hosekD") {
|
||||
if (armory.renderpath.HosekWilkie.data == null) {
|
||||
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
|
||||
}
|
||||
if (armory.renderpath.HosekWilkie.data != null) {
|
||||
v = iron.object.Uniforms.helpVec;
|
||||
v.x = armory.renderpath.HosekWilkie.data.D.x;
|
||||
v.y = armory.renderpath.HosekWilkie.data.D.y;
|
||||
v.z = armory.renderpath.HosekWilkie.data.D.z;
|
||||
}
|
||||
}
|
||||
else if (link == "_hosekE") {
|
||||
if (armory.renderpath.HosekWilkie.data == null) {
|
||||
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
|
||||
}
|
||||
if (armory.renderpath.HosekWilkie.data != null) {
|
||||
v = iron.object.Uniforms.helpVec;
|
||||
v.x = armory.renderpath.HosekWilkie.data.E.x;
|
||||
v.y = armory.renderpath.HosekWilkie.data.E.y;
|
||||
v.z = armory.renderpath.HosekWilkie.data.E.z;
|
||||
}
|
||||
}
|
||||
else if (link == "_hosekF") {
|
||||
if (armory.renderpath.HosekWilkie.data == null) {
|
||||
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
|
||||
}
|
||||
if (armory.renderpath.HosekWilkie.data != null) {
|
||||
v = iron.object.Uniforms.helpVec;
|
||||
v.x = armory.renderpath.HosekWilkie.data.F.x;
|
||||
v.y = armory.renderpath.HosekWilkie.data.F.y;
|
||||
v.z = armory.renderpath.HosekWilkie.data.F.z;
|
||||
}
|
||||
}
|
||||
else if (link == "_hosekG") {
|
||||
if (armory.renderpath.HosekWilkie.data == null) {
|
||||
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
|
||||
}
|
||||
if (armory.renderpath.HosekWilkie.data != null) {
|
||||
v = iron.object.Uniforms.helpVec;
|
||||
v.x = armory.renderpath.HosekWilkie.data.G.x;
|
||||
v.y = armory.renderpath.HosekWilkie.data.G.y;
|
||||
v.z = armory.renderpath.HosekWilkie.data.G.z;
|
||||
}
|
||||
}
|
||||
else if (link == "_hosekH") {
|
||||
if (armory.renderpath.HosekWilkie.data == null) {
|
||||
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
|
||||
}
|
||||
if (armory.renderpath.HosekWilkie.data != null) {
|
||||
v = iron.object.Uniforms.helpVec;
|
||||
v.x = armory.renderpath.HosekWilkie.data.H.x;
|
||||
v.y = armory.renderpath.HosekWilkie.data.H.y;
|
||||
v.z = armory.renderpath.HosekWilkie.data.H.z;
|
||||
}
|
||||
}
|
||||
else if (link == "_hosekI") {
|
||||
if (armory.renderpath.HosekWilkie.data == null) {
|
||||
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
|
||||
}
|
||||
if (armory.renderpath.HosekWilkie.data != null) {
|
||||
v = iron.object.Uniforms.helpVec;
|
||||
v.x = armory.renderpath.HosekWilkie.data.I.x;
|
||||
v.y = armory.renderpath.HosekWilkie.data.I.y;
|
||||
v.z = armory.renderpath.HosekWilkie.data.I.z;
|
||||
}
|
||||
}
|
||||
else if (link == "_hosekZ") {
|
||||
if (armory.renderpath.HosekWilkie.data == null) {
|
||||
armory.renderpath.HosekWilkie.recompute(Scene.active.world);
|
||||
}
|
||||
if (armory.renderpath.HosekWilkie.data != null) {
|
||||
v = iron.object.Uniforms.helpVec;
|
||||
v.x = armory.renderpath.HosekWilkie.data.Z.x;
|
||||
v.y = armory.renderpath.HosekWilkie.data.Z.y;
|
||||
v.z = armory.renderpath.HosekWilkie.data.Z.z;
|
||||
}
|
||||
}
|
||||
#end
|
||||
#if rp_voxelao
|
||||
if (link == "_cameraPositionSnap") {
|
||||
v = iron.object.Uniforms.helpVec;
|
||||
var camera = iron.Scene.active.camera;
|
||||
v.set(camera.transform.worldx(), camera.transform.worldy(), camera.transform.worldz());
|
||||
var l = camera.lookWorld();
|
||||
var e = Main.voxelgiHalfExtents;
|
||||
v.x += l.x * e * 0.9;
|
||||
v.y += l.y * e * 0.9;
|
||||
var f = Main.voxelgiVoxelSize * 8; // Snaps to 3 mip-maps range
|
||||
v.set(Math.floor(v.x / f) * f, Math.floor(v.y / f) * f, Math.floor(v.z / f) * f);
|
||||
}
|
||||
#end
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
public static function floatLink(object: Object, mat: MaterialData, link: String): Null<kha.FastFloat> {
|
||||
#if rp_dynres
|
||||
if (link == "_dynamicScale") {
|
||||
return armory.renderpath.DynamicResolutionScale.dynamicScale;
|
||||
switch (link) {
|
||||
#if rp_dynres
|
||||
case "_dynamicScale": {
|
||||
return armory.renderpath.DynamicResolutionScale.dynamicScale;
|
||||
}
|
||||
#end
|
||||
#if arm_debug
|
||||
case "_debugFloat": {
|
||||
return armory.trait.internal.DebugConsole.debugFloat;
|
||||
}
|
||||
#end
|
||||
#if rp_voxelao
|
||||
case "_voxelBlend": { // Blend current and last voxels
|
||||
var freq = armory.renderpath.RenderPathCreator.voxelFreq;
|
||||
return (armory.renderpath.RenderPathCreator.voxelFrame % freq) / freq;
|
||||
}
|
||||
#end
|
||||
}
|
||||
#end
|
||||
#if arm_debug
|
||||
if (link == "_debugFloat") {
|
||||
return armory.trait.internal.DebugConsole.debugFloat;
|
||||
}
|
||||
#end
|
||||
#if rp_voxelao
|
||||
if (link == "_voxelBlend") { // Blend current and last voxels
|
||||
var freq = armory.renderpath.RenderPathCreator.voxelFreq;
|
||||
return (armory.renderpath.RenderPathCreator.voxelFrame % freq) / freq;
|
||||
}
|
||||
#end
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
190
Sources/armory/renderpath/Nishita.hx
Normal file
190
Sources/armory/renderpath/Nishita.hx
Normal file
|
@ -0,0 +1,190 @@
|
|||
package armory.renderpath;
|
||||
|
||||
import kha.FastFloat;
|
||||
import kha.arrays.Float32Array;
|
||||
import kha.graphics4.TextureFormat;
|
||||
import kha.graphics4.Usage;
|
||||
|
||||
import iron.data.WorldData;
|
||||
import iron.math.Vec2;
|
||||
import iron.math.Vec3;
|
||||
|
||||
import armory.math.Helper;
|
||||
|
||||
/**
|
||||
Utility class to control the Nishita sky model.
|
||||
**/
|
||||
class Nishita {
|
||||
|
||||
public static var data: NishitaData = null;
|
||||
|
||||
/**
|
||||
Recomputes the nishita lookup table after the density settings changed.
|
||||
Do not call this method on every frame (it's slow)!
|
||||
**/
|
||||
public static function recompute(world: WorldData) {
|
||||
if (world == null || world.raw.nishita_density == null) return;
|
||||
if (data == null) data = new NishitaData();
|
||||
|
||||
var density = world.raw.nishita_density;
|
||||
data.computeLUT(new Vec3(density[0], density[1], density[2]));
|
||||
}
|
||||
|
||||
/** Sets the sky's density parameters and calls `recompute()` afterwards. **/
|
||||
public static function setDensity(world: WorldData, densityAir: FastFloat, densityDust: FastFloat, densityOzone: FastFloat) {
|
||||
if (world == null) return;
|
||||
|
||||
if (world.raw.nishita_density == null) world.raw.nishita_density = new Float32Array(3);
|
||||
var density = world.raw.nishita_density;
|
||||
density[0] = Helper.clamp(densityAir, 0, 10);
|
||||
density[1] = Helper.clamp(densityDust, 0, 10);
|
||||
density[2] = Helper.clamp(densityOzone, 0, 10);
|
||||
|
||||
recompute(world);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
This class holds the precalculated result of the inner scattering integral
|
||||
of the Nishita sky model. The outer integral is calculated in
|
||||
[`armory/Shaders/std/sky.glsl`](https://github.com/armory3d/armory/blob/master/Shaders/std/sky.glsl).
|
||||
|
||||
@see `armory.renderpath.Nishita`
|
||||
**/
|
||||
class NishitaData {
|
||||
|
||||
public var lut: kha.Image;
|
||||
|
||||
/**
|
||||
The amount of individual sample heights stored in the LUT (and the width
|
||||
of the LUT image).
|
||||
**/
|
||||
public static var lutHeightSteps = 128;
|
||||
/**
|
||||
The amount of individual sun angle steps stored in the LUT (and the
|
||||
height of the LUT image).
|
||||
**/
|
||||
public static var lutAngleSteps = 128;
|
||||
|
||||
/**
|
||||
Amount of steps for calculating the inner scattering integral. Heigher
|
||||
values are more precise but take longer to compute.
|
||||
**/
|
||||
public static var jSteps = 8;
|
||||
|
||||
/** Radius of the atmosphere in meters. **/
|
||||
public static var radiusAtmo = 6420000;
|
||||
/**
|
||||
Radius of the planet in meters. The default value is the earth radius as
|
||||
defined in Cycles.
|
||||
**/
|
||||
public static var radiusPlanet = 6360000;
|
||||
|
||||
/** Rayleigh scattering scale parameter. **/
|
||||
public static var rayleighScale = 8e3;
|
||||
/** Mie scattering scale parameter. **/
|
||||
public static var mieScale = 1.2e3;
|
||||
|
||||
public function new() {}
|
||||
|
||||
/** Approximates the density of ozone for a given sample height. **/
|
||||
function getOzoneDensity(height: FastFloat): FastFloat {
|
||||
// Values are taken from Cycles code
|
||||
if (height < 10000.0 || height >= 40000.0) {
|
||||
return 0.0;
|
||||
}
|
||||
if (height < 25000.0) {
|
||||
return (height - 10000.0) / 15000.0;
|
||||
}
|
||||
return -((height - 40000.0) / 15000.0);
|
||||
}
|
||||
|
||||
/**
|
||||
Ray-sphere intersection test that assumes the sphere is centered at the
|
||||
origin. There is no intersection when result.x > result.y. Otherwise
|
||||
this function returns the distances to the two intersection points,
|
||||
which might be equal.
|
||||
**/
|
||||
function raySphereIntersection(rayOrigin: Vec3, rayDirection: Vec3, sphereRadius: Int): Vec2 {
|
||||
// Algorithm is described here: https://en.wikipedia.org/wiki/Line%E2%80%93sphere_intersection
|
||||
var a = rayDirection.dot(rayDirection);
|
||||
var b = 2.0 * rayDirection.dot(rayOrigin);
|
||||
var c = rayOrigin.dot(rayOrigin) - (sphereRadius * sphereRadius);
|
||||
var d = (b * b) - 4.0 * a * c;
|
||||
|
||||
// Ray does not intersect the sphere
|
||||
if (d < 0.0) return new Vec2(1e5, -1e5);
|
||||
|
||||
return new Vec2(
|
||||
(-b - Math.sqrt(d)) / (2.0 * a),
|
||||
(-b + Math.sqrt(d)) / (2.0 * a)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
Computes the LUT texture for the given density values.
|
||||
@param density 3D vector of air density, dust density, ozone density
|
||||
**/
|
||||
public function computeLUT(density: Vec3) {
|
||||
var imageData = new haxe.io.Float32Array(lutHeightSteps * lutAngleSteps * 4);
|
||||
|
||||
for (x in 0...lutHeightSteps) {
|
||||
var height = (x / (lutHeightSteps - 1));
|
||||
|
||||
// Use quadratic height for better horizon precision
|
||||
height *= height;
|
||||
height *= radiusAtmo; // Denormalize
|
||||
|
||||
for (y in 0...lutAngleSteps) {
|
||||
var sunTheta = y / (lutAngleSteps - 1) * 2 - 1;
|
||||
|
||||
// Improve horizon precision
|
||||
// See https://sebh.github.io/publications/egsr2020.pdf (5.3)
|
||||
sunTheta = Helper.sign(sunTheta) * sunTheta * sunTheta;
|
||||
sunTheta = sunTheta * Math.PI / 2 + Math.PI / 2; // Denormalize
|
||||
|
||||
var jODepth = sampleSecondaryRay(height, sunTheta, density);
|
||||
|
||||
var pixelIndex = (x + y * lutHeightSteps) * 4;
|
||||
imageData[pixelIndex + 0] = jODepth.x;
|
||||
imageData[pixelIndex + 1] = jODepth.y;
|
||||
imageData[pixelIndex + 2] = jODepth.z;
|
||||
imageData[pixelIndex + 3] = 1.0; // Unused
|
||||
}
|
||||
}
|
||||
|
||||
lut = kha.Image.fromBytes(imageData.view.buffer, lutHeightSteps, lutAngleSteps, TextureFormat.RGBA128, Usage.StaticUsage);
|
||||
}
|
||||
|
||||
/**
|
||||
Calculates the integral for the secondary ray.
|
||||
**/
|
||||
public function sampleSecondaryRay(height: FastFloat, sunTheta: FastFloat, density: Vec3): Vec3 {
|
||||
// Reconstruct values from the shader
|
||||
var iPos = new Vec3(0, 0, height + radiusPlanet);
|
||||
var pSun = new Vec3(0.0, Math.sin(sunTheta), Math.cos(sunTheta)).normalize();
|
||||
|
||||
var jTime: FastFloat = 0.0;
|
||||
var jStepSize: FastFloat = raySphereIntersection(iPos, pSun, radiusAtmo).y / jSteps;
|
||||
|
||||
// Optical depth accumulators for the secondary ray (Rayleigh, Mie, ozone)
|
||||
var jODepth = new Vec3();
|
||||
|
||||
for (i in 0...jSteps) {
|
||||
|
||||
// Calculate the secondary ray sample position and height
|
||||
var jPos = iPos.clone().add(pSun.clone().mult(jTime + jStepSize * 0.5));
|
||||
var jHeight = jPos.length() - radiusPlanet;
|
||||
|
||||
// Accumulate optical depth
|
||||
var optDepthRayleigh = Math.exp(-jHeight / rayleighScale) * density.x;
|
||||
var optDepthMie = Math.exp(-jHeight / mieScale) * density.y;
|
||||
var optDepthOzone = getOzoneDensity(jHeight) * density.z;
|
||||
jODepth.addf(optDepthRayleigh, optDepthMie, optDepthOzone);
|
||||
|
||||
jTime += jStepSize;
|
||||
}
|
||||
|
||||
return jODepth.mult(jStepSize);
|
||||
}
|
||||
}
|
|
@ -2843,6 +2843,7 @@ class ArmoryExporter:
|
|||
out_world['sun_direction'] = list(world.arm_envtex_sun_direction)
|
||||
out_world['turbidity'] = world.arm_envtex_turbidity
|
||||
out_world['ground_albedo'] = world.arm_envtex_ground_albedo
|
||||
out_world['nishita_density'] = list(world.arm_nishita_density)
|
||||
|
||||
disable_hdr = world.arm_envtex_name.endswith('.jpg')
|
||||
|
||||
|
@ -2857,16 +2858,9 @@ class ArmoryExporter:
|
|||
rpdat = arm.utils.get_rp()
|
||||
solid_mat = rpdat.arm_material_model == 'Solid'
|
||||
arm_irradiance = rpdat.arm_irradiance and not solid_mat
|
||||
arm_radiance = False
|
||||
radtex = world.arm_envtex_name.rsplit('.', 1)[0]
|
||||
arm_radiance = rpdat.arm_radiance
|
||||
radtex = world.arm_envtex_name.rsplit('.', 1)[0] # Remove file extension
|
||||
irrsharmonics = world.arm_envtex_irr_name
|
||||
|
||||
# Radiance
|
||||
if '_EnvTex' in world.world_defs:
|
||||
arm_radiance = rpdat.arm_radiance
|
||||
elif '_EnvSky' in world.world_defs:
|
||||
arm_radiance = rpdat.arm_radiance
|
||||
radtex = 'hosek'
|
||||
num_mips = world.arm_envtex_num_mips
|
||||
strength = world.arm_envtex_strength
|
||||
|
||||
|
|
|
@ -17,17 +17,59 @@ shader_datas = []
|
|||
|
||||
|
||||
def build():
|
||||
"""Builds world shaders for all exported worlds."""
|
||||
global shader_datas
|
||||
|
||||
bpy.data.worlds['Arm'].world_defs = ''
|
||||
wrd = bpy.data.worlds['Arm']
|
||||
rpdat = arm.utils.get_rp()
|
||||
|
||||
mobile_mat = rpdat.arm_material_model == 'Mobile' or rpdat.arm_material_model == 'Solid'
|
||||
envpath = os.path.join(arm.utils.get_fp_build(), 'compiled', 'Assets', 'envmaps')
|
||||
|
||||
wrd.world_defs = ''
|
||||
worlds = []
|
||||
shader_datas = []
|
||||
|
||||
for scene in bpy.data.scenes:
|
||||
# Only export worlds from enabled scenes
|
||||
if scene.arm_export and scene.world is not None and scene.world not in worlds:
|
||||
worlds.append(scene.world)
|
||||
create_world_shaders(scene.world)
|
||||
with write_probes.setup_envmap_render():
|
||||
|
||||
for scene in bpy.data.scenes:
|
||||
world = scene.world
|
||||
|
||||
# Only export worlds from enabled scenes and only once per world
|
||||
if scene.arm_export and world is not None and world not in worlds:
|
||||
worlds.append(world)
|
||||
|
||||
world.arm_envtex_name = ''
|
||||
create_world_shaders(world)
|
||||
|
||||
if rpdat.arm_irradiance:
|
||||
# Plain background color
|
||||
if '_EnvCol' in world.world_defs:
|
||||
world_name = arm.utils.safestr(world.name)
|
||||
# Irradiance json file name
|
||||
world.arm_envtex_name = world_name
|
||||
world.arm_envtex_irr_name = world_name
|
||||
write_probes.write_color_irradiance(world_name, world.arm_envtex_color)
|
||||
|
||||
# Render world to envmap for (ir)radiance, if no
|
||||
# other probes are exported
|
||||
elif world.arm_envtex_name == '':
|
||||
write_probes.render_envmap(envpath, world)
|
||||
|
||||
filename = f'env_{arm.utils.safesrc(world.name)}'
|
||||
image_file = f'{filename}.jpg'
|
||||
image_filepath = os.path.join(envpath, image_file)
|
||||
|
||||
world.arm_envtex_name = image_file
|
||||
world.arm_envtex_irr_name = os.path.basename(image_filepath).rsplit('.', 1)[0]
|
||||
|
||||
write_radiance = rpdat.arm_radiance and not mobile_mat
|
||||
mip_count = write_probes.write_probes(image_filepath, True, world.arm_envtex_num_mips, write_radiance)
|
||||
world.arm_envtex_num_mips = mip_count
|
||||
|
||||
if write_radiance:
|
||||
# Set world def, everything else is handled by write_probes()
|
||||
wrd.world_defs += '_Rad'
|
||||
|
||||
|
||||
def create_world_shaders(world: bpy.types.World):
|
||||
|
@ -131,14 +173,7 @@ def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader, con: Sha
|
|||
col = world.color
|
||||
world.arm_envtex_color = [col[0], col[1], col[2], 1.0]
|
||||
world.arm_envtex_strength = 1.0
|
||||
|
||||
# Irradiance/Radiance: clear to color if no texture or sky is provided
|
||||
if rpdat.arm_irradiance or rpdat.arm_irradiance:
|
||||
if '_EnvSky' not in world.world_defs and '_EnvTex' not in world.world_defs and '_EnvImg' not in world.world_defs:
|
||||
# Irradiance json file name
|
||||
world.arm_envtex_name = world_name
|
||||
world.arm_envtex_irr_name = world_name
|
||||
write_probes.write_color_irradiance(world_name, world.arm_envtex_color)
|
||||
world.world_defs += '_EnvCol'
|
||||
|
||||
# Clouds enabled
|
||||
if rpdat.arm_clouds and world.arm_use_clouds:
|
||||
|
@ -279,7 +314,12 @@ 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:
|
||||
func_cloud_radiance += '\tvec3 sun_dir = hosekSunDirection;\n'
|
||||
# Nishita sky
|
||||
if 'vec3 sunDir' in frag.uniforms:
|
||||
func_cloud_radiance += '\tvec3 sun_dir = sunDir;\n'
|
||||
# Hosek
|
||||
else:
|
||||
func_cloud_radiance += '\tvec3 sun_dir = hosekSunDirection;\n'
|
||||
else:
|
||||
func_cloud_radiance += '\tvec3 sun_dir = vec3(0, 0, -1);\n'
|
||||
func_cloud_radiance += '''\tconst int steps = 8;
|
||||
|
@ -293,7 +333,7 @@ def frag_write_clouds(world: bpy.types.World, frag: Shader):
|
|||
}'''
|
||||
frag.add_function(func_cloud_radiance)
|
||||
|
||||
frag.add_function('''vec3 traceClouds(vec3 sky, vec3 dir) {
|
||||
func_trace_clouds = '''vec3 traceClouds(vec3 sky, vec3 dir) {
|
||||
\tconst float step_size = 0.5 / float(cloudsSteps);
|
||||
\tfloat T = 1.0;
|
||||
\tfloat C = 0.0;
|
||||
|
@ -312,6 +352,17 @@ def frag_write_clouds(world: bpy.types.World, frag: Shader):
|
|||
\t\t}
|
||||
\t\tuv += (dir.xy / dir.z) * step_size * cloudsUpper;
|
||||
\t}
|
||||
'''
|
||||
|
||||
\treturn vec3(C) + sky * T;
|
||||
}''')
|
||||
if world.arm_darken_clouds:
|
||||
func_trace_clouds += '\t// Darken clouds when the sun is low\n'
|
||||
|
||||
# Nishita sky
|
||||
if 'vec3 sunDir' in frag.uniforms:
|
||||
func_trace_clouds += '\tC *= smoothstep(-0.02, 0.25, sunDir.z);\n'
|
||||
# Hosek
|
||||
else:
|
||||
func_trace_clouds += '\tC *= smoothstep(0.04, 0.32, hosekSunDirection.z);\n'
|
||||
|
||||
func_trace_clouds += '\treturn vec3(C) + sky * T;\n}'
|
||||
frag.add_function(func_trace_clouds)
|
||||
|
|
|
@ -640,8 +640,12 @@ def to_vec1(v):
|
|||
return str(v)
|
||||
|
||||
|
||||
def to_vec2(v):
|
||||
return f'vec2({v[0]}, {v[1]})'
|
||||
|
||||
|
||||
def to_vec3(v):
|
||||
return 'vec3({0}, {1}, {2})'.format(v[0], v[1], v[2])
|
||||
return f'vec3({v[0]}, {v[1]}, {v[2]})'
|
||||
|
||||
|
||||
def cast_value(val: str, from_type: str, to_type: str) -> str:
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import math
|
||||
import os
|
||||
from typing import Union
|
||||
|
||||
|
@ -293,13 +294,29 @@ def parse_tex_sky(node: bpy.types.ShaderNodeTexSky, out_socket: bpy.types.NodeSo
|
|||
# Pass through
|
||||
return c.to_vec3([0.0, 0.0, 0.0])
|
||||
|
||||
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)
|
||||
|
||||
else:
|
||||
log.error(f'Unsupported sky model: {node.sky_type}!')
|
||||
return c.to_vec3([0.0, 0.0, 0.0])
|
||||
|
||||
|
||||
def parse_sky_hosekwilkie(node: bpy.types.ShaderNodeTexSky, state: ParserState) -> vec3str:
|
||||
world = state.world
|
||||
curshader = state.curshader
|
||||
|
||||
# Match to cycles
|
||||
world.arm_envtex_strength *= 0.1
|
||||
|
||||
world.world_defs += '_EnvSky'
|
||||
assets.add_khafile_def('arm_hosek')
|
||||
curshader.add_uniform('vec3 A', link="_hosekA")
|
||||
curshader.add_uniform('vec3 B', link="_hosekB")
|
||||
|
@ -312,10 +329,10 @@ def parse_tex_sky(node: bpy.types.ShaderNodeTexSky, out_socket: bpy.types.NodeSo
|
|||
curshader.add_uniform('vec3 I', link="_hosekI")
|
||||
curshader.add_uniform('vec3 Z', link="_hosekZ")
|
||||
curshader.add_uniform('vec3 hosekSunDirection', link="_hosekSunDirection")
|
||||
curshader.add_function('''vec3 hosekWilkie(float cos_theta, float gamma, float cos_gamma) {
|
||||
curshader.add_function("""vec3 hosekWilkie(float cos_theta, float gamma, float cos_gamma) {
|
||||
\tvec3 chi = (1 + cos_gamma * cos_gamma) / pow(1 + H * H - 2 * cos_gamma * H, vec3(1.5));
|
||||
\treturn (1 + A * exp(B / (cos_theta + 0.01))) * (C + D * exp(E * gamma) + F * (cos_gamma * cos_gamma) + G * chi + I * sqrt(cos_theta));
|
||||
}''')
|
||||
}""")
|
||||
|
||||
world.arm_envtex_sun_direction = [node.sun_direction[0], node.sun_direction[1], node.sun_direction[2]]
|
||||
world.arm_envtex_turbidity = node.turbidity
|
||||
|
@ -353,6 +370,40 @@ def parse_tex_sky(node: bpy.types.ShaderNodeTexSky, out_socket: bpy.types.NodeSo
|
|||
return 'Z * hosekWilkie(cos_theta, gamma_val, cos_gamma) * envmapStrength;'
|
||||
|
||||
|
||||
def parse_sky_nishita(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,
|
||||
tex_addr_u='clamp', tex_addr_v='clamp')
|
||||
curshader.add_uniform('vec2 nishitaDensity', link='_nishitaDensity', included=True)
|
||||
|
||||
planet_radius = 6360e3 # Earth radius used in Blender
|
||||
ray_origin_z = planet_radius + node.altitude
|
||||
|
||||
state.world.arm_nishita_density = [node.air_density, node.dust_density, node.ozone_density]
|
||||
|
||||
sun = ''
|
||||
if node.sun_disc:
|
||||
# The sun size is calculated relative in terms of the distance
|
||||
# between the sun position and the sky dome normal at every
|
||||
# pixel (see sun_disk() in sky.glsl).
|
||||
#
|
||||
# An isosceles triangle is created with the camera at the
|
||||
# opposite side of the base with node.sun_size being the vertex
|
||||
# angle from which the base angle theta is calculated. Iron's
|
||||
# skydome geometry roughly resembles a unit sphere, so the leg
|
||||
# size is set to 1. The base size is the doubled normal-relative
|
||||
# target size.
|
||||
|
||||
# sun_size is already in radians despite being degrees in the UI
|
||||
theta = 0.5 * (math.pi - node.sun_size)
|
||||
size = math.cos(theta)
|
||||
sun = f'* sun_disk(n, sunDir, {size}, {node.sun_intensity})'
|
||||
|
||||
return f'nishita_atmosphere(n, vec3(0, 0, {ray_origin_z}), sunDir, {planet_radius}){sun}'
|
||||
|
||||
|
||||
def parse_tex_environment(node: bpy.types.ShaderNodeTexEnvironment, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str:
|
||||
if state.context == ParserContext.OBJECT:
|
||||
log.warn('Environment Texture node is not supported for object node trees, using default value')
|
||||
|
|
|
@ -119,16 +119,29 @@ class ShaderContext:
|
|||
c['link'] = link
|
||||
self.constants.append(c)
|
||||
|
||||
def add_texture_unit(self, ctype, name, link=None, is_image=None):
|
||||
def add_texture_unit(self, name, link=None, is_image=None,
|
||||
addr_u=None, addr_v=None,
|
||||
filter_min=None, filter_mag=None, mipmap_filter=None):
|
||||
for c in self.tunits:
|
||||
if c['name'] == name:
|
||||
return
|
||||
|
||||
c = { 'name': name }
|
||||
if link != None:
|
||||
c = {'name': name}
|
||||
if link is not None:
|
||||
c['link'] = link
|
||||
if is_image != None:
|
||||
if is_image is not None:
|
||||
c['is_image'] = is_image
|
||||
if addr_u is not None:
|
||||
c['addressing_u'] = addr_u
|
||||
if addr_v is not None:
|
||||
c['addressing_v'] = addr_v
|
||||
if filter_min is not None:
|
||||
c['filter_min'] = filter_min
|
||||
if filter_mag is not None:
|
||||
c['filter_mag'] = filter_mag
|
||||
if mipmap_filter is not None:
|
||||
c['mipmap_filter'] = mipmap_filter
|
||||
|
||||
self.tunits.append(c)
|
||||
|
||||
def make_vert(self, custom_name: str = None):
|
||||
|
@ -213,7 +226,10 @@ class Shader:
|
|||
if s not in self.outs:
|
||||
self.outs.append(s)
|
||||
|
||||
def add_uniform(self, s, link=None, included=False):
|
||||
def add_uniform(self, s, link=None, included=False,
|
||||
tex_addr_u=None, tex_addr_v=None,
|
||||
tex_filter_min=None, tex_filter_mag=None,
|
||||
tex_mipmap_filter=None):
|
||||
ar = s.split(' ')
|
||||
# layout(RGBA8) image3D voxels
|
||||
utype = ar[-2]
|
||||
|
@ -224,9 +240,15 @@ class Shader:
|
|||
# Add individual units - mySamplers[0], mySamplers[1]
|
||||
for i in range(int(uname[-2])):
|
||||
uname_array = uname[:-2] + str(i) + ']'
|
||||
self.context.add_texture_unit(utype, uname_array, link=link, is_image=is_image)
|
||||
self.context.add_texture_unit(
|
||||
uname_array, link, is_image,
|
||||
tex_addr_u, tex_addr_v,
|
||||
tex_filter_min, tex_filter_mag, tex_mipmap_filter)
|
||||
else:
|
||||
self.context.add_texture_unit(utype, uname, link=link, is_image=is_image)
|
||||
self.context.add_texture_unit(
|
||||
uname, link, is_image,
|
||||
tex_addr_u, tex_addr_v,
|
||||
tex_filter_min, tex_filter_mag, tex_mipmap_filter)
|
||||
else:
|
||||
# Prefer vec4[] for d3d to avoid padding
|
||||
if ar[0] == 'float' and '[' in ar[1]:
|
||||
|
|
|
@ -326,6 +326,7 @@ def init_properties():
|
|||
bpy.types.World.arm_envtex_sun_direction = FloatVectorProperty(name="Sun Direction", size=3, default=[0,0,0])
|
||||
bpy.types.World.arm_envtex_turbidity = FloatProperty(name="Turbidity", default=1.0)
|
||||
bpy.types.World.arm_envtex_ground_albedo = FloatProperty(name="Ground Albedo", default=0.0)
|
||||
bpy.types.World.arm_nishita_density = FloatVectorProperty(name="Nishita Density", size=3, default=[1, 1, 1])
|
||||
bpy.types.Material.arm_cast_shadow = BoolProperty(name="Cast Shadow", default=True)
|
||||
bpy.types.Material.arm_receive_shadow = BoolProperty(name="Receive Shadow", description="Requires forward render path", default=True)
|
||||
bpy.types.Material.arm_overlay = BoolProperty(name="Overlay", default=False)
|
||||
|
@ -435,6 +436,10 @@ def init_properties():
|
|||
bpy.types.World.compo_defs = StringProperty(name="Compositor Shader Defs", default='')
|
||||
|
||||
bpy.types.World.arm_use_clouds = BoolProperty(name="Clouds", default=False, update=assets.invalidate_shader_cache)
|
||||
bpy.types.World.arm_darken_clouds = BoolProperty(
|
||||
name="Darken Clouds at Night",
|
||||
description="Darkens the clouds when the sun is low. This setting is for artistic purposes and is not physically correct",
|
||||
default=False, update=assets.invalidate_shader_cache)
|
||||
bpy.types.World.arm_clouds_lower = FloatProperty(name="Lower", default=1.0, min=0.1, max=10.0, update=assets.invalidate_shader_cache)
|
||||
bpy.types.World.arm_clouds_upper = FloatProperty(name="Upper", default=1.0, min=0.1, max=10.0, update=assets.invalidate_shader_cache)
|
||||
bpy.types.World.arm_clouds_wind = FloatVectorProperty(name="Wind", default=[1.0, 0.0], size=2, update=assets.invalidate_shader_cache)
|
||||
|
|
|
@ -254,6 +254,7 @@ class ARM_PT_WorldPropsPanel(bpy.types.Panel):
|
|||
layout.prop(world, 'arm_use_clouds')
|
||||
col = layout.column(align=True)
|
||||
col.enabled = world.arm_use_clouds
|
||||
col.prop(world, 'arm_darken_clouds')
|
||||
col.prop(world, 'arm_clouds_lower')
|
||||
col.prop(world, 'arm_clouds_upper')
|
||||
col.prop(world, 'arm_clouds_precipitation')
|
||||
|
|
|
@ -1,23 +1,87 @@
|
|||
import bpy
|
||||
from contextlib import contextmanager
|
||||
import math
|
||||
import multiprocessing
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import json
|
||||
import re
|
||||
import arm.utils
|
||||
import subprocess
|
||||
|
||||
import bpy
|
||||
|
||||
import arm.assets as assets
|
||||
import arm.log as log
|
||||
import arm.utils
|
||||
|
||||
|
||||
def add_irr_assets(output_file_irr):
|
||||
assets.add(output_file_irr + '.arm')
|
||||
|
||||
|
||||
def add_rad_assets(output_file_rad, rad_format, num_mips):
|
||||
assets.add(output_file_rad + '.' + rad_format)
|
||||
for i in range(0, num_mips):
|
||||
assets.add(output_file_rad + '_' + str(i) + '.' + rad_format)
|
||||
|
||||
# Generate probes from environment map
|
||||
def write_probes(image_filepath, disable_hdr, cached_num_mips, arm_radiance=True):
|
||||
|
||||
@contextmanager
|
||||
def setup_envmap_render():
|
||||
"""Creates a background scene for rendering environment textures.
|
||||
Use it as a context manager to automatically clean up on errors.
|
||||
"""
|
||||
rpdat = arm.utils.get_rp()
|
||||
radiance_size = int(rpdat.arm_radiance_size)
|
||||
|
||||
# Render worlds in a different scene so that there are no other
|
||||
# objects. The actual scene might be called differently if the name
|
||||
# is already taken
|
||||
scene = bpy.data.scenes.new("_arm_envmap_render")
|
||||
scene.render.engine = "CYCLES"
|
||||
scene.render.image_settings.file_format = "JPEG"
|
||||
scene.render.image_settings.quality = 100
|
||||
scene.render.resolution_x = radiance_size
|
||||
scene.render.resolution_y = radiance_size // 2
|
||||
|
||||
# Set GPU as rendering device if the user enabled it
|
||||
if bpy.context.preferences.addons["cycles"].preferences.compute_device_type == "CUDA":
|
||||
scene.cycles.device = "GPU"
|
||||
else:
|
||||
log.info('Armory: Using CPU for environment render (might be slow). Enable CUDA if possible.')
|
||||
|
||||
# One sample is enough for world background only
|
||||
scene.cycles.samples = 1
|
||||
|
||||
# Setup scene
|
||||
cam = bpy.data.cameras.new("_arm_cam_envmap_render")
|
||||
cam_obj = bpy.data.objects.new("_arm_cam_envmap_render", cam)
|
||||
scene.collection.objects.link(cam_obj)
|
||||
scene.camera = cam_obj
|
||||
|
||||
cam_obj.location = [0.0, 0.0, 0.0]
|
||||
cam.type = "PANO"
|
||||
cam.cycles.panorama_type = "EQUIRECTANGULAR"
|
||||
cam_obj.rotation_euler = [math.radians(90), 0, math.radians(-90)]
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
bpy.data.objects.remove(cam_obj)
|
||||
bpy.data.cameras.remove(cam)
|
||||
bpy.data.scenes.remove(scene)
|
||||
|
||||
|
||||
def render_envmap(target_dir: str, world: bpy.types.World):
|
||||
"""Renders an environment texture for the given world into the
|
||||
target_dir. Use in combination with setup_envmap_render()."""
|
||||
scene = bpy.data.scenes["_arm_envmap_render"]
|
||||
scene.world = world
|
||||
|
||||
render_path = os.path.join(target_dir, f"env_{arm.utils.safesrc(world.name)}.jpg")
|
||||
scene.render.filepath = render_path
|
||||
|
||||
bpy.ops.render.render(write_still=True, scene=scene.name)
|
||||
|
||||
|
||||
def write_probes(image_filepath: str, disable_hdr: bool, cached_num_mips: int, arm_radiance=True) -> int:
|
||||
"""Generate probes from environment map and returns the mipmap count"""
|
||||
envpath = arm.utils.get_fp_build() + '/compiled/Assets/envmaps'
|
||||
|
||||
if not os.path.exists(envpath):
|
||||
|
@ -63,7 +127,7 @@ def write_probes(image_filepath, disable_hdr, cached_num_mips, arm_radiance=True
|
|||
scaled_file = output_file_rad + '.' + rad_format
|
||||
|
||||
if arm.utils.get_os() == 'win':
|
||||
output = subprocess.check_output([ \
|
||||
subprocess.check_output([
|
||||
kraffiti_path,
|
||||
'from=' + input_file,
|
||||
'to=' + scaled_file,
|
||||
|
@ -71,35 +135,35 @@ def write_probes(image_filepath, disable_hdr, cached_num_mips, arm_radiance=True
|
|||
'width=' + str(target_w),
|
||||
'height=' + str(target_h)])
|
||||
else:
|
||||
output = subprocess.check_output([ \
|
||||
kraffiti_path + \
|
||||
' from="' + input_file + '"' + \
|
||||
' to="' + scaled_file + '"' + \
|
||||
' format=' + rad_format + \
|
||||
' width=' + str(target_w) + \
|
||||
' height=' + str(target_h)], shell=True)
|
||||
subprocess.check_output([
|
||||
kraffiti_path
|
||||
+ ' from="' + input_file + '"'
|
||||
+ ' to="' + scaled_file + '"'
|
||||
+ ' format=' + rad_format
|
||||
+ ' width=' + str(target_w)
|
||||
+ ' height=' + str(target_h)], shell=True)
|
||||
|
||||
# Irradiance spherical harmonics
|
||||
if arm.utils.get_os() == 'win':
|
||||
subprocess.call([ \
|
||||
subprocess.call([
|
||||
cmft_path,
|
||||
'--input', scaled_file,
|
||||
'--filter', 'shcoeffs',
|
||||
'--outputNum', '1',
|
||||
'--output0', output_file_irr])
|
||||
else:
|
||||
subprocess.call([ \
|
||||
cmft_path + \
|
||||
' --input ' + '"' + scaled_file + '"' + \
|
||||
' --filter shcoeffs' + \
|
||||
' --outputNum 1' + \
|
||||
' --output0 ' + '"' + output_file_irr + '"'], shell=True)
|
||||
subprocess.call([
|
||||
cmft_path
|
||||
+ ' --input ' + '"' + scaled_file + '"'
|
||||
+ ' --filter shcoeffs'
|
||||
+ ' --outputNum 1'
|
||||
+ ' --output0 ' + '"' + output_file_irr + '"'], shell=True)
|
||||
|
||||
sh_to_json(output_file_irr)
|
||||
add_irr_assets(output_file_irr)
|
||||
|
||||
# Mip-mapped radiance
|
||||
if arm_radiance == False:
|
||||
if not arm_radiance:
|
||||
return cached_num_mips
|
||||
|
||||
# 4096 = 256 face
|
||||
|
@ -200,37 +264,37 @@ def write_probes(image_filepath, disable_hdr, cached_num_mips, arm_radiance=True
|
|||
if disable_hdr is True:
|
||||
for f in generated_files:
|
||||
if arm.utils.get_os() == 'win':
|
||||
subprocess.call([ \
|
||||
subprocess.call([
|
||||
kraffiti_path,
|
||||
'from=' + f + '.hdr',
|
||||
'to=' + f + '.jpg',
|
||||
'format=jpg'])
|
||||
else:
|
||||
subprocess.call([ \
|
||||
kraffiti_path + \
|
||||
' from="' + f + '.hdr"' + \
|
||||
' to="' + f + '.jpg"' + \
|
||||
' format=jpg'], shell=True)
|
||||
subprocess.call([
|
||||
kraffiti_path
|
||||
+ ' from="' + f + '.hdr"'
|
||||
+ ' to="' + f + '.jpg"'
|
||||
+ ' format=jpg'], shell=True)
|
||||
os.remove(f + '.hdr')
|
||||
|
||||
# Scale from (4x2 to 1x1>
|
||||
for i in range (0, 2):
|
||||
for i in range(0, 2):
|
||||
last = generated_files[-1]
|
||||
out = output_file_rad + '_' + str(mip_count + i)
|
||||
if arm.utils.get_os() == 'win':
|
||||
subprocess.call([ \
|
||||
subprocess.call([
|
||||
kraffiti_path,
|
||||
'from=' + last + '.' + rad_format,
|
||||
'to=' + out + '.' + rad_format,
|
||||
'scale=0.5',
|
||||
'format=' + rad_format], shell=True)
|
||||
else:
|
||||
subprocess.call([ \
|
||||
kraffiti_path + \
|
||||
' from=' + '"' + last + '.' + rad_format + '"' + \
|
||||
' to=' + '"' + out + '.' + rad_format + '"' + \
|
||||
' scale=0.5' + \
|
||||
' format=' + rad_format], shell=True)
|
||||
subprocess.call([
|
||||
kraffiti_path
|
||||
+ ' from=' + '"' + last + '.' + rad_format + '"'
|
||||
+ ' to=' + '"' + out + '.' + rad_format + '"'
|
||||
+ ' scale=0.5'
|
||||
+ ' format=' + rad_format], shell=True)
|
||||
generated_files.append(out)
|
||||
|
||||
mip_count += 2
|
||||
|
@ -239,6 +303,7 @@ def write_probes(image_filepath, disable_hdr, cached_num_mips, arm_radiance=True
|
|||
|
||||
return mip_count
|
||||
|
||||
|
||||
def sh_to_json(sh_file):
|
||||
"""Parse sh coefs produced by cmft into json array"""
|
||||
with open(sh_file + '.c') as f:
|
||||
|
@ -251,6 +316,8 @@ def sh_to_json(sh_file):
|
|||
parse_band_floats(irradiance_floats, band0_line)
|
||||
parse_band_floats(irradiance_floats, band1_line)
|
||||
parse_band_floats(irradiance_floats, band2_line)
|
||||
for i in range(0, len(irradiance_floats)):
|
||||
irradiance_floats[i] /= 2
|
||||
|
||||
sh_json = {'irradiance': irradiance_floats}
|
||||
ext = '.arm' if bpy.data.worlds['Arm'].arm_minimize else ''
|
||||
|
@ -259,15 +326,26 @@ def sh_to_json(sh_file):
|
|||
# Clean up .c
|
||||
os.remove(sh_file + '.c')
|
||||
|
||||
|
||||
def parse_band_floats(irradiance_floats, band_line):
|
||||
string_floats = re.findall(r'[-+]?\d*\.\d+|\d+', band_line)
|
||||
string_floats = string_floats[1:] # Remove 'Band 0/1/2' number
|
||||
string_floats = string_floats[1:] # Remove 'Band 0/1/2' number
|
||||
for s in string_floats:
|
||||
irradiance_floats.append(float(s))
|
||||
|
||||
|
||||
def write_sky_irradiance(base_name):
|
||||
# Hosek spherical harmonics
|
||||
irradiance_floats = [1.5519331988822218,2.3352207154503266,2.997277451988076,0.2673894962434794,0.4305630474135794,0.11331825259716752,-0.04453633521758638,-0.038753175134160295,-0.021302768541875794,0.00055858020486499,0.000371654770334503,0.000126606145406403,-0.000135708721978705,-0.000787399554583089,-0.001550090690860059,0.021947399048903773,0.05453650591711572,0.08783641266630278,0.17053593578630663,0.14734127083304463,0.07775404698816404,-2.6924363189795e-05,-7.9350169701934e-05,-7.559914435231e-05,0.27035455385870993,0.23122918445556914,0.12158817295211832]
|
||||
irradiance_floats = [
|
||||
1.5519331988822218, 2.3352207154503266, 2.997277451988076,
|
||||
0.2673894962434794, 0.4305630474135794, 0.11331825259716752,
|
||||
-0.04453633521758638, -0.038753175134160295, -0.021302768541875794,
|
||||
0.00055858020486499, 0.000371654770334503, 0.000126606145406403,
|
||||
-0.000135708721978705, -0.000787399554583089, -0.001550090690860059,
|
||||
0.021947399048903773, 0.05453650591711572, 0.08783641266630278,
|
||||
0.17053593578630663, 0.14734127083304463, 0.07775404698816404,
|
||||
-2.6924363189795e-05, -7.9350169701934e-05, -7.559914435231e-05,
|
||||
0.27035455385870993, 0.23122918445556914, 0.12158817295211832]
|
||||
for i in range(0, len(irradiance_floats)):
|
||||
irradiance_floats[i] /= 2
|
||||
|
||||
|
@ -282,6 +360,7 @@ def write_sky_irradiance(base_name):
|
|||
|
||||
assets.add(output_file + '.arm')
|
||||
|
||||
|
||||
def write_color_irradiance(base_name, col):
|
||||
"""Constant color irradiance"""
|
||||
# Adjust to Cycles
|
||||
|
|
Loading…
Reference in a new issue