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:
Lubos Lenco 2021-04-03 10:07:25 +02:00 committed by GitHub
commit c9182cc152
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 822 additions and 225 deletions

163
Shaders/std/sky.glsl Normal file
View 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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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